A lottery app polls draw status with a recursive setTimeout every second, and every Detox test after launch blocks; why, and what changes fix it?
answer
- setTimeout up to 1.5 s counts as busy
- setInterval is ignored
- recursion means always one pending
- switch to setInterval or longer delay
- or stop polling when unneeded
basics
~20 sDetox treats a pending setTimeout of 1.5 seconds or less as busy and ignores setInterval. A recursive one-second setTimeout always has the next timer queued, so the app never idles. Switch the loop to setInterval, lengthen it, or stop polling when not needed.
solid answer
~50 sDetox synchronizes with JavaScript timers, but selectively: it waits for `setTimeout` timers of up to **1.5 seconds** and ignores `setInterval`. On Android the check asks React Native's timer manager whether any non-repeating timer shorter than 1500 ms is pending. A polling loop written as `function poll() { fetchStatus().finally(() => setTimeout(poll, 1000)); }` always has the next one-second timeout queued, so the app is never idle and the first action after launch blocks until a timeout; the busy log lists the timer. Fixes, in order of preference: stop polling where it is not needed, for example only on the live-draw screen and only while the app is active; rewrite the loop with `setInterval`, which Detox ignores, as its docs suggest; or use a delay longer than 1.5 seconds. Disabling synchronization for the whole run hides the problem and costs all automatic waiting.
code
tsx · 14 linesimport { useEffect } from 'react';
import { refreshDrawStatus } from './drawApi';
export function useDrawStatusPolling(active: boolean) {
useEffect(() => {
if (!active) return;
// setInterval is ignored by Detox synchronization; a recursive
// setTimeout(poll, 1000) would keep the app permanently busy
const id = setInterval(() => {
void refreshDrawStatus();
}, 1000);
return () => clearInterval(id);
}, [active]);
}go deeper
Recall that Detox waits for short setTimeout timers and ignores setInterval, so a timer loop can keep an app busy.
Explain the 1.5 second rule and why a recursive short setTimeout always leaves a pending timer that blocks synchronization.
Diagnose the pending timer from the busy log, fix the app's polling (scope it, use setInterval or longer delays), and explain why URL exclusion alone fails.
Set app-wide guidance on background polling that serves both battery life and testability, so e2e synchronization is not fought case by case.
## How Detox treats JavaScript timers **Detox** includes timers in its idle check because a short `setTimeout` is often the next step of a UI change: a debounce, a delayed navigation, a toast hide. But waiting for every timer would make many apps untestable, so the rule is selective: | Timer | Tracked by default? | |---|---| | `setTimeout` with a delay of up to 1.5 seconds | yes, the app is busy until it fires | | `setTimeout` with a longer delay | no | | `setInterval`, any period | no | On Android, Detox's timer resource asks React Native's `JavaTimerManager` whether a timer is pending in a 1500 ms window, and that check skips repeating timers. The Detox docs state the same behaviour for the platform as a whole: `setInterval` is ignored and only `setTimeout` calls of up to 1.5 seconds are waited for. ## Why the polling loop blocks The lottery app checks whether tonight's draw has started: ```tsx useEffect(() => { let timer: ReturnType<typeof setTimeout>; const poll = async () => { await refreshDrawStatus(); timer = setTimeout(poll, 1000); }; poll(); return () => clearTimeout(timer); }, []); ``` Each time `poll` finishes, it schedules the next call one second later. From Detox's point of view: 1. there is always a pending `setTimeout` of 1000 ms, which is under the 1.5 second threshold; 2. when it fires, the fetch is in flight, which is also busy; 3. when the fetch returns, a new 1000 ms timeout is queued. The app is **never** idle, so the first action after launch waits until the test times out. The synchronization debug log, printed after `session.debugSynchronization` (10 seconds by default), shows the pending timer. ## Fixes, from best to worst 1. **Poll only when needed.** Start the loop on the live-draw screen, stop it when the screen loses focus or the app goes to the background. That also saves battery and data for real users. 2. **Use `setInterval`.** Detox's troubleshooting guide recommends exactly this for short polling loops: the same one-second cadence written with `setInterval` is ignored by synchronization. Make sure overlapping requests are handled, since an interval does not wait for the previous fetch. 3. **Lengthen the delay** beyond 1.5 seconds if the product allows it. 4. **Exclude the polling URL** with `device.setURLBlacklist()` or the `detoxURLBlacklistRegex` launch argument; this addresses the network half, but the short `setTimeout` itself still keeps the app busy, so it does not fix this loop on its own. 5. **Disable synchronization** only as a last resort, and only around the steps that need it. ## Why not change the tests instead? - Adding sleeps does nothing: the app still never idles, so the next Detox step still waits. - Disabling synchronization in `beforeAll` for every test turns the whole suite into a timing-based black-box suite. - Raising timeouts only makes the run hang longer before failing. The blockage is Detox pointing at a real property of the app: aggressive polling on React Native's single JavaScript thread. The docs put it bluntly: avoid aggressive polling if possible. ## Related timer cases - A **debounce** of 300 ms on a search field is tracked, which is what you want: Detox waits until the debounced search has run before the next step. - A **toast** hidden by `setTimeout(hide, 2000)` is not held by the timer rule because the delay exceeds 1.5 seconds (its show and hide animations still are), so a synchronized test can usually see it; one hidden after 1000 ms is tracked, and the toast is gone by the time Detox checks. - A **retry loop** with exponential backoff starts under the threshold and grows past it, so early retries block and later ones do not, which can make a failing endpoint look like intermittent slowness. ## Summary Know the rule, `setTimeout` up to 1.5 seconds tracked and `setInterval` ignored, recognise a recursive short `setTimeout` as a permanent busy state, and fix the app's polling rather than the tests.
- Why does Detox ignore setInterval but track short setTimeout calls?A short `setTimeout` is usually the next step of a UI change, such as a debounce or a delayed navigation, so waiting for it keeps tests in sync. A `setInterval` is usually a background heartbeat that never ends; tracking it would make the app permanently busy. The 1.5 second limit similarly skips long delays that are not part of an interaction.
- Would blacklisting the draw-status URL alone unblock the tests?No. The blacklist removes the fetch from network synchronization, but the recursive one-second `setTimeout` is still a pending short timer between fetches, so the app stays busy. The loop itself has to change.
saying these in an interview costs you the question
- Detox waits for every setInterval tick before each action.
- Detox ignores all JavaScript timers, so polling cannot block it.
- Blacklisting the polling URL is enough to unblock the loop.
- Adding sleep calls lets the polling timer finish first.
- A setTimeout of any length keeps Detox waiting until it fires.