pw-core 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qualicore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,579 @@
1
+ # pw-core
2
+
3
+ A developer-first framework layer built on top of Playwright for creating readable, scalable, maintainable, and AI-friendly automation frameworks.
4
+
5
+ `pw-core` standardizes how pages, locators, components, assertions, and fixtures are defined so teams can focus on writing tests instead of maintaining framework code.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install pw-core
13
+ ```
14
+
15
+ ### Requirements
16
+
17
+ - Existing Playwright project
18
+ - TypeScript recommended
19
+ - Compatible with supported Playwright versions defined by peer dependency requirements
20
+
21
+ ---
22
+
23
+ ## Core Philosophy
24
+
25
+ | Goal | Value |
26
+ | :--------------------- | :------------------------------------------------------ |
27
+ | Single Source of Truth | URLs, selectors, and testIds live in one place |
28
+ | Readability | Tests should read like business workflows |
29
+ | Scalability | Adding pages should not increase framework complexity |
30
+ | Reusability | Components should be defined once and reused everywhere |
31
+ | Type Safety | Catch mistakes during development instead of runtime |
32
+ | AI-Friendly | Standardized patterns reduce context and token usage |
33
+ | Maintainability | Update locators once, not across hundreds of tests |
34
+
35
+ ---
36
+
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
455
+
456
+ Most automation frameworks are inconsistent.
457
+
458
+ One page uses:
459
+
460
+ ```ts
461
+ page.locator(...)
462
+ ```
463
+
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
502
+
503
+ ---
504
+
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
+ ## License
578
+
579
+ MIT
@@ -0,0 +1,48 @@
1
+ import { Locator } from '@playwright/test';
2
+ /**
3
+ * A custom Array subclass representing a list of table rows.
4
+ * Inherits all standard Array methods while adding type-safe get and getAll capabilities.
5
+ */
6
+ export declare class TableRows<T extends Record<string, any>> extends Array<T> {
7
+ constructor(...items: T[]);
8
+ /**
9
+ * Overloaded get method:
10
+ * 1. rows.get('id') -> returns the 'id' value of the first row (T[K] | undefined)
11
+ * 2. rows.get('id', 'value') -> returns the first matching row object (T | undefined)
12
+ */
13
+ get<K extends keyof T>(key: K): T[K] | undefined;
14
+ get<K extends keyof T>(key: K, value: T[K]): T | undefined;
15
+ /**
16
+ * Overloaded getAll method:
17
+ * 1. rows.getAll('id') -> returns an array of all rows' 'id' values (T[K][])
18
+ * 2. rows.getAll('id', 'value') -> returns an array of all matching row objects (T[])
19
+ */
20
+ getAll<K extends keyof T>(key: K): T[K][];
21
+ getAll<K extends keyof T>(key: K, value: T[K]): T[];
22
+ }
23
+ export declare class Table<T extends Record<string, any>> {
24
+ readonly root: Locator;
25
+ constructor(root: Locator);
26
+ /**
27
+ * Retrieves all the headers from the table as lowercase strings.
28
+ */
29
+ getHeaders(): Promise<string[]>;
30
+ /**
31
+ * Retrieves all the rows of the table as typed objects.
32
+ * Dynamically maps table headers to object keys.
33
+ */
34
+ getRows(): Promise<T[]>;
35
+ /**
36
+ * Retrieves all rows from the table, returned as a custom TableRows collection
37
+ * with type-safe finder helper methods.
38
+ */
39
+ get(): Promise<TableRows<T>>;
40
+ /**
41
+ * Gets the total count of data rows in the table.
42
+ */
43
+ getRowCount(): Promise<number>;
44
+ /**
45
+ * Retrieves the value of a specific cell by row index and column key.
46
+ */
47
+ getCellValue(rowIndex: number, column: keyof T): Promise<string>;
48
+ }