@variance-authority/core 0.14.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.
- package/CHANGELOG.md +454 -0
- package/README.md +13 -10
- package/dist/attribute/component-hash.d.ts +3 -2
- package/dist/attribute/component-hash.js +3 -2
- package/dist/attribute/component-hash.js.map +1 -1
- package/dist/attribute/composition.d.ts +16 -0
- package/dist/attribute/composition.js +4 -6
- package/dist/attribute/composition.js.map +1 -1
- package/dist/attribute/index.d.ts +3 -1
- package/dist/attribute/index.js +2 -1
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/region.js +1 -5
- package/dist/attribute/region.js.map +1 -1
- package/dist/attribute/source.d.ts +10 -24
- package/dist/attribute/source.js +4 -38
- package/dist/attribute/source.js.map +1 -1
- package/dist/attribute/subject-pieces.d.ts +37 -0
- package/dist/attribute/subject-pieces.js +122 -0
- package/dist/attribute/subject-pieces.js.map +1 -0
- package/dist/format/capture.d.ts +25 -0
- package/dist/format/capture.js +6 -0
- package/dist/format/capture.js.map +1 -1
- package/dist/format/document.d.ts +2 -2
- package/dist/format/document.js +8 -2
- package/dist/format/document.js.map +1 -1
- package/dist/format/drive.d.ts +191 -0
- package/dist/format/drive.js +343 -0
- package/dist/format/drive.js.map +1 -0
- package/dist/format/environment.d.ts +20 -2
- package/dist/format/environment.js +7 -0
- package/dist/format/environment.js.map +1 -1
- package/dist/format/index.d.ts +6 -3
- package/dist/format/index.js +4 -2
- package/dist/format/index.js.map +1 -1
- package/dist/format/intervention.d.ts +79 -0
- package/dist/format/intervention.js.map +1 -1
- package/dist/format/origin.d.ts +32 -0
- package/dist/format/origin.js +54 -0
- package/dist/format/origin.js.map +1 -0
- package/dist/format/snapshot.d.ts +2 -0
- package/dist/format/snapshot.js.map +1 -1
- package/dist/format/stabilize.d.ts +28 -5
- package/dist/format/stabilize.js +48 -100
- package/dist/format/stabilize.js.map +1 -1
- package/dist/format/waits.d.ts +12 -0
- package/dist/format/waits.js +156 -0
- package/dist/format/waits.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/nearest.d.ts +46 -0
- package/dist/nearest.js +102 -0
- package/dist/nearest.js.map +1 -0
- package/dist/relate/before.d.ts +19 -24
- package/dist/relate/before.js +35 -37
- package/dist/relate/before.js.map +1 -1
- package/dist/relate/beyond.d.ts +59 -0
- package/dist/relate/beyond.js +98 -0
- package/dist/relate/beyond.js.map +1 -0
- package/dist/relate/carried.d.ts +122 -0
- package/dist/relate/carried.js +279 -0
- package/dist/relate/carried.js.map +1 -0
- package/dist/relate/dominate.d.ts +19 -0
- package/dist/relate/dominate.js +77 -0
- package/dist/relate/dominate.js.map +1 -0
- package/dist/relate/index.d.ts +3 -0
- package/dist/relate/index.js +3 -0
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/merkle.js +2 -11
- package/dist/relate/merkle.js.map +1 -1
- package/dist/relate/narrow.d.ts +2 -2
- package/dist/relate/narrow.js +26 -73
- package/dist/relate/narrow.js.map +1 -1
- package/dist/relate/reach.d.ts +1 -1
- package/dist/relate/reach.js +10 -53
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +1 -1
- package/dist/relate/records.js.map +1 -1
- package/dist/rules/index.d.ts +3 -3
- package/dist/rules/index.js +3 -3
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/normalize/cascade.d.ts +12 -0
- package/dist/rules/normalize/cascade.js +34 -16
- package/dist/rules/normalize/cascade.js.map +1 -1
- package/dist/rules/normalize/fixture.d.ts +1 -0
- package/dist/rules/normalize/fixture.js +1 -0
- package/dist/rules/normalize/fixture.js.map +1 -1
- package/dist/rules/normalize/index.d.ts +2 -2
- package/dist/rules/normalize/index.js +9 -2
- package/dist/rules/normalize/index.js.map +1 -1
- package/dist/rules/normalize/value.d.ts +13 -2
- package/dist/rules/normalize/value.js +42 -8
- package/dist/rules/normalize/value.js.map +1 -1
- package/dist/rules/normalize/wrapper.js +5 -0
- package/dist/rules/normalize/wrapper.js.map +1 -1
- package/dist/rules/ruleset.d.ts +24 -4
- package/dist/rules/ruleset.js +24 -4
- package/dist/rules/ruleset.js.map +1 -1
- package/dist/segment/index.d.ts +20 -4
- package/dist/segment/index.js +28 -7
- package/dist/segment/index.js.map +1 -1
- package/dist/shard/index.d.ts +11 -0
- package/dist/shard/index.js +11 -0
- package/dist/shard/index.js.map +1 -0
- package/dist/shard/placement.d.ts +117 -0
- package/dist/shard/placement.js +170 -0
- package/dist/shard/placement.js.map +1 -0
- 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
|