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 +41 -539
- package/dist/page/actions/locator-actions.d.ts +2 -0
- package/dist/page/actions/locator-actions.js +9 -3
- package/dist/page/assertions/verify-chain.d.ts +11 -0
- package/dist/page/assertions/verify-chain.js +5 -2
- package/dist/page/assertions/verify-helpers.d.ts +6 -0
- package/dist/page/assertions/verify-helpers.js +3 -3
- package/dist/page/config.d.ts +20 -0
- package/dist/page/config.js +20 -0
- package/dist/page/locators/resolver.d.ts +1 -0
- package/dist/page/locators/resolver.js +7 -3
- package/dist/page/registry.d.ts +44 -8
- package/dist/page/registry.js +78 -15
- package/dist/page/typed-page.d.ts +1 -0
- package/dist/page/types/proxy-methods.d.ts +8 -1
- package/package.json +11 -5
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
|
-
##
|
|
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
|
|
22
|
+
npm init pw-core
|
|
13
23
|
```
|
|
14
24
|
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
The following custom methods are available on `TypedPage` objects:
|
|
457
57
|
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
page.
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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'
|
|
15
|
-
|
|
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
|
}
|
package/dist/page/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/page/config.js
CHANGED
|
@@ -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
|
|
57
|
+
return resolved;
|
|
54
58
|
}
|
|
55
59
|
if (options?.nth !== undefined) {
|
|
56
|
-
return
|
|
60
|
+
return resolved.nth(options.nth);
|
|
57
61
|
}
|
|
58
|
-
return
|
|
62
|
+
return resolved.first();
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
61
65
|
* Resolve a config key to a proxied Locator with step-wrapped Playwright methods.
|
package/dist/page/registry.d.ts
CHANGED
|
@@ -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
|
|
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<
|
|
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:
|
|
15
|
-
|
|
16
|
-
|
|
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):
|
|
56
|
+
} & Record<string, PageConfig<any>>>(registry: T): typeof test extends TestType<infer P, infer W> ? PageRegistryTest<T, P, W> : never;
|
package/dist/page/registry.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
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.
|
|
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
|
+
}
|