@smartbit4all/playwright-qa
v0.1.11
Published
Playwright-based testing and documentation framework for smartbit4all projects
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,
} 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');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,
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');
// 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);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, checkGridRow, and checkGridRowsOnPage. filterAndSelectGridRow throws if given a rowId filter — use selectGridRow instead, since row identifiers do not need a filter form pass.
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');
// 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');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.
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 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. 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
