pw-core 0.0.1 → 0.0.3

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
@@ -6,17 +6,34 @@ A developer-first framework layer built on top of Playwright for creating readab
6
6
 
7
7
  ---
8
8
 
9
- ## Installation
9
+ ## Documentation
10
+
11
+ - **Online Documentation**: [qecore.github.io/pw-core](https://qecore.github.io/pw-core)
12
+ - **Release Guide**: [releases/v1.0.0.md](./releases/v1.0.0.md)
13
+
14
+ ---
15
+
16
+ ## Installation & Setup
17
+
18
+ ### Quick Start (Recommended)
19
+ To initialize a brand new test suite pre-configured with `pw-core`, run:
10
20
 
11
21
  ```bash
12
- npm install pw-core
22
+ npm init pw-core
13
23
  ```
14
24
 
15
- ### Requirements
25
+ This will automatically:
26
+ - Set up a clean folder structure with template examples in `src/pages` and `src/tests`
27
+ - Create `playwright.config.ts` and `tsconfig.json` configurations
28
+ - Install `pw-core`, `@playwright/test`, and `typescript` dependencies
29
+ - Download required Playwright browser binaries
16
30
 
17
- - Existing Playwright project
18
- - TypeScript recommended
19
- - Compatible with supported Playwright versions defined by peer dependency requirements
31
+ ### Manual Installation
32
+ If you want to add `pw-core` to an existing Playwright project, install it manually:
33
+
34
+ ```bash
35
+ npm install pw-core
36
+ ```
20
37
 
21
38
  ---
22
39
 
@@ -34,546 +51,31 @@ npm install pw-core
34
51
 
35
52
  ---
36
53
 
37
- # Why pw-core?
38
-
39
- Most Playwright frameworks start small and manageable.
40
-
41
- As projects grow, they often become difficult to maintain:
42
-
43
- - Locators duplicated across files
44
- - Massive Page Objects
45
- - Repeated fixture definitions
46
- - Components recreated on every page
47
- - Multiple sources of truth
48
- - Inconsistent coding patterns
49
-
50
- The larger the framework becomes, the more expensive these problems become.
51
-
52
- `pw-core` solves this by introducing:
53
-
54
- - Configuration-driven pages
55
- - Typed Page Objects
56
- - Automatic Page Registry fixtures
57
- - Reusable Components
58
- - Chained Locators
59
- - Built-in Assertions
60
- - Browser Storage Helpers
61
-
62
- ---
63
-
64
- # Traditional Playwright vs pw-core
65
-
66
- ## Traditional Approach
67
-
68
- ```ts
69
- class LoginPage {
70
- constructor(private page: Page) {}
71
-
72
- username = this.page.getByTestId("username-input");
73
- password = this.page.getByTestId("password-input");
74
- loginButton = this.page.getByTestId("login-button");
75
- errorMessage = this.page.getByTestId("login-error");
76
- }
77
- ```
78
-
79
- Usage:
80
-
81
- ```ts
82
- await loginPage.username.fill(user);
83
- await loginPage.password.fill(pass);
84
- await loginPage.loginButton.click();
85
-
86
- await expect(loginPage.errorMessage).toBeVisible();
87
- ```
88
-
89
- Problems:
90
-
91
- - Locator definitions
92
- - Large Page Objects
93
- - Boilerplate code
94
- - Multiple maintenance points
95
- - Runtime locator mistakes
96
-
97
- ---
98
-
99
- ## pw-core Approach
100
-
101
- ```ts
102
- const config = createPageConfig({
103
- url: "/login",
104
-
105
- testIds: {
106
- username: "username-input",
107
- password: "password-input",
108
- loginBtn: "login-button",
109
- loginError: "login-error",
110
- },
111
- });
112
-
113
- class LoginPage extends TypedPage<typeof config> {
114
- constructor(page: Page) {
115
- super(page, config);
116
- }
117
- }
118
- ```
119
-
120
- Usage:
121
-
122
- ```ts
123
- await loginPage.fill("username", "admin");
124
- await loginPage.fill("password", "password");
125
- await loginPage.click("loginBtn");
126
- await login.verify("loginError");
127
- ```
128
-
129
- Benefits:
130
-
131
- - Single source of truth
132
- - Strong typing
133
- - Less boilerplate
134
- - Cleaner tests
135
- - Easier maintenance
136
-
137
- ---
138
-
139
- # Single Source of Truth
140
-
141
- Everything begins with a page configuration.
142
-
143
- ```ts
144
- const config = createPageConfig({
145
- url: "/transactions",
146
-
147
- testIds: {
148
- table: "transactions-table",
149
- next: "next-page",
150
- prev: "prev-page",
151
- },
152
-
153
- selectors: {
154
- pageTitle: "h1",
155
- },
156
- });
157
- ```
158
-
159
- This configuration owns:
160
-
161
- - URLs
162
- - testIds
163
- - selectors
164
-
165
- Everything else consumes this configuration.
166
-
167
- ```ts
168
- await page.goto();
169
-
170
- await page.click("next");
171
-
172
- await page.verify("table");
173
-
174
- await page.verifyURL();
175
- ```
176
-
177
- When a locator changes, update it once.
178
-
179
- Not across dozens of tests and page objects.
180
-
181
- ---
182
-
183
- # TypedPage
184
-
185
- `TypedPage` is the foundation of pw-core.
186
-
187
- It transforms page configurations into fully typed automation objects.
188
-
189
- ```ts
190
- class SettingsPage extends TypedPage<typeof config> {
191
- async toggleLogo() {
192
- await this.click("logoToggle");
193
- }
194
- }
195
- ```
196
-
197
- Automatically provides:
198
-
199
- ```ts
200
- await page.goto();
201
-
202
- await page.click("logoToggle");
203
-
204
- await page.fill("username", "admin");
205
-
206
- await page.check("rememberMe");
207
-
208
- await page.hover("profile");
209
-
210
- await page.textContent("message");
211
-
212
- await page.verify("submit");
213
-
214
- await page.verifyURL();
215
- ```
216
-
217
- No locator declarations required.
218
-
219
- ---
220
-
221
- # Readable Assertions
222
-
223
- Traditional:
224
-
225
- ```ts
226
- await expect(page.getByTestId("login-button")).toBeVisible();
227
- ```
228
-
229
- pw-core:
230
-
231
- ```ts
232
- await login.verify("loginBtn");
233
- ```
234
-
235
- By default:
236
-
237
- ```ts
238
- await login.verify("loginBtn");
239
- ```
240
-
241
- is equivalent to:
242
-
243
- ```ts
244
- await expect(page.getByTestId("login-button")).toBeVisible();
245
- ```
246
-
247
- Custom assertions remain available:
248
-
249
- ```ts
250
- await login.verify("loginBtn").toBeEnabled();
251
-
252
- await login.verify("errorMessage").toContainText("Invalid");
253
- ```
254
-
255
- ---
256
-
257
- # Chained Locators
258
-
259
- Complex nested locators are common in large applications.
260
-
261
- Traditional:
262
-
263
- ```ts
264
- await page
265
- .getByTestId("transactions-table")
266
- .getByTestId("transaction-row")
267
- .nth(2)
268
- .dblclick();
269
- ```
270
-
271
- pw-core:
272
-
273
- ```ts
274
- await transactions.dblclick("table.transactionRows", { nth: 2 });
275
- ```
276
-
277
- Traditional:
278
-
279
- ```ts
280
- await page
281
- .locator('[data-testid="modal"]')
282
- .locator('[data-testid="form"]')
283
- .locator('[data-testid="submit"]')
284
- .click();
285
- ```
286
-
287
- pw-core:
288
-
289
- ```ts
290
- await page.click("modal.form.submit");
291
- ```
292
-
293
- Assertions work the same way:
294
-
295
- ```ts
296
- await page.verify("table.transactionRows").toHaveCount(10);
297
- ```
298
-
299
- Nested locator chains stay readable regardless of depth.
300
-
301
- ---
302
-
303
- # Components
304
-
305
- Most applications reuse the same UI patterns:
306
-
307
- - Tables
308
- - Sidebars
309
- - Filters
310
- - Menus
311
- - Forms
312
- - Modals
313
-
314
- Traditional frameworks often recreate these components repeatedly.
315
-
316
- ```ts
317
- class UsersPage {
318
- usersTable = ...
319
- }
320
-
321
- class OrdersPage {
322
- ordersTable = ...
323
- }
324
-
325
- class ProductsPage {
326
- productsTable = ...
327
- }
328
- ```
329
-
330
- Same component.
331
-
332
- Different implementation.
333
-
334
- Duplicate maintenance.
335
-
336
- ---
337
-
338
- ## Reusable Components
339
-
340
- With pw-core, components become first-class citizens.
341
-
342
- ```ts
343
- export class TransactionsPage extends TypedPage<typeof config> {
344
- constructor(page: Page) {
345
- super(page, config);
346
- this.transactionsTable = new Table<Transaction>(tableLocator);
347
- }
348
- }
349
- ```
350
-
351
- Use the same component across:
352
-
353
- - Users
354
- - Orders
355
- - Products
356
- - Reports
357
-
358
- Define once.
359
-
360
- Same methods across pages, Reuse everywhere.
361
-
362
- Exactly how frontend teams build reusable UI components.
363
-
364
- ---
365
-
366
- # Page Registry
367
-
368
- Not every page requires a custom Page Object.
369
-
370
- Many pages only contain:
371
-
372
- - URLs
373
- - testIds
374
- - selectors
375
-
376
- For those pages, use `createPageRegistry`.
377
-
378
- ```ts
379
- import { createPageRegistry } from "pw-core/page";
380
-
381
- export const test = createPageRegistry({
382
- loginPage: {
383
- url: "/login",
384
-
385
- testIds: {
386
- username: "username-input",
387
- password: "password-input",
388
- submit: "login-button",
389
- },
390
- },
391
- });
392
- ```
393
-
394
- ---
395
-
396
- ## Automatic Fixtures
397
-
398
- Pages immediately become fixtures.
399
-
400
- ```ts
401
- test("login", async ({ loginPage }) => {
402
- await loginPage.fill("username", "admin");
403
-
404
- await loginPage.fill("password", "password");
405
-
406
- await loginPage.click("submit");
407
- });
408
- ```
409
-
410
- No Page Object.
411
-
412
- No fixture setup.
413
-
414
- No boilerplate.
415
-
416
- ---
417
-
418
- # Extending Registry Pages
419
-
420
- As pages become more complex, you can extend them.
421
-
422
- ```ts
423
- class LoginPage extends test.classes.loginPage {
424
- async login() {
425
- await this.fill("username", "admin");
426
-
427
- await this.fill("password", "password");
428
-
429
- await this.click("submit");
430
- }
431
- }
432
- ```
433
-
434
- Update the fixture:
435
-
436
- ```ts
437
- export const test = registry.extend({
438
- loginPage: LoginPage,
439
- });
440
- ```
441
-
442
- Usage:
443
-
444
- ```ts
445
- await loginPage.login();
446
- ```
447
-
448
- Start simple.
449
-
450
- Extend only when needed.
451
-
452
- ---
453
-
454
- # AI-Friendly Automation
54
+ ## Available Custom Methods
455
55
 
456
- Most automation frameworks are inconsistent.
56
+ The following custom methods are available on `TypedPage` objects:
457
57
 
458
- One page uses:
58
+ ### Navigation & Lifecycle
59
+ - `goto(options?)`: Navigate to the configured page URL.
60
+ - `reload(options?)`: Reload the current page.
61
+ - `waitForLoadState(state?, options?)`: Wait for the page to reach the specified load state (e.g. `'load' | 'domcontentloaded' | 'networkidle'`).
62
+ - `waitForURL(pattern?, options?)`: Wait for the page navigation to match the configured page URL or a custom pattern.
459
63
 
460
- ```ts
461
- page.locator(...)
462
- ```
64
+ ### Assertions
65
+ - `verify(key, options?)`: Assertions chain wrapper (e.g. `await page.verify('submit').toBeVisible()`). Supports `nth` and `hasText` filtering.
66
+ - `verifyURL(url?, options?)`: Assert the current URL matches the page configuration URL or a custom string/RegExp pattern.
67
+ - `verifyTitle(title, options?)`: Assert the page title matches the expected title string or RegExp pattern.
68
+ - `verifyHidden(key, options?)`: Shortcut assertion to verify a locator is hidden. Supports `nth` and `hasText` options.
69
+ - `verifyEnabled(key, options?)`: Shortcut assertion to verify a locator is enabled. Supports `nth` and `hasText` options.
70
+ - `verifyDisabled(key, options?)`: Shortcut assertion to verify a locator is disabled. Supports `nth` and `hasText` options.
463
71
 
464
- Another:
465
-
466
- ```ts
467
- page.getByTestId(...)
468
- ```
469
-
470
- Another:
471
-
472
- ```ts
473
- page.getByRole(...)
474
- ```
475
-
476
- Another:
477
-
478
- ```ts
479
- page.locator(...).locator(...)
480
- ```
481
-
482
- AI agents must understand every pattern.
483
-
484
- With pw-core:
485
-
486
- ```ts
487
- page.click(...)
488
- page.fill(...)
489
- page.verify(...)
490
- page.locator(...)
491
- ```
492
-
493
- Everything follows the same structure.
494
-
495
- Benefits:
496
-
497
- - Reduced context size
498
- - Reduced token usage
499
- - Better AI-generated automation
500
- - Easier reviews
501
- - Faster onboarding
72
+ ### Locators
73
+ - `expect(key)`: Returns raw Playwright expectation: `expect(locator)`.
74
+ - `resolveLocator(key, options?)`: Returns the raw Playwright Locator resolved from the page config. Options support `{ nth, hasText, raw }`.
75
+ - `locator(key, options?)`: Returns a proxied, step-wrapped Playwright Locator. Options support `{ nth, hasText }`.
502
76
 
503
77
  ---
504
78
 
505
- # Package Overview
506
-
507
- | Import | Purpose |
508
- | ------------------------- | -------------------------------------------------------- |
509
- | `pw-core` | Browser storage helpers |
510
- | `pw-core/page` | TypedPage, Page Registry, assertions, page configuration |
511
- | `pw-core/component/table` | Reusable typed table component |
512
-
513
- ---
514
-
515
- # Documentation
516
-
517
- | Document | Description |
518
- | -------------------- | ------------------------------------------------------ |
519
- | `docs/page.md` | TypedPage, Page Registry, locator chaining, assertions |
520
- | `docs/components.md` | Reusable UI components |
521
- | `docs/helpers.md` | Browser storage helpers |
522
-
523
- ---
524
-
525
- # When To Use TypedPage
526
-
527
- Use TypedPage when:
528
-
529
- - The page contains workflows
530
- - The page contains business logic
531
- - The page contains reusable actions
532
- - Multiple tests interact with the page
533
-
534
- Example:
535
-
536
- ```ts
537
- await login.login();
538
-
539
- await settings.toggleLogo("enable");
540
- ```
541
-
542
- ---
543
-
544
- # When To Use Page Registry
545
-
546
- Use Page Registry when:
547
-
548
- - Pages only contain locators
549
- - No custom methods are needed
550
- - You want automatic fixtures
551
- - You want minimal framework code
552
-
553
- ---
554
-
555
- # Summary
556
-
557
- pw-core is not a replacement for Playwright.
558
-
559
- It is an architectural layer designed to create:
560
-
561
- - Readable tests
562
- - Reusable components
563
- - Strongly typed pages
564
- - Automatic fixtures
565
- - Single source of truth configurations
566
- - AI-friendly automation frameworks
567
- - Scalable enterprise test suites
568
-
569
- The larger the framework becomes, the more value pw-core provides.
570
-
571
- ---
572
-
573
- More components, helpers, generators, and framework capabilities are planned.
574
-
575
- Stay tuned.
576
-
577
79
  ## License
578
80
 
579
81
  MIT
@@ -2,9 +2,11 @@ import { Locator } from '@playwright/test';
2
2
  import { AllowedMethodKeys } from '../config';
3
3
  export declare function executeAction(prop: AllowedMethodKeys, resolveLocatorFn: (target: any, options?: {
4
4
  nth?: number;
5
+ hasText?: string | RegExp;
5
6
  raw?: boolean;
6
7
  }) => Locator, timeout: number | undefined, args: any[]): Promise<any>;
7
8
  export declare function defineActionMethods(instance: any, resolveLocatorFn: (target: any, options?: {
8
9
  nth?: number;
10
+ hasText?: string | RegExp;
9
11
  raw?: boolean;
10
12
  }) => Locator, timeout: number | undefined): void;
@@ -8,15 +8,21 @@ const formatter_1 = require("../utils/formatter");
8
8
  function executeAction(prop, resolveLocatorFn, timeout, args) {
9
9
  const [locatorKey, ...methodArgs] = args;
10
10
  let optNth = undefined;
11
+ let optHasText = undefined;
11
12
  const optionsIndex = (0, config_1.getOptionsArgumentIndex)(prop);
12
13
  if (optionsIndex !== -1 && methodArgs.length > optionsIndex) {
13
14
  const opts = methodArgs[optionsIndex];
14
- if (opts && typeof opts === 'object' && 'nth' in opts) {
15
- optNth = opts.nth;
15
+ if (opts && typeof opts === 'object') {
16
+ if ('nth' in opts) {
17
+ optNth = opts.nth;
18
+ }
19
+ if ('hasText' in opts) {
20
+ optHasText = opts.hasText;
21
+ }
16
22
  }
17
23
  }
18
24
  const isCount = prop === 'count';
19
- const locator = resolveLocatorFn(locatorKey, { nth: optNth, raw: isCount });
25
+ const locator = resolveLocatorFn(locatorKey, { nth: optNth, hasText: optHasText, raw: isCount });
20
26
  const method = locator[prop];
21
27
  if (typeof method !== 'function') {
22
28
  throw new Error(`Property '${prop}' does not exist on Locator.`);
@@ -3,23 +3,29 @@ import { ChainedKeys, PageKeys } from '../config';
3
3
  export type VerifyOptions = {
4
4
  timeout?: number;
5
5
  nth?: number;
6
+ hasText?: string | RegExp;
6
7
  message?: string;
7
8
  };
8
9
  type PlaywrightLocatorMatchers = ReturnType<typeof playwrightExpect<Locator>>;
9
10
  type ModifyMatcherArgs<Args extends any[]> = Args extends [] ? [options?: {
10
11
  nth?: number;
12
+ hasText?: string | RegExp;
11
13
  message?: string;
12
14
  }] : Args extends [any, any?] ? [Args[0], (Exclude<Args[1], undefined> & {
13
15
  nth?: number;
16
+ hasText?: string | RegExp;
14
17
  message?: string;
15
18
  })?] : Args extends [any?] ? Exclude<Args[0], undefined> extends object ? [(Exclude<Args[0], undefined> & {
16
19
  nth?: number;
20
+ hasText?: string | RegExp;
17
21
  message?: string;
18
22
  })?] : [Exclude<Args[0], undefined>, options?: {
19
23
  nth?: number;
24
+ hasText?: string | RegExp;
20
25
  message?: string;
21
26
  }] : [options?: {
22
27
  nth?: number;
28
+ hasText?: string | RegExp;
23
29
  message?: string;
24
30
  }];
25
31
  type DynamicallyModifiedMatchers<T> = {
@@ -28,6 +34,7 @@ type DynamicallyModifiedMatchers<T> = {
28
34
  export type VerifyMatchers<T> = DynamicallyModifiedMatchers<T> & PromiseLike<void> & {
29
35
  (options?: Parameters<PlaywrightLocatorMatchers['toBeVisible']>[0] & {
30
36
  nth?: number;
37
+ hasText?: string | RegExp;
31
38
  message?: string;
32
39
  }): Promise<void>;
33
40
  };
@@ -38,14 +45,17 @@ export type AssertionsMethod<T> = {
38
45
  };
39
46
  verifyHidden(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeHidden']>[0] & {
40
47
  nth?: number;
48
+ hasText?: string | RegExp;
41
49
  message?: string;
42
50
  }): Promise<void>;
43
51
  verifyEnabled(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeEnabled']>[0] & {
44
52
  nth?: number;
53
+ hasText?: string | RegExp;
45
54
  message?: string;
46
55
  }): Promise<void>;
47
56
  verifyDisabled(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeDisabled']>[0] & {
48
57
  nth?: number;
58
+ hasText?: string | RegExp;
49
59
  message?: string;
50
60
  }): Promise<void>;
51
61
  expect(target: PageKeys<T> | ChainedKeys<T> | Locator, message?: string): ReturnType<typeof playwrightExpect<Locator>>;
@@ -58,6 +68,7 @@ export declare function createVerifyChain<T extends {
58
68
  selectors?: Record<string, string>;
59
69
  }>(resolveLocator: (target: any, options?: {
60
70
  nth?: number;
71
+ hasText?: string | RegExp;
61
72
  raw?: boolean;
62
73
  }) => Locator, target: PageKeys<T> | ChainedKeys<T> | Locator, verifyOptions: VerifyOptions | undefined, isSoft: boolean): VerifyMatchers<T>;
63
74
  export {};
@@ -5,14 +5,16 @@ const test_1 = require("@playwright/test");
5
5
  const formatter_1 = require("../utils/formatter");
6
6
  function createVerifyChain(resolveLocator, target, verifyOptions, isSoft) {
7
7
  const defaultNth = verifyOptions?.nth;
8
+ const defaultHasText = verifyOptions?.hasText;
8
9
  const defaultMessage = verifyOptions?.message;
9
10
  const expectFn = isSoft ? test_1.expect.soft : test_1.expect;
10
11
  const createMatcher = (isNegated) => {
11
12
  const baseFn = async (options) => {
12
13
  const nth = options?.nth !== undefined ? options.nth : defaultNth;
14
+ const hasText = options?.hasText !== undefined ? options.hasText : defaultHasText;
13
15
  const stepName = options?.message ?? defaultMessage ?? (0, formatter_1.formatAssertionDescription)(target, 'toBeVisible', isNegated, [options]);
14
16
  await test_1.test.step(stepName, async () => {
15
- const locator = resolveLocator(target, { nth });
17
+ const locator = resolveLocator(target, { nth, hasText });
16
18
  const expectation = expectFn(locator, stepName);
17
19
  const match = isNegated ? expectation.not : expectation;
18
20
  await match.toBeVisible(options);
@@ -43,10 +45,11 @@ function createVerifyChain(resolveLocator, target, verifyOptions, isSoft) {
43
45
  const valueArgs = lastIsOptions ? args.slice(0, args.length - 1) : args;
44
46
  const options = lastIsOptions ? lastArg : undefined;
45
47
  const nth = (options && 'nth' in options) ? options.nth : defaultNth;
48
+ const hasText = (options && 'hasText' in options) ? options.hasText : defaultHasText;
46
49
  const isHaveCount = prop === 'toHaveCount';
47
50
  const stepName = options?.message ?? defaultMessage ?? (0, formatter_1.formatAssertionDescription)(target, String(prop), isNegated, valueArgs);
48
51
  await test_1.test.step(stepName, async () => {
49
- const locator = resolveLocator(target, { nth, raw: isHaveCount });
52
+ const locator = resolveLocator(target, { nth, hasText, raw: isHaveCount });
50
53
  const expectation = expectFn(locator, stepName);
51
54
  const match = isNegated ? expectation.not : expectation;
52
55
  await match[prop](...args);
@@ -1,22 +1,28 @@
1
1
  import { Locator, expect as playwrightExpect } from '@playwright/test';
2
2
  export declare function verifyHidden(resolveLocator: (target: any, options?: {
3
3
  nth?: number;
4
+ hasText?: string | RegExp;
4
5
  raw?: boolean;
5
6
  }) => Locator, target: any, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeHidden']>[0] & {
6
7
  nth?: number;
8
+ hasText?: string | RegExp;
7
9
  message?: string;
8
10
  }): Promise<void>;
9
11
  export declare function verifyEnabled(resolveLocator: (target: any, options?: {
10
12
  nth?: number;
13
+ hasText?: string | RegExp;
11
14
  raw?: boolean;
12
15
  }) => Locator, target: any, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeEnabled']>[0] & {
13
16
  nth?: number;
17
+ hasText?: string | RegExp;
14
18
  message?: string;
15
19
  }): Promise<void>;
16
20
  export declare function verifyDisabled(resolveLocator: (target: any, options?: {
17
21
  nth?: number;
22
+ hasText?: string | RegExp;
18
23
  raw?: boolean;
19
24
  }) => Locator, target: any, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeDisabled']>[0] & {
20
25
  nth?: number;
26
+ hasText?: string | RegExp;
21
27
  message?: string;
22
28
  }): Promise<void>;
@@ -8,21 +8,21 @@ const formatter_1 = require("../utils/formatter");
8
8
  async function verifyHidden(resolveLocator, target, options) {
9
9
  const stepName = options?.message ?? (0, formatter_1.formatAssertionDescription)(target, 'toBeHidden', false, [options]);
10
10
  await test_1.test.step(stepName, async () => {
11
- const locator = resolveLocator(target, { nth: options?.nth });
11
+ const locator = resolveLocator(target, { nth: options?.nth, hasText: options?.hasText });
12
12
  await (0, test_1.expect)(locator, stepName).toBeHidden(options);
13
13
  });
14
14
  }
15
15
  async function verifyEnabled(resolveLocator, target, options) {
16
16
  const stepName = options?.message ?? (0, formatter_1.formatAssertionDescription)(target, 'toBeEnabled', false, [options]);
17
17
  await test_1.test.step(stepName, async () => {
18
- const locator = resolveLocator(target, { nth: options?.nth });
18
+ const locator = resolveLocator(target, { nth: options?.nth, hasText: options?.hasText });
19
19
  await (0, test_1.expect)(locator, stepName).toBeEnabled(options);
20
20
  });
21
21
  }
22
22
  async function verifyDisabled(resolveLocator, target, options) {
23
23
  const stepName = options?.message ?? (0, formatter_1.formatAssertionDescription)(target, 'toBeDisabled', false, [options]);
24
24
  await test_1.test.step(stepName, async () => {
25
- const locator = resolveLocator(target, { nth: options?.nth });
25
+ const locator = resolveLocator(target, { nth: options?.nth, hasText: options?.hasText });
26
26
  await (0, test_1.expect)(locator, stepName).toBeDisabled(options);
27
27
  });
28
28
  }
@@ -28,4 +28,24 @@ export type PageConfig<T = any> = {
28
28
  } : Record<string, string>;
29
29
  Class?: new (page: Page, config?: any) => any;
30
30
  };
31
+ /**
32
+ * Creates a strongly-typed page configuration object containing URLs, testIds, and CSS selectors.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { createPageConfig } from 'pw-core/page';
37
+ *
38
+ * const config = createPageConfig({
39
+ * url: '/login',
40
+ * testIds: {
41
+ * username: 'username-input',
42
+ * password: 'password-input',
43
+ * submitBtn: 'login-button',
44
+ * },
45
+ * selectors: {
46
+ * errorAlert: '.alert-danger',
47
+ * }
48
+ * });
49
+ * ```
50
+ */
31
51
  export declare function createPageConfig<T extends PageConfig<T>>(config: T): T;
@@ -21,6 +21,26 @@ function getOptionsArgumentIndex(methodName) {
21
21
  return 1;
22
22
  return -1;
23
23
  }
24
+ /**
25
+ * Creates a strongly-typed page configuration object containing URLs, testIds, and CSS selectors.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { createPageConfig } from 'pw-core/page';
30
+ *
31
+ * const config = createPageConfig({
32
+ * url: '/login',
33
+ * testIds: {
34
+ * username: 'username-input',
35
+ * password: 'password-input',
36
+ * submitBtn: 'login-button',
37
+ * },
38
+ * selectors: {
39
+ * errorAlert: '.alert-danger',
40
+ * }
41
+ * });
42
+ * ```
43
+ */
24
44
  function createPageConfig(config) {
25
45
  return config;
26
46
  }
@@ -10,6 +10,7 @@ export declare function resolveLocator<T extends {
10
10
  }>(context: Page | Locator, config: T, target: PageKeys<T> | ChainedKeys<T> | Locator, options?: {
11
11
  nth?: number;
12
12
  raw?: boolean;
13
+ hasText?: string | RegExp;
13
14
  }): Locator;
14
15
  /**
15
16
  * Resolve a config key to a proxied Locator with step-wrapped Playwright methods.
@@ -49,13 +49,17 @@ function resolveLocator(context, config, target, options) {
49
49
  for (let i = 1; i < parts.length; i++) {
50
50
  loc = resolveSingle(parts[i], loc);
51
51
  }
52
+ let resolved = loc;
53
+ if (options?.hasText !== undefined) {
54
+ resolved = resolved.filter({ hasText: options.hasText });
55
+ }
52
56
  if (options?.raw) {
53
- return loc;
57
+ return resolved;
54
58
  }
55
59
  if (options?.nth !== undefined) {
56
- return loc.nth(options.nth);
60
+ return resolved.nth(options.nth);
57
61
  }
58
- return loc.first();
62
+ return resolved.first();
59
63
  }
60
64
  /**
61
65
  * Resolve a config key to a proxied Locator with step-wrapped Playwright methods.
@@ -5,16 +5,52 @@ export type PageRegistry = Record<string, PageConfig>;
5
5
  export interface PageConstructor<C extends PageConfig<any>> {
6
6
  new (page: Page): TypedPage<C>;
7
7
  }
8
- export interface PageRegistryObject<T extends Record<string, PageConfig<any>>> {
8
+ export type PageRegistryTest<T extends Record<string, PageConfig<any>>, P, W, O = {}> = TestType<P & {
9
+ [K in keyof T]: K extends keyof O ? O[K] extends new (page: Page, ...args: any[]) => infer R ? R : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R2 ? R2 : TypedPage<T[K]> : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R ? R : TypedPage<T[K]>;
10
+ }, W & {
11
+ [K in keyof T as `worker${Capitalize<K & string>}`]: K extends keyof O ? O[K] extends new (page: Page, ...args: any[]) => infer R ? R : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R2 ? R2 : TypedPage<T[K]> : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R ? R : TypedPage<T[K]>;
12
+ } & {
13
+ workerPage: Page;
14
+ }> & {
15
+ pages: {
16
+ [K in keyof T]: K extends keyof O ? O[K] : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => any ? NonNullable<T[K]['Class']> : PageConstructor<T[K]>;
17
+ };
9
18
  classes: {
10
- [K in keyof T]: PageConstructor<T[K]>;
19
+ [K in keyof T]: K extends keyof O ? O[K] : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => any ? NonNullable<T[K]['Class']> : PageConstructor<T[K]>;
11
20
  };
12
- extend<O extends Partial<{
21
+ extend<O2 extends Partial<{
13
22
  [K in keyof T]: new (page: Page, ...args: any[]) => any;
14
- }>, B extends TestType<any, any> = typeof test>(overrides: O, base?: B): B extends TestType<infer P, infer W> ? TestType<P & {
15
- [K in keyof T]: K extends keyof O ? O[K] extends new (...args: any[]) => infer R ? R : TypedPage<T[K]> : TypedPage<T[K]>;
16
- }, W> : never;
17
- }
23
+ }>, B extends TestType<any, any> = typeof test>(overrides: O2, base?: B): B extends TestType<infer BaseP, infer BaseW> ? PageRegistryTest<T, BaseP, BaseW, O2> : never;
24
+ };
25
+ /**
26
+ * Creates a page registry that registers page objects as Playwright fixtures,
27
+ * automatically creating both page-scoped fixtures and worker-scoped fixtures prefixed with 'worker'.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { createPageRegistry } from 'pw-core/page';
32
+ * import { LoginPage, config } from './login.page.js';
33
+ *
34
+ * // returns the extended test runner directly
35
+ * const test = createPageRegistry({
36
+ * login: { ...config, Class: LoginPage },
37
+ * });
38
+ *
39
+ * // returns page classes only when accessing .pages
40
+ * const pages = createPageRegistry({
41
+ * login: { ...config, Class: LoginPage },
42
+ * }).pages;
43
+ *
44
+ * // In your tests, you can use both the page-scoped and worker-scoped fixtures:
45
+ * test('login flow', async ({ login, workerLogin }) => {
46
+ * await login.goto();
47
+ * await login.login('alice', 'secret');
48
+ *
49
+ * await workerLogin.goto();
50
+ * await workerLogin.login('bob', 'secret');
51
+ * });
52
+ * ```
53
+ */
18
54
  export declare function createPageRegistry<T extends {
19
55
  [K in keyof T]: PageConfig<T[K]>;
20
- } & Record<string, PageConfig<any>>>(registry: T): PageRegistryObject<T>;
56
+ } & Record<string, PageConfig<any>>>(registry: T): typeof test extends TestType<infer P, infer W> ? PageRegistryTest<T, P, W> : never;
@@ -6,6 +6,83 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createPageRegistry = createPageRegistry;
7
7
  const test_1 = __importDefault(require("@playwright/test"));
8
8
  const typed_page_1 = require("./typed-page");
9
+ function createPageRegistryWithClasses(registry, classes, base) {
10
+ const fixtures = {
11
+ // Shared worker context and page
12
+ workerContext: [async ({ browser }, use) => {
13
+ const context = await browser.newContext();
14
+ await use(context);
15
+ await context.close();
16
+ }, { scope: 'worker' }],
17
+ workerPage: [async ({ workerContext }, use) => {
18
+ const page = await workerContext.newPage();
19
+ await use(page);
20
+ }, { scope: 'worker' }]
21
+ };
22
+ for (const key of Object.keys(registry)) {
23
+ fixtures[key] = async ({ page }, use) => {
24
+ const PageClass = classes[key];
25
+ const instance = new PageClass(page);
26
+ await use(instance);
27
+ };
28
+ const workerKey = `worker${key.charAt(0).toUpperCase()}${key.slice(1)}`;
29
+ fixtures[workerKey] = [async ({ workerPage }, use) => {
30
+ const PageClass = classes[key];
31
+ const instance = new PageClass(workerPage);
32
+ await use(instance);
33
+ }, { scope: 'worker' }];
34
+ }
35
+ const testRunner = base.extend(fixtures);
36
+ testRunner.pages = classes;
37
+ testRunner.classes = classes;
38
+ const originalExtend = testRunner.extend.bind(testRunner);
39
+ testRunner.extend = (overrides, customBase = test_1.default) => {
40
+ if (!overrides) {
41
+ return originalExtend();
42
+ }
43
+ const isPlaywrightFixtures = Object.values(overrides).every(val => (typeof val === 'function' && !val.toString().startsWith('class')) || Array.isArray(val));
44
+ if (isPlaywrightFixtures) {
45
+ return originalExtend(overrides);
46
+ }
47
+ const newClasses = { ...classes };
48
+ for (const key of Object.keys(overrides)) {
49
+ if (overrides[key]) {
50
+ newClasses[key] = overrides[key];
51
+ }
52
+ }
53
+ return createPageRegistryWithClasses(registry, newClasses, customBase);
54
+ };
55
+ return testRunner;
56
+ }
57
+ /**
58
+ * Creates a page registry that registers page objects as Playwright fixtures,
59
+ * automatically creating both page-scoped fixtures and worker-scoped fixtures prefixed with 'worker'.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * import { createPageRegistry } from 'pw-core/page';
64
+ * import { LoginPage, config } from './login.page.js';
65
+ *
66
+ * // returns the extended test runner directly
67
+ * const test = createPageRegistry({
68
+ * login: { ...config, Class: LoginPage },
69
+ * });
70
+ *
71
+ * // returns page classes only when accessing .pages
72
+ * const pages = createPageRegistry({
73
+ * login: { ...config, Class: LoginPage },
74
+ * }).pages;
75
+ *
76
+ * // In your tests, you can use both the page-scoped and worker-scoped fixtures:
77
+ * test('login flow', async ({ login, workerLogin }) => {
78
+ * await login.goto();
79
+ * await login.login('alice', 'secret');
80
+ *
81
+ * await workerLogin.goto();
82
+ * await workerLogin.login('bob', 'secret');
83
+ * });
84
+ * ```
85
+ */
9
86
  function createPageRegistry(registry) {
10
87
  const classes = {};
11
88
  for (const key of Object.keys(registry)) {
@@ -16,19 +93,5 @@ function createPageRegistry(registry) {
16
93
  }
17
94
  });
18
95
  }
19
- return {
20
- classes,
21
- extend(overrides, base = test_1.default) {
22
- const fixtures = {};
23
- for (const key of Object.keys(registry)) {
24
- fixtures[key] = async ({ page }, use) => {
25
- const config = registry[key];
26
- const PageClass = overrides[key] || classes[key];
27
- const instance = new PageClass(page);
28
- await use(instance);
29
- };
30
- }
31
- return base.extend(fixtures);
32
- }
33
- };
96
+ return createPageRegistryWithClasses(registry, classes, test_1.default);
34
97
  }
@@ -33,6 +33,7 @@ declare class TypedPageClass<T extends {
33
33
  resolveLocator(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: {
34
34
  nth?: number;
35
35
  raw?: boolean;
36
+ hasText?: string | RegExp;
36
37
  }): Locator;
37
38
  /** Resolve a config key to a proxied Locator with step-wrapped methods. */
38
39
  locator(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<Locator['filter']>[0] & {
@@ -15,14 +15,19 @@ import type { PageKeys, ChainedKeys } from '../config';
15
15
  type ProxyKeys = 'click' | 'dblclick' | 'hover' | 'focus' | 'blur' | 'check' | 'uncheck' | 'clear' | 'waitFor' | 'isChecked' | 'isDisabled' | 'isVisible' | 'textContent' | 'innerText' | 'allInnerTexts' | 'allTextContents' | 'count' | 'scrollIntoViewIfNeeded' | 'boundingBox' | 'press' | 'pressSequentially' | 'selectOption' | 'setInputFiles' | 'getAttribute';
16
16
  type ModifyProxyArgs<Args extends any[]> = Args extends [] ? [options?: {
17
17
  nth?: number;
18
+ hasText?: string | RegExp;
18
19
  }] : Args extends [any, any?] ? [Args[0], (Exclude<Args[1], undefined> & {
19
20
  nth?: number;
21
+ hasText?: string | RegExp;
20
22
  })?] : Args extends [any?] ? Exclude<Args[0], undefined> extends object ? [(Exclude<Args[0], undefined> & {
21
23
  nth?: number;
24
+ hasText?: string | RegExp;
22
25
  })?] : [Exclude<Args[0], undefined>, options?: {
23
26
  nth?: number;
27
+ hasText?: string | RegExp;
24
28
  }] : [options?: {
25
29
  nth?: number;
30
+ hasText?: string | RegExp;
26
31
  }];
27
32
  type BaseProxyLocatorMethods<T> = {
28
33
  [K in keyof Locator as K extends ProxyKeys ? K : never]: (target: PageKeys<T> | ChainedKeys<T> | Locator, ...args: ModifyProxyArgs<Parameters<Locator[K]>>) => ReturnType<Locator[K]>;
@@ -37,17 +42,19 @@ type BaseProxyLocatorMethods<T> = {
37
42
  * await page.fill('username', 'alice', { nth: 1 });
38
43
  * ```
39
44
  *
40
- * pw-core adds `nth` to options — targets a specific match when multiple elements resolve.
45
+ * pw-core adds `nth` and `hasText` to options — targets a specific match when multiple elements resolve.
41
46
  */
42
47
  export interface ProxyLocatorMethods<T> extends BaseProxyLocatorMethods<T> {
43
48
  /** @see {@link https://playwright.dev/docs/api/class-locator#locator-fill Locator.fill} */
44
49
  fill(target: PageKeys<T> | ChainedKeys<T> | Locator, value: string, options?: Parameters<Locator['fill']>[1] & {
45
50
  nth?: number;
51
+ hasText?: string | RegExp;
46
52
  mask?: boolean;
47
53
  }): Promise<void>;
48
54
  /** @see {@link https://playwright.dev/docs/api/class-locator#locator-drag-to Locator.dragTo} */
49
55
  dragTo(target: PageKeys<T> | ChainedKeys<T> | Locator, destination: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<Locator['dragTo']>[1] & {
50
56
  nth?: number;
57
+ hasText?: string | RegExp;
51
58
  }): Promise<void>;
52
59
  }
53
60
  export {};
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "pw-core",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
+ "workspaces": [
5
+ "examples",
6
+ "create-pw-core"
7
+ ],
4
8
  "description": "Developer-first Playwright framework with typed page objects, components, assertions, and reusable automation utilities.",
5
9
  "author": {
6
10
  "name": "Shanmuka Chandra Teja Anem",
@@ -44,18 +48,20 @@
44
48
  }
45
49
  },
46
50
  "scripts": {
47
- "build": "rimraf dist && tsc",
48
- "prepare": "npm run build"
51
+ "build": "node -e \"const fs = require('fs'); fs.rmSync('dist', {recursive:true,force:true})\" && tsc && npm run build --prefix create-pw-core",
52
+ "prepare": "npm run build",
53
+ "test": "npm run test --workspace=pw-core-demo"
49
54
  },
50
55
  "peerDependencies": {
51
56
  "@playwright/test": "^1.40.0"
52
57
  },
53
58
  "devDependencies": {
54
- "@playwright/test": "^1.60.0",
59
+ "@playwright/test": "^1.61.0",
60
+ "@types/node": "^20.11.0",
55
61
  "rimraf": "^6.0.1",
56
62
  "typescript": "^5.3.3"
57
63
  },
58
64
  "publishConfig": {
59
65
  "access": "public"
60
66
  }
61
- }
67
+ }