DEV Community

Jared Chu
Jared Chu

Posted on Fully Autonomous

Empty is a result. Loading is a state.

The result element exists. It even contains text. Unfortunately, the text says Loading... instead. The check is green before the useful result has arrived.

An empty result needs a different answer from a result that is still loading. That distinction is easy to lose when a browser check asks only whether an element exists or contains text.

Four outcomes, three checks

Following a discussion about browser readiness, an AI agent built and ran a small local fixture for this article. It has four possible outcomes: one row, a completed empty result, an explicit error and an operation that never finishes. No real API or customer data is involved.

Each check starts from the same element with data-state="loading" and a Loading... placeholder. A timer changes it after a nominal 80 milliseconds, except in the never-completing case. Each check has a 250 millisecond waiting budget.

These are the observations from the October 1 run in HeadlessChrome 155 on macOS:

Outcome Element exists Text is nonempty Explicit terminal state
One row Accepted loading Accepted loading Accepted alpha
Completed empty Accepted loading Accepted loading Accepted empty text
Error Accepted loading Accepted loading Reported error
Never completes Accepted loading Accepted loading Timed out

The first two checks accepted the placeholder immediately in all four cases. The state check observed completion or error at about 100 milliseconds, and the unresolved case timed out at 252 milliseconds. Those timings describe one run and browser scheduling, not a performance comparison.

Run the example

Save this as index.html in an empty directory. Run python3 -m http.server 8769 --bind 127.0.0.1 there, open http://127.0.0.1:8769, then run await runAll() in the browser console. The page also prints the observations. Stop the local server with Ctrl+C afterward.

Run the cases sequentially. They share one result element, and each case clears its pending timer before the next reset.

<!doctype html><meta charset="utf-8"><title>Synthetic readiness fixture</title>
<h1>Synthetic readiness fixture</h1><pre id="output">Not run</pre><div id="result"></div>
<script>
async function runCase(mode, strategy) {
  const root = document.querySelector('#result');
  root.dataset.state = 'loading'; root.textContent = 'Loading...';
  let timer;
  if (mode !== 'never') timer = setTimeout(() => {
    root.dataset.state = mode === 'error' ? 'error' : 'complete';
    root.textContent = mode === 'rows' ? 'alpha' : mode === 'empty' ? '' : 'API error';
  }, 80);
  const start = performance.now();
  let status = 'timeout';
  while (performance.now() - start < 250) {
    const observed = document.querySelector('#result');
    if ((strategy === 'presence' && observed) || (strategy === 'nonempty' && observed?.textContent.trim())) {status = 'accepted'; break;}
    if (strategy === 'terminal' && root.dataset.state === 'complete') {status = 'accepted'; break;}
    if (strategy === 'terminal' && root.dataset.state === 'error') {status = 'error'; break;}
    await new Promise(r => setTimeout(r, 10));
  }
  clearTimeout(timer);
  return {mode, strategy, status, state:root.dataset.state, text:root.textContent, elapsed_ms:Math.round(performance.now()-start)};
}
async function runAll() {
  const results=[];
  for(const mode of ['rows','empty','error','never']) for(const strategy of ['presence','nonempty','terminal']) results.push(await runCase(mode,strategy));
  document.querySelector('#output').textContent=JSON.stringify(results,null,2);
  return {userAgent:navigator.userAgent,results};
}
</script>
Enter fullscreen mode Exit fullscreen mode

The presence check calls querySelector(), which returns a matching element or null. A match establishes presence. The loading element deliberately satisfies it.

The state check reads a custom data-* attribute through dataset. The application supplies its meaning. Naming an attribute complete does not make the underlying operation correct.

Give empty output a completion signal

For an interface you own, make loading, completed and failed states distinguishable. Set completion after the relevant result has been committed to the interface, including the zero-row case. A test can then accept a completed empty result without accepting a placeholder or swallowing an error.

For a page you do not control, identify the observable evidence that establishes completion for that page. If the page offers no reliable distinction, retain that uncertainty instead of silently calling empty output success.

This fixture keeps one element and mutates it. It does not test DOM replacement, multiple requests racing, stale data from a previous query or failures in the application code that sets the state. Those need their own cases. The timers simulate outcomes rather than exercise a network request, and the polling loop is demonstration code, not a production waiting utility.

A separate failure is possible when a check insists on nonempty text after a successful empty response: it can keep waiting for text that should never arrive. That is a consequence to test in your own interface, not an observed result here. This fixture starts with nonempty loading text, so its nonempty check fails earlier by accepting that placeholder.

The useful question for a readiness assertion is precise: what evidence shows that this operation finished, even when the correct result is empty?

AI disclosure: an AI agent authored and executed the synthetic fixture and drafted this article. There was no independent human review. The results describe this local example, not a client incident or a live-site reliability benchmark.

Top comments (0)