@variance-authority/core 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +458 -0
  2. package/README.md +13 -10
  3. package/dist/attribute/component-hash.d.ts +3 -2
  4. package/dist/attribute/component-hash.js +3 -2
  5. package/dist/attribute/component-hash.js.map +1 -1
  6. package/dist/attribute/composition.d.ts +16 -0
  7. package/dist/attribute/composition.js +4 -6
  8. package/dist/attribute/composition.js.map +1 -1
  9. package/dist/attribute/index.d.ts +3 -1
  10. package/dist/attribute/index.js +2 -1
  11. package/dist/attribute/index.js.map +1 -1
  12. package/dist/attribute/region.js +1 -5
  13. package/dist/attribute/region.js.map +1 -1
  14. package/dist/attribute/source.d.ts +10 -24
  15. package/dist/attribute/source.js +4 -38
  16. package/dist/attribute/source.js.map +1 -1
  17. package/dist/attribute/subject-pieces.d.ts +37 -0
  18. package/dist/attribute/subject-pieces.js +122 -0
  19. package/dist/attribute/subject-pieces.js.map +1 -0
  20. package/dist/format/capture.d.ts +25 -0
  21. package/dist/format/capture.js +6 -0
  22. package/dist/format/capture.js.map +1 -1
  23. package/dist/format/document.d.ts +2 -2
  24. package/dist/format/document.js +8 -2
  25. package/dist/format/document.js.map +1 -1
  26. package/dist/format/drive.d.ts +191 -0
  27. package/dist/format/drive.js +343 -0
  28. package/dist/format/drive.js.map +1 -0
  29. package/dist/format/environment.d.ts +20 -2
  30. package/dist/format/environment.js +7 -0
  31. package/dist/format/environment.js.map +1 -1
  32. package/dist/format/index.d.ts +6 -3
  33. package/dist/format/index.js +4 -2
  34. package/dist/format/index.js.map +1 -1
  35. package/dist/format/intervention.d.ts +79 -0
  36. package/dist/format/intervention.js.map +1 -1
  37. package/dist/format/origin.d.ts +32 -0
  38. package/dist/format/origin.js +54 -0
  39. package/dist/format/origin.js.map +1 -0
  40. package/dist/format/snapshot.d.ts +2 -0
  41. package/dist/format/snapshot.js.map +1 -1
  42. package/dist/format/stabilize.d.ts +28 -5
  43. package/dist/format/stabilize.js +48 -100
  44. package/dist/format/stabilize.js.map +1 -1
  45. package/dist/format/waits.d.ts +12 -0
  46. package/dist/format/waits.js +156 -0
  47. package/dist/format/waits.js.map +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +2 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/nearest.d.ts +46 -0
  52. package/dist/nearest.js +102 -0
  53. package/dist/nearest.js.map +1 -0
  54. package/dist/relate/before.d.ts +19 -24
  55. package/dist/relate/before.js +35 -37
  56. package/dist/relate/before.js.map +1 -1
  57. package/dist/relate/beyond.d.ts +59 -0
  58. package/dist/relate/beyond.js +98 -0
  59. package/dist/relate/beyond.js.map +1 -0
  60. package/dist/relate/carried.d.ts +122 -0
  61. package/dist/relate/carried.js +279 -0
  62. package/dist/relate/carried.js.map +1 -0
  63. package/dist/relate/dominate.d.ts +19 -0
  64. package/dist/relate/dominate.js +77 -0
  65. package/dist/relate/dominate.js.map +1 -0
  66. package/dist/relate/index.d.ts +3 -0
  67. package/dist/relate/index.js +3 -0
  68. package/dist/relate/index.js.map +1 -1
  69. package/dist/relate/merkle.js +2 -11
  70. package/dist/relate/merkle.js.map +1 -1
  71. package/dist/relate/narrow.d.ts +2 -2
  72. package/dist/relate/narrow.js +26 -73
  73. package/dist/relate/narrow.js.map +1 -1
  74. package/dist/relate/reach.d.ts +1 -1
  75. package/dist/relate/reach.js +10 -53
  76. package/dist/relate/reach.js.map +1 -1
  77. package/dist/relate/records.d.ts +1 -1
  78. package/dist/relate/records.js.map +1 -1
  79. package/dist/rules/index.d.ts +3 -3
  80. package/dist/rules/index.js +3 -3
  81. package/dist/rules/index.js.map +1 -1
  82. package/dist/rules/normalize/cascade.d.ts +12 -0
  83. package/dist/rules/normalize/cascade.js +34 -16
  84. package/dist/rules/normalize/cascade.js.map +1 -1
  85. package/dist/rules/normalize/fixture.d.ts +1 -0
  86. package/dist/rules/normalize/fixture.js +1 -0
  87. package/dist/rules/normalize/fixture.js.map +1 -1
  88. package/dist/rules/normalize/index.d.ts +2 -2
  89. package/dist/rules/normalize/index.js +9 -2
  90. package/dist/rules/normalize/index.js.map +1 -1
  91. package/dist/rules/normalize/value.d.ts +13 -2
  92. package/dist/rules/normalize/value.js +42 -8
  93. package/dist/rules/normalize/value.js.map +1 -1
  94. package/dist/rules/normalize/wrapper.js +5 -0
  95. package/dist/rules/normalize/wrapper.js.map +1 -1
  96. package/dist/rules/ruleset.d.ts +24 -4
  97. package/dist/rules/ruleset.js +24 -4
  98. package/dist/rules/ruleset.js.map +1 -1
  99. package/dist/segment/index.d.ts +20 -4
  100. package/dist/segment/index.js +28 -7
  101. package/dist/segment/index.js.map +1 -1
  102. package/dist/shard/index.d.ts +11 -0
  103. package/dist/shard/index.js +11 -0
  104. package/dist/shard/index.js.map +1 -0
  105. package/dist/shard/placement.d.ts +117 -0
  106. package/dist/shard/placement.js +170 -0
  107. package/dist/shard/placement.js.map +1 -0
  108. package/package.json +8 -1
