@smartbit4all/playwright-qa 0.2.4 → 0.2.5

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
@@ -1,625 +1,625 @@
1
- # @smartbit4all/playwright-qa
2
-
3
- Playwright-based testing and documentation framework. Provides screenshot utilities, typed datapools with locale support, and TestStep/TestSuite conventions.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install @smartbit4all/playwright-qa
9
- ```
10
-
11
- Requires Node.js 20+ and Playwright as a peer dependency:
12
-
13
- ```bash
14
- npm install @playwright/test
15
- npx playwright install --with-deps chromium
16
- ```
17
-
18
- ## Quick Start
19
-
20
- ```typescript
21
- import { test } from '@playwright/test';
22
- import { initSuite, screenshot } from '@smartbit4all/playwright-qa';
23
-
24
- test.beforeAll(() => {
25
- initSuite({ screenshotDir: './screenshots' });
26
- });
27
-
28
- test('take a screenshot', async ({ page }) => {
29
- await page.goto('https://demo.playwright.dev/todomvc');
30
- await screenshot(page, 'todomvc-home');
31
- });
32
- ```
33
-
34
- ## Suite Setup
35
-
36
- Every TestSuite should call `initSuite()` in `beforeAll`. This single call resets all internal state (config, datapools, screenshot directory, seed handlers) and optionally sets up the screenshot directory.
37
-
38
- ```typescript
39
- import { initSuite } from '@smartbit4all/playwright-qa';
40
-
41
- // With screenshots — each run creates a new timestamped subdirectory
42
- initSuite({ screenshotDir: './screenshots' });
43
-
44
- // Clean previous run — deletes the last run's folder before creating a new one
45
- initSuite({ screenshotDir: './screenshots', cleanScreenshots: true });
46
-
47
- // Without screenshots — tests run normally, screenshot calls return null
48
- initSuite();
49
- ```
50
-
51
- By default, each run creates a new timestamped subdirectory and the `latest` symlink always points to the most recent run. Set `cleanScreenshots: true` to delete all previous runs before starting — useful when disk space is limited or only the latest run matters.
52
-
53
- When no `screenshotDir` is provided, all screenshot functions (`screenshot`, `screenshotHighlight`, `screenshotRegion`, `screenshotRegionHighlight`) become no-ops and return `null` instead of a file path. This means TestSteps that include screenshot calls work without modification — no conditional logic needed.
54
-
55
- When multiple suites run in the same test execution with the same `screenshotDir`, the directory is created once and reused — all screenshots land in a single timestamped folder. This works even if a suite fails mid-run: the next suite picks up the same directory and continues the `autoPrefix` counter from where it left off (e.g., if the failed suite ended at `0005-`, the next suite starts at `0006-`). The framework tracks this via a `.current-run` marker file in the screenshot base directory.
56
-
57
- ## Viewport
58
-
59
- Use `getViewport()` in your `playwright.config.ts` to automatically switch between full-size and headed viewport:
60
-
61
- ```typescript
62
- import { defineConfig } from '@playwright/test';
63
- import { getViewport } from '@smartbit4all/playwright-qa';
64
-
65
- export default defineConfig({
66
- use: {
67
- viewport: getViewport(),
68
- },
69
- });
70
- ```
71
-
72
- When running with `--headed`, the viewport automatically shrinks to fit on screen (default: 1280x720). In headless/CI mode, the full viewport is used (default: 1920x1080). Both sizes are configurable in `playwright-qa.config.json`:
73
-
74
- ```json
75
- {
76
- "viewport": { "width": 1920, "height": 1080 },
77
- "headedViewport": { "width": 1280, "height": 720 }
78
- }
79
- ```
80
-
81
- ## API Reference
82
-
83
- ### Screenshot
84
-
85
- ```typescript
86
- import { screenshot, screenshotHighlight, screenshotRegion, screenshotRegionHighlight } from '@smartbit4all/playwright-qa';
87
-
88
- // Full page screenshot — returns file path, or null if screenshots are disabled
89
- await screenshot(page, 'page-name');
90
- await screenshot(page, 'category/page-name'); // name path = subdirectory
91
- await screenshot(page, 'page-name', { fullPage: true });
92
-
93
- // Full page with highlighted element — accepts CSS selector or Locator
94
- await screenshotHighlight(page, 'name', '#selector');
95
- await screenshotHighlight(page, 'name', '#selector', { style: 'subtle' });
96
- await screenshotHighlight(page, 'name', findMenuItem(page, 'Új mappa')); // Locator
97
-
98
- // Region screenshot (single element) — accepts CSS selector or Locator
99
- await screenshotRegion(page, 'name', '#region-selector');
100
-
101
- // Region with highlighted element inside — both accept CSS selector or Locator
102
- await screenshotRegionHighlight(page, 'name', '#region', '#highlight');
103
- await screenshotRegionHighlight(page, 'name', findPopupMenu(page), findMenuItem(page, 'Új mappa'));
104
- ```
105
-
106
- All screenshot functions return `Promise<string | null>`. They return `null` when no screenshot directory has been configured (see [Suite Setup](#suite-setup)). Highlight and region parameters accept both CSS selectors (string) and Playwright Locators.
107
-
108
- ### Datapool
109
-
110
- ```typescript
111
- import { datapool, configureDatapool } from '@smartbit4all/playwright-qa';
112
-
113
- // Global configuration (typically in beforeAll)
114
- configureDatapool({ locale: 'hu', basePath: './datapools' });
115
-
116
- // Load a datapool
117
- const companies = datapool<Company>('companies');
118
-
119
- // Access items
120
- const item = companies.byKey('it4all'); // by key (meta.key field, default: "code")
121
- const derived = companies.derive('it4all', { name: 'Modified' }); // template + overrides
122
- const first = companies.findFirst('city', 'Budapest'); // first match by field
123
- const matches = companies.findAll('role', 'admin'); // all matches by field
124
- const random = companies.random(); // random item
125
- const all = companies.all(); // all items
126
- const count = companies.count(); // count
127
- ```
128
-
129
- ## Built-in Steps
130
-
131
- The package ships reusable TestSteps for common smartbit4all UI patterns (grid, tree navigation). These are available via a separate subpath import to keep them distinct from the core API:
132
-
133
- ```typescript
134
- import { selectGridRow, navigateInTree } from '@smartbit4all/playwright-qa/steps';
135
- ```
136
-
137
- ### Forms
138
-
139
- ```typescript
140
- import {
141
- fillField, fillDate, fillDateTime,
142
- selectOption, selectMultiple, selectRadio,
143
- setCheckbox, setCheckboxGroup, setToggle,
144
- addChip, removeChip, setChips,
145
- uploadFile,
146
- toggleAccordion,
147
- } from '@smartbit4all/playwright-qa/steps';
148
-
149
- // Fill a text input or textarea — by data-testid (preferred) or label text (fallback)
150
- await fillField(dialog, 'data.name', 'Teszt mappa'); // data-testid
151
- await fillField(page, 'dossierContentData.name', 'Teszt dosszié'); // data-testid
152
- await fillField(page, 'Megjegyzés', 'Ez egy megjegyzés'); // label fallback
153
-
154
- // Date picker (ISO format YYYY-MM-DD)
155
- await fillDate(dialog, 'dataSheet.startDate', '2026-04-15');
156
-
157
- // Date-time picker (date + time)
158
- await fillDateTime(dialog, 'data.deadline', '2026-04-15', '14:30');
159
-
160
- // Select from a dropdown (mat-select / p-dropdown) — by data-testid or label
161
- await selectOption(page, 'selectedDocumentTypeCategory', 'Ügyintézés'); // data-testid
162
- await selectOption(dialog, 'Kategória', 'Ügyintézés'); // label fallback
163
-
164
- // Multi-select dropdown
165
- await selectMultiple(dialog, 'data.skills', ['Angular', 'React', 'Vue']);
166
-
167
- // Select a radio button — by data-testid on the radio group or label
168
- await selectRadio(dialog, 'data.canIncludeFiles', 'Nem'); // data-testid
169
- await selectRadio(dialog, 'Tartalmazhat almappákat', 'Igen'); // label fallback
170
-
171
- // Set a checkbox — by data-testid or label
172
- await setCheckbox(dialog, 'Aktív', true);
173
-
174
- // Checkbox group (CHECK_BOX_2) — set multiple checkboxes at once
175
- await setCheckboxGroup(dialog, 'permissions', { 'Olvasás': true, 'Írás': true, 'Törlés': false });
176
-
177
- // Toggle switch
178
- await setToggle(dialog, 'data.isActive', true);
179
-
180
- // Chips — add, remove, or set declaratively
181
- await addChip(dialog, 'data.tags', 'urgent');
182
- await removeChip(dialog, 'data.tags', 'draft');
183
- await setChips(dialog, 'data.tags', ['urgent', 'review']);
184
-
185
- // File upload
186
- await uploadFile(dialog, 'data.attachment', '/path/to/file.pdf');
187
-
188
- // Open/close an accordion tab/panel (p-accordion / mat-accordion) — by data-testid or header text.
189
- // Returns a Locator scoped to the tab's content region, for acting on elements inside it.
190
- await toggleAccordion(dialog, 'Alapadatok', false); // close, ignore the returned content
191
- const content = await toggleAccordion(dialog, 'Alapadatok', true); // open
192
- await fillField(content, 'data.note', 'Ez egy megjegyzés');
193
- ```
194
-
195
- All form utilities accept a `data-testid` value or a visible label text as identifier. When a `data-testid` match is found on the element, it takes priority; otherwise the function falls back to label text matching (`.smart-form-widget-label`, `h3`, `h4`, or `label`). They work with both Angular Material and PrimeNG components, and accept any `Locator` or `Page` as container — use `topDialog(page)` to scope to a dialog.
196
-
197
- #### Generic field setter
198
-
199
- For data-driven scenarios, `setField` and `setFields` route to the right utility based on the value type and the widget's DOM signature — no need to know which form widget you are targeting.
200
-
201
- ```typescript
202
- import { setField, setFields } from '@smartbit4all/playwright-qa/steps';
203
-
204
- // One field at a time
205
- await setField(dialog, 'data.name', 'Teszt mappa'); // → fillField
206
- await setField(dialog, 'data.canIncludeFiles', 'Igen'); // → selectRadio (widget has mat-radio-group)
207
- await setField(dialog, 'data.category', 'Ügyintézés'); // → selectOption (widget has mat-select / p-dropdown)
208
- await setField(dialog, 'data.isActive', true); // → setToggle / setCheckbox
209
- await setField(dialog, 'data.skills', ['Angular', 'React']); // → selectMultiple or setChips
210
- await setField(dialog, 'data.deadline', '2026-04-15 14:30'); // → fillDateTime (matches YYYY-MM-DD HH:mm)
211
- await setField(dialog, 'data.attachment', '/path/file.pdf'); // → uploadFile (widget has smart-file-editor)
212
- await setField(dialog, 'permissions', { 'Olvasás': true, 'Írás': false }); // → setCheckboxGroup
213
-
214
- // Many fields at once
215
- await setFields(dialog, {
216
- 'data.name': 'Teszt mappa',
217
- 'data.canIncludeFiles': 'Nem',
218
- 'data.isActive': true,
219
- 'data.tags': ['urgent', 'review'],
220
- 'permissions': { 'Olvasás': true, 'Írás': true },
221
- });
222
- ```
223
-
224
- Routing is decided by `value`'s TypeScript type plus the DOM tags inside the widget:
225
- - `Record<string, boolean>` → `setCheckboxGroup`
226
- - `boolean` + `mat-slide-toggle` / `p-inputSwitch` → `setToggle`
227
- - `boolean` + `mat-checkbox` / `p-checkbox` → `setCheckbox`
228
- - `string[]` + `mat-chip-grid` / `p-chips` → `setChips`
229
- - `string[]` + `p-multiSelect` / `mat-select[multiple]` → `selectMultiple`
230
- - `string` + `smart-file-editor` → `uploadFile`
231
- - `string` + `mat-select` / `p-dropdown` → `selectOption`
232
- - `string` + `mat-radio-group` / `p-radiobutton` → `selectRadio`
233
- - `string` matching `YYYY-MM-DD HH:mm` → `fillDateTime`
234
- - `string` + any `input` / `textarea` → `fillField`
235
-
236
- If no branch matches, `setField` throws with the identifier in the message. Datetime detection is value-format based (a single `YYYY-MM-DD HH:mm` string), since the Material datetime widget has no distinctive tag — pass an ISO date+time string and let `setField` split it.
237
-
238
- ### Grid
239
-
240
- ```typescript
241
- import {
242
- selectGridRow,
243
- checkGridRow,
244
- checkGridRowsOnPage,
245
- checkAllGridRows,
246
- filterAndSelectGridRow,
247
- applyGridFilters,
248
- clearGridFilters,
249
- } from '@smartbit4all/playwright-qa/steps';
250
-
251
- // Find a row by table content (paginates automatically) and double-click it
252
- await selectGridRow(page, { 'Azonosító': 'DOC-001' });
253
-
254
- // Find by simple text match
255
- await selectGridRow(page, 'DOC-001');
256
-
257
- // Find by row data-testid (rendered from row.id on the <tr>)
258
- await selectGridRow(page, { rowId: 'doc-12345' });
259
-
260
- // Open the row's context menu and click an action
261
- await selectGridRow(page, { 'Azonosító': 'DOC-001' }, 'Szerkesztés');
262
-
263
- // Reach an action nested under submenus — pass an action path (see "Actions" below).
264
- // Intermediate items are submenu triggers (opened by hover), the last is the leaf (clicked).
265
- await selectGridRow(page, { 'Azonosító': 'DOC-001' }, ['MORE', 'EXPORT', 'PDF']);
266
-
267
- // Open context menu only (for screenshots) — pass true, then close with Escape
268
- await selectGridRow(page, { 'Azonosító': 'DOC-001' }, true);
269
- await screenshot(page, 'grid-context-menu');
270
- await page.keyboard.press('Escape');
271
-
272
- // Multi-select grid: check/uncheck a single row (paginates if needed)
273
- await checkGridRow(page, { 'Cím': 'Dokumentátor' }, true);
274
- await checkGridRow(page, { 'Cím': 'Dokumentátor' }, false);
275
-
276
- // Check/uncheck all matching rows on the current page (no pagination)
277
- await checkGridRowsOnPage(page, 'Admin', true);
278
-
279
- // Select all / deselect all via header checkbox
280
- await checkAllGridRows(page, true);
281
-
282
- // Use the filter form above the grid, then select the row
283
- await filterAndSelectGridRow(page, { 'Azonosító': 'DOC-001' });
284
-
285
- // Apply/clear filters independently
286
- await applyGridFilters(page, { 'Név': 'Teszt' });
287
- await clearGridFilters(page);
288
- ```
289
-
290
- Column keys in a row filter accept either the header label or the header `data-testid` (rendered from `col.propertyName` on the `<th>`). Both resolve to the same column, so `{ 'Adószám': '12345' }` and `{ 'taxNumber': '12345' }` are equivalent — prefer `data-testid` for stability against label/locale changes.
291
-
292
- For row-level addressing, `{ rowId: 'doc-12345' }` matches the `<tr>` `data-testid` attribute (rendered from `row.id`). `rowId` must be the only key in the filter object and is supported by `selectGridRow`, `checkGridRow`, and `checkGridRowsOnPage`. `filterAndSelectGridRow` throws if given a `rowId` filter — use `selectGridRow` instead, since row identifiers do not need a filter form pass.
293
-
294
- All grid functions accept `Page` or `Locator` as container — use a Locator to scope to a dialog or section:
295
-
296
- ```typescript
297
- const section = dialog.locator('smart-component-layout[data-testid="participants"]');
298
- await checkGridRow(section, { 'Cím': 'Dokumentátor' }, true);
299
- ```
300
-
301
- ### Navigation
302
-
303
- ```typescript
304
- import { navigateToMain, navigateInTree } from '@smartbit4all/playwright-qa/steps';
305
-
306
- // Click the logo to return to the main screen
307
- await navigateToMain(page);
308
-
309
- // Navigate a tree (PrimeNG or Angular Material) — expands intermediate nodes, clicks the last one
310
- await navigateInTree(page, ['Ügyek']);
311
- await navigateInTree(page, ['Dokumentumok', 'Tender']);
312
-
313
- // Open the last node's context menu (hamburger button) and click an action
314
- await navigateInTree(page, ['Dokumentumok', 'Bejövő e-mail'], 'Új mappa');
315
-
316
- // Nested action: the tree path and the action path are separate arguments.
317
- // ['Dokumentumok','Bejövő e-mail'] is the tree path; ['MORE','NEW_FOLDER'] is the action path.
318
- await navigateInTree(page, ['Dokumentumok', 'Bejövő e-mail'], ['MORE', 'NEW_FOLDER']);
319
-
320
- // Open context menu only (for screenshots) — pass true, then close with Escape
321
- await navigateInTree(page, ['Dokumentumok', 'Bejövő e-mail'], true);
322
- await screenshot(page, 'context-menu-open');
323
- await page.keyboard.press('Escape');
324
- ```
325
-
326
- ### Actions
327
-
328
- Toolbar, navbar and menu actions — including submenus. Since ng-client #29411 an action that has
329
- child actions renders a **submenu**: reaching a nested action means opening the intermediate submenu
330
- triggers first. `clickAction` and `openActionPath` handle that with an **action path**.
331
-
332
- ```typescript
333
- import { clickAction, openActionPath } from '@smartbit4all/playwright-qa/steps';
334
-
335
- // Single action — clicks a plain toolbar button (by data-testid, then text).
336
- await clickAction(page, 'SAVE');
337
-
338
- // Action path: the last element is the leaf action (clicked); the earlier elements are submenu
339
- // triggers opened along the way. Works whether the menu opens on hover or on click — you don't
340
- // need to know which.
341
- await clickAction(page, ['Administrator', 'Admin beállítások']);
342
- await clickAction(page, ['MORE', 'EXPORT', 'PDF']);
343
-
344
- // Scope to a container (dialog, section, toolbar) by passing a Locator.
345
- await clickAction(topDialog(page), ['MORE', 'DUPLICATE']);
346
- ```
347
-
348
- `openActionPath` opens the same path but **returns the leaf's Locator without clicking it** — so you
349
- can screenshot the highlighted item and click when ready:
350
-
351
- ```typescript
352
- const leaf = await openActionPath(page, ['MORE', 'EXPORT', 'PDF']);
353
- await screenshotRegionHighlight(page, 'export-pdf-highlight', findPopupMenu(page), leaf);
354
- await leaf.click();
355
- ```
356
-
357
- `openActionPath` is dual-mode by whether a menu is already open: with none open, `path[0]` is the
358
- top-level trigger to open; with one open (e.g. after a grid/tree menu button, or after
359
- `navigateInTree(..., true)`), `path[0]` is the first submenu trigger inside it. `clickAction` is
360
- exactly `openActionPath` followed by clicking the returned leaf.
361
-
362
- The same action-path form is accepted by the grid and navigation helpers (`selectGridRow`,
363
- `filterAndSelectGridRow`, `navigateInTree`) for their menu action argument.
364
-
365
- ### Dialog
366
-
367
- ```typescript
368
- import { topDialog, clickInDialog } from '@smartbit4all/playwright-qa/steps';
369
-
370
- // Get the topmost open dialog (Angular Material or PrimeNG)
371
- const dialog = topDialog(page);
372
-
373
- // Click a button inside the topmost dialog — useful when multiple dialogs are stacked
374
- await clickInDialog(page, 'Mentés');
375
-
376
- // Example: dialog-in-dialog workflow
377
- await clickInDialog(page, 'Akció beállítás'); // opens second dialog on top
378
- await screenshot(page, 'second-dialog');
379
- await clickInDialog(page, 'Mentés'); // clicks Mentés in the TOP dialog
380
- // first dialog is still open
381
- await clickInDialog(page, 'Bezárás'); // closes first dialog
382
- ```
383
-
384
- When multiple dialogs are open, `topDialog` always targets the last (topmost) one. This prevents accidentally clicking buttons in a background dialog.
385
-
386
- ### Utilities
387
-
388
- ```typescript
389
- import { waitForAngularIdle } from '@smartbit4all/playwright-qa/steps';
390
-
391
- // Wait for Angular SPA to settle (double networkidle with pause)
392
- await waitForAngularIdle(page);
393
- ```
394
-
395
- `waitForAngularIdle` is also available for project-specific steps that need the same wait pattern.
396
-
397
- ### Locators
398
-
399
- ```typescript
400
- import { findButton, findPopupMenu, findMenuItem } from '@smartbit4all/playwright-qa/steps';
401
-
402
- // Find a button by data-testid (preferred) or visible text (fallback)
403
- await findButton(page, 'REFRESH').click(); // matches data-testid="REFRESH"
404
- await findButton(page, 'Mentés').click(); // matches button text "Mentés"
405
-
406
- // Works inside a container (e.g., dialog)
407
- await findButton(topDialog(page), 'Mentés').click();
408
-
409
- // Find the currently visible popup menu (Angular Material or PrimeNG)
410
- const menu = findPopupMenu(page);
411
-
412
- // Find a menu item by data-testid or text
413
- await findMenuItem(page, 'Szerkesztés').click();
414
- await findMenuItem(page, 'DELETE_ACTION').click(); // matches data-testid="DELETE_ACTION"
415
-
416
- // Combine with screenshot functions for highlighting
417
- await screenshotHighlight(page, 'menu-highlight', findMenuItem(page, 'Új mappa'));
418
- await screenshotRegionHighlight(page, 'menu-region',
419
- findPopupMenu(page), findMenuItem(page, 'Új mappa'));
420
- ```
421
-
422
- All built-in steps (`clickInDialog`, `selectGridRow`, `navigateInTree`) use these locators internally, so they automatically support `data-testid` values alongside text matching.
423
-
424
- ### Debug
425
-
426
- ```typescript
427
- import { dumpDom } from '@smartbit4all/playwright-qa/steps';
428
-
429
- // Dump visible elements on the page — useful when writing a new locator
430
- console.log(await dumpDom(page));
431
-
432
- // Scope to a container (dialog, section, grid row)
433
- console.log(await dumpDom(topDialog(page)));
434
-
435
- // Narrow with a selector
436
- console.log(await dumpDom(page, { selector: 'button, [data-testid]' }));
437
-
438
- // Include hidden elements and longer text
439
- console.log(await dumpDom(page, { visibleOnly: false, maxText: 200 }));
440
- ```
441
-
442
- Each entry contains `tag`, `id`, `classes`, `testId` and own `text` (text nodes only, not the recursive `textContent`). Non-rendering tags (`script`, `style`, `meta`, `link`, `head`, `html`, `noscript`) are filtered out, and by default only visible elements are returned. Use this during test authoring to discover available `data-testid` values without manually inspecting the DOM in DevTools.
443
-
444
- The `testId` field falls back through `data-testid` → `data-automationid`. PrimeNG `MenuItem` (e.g. grid row popup menus) does not expose `data-testid`; only the `automationId` MenuItem property is supported, and it renders as the `data-automationid` DOM attribute. `findMenuItem` honors both attributes.
445
-
446
- ## Developer Guide — Writing TestSteps
447
-
448
- TestSteps are simple async functions that implement one atomic UI operation.
449
-
450
- **Rules:**
451
- - Receive data as parameters — never import or reference datapools
452
- - Include locator logic, waits, technical assertions, and screenshots
453
- - Group related steps in one file per entity/feature
454
-
455
- ```typescript
456
- // steps/company.steps.ts
457
- import { Page } from '@playwright/test';
458
- import { screenshot } from '@smartbit4all/playwright-qa';
459
-
460
- export async function fillCompanyForm(page: Page, company: Company) {
461
- await page.fill('[data-testid="company-name"]', company.name);
462
- await page.fill('[data-testid="tax-number"]', company.taxNumber);
463
- await screenshot(page, 'company/form-filled');
464
- await page.click('[data-testid="save-button"]');
465
- await page.waitForSelector('.success-toast');
466
- await screenshot(page, 'company/saved');
467
- }
468
- ```
469
-
470
- ## Test Designer Guide — Writing TestSuites
471
-
472
- TestSuites are Playwright `test.describe` blocks that read like a scenario script.
473
-
474
- **Rules:**
475
- - Call `initSuite()` in `beforeAll` to reset state and configure screenshots
476
- - Initialize datapools in `beforeAll`
477
- - Select data, then call TestSteps in order
478
- - Add business-level assertions
479
-
480
- ```typescript
481
- // suites/company-management.suite.ts
482
- import { test } from '@playwright/test';
483
- import { initSuite, datapool, unique, type DataPool } from '@smartbit4all/playwright-qa';
484
- import { navigateToMain, selectGridRow } from '@smartbit4all/playwright-qa/steps';
485
- import { fillCompanyForm } from '../steps/company.steps';
486
- import type { Company } from '../types';
487
-
488
- test.describe('Company Management', () => {
489
- let companies: DataPool<Company>;
490
-
491
- test.beforeAll(() => {
492
- initSuite({ screenshotDir: './screenshots' });
493
- companies = datapool<Company>('companies');
494
- });
495
-
496
- test('Create new company', async ({ page }) => {
497
- const company = companies.derive('it4all', { taxNumber: unique('TAX') });
498
- await fillCompanyForm(page, company);
499
- await navigateToMain(page);
500
- await selectGridRow(page, { 'Adószám': company.taxNumber });
501
- });
502
- });
503
- ```
504
-
505
- ## Seed Framework
506
-
507
- Seed populates application data from datapools — either via API or through the UI.
508
-
509
- ### API Seeding
510
-
511
- ```typescript
512
- import { registerSeedHandler, seedViaApi } from '@smartbit4all/playwright-qa';
513
-
514
- // Register a handler (project-specific)
515
- registerSeedHandler<Company>('companies', {
516
- seed: async (company, options) => {
517
- await fetch(`${options.baseUrl}/api/companies`, {
518
- method: 'POST',
519
- headers: { Authorization: `Bearer ${options.authToken}` },
520
- body: JSON.stringify(company),
521
- });
522
- },
523
- });
524
-
525
- // Seed all companies
526
- const result = await seedViaApi('companies', { baseUrl, authToken });
527
- console.log(`Seeded ${result.success}/${result.total}`);
528
- ```
529
-
530
- ### UI Seeding
531
-
532
- ```typescript
533
- import { registerUiSeedStep, seedViaUi } from '@smartbit4all/playwright-qa';
534
-
535
- // Register a UI seed step (reuses your TestSteps)
536
- registerUiSeedStep<Company>('companies', async (page, company) => {
537
- await fillCompanyForm(page, company);
538
- });
539
-
540
- // Seed through the UI
541
- const result = await seedViaUi(page, 'companies', { basePath: './datapools', locale: 'hu' });
542
- ```
543
-
544
- ### Seed Profiles
545
-
546
- For complex data with dependencies, use seed profiles:
547
-
548
- ```json
549
- {
550
- "name": "development-full",
551
- "order": ["users", "organizations", "companies", "documents"],
552
- "dependencies": {
553
- "documents": ["companies", "users"],
554
- "companies": ["organizations"]
555
- }
556
- }
557
- ```
558
-
559
- ```typescript
560
- import { seed } from '@smartbit4all/playwright-qa';
561
-
562
- const profile = JSON.parse(fs.readFileSync('seed-profiles/dev.json', 'utf-8'));
563
- const results = await seed(profile, 'api', { baseUrl, authToken, basePath: './datapools' });
564
- ```
565
-
566
- ## CI Setup
567
-
568
- ### Azure DevOps
569
-
570
- 1. Copy `templates/ci/playwright-qa-pipeline.yml` to your project repo
571
- 2. Set pipeline variables:
572
- - `BASE_URL`: Your application URL (e.g., `https://staging.example.com`)
573
- - `LOCALE`: Datapool locale (e.g., `hu`)
574
- 3. The pipeline will:
575
- - Run all Playwright tests
576
- - Publish JUnit results to the Test tab
577
- - Upload screenshots and HTML report as artifacts
578
-
579
- ### Project Setup
580
-
581
- Copy the `templates/playwright-qa/` directory to your project repo:
582
-
583
- ```bash
584
- cp -r node_modules/@smartbit4all/playwright-qa/templates/playwright-qa ./playwright-qa
585
- cd playwright-qa
586
- npm install
587
- npx playwright install --with-deps chromium
588
- ```
589
-
590
- ## Development
591
-
592
- ```bash
593
- git clone <repo-url>
594
- cd platform-playwright
595
- npm install
596
- npx playwright install --with-deps chromium
597
- npm test
598
- ```
599
-
600
- ### Testing locally in a project
601
-
602
- Use `npm link` to try the package in your own project without publishing:
603
-
604
- ```bash
605
- # In platform-playwright:
606
- npm run build
607
- npm link
608
-
609
- # In your project:
610
- npm link @smartbit4all/playwright-qa
611
- ```
612
-
613
- After linking, your project uses the local build directly. When you make changes to the package, rebuild with `npm run build` — no need to re-link.
614
-
615
- To remove the link later:
616
-
617
- ```bash
618
- # In your project:
619
- npm unlink @smartbit4all/playwright-qa
620
- npm install
621
- ```
622
-
623
- ## License
624
-
625
- LGPL-3.0-or-later
1
+ # @smartbit4all/playwright-qa
2
+
3
+ Playwright-based testing and documentation framework. Provides screenshot utilities, typed datapools with locale support, and TestStep/TestSuite conventions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @smartbit4all/playwright-qa
9
+ ```
10
+
11
+ Requires Node.js 20+ and Playwright as a peer dependency:
12
+
13
+ ```bash
14
+ npm install @playwright/test
15
+ npx playwright install --with-deps chromium
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import { test } from '@playwright/test';
22
+ import { initSuite, screenshot } from '@smartbit4all/playwright-qa';
23
+
24
+ test.beforeAll(() => {
25
+ initSuite({ screenshotDir: './screenshots' });
26
+ });
27
+
28
+ test('take a screenshot', async ({ page }) => {
29
+ await page.goto('https://demo.playwright.dev/todomvc');
30
+ await screenshot(page, 'todomvc-home');
31
+ });
32
+ ```
33
+
34
+ ## Suite Setup
35
+
36
+ Every TestSuite should call `initSuite()` in `beforeAll`. This single call resets all internal state (config, datapools, screenshot directory, seed handlers) and optionally sets up the screenshot directory.
37
+
38
+ ```typescript
39
+ import { initSuite } from '@smartbit4all/playwright-qa';
40
+
41
+ // With screenshots — each run creates a new timestamped subdirectory
42
+ initSuite({ screenshotDir: './screenshots' });
43
+
44
+ // Clean previous run — deletes the last run's folder before creating a new one
45
+ initSuite({ screenshotDir: './screenshots', cleanScreenshots: true });
46
+
47
+ // Without screenshots — tests run normally, screenshot calls return null
48
+ initSuite();
49
+ ```
50
+
51
+ By default, each run creates a new timestamped subdirectory and the `latest` symlink always points to the most recent run. Set `cleanScreenshots: true` to delete all previous runs before starting — useful when disk space is limited or only the latest run matters.
52
+
53
+ When no `screenshotDir` is provided, all screenshot functions (`screenshot`, `screenshotHighlight`, `screenshotRegion`, `screenshotRegionHighlight`) become no-ops and return `null` instead of a file path. This means TestSteps that include screenshot calls work without modification — no conditional logic needed.
54
+
55
+ When multiple suites run in the same test execution with the same `screenshotDir`, the directory is created once and reused — all screenshots land in a single timestamped folder. This works even if a suite fails mid-run: the next suite picks up the same directory and continues the `autoPrefix` counter from where it left off (e.g., if the failed suite ended at `0005-`, the next suite starts at `0006-`). The framework tracks this via a `.current-run` marker file in the screenshot base directory.
56
+
57
+ ## Viewport
58
+
59
+ Use `getViewport()` in your `playwright.config.ts` to automatically switch between full-size and headed viewport:
60
+
61
+ ```typescript
62
+ import { defineConfig } from '@playwright/test';
63
+ import { getViewport } from '@smartbit4all/playwright-qa';
64
+
65
+ export default defineConfig({
66
+ use: {
67
+ viewport: getViewport(),
68
+ },
69
+ });
70
+ ```
71
+
72
+ When running with `--headed`, the viewport automatically shrinks to fit on screen (default: 1280x720). In headless/CI mode, the full viewport is used (default: 1920x1080). Both sizes are configurable in `playwright-qa.config.json`:
73
+
74
+ ```json
75
+ {
76
+ "viewport": { "width": 1920, "height": 1080 },
77
+ "headedViewport": { "width": 1280, "height": 720 }
78
+ }
79
+ ```
80
+
81
+ ## API Reference
82
+
83
+ ### Screenshot
84
+
85
+ ```typescript
86
+ import { screenshot, screenshotHighlight, screenshotRegion, screenshotRegionHighlight } from '@smartbit4all/playwright-qa';
87
+
88
+ // Full page screenshot — returns file path, or null if screenshots are disabled
89
+ await screenshot(page, 'page-name');
90
+ await screenshot(page, 'category/page-name'); // name path = subdirectory
91
+ await screenshot(page, 'page-name', { fullPage: true });
92
+
93
+ // Full page with highlighted element — accepts CSS selector or Locator
94
+ await screenshotHighlight(page, 'name', '#selector');
95
+ await screenshotHighlight(page, 'name', '#selector', { style: 'subtle' });
96
+ await screenshotHighlight(page, 'name', findMenuItem(page, 'Új mappa')); // Locator
97
+
98
+ // Region screenshot (single element) — accepts CSS selector or Locator
99
+ await screenshotRegion(page, 'name', '#region-selector');
100
+
101
+ // Region with highlighted element inside — both accept CSS selector or Locator
102
+ await screenshotRegionHighlight(page, 'name', '#region', '#highlight');
103
+ await screenshotRegionHighlight(page, 'name', findPopupMenu(page), findMenuItem(page, 'Új mappa'));
104
+ ```
105
+
106
+ All screenshot functions return `Promise<string | null>`. They return `null` when no screenshot directory has been configured (see [Suite Setup](#suite-setup)). Highlight and region parameters accept both CSS selectors (string) and Playwright Locators.
107
+
108
+ ### Datapool
109
+
110
+ ```typescript
111
+ import { datapool, configureDatapool } from '@smartbit4all/playwright-qa';
112
+
113
+ // Global configuration (typically in beforeAll)
114
+ configureDatapool({ locale: 'hu', basePath: './datapools' });
115
+
116
+ // Load a datapool
117
+ const companies = datapool<Company>('companies');
118
+
119
+ // Access items
120
+ const item = companies.byKey('it4all'); // by key (meta.key field, default: "code")
121
+ const derived = companies.derive('it4all', { name: 'Modified' }); // template + overrides
122
+ const first = companies.findFirst('city', 'Budapest'); // first match by field
123
+ const matches = companies.findAll('role', 'admin'); // all matches by field
124
+ const random = companies.random(); // random item
125
+ const all = companies.all(); // all items
126
+ const count = companies.count(); // count
127
+ ```
128
+
129
+ ## Built-in Steps
130
+
131
+ The package ships reusable TestSteps for common smartbit4all UI patterns (grid, tree navigation). These are available via a separate subpath import to keep them distinct from the core API:
132
+
133
+ ```typescript
134
+ import { selectGridRow, navigateInTree } from '@smartbit4all/playwright-qa/steps';
135
+ ```
136
+
137
+ ### Forms
138
+
139
+ ```typescript
140
+ import {
141
+ fillField, fillDate, fillDateTime,
142
+ selectOption, selectMultiple, selectRadio,
143
+ setCheckbox, setCheckboxGroup, setToggle,
144
+ addChip, removeChip, setChips,
145
+ uploadFile,
146
+ toggleAccordion,
147
+ } from '@smartbit4all/playwright-qa/steps';
148
+
149
+ // Fill a text input or textarea — by data-testid (preferred) or label text (fallback)
150
+ await fillField(dialog, 'data.name', 'Teszt mappa'); // data-testid
151
+ await fillField(page, 'dossierContentData.name', 'Teszt dosszié'); // data-testid
152
+ await fillField(page, 'Megjegyzés', 'Ez egy megjegyzés'); // label fallback
153
+
154
+ // Date picker (ISO format YYYY-MM-DD)
155
+ await fillDate(dialog, 'dataSheet.startDate', '2026-04-15');
156
+
157
+ // Date-time picker (date + time)
158
+ await fillDateTime(dialog, 'data.deadline', '2026-04-15', '14:30');
159
+
160
+ // Select from a dropdown (mat-select / p-dropdown) — by data-testid or label
161
+ await selectOption(page, 'selectedDocumentTypeCategory', 'Ügyintézés'); // data-testid
162
+ await selectOption(dialog, 'Kategória', 'Ügyintézés'); // label fallback
163
+
164
+ // Multi-select dropdown
165
+ await selectMultiple(dialog, 'data.skills', ['Angular', 'React', 'Vue']);
166
+
167
+ // Select a radio button — by data-testid on the radio group or label
168
+ await selectRadio(dialog, 'data.canIncludeFiles', 'Nem'); // data-testid
169
+ await selectRadio(dialog, 'Tartalmazhat almappákat', 'Igen'); // label fallback
170
+
171
+ // Set a checkbox — by data-testid or label
172
+ await setCheckbox(dialog, 'Aktív', true);
173
+
174
+ // Checkbox group (CHECK_BOX_2) — set multiple checkboxes at once
175
+ await setCheckboxGroup(dialog, 'permissions', { 'Olvasás': true, 'Írás': true, 'Törlés': false });
176
+
177
+ // Toggle switch
178
+ await setToggle(dialog, 'data.isActive', true);
179
+
180
+ // Chips — add, remove, or set declaratively
181
+ await addChip(dialog, 'data.tags', 'urgent');
182
+ await removeChip(dialog, 'data.tags', 'draft');
183
+ await setChips(dialog, 'data.tags', ['urgent', 'review']);
184
+
185
+ // File upload
186
+ await uploadFile(dialog, 'data.attachment', '/path/to/file.pdf');
187
+
188
+ // Open/close an accordion tab/panel (p-accordion / mat-accordion) — by data-testid or header text.
189
+ // Returns a Locator scoped to the tab's content region, for acting on elements inside it.
190
+ await toggleAccordion(dialog, 'Alapadatok', false); // close, ignore the returned content
191
+ const content = await toggleAccordion(dialog, 'Alapadatok', true); // open
192
+ await fillField(content, 'data.note', 'Ez egy megjegyzés');
193
+ ```
194
+
195
+ All form utilities accept a `data-testid` value or a visible label text as identifier. When a `data-testid` match is found on the element, it takes priority; otherwise the function falls back to label text matching (`.smart-form-widget-label`, `h3`, `h4`, or `label`). They work with both Angular Material and PrimeNG components, and accept any `Locator` or `Page` as container — use `topDialog(page)` to scope to a dialog.
196
+
197
+ #### Generic field setter
198
+
199
+ For data-driven scenarios, `setField` and `setFields` route to the right utility based on the value type and the widget's DOM signature — no need to know which form widget you are targeting.
200
+
201
+ ```typescript
202
+ import { setField, setFields } from '@smartbit4all/playwright-qa/steps';
203
+
204
+ // One field at a time
205
+ await setField(dialog, 'data.name', 'Teszt mappa'); // → fillField
206
+ await setField(dialog, 'data.canIncludeFiles', 'Igen'); // → selectRadio (widget has mat-radio-group)
207
+ await setField(dialog, 'data.category', 'Ügyintézés'); // → selectOption (widget has mat-select / p-dropdown)
208
+ await setField(dialog, 'data.isActive', true); // → setToggle / setCheckbox
209
+ await setField(dialog, 'data.skills', ['Angular', 'React']); // → selectMultiple or setChips
210
+ await setField(dialog, 'data.deadline', '2026-04-15 14:30'); // → fillDateTime (matches YYYY-MM-DD HH:mm)
211
+ await setField(dialog, 'data.attachment', '/path/file.pdf'); // → uploadFile (widget has smart-file-editor)
212
+ await setField(dialog, 'permissions', { 'Olvasás': true, 'Írás': false }); // → setCheckboxGroup
213
+
214
+ // Many fields at once
215
+ await setFields(dialog, {
216
+ 'data.name': 'Teszt mappa',
217
+ 'data.canIncludeFiles': 'Nem',
218
+ 'data.isActive': true,
219
+ 'data.tags': ['urgent', 'review'],
220
+ 'permissions': { 'Olvasás': true, 'Írás': true },
221
+ });
222
+ ```
223
+
224
+ Routing is decided by `value`'s TypeScript type plus the DOM tags inside the widget:
225
+ - `Record<string, boolean>` → `setCheckboxGroup`
226
+ - `boolean` + `mat-slide-toggle` / `p-inputSwitch` → `setToggle`
227
+ - `boolean` + `mat-checkbox` / `p-checkbox` → `setCheckbox`
228
+ - `string[]` + `mat-chip-grid` / `p-chips` → `setChips`
229
+ - `string[]` + `p-multiSelect` / `mat-select[multiple]` → `selectMultiple`
230
+ - `string` + `smart-file-editor` → `uploadFile`
231
+ - `string` + `mat-select` / `p-dropdown` → `selectOption`
232
+ - `string` + `mat-radio-group` / `p-radiobutton` → `selectRadio`
233
+ - `string` matching `YYYY-MM-DD HH:mm` → `fillDateTime`
234
+ - `string` + any `input` / `textarea` → `fillField`
235
+
236
+ If no branch matches, `setField` throws with the identifier in the message. Datetime detection is value-format based (a single `YYYY-MM-DD HH:mm` string), since the Material datetime widget has no distinctive tag — pass an ISO date+time string and let `setField` split it.
237
+
238
+ ### Grid
239
+
240
+ ```typescript
241
+ import {
242
+ selectGridRow,
243
+ checkGridRow,
244
+ checkGridRowsOnPage,
245
+ checkAllGridRows,
246
+ filterAndSelectGridRow,
247
+ applyGridFilters,
248
+ clearGridFilters,
249
+ } from '@smartbit4all/playwright-qa/steps';
250
+
251
+ // Find a row by table content (paginates automatically) and double-click it
252
+ await selectGridRow(page, { 'Azonosító': 'DOC-001' });
253
+
254
+ // Find by simple text match
255
+ await selectGridRow(page, 'DOC-001');
256
+
257
+ // Find by row data-testid (rendered from row.id on the <tr>)
258
+ await selectGridRow(page, { rowId: 'doc-12345' });
259
+
260
+ // Open the row's context menu and click an action
261
+ await selectGridRow(page, { 'Azonosító': 'DOC-001' }, 'Szerkesztés');
262
+
263
+ // Reach an action nested under submenus — pass an action path (see "Actions" below).
264
+ // Intermediate items are submenu triggers (opened by hover), the last is the leaf (clicked).
265
+ await selectGridRow(page, { 'Azonosító': 'DOC-001' }, ['MORE', 'EXPORT', 'PDF']);
266
+
267
+ // Open context menu only (for screenshots) — pass true, then close with Escape
268
+ await selectGridRow(page, { 'Azonosító': 'DOC-001' }, true);
269
+ await screenshot(page, 'grid-context-menu');
270
+ await page.keyboard.press('Escape');
271
+
272
+ // Multi-select grid: check/uncheck a single row (paginates if needed)
273
+ await checkGridRow(page, { 'Cím': 'Dokumentátor' }, true);
274
+ await checkGridRow(page, { 'Cím': 'Dokumentátor' }, false);
275
+
276
+ // Check/uncheck all matching rows on the current page (no pagination)
277
+ await checkGridRowsOnPage(page, 'Admin', true);
278
+
279
+ // Select all / deselect all via header checkbox
280
+ await checkAllGridRows(page, true);
281
+
282
+ // Use the filter form above the grid, then select the row
283
+ await filterAndSelectGridRow(page, { 'Azonosító': 'DOC-001' });
284
+
285
+ // Apply/clear filters independently
286
+ await applyGridFilters(page, { 'Név': 'Teszt' });
287
+ await clearGridFilters(page);
288
+ ```
289
+
290
+ Column keys in a row filter accept either the header label or the header `data-testid` (rendered from `col.propertyName` on the `<th>`). Both resolve to the same column, so `{ 'Adószám': '12345' }` and `{ 'taxNumber': '12345' }` are equivalent — prefer `data-testid` for stability against label/locale changes.
291
+
292
+ For row-level addressing, `{ rowId: 'doc-12345' }` matches the `<tr>` `data-testid` attribute (rendered from `row.id`). `rowId` must be the only key in the filter object and is supported by `selectGridRow`, `checkGridRow`, and `checkGridRowsOnPage`. `filterAndSelectGridRow` throws if given a `rowId` filter — use `selectGridRow` instead, since row identifiers do not need a filter form pass.
293
+
294
+ All grid functions accept `Page` or `Locator` as container — use a Locator to scope to a dialog or section:
295
+
296
+ ```typescript
297
+ const section = dialog.locator('smart-component-layout[data-testid="participants"]');
298
+ await checkGridRow(section, { 'Cím': 'Dokumentátor' }, true);
299
+ ```
300
+
301
+ ### Navigation
302
+
303
+ ```typescript
304
+ import { navigateToMain, navigateInTree } from '@smartbit4all/playwright-qa/steps';
305
+
306
+ // Click the logo to return to the main screen
307
+ await navigateToMain(page);
308
+
309
+ // Navigate a tree (PrimeNG or Angular Material) — expands intermediate nodes, clicks the last one
310
+ await navigateInTree(page, ['Ügyek']);
311
+ await navigateInTree(page, ['Dokumentumok', 'Tender']);
312
+
313
+ // Open the last node's context menu (hamburger button) and click an action
314
+ await navigateInTree(page, ['Dokumentumok', 'Bejövő e-mail'], 'Új mappa');
315
+
316
+ // Nested action: the tree path and the action path are separate arguments.
317
+ // ['Dokumentumok','Bejövő e-mail'] is the tree path; ['MORE','NEW_FOLDER'] is the action path.
318
+ await navigateInTree(page, ['Dokumentumok', 'Bejövő e-mail'], ['MORE', 'NEW_FOLDER']);
319
+
320
+ // Open context menu only (for screenshots) — pass true, then close with Escape
321
+ await navigateInTree(page, ['Dokumentumok', 'Bejövő e-mail'], true);
322
+ await screenshot(page, 'context-menu-open');
323
+ await page.keyboard.press('Escape');
324
+ ```
325
+
326
+ ### Actions
327
+
328
+ Toolbar, navbar and menu actions — including submenus. Since ng-client #29411 an action that has
329
+ child actions renders a **submenu**: reaching a nested action means opening the intermediate submenu
330
+ triggers first. `clickAction` and `openActionPath` handle that with an **action path**.
331
+
332
+ ```typescript
333
+ import { clickAction, openActionPath } from '@smartbit4all/playwright-qa/steps';
334
+
335
+ // Single action — clicks a plain toolbar button (by data-testid, then text).
336
+ await clickAction(page, 'SAVE');
337
+
338
+ // Action path: the last element is the leaf action (clicked); the earlier elements are submenu
339
+ // triggers opened along the way. Works whether the menu opens on hover or on click — you don't
340
+ // need to know which.
341
+ await clickAction(page, ['Administrator', 'Admin beállítások']);
342
+ await clickAction(page, ['MORE', 'EXPORT', 'PDF']);
343
+
344
+ // Scope to a container (dialog, section, toolbar) by passing a Locator.
345
+ await clickAction(topDialog(page), ['MORE', 'DUPLICATE']);
346
+ ```
347
+
348
+ `openActionPath` opens the same path but **returns the leaf's Locator without clicking it** — so you
349
+ can screenshot the highlighted item and click when ready:
350
+
351
+ ```typescript
352
+ const leaf = await openActionPath(page, ['MORE', 'EXPORT', 'PDF']);
353
+ await screenshotRegionHighlight(page, 'export-pdf-highlight', findPopupMenu(page), leaf);
354
+ await leaf.click();
355
+ ```
356
+
357
+ `openActionPath` is dual-mode by whether a menu is already open: with none open, `path[0]` is the
358
+ top-level trigger to open; with one open (e.g. after a grid/tree menu button, or after
359
+ `navigateInTree(..., true)`), `path[0]` is the first submenu trigger inside it. `clickAction` is
360
+ exactly `openActionPath` followed by clicking the returned leaf.
361
+
362
+ The same action-path form is accepted by the grid and navigation helpers (`selectGridRow`,
363
+ `filterAndSelectGridRow`, `navigateInTree`) for their menu action argument.
364
+
365
+ ### Dialog
366
+
367
+ ```typescript
368
+ import { topDialog, clickInDialog } from '@smartbit4all/playwright-qa/steps';
369
+
370
+ // Get the topmost open dialog (Angular Material or PrimeNG)
371
+ const dialog = topDialog(page);
372
+
373
+ // Click a button inside the topmost dialog — useful when multiple dialogs are stacked
374
+ await clickInDialog(page, 'Mentés');
375
+
376
+ // Example: dialog-in-dialog workflow
377
+ await clickInDialog(page, 'Akció beállítás'); // opens second dialog on top
378
+ await screenshot(page, 'second-dialog');
379
+ await clickInDialog(page, 'Mentés'); // clicks Mentés in the TOP dialog
380
+ // first dialog is still open
381
+ await clickInDialog(page, 'Bezárás'); // closes first dialog
382
+ ```
383
+
384
+ When multiple dialogs are open, `topDialog` always targets the last (topmost) one. This prevents accidentally clicking buttons in a background dialog.
385
+
386
+ ### Utilities
387
+
388
+ ```typescript
389
+ import { waitForAngularIdle } from '@smartbit4all/playwright-qa/steps';
390
+
391
+ // Wait for Angular SPA to settle (double networkidle with pause)
392
+ await waitForAngularIdle(page);
393
+ ```
394
+
395
+ `waitForAngularIdle` is also available for project-specific steps that need the same wait pattern.
396
+
397
+ ### Locators
398
+
399
+ ```typescript
400
+ import { findButton, findPopupMenu, findMenuItem } from '@smartbit4all/playwright-qa/steps';
401
+
402
+ // Find a button by data-testid (preferred) or visible text (fallback)
403
+ await findButton(page, 'REFRESH').click(); // matches data-testid="REFRESH"
404
+ await findButton(page, 'Mentés').click(); // matches button text "Mentés"
405
+
406
+ // Works inside a container (e.g., dialog)
407
+ await findButton(topDialog(page), 'Mentés').click();
408
+
409
+ // Find the currently visible popup menu (Angular Material or PrimeNG)
410
+ const menu = findPopupMenu(page);
411
+
412
+ // Find a menu item by data-testid or text
413
+ await findMenuItem(page, 'Szerkesztés').click();
414
+ await findMenuItem(page, 'DELETE_ACTION').click(); // matches data-testid="DELETE_ACTION"
415
+
416
+ // Combine with screenshot functions for highlighting
417
+ await screenshotHighlight(page, 'menu-highlight', findMenuItem(page, 'Új mappa'));
418
+ await screenshotRegionHighlight(page, 'menu-region',
419
+ findPopupMenu(page), findMenuItem(page, 'Új mappa'));
420
+ ```
421
+
422
+ All built-in steps (`clickInDialog`, `selectGridRow`, `navigateInTree`) use these locators internally, so they automatically support `data-testid` values alongside text matching.
423
+
424
+ ### Debug
425
+
426
+ ```typescript
427
+ import { dumpDom } from '@smartbit4all/playwright-qa/steps';
428
+
429
+ // Dump visible elements on the page — useful when writing a new locator
430
+ console.log(await dumpDom(page));
431
+
432
+ // Scope to a container (dialog, section, grid row)
433
+ console.log(await dumpDom(topDialog(page)));
434
+
435
+ // Narrow with a selector
436
+ console.log(await dumpDom(page, { selector: 'button, [data-testid]' }));
437
+
438
+ // Include hidden elements and longer text
439
+ console.log(await dumpDom(page, { visibleOnly: false, maxText: 200 }));
440
+ ```
441
+
442
+ Each entry contains `tag`, `id`, `classes`, `testId` and own `text` (text nodes only, not the recursive `textContent`). Non-rendering tags (`script`, `style`, `meta`, `link`, `head`, `html`, `noscript`) are filtered out, and by default only visible elements are returned. Use this during test authoring to discover available `data-testid` values without manually inspecting the DOM in DevTools.
443
+
444
+ The `testId` field falls back through `data-testid` → `data-automationid`. PrimeNG `MenuItem` (e.g. grid row popup menus) does not expose `data-testid`; only the `automationId` MenuItem property is supported, and it renders as the `data-automationid` DOM attribute. `findMenuItem` honors both attributes.
445
+
446
+ ## Developer Guide — Writing TestSteps
447
+
448
+ TestSteps are simple async functions that implement one atomic UI operation.
449
+
450
+ **Rules:**
451
+ - Receive data as parameters — never import or reference datapools
452
+ - Include locator logic, waits, technical assertions, and screenshots
453
+ - Group related steps in one file per entity/feature
454
+
455
+ ```typescript
456
+ // steps/company.steps.ts
457
+ import { Page } from '@playwright/test';
458
+ import { screenshot } from '@smartbit4all/playwright-qa';
459
+
460
+ export async function fillCompanyForm(page: Page, company: Company) {
461
+ await page.fill('[data-testid="company-name"]', company.name);
462
+ await page.fill('[data-testid="tax-number"]', company.taxNumber);
463
+ await screenshot(page, 'company/form-filled');
464
+ await page.click('[data-testid="save-button"]');
465
+ await page.waitForSelector('.success-toast');
466
+ await screenshot(page, 'company/saved');
467
+ }
468
+ ```
469
+
470
+ ## Test Designer Guide — Writing TestSuites
471
+
472
+ TestSuites are Playwright `test.describe` blocks that read like a scenario script.
473
+
474
+ **Rules:**
475
+ - Call `initSuite()` in `beforeAll` to reset state and configure screenshots
476
+ - Initialize datapools in `beforeAll`
477
+ - Select data, then call TestSteps in order
478
+ - Add business-level assertions
479
+
480
+ ```typescript
481
+ // suites/company-management.suite.ts
482
+ import { test } from '@playwright/test';
483
+ import { initSuite, datapool, unique, type DataPool } from '@smartbit4all/playwright-qa';
484
+ import { navigateToMain, selectGridRow } from '@smartbit4all/playwright-qa/steps';
485
+ import { fillCompanyForm } from '../steps/company.steps';
486
+ import type { Company } from '../types';
487
+
488
+ test.describe('Company Management', () => {
489
+ let companies: DataPool<Company>;
490
+
491
+ test.beforeAll(() => {
492
+ initSuite({ screenshotDir: './screenshots' });
493
+ companies = datapool<Company>('companies');
494
+ });
495
+
496
+ test('Create new company', async ({ page }) => {
497
+ const company = companies.derive('it4all', { taxNumber: unique('TAX') });
498
+ await fillCompanyForm(page, company);
499
+ await navigateToMain(page);
500
+ await selectGridRow(page, { 'Adószám': company.taxNumber });
501
+ });
502
+ });
503
+ ```
504
+
505
+ ## Seed Framework
506
+
507
+ Seed populates application data from datapools — either via API or through the UI.
508
+
509
+ ### API Seeding
510
+
511
+ ```typescript
512
+ import { registerSeedHandler, seedViaApi } from '@smartbit4all/playwright-qa';
513
+
514
+ // Register a handler (project-specific)
515
+ registerSeedHandler<Company>('companies', {
516
+ seed: async (company, options) => {
517
+ await fetch(`${options.baseUrl}/api/companies`, {
518
+ method: 'POST',
519
+ headers: { Authorization: `Bearer ${options.authToken}` },
520
+ body: JSON.stringify(company),
521
+ });
522
+ },
523
+ });
524
+
525
+ // Seed all companies
526
+ const result = await seedViaApi('companies', { baseUrl, authToken });
527
+ console.log(`Seeded ${result.success}/${result.total}`);
528
+ ```
529
+
530
+ ### UI Seeding
531
+
532
+ ```typescript
533
+ import { registerUiSeedStep, seedViaUi } from '@smartbit4all/playwright-qa';
534
+
535
+ // Register a UI seed step (reuses your TestSteps)
536
+ registerUiSeedStep<Company>('companies', async (page, company) => {
537
+ await fillCompanyForm(page, company);
538
+ });
539
+
540
+ // Seed through the UI
541
+ const result = await seedViaUi(page, 'companies', { basePath: './datapools', locale: 'hu' });
542
+ ```
543
+
544
+ ### Seed Profiles
545
+
546
+ For complex data with dependencies, use seed profiles:
547
+
548
+ ```json
549
+ {
550
+ "name": "development-full",
551
+ "order": ["users", "organizations", "companies", "documents"],
552
+ "dependencies": {
553
+ "documents": ["companies", "users"],
554
+ "companies": ["organizations"]
555
+ }
556
+ }
557
+ ```
558
+
559
+ ```typescript
560
+ import { seed } from '@smartbit4all/playwright-qa';
561
+
562
+ const profile = JSON.parse(fs.readFileSync('seed-profiles/dev.json', 'utf-8'));
563
+ const results = await seed(profile, 'api', { baseUrl, authToken, basePath: './datapools' });
564
+ ```
565
+
566
+ ## CI Setup
567
+
568
+ ### Azure DevOps
569
+
570
+ 1. Copy `templates/ci/playwright-qa-pipeline.yml` to your project repo
571
+ 2. Set pipeline variables:
572
+ - `BASE_URL`: Your application URL (e.g., `https://staging.example.com`)
573
+ - `LOCALE`: Datapool locale (e.g., `hu`)
574
+ 3. The pipeline will:
575
+ - Run all Playwright tests
576
+ - Publish JUnit results to the Test tab
577
+ - Upload screenshots and HTML report as artifacts
578
+
579
+ ### Project Setup
580
+
581
+ Copy the `templates/playwright-qa/` directory to your project repo:
582
+
583
+ ```bash
584
+ cp -r node_modules/@smartbit4all/playwright-qa/templates/playwright-qa ./playwright-qa
585
+ cd playwright-qa
586
+ npm install
587
+ npx playwright install --with-deps chromium
588
+ ```
589
+
590
+ ## Development
591
+
592
+ ```bash
593
+ git clone <repo-url>
594
+ cd platform-playwright
595
+ npm install
596
+ npx playwright install --with-deps chromium
597
+ npm test
598
+ ```
599
+
600
+ ### Testing locally in a project
601
+
602
+ Use `npm link` to try the package in your own project without publishing:
603
+
604
+ ```bash
605
+ # In platform-playwright:
606
+ npm run build
607
+ npm link
608
+
609
+ # In your project:
610
+ npm link @smartbit4all/playwright-qa
611
+ ```
612
+
613
+ After linking, your project uses the local build directly. When you make changes to the package, rebuild with `npm run build` — no need to re-link.
614
+
615
+ To remove the link later:
616
+
617
+ ```bash
618
+ # In your project:
619
+ npm unlink @smartbit4all/playwright-qa
620
+ npm install
621
+ ```
622
+
623
+ ## License
624
+
625
+ LGPL-3.0-or-later