@element-hq/element-web-playwright-common 3.0.0 → 4.0.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/Dockerfile CHANGED
@@ -6,10 +6,17 @@ ARG PLAYWRIGHT_VERSION
6
6
  WORKDIR /work
7
7
 
8
8
  # fonts-dejavu is needed for the same RTL rendering as on CI
9
- RUN apt-get update && apt-get -y install docker.io fonts-dejavu
9
+ RUN apt-get update && \
10
+ apt-get -y install docker.io fonts-dejavu && \
11
+ apt-get purge -y --auto-remove && \
12
+ rm -rf /var/lib/apt/lists/*
13
+
10
14
  # Install the matching playwright runtime, the docker image only includes browsers
11
15
  RUN npm i -g playwright@${PLAYWRIGHT_VERSION}
12
16
 
17
+ # switch to pwuser
18
+ USER 1001:1001
19
+
13
20
  COPY docker-entrypoint.sh /docker-entrypoint.sh
14
21
 
15
22
  # We use `docker-init` as PID 1, which means that the container shuts down correctly on SIGTERM.
@@ -0,0 +1,73 @@
1
+ import { type Locator, type Page } from "@playwright/test";
2
+ export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
3
+ axe: import("@axe-core/playwright").AxeBuilder;
4
+ } & import("./services.js").TestFixtures & {
5
+ /**
6
+ * Convenience functions for handling toasts.
7
+ */
8
+ toasts: Toasts;
9
+ }, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions & import("./services.js").WorkerOptions & import("./services.js").Services>;
10
+ declare class Toasts {
11
+ readonly page: Page;
12
+ constructor(page: Page);
13
+ /**
14
+ * Assert that no toasts exist.
15
+ */
16
+ assertNoToasts(): Promise<void>;
17
+ /**
18
+ * Return the toast with the supplied title. Fail or return null if it does
19
+ * not exist.
20
+ *
21
+ * If `required` is false, you should supply a relatively short `timeout`
22
+ * (e.g. 2000, meaning 2 seconds) to prevent your test taking too long.
23
+ *
24
+ * @param title - Expected title of the toast.
25
+ * @param timeout - Time in ms before we give up and decide the toast does
26
+ * not exist. If `required` is true, defaults to `timeout`
27
+ * in `TestConfig.expect`. Otherwise, defaults to 2000 (2
28
+ * seconds).
29
+ * @param required - If true, fail the test (throw an exception) if the
30
+ * toast is not visible. Otherwise, just return null if
31
+ * the toast is not visible.
32
+ * @returns the Locator for the matching toast, or null if it is not
33
+ * visible. (null will only be returned if `required` is false.)
34
+ */
35
+ getToast(title: string, timeout?: number, required?: true): Promise<Locator>;
36
+ getToast(title: string, timeout: number | undefined, required: false): Promise<Locator | null>;
37
+ /**
38
+ * Accept the toast with the supplied title, or fail if it does not exist.
39
+ *
40
+ * Only works if this toast is at the top of the stack of toasts.
41
+ *
42
+ * @param title - Expected title of the toast.
43
+ */
44
+ acceptToast(title: string): Promise<void>;
45
+ /**
46
+ * Accept the toast with the supplied title, if it exists, or return after 2
47
+ * seconds if it is not found.
48
+ *
49
+ * Only works if this toast is at the top of the stack of toasts.
50
+ *
51
+ * @param title - Expected title of the toast.
52
+ */
53
+ acceptToastIfExists(title: string): Promise<void>;
54
+ /**
55
+ * Reject the toast with the supplied title, or fail if it does not exist.
56
+ *
57
+ * Only works if this toast is at the top of the stack of toasts.
58
+ *
59
+ * @param title - Expected title of the toast.
60
+ */
61
+ rejectToast(title: string): Promise<void>;
62
+ /**
63
+ * Reject the toast with the supplied title, if it exists, or return after 2
64
+ * seconds if it is not found.
65
+ *
66
+ * Only works if this toast is at the top of the stack of toasts.
67
+ *
68
+ * @param title - Expected title of the toast.
69
+ */
70
+ rejectToastIfExists(title: string): Promise<void>;
71
+ }
72
+ export {};
73
+ //# sourceMappingURL=toasts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toasts.d.ts","sourceRoot":"","sources":["../../src/fixtures/toasts.ts"],"names":[],"mappings":"AAOA,OAAO,EAAU,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAQnE,eAAO,MAAM,IAAI;;;IACb;;OAEG;YACK,MAAM;iLAMhB,CAAC;AAEH,cAAM,MAAM;aAC2B,IAAI,EAAE,IAAI;gBAAV,IAAI,EAAE,IAAI;IAE7C;;OAEG;IACU,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5C;;;;;;;;;;;;;;;;;OAiBG;IACU,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAC5E,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAqB3G;;;;;;OAMG;IACU,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItD;;;;;;;OAOG;IACU,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9D;;;;;;OAMG;IACU,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItD;;;;;;;OAOG;IACU,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGjE"}
@@ -0,0 +1,124 @@
1
+ /*
2
+ * Copyright 2026 Element Creations Ltd.
3
+ *
4
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5
+ * Please see LICENSE files in the repository root for full details.
6
+ */
7
+ import { expect } from "@playwright/test";
8
+ // We want to avoid using `mergeTests` in index.ts because it drops useful type
9
+ // information about the fixtures. Instead, we add `services` into our fixture
10
+ // suite by using its `test` as a base, so that there is a linear hierarchy.
11
+ import { test as base } from "./services.js";
12
+ // This fixture provides convenient handling of Element Web's toasts.
13
+ export const test = base.extend({
14
+ toasts: async ({ page }, use) => {
15
+ const toasts = new Toasts(page);
16
+ await use(toasts);
17
+ },
18
+ });
19
+ class Toasts {
20
+ page;
21
+ constructor(page) {
22
+ this.page = page;
23
+ }
24
+ /**
25
+ * Assert that no toasts exist.
26
+ */
27
+ async assertNoToasts() {
28
+ await expect(this.page.locator(".mx_Toast_toast")).not.toBeVisible();
29
+ }
30
+ async getToast(title, timeout, required = true) {
31
+ const toast = this.page.locator(".mx_Toast_toast", { hasText: title }).first();
32
+ if (required) {
33
+ await expect(toast).toBeVisible({ timeout });
34
+ return toast;
35
+ }
36
+ else {
37
+ // If we don't set a timeout, waitFor will wait forever. Since
38
+ // required is false, we definitely don't want to wait forever.
39
+ timeout = timeout ?? 2000;
40
+ try {
41
+ await toast.waitFor({ state: "visible", timeout });
42
+ return toast;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Accept the toast with the supplied title, or fail if it does not exist.
51
+ *
52
+ * Only works if this toast is at the top of the stack of toasts.
53
+ *
54
+ * @param title - Expected title of the toast.
55
+ */
56
+ async acceptToast(title) {
57
+ return await clickToastButton(this, title, "primary");
58
+ }
59
+ /**
60
+ * Accept the toast with the supplied title, if it exists, or return after 2
61
+ * seconds if it is not found.
62
+ *
63
+ * Only works if this toast is at the top of the stack of toasts.
64
+ *
65
+ * @param title - Expected title of the toast.
66
+ */
67
+ async acceptToastIfExists(title) {
68
+ return await clickToastButton(this, title, "primary", 2000, false);
69
+ }
70
+ /**
71
+ * Reject the toast with the supplied title, or fail if it does not exist.
72
+ *
73
+ * Only works if this toast is at the top of the stack of toasts.
74
+ *
75
+ * @param title - Expected title of the toast.
76
+ */
77
+ async rejectToast(title) {
78
+ return await clickToastButton(this, title, "secondary");
79
+ }
80
+ /**
81
+ * Reject the toast with the supplied title, if it exists, or return after 2
82
+ * seconds if it is not found.
83
+ *
84
+ * Only works if this toast is at the top of the stack of toasts.
85
+ *
86
+ * @param title - Expected title of the toast.
87
+ */
88
+ async rejectToastIfExists(title) {
89
+ return await clickToastButton(this, title, "secondary", 2000, false);
90
+ }
91
+ }
92
+ /**
93
+ * Find the toast with the supplied title and click a button on it.
94
+ *
95
+ * Only works if this toast is at the top of the stack of toasts.
96
+ *
97
+ * If `required` is false, you should supply a relatively short `timeout`
98
+ * (e.g. 2000, meaning 2 seconds) to prevent your test taking too long.
99
+ *
100
+ * @param toasts - A Toasts instance.
101
+ * @param title - Expected title of the toast.
102
+ * @param button - Which button to click on the toast. Allowed values are
103
+ * "primary", which will accept the toast, or "secondary",
104
+ * which will reject it.
105
+ * @param timeout - Time in ms before we give up and decide the toast does
106
+ * not exist. If `required` is true, defaults to `timeout`
107
+ * in `TestConfig.expect`. Otherwise, defaults to 2000 (2
108
+ * seconds).
109
+ * @param required - If true, fail the test (throw an exception) if the
110
+ * toast is not visible. Otherwise, just return after
111
+ * `timeout` if the toast is not visible.
112
+ */
113
+ async function clickToastButton(toasts, title, button, timeout, required = true) {
114
+ let toast;
115
+ if (required) {
116
+ toast = await toasts.getToast(title, timeout, true);
117
+ }
118
+ else {
119
+ toast = await toasts.getToast(title, timeout, false);
120
+ }
121
+ if (toast) {
122
+ await toast.locator(`.mx_Toast_buttons button[data-kind="${button}"]`).click();
123
+ }
124
+ }
@@ -5,6 +5,17 @@ export declare function populateLocalStorageWithCredentials(page: Page, credenti
5
5
  export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
6
6
  axe: import("@axe-core/playwright").AxeBuilder;
7
7
  } & import("./services.js").TestFixtures & {
8
+ toasts: {
9
+ readonly page: Page;
10
+ assertNoToasts(): Promise<void>;
11
+ getToast(title: string, timeout?: number, required?: true): Promise<import("playwright-core").Locator>;
12
+ getToast(title: string, timeout: number | undefined, required: false): Promise<import("playwright-core").Locator | null>;
13
+ acceptToast(title: string): Promise<void>;
14
+ acceptToastIfExists(title: string): Promise<void>;
15
+ rejectToast(title: string): Promise<void>;
16
+ rejectToastIfExists(title: string): Promise<void>;
17
+ };
18
+ } & {
8
19
  /**
9
20
  * The displayname to use for the user registered in {@link #credentials}.
10
21
  *
@@ -1 +1 @@
1
- {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/fixtures/user.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAI7C,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,0IAA0I;AAC1I,wBAAsB,mCAAmC,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,iBAuB7F;AAED,eAAO,MAAM,IAAI;;;IACb;;;;;OAKG;kBACW,MAAM;IAEpB;;;OAGG;iBACU,WAAW;IAExB;;;;;;OAMG;yBACkB,IAAI;IAEzB;;;;OAIG;UACG,WAAW;iLA+BnB,CAAC"}
1
+ {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/fixtures/user.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAO7C,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,0IAA0I;AAC1I,wBAAsB,mCAAmC,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,iBAuB7F;AAED,eAAO,MAAM,IAAI;;;;;;;;;;;;;;IACb;;;;;OAKG;kBACW,MAAM;IAEpB;;;OAGG;iBACU,WAAW;IAExB;;;;;;OAMG;yBACkB,IAAI;IAEzB;;;;OAIG;UACG,WAAW;iLA+BnB,CAAC"}
@@ -6,7 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6
6
  Please see LICENSE files in the repository root for full details.
7
7
  */
8
8
  import { sample, uniqueId } from "lodash-es";
9
- import { test as base } from "./services.js";
9
+ // We want to avoid using `mergeTests` in index.ts because it drops useful type
10
+ // information about the fixtures. Instead, we add `toasts` into our fixture
11
+ // suite by using its `test` as a base, so that there is a linear hierarchy.
12
+ import { test as base } from "./toasts.js";
10
13
  /** Adds an initScript to the given page which will populate localStorage appropriately so that Element will use the given credentials. */
11
14
  export async function populateLocalStorageWithCredentials(page, credentials) {
12
15
  await page.addInitScript(({ credentials }) => {
package/lib/index.d.ts CHANGED
@@ -41,6 +41,17 @@ export interface TestFixtures {
41
41
  export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & {
42
42
  axe: import("@axe-core/playwright").AxeBuilder;
43
43
  } & import("./fixtures/services.js").TestFixtures & {
44
+ toasts: {
45
+ readonly page: import("playwright-core").Page;
46
+ assertNoToasts(): Promise<void>;
47
+ getToast(title: string, timeout?: number, required?: true): Promise<import("playwright-core").Locator>;
48
+ getToast(title: string, timeout: number | undefined, required: false): Promise<import("playwright-core").Locator | null>;
49
+ acceptToast(title: string): Promise<void>;
50
+ acceptToastIfExists(title: string): Promise<void>;
51
+ rejectToast(title: string): Promise<void>;
52
+ rejectToastIfExists(title: string): Promise<void>;
53
+ };
54
+ } & {
44
55
  displayName?: string;
45
56
  credentials: import("./utils/api.js").Credentials;
46
57
  pageWithCredentials: import("playwright-core").Page;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,MAAM,IAAI,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAK/E,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AAEnC,OAAO,EAAE,mCAAmC,EAAE,MAAM,oBAAoB,CAAC;AAQzE,MAAM,WAAW,MAAO,SAAQ,UAAU;IACtC,qBAAqB,EAAE;QACnB,cAAc,CAAC,EAAE;YACb,QAAQ,EAAE,MAAM,CAAC;YACjB,WAAW,CAAC,EAAE,MAAM,CAAC;SACxB,CAAC;QACF,mBAAmB,CAAC,EAAE;YAClB,QAAQ,EAAE,MAAM,CAAC;YACjB,WAAW,CAAC,EAAE,MAAM,CAAC;SACxB,CAAC;KACL,CAAC;IACF,yBAAyB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpD,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAGD,eAAO,MAAM,WAAW,EAAE,OAAO,CAAC,MAAM,CAevC,CAAC;AAEF,MAAM,WAAW,YAAY;IACzB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC;IAEpC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC/B;AAED,eAAO,MAAM,IAAI;;;;;;;kNAmBf,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,MAAM,IAAI,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAK/E,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AAEnC,OAAO,EAAE,mCAAmC,EAAE,MAAM,oBAAoB,CAAC;AAQzE,MAAM,WAAW,MAAO,SAAQ,UAAU;IACtC,qBAAqB,EAAE;QACnB,cAAc,CAAC,EAAE;YACb,QAAQ,EAAE,MAAM,CAAC;YACjB,WAAW,CAAC,EAAE,MAAM,CAAC;SACxB,CAAC;QACF,mBAAmB,CAAC,EAAE;YAClB,QAAQ,EAAE,MAAM,CAAC;YACjB,WAAW,CAAC,EAAE,MAAM,CAAC;SACxB,CAAC;KACL,CAAC;IACF,yBAAyB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpD,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAGD,eAAO,MAAM,WAAW,EAAE,OAAO,CAAC,MAAM,CAevC,CAAC;AAEF,MAAM,WAAW,YAAY;IACzB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC;IAEpC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC/B;AAED,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;kNAmBf,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@element-hq/element-web-playwright-common",
3
3
  "type": "module",
4
- "version": "3.0.0",
4
+ "version": "4.0.0",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "repository": {
7
7
  "type": "git",
@@ -19,13 +19,12 @@
19
19
  },
20
20
  "scripts": {
21
21
  "prepack": "nx build:playwright",
22
- "lint:types": "tsc --noEmit"
22
+ "lint:types": "nx lint:types"
23
23
  },
24
24
  "devDependencies": {
25
- "@element-hq/element-web-module-api": "*",
25
+ "@element-hq/element-web-module-api": "workspace:*",
26
26
  "@types/lodash-es": "^4.17.12",
27
- "typescript": "^5.8.2",
28
- "wait-on": "^9.0.4"
27
+ "typescript": "^6.0.0"
29
28
  },
30
29
  "dependencies": {
31
30
  "@axe-core/playwright": "^4.10.1",
@@ -35,6 +34,7 @@
35
34
  "mailpit-api": "^1.2.0",
36
35
  "strip-ansi": "^7.1.0",
37
36
  "testcontainers": "^11.0.0",
37
+ "wait-on": "^9.0.4",
38
38
  "yaml": "^2.7.0"
39
39
  },
40
40
  "peerDependencies": {
@@ -9,7 +9,7 @@ SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
9
9
  function build_image() {
10
10
  local IMAGE_NAME="$1"
11
11
 
12
- echo "Building $IMAGE_NAME image in $SCRIPT_DIR"
12
+ echo "playwright-screenshots: Building $IMAGE_NAME image in $SCRIPT_DIR"
13
13
  docker build -t "$IMAGE_NAME" --build-arg "PLAYWRIGHT_VERSION=${IMAGE_NAME#*:}" "$SCRIPT_DIR"
14
14
  }
15
15
 
@@ -34,16 +34,19 @@ CONTAINER=$(docker run --network=host -v /tmp:/tmp --rm -d -e PORT="$WS_PORT" "$
34
34
  # Set up an exit trap to clean up the docker container
35
35
  clean_up() {
36
36
  ARG=$?
37
- echo "Stopping playwright-server"
37
+ echo "playwright-screenshots: Stopping playwright-server"
38
38
  docker stop "$CONTAINER" > /dev/null
39
39
  exit $ARG
40
40
  }
41
41
  trap clean_up EXIT
42
42
 
43
43
  # Wait for playwright-server to be ready
44
- echo "Waiting for playwright-server"
44
+ echo "playwright-screenshots: Waiting for playwright-server"
45
45
  pnpm --dir "$SCRIPT_DIR" exec wait-on "tcp:$WS_PORT"
46
46
 
47
+ # Playwright seems to overwrite the last line from the console, so add an
48
+ # extra newline to make sure this doesn't get lost.
49
+ echo -e "playwright-screenshots: Running '$@'\n"
50
+
47
51
  # Run the test we were given, setting PW_TEST_CONNECT_WS_ENDPOINT accordingly
48
- echo "Running '$@'"
49
52
  PW_TEST_CONNECT_WS_ENDPOINT="http://localhost:$WS_PORT" "$@"
package/project.json CHANGED
@@ -8,7 +8,13 @@
8
8
  "command": "tsc",
9
9
  "inputs": ["src"],
10
10
  "outputs": ["{projectRoot}/lib"],
11
- "options": { "cwd": "packages/playwright-common" }
11
+ "options": { "cwd": "packages/playwright-common" },
12
+ "dependsOn": ["^build"]
13
+ },
14
+ "lint:types": {
15
+ "command": "pnpm exec tsc --noEmit",
16
+ "options": { "cwd": "packages/playwright-common" },
17
+ "dependsOn": ["^build"]
12
18
  },
13
19
  "docker:prebuild": {
14
20
  "cache": true,
@@ -0,0 +1,163 @@
1
+ /*
2
+ * Copyright 2026 Element Creations Ltd.
3
+ *
4
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5
+ * Please see LICENSE files in the repository root for full details.
6
+ */
7
+
8
+ import { expect, type Locator, type Page } from "@playwright/test";
9
+
10
+ // We want to avoid using `mergeTests` in index.ts because it drops useful type
11
+ // information about the fixtures. Instead, we add `services` into our fixture
12
+ // suite by using its `test` as a base, so that there is a linear hierarchy.
13
+ import { test as base } from "./services.js";
14
+
15
+ // This fixture provides convenient handling of Element Web's toasts.
16
+ export const test = base.extend<{
17
+ /**
18
+ * Convenience functions for handling toasts.
19
+ */
20
+ toasts: Toasts;
21
+ }>({
22
+ toasts: async ({ page }, use) => {
23
+ const toasts = new Toasts(page);
24
+ await use(toasts);
25
+ },
26
+ });
27
+
28
+ class Toasts {
29
+ public constructor(public readonly page: Page) {}
30
+
31
+ /**
32
+ * Assert that no toasts exist.
33
+ */
34
+ public async assertNoToasts(): Promise<void> {
35
+ await expect(this.page.locator(".mx_Toast_toast")).not.toBeVisible();
36
+ }
37
+
38
+ /**
39
+ * Return the toast with the supplied title. Fail or return null if it does
40
+ * not exist.
41
+ *
42
+ * If `required` is false, you should supply a relatively short `timeout`
43
+ * (e.g. 2000, meaning 2 seconds) to prevent your test taking too long.
44
+ *
45
+ * @param title - Expected title of the toast.
46
+ * @param timeout - Time in ms before we give up and decide the toast does
47
+ * not exist. If `required` is true, defaults to `timeout`
48
+ * in `TestConfig.expect`. Otherwise, defaults to 2000 (2
49
+ * seconds).
50
+ * @param required - If true, fail the test (throw an exception) if the
51
+ * toast is not visible. Otherwise, just return null if
52
+ * the toast is not visible.
53
+ * @returns the Locator for the matching toast, or null if it is not
54
+ * visible. (null will only be returned if `required` is false.)
55
+ */
56
+ public async getToast(title: string, timeout?: number, required?: true): Promise<Locator>;
57
+ public async getToast(title: string, timeout: number | undefined, required: false): Promise<Locator | null>;
58
+ public async getToast(title: string, timeout?: number, required = true): Promise<Locator | null> {
59
+ const toast = this.page.locator(".mx_Toast_toast", { hasText: title }).first();
60
+
61
+ if (required) {
62
+ await expect(toast).toBeVisible({ timeout });
63
+ return toast;
64
+ } else {
65
+ // If we don't set a timeout, waitFor will wait forever. Since
66
+ // required is false, we definitely don't want to wait forever.
67
+ timeout = timeout ?? 2000;
68
+
69
+ try {
70
+ await toast.waitFor({ state: "visible", timeout });
71
+ return toast;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Accept the toast with the supplied title, or fail if it does not exist.
80
+ *
81
+ * Only works if this toast is at the top of the stack of toasts.
82
+ *
83
+ * @param title - Expected title of the toast.
84
+ */
85
+ public async acceptToast(title: string): Promise<void> {
86
+ return await clickToastButton(this, title, "primary");
87
+ }
88
+
89
+ /**
90
+ * Accept the toast with the supplied title, if it exists, or return after 2
91
+ * seconds if it is not found.
92
+ *
93
+ * Only works if this toast is at the top of the stack of toasts.
94
+ *
95
+ * @param title - Expected title of the toast.
96
+ */
97
+ public async acceptToastIfExists(title: string): Promise<void> {
98
+ return await clickToastButton(this, title, "primary", 2000, false);
99
+ }
100
+
101
+ /**
102
+ * Reject the toast with the supplied title, or fail if it does not exist.
103
+ *
104
+ * Only works if this toast is at the top of the stack of toasts.
105
+ *
106
+ * @param title - Expected title of the toast.
107
+ */
108
+ public async rejectToast(title: string): Promise<void> {
109
+ return await clickToastButton(this, title, "secondary");
110
+ }
111
+
112
+ /**
113
+ * Reject the toast with the supplied title, if it exists, or return after 2
114
+ * seconds if it is not found.
115
+ *
116
+ * Only works if this toast is at the top of the stack of toasts.
117
+ *
118
+ * @param title - Expected title of the toast.
119
+ */
120
+ public async rejectToastIfExists(title: string): Promise<void> {
121
+ return await clickToastButton(this, title, "secondary", 2000, false);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Find the toast with the supplied title and click a button on it.
127
+ *
128
+ * Only works if this toast is at the top of the stack of toasts.
129
+ *
130
+ * If `required` is false, you should supply a relatively short `timeout`
131
+ * (e.g. 2000, meaning 2 seconds) to prevent your test taking too long.
132
+ *
133
+ * @param toasts - A Toasts instance.
134
+ * @param title - Expected title of the toast.
135
+ * @param button - Which button to click on the toast. Allowed values are
136
+ * "primary", which will accept the toast, or "secondary",
137
+ * which will reject it.
138
+ * @param timeout - Time in ms before we give up and decide the toast does
139
+ * not exist. If `required` is true, defaults to `timeout`
140
+ * in `TestConfig.expect`. Otherwise, defaults to 2000 (2
141
+ * seconds).
142
+ * @param required - If true, fail the test (throw an exception) if the
143
+ * toast is not visible. Otherwise, just return after
144
+ * `timeout` if the toast is not visible.
145
+ */
146
+ async function clickToastButton(
147
+ toasts: Toasts,
148
+ title: string,
149
+ button: "primary" | "secondary",
150
+ timeout?: number,
151
+ required = true,
152
+ ): Promise<void> {
153
+ let toast: Locator | null;
154
+ if (required) {
155
+ toast = await toasts.getToast(title, timeout, true);
156
+ } else {
157
+ toast = await toasts.getToast(title, timeout, false);
158
+ }
159
+
160
+ if (toast) {
161
+ await toast.locator(`.mx_Toast_buttons button[data-kind="${button}"]`).click();
162
+ }
163
+ }
@@ -9,7 +9,10 @@ Please see LICENSE files in the repository root for full details.
9
9
  import { type Page } from "@playwright/test";
10
10
  import { sample, uniqueId } from "lodash-es";
11
11
 
12
- import { test as base } from "./services.js";
12
+ // We want to avoid using `mergeTests` in index.ts because it drops useful type
13
+ // information about the fixtures. Instead, we add `toasts` into our fixture
14
+ // suite by using its `test` as a base, so that there is a linear hierarchy.
15
+ import { test as base } from "./toasts.js";
13
16
  import { type Credentials } from "../utils/api.js";
14
17
 
15
18
  /** Adds an initScript to the given page which will populate localStorage appropriately so that Element will use the given credentials. */
package/tsconfig.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json.schemastore.org/tsconfig",
3
3
  "compilerOptions": {
4
+ "rootDir": "./src",
4
5
  "target": "esnext",
5
6
  "lib": ["dom", "es2022", "esnext"],
6
7
  "esModuleInterop": true,