@haibun/web-playwright 1.13.5 → 1.13.13
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/build/BrowserFactory.d.ts +19 -16
- package/build/BrowserFactory.js +43 -57
- package/build/BrowserFactory.js.map +1 -1
- package/build/BrowserFactory.test.d.ts +0 -1
- package/build/BrowserFactory.test.js +39 -67
- package/build/BrowserFactory.test.js.map +1 -1
- package/build/web-playwright.d.ts +38 -48
- package/build/web-playwright.js +435 -447
- package/build/web-playwright.js.map +1 -1
- package/build/web-playwright.test.d.ts +0 -1
- package/build/web-playwright.test.js +15 -15
- package/build/web-playwright.test.js.map +1 -1
- package/package.json +10 -22
- package/build/BrowserFactory.d.ts.map +0 -1
- package/build/BrowserFactory.test.d.ts.map +0 -1
- package/build/web-playwright.d.ts.map +0 -1
- package/build/web-playwright.test.d.ts.map +0 -1
package/build/web-playwright.js
CHANGED
|
@@ -1,460 +1,448 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
gwta: 'dialog {what} {type} says {value}',
|
|
71
|
-
action: async ({ what, type, value }) => {
|
|
72
|
-
const cur = this.getWorld().shared.get(what)?.[type];
|
|
73
|
-
return cur === value ? defs_1.OK : (0, util_1.actionNotOK)(`${what} is ${cur}`);
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
dialogIsUnset: {
|
|
77
|
-
gwta: 'dialog {what} {type} not set',
|
|
78
|
-
action: async ({ what, type, value }) => {
|
|
79
|
-
const cur = this.getWorld().shared.get(what)?.[type];
|
|
80
|
-
return !cur ? defs_1.OK : (0, util_1.actionNotOK)(`${what} is ${cur}`);
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
seeTextIn: {
|
|
84
|
-
gwta: 'in {selector}, should see {text}',
|
|
85
|
-
action: async ({ text, selector }) => {
|
|
86
|
-
let textContent = null;
|
|
87
|
-
// FIXME retry sometimes required?
|
|
88
|
-
for (let a = 0; a < 2; a++) {
|
|
89
|
-
textContent = await this.withPage(async (page) => await page.textContent(selector, { timeout: 1e9 }));
|
|
90
|
-
if (textContent?.toString().includes(text)) {
|
|
91
|
-
return defs_1.OK;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const topics = { textContent: { summary: `in ${textContent?.length} characters`, details: textContent } };
|
|
95
|
-
return (0, util_1.actionNotOK)(`Did not find text "${text}" in ${selector}`, { topics });
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
seeText: {
|
|
99
|
-
gwta: 'should see {text}',
|
|
100
|
-
action: async ({ text }) => {
|
|
101
|
-
let textContent = null;
|
|
102
|
-
// FIXME retry sometimes required?
|
|
103
|
-
for (let a = 0; a < 2; a++) {
|
|
104
|
-
textContent = await this.withPage(async (page) => await page.textContent('body', { timeout: 1e9 }));
|
|
105
|
-
if (textContent?.toString().includes(text)) {
|
|
106
|
-
return defs_1.OK;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const topics = { textContent: { summary: `in ${textContent?.length} characters`, details: textContent } };
|
|
110
|
-
return (0, util_1.actionNotOK)(`Did not find text "${text}" in document`, { topics });
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
waitFor: {
|
|
114
|
-
gwta: 'wait for {what}',
|
|
115
|
-
action: async ({ what }) => {
|
|
116
|
-
const found = await this.withPage(async (page) => await page.waitForSelector(what));
|
|
117
|
-
if (found) {
|
|
118
|
-
return defs_1.OK;
|
|
119
|
-
}
|
|
120
|
-
return (0, util_1.actionNotOK)(`Did not find ${what}`);
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
onNewPage: {
|
|
124
|
-
gwta: `on a new tab`,
|
|
125
|
-
action: async ({ name }) => {
|
|
126
|
-
this.tab = this.tab + 1;
|
|
127
|
-
return defs_1.OK;
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
onTabX: {
|
|
131
|
-
gwta: `on tab {tab}`,
|
|
132
|
-
action: async ({ tab }) => {
|
|
133
|
-
this.tab = parseInt(tab, 10);
|
|
134
|
-
return defs_1.OK;
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
beOnPage: {
|
|
138
|
-
gwta: `should be on the {name} page`,
|
|
139
|
-
action: async ({ name }) => {
|
|
140
|
-
const nowon = await this.withPage(async (page) => await page.url());
|
|
141
|
-
if (nowon === name) {
|
|
142
|
-
return defs_1.OK;
|
|
143
|
-
}
|
|
144
|
-
return (0, util_1.actionNotOK)(`expected ${name} but on ${nowon}`);
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
extensionContext: {
|
|
148
|
-
gwta: `open extension popup for tab {tab}`,
|
|
149
|
-
action: async ({ tab }) => {
|
|
150
|
-
if (!this.factoryOptions?.persistentDirectory || this.factoryOptions?.browser.headless) {
|
|
151
|
-
throw Error(`extensions require ${WebPlaywright.PERSISTENT_DIRECTORY} and not HEADLESS`);
|
|
152
|
-
}
|
|
153
|
-
const context = await this.getContext();
|
|
154
|
-
if (!context) {
|
|
155
|
-
throw Error(`no context`);
|
|
156
|
-
}
|
|
157
|
-
let background = context?.serviceWorkers()[0];
|
|
158
|
-
// if (!background) {
|
|
159
|
-
// console.log('waiting');
|
|
160
|
-
// background = await context!.waitForEvent("serviceworker");
|
|
161
|
-
// }
|
|
162
|
-
const extensionId = background.url().split("/")[2];
|
|
163
|
-
this.getWorld().shared.set('extensionContext', extensionId);
|
|
164
|
-
await this.withPage(async (page) => {
|
|
165
|
-
const popupURI = `chrome-extension://${extensionId}/popup.html?${tab}`;
|
|
166
|
-
return await page.goto(popupURI);
|
|
167
|
-
});
|
|
168
|
-
return defs_1.OK;
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
cookieShouldBe: {
|
|
172
|
-
gwta: 'cookie {name} should be {value}',
|
|
173
|
-
action: async ({ name, value }) => {
|
|
174
|
-
const context = await this.getContext();
|
|
175
|
-
const cookies = await context?.cookies();
|
|
176
|
-
const found = cookies?.find(c => c.name === name && c.value === value);
|
|
177
|
-
return found ? defs_1.OK : (0, util_1.actionNotOK)(`did not find cookie ${name} with value ${value}`);
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
URIContains: {
|
|
181
|
-
gwta: 'URI should include {what}',
|
|
182
|
-
action: async ({ what }) => {
|
|
183
|
-
const uri = await this.withPage(async (page) => await page.url());
|
|
184
|
-
return uri.includes(what) ? defs_1.OK : (0, util_1.actionNotOK)(`current URI ${uri} does not contain ${what}`);
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
URIQueryParameterIs: {
|
|
188
|
-
gwta: 'URI query parameter {what} is {value}',
|
|
189
|
-
action: async ({ what, value }) => {
|
|
190
|
-
const uri = await this.withPage(async (page) => await page.url());
|
|
191
|
-
const found = new URL(uri).searchParams.get(what);
|
|
192
|
-
if (found === value) {
|
|
193
|
-
return defs_1.OK;
|
|
194
|
-
}
|
|
195
|
-
return (0, util_1.actionNotOK)(`URI query ${what} contains "${found}"", not "${value}""`);
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
URIStartsWith: {
|
|
199
|
-
gwta: 'URI should start with {start}',
|
|
200
|
-
action: async ({ start }) => {
|
|
201
|
-
const uri = await this.withPage(async (page) => await page.url());
|
|
202
|
-
return uri.startsWith(start) ? defs_1.OK : (0, util_1.actionNotOK)(`current URI ${uri} does not start with ${start}`);
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
URIMatches: {
|
|
206
|
-
gwta: 'URI should match {what}',
|
|
207
|
-
action: async ({ what }) => {
|
|
208
|
-
const uri = await this.withPage(async (page) => await page.url());
|
|
209
|
-
return uri.match(what) ? defs_1.OK : (0, util_1.actionNotOK)(`current URI ${uri} does not match ${what}`);
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
// CLICK
|
|
213
|
-
clickOn: {
|
|
214
|
-
gwta: 'click on (?<name>.[^s]+)',
|
|
215
|
-
action: async ({ name }) => {
|
|
216
|
-
const what = this.getWorld().shared.get(name) || `text=${name}`;
|
|
217
|
-
await this.withPage(async (page) => await page.click(what));
|
|
218
|
-
return defs_1.OK;
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
clickCheckbox: {
|
|
222
|
-
gwta: 'click the checkbox (?<name>.+)',
|
|
223
|
-
action: async ({ name }) => {
|
|
224
|
-
const what = this.getWorld().shared.get(name) || name;
|
|
225
|
-
this.getWorld().logger.log(`click ${name} ${what}`);
|
|
226
|
-
await this.withPage(async (page) => await page.click(what));
|
|
227
|
-
return defs_1.OK;
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
clickShared: {
|
|
231
|
-
gwta: 'click `(?<id>.+)`',
|
|
232
|
-
action: async ({ id }) => {
|
|
233
|
-
const name = this.getWorld().shared.get(id);
|
|
234
|
-
await this.withPage(async (page) => await page.click(name));
|
|
235
|
-
return defs_1.OK;
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
clickQuoted: {
|
|
239
|
-
gwta: 'click "(?<name>.+)"',
|
|
240
|
-
action: async ({ name }) => {
|
|
241
|
-
await this.withPage(async (page) => await page.click(`text=${name}`));
|
|
242
|
-
return defs_1.OK;
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
clickLink: {
|
|
246
|
-
gwta: 'click the link (?<uri>.+)',
|
|
247
|
-
action: async ({ name }) => {
|
|
248
|
-
const field = this.getWorld().shared.get(name) || name;
|
|
249
|
-
await this.withPage(async (page) => await page.click(field));
|
|
250
|
-
return defs_1.OK;
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
clickButton: {
|
|
254
|
-
gwta: 'click the button (?<id>.+)',
|
|
255
|
-
action: async ({ id }) => {
|
|
256
|
-
const field = this.getWorld().shared.get(id) || id;
|
|
257
|
-
const a = await this.withPage(async (page) => await page.click(field));
|
|
258
|
-
return defs_1.OK;
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
// NAVIGATION
|
|
262
|
-
onPage: {
|
|
263
|
-
gwta: `On the {name} ${domain_webpage_1.WEB_PAGE}`,
|
|
264
|
-
action: async ({ name }, vstep) => {
|
|
265
|
-
const location = name.includes('://') ? name : (0, vars_1.onCurrentTypeForDomain)({ name, type: domain_webpage_1.WEB_PAGE }, this.getWorld());
|
|
266
|
-
const response = await this.withPage(async (page) => {
|
|
267
|
-
return await page.goto(location);
|
|
268
|
-
});
|
|
269
|
-
return response?.ok ? defs_1.OK : (0, util_1.actionNotOK)(`response not ok`, response);
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
goBack: {
|
|
273
|
-
gwta: 'go back',
|
|
274
|
-
action: async () => {
|
|
275
|
-
await this.withPage(async (page) => await page.goBack());
|
|
276
|
-
return defs_1.OK;
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
pressBack: {
|
|
280
|
-
gwta: 'press the back button',
|
|
281
|
-
action: async () => {
|
|
282
|
-
// FIXME
|
|
283
|
-
await this.withPage(async (page) => await page.evaluate(() => {
|
|
284
|
-
console.debug('going back', globalThis.history);
|
|
285
|
-
globalThis.history.go(-1);
|
|
286
|
-
}));
|
|
287
|
-
// await page.focus('body');
|
|
288
|
-
// await page.keyboard.press('Alt+ArrowRight');
|
|
289
|
-
return defs_1.OK;
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
// BROWSER
|
|
293
|
-
usingBrowser: {
|
|
294
|
-
gwta: 'using (?<browser>[^`].+[^`]) browser',
|
|
295
|
-
action: async ({ browser }) => await this.setBrowser(browser),
|
|
296
|
-
},
|
|
297
|
-
usingBrowserVar: {
|
|
298
|
-
gwta: 'using {browser} browser',
|
|
299
|
-
action: async ({ browser }) => {
|
|
300
|
-
return this.setBrowser(browser);
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
// MISC
|
|
304
|
-
captureDialog: {
|
|
305
|
-
gwta: 'Accept next dialog to {where}',
|
|
306
|
-
action: async ({ where }) => {
|
|
307
|
-
const a = await this.withPage(async (page) => page.on('dialog', dialog => {
|
|
308
|
-
const res = {
|
|
309
|
-
defaultValue: dialog.defaultValue(),
|
|
310
|
-
message: dialog.message(),
|
|
311
|
-
type: dialog.type()
|
|
312
|
-
};
|
|
313
|
-
dialog.accept();
|
|
314
|
-
this.getWorld().shared.set(where, res);
|
|
315
|
-
}));
|
|
316
|
-
return defs_1.OK;
|
|
317
|
-
},
|
|
318
|
-
},
|
|
319
|
-
takeScreenshot: {
|
|
320
|
-
gwta: 'take a screenshot',
|
|
321
|
-
action: async () => {
|
|
322
|
-
const loc = { ...this.getWorld(), mediaType: "image" /* EMediaTypes.image */ };
|
|
323
|
-
const dir = await this.storage.ensureCaptureLocation(loc, 'screenshots');
|
|
324
|
-
await this.withPage(async (page) => await page.screenshot({
|
|
325
|
-
path: `${dir}/screenshot-${Date.now()}.png`,
|
|
326
|
-
}));
|
|
327
|
-
return defs_1.OK;
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
assertOpen: {
|
|
331
|
-
gwta: '{what} is expanded with the {using}',
|
|
332
|
-
action: async ({ what, using }) => {
|
|
333
|
-
const isVisible = await this.withPage(async (page) => await page.isVisible(what));
|
|
334
|
-
if (!isVisible) {
|
|
335
|
-
await this.withPage(async (page) => await page.click(using));
|
|
336
|
-
}
|
|
337
|
-
return defs_1.OK;
|
|
338
|
-
},
|
|
339
|
-
},
|
|
340
|
-
setToURIQueryParameter: {
|
|
341
|
-
gwta: 'save URI query parameter {what} to {where}',
|
|
342
|
-
action: async ({ what, where }) => {
|
|
343
|
-
const uri = await this.withPage(async (page) => await page.url());
|
|
344
|
-
const found = new URL(uri).searchParams.get(what);
|
|
345
|
-
this.getWorld().shared.set(where, found);
|
|
346
|
-
return defs_1.OK;
|
|
347
|
-
},
|
|
348
|
-
},
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
async setWorld(world, steppers) {
|
|
352
|
-
super.setWorld(world, steppers);
|
|
353
|
-
this.storage = (0, util_1.findStepperFromOption)(steppers, this, this.getWorld().extraOptions, WebPlaywright.STORAGE);
|
|
354
|
-
const headless = (0, util_1.getStepperOption)(this, 'HEADLESS', this.getWorld().extraOptions) === 'true';
|
|
355
|
-
const devtools = (0, util_1.getStepperOption)(this, 'DEVTOOLS', this.getWorld().extraOptions) === 'true';
|
|
356
|
-
const args = (0, util_1.getStepperOption)(this, 'ARGS', this.getWorld().extraOptions)?.split(';');
|
|
357
|
-
const persistentDirectory = (0, util_1.getStepperOption)(this, WebPlaywright.PERSISTENT_DIRECTORY, this.getWorld().extraOptions) === 'true';
|
|
358
|
-
const defaultTimeout = parseInt((0, util_1.getStepperOption)(this, 'TIMEOUT', this.getWorld().extraOptions)) || 30000;
|
|
359
|
-
const captureVideo = (0, util_1.getStepperOption)(this, 'CAPTURE_VIDEO', this.getWorld().extraOptions);
|
|
360
|
-
const { trace: doTrace } = this.getWorld().tag;
|
|
361
|
-
const trace = doTrace ? {
|
|
362
|
-
response: {
|
|
363
|
-
listener: async (res) => {
|
|
364
|
-
const url = res.url();
|
|
365
|
-
const headers = await res.headersArray();
|
|
366
|
-
const headersContent = (await Promise.allSettled(headers)).map(h => h.value);
|
|
367
|
-
this.getWorld().logger.debug(`response trace ${headersContent.map(h => h.name)}`, { topic: { trace: { response: { headersContent } } } });
|
|
368
|
-
const trace = { 'response': { url, since: this.getWorld().timer.since(), trace: { headersContent } } };
|
|
369
|
-
this.getWorld().shared.concat('_trace', trace);
|
|
370
|
-
}
|
|
1
|
+
import { OK, AStepper } from '@haibun/core/build/lib/defs.js';
|
|
2
|
+
import { onCurrentTypeForDomain } from '@haibun/core/build/steps/vars.js';
|
|
3
|
+
import { BrowserFactory } from './BrowserFactory.js';
|
|
4
|
+
import { actionNotOK, getStepperOption, boolOrError, intOrError, stringOrError, findStepperFromOption } from '@haibun/core/build/lib/util/index.js';
|
|
5
|
+
import { WEB_PAGE, WEB_CONTROL } from '@haibun/domain-webpage/build/domain-webpage.js';
|
|
6
|
+
// TODO: base on these - https://testing-library.com/docs/queries/byrole/, https://playwright.dev/docs/release-notes#locators
|
|
7
|
+
const WebPlaywright = class WebPlaywright extends AStepper {
|
|
8
|
+
static STORAGE = 'STORAGE';
|
|
9
|
+
static PERSISTENT_DIRECTORY = 'PERSISTENT_DIRECTORY';
|
|
10
|
+
requireDomains = [WEB_PAGE, WEB_CONTROL];
|
|
11
|
+
options = {
|
|
12
|
+
HEADLESS: {
|
|
13
|
+
desc: 'run browsers without a window (true or false)',
|
|
14
|
+
parse: (input) => boolOrError(input)
|
|
15
|
+
},
|
|
16
|
+
DEVTOOLS: {
|
|
17
|
+
desc: `show browser devtools (true or false)`,
|
|
18
|
+
parse: (input) => boolOrError(input)
|
|
19
|
+
},
|
|
20
|
+
[WebPlaywright.PERSISTENT_DIRECTORY]: {
|
|
21
|
+
desc: 'run browsers with a persistent directory (true or false)',
|
|
22
|
+
parse: (input) => boolOrError(input)
|
|
23
|
+
},
|
|
24
|
+
ARGS: {
|
|
25
|
+
desc: 'pass arguments',
|
|
26
|
+
parse: (input) => stringOrError(input)
|
|
27
|
+
},
|
|
28
|
+
CAPTURE_VIDEO: {
|
|
29
|
+
desc: 'capture video for every agent',
|
|
30
|
+
parse: (input) => boolOrError(input),
|
|
31
|
+
},
|
|
32
|
+
STEP_CAPTURE_SCREENSHOT: {
|
|
33
|
+
desc: 'capture screenshot for every step',
|
|
34
|
+
parse: (input) => boolOrError(input),
|
|
35
|
+
},
|
|
36
|
+
TIMEOUT: {
|
|
37
|
+
desc: 'timeout for each step',
|
|
38
|
+
parse: (input) => intOrError(input),
|
|
39
|
+
},
|
|
40
|
+
[WebPlaywright.STORAGE]: {
|
|
41
|
+
required: true,
|
|
42
|
+
desc: 'Storage for output',
|
|
43
|
+
parse: (input) => stringOrError(input),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
hasFactory = false;
|
|
47
|
+
bf;
|
|
48
|
+
storage;
|
|
49
|
+
factoryOptions;
|
|
50
|
+
tab = 0;
|
|
51
|
+
async setWorld(world, steppers) {
|
|
52
|
+
super.setWorld(world, steppers);
|
|
53
|
+
this.storage = findStepperFromOption(steppers, this, this.getWorld().extraOptions, WebPlaywright.STORAGE);
|
|
54
|
+
const headless = getStepperOption(this, 'HEADLESS', this.getWorld().extraOptions) === 'true';
|
|
55
|
+
const devtools = getStepperOption(this, 'DEVTOOLS', this.getWorld().extraOptions) === 'true';
|
|
56
|
+
const args = getStepperOption(this, 'ARGS', this.getWorld().extraOptions)?.split(';');
|
|
57
|
+
const persistentDirectory = getStepperOption(this, WebPlaywright.PERSISTENT_DIRECTORY, this.getWorld().extraOptions) === 'true';
|
|
58
|
+
const defaultTimeout = parseInt(getStepperOption(this, 'TIMEOUT', this.getWorld().extraOptions)) || 30000;
|
|
59
|
+
const captureVideo = getStepperOption(this, 'CAPTURE_VIDEO', this.getWorld().extraOptions);
|
|
60
|
+
const { trace: doTrace } = this.getWorld().tag;
|
|
61
|
+
const trace = doTrace ? {
|
|
62
|
+
response: {
|
|
63
|
+
listener: async (res) => {
|
|
64
|
+
const url = res.url();
|
|
65
|
+
const headers = await res.headersArray();
|
|
66
|
+
const headersContent = (await Promise.allSettled(headers)).map(h => h.value);
|
|
67
|
+
this.getWorld().logger.debug(`response trace ${headersContent.map(h => h.name)}`, { topic: { trace: { response: { headersContent } } } });
|
|
68
|
+
const trace = { 'response': { url, since: this.getWorld().timer.since(), trace: { headersContent } } };
|
|
69
|
+
this.getWorld().shared.concat('_trace', trace);
|
|
371
70
|
}
|
|
372
|
-
} : undefined;
|
|
373
|
-
let recordVideo;
|
|
374
|
-
if (captureVideo) {
|
|
375
|
-
const loc = { ...this.getWorld(), mediaType: "video" /* EMediaTypes.video */ };
|
|
376
|
-
recordVideo = {
|
|
377
|
-
dir: await this.storage.ensureCaptureLocation(loc, 'video'),
|
|
378
|
-
};
|
|
379
71
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
recordVideo,
|
|
387
|
-
defaultTimeout,
|
|
388
|
-
persistentDirectory,
|
|
389
|
-
trace
|
|
72
|
+
} : undefined;
|
|
73
|
+
let recordVideo;
|
|
74
|
+
if (captureVideo) {
|
|
75
|
+
const loc = { ...this.getWorld(), mediaType: "video" /* EMediaTypes.video */ };
|
|
76
|
+
recordVideo = {
|
|
77
|
+
dir: await this.storage.ensureCaptureLocation(loc, 'video'),
|
|
390
78
|
};
|
|
391
79
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
80
|
+
this.factoryOptions = {
|
|
81
|
+
browser: {
|
|
82
|
+
headless,
|
|
83
|
+
args,
|
|
84
|
+
devtools,
|
|
85
|
+
},
|
|
86
|
+
recordVideo,
|
|
87
|
+
defaultTimeout,
|
|
88
|
+
persistentDirectory,
|
|
89
|
+
trace
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async getBrowserFactory() {
|
|
93
|
+
if (!this.hasFactory) {
|
|
94
|
+
this.bf = await BrowserFactory.getBrowserFactory(this.getWorld().logger, this.factoryOptions);
|
|
95
|
+
this.hasFactory = true;
|
|
402
96
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
97
|
+
return this.bf;
|
|
98
|
+
}
|
|
99
|
+
async getContext() {
|
|
100
|
+
const context = (await this.getBrowserFactory()).getExistingContext(this.getWorld().tag);
|
|
101
|
+
return context;
|
|
102
|
+
}
|
|
103
|
+
async getPage() {
|
|
104
|
+
const page = await (await this.getBrowserFactory()).getBrowserContextPage(this.getWorld().tag, this.tab);
|
|
105
|
+
return page;
|
|
106
|
+
}
|
|
107
|
+
async withPage(f) {
|
|
108
|
+
const page = await this.getPage();
|
|
109
|
+
return await f(page);
|
|
110
|
+
}
|
|
111
|
+
async onFailure(result) {
|
|
112
|
+
if (this.bf?.hasPage(this.getWorld().tag, this.tab)) {
|
|
408
113
|
const page = await this.getPage();
|
|
409
|
-
|
|
114
|
+
const path = await this.storage.getCaptureLocation({ ...this.getWorld(), mediaType: "image" /* EMediaTypes.image */ }, 'failure') + `/${result.seq}.png`;
|
|
115
|
+
await page.screenshot({ path, fullPage: true, timeout: 60000 });
|
|
410
116
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
catch (e) {
|
|
417
|
-
return (0, util_1.actionNotOK)(e.message, { topics: { error: e } });
|
|
418
|
-
}
|
|
117
|
+
}
|
|
118
|
+
async nextStep() {
|
|
119
|
+
const captureScreenshot = getStepperOption(this, 'STEP_CAPTURE_SCREENSHOT', this.getWorld().extraOptions);
|
|
120
|
+
if (captureScreenshot) {
|
|
121
|
+
console.debug('captureScreenshot');
|
|
419
122
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
123
|
+
}
|
|
124
|
+
async endFeature() {
|
|
125
|
+
// close the context, which closes any pages
|
|
126
|
+
if (this.hasFactory) {
|
|
127
|
+
await this.bf?.closeContext(this.getWorld().tag);
|
|
128
|
+
return;
|
|
426
129
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
async endFeature() {
|
|
434
|
-
// close the context, which closes any pages
|
|
435
|
-
if (this.hasFactory) {
|
|
436
|
-
await this.bf?.closeContext(this.getWorld().tag);
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
130
|
+
}
|
|
131
|
+
async close() {
|
|
132
|
+
// close the context, which closes any pages
|
|
133
|
+
if (this.hasFactory) {
|
|
134
|
+
await this.bf?.closeContext(this.getWorld().tag);
|
|
135
|
+
return;
|
|
439
136
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
137
|
+
}
|
|
138
|
+
// FIXME
|
|
139
|
+
async finish() {
|
|
140
|
+
if (this.hasFactory) {
|
|
141
|
+
this.bf?.close();
|
|
142
|
+
this.bf = undefined;
|
|
143
|
+
this.hasFactory = false;
|
|
446
144
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
145
|
+
}
|
|
146
|
+
steps = {
|
|
147
|
+
// INPUT
|
|
148
|
+
inputVariable: {
|
|
149
|
+
gwta: `input {what} for {field}`,
|
|
150
|
+
action: async ({ what, field }) => {
|
|
151
|
+
await this.withPage(async (page) => await page.fill(field, what));
|
|
152
|
+
return OK;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
selectionOption: {
|
|
156
|
+
gwta: `select {option} for {field: ${WEB_CONTROL}}`,
|
|
157
|
+
action: async ({ option, field }) => {
|
|
158
|
+
const res = await this.withPage(async (page) => await page.selectOption(field, { label: option }));
|
|
159
|
+
// FIXME have to use id value
|
|
160
|
+
// return res === [id] ? ok : {...notOk, details: { message: `received ${res} selecting from ${what} with id ${id}`}};
|
|
161
|
+
return OK;
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
// ASSERTIONS
|
|
165
|
+
dialogIs: {
|
|
166
|
+
gwta: 'dialog {what} {type} says {value}',
|
|
167
|
+
action: async ({ what, type, value }) => {
|
|
168
|
+
const cur = this.getWorld().shared.get(what)?.[type];
|
|
169
|
+
return cur === value ? OK : actionNotOK(`${what} is ${cur}`);
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
dialogIsUnset: {
|
|
173
|
+
gwta: 'dialog {what} {type} not set',
|
|
174
|
+
action: async ({ what, type, value }) => {
|
|
175
|
+
const cur = this.getWorld().shared.get(what)?.[type];
|
|
176
|
+
return !cur ? OK : actionNotOK(`${what} is ${cur}`);
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
seeTextIn: {
|
|
180
|
+
gwta: 'in {selector}, should see {text}',
|
|
181
|
+
action: async ({ text, selector }) => {
|
|
182
|
+
let textContent = null;
|
|
183
|
+
// FIXME retry sometimes required?
|
|
184
|
+
for (let a = 0; a < 2; a++) {
|
|
185
|
+
textContent = await this.withPage(async (page) => await page.textContent(selector, { timeout: 1e9 }));
|
|
186
|
+
if (textContent?.toString().includes(text)) {
|
|
187
|
+
return OK;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const topics = { textContent: { summary: `in ${textContent?.length} characters`, details: textContent } };
|
|
191
|
+
return actionNotOK(`Did not find text "${text}" in ${selector}`, { topics });
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
seeText: {
|
|
195
|
+
gwta: 'should see {text}',
|
|
196
|
+
action: async ({ text }) => {
|
|
197
|
+
let textContent = null;
|
|
198
|
+
// FIXME retry sometimes required?
|
|
199
|
+
for (let a = 0; a < 2; a++) {
|
|
200
|
+
textContent = await this.withPage(async (page) => await page.textContent('body', { timeout: 1e9 }));
|
|
201
|
+
if (textContent?.toString().includes(text)) {
|
|
202
|
+
return OK;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const topics = { textContent: { summary: `in ${textContent?.length} characters`, details: textContent } };
|
|
206
|
+
return actionNotOK(`Did not find text "${text}" in document`, { topics });
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
waitFor: {
|
|
210
|
+
gwta: 'wait for {what}',
|
|
211
|
+
action: async ({ what }) => {
|
|
212
|
+
const found = await this.withPage(async (page) => await page.waitForSelector(what));
|
|
213
|
+
if (found) {
|
|
214
|
+
return OK;
|
|
215
|
+
}
|
|
216
|
+
return actionNotOK(`Did not find ${what}`);
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
onNewPage: {
|
|
220
|
+
gwta: `on a new tab`,
|
|
221
|
+
action: async ({ name }) => {
|
|
222
|
+
this.tab = this.tab + 1;
|
|
223
|
+
return OK;
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
onTabX: {
|
|
227
|
+
gwta: `on tab {tab}`,
|
|
228
|
+
action: async ({ tab }) => {
|
|
229
|
+
this.tab = parseInt(tab, 10);
|
|
230
|
+
return OK;
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
beOnPage: {
|
|
234
|
+
gwta: `should be on the {name} page`,
|
|
235
|
+
action: async ({ name }) => {
|
|
236
|
+
const nowon = await this.withPage(async (page) => await page.url());
|
|
237
|
+
if (nowon === name) {
|
|
238
|
+
return OK;
|
|
239
|
+
}
|
|
240
|
+
return actionNotOK(`expected ${name} but on ${nowon}`);
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
extensionContext: {
|
|
244
|
+
gwta: `open extension popup for tab {tab}`,
|
|
245
|
+
action: async ({ tab }) => {
|
|
246
|
+
if (!this.factoryOptions?.persistentDirectory || this.factoryOptions?.browser.headless) {
|
|
247
|
+
throw Error(`extensions require ${WebPlaywright.PERSISTENT_DIRECTORY} and not HEADLESS`);
|
|
248
|
+
}
|
|
249
|
+
const context = await this.getContext();
|
|
250
|
+
if (!context) {
|
|
251
|
+
throw Error(`no context`);
|
|
252
|
+
}
|
|
253
|
+
let background = context?.serviceWorkers()[0];
|
|
254
|
+
if (!background) {
|
|
255
|
+
console.log('no background', context.serviceWorkers());
|
|
256
|
+
// background = await context!.waitForEvent("serviceworker");
|
|
257
|
+
}
|
|
258
|
+
const extensionId = background.url().split("/")[2];
|
|
259
|
+
this.getWorld().shared.set('extensionContext', extensionId);
|
|
260
|
+
await this.withPage(async (page) => {
|
|
261
|
+
const popupURI = `chrome-extension://${extensionId}/popup.html?${tab}`;
|
|
262
|
+
return await page.goto(popupURI);
|
|
263
|
+
});
|
|
264
|
+
return OK;
|
|
453
265
|
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
266
|
+
},
|
|
267
|
+
cookieShouldBe: {
|
|
268
|
+
gwta: 'cookie {name} should be {value}',
|
|
269
|
+
action: async ({ name, value }) => {
|
|
270
|
+
const context = await this.getContext();
|
|
271
|
+
const cookies = await context?.cookies();
|
|
272
|
+
const found = cookies?.find(c => c.name === name && c.value === value);
|
|
273
|
+
return found ? OK : actionNotOK(`did not find cookie ${name} with value ${value}`);
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
URIContains: {
|
|
277
|
+
gwta: 'URI should include {what}',
|
|
278
|
+
action: async ({ what }) => {
|
|
279
|
+
const uri = await this.withPage(async (page) => await page.url());
|
|
280
|
+
return uri.includes(what) ? OK : actionNotOK(`current URI ${uri} does not contain ${what}`);
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
URIQueryParameterIs: {
|
|
284
|
+
gwta: 'URI query parameter {what} is {value}',
|
|
285
|
+
action: async ({ what, value }) => {
|
|
286
|
+
const uri = await this.withPage(async (page) => await page.url());
|
|
287
|
+
const found = new URL(uri).searchParams.get(what);
|
|
288
|
+
if (found === value) {
|
|
289
|
+
return OK;
|
|
290
|
+
}
|
|
291
|
+
return actionNotOK(`URI query ${what} contains "${found}"", not "${value}""`);
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
URIStartsWith: {
|
|
295
|
+
gwta: 'URI should start with {start}',
|
|
296
|
+
action: async ({ start }) => {
|
|
297
|
+
const uri = await this.withPage(async (page) => await page.url());
|
|
298
|
+
return uri.startsWith(start) ? OK : actionNotOK(`current URI ${uri} does not start with ${start}`);
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
URIMatches: {
|
|
302
|
+
gwta: 'URI should match {what}',
|
|
303
|
+
action: async ({ what }) => {
|
|
304
|
+
const uri = await this.withPage(async (page) => await page.url());
|
|
305
|
+
return uri.match(what) ? OK : actionNotOK(`current URI ${uri} does not match ${what}`);
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
// CLICK
|
|
309
|
+
clickOn: {
|
|
310
|
+
gwta: 'click on (?<name>.[^s]+)',
|
|
311
|
+
action: async ({ name }) => {
|
|
312
|
+
const what = this.getWorld().shared.get(name) || `text=${name}`;
|
|
313
|
+
await this.withPage(async (page) => await page.click(what));
|
|
314
|
+
return OK;
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
clickCheckbox: {
|
|
318
|
+
gwta: 'click the checkbox (?<name>.+)',
|
|
319
|
+
action: async ({ name }) => {
|
|
320
|
+
const what = this.getWorld().shared.get(name) || name;
|
|
321
|
+
this.getWorld().logger.log(`click ${name} ${what}`);
|
|
322
|
+
await this.withPage(async (page) => await page.click(what));
|
|
323
|
+
return OK;
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
clickShared: {
|
|
327
|
+
gwta: 'click `(?<id>.+)`',
|
|
328
|
+
action: async ({ id }) => {
|
|
329
|
+
const name = this.getWorld().shared.get(id);
|
|
330
|
+
await this.withPage(async (page) => await page.click(name));
|
|
331
|
+
return OK;
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
clickQuoted: {
|
|
335
|
+
gwta: 'click "(?<name>.+)"',
|
|
336
|
+
action: async ({ name }) => {
|
|
337
|
+
await this.withPage(async (page) => await page.click(`text=${name}`));
|
|
338
|
+
return OK;
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
clickLink: {
|
|
342
|
+
gwta: 'click the link (?<uri>.+)',
|
|
343
|
+
action: async ({ name }) => {
|
|
344
|
+
const field = this.getWorld().shared.get(name) || name;
|
|
345
|
+
await this.withPage(async (page) => await page.click(field));
|
|
346
|
+
return OK;
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
clickButton: {
|
|
350
|
+
gwta: 'click the button (?<id>.+)',
|
|
351
|
+
action: async ({ id }) => {
|
|
352
|
+
const field = this.getWorld().shared.get(id) || id;
|
|
353
|
+
const a = await this.withPage(async (page) => await page.click(field));
|
|
354
|
+
return OK;
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
// NAVIGATION
|
|
358
|
+
onPage: {
|
|
359
|
+
gwta: `On the {name} ${WEB_PAGE}`,
|
|
360
|
+
action: async ({ name }, vstep) => {
|
|
361
|
+
const location = name.includes('://') ? name : onCurrentTypeForDomain({ name, type: WEB_PAGE }, this.getWorld());
|
|
362
|
+
const response = await this.withPage(async (page) => {
|
|
363
|
+
return await page.goto(location);
|
|
364
|
+
});
|
|
365
|
+
return response?.ok ? OK : actionNotOK(`response not ok`, response);
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
goBack: {
|
|
369
|
+
gwta: 'go back',
|
|
370
|
+
action: async () => {
|
|
371
|
+
await this.withPage(async (page) => await page.goBack());
|
|
372
|
+
return OK;
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
pressBack: {
|
|
376
|
+
gwta: 'press the back button',
|
|
377
|
+
action: async () => {
|
|
378
|
+
// FIXME
|
|
379
|
+
await this.withPage(async (page) => await page.evaluate(() => {
|
|
380
|
+
console.debug('going back', globalThis.history);
|
|
381
|
+
globalThis.history.go(-1);
|
|
382
|
+
}));
|
|
383
|
+
// await page.focus('body');
|
|
384
|
+
// await page.keyboard.press('Alt+ArrowRight');
|
|
385
|
+
return OK;
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
// BROWSER
|
|
389
|
+
// usingBrowser: {
|
|
390
|
+
// gwta: 'using (?<browser>[^`].+[^`]) browser',
|
|
391
|
+
// action: async ({ browser }: TNamed) => await this.setBrowser(browser),
|
|
392
|
+
// },
|
|
393
|
+
// usingBrowserVar: {
|
|
394
|
+
// gwta: 'using {browser} browser',
|
|
395
|
+
// action: async ({ browser }: TNamed) => {
|
|
396
|
+
// return this.setBrowser(browser);
|
|
397
|
+
// },
|
|
398
|
+
// },
|
|
399
|
+
// MISC
|
|
400
|
+
captureDialog: {
|
|
401
|
+
gwta: 'Accept next dialog to {where}',
|
|
402
|
+
action: async ({ where }) => {
|
|
403
|
+
const a = await this.withPage(async (page) => page.on('dialog', dialog => {
|
|
404
|
+
const res = {
|
|
405
|
+
defaultValue: dialog.defaultValue(),
|
|
406
|
+
message: dialog.message(),
|
|
407
|
+
type: dialog.type()
|
|
408
|
+
};
|
|
409
|
+
dialog.accept();
|
|
410
|
+
this.getWorld().shared.set(where, res);
|
|
411
|
+
}));
|
|
412
|
+
return OK;
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
takeScreenshot: {
|
|
416
|
+
gwta: 'take a screenshot',
|
|
417
|
+
action: async () => {
|
|
418
|
+
const loc = { ...this.getWorld(), mediaType: "image" /* EMediaTypes.image */ };
|
|
419
|
+
const dir = await this.storage.ensureCaptureLocation(loc, 'screenshots');
|
|
420
|
+
await this.withPage(async (page) => await page.screenshot({
|
|
421
|
+
path: `${dir}/screenshot-${Date.now()}.png`,
|
|
422
|
+
}));
|
|
423
|
+
return OK;
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
assertOpen: {
|
|
427
|
+
gwta: '{what} is expanded with the {using}',
|
|
428
|
+
action: async ({ what, using }) => {
|
|
429
|
+
const isVisible = await this.withPage(async (page) => await page.isVisible(what));
|
|
430
|
+
if (!isVisible) {
|
|
431
|
+
await this.withPage(async (page) => await page.click(using));
|
|
432
|
+
}
|
|
433
|
+
return OK;
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
setToURIQueryParameter: {
|
|
437
|
+
gwta: 'save URI query parameter {what} to {where}',
|
|
438
|
+
action: async ({ what, where }) => {
|
|
439
|
+
const uri = await this.withPage(async (page) => await page.url());
|
|
440
|
+
const found = new URL(uri).searchParams.get(what);
|
|
441
|
+
this.getWorld().shared.set(where, found);
|
|
442
|
+
return OK;
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
export default WebPlaywright;
|
|
460
448
|
//# sourceMappingURL=web-playwright.js.map
|