Skip to content

test: the test runtime keeps every StyleX rule its dev runtime keys by an at-rule prelude - #8695

Merged
cixzhang merged 5 commits into
meta:mainfrom
vjeux:test/stylex-multi-condition-inject
Nov 4, 2026
Merged

cixzhang merged 5 commits into
meta:mainfrom
vjeux:test/stylex-multi-condition-inject

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

This must merge before the second of #8226 and #8682.

Why

A test that reads a component's styles can find nothing where the build has a rule.

  • Under a media query with more than one condition, such as @media (prefers-reduced-motion: reduce) and (pointer: coarse), the test runtime keeps only the first rule written, and a test reading a later one gets undefined. The same holds under such a query nested in a one-condition one, under a named container query, and under @starting-style.
  • Main already writes rules under several of these: Dialog, Switch, TopNavMegaMenu, the layer animations, the reduced-motion queries, and several @starting-style rules.
  • feat(core): under a finger a selection mark changes in one frame #8226 and feat(press): a mouse's or a pen's press paints at once through the press controller #8682 together add enough that Indicator/selectionMotion.test.ts fails 2 of its 6 cases ("expected undefined to be '0s'").
  • The build keeps every rule, so apps are not affected.

What led to it

Making touch components behave like native controls puts more styles behind combined queries, such as reduced motion on a touch screen.

The tests run StyleX 0.19.0's development runtime, which keys each rule it injects and inserts one rule per key.

  • A one-condition @media, @supports or @container query is keyed by its prelude plus the rule inside it.
  • Any other at-rule is keyed by its prelude alone, so it gets one rule.

The upstream fix, meta/stylex#1991 (merged, not yet released), keys every conditional prelude by the rule inside it. @starting-style rules still share one key there.

Options and tradeoffs

Why this choice

The test runtime then holds every rule StyleX's dev runtime dropped under a prelude-only key, as the build does, which is what these tests assume. It does so without touching product code or StyleX, and with a clear point where it comes out.

  • The injection keys rules with a copy of StyleX's own getSeenRuleKey, which StyleX does not export. It restores each later rule whose key is an at-rule's prelude, once.
  • One exception: the group last-member rule, which the test setup keeps out of jsdom on purpose (ButtonGroup FR3). Its marker lives in jsdomRefusedRules.ts, shared by the setup and the injection.
  • A restored rule the test DOM refuses throws instead of being skipped.

How to check it

  1. Run pnpm exec vitest run --project ui packages/core/src/__tests__/stylexMultiConditionMedia.test.ts. On main it fails, because the later rules read nothing. Its three cases:
    • two styles, and one style with two declarations, under one two-condition query;
    • two styles under a two-condition query nested in a one-condition one, and two @starting-style styles;
    • the group last-member rule as a later rule under a two-condition query (left out, and the file still loads), and the same first rule compiled twice (in the page once).
  2. With feat(core): under a finger a selection mark changes in one frame #8226 and feat(press): a mouse's or a pen's press paints at once through the press controller #8682 applied, Indicator/selectionMotion.test.ts passes 6 of 6 instead of 4.
  3. The whole UI suite runs with no test changing result against main.
    • Its one deterministic failure, motion/roles.test.ts, fails on main too.
    • Four files that timed out loading a lazy module in the loaded full run pass alone on both sides: DropdownMenuLazySheet, Item.swipeLayer, Item and Resizable/utils, 105 of 105, twice.

Evidence

  • Each part of the injection is load-bearing:
    • without the group-rule skip, the test file fails to load with "jsdom does not receive the group last-member rule";
    • without recording the first rule, the twice-compiled rule appears twice;
    • restoring only top-level multi-condition queries leaves the nested query's second rule missing.
  • ButtonGroup, InputGroup and Button's jsdom tests pass, 159 of 159 with this file.
  • The upstream fix this stands in for: [stylex] Keep every rule under a compound or named condition in runtime injection stylex#1991

Local gates at e3719ab:

  • the file above;
  • ButtonGroup, InputGroup and Button;
  • the whole UI suite;
  • the core typecheck;
  • pnpm lint (0 errors) and pnpm check:repo.

@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 3, 2026 5:46pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 3, 2026, 5:58 PM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks ready to merge: the new runtime injection restores every rule StyleX 0.19.0's dev runtime drops under a conditional prelude, and the new guard test fails without it and passes with it. Two optional follow-ups:

  • stylexInject.ts records which preludes are taken but not the rule that took each one, so the same declaration compiled twice would leave that first rule in both sheets; adding inserted.add(text); beside preludes.add(prelude); closes it.
  • Later @starting-style rules are dropped by the same keying and are not restored, so the description's "holds every rule StyleX generated" is true for conditional preludes only; extend the wrapper or narrow that sentence.

[Automated review]

