@atomic-testing/playwright 0.87.0 → 0.89.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/README.md +7 -0
- package/dist/index.cjs +213 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -29
- package/dist/index.d.mts +113 -29
- package/dist/index.mjs +215 -71
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/PlaywrightInteractor.ts +257 -40
- package/src/index.ts +0 -1
- package/src/testRunnerAdapter.ts +0 -79
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BlurOption,
|
|
3
|
+
BoundingRect,
|
|
3
4
|
byCssSelector,
|
|
4
5
|
ClickOption,
|
|
5
6
|
CssProperty,
|
|
6
7
|
dateUtil,
|
|
7
8
|
defaultWaitForOption,
|
|
9
|
+
ElementNotFoundError,
|
|
8
10
|
EnterTextOption,
|
|
9
11
|
FocusOption,
|
|
10
12
|
HoverOption,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
MouseUpOption,
|
|
20
22
|
Optional,
|
|
21
23
|
PartLocator,
|
|
24
|
+
Point,
|
|
22
25
|
PressKeyOption,
|
|
23
26
|
timingUtil,
|
|
24
27
|
WaitForOption,
|
|
@@ -35,15 +38,150 @@ export class PlaywrightInteractor implements Interactor {
|
|
|
35
38
|
*/
|
|
36
39
|
constructor(public readonly page: Page) {}
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Run a Playwright mutation and normalize a "locator matched nothing" failure
|
|
43
|
+
* into {@link ElementNotFoundError}, so a missing element throws the same error
|
|
44
|
+
* class here as it does in `DOMInteractor`, regardless of environment (the
|
|
45
|
+
* unified contract ratified in ADR-006).
|
|
46
|
+
*
|
|
47
|
+
* Playwright auto-waits for actionability and then throws its own
|
|
48
|
+
* `TimeoutError`. We translate that to `ElementNotFoundError` ONLY when the
|
|
49
|
+
* element genuinely does not exist (count 0) and otherwise rethrow the original
|
|
50
|
+
* error — preserving Playwright's auto-wait for an element that exists but is
|
|
51
|
+
* briefly not actionable (covered, disabled, animating). The trade-off is that
|
|
52
|
+
* a truly-missing element waits out the page's action timeout before throwing;
|
|
53
|
+
* bound it with `page.setDefaultTimeout` when fast failure matters.
|
|
54
|
+
*
|
|
55
|
+
* @param locator - Locator the mutation targets
|
|
56
|
+
* @param action - Method name used in the error message (e.g. `'click'`)
|
|
57
|
+
* @param run - The Playwright action to execute
|
|
58
|
+
* @throws {ElementNotFoundError} If the action fails and the element is absent
|
|
59
|
+
*/
|
|
60
|
+
private async runMutation<T>(locator: PartLocator, action: string, run: () => Promise<T>): Promise<T> {
|
|
61
|
+
try {
|
|
62
|
+
return await run();
|
|
63
|
+
} catch (e) {
|
|
64
|
+
if ((await this.exists(locator)) === false) {
|
|
65
|
+
throw new ElementNotFoundError(locator, action);
|
|
66
|
+
}
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
38
71
|
/**
|
|
39
72
|
* Select the given option values on a `<select>` element.
|
|
40
73
|
*
|
|
41
74
|
* @param locator - Locator to the `<select>` element.
|
|
42
75
|
* @param values - Values to select.
|
|
76
|
+
* @throws {ElementNotFoundError} If the element is not found
|
|
43
77
|
*/
|
|
44
78
|
async selectOptionValue(locator: PartLocator, values: string[]): Promise<void> {
|
|
45
79
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
46
|
-
await this.page.locator(cssLocator).selectOption(values);
|
|
80
|
+
await this.runMutation(locator, 'selectOptionValue', () => this.page.locator(cssLocator).selectOption(values));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set the selected files on a `<input type="file">` element.
|
|
85
|
+
*
|
|
86
|
+
* Playwright's native `setInputFiles` reads the given paths from disk and
|
|
87
|
+
* populates the input's `FileList`, firing the change event — the only way to
|
|
88
|
+
* fill a file input, whose value cannot be set programmatically.
|
|
89
|
+
*
|
|
90
|
+
* @param locator - Locator of the `<input type="file">` element
|
|
91
|
+
* @param files - One or more filesystem paths to upload
|
|
92
|
+
* @throws {ElementNotFoundError} If the element is not found
|
|
93
|
+
*/
|
|
94
|
+
async setInputFiles(locator: PartLocator, files: string | string[]): Promise<void> {
|
|
95
|
+
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
96
|
+
await this.runMutation(locator, 'setInputFiles', () => this.page.locator(cssLocator).setInputFiles(files));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Scroll the located element into view, no-op if already visible.
|
|
101
|
+
*
|
|
102
|
+
* Delegates to Playwright's `scrollIntoViewIfNeeded`, which performs a real
|
|
103
|
+
* layout-aware scroll in the browser.
|
|
104
|
+
*
|
|
105
|
+
* @param locator - Locator of the element to scroll into view
|
|
106
|
+
* @throws {ElementNotFoundError} If the element is not found
|
|
107
|
+
*/
|
|
108
|
+
async scrollIntoView(locator: PartLocator): Promise<void> {
|
|
109
|
+
const css = await locatorUtil.toCssSelector(locator, this);
|
|
110
|
+
await this.runMutation(locator, 'scrollIntoView', () => this.page.locator(css).scrollIntoViewIfNeeded());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Scroll the located element by the given pixel delta.
|
|
115
|
+
*
|
|
116
|
+
* The scroll is performed by evaluating `el.scrollBy(dx, dy)` on the element
|
|
117
|
+
* itself rather than `page.mouse.wheel`. A wheel event scrolls whatever sits
|
|
118
|
+
* under the pointer and is non-deterministic across chromium/firefox/webkit,
|
|
119
|
+
* whereas evaluating `scrollBy` on the resolved element scrolls exactly that
|
|
120
|
+
* element. This is a deliberate deviation from ADR 0001's per-engine table
|
|
121
|
+
* (which lists `page.mouse.wheel`), taking the alternative the step-5 prompt
|
|
122
|
+
* permits ("or evaluate el.scrollBy") for cross-browser determinism.
|
|
123
|
+
*
|
|
124
|
+
* @param locator - Locator of the scrollable element
|
|
125
|
+
* @param delta - Pixel offset to scroll by
|
|
126
|
+
* @throws {ElementNotFoundError} If the element is not found
|
|
127
|
+
*/
|
|
128
|
+
async scrollBy(locator: PartLocator, delta: Point): Promise<void> {
|
|
129
|
+
const css = await locatorUtil.toCssSelector(locator, this);
|
|
130
|
+
await this.runMutation(locator, 'scrollBy', () =>
|
|
131
|
+
this.page.locator(css).evaluate((el, d) => el.scrollBy(d.x, d.y), { x: delta.x, y: delta.y })
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Drag the source element and drop it onto the target element.
|
|
137
|
+
*
|
|
138
|
+
* Delegates to Playwright's native `Locator.dragTo`, which performs a real,
|
|
139
|
+
* layout-aware drag gesture in the browser.
|
|
140
|
+
*
|
|
141
|
+
* @param source - Locator of the element to drag
|
|
142
|
+
* @param target - Locator of the drop target
|
|
143
|
+
* @throws {ElementNotFoundError} If either the source or target is not found
|
|
144
|
+
*/
|
|
145
|
+
async dragTo(source: PartLocator, target: PartLocator): Promise<void> {
|
|
146
|
+
const srcCss = await locatorUtil.toCssSelector(source, this);
|
|
147
|
+
const tgtCss = await locatorUtil.toCssSelector(target, this);
|
|
148
|
+
try {
|
|
149
|
+
await this.page.locator(srcCss).dragTo(this.page.locator(tgtCss));
|
|
150
|
+
} catch (e) {
|
|
151
|
+
if ((await this.exists(source)) === false) {
|
|
152
|
+
throw new ElementNotFoundError(source, 'dragTo');
|
|
153
|
+
}
|
|
154
|
+
if ((await this.exists(target)) === false) {
|
|
155
|
+
throw new ElementNotFoundError(target, 'dragTo');
|
|
156
|
+
}
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Drag the located element by the given pixel delta from its center.
|
|
163
|
+
*
|
|
164
|
+
* The gesture is a single uninterrupted `move → down → move → up` sequence
|
|
165
|
+
* computed from the element's center. It deliberately does NOT reuse
|
|
166
|
+
* {@link mouseMove}/{@link mouseDown} — `mouseMove` resets the pointer with
|
|
167
|
+
* `page.mouse.move(0, 0)` after hovering, which would corrupt the drag path
|
|
168
|
+
* (see ADR 0001, option 5). The center comes from {@link getBoundingRect},
|
|
169
|
+
* which throws `ElementNotFoundError` when the element has no box, so this
|
|
170
|
+
* shares that "element not found" contract instead of re-deriving the box +
|
|
171
|
+
* guard here.
|
|
172
|
+
*
|
|
173
|
+
* @param locator - Locator of the element to drag
|
|
174
|
+
* @param delta - Pixel offset to drag by
|
|
175
|
+
* @throws {ElementNotFoundError} If the element has no bounding box
|
|
176
|
+
*/
|
|
177
|
+
async drag(locator: PartLocator, delta: Point): Promise<void> {
|
|
178
|
+
const rect = await this.getBoundingRect(locator);
|
|
179
|
+
const cx = rect.x + rect.width / 2;
|
|
180
|
+
const cy = rect.y + rect.height / 2;
|
|
181
|
+
await this.page.mouse.move(cx, cy);
|
|
182
|
+
await this.page.mouse.down();
|
|
183
|
+
await this.page.mouse.move(cx + delta.x, cy + delta.y, { steps: 8 });
|
|
184
|
+
await this.page.mouse.up();
|
|
47
185
|
}
|
|
48
186
|
|
|
49
187
|
/**
|
|
@@ -104,93 +242,152 @@ export class PlaywrightInteractor implements Interactor {
|
|
|
104
242
|
|
|
105
243
|
async enterText(locator: PartLocator, text: string, option?: Optional<Partial<EnterTextOption>>): Promise<void> {
|
|
106
244
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
245
|
+
await this.runMutation(locator, 'enterText', async () => {
|
|
246
|
+
if (!option?.append) {
|
|
247
|
+
await this.page.locator(cssLocator).clear();
|
|
248
|
+
}
|
|
110
249
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
250
|
+
// If it is a date, time or datetime-local input, validate the date format
|
|
251
|
+
const type = (await this.getAttribute(locator, 'type')) ?? '';
|
|
252
|
+
if (dateUtil.isHtmlDateInputType(type)) {
|
|
253
|
+
const result = dateUtil.validateHtmlDateInput(type, text);
|
|
254
|
+
if (!result.valid) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Invalid date format for type: ${type}, expected format: ${result.format}, example: ${result.example}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
119
259
|
}
|
|
120
|
-
|
|
121
|
-
|
|
260
|
+
await this.page.locator(cssLocator).fill(text);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async setRangeValue(locator: PartLocator, value: number): Promise<void> {
|
|
265
|
+
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
266
|
+
// Playwright's `fill` rejects `<input type="range">` (it is not a fillable
|
|
267
|
+
// text control), so set the value in-page through the native value setter.
|
|
268
|
+
// Calling the prototype setter both sanitizes the value to the input's step
|
|
269
|
+
// (the browser snaps an off-step target to the nearest valid step) and lets
|
|
270
|
+
// React's value tracker observe the change; the dispatched input/change
|
|
271
|
+
// events then drive a controlled component (e.g. MUI Slider) to re-render.
|
|
272
|
+
await this.runMutation(locator, 'setRangeValue', () =>
|
|
273
|
+
this.page.locator(cssLocator).evaluate((el, nextValue) => {
|
|
274
|
+
const input = el as HTMLInputElement;
|
|
275
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
|
276
|
+
setter?.call(input, nextValue);
|
|
277
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
278
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
279
|
+
}, String(value))
|
|
280
|
+
);
|
|
122
281
|
}
|
|
123
282
|
|
|
124
283
|
async click(locator: PartLocator, option?: Partial<ClickOption>): Promise<void> {
|
|
125
284
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
126
|
-
await this.page.locator(cssLocator).click({ position: option?.position });
|
|
285
|
+
await this.runMutation(locator, 'click', () => this.page.locator(cssLocator).click({ position: option?.position }));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Dispatch a right-click on the located element to open its context menu.
|
|
290
|
+
*
|
|
291
|
+
* Delegates to Playwright's native right-button click, which fires a real
|
|
292
|
+
* `contextmenu` event in the browser.
|
|
293
|
+
*
|
|
294
|
+
* @param locator - Locator of the element to right-click
|
|
295
|
+
* @throws {ElementNotFoundError} If the element is not found
|
|
296
|
+
*/
|
|
297
|
+
async contextMenu(locator: PartLocator): Promise<void> {
|
|
298
|
+
const css = await locatorUtil.toCssSelector(locator, this);
|
|
299
|
+
await this.runMutation(locator, 'contextMenu', () => this.page.locator(css).click({ button: 'right' }));
|
|
127
300
|
}
|
|
128
301
|
|
|
129
302
|
async hover(locator: PartLocator, option?: Partial<HoverOption>): Promise<void> {
|
|
130
303
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
131
|
-
await this.page.locator(cssLocator).hover({ position: option?.position });
|
|
304
|
+
await this.runMutation(locator, 'hover', () => this.page.locator(cssLocator).hover({ position: option?.position }));
|
|
132
305
|
}
|
|
133
306
|
|
|
134
307
|
async mouseMove(locator: PartLocator, option?: Partial<MouseMoveOption>): Promise<void> {
|
|
135
|
-
await this.
|
|
136
|
-
position: option?.position
|
|
308
|
+
await this.runMutation(locator, 'mouseMove', async () => {
|
|
309
|
+
await this.hover(locator, { position: option?.position });
|
|
310
|
+
await this.page.mouse.move(0, 0);
|
|
137
311
|
});
|
|
138
|
-
await this.page.mouse.move(0, 0);
|
|
139
312
|
}
|
|
140
313
|
|
|
141
314
|
async mouseDown(locator: PartLocator, option?: Partial<MouseDownOption>): Promise<void> {
|
|
142
|
-
await this.
|
|
143
|
-
position: option?.position
|
|
315
|
+
await this.runMutation(locator, 'mouseDown', async () => {
|
|
316
|
+
await this.hover(locator, { position: option?.position });
|
|
317
|
+
await this.page.mouse.down();
|
|
144
318
|
});
|
|
145
|
-
await this.page.mouse.down();
|
|
146
319
|
}
|
|
147
320
|
|
|
148
321
|
async mouseUp(locator: PartLocator, option?: Partial<MouseUpOption>): Promise<void> {
|
|
149
|
-
await this.
|
|
150
|
-
position: option?.position
|
|
322
|
+
await this.runMutation(locator, 'mouseUp', async () => {
|
|
323
|
+
await this.hover(locator, { position: option?.position });
|
|
324
|
+
await this.page.mouse.up();
|
|
151
325
|
});
|
|
152
|
-
await this.page.mouse.up();
|
|
153
326
|
}
|
|
154
327
|
|
|
155
328
|
async mouseOver(locator: PartLocator, option?: Partial<HoverOption>): Promise<void> {
|
|
156
|
-
|
|
329
|
+
await this.runMutation(locator, 'mouseOver', () => this.hover(locator, option));
|
|
157
330
|
}
|
|
158
331
|
|
|
159
332
|
async mouseOut(locator: PartLocator, _option?: Partial<MouseOutOption>): Promise<void> {
|
|
160
333
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
334
|
+
await this.runMutation(locator, 'mouseOut', async () => {
|
|
335
|
+
// First hover over the element to trigger mouseenter/mouseover
|
|
336
|
+
await this.page.locator(cssLocator).hover();
|
|
337
|
+
// Then dispatch mouseout event directly for cross-browser reliability
|
|
338
|
+
await this.page.locator(cssLocator).dispatchEvent('mouseout');
|
|
339
|
+
});
|
|
165
340
|
}
|
|
166
341
|
|
|
167
342
|
async mouseEnter(locator: PartLocator, _option?: Partial<MouseEnterOption>): Promise<void> {
|
|
168
|
-
|
|
343
|
+
await this.runMutation(locator, 'mouseEnter', () => this.hover(locator));
|
|
169
344
|
}
|
|
170
345
|
|
|
171
346
|
async mouseLeave(locator: PartLocator, _option?: Partial<MouseLeaveOption>): Promise<void> {
|
|
172
347
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
348
|
+
await this.runMutation(locator, 'mouseLeave', async () => {
|
|
349
|
+
// First hover over the element to trigger mouseenter/mouseover
|
|
350
|
+
await this.page.locator(cssLocator).hover();
|
|
351
|
+
// Dispatch mouseout which triggers both mouseout and mouseleave handlers in React
|
|
352
|
+
await this.page.locator(cssLocator).dispatchEvent('mouseout');
|
|
353
|
+
});
|
|
177
354
|
}
|
|
178
355
|
|
|
179
356
|
async focus(locator: PartLocator, _option?: Partial<FocusOption>): Promise<void> {
|
|
180
357
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
181
|
-
|
|
358
|
+
await this.runMutation(locator, 'focus', () => this.page.focus(cssLocator));
|
|
182
359
|
}
|
|
183
360
|
|
|
184
361
|
async blur(locator: PartLocator, _option?: Partial<BlurOption>): Promise<void> {
|
|
185
362
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
186
|
-
await this.page.locator(cssLocator).blur();
|
|
363
|
+
await this.runMutation(locator, 'blur', () => this.page.locator(cssLocator).blur());
|
|
187
364
|
}
|
|
188
365
|
|
|
189
|
-
async pressKey(locator: PartLocator, key: string,
|
|
366
|
+
async pressKey(locator: PartLocator, key: string, option?: Partial<PressKeyOption>): Promise<void> {
|
|
190
367
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
368
|
+
// Compose Playwright's chord syntax — modifiers joined to the key by `+`, in
|
|
369
|
+
// Playwright's accepted Control+Alt+Shift+Meta order — so the browser holds
|
|
370
|
+
// those modifiers across the keypress and the event carries ctrlKey/etc.
|
|
371
|
+
const modifiers: string[] = [];
|
|
372
|
+
if (option?.ctrl) {
|
|
373
|
+
modifiers.push('Control');
|
|
374
|
+
}
|
|
375
|
+
if (option?.alt) {
|
|
376
|
+
modifiers.push('Alt');
|
|
377
|
+
}
|
|
378
|
+
if (option?.shift) {
|
|
379
|
+
modifiers.push('Shift');
|
|
380
|
+
}
|
|
381
|
+
if (option?.meta) {
|
|
382
|
+
modifiers.push('Meta');
|
|
383
|
+
}
|
|
384
|
+
const chord = modifiers.length > 0 ? `${modifiers.join('+')}+${key}` : key;
|
|
191
385
|
// locator.press auto-focuses the element, then dispatches a real, trusted
|
|
192
386
|
// KeyboardEvent — the browser equivalent of the DOM focus-first keyDown/keyUp.
|
|
193
|
-
|
|
387
|
+
// Caveat: for Shift + a printable key the browser case-folds `event.key`
|
|
388
|
+
// (`Shift+a` → `'A'`) whereas the jsdom path keeps `'a'` — only the modifier
|
|
389
|
+
// flags are delivered identically across engines (see #924).
|
|
390
|
+
await this.runMutation(locator, 'pressKey', () => this.page.locator(cssLocator).press(chord));
|
|
194
391
|
}
|
|
195
392
|
|
|
196
393
|
async activate(locator: PartLocator): Promise<void> {
|
|
@@ -198,7 +395,7 @@ export class PlaywrightInteractor implements Interactor {
|
|
|
198
395
|
// Geometry-free activation mirrors the mouseout dispatch precedent above: it
|
|
199
396
|
// bypasses hit-testing to actuate a covered or zero-size input that
|
|
200
397
|
// locator.click() (a real geometry hit-test) cannot reach.
|
|
201
|
-
await this.page.locator(cssLocator).dispatchEvent('click');
|
|
398
|
+
await this.runMutation(locator, 'activate', () => this.page.locator(cssLocator).dispatchEvent('click'));
|
|
202
399
|
}
|
|
203
400
|
|
|
204
401
|
//#region wait conditions
|
|
@@ -249,6 +446,26 @@ export class PlaywrightInteractor implements Interactor {
|
|
|
249
446
|
return text ?? undefined;
|
|
250
447
|
}
|
|
251
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Get the located element's bounding rectangle.
|
|
451
|
+
*
|
|
452
|
+
* `boundingBox()` returns `null` for a detached/invisible element rather than
|
|
453
|
+
* auto-waiting, so this throws `ElementNotFoundError` directly (no auto-wait)
|
|
454
|
+
* — matching the unified "element not found" contract (ADR-006).
|
|
455
|
+
*
|
|
456
|
+
* @param locator - Locator of the element to measure
|
|
457
|
+
* @returns The element's bounding rectangle in CSS pixels
|
|
458
|
+
* @throws {ElementNotFoundError} If the element has no bounding box
|
|
459
|
+
*/
|
|
460
|
+
async getBoundingRect(locator: PartLocator): Promise<BoundingRect> {
|
|
461
|
+
const css = await locatorUtil.toCssSelector(locator, this);
|
|
462
|
+
const box = await this.page.locator(css).boundingBox();
|
|
463
|
+
if (box == null) {
|
|
464
|
+
throw new ElementNotFoundError(locator, 'getBoundingRect');
|
|
465
|
+
}
|
|
466
|
+
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
467
|
+
}
|
|
468
|
+
|
|
252
469
|
async exists(locator: PartLocator): Promise<boolean> {
|
|
253
470
|
const cssLocator = await locatorUtil.toCssSelector(locator, this);
|
|
254
471
|
const count = await this.page.locator(cssLocator).count();
|
package/src/index.ts
CHANGED
package/src/testRunnerAdapter.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { ScenePart, TestEngine } from '@atomic-testing/core';
|
|
2
|
-
import {
|
|
3
|
-
E2eTestInterface,
|
|
4
|
-
E2eTestRunEnvironmentFixture,
|
|
5
|
-
TestFrameworkMapper,
|
|
6
|
-
} from '@atomic-testing/internal-test-runner';
|
|
7
|
-
import { expect, Page, test } from '@playwright/test';
|
|
8
|
-
|
|
9
|
-
import { createTestEngine } from './createTestEngine';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Navigate the current Playwright page to the provided URL.
|
|
13
|
-
*
|
|
14
|
-
* @param url - Destination URL to load.
|
|
15
|
-
* @param fixture - Optional test fixture supplying the Playwright page.
|
|
16
|
-
*/
|
|
17
|
-
export async function goto(url: string): Promise<void>;
|
|
18
|
-
export async function goto(url: string, fixture: E2eTestRunEnvironmentFixture): Promise<void>;
|
|
19
|
-
export async function goto(url: string, fixture?: E2eTestRunEnvironmentFixture): Promise<void> {
|
|
20
|
-
const page = fixture!.page as Page;
|
|
21
|
-
await page.goto(url);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Create a {@link TestEngine} bound to the Playwright page in the given fixture.
|
|
26
|
-
*
|
|
27
|
-
* @param scenePart - Scene definition to drive.
|
|
28
|
-
* @param fixture - Fixture providing the Playwright page.
|
|
29
|
-
*/
|
|
30
|
-
export function playwrightGetTestEngine<T extends ScenePart>(
|
|
31
|
-
scenePart: T,
|
|
32
|
-
fixture: E2eTestRunEnvironmentFixture
|
|
33
|
-
): TestEngine<T> {
|
|
34
|
-
const page = fixture.page as Page;
|
|
35
|
-
return createTestEngine(page, scenePart);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Playwright adapter for the TestFrameworkMapper interface.
|
|
40
|
-
*/
|
|
41
|
-
export const playWrightTestFrameworkMapper: TestFrameworkMapper = {
|
|
42
|
-
/*
|
|
43
|
-
* INTENTIONAL @ts-expect-error comments: Playwright's test functions have different type
|
|
44
|
-
* signatures than the normalized TestFrameworkMapper interface. Playwright uses fixture-based
|
|
45
|
-
* callbacks with destructuring ({ page, browser }) while our interface uses a union type for
|
|
46
|
-
* Jest compatibility (done callback or fixture object). The functions are compatible at runtime
|
|
47
|
-
* but TypeScript cannot verify this due to these fundamental signature differences.
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
assertEqual: (a, b) => expect(a).toEqual(b),
|
|
51
|
-
assertNotEqual: (a, b) => expect(a).not.toEqual(b),
|
|
52
|
-
assertTrue: value => expect(value).toBe(true),
|
|
53
|
-
assertFalse: value => expect(value).toBe(false),
|
|
54
|
-
assertApproxEqual: (actual, expected, tolerance) =>
|
|
55
|
-
expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance),
|
|
56
|
-
// @ts-expect-error - Playwright describe signature differs from TestFrameworkMapper.Describe
|
|
57
|
-
describe: test.describe,
|
|
58
|
-
|
|
59
|
-
beforeEach: test.beforeEach,
|
|
60
|
-
afterEach: test.afterEach,
|
|
61
|
-
beforeAll: test.beforeAll,
|
|
62
|
-
afterAll: test.afterAll,
|
|
63
|
-
|
|
64
|
-
// @ts-expect-error - Playwright test signature differs from TestFrameworkMapper.Test
|
|
65
|
-
test: test,
|
|
66
|
-
|
|
67
|
-
// @ts-expect-error - Playwright test signature differs from TestFrameworkMapper.Test
|
|
68
|
-
it: test,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get a typed interface for running end-to-end tests with Playwright.
|
|
73
|
-
*/
|
|
74
|
-
export function getTestRunnerInterface<T extends ScenePart>(): E2eTestInterface<T> {
|
|
75
|
-
return {
|
|
76
|
-
getTestEngine: playwrightGetTestEngine,
|
|
77
|
-
goto,
|
|
78
|
-
};
|
|
79
|
-
}
|