@@ -0,0 +1,191 @@
1
+ import type { DriverTarget, Intervention } from './intervention.js';
2
+ /**
3
+ * The tricks a stylesheet cannot reach, and the one call each makes of a driver.
4
+ *
5
+ * Every other trick in [`stabilize.ts`](./stabilize.ts) is CSS, a screenshot
6
+ * option or a script the page runs on itself. Three causes of a page that reads
7
+ * two ways sit outside the document:
8
+ *
9
+ * - **the motion preference** — a page that honours `prefers-reduced-motion`
10
+ * runs its script animation only when the browser says motion is welcome, and
11
+ * a stylesheet cannot reach a `requestAnimationFrame` loop;
12
+ * - **the clock** — a page that prints `new Date()` prints a new value every
13
+ * run, and the date is read by the page's first script, before any stylesheet
14
+ * or settle step exists, so it is replaced by a script the browser runs before
15
+ * the page's own;
16
+ * - **the pointer** — a hover ends when the pointer leaves, and nothing in the
17
+ * page can move it.
18
+ *
19
+ * So each is declared as a {@link DriverState} and applied here, through the
20
+ * structural {@link DriverTarget} a Playwright `Page` already is. A host that
21
+ * owns the browser calls {@link driveBeforeLoad} before its first navigation
22
+ * and hands the page the ids it applied. A host that shares the page with
23
+ * somebody who moves the pointer, such as a test, also calls
24
+ * {@link driveBeforeRead} before each subject is read. A host that does not — a page agent, a test runner's own
25
+ * browser — drives nothing, and {@link forDriver} keeps those tricks out of its
26
+ * recorded recipe, so the absence shows in the reading's ids and digest the way
27
+ * a trick filtered by `needs` does.
28
+ */
29
+ /**
30
+ * The instant a fixed clock reads unless a project names another.
31
+ *
32
+ * Noon UTC, so it is the same calendar date in every zone from UTC−11 to
33
+ * UTC+11: a page that prints today's date prints one date whichever zone the
34
+ * browser runs in. A date in the past, so a page that compares against "now"
35
+ * to show something as expired does so on every run or on none.
36
+ */
37
+ export declare const DEFAULT_NOW = "2024-06-15T12:00:00.000Z";
38
+ /**
39
+ * The milliseconds an ISO 8601 instant names, or an error that says what an
40
+ * instant is.
41
+ *
42
+ * The zone is required. `2024-06-15T12:00` with no zone is read in whatever
43
+ * zone the machine runs in, so two machines would fix two clocks — the
44
+ * variance this trick exists to remove.
45
+ */
46
+ export declare function instantOf(now: string): number;
47
+ /**
48
+ * `prefers-reduced-motion: reduce`, the switch a well-built page already reads.
49
+ *
50
+ * It reaches what `pin-animations` cannot: a script animation, which a
51
+ * stylesheet has no handle on. It reaches only a page that honours the
52
+ * preference, and that is the cost: the page is read as a user who asked for
53
+ * less motion sees it, which can differ from what everybody else sees.
54
+ */
55
+ export declare const reducedMotion: Intervention;
56
+ /**
57
+ * `Date` reads one instant; timers, animation frames and `performance` keep
58
+ * running.
59
+ *
60
+ * An init script, {@link freezeDate}, run in every frame before the page's own
61
+ * first script. It replaces `Date` and nothing else, because the thing that
62
+ * prints two ways is the date a page shows, and the things that make a page
63
+ * settle are timers and frames. Playwright's `clock.setFixedTime` was measured
64
+ * against it and rejected: it installs a fake for `setTimeout`,
65
+ * `requestAnimationFrame` and `performance.now` as well, and a frame's
66
+ * timestamp no longer agrees with `document.timeline`, which drives every CSS
67
+ * and Web Animations frame. Installing no clock also leaves `page.clock` to the
68
+ * test that wants one.
69
+ *
70
+ * The id carries the instant when it is not {@link DEFAULT_NOW}, as
71
+ * `freeze-clock@2031-01-02T03:04:05.000Z`, so the instant is in the recipe's
72
+ * digest and so in the environment key: a baseline read at one instant and a
73
+ * run read at another are compared, and the instant is named as the cause of
74
+ * whatever changed.
75
+ *
76
+ * The cost: code that measures elapsed time with `Date.now()`, such as a
77
+ * debounce, sees no time pass and never fires. `performance.now()` keeps
78
+ * counting.
79
+ */
80
+ export declare function freezeClockAt(now: string): Intervention;
81
+ /** {@link freezeClockAt} the default instant, {@link DEFAULT_NOW}. */
82
+ export declare const freezeClock: Intervention;
83
+ /**
84
+ * The clock trick a `freeze-clock@<instant>` id names, or undefined for any
85
+ * other id.
86
+ *
87
+ * The registry holds `freeze-clock` alone; an id with an instant in it is
88
+ * resolved here, so a recipe that crossed into a page as ids keeps its instant.
89
+ */
90
+ export declare function clockById(id: string): Intervention | undefined;
91
+ /**
92
+ * The init script {@link freezeClock} installs: `Date` answers `frozen`, and
93
+ * nothing else changes.
94
+ *
95
+ * Runs in the page, so it closes over nothing. `new Date()`, `Date()` and
96
+ * `Date.now()` read the instant; `new Date(value)`, `Date.UTC`, `Date.parse`
97
+ * and every method on a date are the browser's own. `Intl.DateTimeFormat`'s
98
+ * `format()` and `formatToParts()` with no date read the browser's clock rather
99
+ * than `Date`, so they are given the instant too.
100
+ */
101
+ export declare function freezeDate(frozen: number): void;
102
+ /**
103
+ * The pointer moved off the page, so nothing is hovered.
104
+ *
105
+ * To `(-1, -1)` rather than to the page's corner: content at the top left is
106
+ * under `(0, 0)` and stays hovered there. `pointer-events: none` is not a
107
+ * substitute — measured, an element already hovered keeps `:hover` under it.
108
+ *
109
+ * Once per page, before load, where nothing but the host moves the pointer. A
110
+ * move is acknowledged on the next frame, ~14 ms measured, so a host that alone
111
+ * drives its page does not move it again. A host that shares the page with a
112
+ * test does, before every reading: see {@link driveBeforeRead}.
113
+ */
114
+ export declare const noHover: Intervention;
115
+ /**
116
+ * Nothing focused, so a field a test filled or a page focused on load reads
117
+ * as it does untouched.
118
+ *
119
+ * A settle step rather than a stylesheet: `:focus` and `:focus-visible` follow
120
+ * the element that holds focus, and no rule can unmatch them. It blurs the
121
+ * deepest focused element, walking through shadow roots, because a shadow host
122
+ * is the document's `activeElement` for whatever its tree focused, and blurring
123
+ * the host leaves the input inside it focused and outlined.
124
+ *
125
+ * The cost: a page evaluate per subject, and an animation frame as well when
126
+ * the document holds an `[autofocus]`. A host that reads a page somebody goes
127
+ * on using — a test, an agent's browser — gives focus back once the reading
128
+ * is taken; a host that owns its page does not need to.
129
+ */
130
+ export declare const noFocus: Intervention;
131
+ /**
132
+ * The tricks that put a page into the state nobody is touching, in every
133
+ * default collection recipe whose tier can see them.
134
+ *
135
+ * A hover, a focus, a script animation and a printed date are each a second
136
+ * reading of one subject, and each comes from something other than the code
137
+ * under review. Three of them reach the page only through a driver, so a host
138
+ * that cannot drive one leaves it out of the ids it records — see {@link forDriver}.
139
+ */
140
+ export declare const UNTOUCHED: readonly Intervention[];
141
+ /**
142
+ * The media features a recipe's driver tricks make the browser report, merged
143
+ * under the ones a project declared, or undefined when there are none.
144
+ *
145
+ * Collection resolves `@media` against declared features, not against the
146
+ * browser, so a preference the driver emulated and nobody declared would be
147
+ * read as unknown: both the `reduce` and the `no-preference` block applied, a
148
+ * box the browser never drew. Declaring what the trick emulates gives the
149
+ * preference one owner. A project's own value wins, and
150
+ * {@link driveBeforeLoad} emulates that value, so the two still agree.
151
+ */
152
+ export declare function featuresOf(recipe: readonly Intervention[], declared?: Readonly<Record<string, string>>): Readonly<Record<string, string>> | undefined;
153
+ /**
154
+ * Apply what a recipe asks of the browser before the page loads.
155
+ *
156
+ * All three survive navigation, so a host calls this once per page, before its
157
+ * first `goto` or `setContent`. Returns the ids applied, in recipe order. The
158
+ * clock's instant is the trick's own, so a host that wants another instant
159
+ * asks `recipeToDrive` for the recipe rather than telling this.
160
+ *
161
+ * `declared` is the media features the project declared. A declared
162
+ * `prefers-reduced-motion` is what the motion trick emulates, so the browser
163
+ * reports what the record says; see {@link featuresOf}.
164
+ */
165
+ export declare function driveBeforeLoad(recipe: readonly Intervention[], target: DriverTarget, declared?: Readonly<Record<string, string>>): Promise<readonly string[]>;
166
+ /**
167
+ * Take the pointer off the page again before a subject is read.
168
+ *
169
+ * For a host that shares the page: a test hovers a menu to open it and then
170
+ * observes, so the pointer is wherever the test's last step left it. A host
171
+ * that alone drives its page needs only {@link driveBeforeLoad}.
172
+ *
173
+ * Every reading pays the frame, hovered or not: measured in Chromium, a
174
+ * Playwright hover restyles the element while `:hover` matches nothing from
175
+ * script, so the page cannot say whether the move is needed. The pointer is
176
+ * not put back afterwards, and the move fires `mouseleave`: a menu a script
177
+ * opened on hover is closed for the test's next step.
178
+ */
179
+ export declare function driveBeforeRead(recipe: readonly Intervention[], target: Pick<DriverTarget, 'mouse'>): Promise<readonly string[]>;
180
+ /**
181
+ * The recipe as it holds on a page, given the driver tricks a host applied.
182
+ *
183
+ * A trick with no `driver` is kept. A trick with one is kept only when its id
184
+ * is in `driven`: a host that could not drive it leaves it out of the ids and
185
+ * the digest it records, rather than claiming a hold the page never had. The
186
+ * clock is the one trick whose id the driver may change, because the instant is
187
+ * the host's to choose: the recipe's clock is recorded as the one the host
188
+ * fixed.
189
+ */
190
+ export declare function forDriver(recipe: readonly Intervention[], driven: readonly string[]): readonly Intervention[];
191
+ //# sourceMappingURL=drive.d.ts.map
@@ -0,0 +1,343 @@
1
+ /**
2
+ * The tricks a stylesheet cannot reach, and the one call each makes of a driver.
3
+ *
4
+ * Every other trick in [`stabilize.ts`](./stabilize.ts) is CSS, a screenshot
5
+ * option or a script the page runs on itself. Three causes of a page that reads
6
+ * two ways sit outside the document:
7
+ *
8
+ * - **the motion preference** — a page that honours `prefers-reduced-motion`
9
+ * runs its script animation only when the browser says motion is welcome, and
10
+ * a stylesheet cannot reach a `requestAnimationFrame` loop;
11
+ * - **the clock** — a page that prints `new Date()` prints a new value every
12
+ * run, and the date is read by the page's first script, before any stylesheet
13
+ * or settle step exists, so it is replaced by a script the browser runs before
14
+ * the page's own;
15
+ * - **the pointer** — a hover ends when the pointer leaves, and nothing in the
16
+ * page can move it.
17
+ *
18
+ * So each is declared as a {@link DriverState} and applied here, through the
19
+ * structural {@link DriverTarget} a Playwright `Page` already is. A host that
20
+ * owns the browser calls {@link driveBeforeLoad} before its first navigation
21
+ * and hands the page the ids it applied. A host that shares the page with
22
+ * somebody who moves the pointer, such as a test, also calls
23
+ * {@link driveBeforeRead} before each subject is read. A host that does not — a page agent, a test runner's own
24
+ * browser — drives nothing, and {@link forDriver} keeps those tricks out of its
25
+ * recorded recipe, so the absence shows in the reading's ids and digest the way
26
+ * a trick filtered by `needs` does.
27
+ */
28
+ /**
29
+ * The instant a fixed clock reads unless a project names another.
30
+ *
31
+ * Noon UTC, so it is the same calendar date in every zone from UTC−11 to
32
+ * UTC+11: a page that prints today's date prints one date whichever zone the
33
+ * browser runs in. A date in the past, so a page that compares against "now"
34
+ * to show something as expired does so on every run or on none.
35
+ */
36
+ export const DEFAULT_NOW = '2024-06-15T12:00:00.000Z';
37
+ const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
38
+ /**
39
+ * The milliseconds an ISO 8601 instant names, or an error that says what an
40
+ * instant is.
41
+ *
42
+ * The zone is required. `2024-06-15T12:00` with no zone is read in whatever
43
+ * zone the machine runs in, so two machines would fix two clocks — the
44
+ * variance this trick exists to remove.
45
+ */
46
+ export function instantOf(now) {
47
+ const time = INSTANT.test(now) ? Date.parse(now) : Number.NaN;
48
+ if (Number.isNaN(time)) {
49
+ throw new Error('`now` must be an ISO 8601 instant with its zone, such as "2024-06-15T12:00:00Z" or ' +
50
+ `"2024-06-15T14:00:00+02:00", not ${JSON.stringify(now)}`);
51
+ }
52
+ return time;
53
+ }
54
+ /**
55
+ * `prefers-reduced-motion: reduce`, the switch a well-built page already reads.
56
+ *
57
+ * It reaches what `pin-animations` cannot: a script animation, which a
58
+ * stylesheet has no handle on. It reaches only a page that honours the
59
+ * preference, and that is the cost: the page is read as a user who asked for
60
+ * less motion sees it, which can differ from what everybody else sees.
61
+ */
62
+ export const reducedMotion = {
63
+ id: 'reduced-motion',
64
+ trick: 'reset',
65
+ needs: 'layout',
66
+ governs: 'motion-preference',
67
+ because: 'the browser reports prefers-reduced-motion: reduce, so a page that honours it stops its script animations',
68
+ driver: { reducedMotion: 'reduce' },
69
+ };
70
+ const CLOCK_ID = 'freeze-clock';
71
+ /**
72
+ * `Date` reads one instant; timers, animation frames and `performance` keep
73
+ * running.
74
+ *
75
+ * An init script, {@link freezeDate}, run in every frame before the page's own
76
+ * first script. It replaces `Date` and nothing else, because the thing that
77
+ * prints two ways is the date a page shows, and the things that make a page
78
+ * settle are timers and frames. Playwright's `clock.setFixedTime` was measured
79
+ * against it and rejected: it installs a fake for `setTimeout`,
80
+ * `requestAnimationFrame` and `performance.now` as well, and a frame's
81
+ * timestamp no longer agrees with `document.timeline`, which drives every CSS
82
+ * and Web Animations frame. Installing no clock also leaves `page.clock` to the
83
+ * test that wants one.
84
+ *
85
+ * The id carries the instant when it is not {@link DEFAULT_NOW}, as
86
+ * `freeze-clock@2031-01-02T03:04:05.000Z`, so the instant is in the recipe's
87
+ * digest and so in the environment key: a baseline read at one instant and a
88
+ * run read at another are compared, and the instant is named as the cause of
89
+ * whatever changed.
90
+ *
91
+ * The cost: code that measures elapsed time with `Date.now()`, such as a
92
+ * debounce, sees no time pass and never fires. `performance.now()` keeps
93
+ * counting.
94
+ */
95
+ export function freezeClockAt(now) {
96
+ const at = instantOf(now);
97
+ return {
98
+ id: at === instantOf(DEFAULT_NOW) ? CLOCK_ID : `${CLOCK_ID}@${new Date(at).toISOString()}`,
99
+ trick: 'hold',
100
+ needs: 'layout',
101
+ governs: 'clock',
102
+ because: 'Date reads one fixed instant, so elapsed time measured with Date.now() reads zero',
103
+ driver: { clock: { at } },
104
+ };
105
+ }
106
+ /** {@link freezeClockAt} the default instant, {@link DEFAULT_NOW}. */
107
+ export const freezeClock = freezeClockAt(DEFAULT_NOW);
108
+ /**
109
+ * The clock trick a `freeze-clock@<instant>` id names, or undefined for any
110
+ * other id.
111
+ *
112
+ * The registry holds `freeze-clock` alone; an id with an instant in it is
113
+ * resolved here, so a recipe that crossed into a page as ids keeps its instant.
114
+ */
115
+ export function clockById(id) {
116
+ return id.startsWith(`${CLOCK_ID}@`) ? freezeClockAt(id.slice(CLOCK_ID.length + 1)) : undefined;
117
+ }
118
+ /**
119
+ * The init script {@link freezeClock} installs: `Date` answers `frozen`, and
120
+ * nothing else changes.
121
+ *
122
+ * Runs in the page, so it closes over nothing. `new Date()`, `Date()` and
123
+ * `Date.now()` read the instant; `new Date(value)`, `Date.UTC`, `Date.parse`
124
+ * and every method on a date are the browser's own. `Intl.DateTimeFormat`'s
125
+ * `format()` and `formatToParts()` with no date read the browser's clock rather
126
+ * than `Date`, so they are given the instant too.
127
+ */
128
+ export function freezeDate(frozen) {
129
+ // TODO: `Temporal.Now` and a worker's `Date` read the real clock. This runs
130
+ // in each frame's window and replaces `Date` alone, so a page that prints a
131
+ // date from either prints a second reading.
132
+ const Real = Date;
133
+ function Frozen(...args) {
134
+ if (new.target === undefined)
135
+ return new Real(frozen).toString();
136
+ return Reflect.construct(Real, args.length === 0 ? [frozen] : args, new.target);
137
+ }
138
+ Object.setPrototypeOf(Frozen, Real);
139
+ Object.defineProperties(Frozen, {
140
+ prototype: { value: Real.prototype },
141
+ name: { value: 'Date' },
142
+ length: { value: 7 },
143
+ now: { value: () => frozen, writable: true, configurable: true },
144
+ });
145
+ Object.defineProperty(Real.prototype, 'constructor', { value: Frozen, writable: true, configurable: true });
146
+ Object.defineProperty(globalThis, 'Date', { value: Frozen, writable: true, configurable: true });
147
+ const format = Intl.DateTimeFormat.prototype;
148
+ const formatting = Object.getOwnPropertyDescriptor(format, 'format')?.get;
149
+ if (formatting !== undefined) {
150
+ Object.defineProperty(format, 'format', {
151
+ configurable: true,
152
+ get() {
153
+ const bound = formatting.call(this);
154
+ return (date) => bound(date === undefined ? frozen : date);
155
+ },
156
+ });
157
+ }
158
+ const toParts = format.formatToParts;
159
+ Object.defineProperty(format, 'formatToParts', {
160
+ configurable: true,
161
+ writable: true,
162
+ value(date) {
163
+ return toParts.call(this, date === undefined ? frozen : date);
164
+ },
165
+ });
166
+ }
167
+ /**
168
+ * The pointer moved off the page, so nothing is hovered.
169
+ *
170
+ * To `(-1, -1)` rather than to the page's corner: content at the top left is
171
+ * under `(0, 0)` and stays hovered there. `pointer-events: none` is not a
172
+ * substitute — measured, an element already hovered keeps `:hover` under it.
173
+ *
174
+ * Once per page, before load, where nothing but the host moves the pointer. A
175
+ * move is acknowledged on the next frame, ~14 ms measured, so a host that alone
176
+ * drives its page does not move it again. A host that shares the page with a
177
+ * test does, before every reading: see {@link driveBeforeRead}.
178
+ */
179
+ export const noHover = {
180
+ id: 'no-hover',
181
+ trick: 'reset',
182
+ needs: 'layout',
183
+ governs: 'pointer',
184
+ because: 'the pointer moved off the page, so a hover a test left behind is not read',
185
+ driver: { pointer: 'outside' },
186
+ };
187
+ /**
188
+ * Nothing focused, so a field a test filled or a page focused on load reads
189
+ * as it does untouched.
190
+ *
191
+ * A settle step rather than a stylesheet: `:focus` and `:focus-visible` follow
192
+ * the element that holds focus, and no rule can unmatch them. It blurs the
193
+ * deepest focused element, walking through shadow roots, because a shadow host
194
+ * is the document's `activeElement` for whatever its tree focused, and blurring
195
+ * the host leaves the input inside it focused and outlined.
196
+ *
197
+ * The cost: a page evaluate per subject, and an animation frame as well when
198
+ * the document holds an `[autofocus]`. A host that reads a page somebody goes
199
+ * on using — a test, an agent's browser — gives focus back once the reading
200
+ * is taken; a host that owns its page does not need to.
201
+ */
202
+ export const noFocus = {
203
+ id: 'no-focus',
204
+ trick: 'reset',
205
+ needs: 'layout',
206
+ governs: 'focus',
207
+ because: 'focus released, so a field a test or the page focused is read unfocused',
208
+ // Again in a frame callback when the document holds an `autofocus`, because
209
+ // the browser honours one at the page's next rendering update: a blur before
210
+ // it is undone, and a document written by `setContent` has had no update yet.
211
+ // Only then, so a page without one pays no frame per subject.
212
+ settle: async (target) => {
213
+ await target.evaluate(() => new Promise((resolve) => {
214
+ const view = globalThis;
215
+ const release = () => {
216
+ let focused = view.document.activeElement;
217
+ while (focused?.shadowRoot?.activeElement != null)
218
+ focused = focused.shadowRoot.activeElement;
219
+ focused?.blur?.();
220
+ };
221
+ release();
222
+ if (view.document.querySelector('[autofocus]') === null)
223
+ return resolve();
224
+ view.requestAnimationFrame(() => {
225
+ release();
226
+ resolve();
227
+ });
228
+ }));
229
+ },
230
+ };
231
+ /**
232
+ * The tricks that put a page into the state nobody is touching, in every
233
+ * default collection recipe whose tier can see them.
234
+ *
235
+ * A hover, a focus, a script animation and a printed date are each a second
236
+ * reading of one subject, and each comes from something other than the code
237
+ * under review. Three of them reach the page only through a driver, so a host
238
+ * that cannot drive one leaves it out of the ids it records — see {@link forDriver}.
239
+ */
240
+ export const UNTOUCHED = [noHover, noFocus, reducedMotion, freezeClock];
241
+ const MOTION = 'prefers-reduced-motion';
242
+ /**
243
+ * The media features a recipe's driver tricks make the browser report, merged
244
+ * under the ones a project declared, or undefined when there are none.
245
+ *
246
+ * Collection resolves `@media` against declared features, not against the
247
+ * browser, so a preference the driver emulated and nobody declared would be
248
+ * read as unknown: both the `reduce` and the `no-preference` block applied, a
249
+ * box the browser never drew. Declaring what the trick emulates gives the
250
+ * preference one owner. A project's own value wins, and
251
+ * {@link driveBeforeLoad} emulates that value, so the two still agree.
252
+ */
253
+ export function featuresOf(recipe, declared) {
254
+ const motion = recipe.find((intervention) => intervention.driver?.reducedMotion !== undefined);
255
+ const features = {
256
+ ...(motion === undefined ? {} : { [MOTION]: motion.driver.reducedMotion }),
257
+ ...declared,
258
+ };
259
+ return Object.keys(features).length === 0 ? undefined : features;
260
+ }
261
+ /**
262
+ * Apply what a recipe asks of the browser before the page loads.
263
+ *
264
+ * All three survive navigation, so a host calls this once per page, before its
265
+ * first `goto` or `setContent`. Returns the ids applied, in recipe order. The
266
+ * clock's instant is the trick's own, so a host that wants another instant
267
+ * asks `recipeToDrive` for the recipe rather than telling this.
268
+ *
269
+ * `declared` is the media features the project declared. A declared
270
+ * `prefers-reduced-motion` is what the motion trick emulates, so the browser
271
+ * reports what the record says; see {@link featuresOf}.
272
+ */
273
+ export async function driveBeforeLoad(recipe, target, declared) {
274
+ const applied = [];
275
+ for (const intervention of recipe) {
276
+ const driver = intervention.driver;
277
+ if (driver?.pointer === 'outside') {
278
+ await target.mouse.move(-1, -1);
279
+ applied.push(intervention.id);
280
+ }
281
+ else if (driver?.reducedMotion !== undefined) {
282
+ const preference = motionOf(declared?.[MOTION] ?? driver.reducedMotion);
283
+ await target.emulateMedia({ reducedMotion: preference });
284
+ // A declared `no-preference` is emulated and recorded among the features;
285
+ // the trick's id would claim a reduced motion the page never had.
286
+ if (preference === driver.reducedMotion)
287
+ applied.push(intervention.id);
288
+ }
289
+ else if (driver?.clock !== undefined) {
290
+ await target.addInitScript(freezeDate, driver.clock.at);
291
+ applied.push(intervention.id);
292
+ }
293
+ }
294
+ return applied;
295
+ }
296
+ function motionOf(value) {
297
+ if (value === 'reduce' || value === 'no-preference')
298
+ return value;
299
+ throw new Error(`\`${MOTION}\` is declared as ${JSON.stringify(value)}, which no browser reports; ` +
300
+ 'declare "reduce" or "no-preference"');
301
+ }
302
+ /**
303
+ * Take the pointer off the page again before a subject is read.
304
+ *
305
+ * For a host that shares the page: a test hovers a menu to open it and then
306
+ * observes, so the pointer is wherever the test's last step left it. A host
307
+ * that alone drives its page needs only {@link driveBeforeLoad}.
308
+ *
309
+ * Every reading pays the frame, hovered or not: measured in Chromium, a
310
+ * Playwright hover restyles the element while `:hover` matches nothing from
311
+ * script, so the page cannot say whether the move is needed. The pointer is
312
+ * not put back afterwards, and the move fires `mouseleave`: a menu a script
313
+ * opened on hover is closed for the test's next step.
314
+ */
315
+ export async function driveBeforeRead(recipe, target) {
316
+ const applied = [];
317
+ for (const intervention of recipe) {
318
+ if (intervention.driver?.pointer === 'outside') {
319
+ await target.mouse.move(-1, -1);
320
+ applied.push(intervention.id);
321
+ }
322
+ }
323
+ return applied;
324
+ }
325
+ /**
326
+ * The recipe as it holds on a page, given the driver tricks a host applied.
327
+ *
328
+ * A trick with no `driver` is kept. A trick with one is kept only when its id
329
+ * is in `driven`: a host that could not drive it leaves it out of the ids and
330
+ * the digest it records, rather than claiming a hold the page never had. The
331
+ * clock is the one trick whose id the driver may change, because the instant is
332
+ * the host's to choose: the recipe's clock is recorded as the one the host
333
+ * fixed.
334
+ */
335
+ export function forDriver(recipe, driven) {
336
+ const fixed = driven.map((id) => (id === freezeClock.id ? freezeClock : clockById(id))).find(Boolean);
337
+ return recipe.flatMap((intervention) => {
338
+ if (intervention.driver === undefined || driven.includes(intervention.id))
339
+ return [intervention];
340
+ return intervention.driver.clock !== undefined && fixed !== undefined ? [fixed] : [];
341
+ });
342
+ }
343
+ //# sourceMappingURL=drive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drive.js","sourceRoot":"","sources":["../../src/format/drive.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DriverTarget,\n Intervention,\n MotionPreference,\n PageFocusable,\n PageGlobals,\n} from './intervention.js';\n\n/**\n * The tricks a stylesheet cannot reach, and the one call each makes of a driver.\n *\n * Every other trick in [`stabilize.ts`](./stabilize.ts) is CSS, a screenshot\n * option or a script the page runs on itself. Three causes of a page that reads\n * two ways sit outside the document:\n *\n * - **the motion preference** — a page that honours `prefers-reduced-motion`\n * runs its script animation only when the browser says motion is welcome, and\n * a stylesheet cannot reach a `requestAnimationFrame` loop;\n * - **the clock** — a page that prints `new Date()` prints a new value every\n * run, and the date is read by the page's first script, before any stylesheet\n * or settle step exists, so it is replaced by a script the browser runs before\n * the page's own;\n * - **the pointer** — a hover ends when the pointer leaves, and nothing in the\n * page can move it.\n *\n * So each is declared as a {@link DriverState} and applied here, through the\n * structural {@link DriverTarget} a Playwright `Page` already is. A host that\n * owns the browser calls {@link driveBeforeLoad} before its first navigation\n * and hands the page the ids it applied. A host that shares the page with\n * somebody who moves the pointer, such as a test, also calls\n * {@link driveBeforeRead} before each subject is read. A host that does not — a page agent, a test runner's own\n * browser — drives nothing, and {@link forDriver} keeps those tricks out of its\n * recorded recipe, so the absence shows in the reading's ids and digest the way\n * a trick filtered by `needs` does.\n */\n\n/**\n * The instant a fixed clock reads unless a project names another.\n *\n * Noon UTC, so it is the same calendar date in every zone from UTC−11 to\n * UTC+11: a page that prints today's date prints one date whichever zone the\n * browser runs in. A date in the past, so a page that compares against \"now\"\n * to show something as expired does so on every run or on none.\n */\nexport const DEFAULT_NOW = '2024-06-15T12:00:00.000Z';\n\nconst INSTANT = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?(Z|[+-]\\d{2}:\\d{2})$/;\n\n/**\n * The milliseconds an ISO 8601 instant names, or an error that says what an\n * instant is.\n *\n * The zone is required. `2024-06-15T12:00` with no zone is read in whatever\n * zone the machine runs in, so two machines would fix two clocks — the\n * variance this trick exists to remove.\n */\nexport function instantOf(now: string): number {\n const time = INSTANT.test(now) ? Date.parse(now) : Number.NaN;\n if (Number.isNaN(time)) {\n throw new Error(\n '`now` must be an ISO 8601 instant with its zone, such as \"2024-06-15T12:00:00Z\" or ' +\n `\"2024-06-15T14:00:00+02:00\", not ${JSON.stringify(now)}`,\n );\n }\n return time;\n}\n\n/**\n * `prefers-reduced-motion: reduce`, the switch a well-built page already reads.\n *\n * It reaches what `pin-animations` cannot: a script animation, which a\n * stylesheet has no handle on. It reaches only a page that honours the\n * preference, and that is the cost: the page is read as a user who asked for\n * less motion sees it, which can differ from what everybody else sees.\n */\nexport const reducedMotion: Intervention = {\n id: 'reduced-motion',\n trick: 'reset',\n needs: 'layout',\n governs: 'motion-preference',\n because: 'the browser reports prefers-reduced-motion: reduce, so a page that honours it stops its script animations',\n driver: { reducedMotion: 'reduce' },\n};\n\nconst CLOCK_ID = 'freeze-clock';\n\n/**\n * `Date` reads one instant; timers, animation frames and `performance` keep\n * running.\n *\n * An init script, {@link freezeDate}, run in every frame before the page's own\n * first script. It replaces `Date` and nothing else, because the thing that\n * prints two ways is the date a page shows, and the things that make a page\n * settle are timers and frames. Playwright's `clock.setFixedTime` was measured\n * against it and rejected: it installs a fake for `setTimeout`,\n * `requestAnimationFrame` and `performance.now` as well, and a frame's\n * timestamp no longer agrees with `document.timeline`, which drives every CSS\n * and Web Animations frame. Installing no clock also leaves `page.clock` to the\n * test that wants one.\n *\n * The id carries the instant when it is not {@link DEFAULT_NOW}, as\n * `freeze-clock@2031-01-02T03:04:05.000Z`, so the instant is in the recipe's\n * digest and so in the environment key: a baseline read at one instant and a\n * run read at another are compared, and the instant is named as the cause of\n * whatever changed.\n *\n * The cost: code that measures elapsed time with `Date.now()`, such as a\n * debounce, sees no time pass and never fires. `performance.now()` keeps\n * counting.\n */\nexport function freezeClockAt(now: string): Intervention {\n const at = instantOf(now);\n return {\n id: at === instantOf(DEFAULT_NOW) ? CLOCK_ID : `${CLOCK_ID}@${new Date(at).toISOString()}`,\n trick: 'hold',\n needs: 'layout',\n governs: 'clock',\n because: 'Date reads one fixed instant, so elapsed time measured with Date.now() reads zero',\n driver: { clock: { at } },\n };\n}\n\n/** {@link freezeClockAt} the default instant, {@link DEFAULT_NOW}. */\nexport const freezeClock: Intervention = freezeClockAt(DEFAULT_NOW);\n\n/**\n * The clock trick a `freeze-clock@<instant>` id names, or undefined for any\n * other id.\n *\n * The registry holds `freeze-clock` alone; an id with an instant in it is\n * resolved here, so a recipe that crossed into a page as ids keeps its instant.\n */\nexport function clockById(id: string): Intervention | undefined {\n return id.startsWith(`${CLOCK_ID}@`) ? freezeClockAt(id.slice(CLOCK_ID.length + 1)) : undefined;\n}\n\n/**\n * The init script {@link freezeClock} installs: `Date` answers `frozen`, and\n * nothing else changes.\n *\n * Runs in the page, so it closes over nothing. `new Date()`, `Date()` and\n * `Date.now()` read the instant; `new Date(value)`, `Date.UTC`, `Date.parse`\n * and every method on a date are the browser's own. `Intl.DateTimeFormat`'s\n * `format()` and `formatToParts()` with no date read the browser's clock rather\n * than `Date`, so they are given the instant too.\n */\nexport function freezeDate(frozen: number): void {\n // TODO: `Temporal.Now` and a worker's `Date` read the real clock. This runs\n // in each frame's window and replaces `Date` alone, so a page that prints a\n // date from either prints a second reading.\n const Real = Date;\n function Frozen(...args: unknown[]): unknown {\n if (new.target === undefined) return new Real(frozen).toString();\n return Reflect.construct(Real, args.length === 0 ? [frozen] : args, new.target);\n }\n Object.setPrototypeOf(Frozen, Real);\n Object.defineProperties(Frozen, {\n prototype: { value: Real.prototype },\n name: { value: 'Date' },\n length: { value: 7 },\n now: { value: () => frozen, writable: true, configurable: true },\n });\n Object.defineProperty(Real.prototype, 'constructor', { value: Frozen, writable: true, configurable: true });\n Object.defineProperty(globalThis, 'Date', { value: Frozen, writable: true, configurable: true });\n\n const format = Intl.DateTimeFormat.prototype;\n const formatting = Object.getOwnPropertyDescriptor(format, 'format')?.get;\n if (formatting !== undefined) {\n Object.defineProperty(format, 'format', {\n configurable: true,\n get(this: Intl.DateTimeFormat) {\n const bound = formatting.call(this) as (date?: Date | number) => string;\n return (date?: Date | number) => bound(date === undefined ? frozen : date);\n },\n });\n }\n const toParts = format.formatToParts;\n Object.defineProperty(format, 'formatToParts', {\n configurable: true,\n writable: true,\n value(this: Intl.DateTimeFormat, date?: Date | number) {\n return toParts.call(this, date === undefined ? frozen : date);\n },\n });\n}\n\n/**\n * The pointer moved off the page, so nothing is hovered.\n *\n * To `(-1, -1)` rather than to the page's corner: content at the top left is\n * under `(0, 0)` and stays hovered there. `pointer-events: none` is not a\n * substitute — measured, an element already hovered keeps `:hover` under it.\n *\n * Once per page, before load, where nothing but the host moves the pointer. A\n * move is acknowledged on the next frame, ~14 ms measured, so a host that alone\n * drives its page does not move it again. A host that shares the page with a\n * test does, before every reading: see {@link driveBeforeRead}.\n */\nexport const noHover: Intervention = {\n id: 'no-hover',\n trick: 'reset',\n needs: 'layout',\n governs: 'pointer',\n because: 'the pointer moved off the page, so a hover a test left behind is not read',\n driver: { pointer: 'outside' },\n};\n\n/**\n * Nothing focused, so a field a test filled or a page focused on load reads\n * as it does untouched.\n *\n * A settle step rather than a stylesheet: `:focus` and `:focus-visible` follow\n * the element that holds focus, and no rule can unmatch them. It blurs the\n * deepest focused element, walking through shadow roots, because a shadow host\n * is the document's `activeElement` for whatever its tree focused, and blurring\n * the host leaves the input inside it focused and outlined.\n *\n * The cost: a page evaluate per subject, and an animation frame as well when\n * the document holds an `[autofocus]`. A host that reads a page somebody goes\n * on using — a test, an agent's browser — gives focus back once the reading\n * is taken; a host that owns its page does not need to.\n */\nexport const noFocus: Intervention = {\n id: 'no-focus',\n trick: 'reset',\n needs: 'layout',\n governs: 'focus',\n because: 'focus released, so a field a test or the page focused is read unfocused',\n // Again in a frame callback when the document holds an `autofocus`, because\n // the browser honours one at the page's next rendering update: a blur before\n // it is undone, and a document written by `setContent` has had no update yet.\n // Only then, so a page without one pays no frame per subject.\n settle: async (target) => {\n await target.evaluate(\n () =>\n new Promise<void>((resolve) => {\n const view = globalThis as unknown as PageGlobals;\n const release = (): void => {\n let focused: PageFocusable | null | undefined = view.document.activeElement;\n while (focused?.shadowRoot?.activeElement != null) focused = focused.shadowRoot.activeElement;\n focused?.blur?.();\n };\n release();\n if (view.document.querySelector('[autofocus]') === null) return resolve();\n view.requestAnimationFrame(() => {\n release();\n resolve();\n });\n }),\n );\n },\n};\n\n/**\n * The tricks that put a page into the state nobody is touching, in every\n * default collection recipe whose tier can see them.\n *\n * A hover, a focus, a script animation and a printed date are each a second\n * reading of one subject, and each comes from something other than the code\n * under review. Three of them reach the page only through a driver, so a host\n * that cannot drive one leaves it out of the ids it records — see {@link forDriver}.\n */\nexport const UNTOUCHED: readonly Intervention[] = [noHover, noFocus, reducedMotion, freezeClock];\n\nconst MOTION = 'prefers-reduced-motion';\n\n/**\n * The media features a recipe's driver tricks make the browser report, merged\n * under the ones a project declared, or undefined when there are none.\n *\n * Collection resolves `@media` against declared features, not against the\n * browser, so a preference the driver emulated and nobody declared would be\n * read as unknown: both the `reduce` and the `no-preference` block applied, a\n * box the browser never drew. Declaring what the trick emulates gives the\n * preference one owner. A project's own value wins, and\n * {@link driveBeforeLoad} emulates that value, so the two still agree.\n */\nexport function featuresOf(\n recipe: readonly Intervention[],\n declared?: Readonly<Record<string, string>>,\n): Readonly<Record<string, string>> | undefined {\n const motion = recipe.find((intervention) => intervention.driver?.reducedMotion !== undefined);\n const features = {\n ...(motion === undefined ? {} : { [MOTION]: motion.driver!.reducedMotion! }),\n ...declared,\n };\n return Object.keys(features).length === 0 ? undefined : features;\n}\n\n/**\n * Apply what a recipe asks of the browser before the page loads.\n *\n * All three survive navigation, so a host calls this once per page, before its\n * first `goto` or `setContent`. Returns the ids applied, in recipe order. The\n * clock's instant is the trick's own, so a host that wants another instant\n * asks `recipeToDrive` for the recipe rather than telling this.\n *\n * `declared` is the media features the project declared. A declared\n * `prefers-reduced-motion` is what the motion trick emulates, so the browser\n * reports what the record says; see {@link featuresOf}.\n */\nexport async function driveBeforeLoad(\n recipe: readonly Intervention[],\n target: DriverTarget,\n declared?: Readonly<Record<string, string>>,\n): Promise<readonly string[]> {\n const applied: string[] = [];\n for (const intervention of recipe) {\n const driver = intervention.driver;\n if (driver?.pointer === 'outside') {\n await target.mouse.move(-1, -1);\n applied.push(intervention.id);\n } else if (driver?.reducedMotion !== undefined) {\n const preference = motionOf(declared?.[MOTION] ?? driver.reducedMotion);\n await target.emulateMedia({ reducedMotion: preference });\n // A declared `no-preference` is emulated and recorded among the features;\n // the trick's id would claim a reduced motion the page never had.\n if (preference === driver.reducedMotion) applied.push(intervention.id);\n } else if (driver?.clock !== undefined) {\n await target.addInitScript(freezeDate, driver.clock.at);\n applied.push(intervention.id);\n }\n }\n return applied;\n}\n\nfunction motionOf(value: string): MotionPreference {\n if (value === 'reduce' || value === 'no-preference') return value;\n throw new Error(\n `\\`${MOTION}\\` is declared as ${JSON.stringify(value)}, which no browser reports; ` +\n 'declare \"reduce\" or \"no-preference\"',\n );\n}\n\n/**\n * Take the pointer off the page again before a subject is read.\n *\n * For a host that shares the page: a test hovers a menu to open it and then\n * observes, so the pointer is wherever the test's last step left it. A host\n * that alone drives its page needs only {@link driveBeforeLoad}.\n *\n * Every reading pays the frame, hovered or not: measured in Chromium, a\n * Playwright hover restyles the element while `:hover` matches nothing from\n * script, so the page cannot say whether the move is needed. The pointer is\n * not put back afterwards, and the move fires `mouseleave`: a menu a script\n * opened on hover is closed for the test's next step.\n */\nexport async function driveBeforeRead(\n recipe: readonly Intervention[],\n target: Pick<DriverTarget, 'mouse'>,\n): Promise<readonly string[]> {\n const applied: string[] = [];\n for (const intervention of recipe) {\n if (intervention.driver?.pointer === 'outside') {\n await target.mouse.move(-1, -1);\n applied.push(intervention.id);\n }\n }\n return applied;\n}\n\n/**\n * The recipe as it holds on a page, given the driver tricks a host applied.\n *\n * A trick with no `driver` is kept. A trick with one is kept only when its id\n * is in `driven`: a host that could not drive it leaves it out of the ids and\n * the digest it records, rather than claiming a hold the page never had. The\n * clock is the one trick whose id the driver may change, because the instant is\n * the host's to choose: the recipe's clock is recorded as the one the host\n * fixed.\n */\nexport function forDriver(\n recipe: readonly Intervention[],\n driven: readonly string[],\n): readonly Intervention[] {\n const fixed = driven.map((id) => (id === freezeClock.id ? freezeClock : clockById(id))).find(Boolean);\n return recipe.flatMap((intervention) => {\n if (intervention.driver === undefined || driven.includes(intervention.id)) return [intervention];\n return intervention.driver.clock !== undefined && fixed !== undefined ? [fixed] : [];\n });\n}\n"]}
@@ -12,7 +12,7 @@ import type { ProfileId } from './profile.js';
12
12
  * treated as one. Leaving a field out is a correctness bug. When in doubt,
