Skip to content

vscode/test-infra: prefer render-time attributes over post-render effect DOM-mutation for anything tests or accessibility tools read synchronously #1028

Description

@amrmelsayed

Summary

A recurring failure class surfaced while landing PR #1027 (@cluesmith/codev-artifact-canvas, spec 945): post-render DOM-mutation effects race against synchronous DOM reads. When a component renders raw HTML (e.g. via dangerouslySetInnerHTML) and then decorates that DOM in a useEffect (setting tabindex, classes, focus, ARIA, etc.), anything that reads those decorations synchronously right after the element appears can observe the pre-effect state. Tests are the most visible victim, but the same theoretical race exists against assistive tech and focus management on a fast paint.

This tracker captures the principle; individual cases are evaluated as they come up (see Out of scope).

Originating evidence (two CI-only failures on PR #1027)

Both passed locally every time and failed only in CI (slower effect-flush scheduling exposed the window):

  1. Stale-activeLine / overlay race (Phase 4 e2e). A hover set activeLine; an unconditional reset effect keyed on content could fire after the hover and clobber it, so the + overlay never mounted and the test's findByRole timed out. Fixed by validating the anchor against the reloaded DOM instead of blindly resetting.
  2. tabindex race (Phase 3 keyboard test). A <p>'s default .tabIndex is -1; the decoration effect set it to 0 after render, and the test read p.tabIndex synchronously the instant the element appeared (expected -1 to be +0). Fixed by stamping tabindex="0" at render time in the markdown renderer (in the HTML, not via an effect).

Evidence chain that pinned #2 as a timing race (not a data bug): markdown-it is deterministic and version-pinned (yields data-line="0"), the renderer's own test passed in the same failing CI run, and the workflow flipped pass to fail across docs-only commits with no code change.

The pattern

  • Trigger: render output the component does not own as React elements (sanitized HTML), decorated by a follow-up effect.
  • Race: a synchronous reader (a test, a screen reader, programmatic focus) observes the element after mount but before the effect runs.
  • Why CI: identical code, only the scheduler timing differs; a slower runner widens the window.

General fix shape

  • Value known at render time (focusability, static ARIA, stable attributes): stamp it into the rendered HTML so it is present the instant the node mounts. No effect, no window. (This is what the tabindex fix did.)
  • Value not known until later (e.g. marker classes that depend on asynchronously-loaded data): keep the effect, but readers must wait for the decorated state (tests use waitFor; runtime consumers should not assume synchronous availability).

Not a Vitest/jsdom-specific gap

This is not a test-harness quirk. Production code carries the same theoretical race: assistive technology or focus logic that reads the DOM right after a fast paint can see the pre-effect state. The test failures are the canary, not the whole problem.

Suggested approach

BUGFIX with a small sweep: the plan is a short "what is the audit shape" (how to identify post-render decoration that something reads synchronously), then a survey pass through existing post-render decoration patterns (vscode webviews, artifact-canvas, future packages) to flag candidates. Convert the clear render-time-knowable ones; document the rest.

Out of scope

Rewriting every effect. This tracker captures the principle and the audit; individual cases are evaluated on their merits as they are found.

Origin

Surfaced during PR #1027 (spec 945). The two fixes above already landed on that branch; this tracker generalizes the lesson so future packages do not rediscover it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/cross-cuttingTouches multiple areas — needs coordinated handling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions