@smartbit4all/playwright-qa 0.2.3 → 0.2.4
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 +625 -625
- package/dist/steps/actions.d.ts.map +1 -1
- package/dist/steps/actions.js +5 -2
- package/dist/steps/actions.js.map +1 -1
- package/dist/steps/locators.d.ts +11 -5
- package/dist/steps/locators.d.ts.map +1 -1
- package/dist/steps/locators.js +17 -9
- package/dist/steps/locators.js.map +1 -1
- package/package.json +64 -64
- package/templates/ci/playwright-qa-pipeline.yml +54 -54
- package/templates/playwright-qa/package.json +14 -14
- package/templates/playwright-qa/playwright.config.ts +18 -18
- package/templates/playwright-qa/tsconfig.json +11 -11
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
|