13
13
  * include it: over-invalidation costs CI minutes, under-invalidation costs trust.
14
14
  */
15
- export interface EnvironmentInputs {
15
+ export interface EnvironmentInputs extends PageRegion {
16
16
  /** Which collector, and therefore which dimensions exist at all (ADR-0002). */
17
17
  readonly profile: ProfileId;
18
18
  /** Engine identity. `jsdom@26.1.0`, `chromium@131.0.6778.33`. */
@@ -60,6 +60,24 @@ export interface EnvironmentInputs {
60
60
  */
61
61
  readonly stabilization?: Digest;
62
62
  }
63
+ /**
64
+ * The timezone a page's clock reads in and the language it formats for, as
65
+ * Playwright names them.
66
+ *
67
+ * A page that formats a date or a number without naming a timezone or a language
68
+ * of its own gets its browser's, so one build reads `12:30 AM` in one timezone
69
+ * and `4:30 PM` in another — different text, different widths, no edit anywhere.
70
+ * A collector that opened its page in a region it chose writes it to the
71
+ * environment key, the way the viewport carries its colour scheme. A field is
72
+ * absent where nobody chose it: a jsdom capture runs in its Node process, and an
73
+ * absent field says so rather than naming a timezone nobody set.
74
+ */
75
+ export interface PageRegion {
76
+ /** An IANA timezone, such as `Europe/Berlin`. */
77
+ readonly timezoneId?: string;
78
+ /** A BCP 47 language tag, such as `de-DE`: a run's base locale (spec 0008). */
79
+ readonly locale?: string;
80
+ }
63
81
  export interface Viewport {
64
82
  readonly width: number;
65
83
  readonly height: number;
@@ -111,7 +129,7 @@ export declare function environmentKey(inputs: EnvironmentInputs): EnvironmentKe
111
129
  * environments is a product feature, not a debugging aid.
112
130
  */
113
131
  export declare function diffEnvironments(before: EnvironmentInputs, after: EnvironmentInputs): readonly EnvironmentDelta[];
114
- export type EnvironmentField = 'profile' | 'engine' | 'ruleset' | 'allowlist' | 'viewport' | 'fonts' | 'conditions' | 'assets' | 'stabilization';
132
+ export type EnvironmentField = 'profile' | 'engine' | 'ruleset' | 'allowlist' | 'viewport' | 'fonts' | 'conditions' | 'assets' | 'stabilization' | 'timezoneId' | 'locale';
115
133
  export interface EnvironmentDelta {
116
134
  readonly field: EnvironmentField;
117
135
  readonly from: string;
@@ -12,6 +12,11 @@ export function environmentKey(inputs) {
12
12
  // the recipe's first job is to stop an animation from moving `transform`,
13
13
  // which is a value the semantic representation carries.
14
14
  stabilization: inputs.stabilization,
15
+ // Text is laid out, so a timezone or a language reaches the box tree, and both
16
+ // keys carry them. `undefined` is omitted from the canonical form, so a
17
+ // capture that chose neither keeps the key it had before either existed.
18
+ timezoneId: inputs.timezoneId,
19
+ locale: inputs.locale,
15
20
  };
16
21
  // Layout depends on the viewport's size and colour scheme; rasterization also
17
22
  // depends on its scale factor. Splitting the viewport is what makes the two
@@ -48,6 +53,8 @@ export function diffEnvironments(before, after) {
48
53
  scalar('conditions', before.conditions, after.conditions);
49
54
  scalar('assets', before.assets, after.assets);
50
55
  scalar('stabilization', before.stabilization, after.stabilization);
56
+ scalar('timezoneId', before.timezoneId, after.timezoneId);
57
+ scalar('locale', before.locale, after.locale);
51
58
  return deltas;
52
59
  }
53
60
  //# sourceMappingURL=environment.js.map