@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 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, WebKit autofocus stabilization, and
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, reusable mocks, and the `suppressWebKitAutofocus()` init
29
- script (installed explicitly in WebKit contexts).
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
+ }
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lullabot/playwright-testing",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Framework-neutral Playwright testing utilities",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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
+ });
@@ -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";