@lullabot/playwright-testing 1.2.1 → 1.3.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 +11 -4
- package/lib/ckeditor5.d.ts +58 -0
- package/lib/ckeditor5.js +82 -0
- package/lib/details.d.ts +6 -0
- package/lib/details.js +14 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/package.json +1 -1
- package/src/ckeditor5.test.ts +20 -0
- package/src/ckeditor5.ts +84 -0
- package/src/details.ts +13 -0
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# `@lullabot/playwright-testing`
|
|
2
2
|
|
|
3
3
|
Framework-neutral utilities for stable Playwright screenshots, accessibility
|
|
4
|
-
baselines, URL-driven visual comparisons,
|
|
5
|
-
GitHub reporting.
|
|
4
|
+
baselines, URL-driven visual comparisons, CKEditor 5 editing, WebKit autofocus
|
|
5
|
+
stabilization, and GitHub reporting.
|
|
6
6
|
|
|
7
7
|
This package is developed in the
|
|
8
8
|
[`playwright-drupal` monorepo](https://github.com/Lullabot/playwright-drupal),
|
|
@@ -25,8 +25,9 @@ version.
|
|
|
25
25
|
|
|
26
26
|
- `@lullabot/playwright-testing` exports screenshot stabilization,
|
|
27
27
|
accessibility checks and baselines, visual-diff definitions, interaction and
|
|
28
|
-
pseudo-state helpers,
|
|
29
|
-
script (installed explicitly in WebKit
|
|
28
|
+
pseudo-state helpers, `openAllDetails()`, CKEditor 5 editing, reusable mocks,
|
|
29
|
+
and the `suppressWebKitAutofocus()` init script (installed explicitly in WebKit
|
|
30
|
+
contexts).
|
|
30
31
|
- `@lullabot/playwright-testing/github` exports the optional GitHub report,
|
|
31
32
|
attachment-upload, and path-remapping APIs.
|
|
32
33
|
- `playwright-testing-a11y-summary` and
|
|
@@ -51,6 +52,8 @@ test('home page', async ({ page }, testInfo) => {
|
|
|
51
52
|
|
|
52
53
|
- [Accessibility testing][testing-accessibility]
|
|
53
54
|
- [Stable screenshots and visual comparisons][testing-screenshots]
|
|
55
|
+
- [Page readiness and browser state][testing-page-readiness]
|
|
56
|
+
- [CKEditor 5 editing][testing-ckeditor5]
|
|
54
57
|
- [WebKit native autofocus workaround][testing-webkit-autofocus]
|
|
55
58
|
- [GitHub reporting][testing-github-reporting]
|
|
56
59
|
|
|
@@ -67,3 +70,7 @@ package.
|
|
|
67
70
|
[testing-screenshots]: https://github.com/Lullabot/playwright-drupal/blob/main/packages/playwright-testing/docs/screenshots-and-visual-comparisons.md
|
|
68
71
|
[testing-github-reporting]: https://github.com/Lullabot/playwright-drupal/blob/main/packages/playwright-testing/docs/github-reporting.md
|
|
69
72
|
[testing-webkit-autofocus]: https://github.com/Lullabot/playwright-drupal/blob/main/packages/playwright-testing/docs/webkit-autofocus.md
|
|
73
|
+
|
|
74
|
+
[testing-page-readiness]: https://github.com/Lullabot/playwright-drupal/blob/main/packages/playwright-testing/docs/page-readiness.md
|
|
75
|
+
|
|
76
|
+
[testing-ckeditor5]: https://github.com/Lullabot/playwright-drupal/blob/main/packages/playwright-testing/docs/ckeditor5.md
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { FrameLocator, Page } from "@playwright/test";
|
|
2
|
+
/**
|
|
3
|
+
* Return the select-all modifier key for a given platform.
|
|
4
|
+
*
|
|
5
|
+
* Exported so unit tests can cover the platform branch without mocking
|
|
6
|
+
* `process.platform`. Defaults to the current platform.
|
|
7
|
+
*/
|
|
8
|
+
export declare function selectAllModifier(platform?: NodeJS.Platform): "Meta" | "Control";
|
|
9
|
+
/**
|
|
10
|
+
* Drive a CKEditor **5** field from a Playwright test.
|
|
11
|
+
*
|
|
12
|
+
* This class is specifically for CKEditor 5. It does **not** work with
|
|
13
|
+
* CKEditor 4.
|
|
14
|
+
*
|
|
15
|
+
* CKEditor 5 keeps a virtual-DOM model that it synchronises with the visible
|
|
16
|
+
* contenteditable element. Setting the DOM directly (e.g. via Playwright's
|
|
17
|
+
* `locator.fill()`) can be silently dropped because the editor's next
|
|
18
|
+
* re-render overwrites it; it also bypasses any input handlers CKEditor
|
|
19
|
+
* plugins register. `fill()` here dispatches real keyboard events through
|
|
20
|
+
* `page.keyboard`, which CKEditor's event pipeline processes as normal
|
|
21
|
+
* edits.
|
|
22
|
+
*
|
|
23
|
+
* The `selector` targets the widget **wrapper** (e.g.
|
|
24
|
+
* `#body-editor` or `[data-testid="body-editor"]`),
|
|
25
|
+
* not the contenteditable itself — the class drills into
|
|
26
|
+
* `.ck-editor__editable` internally so callers don't have to memorise
|
|
27
|
+
* CKEditor 5's markup.
|
|
28
|
+
*
|
|
29
|
+
* For editors rendered inside an iframe, pass the `FrameLocator` as `root`
|
|
30
|
+
* while keeping `page` as the owning page so keyboard events still reach the
|
|
31
|
+
* right window.
|
|
32
|
+
*/
|
|
33
|
+
export declare class Ckeditor5 {
|
|
34
|
+
page: Page;
|
|
35
|
+
root: Page | FrameLocator;
|
|
36
|
+
protected selector: string;
|
|
37
|
+
/**
|
|
38
|
+
* @param page
|
|
39
|
+
* The page the CKEditor 5 instance lives on. Keyboard events are sent to
|
|
40
|
+
* this page.
|
|
41
|
+
* @param selector
|
|
42
|
+
* A selector that resolves to the widget wrapper containing the editor
|
|
43
|
+
* (e.g. `#body-editor`). The class finds the
|
|
44
|
+
* `.ck-editor__editable` element inside.
|
|
45
|
+
* @param root
|
|
46
|
+
* Optional frame locator if the editor is inside an iframe. Defaults to
|
|
47
|
+
* `page`.
|
|
48
|
+
*/
|
|
49
|
+
constructor(page: Page, selector: string, root?: Page | FrameLocator);
|
|
50
|
+
/**
|
|
51
|
+
* Replace the editor's contents with the given text.
|
|
52
|
+
*
|
|
53
|
+
* Clears existing content (select-all + Backspace) so the call has
|
|
54
|
+
* Playwright-style `fill()` semantics: the final value is exactly `text`,
|
|
55
|
+
* regardless of whether the field was empty.
|
|
56
|
+
*/
|
|
57
|
+
fill(text: string): Promise<void>;
|
|
58
|
+
}
|
package/lib/ckeditor5.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Ckeditor5 = void 0;
|
|
4
|
+
exports.selectAllModifier = selectAllModifier;
|
|
5
|
+
/**
|
|
6
|
+
* Return the select-all modifier key for a given platform.
|
|
7
|
+
*
|
|
8
|
+
* Exported so unit tests can cover the platform branch without mocking
|
|
9
|
+
* `process.platform`. Defaults to the current platform.
|
|
10
|
+
*/
|
|
11
|
+
function selectAllModifier(platform = process.platform) {
|
|
12
|
+
return platform === "darwin" ? "Meta" : "Control";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Drive a CKEditor **5** field from a Playwright test.
|
|
16
|
+
*
|
|
17
|
+
* This class is specifically for CKEditor 5. It does **not** work with
|
|
18
|
+
* CKEditor 4.
|
|
19
|
+
*
|
|
20
|
+
* CKEditor 5 keeps a virtual-DOM model that it synchronises with the visible
|
|
21
|
+
* contenteditable element. Setting the DOM directly (e.g. via Playwright's
|
|
22
|
+
* `locator.fill()`) can be silently dropped because the editor's next
|
|
23
|
+
* re-render overwrites it; it also bypasses any input handlers CKEditor
|
|
24
|
+
* plugins register. `fill()` here dispatches real keyboard events through
|
|
25
|
+
* `page.keyboard`, which CKEditor's event pipeline processes as normal
|
|
26
|
+
* edits.
|
|
27
|
+
*
|
|
28
|
+
* The `selector` targets the widget **wrapper** (e.g.
|
|
29
|
+
* `#body-editor` or `[data-testid="body-editor"]`),
|
|
30
|
+
* not the contenteditable itself — the class drills into
|
|
31
|
+
* `.ck-editor__editable` internally so callers don't have to memorise
|
|
32
|
+
* CKEditor 5's markup.
|
|
33
|
+
*
|
|
34
|
+
* For editors rendered inside an iframe, pass the `FrameLocator` as `root`
|
|
35
|
+
* while keeping `page` as the owning page so keyboard events still reach the
|
|
36
|
+
* right window.
|
|
37
|
+
*/
|
|
38
|
+
class Ckeditor5 {
|
|
39
|
+
page;
|
|
40
|
+
root;
|
|
41
|
+
selector;
|
|
42
|
+
/**
|
|
43
|
+
* @param page
|
|
44
|
+
* The page the CKEditor 5 instance lives on. Keyboard events are sent to
|
|
45
|
+
* this page.
|
|
46
|
+
* @param selector
|
|
47
|
+
* A selector that resolves to the widget wrapper containing the editor
|
|
48
|
+
* (e.g. `#body-editor`). The class finds the
|
|
49
|
+
* `.ck-editor__editable` element inside.
|
|
50
|
+
* @param root
|
|
51
|
+
* Optional frame locator if the editor is inside an iframe. Defaults to
|
|
52
|
+
* `page`.
|
|
53
|
+
*/
|
|
54
|
+
constructor(page, selector, root) {
|
|
55
|
+
this.page = page;
|
|
56
|
+
this.selector = selector;
|
|
57
|
+
this.root = root ?? page;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Replace the editor's contents with the given text.
|
|
61
|
+
*
|
|
62
|
+
* Clears existing content (select-all + Backspace) so the call has
|
|
63
|
+
* Playwright-style `fill()` semantics: the final value is exactly `text`,
|
|
64
|
+
* regardless of whether the field was empty.
|
|
65
|
+
*/
|
|
66
|
+
async fill(text) {
|
|
67
|
+
const editable = this.root
|
|
68
|
+
.locator(this.selector)
|
|
69
|
+
.locator(".ck-editor__editable");
|
|
70
|
+
await editable.waitFor({ state: "visible", timeout: 15000 });
|
|
71
|
+
// Click places the caret inside the editable so the keyboard events
|
|
72
|
+
// below land in CKEditor rather than the outer document.
|
|
73
|
+
await editable.click();
|
|
74
|
+
await this.page.keyboard.press(`${selectAllModifier()}+A`);
|
|
75
|
+
await this.page.keyboard.press("Backspace");
|
|
76
|
+
// keyboard.type fires keydown/keypress/input events that CKEditor 5's
|
|
77
|
+
// event pipeline processes. locator.fill() would set the DOM directly
|
|
78
|
+
// and can be silently dropped on the next model re-render.
|
|
79
|
+
await this.page.keyboard.type(text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.Ckeditor5 = Ckeditor5;
|
package/lib/details.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
|
+
/**
|
|
3
|
+
* Expand every collapsed `<details>` element on the page so nested controls
|
|
4
|
+
* become interactable. Call after navigation and before filling nested fields.
|
|
5
|
+
*/
|
|
6
|
+
export declare function openAllDetails(page: Page): Promise<void>;
|
package/lib/details.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openAllDetails = openAllDetails;
|
|
4
|
+
/**
|
|
5
|
+
* Expand every collapsed `<details>` element on the page so nested controls
|
|
6
|
+
* become interactable. Call after navigation and before filling nested fields.
|
|
7
|
+
*/
|
|
8
|
+
async function openAllDetails(page) {
|
|
9
|
+
await page.evaluate(() => {
|
|
10
|
+
document.querySelectorAll("details:not([open])").forEach((d) => {
|
|
11
|
+
d.open = true;
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./details.js";
|
|
1
2
|
export * from "./focus.js";
|
|
2
3
|
export * from "./accessibility-baseline.js";
|
|
3
4
|
export * from "./accessibility-baseline-file.js";
|
|
@@ -12,3 +13,4 @@ export * from "./videos.js";
|
|
|
12
13
|
export * from "./visualdiff.js";
|
|
13
14
|
export * from "./mock/index.js";
|
|
14
15
|
export * from "./webkit-autofocus.js";
|
|
16
|
+
export * from "./ckeditor5.js";
|
package/lib/index.js
CHANGED
|
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./details.js"), exports);
|
|
17
18
|
__exportStar(require("./focus.js"), exports);
|
|
18
19
|
__exportStar(require("./accessibility-baseline.js"), exports);
|
|
19
20
|
__exportStar(require("./accessibility-baseline-file.js"), exports);
|
|
@@ -28,3 +29,4 @@ __exportStar(require("./videos.js"), exports);
|
|
|
28
29
|
__exportStar(require("./visualdiff.js"), exports);
|
|
29
30
|
__exportStar(require("./mock/index.js"), exports);
|
|
30
31
|
__exportStar(require("./webkit-autofocus.js"), exports);
|
|
32
|
+
__exportStar(require("./ckeditor5.js"), exports);
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { selectAllModifier } from "./ckeditor5";
|
|
3
|
+
|
|
4
|
+
describe("selectAllModifier", () => {
|
|
5
|
+
it("returns Meta on darwin", () => {
|
|
6
|
+
expect(selectAllModifier("darwin")).toBe("Meta");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns Control on linux", () => {
|
|
10
|
+
expect(selectAllModifier("linux")).toBe("Control");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns Control on win32", () => {
|
|
14
|
+
expect(selectAllModifier("win32")).toBe("Control");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns Control on freebsd / other", () => {
|
|
18
|
+
expect(selectAllModifier("freebsd")).toBe("Control");
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/ckeditor5.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { FrameLocator, Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return the select-all modifier key for a given platform.
|
|
5
|
+
*
|
|
6
|
+
* Exported so unit tests can cover the platform branch without mocking
|
|
7
|
+
* `process.platform`. Defaults to the current platform.
|
|
8
|
+
*/
|
|
9
|
+
export function selectAllModifier(
|
|
10
|
+
platform: NodeJS.Platform = process.platform,
|
|
11
|
+
): "Meta" | "Control" {
|
|
12
|
+
return platform === "darwin" ? "Meta" : "Control";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Drive a CKEditor **5** field from a Playwright test.
|
|
17
|
+
*
|
|
18
|
+
* This class is specifically for CKEditor 5. It does **not** work with
|
|
19
|
+
* CKEditor 4.
|
|
20
|
+
*
|
|
21
|
+
* CKEditor 5 keeps a virtual-DOM model that it synchronises with the visible
|
|
22
|
+
* contenteditable element. Setting the DOM directly (e.g. via Playwright's
|
|
23
|
+
* `locator.fill()`) can be silently dropped because the editor's next
|
|
24
|
+
* re-render overwrites it; it also bypasses any input handlers CKEditor
|
|
25
|
+
* plugins register. `fill()` here dispatches real keyboard events through
|
|
26
|
+
* `page.keyboard`, which CKEditor's event pipeline processes as normal
|
|
27
|
+
* edits.
|
|
28
|
+
*
|
|
29
|
+
* The `selector` targets the widget **wrapper** (e.g.
|
|
30
|
+
* `#body-editor` or `[data-testid="body-editor"]`),
|
|
31
|
+
* not the contenteditable itself — the class drills into
|
|
32
|
+
* `.ck-editor__editable` internally so callers don't have to memorise
|
|
33
|
+
* CKEditor 5's markup.
|
|
34
|
+
*
|
|
35
|
+
* For editors rendered inside an iframe, pass the `FrameLocator` as `root`
|
|
36
|
+
* while keeping `page` as the owning page so keyboard events still reach the
|
|
37
|
+
* right window.
|
|
38
|
+
*/
|
|
39
|
+
export class Ckeditor5 {
|
|
40
|
+
public page: Page;
|
|
41
|
+
public root: Page | FrameLocator;
|
|
42
|
+
protected selector: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param page
|
|
46
|
+
* The page the CKEditor 5 instance lives on. Keyboard events are sent to
|
|
47
|
+
* this page.
|
|
48
|
+
* @param selector
|
|
49
|
+
* A selector that resolves to the widget wrapper containing the editor
|
|
50
|
+
* (e.g. `#body-editor`). The class finds the
|
|
51
|
+
* `.ck-editor__editable` element inside.
|
|
52
|
+
* @param root
|
|
53
|
+
* Optional frame locator if the editor is inside an iframe. Defaults to
|
|
54
|
+
* `page`.
|
|
55
|
+
*/
|
|
56
|
+
public constructor(page: Page, selector: string, root?: Page | FrameLocator) {
|
|
57
|
+
this.page = page;
|
|
58
|
+
this.selector = selector;
|
|
59
|
+
this.root = root ?? page;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Replace the editor's contents with the given text.
|
|
64
|
+
*
|
|
65
|
+
* Clears existing content (select-all + Backspace) so the call has
|
|
66
|
+
* Playwright-style `fill()` semantics: the final value is exactly `text`,
|
|
67
|
+
* regardless of whether the field was empty.
|
|
68
|
+
*/
|
|
69
|
+
public async fill(text: string): Promise<void> {
|
|
70
|
+
const editable = this.root
|
|
71
|
+
.locator(this.selector)
|
|
72
|
+
.locator(".ck-editor__editable");
|
|
73
|
+
await editable.waitFor({ state: "visible", timeout: 15000 });
|
|
74
|
+
// Click places the caret inside the editable so the keyboard events
|
|
75
|
+
// below land in CKEditor rather than the outer document.
|
|
76
|
+
await editable.click();
|
|
77
|
+
await this.page.keyboard.press(`${selectAllModifier()}+A`);
|
|
78
|
+
await this.page.keyboard.press("Backspace");
|
|
79
|
+
// keyboard.type fires keydown/keypress/input events that CKEditor 5's
|
|
80
|
+
// event pipeline processes. locator.fill() would set the DOM directly
|
|
81
|
+
// and can be silently dropped on the next model re-render.
|
|
82
|
+
await this.page.keyboard.type(text);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/details.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expand every collapsed `<details>` element on the page so nested controls
|
|
5
|
+
* become interactable. Call after navigation and before filling nested fields.
|
|
6
|
+
*/
|
|
7
|
+
export async function openAllDetails(page: Page): Promise<void> {
|
|
8
|
+
await page.evaluate(() => {
|
|
9
|
+
document.querySelectorAll("details:not([open])").forEach((d) => {
|
|
10
|
+
(d as HTMLDetailsElement).open = true;
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./details.js";
|
|
1
2
|
export * from "./focus.js";
|
|
2
3
|
export * from "./accessibility-baseline.js";
|
|
3
4
|
export * from "./accessibility-baseline-file.js";
|
|
@@ -12,3 +13,4 @@ export * from "./videos.js";
|
|
|
12
13
|
export * from "./visualdiff.js";
|
|
13
14
|
export * from "./mock/index.js";
|
|
14
15
|
export * from "./webkit-autofocus.js";
|
|
16
|
+
export * from "./ckeditor5.js";
|