Comment thread internal/test-utils/src/stylexInject.ts Outdated
const prelude = text.slice(0, text.indexOf('{')).trim().replace(COMMA, '$1');
if (!preludes.has(prelude)) {
// The first rule under this prelude: StyleX inserted it.
preludes.add(prelude);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: this marks the prelude as taken without recording the rule that took it. If the same declaration is compiled twice, StyleX's correct dedupe of the second injection falls through to the branch below and the first rule is inserted again into the test sheet. inserted.add(text); here closes that.

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · b7c987e

Storybook · Sandbox · built from this PR's head

Package size: @astryxdesign/core 6.22 MB packed (−3 B); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: core 2 rows; run pnpm api:report.

Not run on this PR: add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.


Generated by PR Enrichment workflow | View full report

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks ready to merge: the new runtime injection restores the conditional rules StyleX 0.19.0's dev runtime skips, and the new guard test fails without it and passes with it. Two optional hardening points, since no module reaches either path today:

  • Adding inserted.add(text); beside preludes.add(prelude); in stylexInject.ts stops a first rule compiled in two modules from being copied into the test sheet.
  • Keying rules with StyleX's own getSeenRuleKey logic would also restore a two-condition query nested in a one-condition one, and the 16 later @starting-style rules that the description's "every rule StyleX generated" covers.

[Automated review]

Comment thread internal/test-utils/src/stylexInject.ts Outdated

export default function stylexTestInject(args: InjectArgs): string {
const text = inject(args);
if (text === '' || !CONDITIONAL.test(text) || ONE_CONDITION.test(text)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: a two-condition query nested in a one-condition one, such as @media (min-width: 100px){@media (hover: hover) and (pointer: fine){…}}, matches ONE_CONDITION on its outer prelude and returns here, but StyleX keys it by both preludes without the selector, so a second rule under that pair is skipped and never restored. No current module nests conditions this way; keying with StyleX's own getSeenRuleKey logic would cover it, along with the later @starting-style rules.

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:39
vjeux added 2 commits November 1, 2026 18:11
…n prelude

StyleX 0.19.0's dev runtime keys a conditional rule whose prelude has more than one parenthesized condition by the prelude alone, so jsdom got only the first rule written under, say, @media (prefers-reduced-motion: reduce) and (pointer: coarse), and a test that read a later one read nothing. vitest.config.ts now names internal/test-utils/src/stylexInject.ts as the babel plugin's runtimeInjection: StyleX's own inject, plus each rule it drops, inserted once into a sheet of its own. Test-only: no StyleX code is patched, and the build keeps every rule as before. It comes out at the StyleX bump to a release that carries meta/stylex#1991, which fixes the key upstream.

Red first: the new stylexMultiConditionMedia test (two styles, and one style with two declarations, under one two-condition query) failed with StyleX's own injection; on meta#8226 + meta#8682 + main, Indicator/selectionMotion.test.ts failed 2 of 6. Both pass with it, and the full ui suite on main has the same results (15,951 passing, plus the new test).
The setup caught and dropped any error from inserting a rule StyleX's dev runtime had dropped, so a rule jsdom refused would have gone missing without a trace. It never fired: the full ui suite passes with the catch removed (15,952 tests). A refused rule now throws with the rule's text.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: the title and description promise every StyleX rule, but later @starting-style rules (16 load with the ui entry points today) and a multi-condition query nested in a one-condition one are still dropped, so a test that reads them still gets nothing. Narrow the title, the description's "holds every rule the build holds" and the two file headers to top-level @media, @supports and @container preludes, or key rules with StyleX's own getSeenRuleKey so every dropped rule comes back.

  • stylexInject.ts:72 now throws on the group last-member rule that setup.ts deliberately refuses, so such a rule under a restored prelude would fail its component's import; skip that marker before inserting.
  • inserted.add(text); beside preludes.add(prelude); stops a first rule compiled in two modules from being copied into the test sheet.

[Automated review]

Comment thread internal/test-utils/src/stylexInject.ts Outdated
const target = ownSheet();
if (target != null && !inserted.has(text)) {
inserted.add(text);
target.insertRule(text, target.cssRules.length);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal/test-utils/src/setup.ts makes insertRule throw for any rule containing :nth-last-child(1 of so StyleX's injector skips it. Without the catch, such a rule under a restored prelude now throws out of the component module's top-level inject call and fails every test that imports it (a fixture under @media (hover: hover) and (pointer: fine) fails to import with jsdom does not receive the group last-member rule). Skipping texts that contain that marker before this line keeps the throw for other refusals.

…eys by a prelude alone

Review: StyleX 0.19.0's dev runtime keys a rule by an at-rule's prelude
alone not only under a top-level multi-condition query, but also under one
nested in a one-condition query (`@supports (…) { @media (…) and (…) {…} }`),
under a named container query, and under `@starting-style`; the injection
restored only the first case, so a test that read a later rule under the
others still read nothing.

The injection now keys rules with a copy of StyleX's own getSeenRuleKey,
which StyleX does not export, and restores every later rule whose key is an
at-rule's prelude rather than a selector, once each.

It also skips the group last-member rule, which the test setup refuses on
purpose (its marker now lives in jsdomRefusedRules.ts, shared with
setup.ts): a refusal of that rule threw out of the component's import. Other
refusals still throw. And it records the first rule under a key as present,
so the same rule compiled in two modules is not copied into the test sheet.
@vjeux vjeux changed the title test: the test runtime keeps every StyleX rule under a multi-condition prelude test: the test runtime keeps every StyleX rule its dev runtime keys by an at-rule prelude Nov 2, 2026
@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

All three fixed in e3719ab:

  • The injection now keys rules with a copy of StyleX's own getSeenRuleKey. It restores every later rule whose key is an at-rule's prelude: a multi-condition query nested in a one-condition one, named containers and @starting-style, as well as top-level queries. The title, description and headers say so. The file's new case reads two nested-query rules and two @starting-style rules.
  • The group last-member rule is skipped before inserting. Its marker moved to jsdomRefusedRules.ts, which setup.ts shares. A later group rule under a two-condition query now loads; without the skip, the test file fails with "jsdom does not receive the group last-member rule".
  • The first rule under a key is recorded as present, so the same rule compiled twice appears once; the new case checks it.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested, for wording only: the injection now restores every rule StyleX's dev runtime drops by key, and the three earlier points are fixed. Two sentences under "Why this choice" still promise more than the head does: @property --astryx-press-alpha and the five group last-member rules never reach jsdom, and when jsdom refuses a rule StyleX inserts itself, StyleX skips it without a throw. Reword them, for example to "holds every rule StyleX's dev runtime dropped under a prelude-only key, as the build does" and "a restored rule the test DOM refuses throws instead of being skipped".

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: the injection now restores every rule StyleX's dev runtime drops by key, and the earlier wording points are fixed, but its record of restored rules does not survive vi.resetModules().

  • internal/test-utils/src/stylexInject.ts:71-73 keeps keys, present and sheet in module state that vi.resetModules() resets while StyleX's own injector keeps its keys, so restored rules are inserted again after each reset (DropdownMenu.ssr.test.tsx ends with each of its 6 restored rules four times) and a later rule imported first after a reset is never restored; holding the three on globalThis (for example under Symbol.for('astryx.stylexTestInject')) and adding a guard case that resets between two fixtures under one prelude fixes both.

[Automated review]

Comment thread internal/test-utils/src/stylexInject.ts Outdated
Comment on lines +71 to +73
const keys = new Set<string>();
const present = new Set<string>();
let sheet: CSSStyleSheet | null = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This record lives in module state. Vitest inlines this file, so vi.resetModules() starts a fresh copy, while StyleX's inject (from node_modules) keeps its seen keys and sheet. After a reset, every rule restored earlier is inserted again into a new style[data-stylex-test-inject] element (six existing ui files hold duplicates; DropdownMenu.ssr.test.tsx has each of its 6 restored rules four times). And a later rule that comes first after the reset is taken for the rule StyleX kept, so it is never restored.

Keeping the three values on a global fixes both:

const STATE = Symbol.for('astryx.stylexTestInject');
type State = {keys: Set<string>; present: Set<string>; sheet: CSSStyleSheet | null};
const state: State = ((globalThis as {[STATE]?: State})[STATE] ??= {
  keys: new Set(),
  present: new Set(),
  sheet: null,
});

A guard case that imports one fixture, calls vi.resetModules(), imports a second under the same prelude and expects the later rule exactly once would cover it.

Comment thread vitest.config.ts
Comment on lines +40 to +41
// StyleX's runtime injection, plus each rule its dev runtime drops
// under a prelude with more than one condition (StyleX 0.19.0).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: the module also restores rules under named containers, @starting-style and @supports (x: f()), not only multi-condition preludes (28 of the 46 it restores today). Maybe: StyleX's runtime injection, plus each rule its 0.19.0 dev runtime drops because it keys the rule by an at-rule's prelude alone.

…es()

CodeBunny on meta#8695: the record of restored rules lived in module state,
which vi.resetModules() resets while StyleX's injector keeps its keys, so
restored rules were inserted again and a later rule imported first after a
reset was lost. The record lives on globalThis; a guard case resets between
fixtures under one prelude.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks ready to merge: the injection's record now lives on globalThis, so a restored rule stays single after vi.resetModules() and a later rule imported first after a reset is restored, and the new reset case fails without the change.

Optional, non-blocking: the comment at vitest.config.ts:40-41 still describes only multi-condition preludes (the module also restores named-container, @starting-style and nested-query rules), packages/core/src/__tests__/stylexDeclarations.ts:11 still says runtimeInjection: true, and the description's "Its three cases" now has a fourth.

[Automated review]

@cixzhang
cixzhang marked this pull request as ready for review November 3, 2026 18:06
@cixzhang
cixzhang marked this pull request as draft November 4, 2026 01:26
@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 4, 2026
@cixzhang
cixzhang marked this pull request as ready for review November 4, 2026 02:28
@cixzhang
cixzhang marked this pull request as draft November 4, 2026 03:27
@cixzhang
cixzhang marked this pull request as ready for review November 4, 2026 03:27
@cixzhang
cixzhang merged commit 1b2bfe2 into meta:main Nov 4, 2026
198 of 209 checks passed

This branch was successfully deployed

1 active deployment
Preview — b7c987e4 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants