@immense/vue-pom-generator 1.0.3 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,69 +47,87 @@ import vue from "@vitejs/plugin-vue";
47
47
  import { createVuePomGeneratorPlugins } from "@immense/vue-pom-generator";
48
48
 
49
49
  export default defineConfig(() => {
50
- const vueOptions = {
51
- script: { defineModel: true, propsDestructure: true },
52
- };
53
-
54
- return {
55
- plugins: [
56
- ...createVuePomGeneratorPlugins({
57
- vueOptions,
58
- logging: { verbosity: "info" },
59
-
60
- injection: {
61
- // Attribute to inject/read as the test id (default: data-testid)
62
- attribute: "data-testid",
63
-
64
- // Used to classify Vue files as "views" vs components (default: src/views)
65
- viewsDir: "src/views",
66
-
67
- // Optional: wrapper semantics for design-system components
68
- nativeWrappers: {
69
- ImmyButton: { role: "button" },
70
- ImmyInput: { role: "input" },
71
- },
72
-
73
- // Optional: opt specific components out of injection
74
- excludeComponents: ["ImmyButton"],
75
-
76
- // Optional: preserve/overwrite/error when an author already set the attribute
77
- existingIdBehavior: "preserve",
78
- },
79
-
80
- generation: {
81
- // Default: tests/playwright/generated
82
- outDir: "tests/playwright/generated",
83
-
84
- // Controls how to handle duplicate generated member names within a single POM class.
85
- // - "error": fail compilation
86
- // - "warn": warn and suffix
87
- // - "suffix": suffix silently (default)
88
- nameCollisionBehavior: "suffix",
89
-
90
- // Enable router introspection. When provided, router-aware POM helpers are generated.
91
- router: { entry: "src/router.ts" },
92
-
93
- playwright: {
94
- fixtures: true,
95
- customPoms: {
96
- // Default: tests/playwright/pom/custom
97
- dir: "tests/playwright/pom/custom",
98
- importAliases: { ImmyCheckBox: "CheckboxWidget" },
99
- attachments: [
100
- {
101
- className: "ConfirmationModal",
102
- propertyName: "confirmationModal",
103
- attachWhenUsesComponents: ["Page"],
104
- },
105
- ],
106
- },
107
- },
108
- },
109
- }),
110
- vue(vueOptions),
111
- ],
112
- };
50
+ const vueOptions = {
51
+ script: { defineModel: true, propsDestructure: true },
52
+ };
53
+
54
+ return {
55
+ plugins: [
56
+ ...createVuePomGeneratorPlugins({
57
+ vueOptions,
58
+ logging: { verbosity: "info" },
59
+
60
+ injection: {
61
+ // Attribute to inject/read as the test id (default: data-testid)
62
+ attribute: "data-testid",
63
+
64
+ // Used to classify Vue files as "views" vs components (default: src/views)
65
+ viewsDir: "src/views",
66
+
67
+ // Directories to scan for .vue files when building the POM library (default: ["src"])
68
+ // For Nuxt, you might want ["app", "components", "pages", "layouts"]
69
+ scanDirs: ["src"],
70
+
71
+ // Optional: wrapper semantics for design-system components
72
+ nativeWrappers: {
73
+ MyButton: { role: "button" },
74
+ MyInput: { role: "input" },
75
+ },
76
+
77
+ // Optional: opt specific components out of injection
78
+ excludeComponents: ["MyButton"],
79
+
80
+ // Optional: preserve/overwrite/error when an author already set the attribute
81
+ existingIdBehavior: "preserve",
82
+ },
83
+
84
+ generation: {
85
+ // Default: ["ts"]
86
+ emit: ["ts", "csharp"],
87
+
88
+ // C# specific configuration
89
+ csharp: {
90
+ // The namespace for generated C# classes (default: Playwright.Generated)
91
+ namespace: "MyProject.Tests.Generated",
92
+ },
93
+
94
+ // Default: tests/playwright/generated
95
+ outDir: "tests/playwright/generated",
96
+
97
+ // Controls how to handle duplicate generated member names within a single POM class.
98
+ // - "error": fail compilation
99
+ // - "warn": warn and suffix
100
+ // - "suffix": suffix silently (default)
101
+ nameCollisionBehavior: "suffix",
102
+
103
+ // Enable router introspection. When provided, router-aware POM helpers are generated.
104
+ router: {
105
+ // For standard Vue apps:
106
+ entry: "src/router.ts",
107
+ // For Nuxt apps (file-based routing):
108
+ // type: "nuxt"
109
+ },
110
+
111
+ playwright: {
112
+ fixtures: true,
113
+ customPoms: {
114
+ // Default: tests/playwright/pom/custom
115
+ dir: "tests/playwright/pom/custom",
116
+ importAliases: { MyCheckBox: "CheckboxWidget" },
117
+ attachments: [
118
+ {
119
+ className: "ConfirmationModal",
120
+ propertyName: "confirmationModal",
121
+ attachWhenUsesComponents: ["Page"],
122
+ },
123
+ ],
124
+ },
125
+ },
126
+ },
127
+ }),
128
+ vue(vueOptions),
129
+ ],
130
+ };
113
131
  });
114
132
  ```
115
133
 
@@ -119,16 +137,12 @@ Notes:
119
137
  - **Generation is enabled by default** and can be disabled via `generation: false`.
120
138
  - **Router-aware POM helpers are enabled** when `generation.router.entry` is provided (the generator will introspect your router).
121
139
 
122
- ### `generation.router.entry: string`
140
+ ### `generation.router`
123
141
 
124
- Controls where router introspection loads your Vue Router definition from (used for `:to` analysis and navigation helper generation).
142
+ Controls router introspection for `:to` analysis and navigation helper generation.
125
143
 
126
- Resolution:
127
-
128
- - relative paths are resolved relative to Vite's resolved `config.root`
129
- - absolute paths are used as-is
130
-
131
- This file must export a **default router factory function** (e.g. `export default makeRouter`).
144
+ - `entry: string`: For standard Vue apps, where router introspection loads your Vue Router definition from. This file must export a **default router factory function** (e.g. `export default makeRouter`).
145
+ - `type: "vue-router" | "nuxt"`: The introspection provider. Defaults to `"vue-router"`. Use `"nuxt"` for file-based routing discovery (e.g. `app/pages` or `pages`).
132
146
 
133
147
  ### `generation.playwright.fixtures: boolean | string | { outDir?: string }`
134
148
 
@@ -0,0 +1,38 @@
1
+ ● ## Highlights
2
+ - Added Nuxt pages introspection to router introspection.
3
+ - Major overhaul of Vue plugin with expanded support/dev/build plugins and types.
4
+ - Introduced Playwright types and updated class generation pipeline.
5
+ - Significant updates to transform and utility modules to support new features.
6
+ - CI improvements: release on main without gate and a pre-push up-to-date guard.
7
+
8
+ ## Changes
9
+ - Router Introspection
10
+ - Enhanced router-introspection.ts to add Nuxt pages introspection functionality.
11
+ - Plugin System
12
+ - Overhauled plugin/vue-plugin.ts (+286/- changes) with expanded capabilities.
13
+ - Updated support plugins: dev-plugin, build-plugin, support-plugins, path-utils, logger, and
14
+ types.
15
+ - Improved create-vue-pom-generator-plugins.ts for plugin creation.
16
+ - Class Generation
17
+ - Added class-generation/playwright-types.ts.
18
+ - Updated BasePage, Pointer, index.ts, and method-generation.ts for generation flow.
19
+ - Transform & Utilities
20
+ - Refactored transform.ts and utils.ts with substantial changes.
21
+ - Minor eslint.config.ts adjustments.
22
+ - CI & Tooling
23
+ - Updated .github/workflows/release.yml and agentic-release-notes.lock.yml.
24
+ - Added scripts/git-hooks/pre-push.sh pre-push guard.
25
+ - Cleaned up .gitignore; updated package.json and package-lock.json for git hooks.
26
+ - Documentation & Logs
27
+ - Linted README.md.
28
+ - Added build_v5.log and build_web_v5.log.
29
+
30
+ ## Pull Requests Included
31
+ - #1 Add PR release-notes preview comments (https://github.com/immense/vue-pom-generator/pull/1)
32
+ by @dkattan
33
+
34
+ ## Testing
35
+ - Updated tests in tests/options.test.ts and tests/utils-coverage.test.ts; adjusted fixture
36
+ MyComp_CancelButtons_InnerText.vue.
37
+ - No new test suites added.
38
+
@@ -1,8 +1,8 @@
1
- import type { Locator as PwLocator, Page as PwPage } from "@playwright/test";
1
+ import type { PwLocator, PwPage } from "./playwright-types";
2
2
  import { TESTID_CLICK_EVENT_NAME, TESTID_CLICK_EVENT_STRICT_FLAG } from "../click-instrumentation";
3
3
  import type { TestIdClickEventDetail } from "../click-instrumentation";
4
4
  import { Pointer } from "./Pointer";
5
- import type { AfterPointerClickInfo } from "./Pointer";
5
+ import type { AfterPointerClick, AfterPointerClickInfo } from "./Pointer";
6
6
 
7
7
  // Click instrumentation is a core contract for generated POMs.
8
8
  const REQUIRE_CLICK_EVENT = true;
@@ -218,6 +218,21 @@ export class BasePage {
218
218
  return this.page.locator(this.selectorForTestId(testId));
219
219
  }
220
220
 
221
+ /**
222
+ * Animates the cursor to an element.
223
+ */
224
+ protected async animateCursorToElement(
225
+ target: string | PwLocator,
226
+ executeClick: boolean = true,
227
+ delayMs: number = 100,
228
+ annotationText: string = "",
229
+ options?: {
230
+ afterClick?: AfterPointerClick;
231
+ },
232
+ ): Promise<void> {
233
+ await this.pointer.animateCursorToElement(target, executeClick, delayMs, annotationText, options);
234
+ }
235
+
221
236
  /**
222
237
  * Creates an indexable proxy for keyed elements so generated POMs can expose
223
238
  * ergonomic accessors like:
@@ -1,4 +1,4 @@
1
- import type { Locator as PwLocator, Page as PwPage } from "@playwright/test";
1
+ import type { PwLocator, PwPage } from "./playwright-types";
2
2
 
3
3
  export interface PlaywrightAnimationOptions {
4
4
  /**
@@ -3,9 +3,7 @@ import path from "node:path";
3
3
  import process from "node:process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { generateViewObjectModelMethodContent } from "../method-generation";
6
- import { parseRouterFileFromCwd } from "../router-introspection";
7
- // NOTE: This module intentionally does not depend on Babel parsing.
8
-
6
+ import { introspectNuxtPages, parseRouterFileFromCwd } from "../router-introspection";
9
7
  import { IComponentDependencies, IDataTestId, PomExtraClickMethodSpec, PomPrimarySpec, upperFirst } from "../utils";
10
8
 
11
9
  // Intentionally imported so tooling understands this exported helper is part of the
@@ -62,9 +60,16 @@ interface RouteMeta {
62
60
  template: string;
63
61
  }
64
62
 
65
- async function getRouteMetaByComponent(projectRoot?: string, routerEntry?: string): Promise<Record<string, RouteMeta>> {
66
- const resolvedRouterEntry = resolveRouterEntry(projectRoot, routerEntry);
67
- const { routeMetaEntries } = await parseRouterFileFromCwd(resolvedRouterEntry);
63
+ async function getRouteMetaByComponent(
64
+ projectRoot?: string,
65
+ routerEntry?: string,
66
+ routerType?: "vue-router" | "nuxt",
67
+ ): Promise<Record<string, RouteMeta>> {
68
+ const root = projectRoot ?? process.cwd();
69
+
70
+ const { routeMetaEntries } = routerType === "nuxt"
71
+ ? await introspectNuxtPages(root)
72
+ : await parseRouterFileFromCwd(resolveRouterEntry(root, routerEntry));
68
73
 
69
74
  const map = new Map<string, RouteMeta[]>();
70
75
  for (const entry of routeMetaEntries) {
@@ -327,12 +332,20 @@ export interface GenerateFilesOptions {
327
332
  /** Which POM languages to emit. Defaults to ["ts"]. */
328
333
  emitLanguages?: Array<"ts" | "csharp">;
329
334
 
335
+ /** C# generation options. */
336
+ csharp?: {
337
+ namespace?: string;
338
+ };
339
+
330
340
  /** When true, generate router-aware helpers like goToSelf() on view POMs. */
331
341
  vueRouterFluentChaining?: boolean;
332
342
 
333
343
  /** Router entry path used for vue-router introspection when fluent chaining is enabled. */
334
344
  routerEntry?: string;
335
345
 
346
+ /** The type of router introspection to perform. */
347
+ routerType?: "vue-router" | "nuxt";
348
+
336
349
  routeMetaByComponent?: Record<string, RouteMeta>;
337
350
  }
338
351
 
@@ -382,8 +395,10 @@ export async function generateFiles(
382
395
  customPomImportAliases,
383
396
  testIdAttribute,
384
397
  emitLanguages: emitLanguagesOverride,
398
+ csharp,
385
399
  vueRouterFluentChaining,
386
400
  routerEntry,
401
+ routerType,
387
402
  } = options;
388
403
 
389
404
  const emitLanguages: Array<"ts" | "csharp"> = emitLanguagesOverride?.length
@@ -393,7 +408,7 @@ export async function generateFiles(
393
408
  const outDir = outDirOverride ?? "./pom";
394
409
 
395
410
  const routeMetaByComponent = vueRouterFluentChaining
396
- ? await getRouteMetaByComponent(projectRoot, routerEntry)
411
+ ? await getRouteMetaByComponent(projectRoot, routerEntry, routerType)
397
412
  : undefined;
398
413
 
399
414
  if (emitLanguages.includes("ts")) {
@@ -421,6 +436,8 @@ export async function generateFiles(
421
436
  if (emitLanguages.includes("csharp")) {
422
437
  const csFiles = generateAggregatedCSharpFiles(componentHierarchyMap, outDir, {
423
438
  projectRoot,
439
+ testIdAttribute,
440
+ csharp,
424
441
  });
425
442
  for (const file of csFiles) {
426
443
  createFile(file.filePath, file.content);
@@ -454,11 +471,11 @@ function toCSharpParam(paramTypeExpr: string): { type: string; defaultExpr?: str
454
471
  const typePart = left.includes("|") ? "string" : left;
455
472
 
456
473
  let type = "string";
457
- if (/(^|\s)boolean(\s|$)/.test(typePart))
474
+ if (/(?:^|\s)boolean(?:\s|$)/.test(typePart))
458
475
  type = "bool";
459
- else if (/(^|\s)string(\s|$)/.test(typePart))
476
+ else if (/(?:^|\s)string(?:\s|$)/.test(typePart))
460
477
  type = "string";
461
- else if (/(^|\s)number(\s|$)/.test(typePart))
478
+ else if (/(?:^|\s)number(?:\s|$)/.test(typePart))
462
479
  type = "int";
463
480
  else if (/\d+/.test(typePart) && typePart === "")
464
481
  type = "int";
@@ -508,10 +525,17 @@ function formatCSharpParams(params: Record<string, string> | undefined): { signa
508
525
  function generateAggregatedCSharpFiles(
509
526
  componentHierarchyMap: Map<string, IComponentDependencies>,
510
527
  outDir: string,
511
- options: { projectRoot?: string } = {},
528
+ options: {
529
+ projectRoot?: string;
530
+ testIdAttribute?: string;
531
+ csharp?: {
532
+ namespace?: string;
533
+ };
534
+ } = {},
512
535
  ): Array<{ filePath: string; content: string }> {
513
- const projectRoot = options.projectRoot ?? process.cwd();
514
- const outAbs = path.isAbsolute(outDir) ? outDir : path.resolve(projectRoot, outDir);
536
+ const outAbs = ensureDir(outDir);
537
+ const namespace = options.csharp?.namespace ?? "Playwright.Generated";
538
+ const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
515
539
 
516
540
  const entries = Array.from(componentHierarchyMap.entries()).sort((a, b) => a[0].localeCompare(b[0]));
517
541
 
@@ -527,13 +551,13 @@ function generateAggregatedCSharpFiles(
527
551
  "using System.Threading.Tasks;",
528
552
  "using Microsoft.Playwright;",
529
553
  "",
530
- "namespace ImmyBot.Playwright.Generated;",
554
+ `namespace ${namespace};`,
531
555
  "",
532
- "public abstract class BasePage",
556
+ "public abstract partial class BasePage",
533
557
  "{",
534
558
  " protected BasePage(IPage page) => Page = page;",
535
559
  " protected IPage Page { get; }",
536
- " protected ILocator LocatorByTestId(string testId) => Page.GetByTestId(testId);",
560
+ ` protected ILocator LocatorByTestId(string testId) => Page.Locator($"[${testIdAttribute}=\\"{testId}\\"]");`,
537
561
  "",
538
562
  " // Minimal vue-select helper mirroring the TS BasePage.selectVSelectByTestId behavior.",
539
563
  " // Note: annotationText is currently a no-op in C# output (we don't render a cursor overlay).",
@@ -559,8 +583,9 @@ function generateAggregatedCSharpFiles(
559
583
  const chunks: string[] = [header];
560
584
 
561
585
  for (const [componentName, deps] of entries) {
586
+ const className = toPascalCaseLocal(componentName);
562
587
  chunks.push(
563
- `public sealed class ${componentName} : BasePage\n{\n public ${componentName}(IPage page) : base(page) { }\n`,
588
+ `public partial class ${className} : BasePage\n{\n public ${className}(IPage page) : base(page) { }\n`,
564
589
  );
565
590
 
566
591
  // Primary specs
@@ -748,7 +773,7 @@ function maybeGenerateFixtureRegistry(
748
773
  projectRoot?: string;
749
774
  },
750
775
  ) {
751
- const { generateFixtures, pomOutDir, projectRoot } = options;
776
+ const { generateFixtures, pomOutDir } = options;
752
777
  if (!generateFixtures)
753
778
  return;
754
779
 
@@ -767,7 +792,7 @@ function maybeGenerateFixtureRegistry(
767
792
  const fixtureOutDirRel = looksLikeFilePath ? path.dirname(fixtureOutRel) : fixtureOutRel;
768
793
  const fixtureFileName = looksLikeFilePath ? path.basename(fixtureOutRel) : "fixtures.g.ts";
769
794
 
770
- const root = projectRoot ?? process.cwd();
795
+ const root = options.projectRoot ?? process.cwd();
771
796
  const fixtureOutDirAbs = path.isAbsolute(fixtureOutDirRel)
772
797
  ? fixtureOutDirRel
773
798
  : path.resolve(root, fixtureOutDirRel);
@@ -1132,8 +1157,7 @@ async function generateAggregatedFiles(
1132
1157
  outputDir: string,
1133
1158
  items: Array<[string, IComponentDependencies]>,
1134
1159
  ) => {
1135
- // Alias Playwright types to avoid collisions with generated classes (e.g. a Vue component named `Page`).
1136
- const imports: string[] = ["import type { Locator as PwLocator, Page as PwPage } from \"@playwright/test\";"];
1160
+ const imports: string[] = [];
1137
1161
 
1138
1162
  if (!basePageClassPath) {
1139
1163
  throw new Error("basePageClassPath is required for aggregated generation");
@@ -1158,6 +1182,20 @@ async function generateAggregatedFiles(
1158
1182
  "}",
1159
1183
  ].join("\n");
1160
1184
 
1185
+ const inlinePlaywrightTypesModule = () => {
1186
+ const typesPath = fileURLToPath(new URL("./playwright-types.ts", import.meta.url));
1187
+
1188
+ let typesSource = "";
1189
+ try {
1190
+ typesSource = fs.readFileSync(typesPath, "utf8");
1191
+ }
1192
+ catch {
1193
+ throw new Error(`Failed to read playwright-types.ts at ${typesPath}`);
1194
+ }
1195
+
1196
+ return typesSource.trim();
1197
+ };
1198
+
1161
1199
  const inlinePointerModule = () => {
1162
1200
  // Inline Pointer.ts from this package so generated POMs are self-contained and do not
1163
1201
  // rely on runtime TS module resolution within workspace packages.
@@ -1190,6 +1228,12 @@ async function generateAggregatedFiles(
1190
1228
  "",
1191
1229
  );
1192
1230
 
1231
+ // The aggregated file inlines these structural types once at the top.
1232
+ pointerSource = pointerSource.replace(
1233
+ /import\s+type\s*\{\s*PwLocator\s*,\s*PwPage\s*\}\s*from\s*["']\.\/playwright-types["'];?\s*/,
1234
+ "",
1235
+ );
1236
+
1193
1237
  return pointerSource.trim();
1194
1238
  };
1195
1239
 
@@ -1222,15 +1266,22 @@ async function generateAggregatedFiles(
1222
1266
  "",
1223
1267
  );
1224
1268
 
1269
+ // The aggregated file inlines these structural types once at the top.
1270
+ basePageSource = basePageSource.replace(
1271
+ /import\s+type\s*\{\s*PwLocator\s*,\s*PwPage\s*\}\s*from\s*["']\.\/playwright-types["'];?\s*/,
1272
+ "",
1273
+ );
1274
+
1225
1275
  // BasePage references Pointer, but in aggregated output we inline Pointer above.
1226
1276
  basePageSource = basePageSource.replace(
1227
- /import\s*\{\s*Pointer\s*\}\s*from\s*["']\.\/Pointer["'];?\s*/g,
1277
+ /import\s+(?:type\s*)?\{[\s\S]*?\}\s*from\s*["']\.\/Pointer["'];?\s*/g,
1228
1278
  "",
1229
1279
  );
1230
1280
 
1231
1281
  return basePageSource.trim();
1232
1282
  };
1233
1283
 
1284
+ const playwrightTypesInline = inlinePlaywrightTypesModule();
1234
1285
  const pointerInline = inlinePointerModule();
1235
1286
  const basePageInline = inlineBasePageModule();
1236
1287
 
@@ -1244,7 +1295,7 @@ async function generateAggregatedFiles(
1244
1295
  const importAliases: Record<string, string> = {
1245
1296
  Toggle: "ToggleWidget",
1246
1297
  Checkbox: "CheckboxWidget",
1247
- ...(options.customPomImportAliases ?? {}),
1298
+ ...(options.customPomImportAliases),
1248
1299
  };
1249
1300
 
1250
1301
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
@@ -1489,6 +1540,8 @@ async function generateAggregatedFiles(
1489
1540
  header,
1490
1541
  ...imports,
1491
1542
  "",
1543
+ playwrightTypesInline,
1544
+ "",
1492
1545
  pointerInline,
1493
1546
  "",
1494
1547
  basePageInline,
@@ -1502,7 +1555,7 @@ async function generateAggregatedFiles(
1502
1555
 
1503
1556
  const base = ensureDir(outDir);
1504
1557
  const outputFile = path.join(base, "page-object-models.g.ts");
1505
- const header = `${eslintSuppressionHeader}/**\n * Aggregated generated POMs\n${AUTO_GENERATED_COMMENT}`;
1558
+ const header = `/// <reference lib="es2015" />\n${eslintSuppressionHeader}/**\n * Aggregated generated POMs\n${AUTO_GENERATED_COMMENT}`;
1506
1559
  const content = makeAggregatedContent(header, path.dirname(outputFile), [...views, ...components]);
1507
1560
 
1508
1561
  const indexFile = path.join(base, "index.ts");
@@ -0,0 +1,23 @@
1
+ export interface PwLocator {
2
+ locator: (selector: string) => PwLocator;
3
+ first: () => PwLocator;
4
+ count: () => Promise<number>;
5
+ click: (options?: { force?: boolean }) => Promise<void>;
6
+ fill: (value: string, options?: { force?: boolean; timeout?: number }) => Promise<void>;
7
+ getAttribute: (name: string) => Promise<string | null>;
8
+ scrollIntoViewIfNeeded: (options?: { timeout?: number }) => Promise<void>;
9
+ }
10
+
11
+ export type PwSelectOption = string | { value?: string; label?: string; index?: number };
12
+
13
+ export interface PwPage {
14
+ locator: (selector: string) => PwLocator;
15
+ url: () => string;
16
+ waitForTimeout: (timeout: number) => Promise<void>;
17
+ evaluate: <R, Arg>(pageFunction: (arg: Arg) => R | Promise<R>, arg: Arg) => Promise<R>;
18
+ isVisible: (selector: string, options?: { timeout?: number }) => Promise<boolean>;
19
+ textContent: (selector: string, options?: { timeout?: number }) => Promise<string | null>;
20
+ waitForSelector: (selector: string, options?: { timeout?: number }) => Promise<object | null>;
21
+ hover: (selector: string, options?: { timeout?: number }) => Promise<void>;
22
+ selectOption: (selector: string, values: PwSelectOption | PwSelectOption[], options?: { timeout?: number }) => Promise<string[]>;
23
+ }