walkwright 0.0.1

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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/bin/walkwright.js +3 -0
  4. package/build/aria.d.ts +92 -0
  5. package/build/aria.js +215 -0
  6. package/build/aria.js.map +1 -0
  7. package/build/browser.d.ts +3 -0
  8. package/build/browser.js +18 -0
  9. package/build/browser.js.map +1 -0
  10. package/build/chunk-5CHKYNP7.js +507 -0
  11. package/build/chunk-5CHKYNP7.js.map +1 -0
  12. package/build/chunk-6YRKPGUO.js +1197 -0
  13. package/build/chunk-6YRKPGUO.js.map +1 -0
  14. package/build/chunk-A452S7R3.js +35 -0
  15. package/build/chunk-A452S7R3.js.map +1 -0
  16. package/build/chunk-BODO6HIH.js +60 -0
  17. package/build/chunk-BODO6HIH.js.map +1 -0
  18. package/build/chunk-FJI2CMCE.js +439 -0
  19. package/build/chunk-FJI2CMCE.js.map +1 -0
  20. package/build/chunk-JYHZVRB2.js +80 -0
  21. package/build/chunk-JYHZVRB2.js.map +1 -0
  22. package/build/chunk-KQN5IYN6.js +97 -0
  23. package/build/chunk-KQN5IYN6.js.map +1 -0
  24. package/build/chunk-KTXNERHT.js +72 -0
  25. package/build/chunk-KTXNERHT.js.map +1 -0
  26. package/build/chunk-MFTC7XL2.js +141 -0
  27. package/build/chunk-MFTC7XL2.js.map +1 -0
  28. package/build/chunk-N4SSLHMP.js +108 -0
  29. package/build/chunk-N4SSLHMP.js.map +1 -0
  30. package/build/chunk-RNNW7OW2.js +270 -0
  31. package/build/chunk-RNNW7OW2.js.map +1 -0
  32. package/build/chunk-UAWBPTDW.js +51 -0
  33. package/build/chunk-UAWBPTDW.js.map +1 -0
  34. package/build/chunk-Z7CW5CAY.js +64 -0
  35. package/build/chunk-Z7CW5CAY.js.map +1 -0
  36. package/build/cli.d.ts +1 -0
  37. package/build/cli.js +253 -0
  38. package/build/cli.js.map +1 -0
  39. package/build/config.d.ts +13 -0
  40. package/build/config.js +15 -0
  41. package/build/config.js.map +1 -0
  42. package/build/descriptor.d.ts +96 -0
  43. package/build/descriptor.js +27 -0
  44. package/build/descriptor.js.map +1 -0
  45. package/build/device-auth.d.ts +6 -0
  46. package/build/device-auth.js +11 -0
  47. package/build/device-auth.js.map +1 -0
  48. package/build/locator.d.ts +140 -0
  49. package/build/locator.js +92 -0
  50. package/build/locator.js.map +1 -0
  51. package/build/paths.d.ts +11 -0
  52. package/build/paths.js +22 -0
  53. package/build/paths.js.map +1 -0
  54. package/build/pom-kcSMxwX2.d.ts +349 -0
  55. package/build/pom.d.ts +6 -0
  56. package/build/pom.js +37 -0
  57. package/build/pom.js.map +1 -0
  58. package/build/project.d.ts +13 -0
  59. package/build/project.js +19 -0
  60. package/build/project.js.map +1 -0
  61. package/build/provisioner.d.ts +34 -0
  62. package/build/provisioner.js +14 -0
  63. package/build/provisioner.js.map +1 -0
  64. package/build/recorder.d.ts +6 -0
  65. package/build/recorder.js +15 -0
  66. package/build/recorder.js.map +1 -0
  67. package/build/reporter.d.ts +16 -0
  68. package/build/reporter.js +426 -0
  69. package/build/reporter.js.map +1 -0
  70. package/build/schema.d.ts +49 -0
  71. package/build/schema.js +16 -0
  72. package/build/schema.js.map +1 -0
  73. package/build/storage-state.d.ts +22 -0
  74. package/build/storage-state.js +42 -0
  75. package/build/storage-state.js.map +1 -0
  76. package/build/store.d.ts +17 -0
  77. package/build/store.js +9 -0
  78. package/build/store.js.map +1 -0
  79. package/build/ui.d.ts +49 -0
  80. package/build/ui.js +56 -0
  81. package/build/ui.js.map +1 -0
  82. package/build/upload.d.ts +10 -0
  83. package/build/upload.js +8 -0
  84. package/build/upload.js.map +1 -0
  85. package/build/wait.d.ts +14 -0
  86. package/build/wait.js +50 -0
  87. package/build/wait.js.map +1 -0
  88. package/package.json +52 -0
  89. package/src/aria.ts +326 -0
  90. package/src/browser.ts +15 -0
  91. package/src/cli.ts +295 -0
  92. package/src/config.ts +16 -0
  93. package/src/descriptor.ts +179 -0
  94. package/src/device-auth.ts +188 -0
  95. package/src/locator.ts +683 -0
  96. package/src/paths.ts +70 -0
  97. package/src/pom.ts +2363 -0
  98. package/src/project.ts +77 -0
  99. package/src/provisioner.ts +183 -0
  100. package/src/recorder.ts +877 -0
  101. package/src/reporter.ts +561 -0
  102. package/src/schema.ts +37 -0
  103. package/src/storage-state.ts +71 -0
  104. package/src/store.ts +96 -0
  105. package/src/ui.ts +333 -0
  106. package/src/upload.ts +116 -0
  107. package/src/wait.ts +63 -0
