@smartbit4all/playwright-qa
v0.2.8
Published
Playwright-based testing and documentation framework for smartbit4all projects
Downloads
783
Readme
@smartbit4all/playwright-qa
Playwright-based testing and documentation framework. Provides screenshot utilities, typed datapools with locale support, and TestStep/TestSuite conventions.
Installation
npm install @smartbit4all/playwright-qaRequires Node.js 20+ and Playwright as a peer dependency:
npm install @playwright/test
npx playwright install --with-deps chromiumQuick Start
import { test } from '@playwright/test';
import { initSuite, screenshot } from '@smartbit4all/playwright-qa';
test.beforeAll(() => {
initSuite({ screenshotDir: './screenshots' });
});
test('take a screenshot', async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc');
await screenshot(page, 'todomvc-home');
});Suite Setup
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.
import { initSuite } from '@smartbit4all/playwright-qa';
// With screenshots — each run creates a new timestamped subdirectory
initSuite({ screenshotDir: './screenshots' });
// Clean previous run — deletes the last run's folder before creating a new one
initSuite({ screenshotDir: './screenshots', cleanScreenshots: true });
// Without screenshots — tests run normally, screenshot calls return null
initSuite();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.
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.
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.
Viewport
Use getViewport() in your playwright.config.ts to automatically switch between full-size and headed viewport:
import { defineConfig } from '@playwright/test';
import { getViewport } from '@smartbit4all/playwright-qa';
export default defineConfig({
use: {
viewport: getViewport(),
},
});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:
{
"viewport": { "width": 1920, "height": 1080 },
"headedViewport": { "width": 1280, "height": 720 }
}API Reference
Screenshot
import { screenshot, screenshotHighlight, screenshotRegion, screenshotRegionHighlight } from '@smartbit4all/playwright-qa';
// Full page screenshot — returns file path, or null if screenshots are disabled
await screenshot(page, 'page-name');
await screenshot(page, 'category/page-name'); // name path = subdirectory
await screenshot(page, 'page-name', { fullPage: true });
// Full page with highlighted element — accepts CSS selector or Locator
await screenshotHighlight(page, 'name', '#selector');
await screenshotHighlight(page, 'name', '#selector', { style: 'subtle' });
await screenshotHighlight(page, 'name', findMenuItem(page, 'Új mappa')); // Locator
// Region screenshot (single element) — accepts CSS selector or Locator
await screenshotRegion(page, 'name', '#region-selector');
// Region with highlighted element inside — both accept CSS selector or Locator
await screenshotRegionHighlight(page, 'name', '#region', '#highlight');
await screenshotRegionHighlight(page, 'name', findPopupMenu(page), findMenuItem(page, 'Új mappa'));All screenshot functions return Promise<string | null>. They return null when no screenshot directory has been configured (see Suite Setup). Highlight and region parameters accept both CSS selectors (string) and Playwright Locators.
Datapool
import { datapool, configureDatapool } from '@smartbit4all/playwright-qa';
// Global configuration (typically in beforeAll)
configureDatapool({ locale: 'hu', basePath: './datapools' });
// Load a datapool
const companies = datapool<Company>('companies');
// Access items
const item = companies.byKey('it4all'); // by key (meta.key field, default: "code")
const derived = companies.derive('it4all', { name: 'Modified' }); // template + overrides
const first = companies.findFirst('city', 'Budapest'); // first match by field
const matches = companies.findAll('role', 'admin'); // all matches by field
const random = companies.random(); // random item
const all = companies.all(); // all items
const count = companies.count(); // countBuilt-in Steps
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:
import { selectGridRow, navigateInTree } from '@smartbit4all/playwright-qa/steps';Forms
import {
fillField, fillDate, fillDateTime,
selectOption, selectMultiple, selectRadio,
setCheckbox, setCheckboxGroup, setToggle,
addChip, removeChip, setChips,
uploadFile,
toggleAccordion,
} from '@smartbit4all/playwright-qa/steps';
// Fill a text input or textarea — by data-testid (preferred) or label text (fallback)
await fillField(dialog, 'data.name', 'Teszt mappa'); // data-testid
await fillField(page, 'dossierContentData.name', 'Teszt dosszié'); // data-testid
await fillField(page, 'Megjegyzés', 'Ez egy megjegyzés'); // label fallback
// Date picker (ISO format YYYY-MM-DD)
await fillDate(dialog, 'dataSheet.startDate', '2026-04-15');
// Date-time picker (date + time)
await fillDateTime(dialog, 'data.deadline', '2026-04-15', '14:30');
// Select from a dropdown (mat-select / p-dropdown) — by data-testid or label
await selectOption(page, 'selectedDocumentTypeCategory', 'Ügyintézés'); // data-testid
await selectOption(dialog, 'Kategória', 'Ügyintézés'); // label fallback
// Multi-select dropdown
await selectMultiple(dialog, 'data.skills', ['Angular', 'React', 'Vue']);
// Select a radio button — by data-testid on the radio group or label
await selectRadio(dialog, 'data.canIncludeFiles', 'Nem'); // data-testid
await selectRadio(dialog, 'Tartalmazhat almappákat', 'Igen'); // label fallback
// Set a checkbox — by data-testid or label
await setCheckbox(dialog, 'Aktív', true);
// Checkbox group (CHECK_BOX_2) — set multiple checkboxes at once
await setCheckboxGroup(dialog, 'permissions', { 'Olvasás': true, 'Írás': true, 'Törlés': false });
// Toggle switch
await setToggle(dialog, 'data.isActive', true);
// Chips — add, remove, or set declaratively
await addChip(dialog, 'data.tags', 'urgent');
await removeChip(dialog, 'data.tags', 'draft');
await setChips(dialog, 'data.tags', ['urgent', 'review']);
// File upload
await uploadFile(dialog, 'data.attachment', '/path/to/file.pdf');
// Open/close an accordion tab/panel (p-accordion / mat-accordion) — by data-testid or header text.
// Returns a Locator scoped to the tab's content region, for acting on elements inside it.
await toggleAccordion(dialog, 'Alapadatok', false); // close, ignore the returned content
const content = await toggleAccordion(dialog, 'Alapadatok', true); // open
await fillField(content, 'data.note', 'Ez egy megjegyzés');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.
Generic field setter
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.
import { setField, setFields } from '@smartbit4all/playwright-qa/steps';
// One field at a time
await setField(dialog, 'data.name', 'Teszt mappa'); // → fillField
await setField(dialog, 'data.canIncludeFiles', 'Igen'); // → selectRadio (widget has mat-radio-group)
await setField(dialog, 'data.category', 'Ügyintézés'); // → selectOption (widget has mat-select / p-dropdown)
await setField(dialog, 'data.isActive', true); // → setToggle / setCheckbox
await setField(dialog, 'data.skills', ['Angular', 'React']); // → selectMultiple or setChips
await setField(dialog, 'data.deadline', '2026-04-15 14:30'); // → fillDateTime (matches YYYY-MM-DD HH:mm)
await setField(dialog, 'data.attachment', '/path/file.pdf'); // → uploadFile (widget has smart-file-editor)
await setField(dialog, 'permissions', { 'Olvasás': true, 'Írás': false }); // → setCheckboxGroup
// Many fields at once
await setFields(dialog, {
'data.name': 'Teszt mappa',
'data.canIncludeFiles': 'Nem',
'data.isActive': true,
'data.tags': ['urgent', 'review'],
'permissions': { 'Olvasás': true, 'Írás': true },
});Routing is decided by value's TypeScript type plus the DOM tags inside the widget:
Record<string, boolean>→setCheckboxGroupboolean+mat-slide-toggle/p-inputSwitch→setToggleboolean+mat-checkbox/p-checkbox→setCheckboxstring[]+mat-chip-grid/p-chips→setChipsstring[]+p-multiSelect/mat-select[multiple]→selectMultiplestring+smart-file-editor→uploadFilestring+mat-select/p-dropdown→selectOptionstring+mat-radio-group/p-radiobutton→selectRadiostringmatchingYYYY-MM-DD HH:mm→fillDateTimestring+ anyinput/textarea→fillField
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.
Grid
import {
selectGridRow,
findGridRow,
checkGridRow,
checkGridRowsOnPage,
checkAllGridRows,
filterAndSelectGridRow,
applyGridFilters,
clearGridFilters,
} from '@smartbit4all/playwright-qa/steps';
// Find a row by table content (paginates automatically) and double-click it
await selectGridRow(page, { 'Azonosító': 'DOC-001' });
// Find by simple text match
await selectGridRow(page, 'DOC-001');
// Find by row data-testid (rendered from row.id on the <tr>)
await selectGridRow(page, { rowId: 'doc-12345' });
// Open the row's context menu and click an action
await selectGridRow(page, { 'Azonosító': 'DOC-001' }, 'Szerkesztés');
// Reach an action nested under submenus — pass an action path (see "Actions" below).
// Intermediate items are submenu triggers (opened by hover), the last is the leaf (clicked).
await selectGridRow(page, { 'Azonosító': 'DOC-001' }, ['MORE', 'EXPORT', 'PDF']);
// Open context menu only (for screenshots) — pass true, then close with Escape
await selectGridRow(page, { 'Azonosító': 'DOC-001' }, true);
await screenshot(page, 'grid-context-menu');
await page.keyboard.press('Escape');
// Multi-select grid: check/uncheck a single row (paginates if needed)
await checkGridRow(page, { 'Cím': 'Dokumentátor' }, true);
await checkGridRow(page, { 'Cím': 'Dokumentátor' }, false);
// Check/uncheck all matching rows on the current page (no pagination)
await checkGridRowsOnPage(page, 'Admin', true);
// Select all / deselect all via header checkbox
await checkAllGridRows(page, true);
// Use the filter form above the grid, then select the row
await filterAndSelectGridRow(page, { 'Azonosító': 'DOC-001' });
// Apply/clear filters independently
await applyGridFilters(page, { 'Név': 'Teszt' });
await clearGridFilters(page);
// Find a row/card without acting on it — for content assertions (paginates automatically,
// works for both table-rendered and card-rendered grids)
const row = await findGridRow(page, { 'Azonosító': 'DOC-001' });
await expect(row.locator('td:nth-child(2)')).toHaveText('Aktív');
const card = await findGridRow(page, { rowId: '2' });
await expect(card.locator('markdown p')).toContainText('szöveg, amit ellenőrizni akarunk');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.
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, findGridRow, checkGridRow, and checkGridRowsOnPage. filterAndSelectGridRow throws if given a rowId filter — use selectGridRow instead, since row identifiers do not need a filter form pass. Note that a plain string filter matches visible text anywhere in the row/card — it does not match data-testid; use { rowId } for that.
All grid functions accept Page or Locator as container — use a Locator to scope to a dialog or section:
const section = dialog.locator('smart-component-layout[data-testid="participants"]');
await checkGridRow(section, { 'Cím': 'Dokumentátor' }, true);Navigation
import { navigateToMain, navigateInTree } from '@smartbit4all/playwright-qa/steps';
// Click the logo to return to the main screen
await navigateToMain(page);
// Navigate a tree (PrimeNG or Angular Material) — expands intermediate nodes, clicks the last one
await navigateInTree(page, ['Ügyek']);
await navigateInTree(page, ['Dokumentumok', 'Tender']);
// Open the last node's context menu (hamburger button) and click an action
await navigateInTree(page, ['Dokumentumok', 'Bejövő e-mail'], 'Új mappa');
// Nested action: the tree path and the action path are separate arguments.
// ['Dokumentumok','Bejövő e-mail'] is the tree path; ['MORE','NEW_FOLDER'] is the action path.
await navigateInTree(page, ['Dokumentumok', 'Bejövő e-mail'], ['MORE', 'NEW_FOLDER']);
// Open context menu only (for screenshots) — pass true, then close with Escape
await navigateInTree(page, ['Dokumentumok', 'Bejövő e-mail'], true);
await screenshot(page, 'context-menu-open');
await page.keyboard.press('Escape');Actions
Toolbar, navbar and menu actions — including submenus. Since ng-client #29411 an action that has
child actions renders a submenu: reaching a nested action means opening the intermediate submenu
triggers first. clickAction and openActionPath handle that with an action path.
import { clickAction, openActionPath } from '@smartbit4all/playwright-qa/steps';
// Single action — clicks a plain toolbar button (by data-testid, then text).
await clickAction(page, 'SAVE');
// Action path: the last element is the leaf action (clicked); the earlier elements are submenu
// triggers opened along the way. Works whether the menu opens on hover or on click — you don't
// need to know which.
await clickAction(page, ['Administrator', 'Admin beállítások']);
await clickAction(page, ['MORE', 'EXPORT', 'PDF']);
// Scope to a container (dialog, section, toolbar) by passing a Locator.
await clickAction(topDialog(page), ['MORE', 'DUPLICATE']);openActionPath opens the same path but returns the leaf's Locator without clicking it — so you
can screenshot the highlighted item and click when ready:
const leaf = await openActionPath(page, ['MORE', 'EXPORT', 'PDF']);
await screenshotRegionHighlight(page, 'export-pdf-highlight', findPopupMenu(page), leaf);
await leaf.click();openActionPath is dual-mode by whether a menu is already open: with none open, path[0] is the
top-level trigger to open; with one open (e.g. after a grid/tree menu button, or after
navigateInTree(..., true)), path[0] is the first submenu trigger inside it. clickAction is
exactly openActionPath followed by clicking the returned leaf.
The same action-path form is accepted by the grid and navigation helpers (selectGridRow,
filterAndSelectGridRow, navigateInTree) for their menu action argument.
Dialog
import { topDialog, clickInDialog } from '@smartbit4all/playwright-qa/steps';
// Get the topmost open dialog (Angular Material or PrimeNG)
const dialog = topDialog(page);
// Click a button inside the topmost dialog — useful when multiple dialogs are stacked
await clickInDialog(page, 'Mentés');
// Example: dialog-in-dialog workflow
await clickInDialog(page, 'Akció beállítás'); // opens second dialog on top
await screenshot(page, 'second-dialog');
await clickInDialog(page, 'Mentés'); // clicks Mentés in the TOP dialog
// first dialog is still open
await clickInDialog(page, 'Bezárás'); // closes first dialogWhen multiple dialogs are open, topDialog always targets the last (topmost) one. This prevents accidentally clicking buttons in a background dialog.
Download
import { downloadWithButton } from '@smartbit4all/playwright-qa/steps';
// Clicks the button and returns the Playwright Download object
const download = await downloadWithButton(page, 'DOWNLOAD_DOCUMENT');
expect(download.suggestedFilename()).toBe('report.pdf');
const path = await download.path(); // temp copy, for reading/asserting file content
// To keep the file with its real name/extension on disk:
await download.saveAs(`downloads/${download.suggestedFilename()}`);A plain .click() is not enough for a download-triggering button — Playwright only exposes the
resulting file through the download event, so without capturing it the test has no handle to
assert or save the file. downloadWithButton wraps the click with page.waitForEvent('download')
for you. Note it listens on page itself: if the button opens the download in a new tab
(target="_blank" / window.open), the event fires on that new page instead and this helper
won't see it.
Utilities
import { waitForAngularIdle } from '@smartbit4all/playwright-qa/steps';
// Wait for Angular SPA to settle (double networkidle with pause)
await waitForAngularIdle(page);waitForAngularIdle is also available for project-specific steps that need the same wait pattern.
Locators
import { findButton, findPopupMenu, findMenuItem } from '@smartbit4all/playwright-qa/steps';
// Find a button by data-testid (preferred) or visible text (fallback)
await findButton(page, 'REFRESH').click(); // matches data-testid="REFRESH"
await findButton(page, 'Mentés').click(); // matches button text "Mentés"
// Works inside a container (e.g., dialog)
await findButton(topDialog(page), 'Mentés').click();
// Find the currently visible popup menu (Angular Material or PrimeNG)
const menu = findPopupMenu(page);
// Find a menu item by data-testid or text
await findMenuItem(page, 'Szerkesztés').click();
await findMenuItem(page, 'DELETE_ACTION').click(); // matches data-testid="DELETE_ACTION"
// Combine with screenshot functions for highlighting
await screenshotHighlight(page, 'menu-highlight', findMenuItem(page, 'Új mappa'));
await screenshotRegionHighlight(page, 'menu-region',
findPopupMenu(page), findMenuItem(page, 'Új mappa'));All built-in steps (clickInDialog, selectGridRow, navigateInTree) use these locators internally, so they automatically support data-testid values alongside text matching.
Debug
import { dumpDom } from '@smartbit4all/playwright-qa/steps';
// Dump visible elements on the page — useful when writing a new locator
console.log(await dumpDom(page));
// Scope to a container (dialog, section, grid row)
console.log(await dumpDom(topDialog(page)));
// Narrow with a selector
console.log(await dumpDom(page, { selector: 'button, [data-testid]' }));
// Include hidden elements and longer text
console.log(await dumpDom(page, { visibleOnly: false, maxText: 200 }));Each entry contains tag, id, classes, testId, testId2 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.
The testId field falls back through data-testid → data-automationid, and testId2 reports data-testid2 separately — ui-action-button renders the bare action code as data-testid and the identifier-qualified code ({code}_{identifier}) as data-testid2, and the action helpers (findButton, findMenuItem, clickAction) accept either form. 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.
Developer Guide — Writing TestSteps
TestSteps are simple async functions that implement one atomic UI operation.
Rules:
- Receive data as parameters — never import or reference datapools
- Include locator logic, waits, technical assertions, and screenshots
- Group related steps in one file per entity/feature
// steps/company.steps.ts
import { Page } from '@playwright/test';
import { screenshot } from '@smartbit4all/playwright-qa';
export async function fillCompanyForm(page: Page, company: Company) {
await page.fill('[data-testid="company-name"]', company.name);
await page.fill('[data-testid="tax-number"]', company.taxNumber);
await screenshot(page, 'company/form-filled');
await page.click('[data-testid="save-button"]');
await page.waitForSelector('.success-toast');
await screenshot(page, 'company/saved');
}Test Designer Guide — Writing TestSuites
TestSuites are Playwright test.describe blocks that read like a scenario script.
Rules:
- Call
initSuite()inbeforeAllto reset state and configure screenshots - Initialize datapools in
beforeAll - Select data, then call TestSteps in order
- Add business-level assertions
// suites/company-management.suite.ts
import { test } from '@playwright/test';
import { initSuite, datapool, unique, type DataPool } from '@smartbit4all/playwright-qa';
import { navigateToMain, selectGridRow } from '@smartbit4all/playwright-qa/steps';
import { fillCompanyForm } from '../steps/company.steps';
import type { Company } from '../types';
test.describe('Company Management', () => {
let companies: DataPool<Company>;
test.beforeAll(() => {
initSuite({ screenshotDir: './screenshots' });
companies = datapool<Company>('companies');
});
test('Create new company', async ({ page }) => {
const company = companies.derive('it4all', { taxNumber: unique('TAX') });
await fillCompanyForm(page, company);
await navigateToMain(page);
await selectGridRow(page, { 'Adószám': company.taxNumber });
});
});Seed Framework
Seed populates application data from datapools — either via API or through the UI.
API Seeding
import { registerSeedHandler, seedViaApi } from '@smartbit4all/playwright-qa';
// Register a handler (project-specific)
registerSeedHandler<Company>('companies', {
seed: async (company, options) => {
await fetch(`${options.baseUrl}/api/companies`, {
method: 'POST',
headers: { Authorization: `Bearer ${options.authToken}` },
body: JSON.stringify(company),
});
},
});
// Seed all companies
const result = await seedViaApi('companies', { baseUrl, authToken });
console.log(`Seeded ${result.success}/${result.total}`);UI Seeding
import { registerUiSeedStep, seedViaUi } from '@smartbit4all/playwright-qa';
// Register a UI seed step (reuses your TestSteps)
registerUiSeedStep<Company>('companies', async (page, company) => {
await fillCompanyForm(page, company);
});
// Seed through the UI
const result = await seedViaUi(page, 'companies', { basePath: './datapools', locale: 'hu' });Seed Profiles
For complex data with dependencies, use seed profiles:
{
"name": "development-full",
"order": ["users", "organizations", "companies", "documents"],
"dependencies": {
"documents": ["companies", "users"],
"companies": ["organizations"]
}
}import { seed } from '@smartbit4all/playwright-qa';
const profile = JSON.parse(fs.readFileSync('seed-profiles/dev.json', 'utf-8'));
const results = await seed(profile, 'api', { baseUrl, authToken, basePath: './datapools' });CI Setup
Azure DevOps
- Copy
templates/ci/playwright-qa-pipeline.ymlto your project repo - Set pipeline variables:
BASE_URL: Your application URL (e.g.,https://staging.example.com)LOCALE: Datapool locale (e.g.,hu)
- The pipeline will:
- Run all Playwright tests
- Publish JUnit results to the Test tab
- Upload screenshots and HTML report as artifacts
Project Setup
Copy the templates/playwright-qa/ directory to your project repo:
cp -r node_modules/@smartbit4all/playwright-qa/templates/playwright-qa ./playwright-qa
cd playwright-qa
npm install
npx playwright install --with-deps chromiumDevelopment
git clone <repo-url>
cd platform-playwright
npm install
npx playwright install --with-deps chromium
npm testTesting locally in a project
Use npm link to try the package in your own project without publishing:
# In platform-playwright:
npm run build
npm link
# In your project:
npm link @smartbit4all/playwright-qaAfter 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.
To remove the link later:
# In your project:
npm unlink @smartbit4all/playwright-qa
npm installLicense
LGPL-3.0-or-later
