@letsrunit/controller 0.3.8 → 0.3.10
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/dist/index.d.ts +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/controller.ts +6 -5
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ declare class Controller {
|
|
|
22
22
|
private world;
|
|
23
23
|
readonly journal: Journal;
|
|
24
24
|
private readonly pendingArtifacts;
|
|
25
|
-
static
|
|
25
|
+
static selectorsAreRegistered: boolean;
|
|
26
26
|
private constructor();
|
|
27
27
|
get lang(): {
|
|
28
28
|
code: string;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { typeDefinitions, stepsDefinitions, toFile } from '@letsrunit/bdd';
|
|
2
2
|
import { parseFeature } from '@letsrunit/gherkin';
|
|
3
3
|
import { Journal } from '@letsrunit/journal';
|
|
4
|
-
import { createFieldEngine, createDateEngine, browse, snapshot, scrollToCenter, formatHtml, screenshot,
|
|
4
|
+
import { createFieldEngine, createDateEngine, browse, snapshot, scrollToCenter, formatHtml, screenshot, fuzzyLocator } from '@letsrunit/playwright';
|
|
5
5
|
import { omit, clean, hashKey } from '@letsrunit/utils';
|
|
6
6
|
import { selectors, chromium } from '@playwright/test';
|
|
7
7
|
import { Runner } from '@letsrunit/gherker';
|
|
@@ -23,7 +23,7 @@ var Controller = class _Controller {
|
|
|
23
23
|
this.journal = journal;
|
|
24
24
|
this.pendingArtifacts = pendingArtifacts;
|
|
25
25
|
}
|
|
26
|
-
static
|
|
26
|
+
static selectorsAreRegistered = false;
|
|
27
27
|
get lang() {
|
|
28
28
|
return this.world.lang ?? null;
|
|
29
29
|
}
|
|
@@ -31,10 +31,11 @@ var Controller = class _Controller {
|
|
|
31
31
|
return this.world.page;
|
|
32
32
|
}
|
|
33
33
|
static async registerFieldSelector() {
|
|
34
|
-
if (this.
|
|
34
|
+
if (this.selectorsAreRegistered) return;
|
|
35
35
|
try {
|
|
36
36
|
await selectors.register("field", createFieldEngine);
|
|
37
37
|
await selectors.register("date", createDateEngine);
|
|
38
|
+
this.selectorsAreRegistered = true;
|
|
38
39
|
} catch {
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -129,11 +130,11 @@ var Controller = class _Controller {
|
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
async getLocatorArgs(page, args) {
|
|
132
|
-
const promises = args.filter((arg) => arg.getParameterType().name === "locator").map((arg) => arg.getValue(null)).filter((arg) => arg !== null).map((arg) =>
|
|
133
|
+
const promises = args.filter((arg) => arg.getParameterType().name === "locator").map((arg) => arg.getValue(null)).filter((arg) => arg !== null).map((arg) => fuzzyLocator(page, arg));
|
|
133
134
|
return Promise.all(promises);
|
|
134
135
|
}
|
|
135
136
|
async areAllVisible(locators) {
|
|
136
|
-
if (
|
|
137
|
+
if (locators.length === 0) return true;
|
|
137
138
|
const visible = await Promise.all(locators.map((l) => l.isVisible()));
|
|
138
139
|
return visible.every(Boolean);
|
|
139
140
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runner/dsl.ts","../src/controller.ts","../../utils/src/path.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import { stepsDefinitions, typeDefinitions, type World } from '@letsrunit/bdd';\nimport { Runner } from '@letsrunit/gherker';\n\nexport const runner = new Runner<World>();\n\nfor (const type of typeDefinitions) {\n runner.defineParameterType(type);\n}\n\nfor (const step of stepsDefinitions) {\n runner.defineStep(step.type, step.expression, step.fn, step.comment);\n}\n","import type { Argument } from '@cucumber/cucumber-expressions';\nimport { toFile, type World } from '@letsrunit/bdd';\nimport type { ParsedStep, StepDescription, StepResult } from '@letsrunit/gherker';\nimport { parseFeature } from '@letsrunit/gherkin';\nimport { Journal } from '@letsrunit/journal';\nimport {\n browse,\n createDateEngine,\n createFieldEngine,\n formatHtml,\n locator,\n screenshot,\n scrollToCenter,\n snapshot,\n} from '@letsrunit/playwright';\nimport { clean, hashKey, omit, type RequireOnly } from '@letsrunit/utils';\nimport {\n type Browser,\n type BrowserContextOptions,\n chromium,\n type Locator,\n type Page,\n type PageScreenshotOptions,\n selectors,\n} from '@playwright/test';\nimport { runner } from './runner';\nimport type { Result } from './types';\n\nexport interface ControllerOptions extends BrowserContextOptions {\n headless?: boolean;\n debug?: boolean;\n journal?: Journal;\n}\n\nexport interface RunOptions {\n signal?: AbortSignal;\n}\n\nexport class Controller {\n static fieldSelectorIsRegistered: boolean = false;\n\n private constructor(\n private browser: Browser,\n private world: World,\n readonly journal: Journal,\n private readonly pendingArtifacts: Set<File>,\n ) {}\n\n get lang(): { code: string; name: string } | null {\n return this.world.lang ?? null;\n }\n\n get page(): Page {\n return this.world.page;\n }\n\n static async registerFieldSelector() {\n if (this.fieldSelectorIsRegistered) return;\n try {\n await selectors.register('field', createFieldEngine);\n await selectors.register('date', createDateEngine);\n } catch {}\n }\n\n static createWorld(\n page: Page,\n options: RequireOnly<ControllerOptions, 'journal'>,\n pendingArtifacts: Set<File>,\n ): World {\n const journal = options.journal;\n\n return {\n page,\n parameters: omit(options, ['headless', 'journal', 'debug']),\n startTime: Date.now(),\n async attach(data, options) {\n pendingArtifacts.add(toFile(data, options));\n },\n async link(text: string) {\n await journal.info(text);\n },\n async log(text: string) {\n await journal.info(text);\n },\n };\n }\n\n static async launch(options: ControllerOptions = {}): Promise<Controller> {\n await this.registerFieldSelector();\n\n const browser = await chromium.launch({ headless: options.headless ?? true });\n const page = await browse(browser, options);\n\n if (options.debug) {\n page.on('console', (msg) => {\n console.log('[page]', msg.type(), msg.text());\n });\n }\n\n const journal = options.journal ?? Journal.nil();\n const pendingArtifacts = new Set<File>();\n const world = this.createWorld(page, { ...options, journal }, pendingArtifacts);\n\n return new Controller(browser, world, journal, pendingArtifacts);\n }\n\n async run(feature: string, opts: RunOptions = {}): Promise<Result> {\n await this.logFeature(feature);\n\n const { world: _, ...result } = await runner.run(feature, this.world, (...args) => this.runStep(...args), opts);\n const page = await snapshot(this.world.page);\n\n return { ...result, page };\n }\n\n validate(feature: string): { valid: boolean; steps: ParsedStep[] } {\n const steps = runner.parse(feature);\n const valid = steps.every((step) => !!step.def);\n\n return { valid, steps };\n }\n\n async close(): Promise<void> {\n await this.browser.close();\n }\n\n listSteps(type?: 'Given' | 'When' | 'Then'): string[] {\n return runner.defs\n .filter((def) => def.comment !== 'hidden' && (!type || def.type === type))\n .map((def) => `${def.type} ${def.source}` + (def.comment ? ` # ${def.comment}` : ''));\n }\n\n private async runStep(step: StepDescription, run: () => Promise<StepResult>): Promise<StepResult> {\n const locators = !step.text.match(/\\b(don't see|not contains)\\b/i)\n ? await this.getLocatorArgs(this.world.page, step.args)\n : [];\n\n if (locators.length > 0) {\n await scrollToCenter(locators[0]);\n }\n\n const urlBefore = this.world.page.url();\n const screenshotBefore = urlBefore !== 'about:blank' ? await this.makeScreenshot({ mask: locators }) : undefined;\n const htmlBefore = await this.makeHtmlFile();\n await this.journal.start(step.text, { artifacts: clean([screenshotBefore, htmlBefore]) });\n\n const result = await run();\n\n const urlAfter = this.world.page.url();\n const screenshotAfter =\n (!screenshotBefore && urlAfter !== 'about:blank') ||\n (!step.text.startsWith('Then') && urlAfter === urlBefore && (await this.areAllVisible(locators)))\n ? await this.makeScreenshot({ mask: locators })\n : undefined;\n\n await this.journal\n .batch()\n .log(step.text, {\n type: result.status,\n artifacts: clean([screenshotAfter, ...this.pendingArtifacts]),\n })\n .error(result.reason?.message)\n .flush();\n\n this.pendingArtifacts.clear();\n\n return result;\n }\n\n private async makeHtmlFile(): Promise<File | undefined> {\n try {\n const html = await formatHtml(this.world.page);\n const filename = await hashKey('{hash}.html', html);\n return new File([html], filename, { type: 'text/html' });\n } catch (e) {\n const message = (e as any).message ?? String(e);\n await this.journal.warn(`Failed to get HTML of ${this.world.page.url()}: ${message}`, {\n meta: { reason: e },\n });\n }\n }\n\n private async makeScreenshot(options?: PageScreenshotOptions): Promise<File | undefined> {\n try {\n const mask = (\n await Promise.all(options?.mask?.map((loc) => loc.isVisible().then((v) => (v ? loc : null))) ?? [])\n ).filter(Boolean) as Locator[];\n\n return await screenshot(this.world.page, { ...options, mask });\n } catch (e) {\n const message = (e as any).message ?? String(e);\n await this.journal.warn(`Failed to take screenshot of ${this.world.page.url()}: ${message}`, {\n meta: { reason: e },\n });\n }\n }\n\n private async getLocatorArgs(page: Page, args: readonly Argument[]): Promise<Locator[]> {\n const promises = args\n .filter((arg) => arg.getParameterType().name === 'locator')\n .map((arg) => arg.getValue<string>(null))\n .filter((arg) => arg !== null)\n .map((arg) => locator(page, arg));\n\n return Promise.all(promises);\n }\n\n private async areAllVisible(locators: Locator[]): Promise<boolean> {\n if (locator.length === 0) return true;\n\n const visible = await Promise.all(locators.map((l) => l.isVisible()));\n return visible.every(Boolean);\n }\n\n private async logFeature(feature: string): Promise<void> {\n const journal = this.journal.batch();\n const { name, description, background, steps } = parseFeature(feature);\n\n try {\n if (name) journal.title(name);\n if (description) journal.info(description);\n\n for (const step of [...(background ?? []), ...steps]) {\n journal.prepare(step);\n }\n } finally {\n await journal.flush();\n }\n }\n}\n","export function splitUrl(url: string): { base: string; path: string } {\n const parsed = new URL(url);\n const base = `${parsed.protocol}//${parsed.host}`;\n const path = parsed.pathname + parsed.search + parsed.hash;\n return { base, path };\n}\n\nexport function asFilename(name: string, ext?: string): string {\n const filename = name\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-') // collapse to dashes\n .replace(/^-+|-+$/g, ''); // trim dashes\n\n return filename + (ext ? `.${ext}` : '');\n}\n\n// Build a regex from the pattern (eg `/books/:id`) to extract params\nexport function pathRegexp(path: string): { regexp: RegExp, names: string[] } {\n const names: string[] = [];\n\n const escape = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const pattern = path\n .split('/')\n .map((seg) => {\n if (seg.startsWith(':')) {\n names.push(seg.slice(1));\n return '([^/]+)';\n }\n return escape(seg);\n })\n .join('/');\n\n const regexp = new RegExp(`^${pattern}$`);\n\n return { regexp, names };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/runner/dsl.ts","../src/controller.ts","../../utils/src/path.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import { stepsDefinitions, typeDefinitions, type World } from '@letsrunit/bdd';\nimport { Runner } from '@letsrunit/gherker';\n\nexport const runner = new Runner<World>();\n\nfor (const type of typeDefinitions) {\n runner.defineParameterType(type);\n}\n\nfor (const step of stepsDefinitions) {\n runner.defineStep(step.type, step.expression, step.fn, step.comment);\n}\n","import type { Argument } from '@cucumber/cucumber-expressions';\nimport { toFile, type World } from '@letsrunit/bdd';\nimport type { ParsedStep, StepDescription, StepResult } from '@letsrunit/gherker';\nimport { parseFeature } from '@letsrunit/gherkin';\nimport { Journal } from '@letsrunit/journal';\nimport {\n browse,\n createDateEngine,\n createFieldEngine,\n formatHtml,\n fuzzyLocator,\n screenshot,\n scrollToCenter,\n snapshot,\n} from '@letsrunit/playwright';\nimport { clean, hashKey, omit, type RequireOnly } from '@letsrunit/utils';\nimport {\n type Browser,\n type BrowserContextOptions,\n chromium,\n type Locator,\n type Page,\n type PageScreenshotOptions,\n selectors,\n} from '@playwright/test';\nimport { runner } from './runner';\nimport type { Result } from './types';\n\nexport interface ControllerOptions extends BrowserContextOptions {\n headless?: boolean;\n debug?: boolean;\n journal?: Journal;\n}\n\nexport interface RunOptions {\n signal?: AbortSignal;\n}\n\nexport class Controller {\n static selectorsAreRegistered: boolean = false;\n\n private constructor(\n private browser: Browser,\n private world: World,\n readonly journal: Journal,\n private readonly pendingArtifacts: Set<File>,\n ) {}\n\n get lang(): { code: string; name: string } | null {\n return this.world.lang ?? null;\n }\n\n get page(): Page {\n return this.world.page;\n }\n\n static async registerFieldSelector() {\n if (this.selectorsAreRegistered) return;\n try {\n await selectors.register('field', createFieldEngine);\n await selectors.register('date', createDateEngine);\n this.selectorsAreRegistered = true;\n } catch {}\n }\n\n static createWorld(\n page: Page,\n options: RequireOnly<ControllerOptions, 'journal'>,\n pendingArtifacts: Set<File>,\n ): World {\n const journal = options.journal;\n\n return {\n page,\n parameters: omit(options, ['headless', 'journal', 'debug']),\n startTime: Date.now(),\n async attach(data, options) {\n pendingArtifacts.add(toFile(data, options));\n },\n async link(text: string) {\n await journal.info(text);\n },\n async log(text: string) {\n await journal.info(text);\n },\n };\n }\n\n static async launch(options: ControllerOptions = {}): Promise<Controller> {\n await this.registerFieldSelector();\n\n const browser = await chromium.launch({ headless: options.headless ?? true });\n const page = await browse(browser, options);\n\n if (options.debug) {\n page.on('console', (msg) => {\n console.log('[page]', msg.type(), msg.text());\n });\n }\n\n const journal = options.journal ?? Journal.nil();\n const pendingArtifacts = new Set<File>();\n const world = this.createWorld(page, { ...options, journal }, pendingArtifacts);\n\n return new Controller(browser, world, journal, pendingArtifacts);\n }\n\n async run(feature: string, opts: RunOptions = {}): Promise<Result> {\n await this.logFeature(feature);\n\n const { world: _, ...result } = await runner.run(feature, this.world, (...args) => this.runStep(...args), opts);\n const page = await snapshot(this.world.page);\n\n return { ...result, page };\n }\n\n validate(feature: string): { valid: boolean; steps: ParsedStep[] } {\n const steps = runner.parse(feature);\n const valid = steps.every((step) => !!step.def);\n\n return { valid, steps };\n }\n\n async close(): Promise<void> {\n await this.browser.close();\n }\n\n listSteps(type?: 'Given' | 'When' | 'Then'): string[] {\n return runner.defs\n .filter((def) => def.comment !== 'hidden' && (!type || def.type === type))\n .map((def) => `${def.type} ${def.source}` + (def.comment ? ` # ${def.comment}` : ''));\n }\n\n private async runStep(step: StepDescription, run: () => Promise<StepResult>): Promise<StepResult> {\n const locators = !step.text.match(/\\b(don't see|not contains)\\b/i)\n ? await this.getLocatorArgs(this.world.page, step.args)\n : [];\n\n if (locators.length > 0) {\n await scrollToCenter(locators[0]);\n }\n\n const urlBefore = this.world.page.url();\n const screenshotBefore = urlBefore !== 'about:blank' ? await this.makeScreenshot({ mask: locators }) : undefined;\n const htmlBefore = await this.makeHtmlFile();\n await this.journal.start(step.text, { artifacts: clean([screenshotBefore, htmlBefore]) });\n\n const result = await run();\n\n const urlAfter = this.world.page.url();\n const screenshotAfter =\n (!screenshotBefore && urlAfter !== 'about:blank') ||\n (!step.text.startsWith('Then') && urlAfter === urlBefore && (await this.areAllVisible(locators)))\n ? await this.makeScreenshot({ mask: locators })\n : undefined;\n\n await this.journal\n .batch()\n .log(step.text, {\n type: result.status,\n artifacts: clean([screenshotAfter, ...this.pendingArtifacts]),\n })\n .error(result.reason?.message)\n .flush();\n\n this.pendingArtifacts.clear();\n\n return result;\n }\n\n private async makeHtmlFile(): Promise<File | undefined> {\n try {\n const html = await formatHtml(this.world.page);\n const filename = await hashKey('{hash}.html', html);\n return new File([html], filename, { type: 'text/html' });\n } catch (e) {\n const message = (e as any).message ?? String(e);\n await this.journal.warn(`Failed to get HTML of ${this.world.page.url()}: ${message}`, {\n meta: { reason: e },\n });\n }\n }\n\n private async makeScreenshot(options?: PageScreenshotOptions): Promise<File | undefined> {\n try {\n const mask = (\n await Promise.all(options?.mask?.map((loc) => loc.isVisible().then((v) => (v ? loc : null))) ?? [])\n ).filter(Boolean) as Locator[];\n\n return await screenshot(this.world.page, { ...options, mask });\n } catch (e) {\n const message = (e as any).message ?? String(e);\n await this.journal.warn(`Failed to take screenshot of ${this.world.page.url()}: ${message}`, {\n meta: { reason: e },\n });\n }\n }\n\n private async getLocatorArgs(page: Page, args: readonly Argument[]): Promise<Locator[]> {\n const promises = args\n .filter((arg) => arg.getParameterType().name === 'locator')\n .map((arg) => arg.getValue<string>(null))\n .filter((arg) => arg !== null)\n .map((arg) => fuzzyLocator(page, arg));\n\n return Promise.all(promises);\n }\n\n private async areAllVisible(locators: Locator[]): Promise<boolean> {\n if (locators.length === 0) return true;\n\n const visible = await Promise.all(locators.map((l) => l.isVisible()));\n return visible.every(Boolean);\n }\n\n private async logFeature(feature: string): Promise<void> {\n const journal = this.journal.batch();\n const { name, description, background, steps } = parseFeature(feature);\n\n try {\n if (name) journal.title(name);\n if (description) journal.info(description);\n\n for (const step of [...(background ?? []), ...steps]) {\n journal.prepare(step);\n }\n } finally {\n await journal.flush();\n }\n }\n}\n","export function splitUrl(url: string): { base: string; path: string } {\n const parsed = new URL(url);\n const base = `${parsed.protocol}//${parsed.host}`;\n const path = parsed.pathname + parsed.search + parsed.hash;\n return { base, path };\n}\n\nexport function asFilename(name: string, ext?: string): string {\n const filename = name\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-') // collapse to dashes\n .replace(/^-+|-+$/g, ''); // trim dashes\n\n return filename + (ext ? `.${ext}` : '');\n}\n\n// Build a regex from the pattern (eg `/books/:id`) to extract params\nexport function pathRegexp(path: string): { regexp: RegExp, names: string[] } {\n const names: string[] = [];\n\n const escape = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const pattern = path\n .split('/')\n .map((seg) => {\n if (seg.startsWith(':')) {\n names.push(seg.slice(1));\n return '([^/]+)';\n }\n return escape(seg);\n })\n .join('/');\n\n const regexp = new RegExp(`^${pattern}$`);\n\n return { regexp, names };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letsrunit/controller",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Browser automation controller for letsrunit",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
},
|
|
46
46
|
"packageManager": "yarn@4.10.3",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@letsrunit/bdd": "0.3.
|
|
49
|
-
"@letsrunit/gherker": "0.3.
|
|
50
|
-
"@letsrunit/gherkin": "0.3.
|
|
51
|
-
"@letsrunit/journal": "0.3.
|
|
52
|
-
"@letsrunit/playwright": "0.3.
|
|
53
|
-
"@letsrunit/utils": "0.3.
|
|
48
|
+
"@letsrunit/bdd": "0.3.10",
|
|
49
|
+
"@letsrunit/gherker": "0.3.10",
|
|
50
|
+
"@letsrunit/gherkin": "0.3.10",
|
|
51
|
+
"@letsrunit/journal": "0.3.10",
|
|
52
|
+
"@letsrunit/playwright": "0.3.10",
|
|
53
|
+
"@letsrunit/utils": "0.3.10",
|
|
54
54
|
"@playwright/test": "^1.57.0",
|
|
55
55
|
"jsdom": "^27.4.0",
|
|
56
56
|
"zod": "^4.3.5"
|
package/src/controller.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
createDateEngine,
|
|
9
9
|
createFieldEngine,
|
|
10
10
|
formatHtml,
|
|
11
|
-
|
|
11
|
+
fuzzyLocator,
|
|
12
12
|
screenshot,
|
|
13
13
|
scrollToCenter,
|
|
14
14
|
snapshot,
|
|
@@ -37,7 +37,7 @@ export interface RunOptions {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export class Controller {
|
|
40
|
-
static
|
|
40
|
+
static selectorsAreRegistered: boolean = false;
|
|
41
41
|
|
|
42
42
|
private constructor(
|
|
43
43
|
private browser: Browser,
|
|
@@ -55,10 +55,11 @@ export class Controller {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
static async registerFieldSelector() {
|
|
58
|
-
if (this.
|
|
58
|
+
if (this.selectorsAreRegistered) return;
|
|
59
59
|
try {
|
|
60
60
|
await selectors.register('field', createFieldEngine);
|
|
61
61
|
await selectors.register('date', createDateEngine);
|
|
62
|
+
this.selectorsAreRegistered = true;
|
|
62
63
|
} catch {}
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -200,13 +201,13 @@ export class Controller {
|
|
|
200
201
|
.filter((arg) => arg.getParameterType().name === 'locator')
|
|
201
202
|
.map((arg) => arg.getValue<string>(null))
|
|
202
203
|
.filter((arg) => arg !== null)
|
|
203
|
-
.map((arg) =>
|
|
204
|
+
.map((arg) => fuzzyLocator(page, arg));
|
|
204
205
|
|
|
205
206
|
return Promise.all(promises);
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
private async areAllVisible(locators: Locator[]): Promise<boolean> {
|
|
209
|
-
if (
|
|
210
|
+
if (locators.length === 0) return true;
|
|
210
211
|
|
|
211
212
|
const visible = await Promise.all(locators.map((l) => l.isVisible()));
|
|
212
213
|
return visible.every(Boolean);
|