package/build/ui.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ type Tone = "accent" | "text" | "dim" | "faint" | "ok" | "warn" | "fail" | "code" | "border";
2
+ declare const glyph: {
3
+ readonly ok: "✓";
4
+ readonly fail: "✗";
5
+ readonly carried: "↺";
6
+ readonly unseen: "●";
7
+ readonly open: "▾";
8
+ readonly closed: "▸";
9
+ readonly branch: "├";
10
+ readonly last: "└";
11
+ readonly cursor: "▌";
12
+ readonly prompt: ">";
13
+ readonly filled: "▰";
14
+ readonly empty: "▱";
15
+ readonly dot: "·";
16
+ readonly bar: "│";
17
+ };
18
+ declare function paint(tone: Tone, value: string): string;
19
+ declare function bold(value: string): string;
20
+ declare function plain(value: string): string;
21
+ declare function width(value: string): number;
22
+ declare function columns(): number;
23
+ declare function pad(value: string, to: number): string;
24
+ declare function clip(value: string, to: number): string;
25
+ declare function line(...parts: string[]): void;
26
+ declare function blank(): void;
27
+ declare function status(...segments: string[]): string;
28
+ declare function badge(count: number, tone?: Tone): string;
29
+ declare function key(name: string): string;
30
+ declare function action(verb: string): string;
31
+ declare function prompt(text: string): string;
32
+ declare function dashed(inner: number): string;
33
+ interface BoxOptions {
34
+ legend: string;
35
+ title?: string;
36
+ primary?: boolean;
37
+ min?: number;
38
+ }
39
+ declare function box(rows: string[], options: BoxOptions): string[];
40
+ declare function progress(done: number, total: number, cells?: number): string;
41
+ declare function duration(ms: number): string;
42
+ declare function count(value: number, noun: string): string;
43
+ declare function failure(message: string, hint?: string): void;
44
+ declare function transient(text: string): () => void;
45
+ declare function pulse(text: string, intervalMs?: number): () => void;
46
+ declare function wordmark(): string;
47
+ declare function logo(): string[];
48
+
49
+ export { type BoxOptions, type Tone, action, badge, blank, bold, box, clip, columns, count, dashed, duration, failure, glyph, key, line, logo, pad, paint, plain, progress, prompt, pulse, status, transient, width, wordmark };
package/build/ui.js ADDED
@@ -0,0 +1,56 @@
1
+ import {
2
+ action,
3
+ badge,
4
+ blank,
5
+ bold,
6
+ box,
7
+ clip,
8
+ columns,
9
+ count,
10
+ dashed,
11
+ duration,
12
+ failure,
13
+ glyph,
14
+ key,
15
+ line,
16
+ logo,
17
+ pad,
18
+ paint,
19
+ plain,
20
+ progress,
21
+ prompt,
22
+ pulse,
23
+ status,
24
+ transient,
25
+ width,
26
+ wordmark
27
+ } from "./chunk-RNNW7OW2.js";
28
+ import "./chunk-UAWBPTDW.js";
29
+ export {
30
+ action,
31
+ badge,
32
+ blank,
33
+ bold,
34
+ box,
35
+ clip,
36
+ columns,
37
+ count,
38
+ dashed,
39
+ duration,
40
+ failure,
41
+ glyph,
42
+ key,
43
+ line,
44
+ logo,
45
+ pad,
46
+ paint,
47
+ plain,
48
+ progress,
49
+ prompt,
50
+ pulse,
51
+ status,
52
+ transient,
53
+ width,
54
+ wordmark
55
+ };
56
+ //# sourceMappingURL=ui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,10 @@
1
+ import { WalkwrightConfig } from './project.js';
2
+
3
+ type UploadProgress = (done: number, total: number, file: string) => void;
4
+ interface Uploaded {
5
+ url: string;
6
+ number: number | undefined;
7
+ }
8
+ declare function uploadRun(config: WalkwrightConfig, branch: string, runDir: string, files: string[], onProgress?: UploadProgress): Promise<Uploaded>;
9
+
10
+ export { type UploadProgress, type Uploaded, uploadRun };
@@ -0,0 +1,8 @@
1
+ import {
2
+ uploadRun
3
+ } from "./chunk-JYHZVRB2.js";
4
+ import "./chunk-UAWBPTDW.js";
5
+ export {
6
+ uploadRun
7
+ };
8
+ //# sourceMappingURL=upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,14 @@
1
+ import { Page, Locator } from '@playwright/test';
2
+
3
+ declare function visibleWithin(locator: Locator, timeoutMs: number): Promise<boolean>;
4
+ declare function pollUntil(page: Page, ready: () => Promise<boolean>, options?: {
5
+ timeoutMs?: number;
6
+ refresh?: () => Promise<void>;
7
+ }): Promise<boolean>;
8
+ declare function revealVirtualizedRow(page: Page, options: {
9
+ scrollOver: Locator;
10
+ target: Locator;
11
+ timeoutMs?: number;
12
+ }): Promise<void>;
13
+
14
+ export { pollUntil, revealVirtualizedRow, visibleWithin };
package/build/wait.js ADDED
@@ -0,0 +1,50 @@
1
+ import "./chunk-UAWBPTDW.js";
2
+
3
+ // src/wait.ts
4
+ async function visibleWithin(locator, timeoutMs) {
5
+ try {
6
+ await locator.waitFor({ state: "visible", timeout: timeoutMs });
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+ async function pollUntil(page, ready, options = {}) {
13
+ const timeoutMs = options.timeoutMs ?? 6e4;
14
+ const deadline = Date.now() + timeoutMs;
15
+ for (; ; ) {
16
+ if (await ready()) {
17
+ return true;
18
+ }
19
+ if (Date.now() >= deadline) {
20
+ return false;
21
+ }
22
+ await page.waitForTimeout(1e4);
23
+ await (options.refresh?.() ?? page.reload());
24
+ await page.waitForTimeout(1e3);
25
+ }
26
+ }
27
+ async function revealVirtualizedRow(page, options) {
28
+ const timeoutMs = options.timeoutMs ?? 6e4;
29
+ const deadline = Date.now() + timeoutMs;
30
+ await options.scrollOver.hover();
31
+ for (; ; ) {
32
+ if (await options.target.count() > 0) {
33
+ break;
34
+ }
35
+ if (Date.now() >= deadline) {
36
+ throw new Error(
37
+ `the row did not appear within ${timeoutMs}ms of scrolling`
38
+ );
39
+ }
40
+ await page.mouse.wheel(0, 400);
41
+ await page.waitForTimeout(250);
42
+ }
43
+ await options.target.first().scrollIntoViewIfNeeded();
44
+ }
45
+ export {
46
+ pollUntil,
47
+ revealVirtualizedRow,
48
+ visibleWithin
49
+ };
50
+ //# sourceMappingURL=wait.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/wait.ts"],"sourcesContent":["import type { Locator, Page } from \"@playwright/test\";\n\nexport async function visibleWithin(\n locator: Locator,\n timeoutMs: number,\n): Promise<boolean> {\n try {\n await locator.waitFor({ state: \"visible\", timeout: timeoutMs });\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function pollUntil(\n page: Page,\n ready: () => Promise<boolean>,\n options: { timeoutMs?: number; refresh?: () => Promise<void> } = {},\n): Promise<boolean> {\n const timeoutMs = options.timeoutMs ?? 60_000;\n const deadline = Date.now() + timeoutMs;\n\n for (;;) {\n if (await ready()) {\n return true;\n }\n\n if (Date.now() >= deadline) {\n return false;\n }\n\n await page.waitForTimeout(10_000);\n await (options.refresh?.() ?? page.reload());\n await page.waitForTimeout(1_000);\n }\n}\n\nexport async function revealVirtualizedRow(\n page: Page,\n options: { scrollOver: Locator; target: Locator; timeoutMs?: number },\n): Promise<void> {\n const timeoutMs = options.timeoutMs ?? 60_000;\n const deadline = Date.now() + timeoutMs;\n\n await options.scrollOver.hover();\n\n for (;;) {\n if ((await options.target.count()) > 0) {\n break;\n }\n\n if (Date.now() >= deadline) {\n throw new Error(\n `the row did not appear within ${timeoutMs}ms of scrolling`,\n );\n }\n\n await page.mouse.wheel(0, 400);\n await page.waitForTimeout(250);\n }\n\n await options.target.first().scrollIntoViewIfNeeded();\n}\n"],"mappings":";;;AAEA,eAAsB,cACpB,SACA,WACkB;AAClB,MAAI;AACF,UAAM,QAAQ,QAAQ,EAAE,OAAO,WAAW,SAAS,UAAU,CAAC;AAC9D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,UACpB,MACA,OACA,UAAiE,CAAC,GAChD;AAClB,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,WAAW,KAAK,IAAI,IAAI;AAE9B,aAAS;AACP,QAAI,MAAM,MAAM,GAAG;AACjB,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,IAAI,KAAK,UAAU;AAC1B,aAAO;AAAA,IACT;AAEA,UAAM,KAAK,eAAe,GAAM;AAChC,WAAO,QAAQ,UAAU,KAAK,KAAK,OAAO;AAC1C,UAAM,KAAK,eAAe,GAAK;AAAA,EACjC;AACF;AAEA,eAAsB,qBACpB,MACA,SACe;AACf,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,WAAW,KAAK,IAAI,IAAI;AAE9B,QAAM,QAAQ,WAAW,MAAM;AAE/B,aAAS;AACP,QAAK,MAAM,QAAQ,OAAO,MAAM,IAAK,GAAG;AACtC;AAAA,IACF;AAEA,QAAI,KAAK,IAAI,KAAK,UAAU;AAC1B,YAAM,IAAI;AAAA,QACR,iCAAiC,SAAS;AAAA,MAC5C;AAAA,IACF;AAEA,UAAM,KAAK,MAAM,MAAM,GAAG,GAAG;AAC7B,UAAM,KAAK,eAAe,GAAG;AAAA,EAC/B;AAEA,QAAM,QAAQ,OAAO,MAAM,EAAE,uBAAuB;AACtD;","names":[]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "walkwright",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Walkwright runtime: walk your app with Playwright and record a browsable report",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/walkwright/walkwright.git",
9
+ "directory": "packages/runtime"
10
+ },
11
+ "homepage": "https://github.com/walkwright/walkwright#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/walkwright/walkwright/issues"
14
+ },
15
+ "license": "MIT",
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "bin": {
20
+ "walkwright": "./bin/walkwright.js"
21
+ },
22
+ "exports": {
23
+ "./*": {
24
+ "walkwright:source": "./src/*.ts",
25
+ "types": "./build/*.d.ts",
26
+ "default": "./build/*.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "bin/walkwright.js",
31
+ "build/**/*.d.ts",
32
+ "build/**/*.js",
33
+ "build/**/*.js.map",
34
+ "src/**/*.ts"
35
+ ],
36
+ "peerDependencies": {
37
+ "@playwright/test": ">=1.55.0",
38
+ "zod": "^4.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@playwright/test": "1.62.1",
42
+ "@types/node": "24.13.3",
43
+ "tsup": "8.5.1",
44
+ "typescript": "5.9.3",
45
+ "zod": "4.4.3"
46
+ },
47
+ "sideEffects": false,
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "typecheck": "tsc -p tsconfig.json --noEmit"
51
+ }
52
+ }
package/src/aria.ts ADDED
@@ -0,0 +1,326 @@
1
+ import type { Locator, Page } from "@playwright/test";
2
+
3
+ import type { LazyLocator, Mount } from "./locator.js";
4
+ import {
5
+ button,
6
+ listbox,
7
+ menu as menuRole,
8
+ menuitem,
9
+ option as optionLeaf,
10
+ radio as radioLeaf,
11
+ radiogroup,
12
+ resolveLocator,
13
+ tab as tabLeaf,
14
+ tabpanel as tabpanelLeaf,
15
+ textbox,
16
+ } from "./locator.js";
17
+ import type {
18
+ AnySurface,
19
+ Component,
20
+ Overlay,
21
+ OverlayFactory,
22
+ Rooted,
23
+ RootedComponent,
24
+ Surface,
25
+ SurfaceInstance,
26
+ } from "./pom.js";
27
+ import { defineComponent, defineDialog, defineOverlay, rootOf } from "./pom.js";
28
+
29
+ export interface Menu<Action extends string> extends Overlay<{
30
+ item: (action: Action) => Locator;
31
+ }> {
32
+ choose: (action: Action) => Promise<void>;
33
+ }
34
+
35
+ export interface MenuFactory {
36
+ <Action extends string>(
37
+ name: string | RegExp,
38
+ options?: { label?: string },
39
+ ): OverlayFactory<Menu<Action>>;
40
+ parameterized: true;
41
+ }
42
+
43
+ function labelOf(name: string | RegExp, label: string | undefined): string {
44
+ if (label !== undefined) {
45
+ return label;
46
+ }
47
+
48
+ if (typeof name === "string") {
49
+ return name;
50
+ }
51
+
52
+ throw new Error(
53
+ `the ${name.source} menu needs a label: its name is a pattern`,
54
+ );
55
+ }
56
+
57
+ export const menu = defineOverlay(
58
+ (name: string | RegExp, options?: { label?: string }) => ({
59
+ root: menuRole(name),
60
+ recorder: { label: labelOf(name, options?.label) },
61
+ elements: {
62
+ item: (action: string): LazyLocator => menuitem(action),
63
+ },
64
+ actions: {
65
+ async choose(action: string): Promise<void> {
66
+ await this.open();
67
+ await this.elements.item(action).click();
68
+ await this.waitFor("hidden");
69
+ },
70
+ },
71
+ }),
72
+ ) as unknown as MenuFactory;
73
+
74
+ async function selectTab(tab: Locator): Promise<void> {
75
+ if ((await tab.getAttribute("aria-selected")) !== "true") {
76
+ await tab.click();
77
+ }
78
+ }
79
+
80
+ export interface TabsElements<N extends string> {
81
+ names: () => readonly N[];
82
+ tab: (n: N) => LazyLocator;
83
+ tabPanel: (n: N) => LazyLocator;
84
+ select: (n: N) => (scope: Mount) => Promise<void>;
85
+ }
86
+
87
+ export const tabs = <const N extends string>(
88
+ names: readonly N[],
89
+ ): TabsElements<N> => ({
90
+ names: (): readonly N[] => names,
91
+ tab: (n: N): LazyLocator => tabLeaf(n),
92
+ tabPanel: (n: N): LazyLocator => tabpanelLeaf(n),
93
+ select:
94
+ (n: N) =>
95
+ async (scope: Mount): Promise<void> => {
96
+ await selectTab(resolveLocator(scope, tabLeaf(n).chain));
97
+ await resolveLocator(scope, tabpanelLeaf(n).chain).waitFor();
98
+ },
99
+ });
100
+
101
+ export type RadioGroup<Option extends string> = RootedComponent<
102
+ { radio: (option: Option) => Locator },
103
+ { select: (option: Option) => Promise<void> }
104
+ >;
105
+
106
+ export interface RadioGroupFactory {
107
+ <Option extends string>(name: string): Surface<RadioGroup<Option>>;
108
+ parameterized: true;
109
+ }
110
+
111
+ export const radioGroup = defineComponent((name: string) => ({
112
+ root: radiogroup(name),
113
+ elements: {
114
+ radio: (option: string): LazyLocator => radioLeaf(option),
115
+ },
116
+ actions: {
117
+ async select(option: string): Promise<void> {
118
+ await this.elements.radio(option).check();
119
+ },
120
+ },
121
+ })) as unknown as RadioGroupFactory;
122
+
123
+ const comboboxOptions = defineOverlay({
124
+ root: listbox(),
125
+ recorder: { label: "Options" },
126
+ elements: {
127
+ option: (name: string): LazyLocator => optionLeaf(name),
128
+ },
129
+ });
130
+
131
+ export type ComboboxOptions<Value extends string> = Overlay<{
132
+ option: (value: Value) => Locator;
133
+ }>;
134
+
135
+ export type SelectCombobox<Value extends string> = RootedComponent<
136
+ { options: ComboboxOptions<Value> },
137
+ { ensureSelected: (value: Value) => Promise<void> }
138
+ >;
139
+
140
+ export type SelectComboboxFactory = <Value extends string>(
141
+ scope: LazyLocator,
142
+ ) => Surface<SelectCombobox<Value>>;
143
+
144
+ export const selectCombobox = defineComponent({
145
+ elements: {
146
+ options: comboboxOptions,
147
+ },
148
+ actions: {
149
+ async ensureSelected(value: string): Promise<void> {
150
+ await rootOf(this).click();
151
+ await this.elements.options.elements.option(value).click();
152
+ },
153
+ },
154
+ }) as unknown as SelectComboboxFactory;
155
+
156
+ export type MultiCombobox<Value extends string> = RootedComponent<
157
+ {
158
+ options: ComboboxOptions<Value>;
159
+ selectedTag: (value: Value) => Locator;
160
+ },
161
+ {
162
+ ensureSelected: (
163
+ values: readonly Value[],
164
+ allValues?: readonly Value[],
165
+ ) => Promise<void>;
166
+ }
167
+ >;
168
+
169
+ export interface MultiComboboxFactory {
170
+ <Value extends string>(
171
+ selectedTag: (value: Value) => LazyLocator,
172
+ ): (scope: LazyLocator) => Surface<MultiCombobox<Value>>;
173
+ parameterized: true;
174
+ }
175
+
176
+ export const multiCombobox = defineComponent(
177
+ (selectedTag: (value: string) => LazyLocator) => ({
178
+ elements: {
179
+ options: comboboxOptions,
180
+ selectedTag,
181
+ },
182
+ actions: {
183
+ async ensureSelected(
184
+ values: readonly string[],
185
+ allValues: readonly string[] = [],
186
+ ): Promise<void> {
187
+ const root = rootOf(this);
188
+ const { options, selectedTag } = this.elements;
189
+
190
+ await root.click();
191
+
192
+ for (const value of values) {
193
+ const option = options.elements.option(value);
194
+ if ((await option.getAttribute("aria-selected")) !== "true") {
195
+ await option.click();
196
+ }
197
+ }
198
+
199
+ await root.press("Escape");
200
+
201
+ for (const value of values) {
202
+ await selectedTag(value).waitFor();
203
+ }
204
+
205
+ for (const value of allValues.filter((it) => !values.includes(it))) {
206
+ const tag = selectedTag(value);
207
+ if (!(await tag.isVisible())) {
208
+ continue;
209
+ }
210
+
211
+ await root.click();
212
+ await options.elements.option(value).click();
213
+ await root.press("Escape");
214
+ await tag.waitFor({ state: "hidden" });
215
+ }
216
+ },
217
+ },
218
+ }),
219
+ ) as unknown as MultiComboboxFactory;
220
+
221
+ export const freeCombobox = defineComponent({
222
+ actions: {
223
+ async ensureSelected(values: readonly string[]): Promise<void> {
224
+ const root = rootOf(this);
225
+
226
+ for (const value of values) {
227
+ await root.fill(value);
228
+ await root.press("Enter");
229
+ }
230
+
231
+ await root.press("Escape");
232
+ },
233
+ },
234
+ });
235
+
236
+ export type Disclosure<Panel = Locator> = Component<
237
+ { trigger: Locator; panel: Panel },
238
+ { expand: () => Promise<void>; collapse: () => Promise<void> }
239
+ >;
240
+
241
+ export interface DisclosureFactory {
242
+ (trigger: LazyLocator, panel: LazyLocator): Surface<Disclosure>;
243
+ <I extends SurfaceInstance>(
244
+ trigger: LazyLocator,
245
+ panel: LazyLocator,
246
+ contents: Surface<I>,
247
+ ): Surface<Disclosure<I & Rooted>>;
248
+ }
249
+
250
+ export const disclosure = ((
251
+ trigger: LazyLocator,
252
+ panel: LazyLocator,
253
+ contents?: AnySurface,
254
+ ): AnySurface => {
255
+ const within = panel.within(trigger);
256
+
257
+ return defineComponent({
258
+ elements: {
259
+ trigger,
260
+ panel: contents === undefined ? within : contents(within),
261
+ },
262
+ actions: {
263
+ async expand(): Promise<void> {
264
+ const { trigger } = this.elements;
265
+ const expanded = trigger.and(
266
+ this.page.locator('[aria-expanded="true"]'),
267
+ );
268
+ const collapsed = trigger.and(
269
+ this.page.locator('[aria-expanded="false"]'),
270
+ );
271
+
272
+ await expanded.or(collapsed).waitFor();
273
+
274
+ if ((await expanded.count()) > 0) {
275
+ return;
276
+ }
277
+
278
+ await trigger.click();
279
+ await expanded.waitFor();
280
+ },
281
+ async collapse(): Promise<void> {
282
+ const { trigger } = this.elements;
283
+ const expanded = trigger.and(
284
+ this.page.locator('[aria-expanded="true"]'),
285
+ );
286
+ const collapsed = trigger.and(
287
+ this.page.locator('[aria-expanded="false"]'),
288
+ );
289
+
290
+ await expanded.or(collapsed).waitFor();
291
+
292
+ if ((await collapsed.count()) > 0) {
293
+ return;
294
+ }
295
+
296
+ await trigger.click();
297
+ await collapsed.waitFor();
298
+ },
299
+ },
300
+ });
301
+ }) as unknown as DisclosureFactory;
302
+
303
+ export const confirmDelete = defineDialog(
304
+ (def: {
305
+ name: string;
306
+ confirmLabel: string;
307
+ cancelLabel: string;
308
+ confirmed?: (page: Page) => Promise<void>;
309
+ }) => ({
310
+ name: def.name,
311
+ close: { button: button(def.cancelLabel) },
312
+ elements: {
313
+ confirmTextbox: textbox(def.confirmLabel),
314
+ deleteButton: button(def.name),
315
+ cancelButton: button(def.cancelLabel),
316
+ },
317
+ actions: {
318
+ async confirm(name: string): Promise<void> {
319
+ await this.elements.confirmTextbox.fill(name);
320
+ await this.elements.deleteButton.click();
321
+ await this.root.waitFor({ state: "hidden", timeout: 15_000 });
322
+ await def.confirmed?.(this.page);
323
+ },
324
+ },
325
+ }),
326
+ );
package/src/browser.ts ADDED
@@ -0,0 +1,15 @@
1
+ export function hideStyle(selectors: string[]): string | undefined {
2
+ if (selectors.length === 0) {
3
+ return undefined;
4
+ }
5
+
6
+ const css = selectors
7
+ .map((selector) => `${selector}{display:none;}`)
8
+ .join("");
9
+
10
+ return `document.addEventListener("DOMContentLoaded", () => {
11
+ const style = document.createElement("style");
12
+ style.textContent = ${JSON.stringify(css)};
13
+ document.head.append(style);
14
+ });`;
15
+ }