QSF Playwright E2E Conventions
Project-specific conventions for the QSF Playwright suite. For generic Playwright
best practices (locators, auto-waiting, POM, anti-patterns), follow the test-ui
skill — this skill only adds what is QSF-specific.
Commands
source ./bin/load_env.sh && bunx playwright test— run all tests headlesssource ./bin/load_env.sh && bunx playwright test --project=regression-tests— skip auth-setupbun run test/bunx playwright test --ui— interactive UI modebunx playwright test --reporter=list— verbose terminal outputbunx playwright test --grep '@smoke'— run tagged testsbunx playwright show-trace <trace.zip>— inspect failure traces
Test IDs
getByTestId resolves to the data-test-label attribute (set via
testIdAttribute in playwright.config.ts):
page.getByTestId("no-handles-available-indicator");
page.getByTestId("open-filter-section-button");
Fixtures (Auth Pattern)
The project extends Playwright's test with pre-authenticated user fixtures via
storage state files in playwright-tests/.auth/.
// fixtures/authFixtures.ts
const authFiles = {
guest: "guest_storage_state.json",
testView: "test_view_storage_state.json",
testNoRights: "test_no_rights_storage_state.json",
// ... more users
};
const baseFixtures = Object.fromEntries(
Object.entries(authFiles).map(([key, fileName]) => [
key,
async ({ browser }: { browser: Browser }, use: (page: Page) => Promise<void>) => {
const storageState = path.join(__dirname, "../.auth", fileName);
const context = await browser.newContext({ storageState });
const page = await context.newPage();
try {
await use(page);
} finally {
await context.close();
}
},
]),
);
export const test = baseTest.extend(baseFixtures);
export const expect = test.expect;
Fixture Usage
// Tests import from authFixtures, NOT from @playwright/test
import { test, expect } from "../../fixtures/authFixtures";
test("user sees their handles", async ({ guest }) => {
await guest.goto("/"); // `guest` is a pre-authenticated Page — no login needed
});
test("restricted user cannot access admin", async ({ testNoRights }) => {
await testNoRights.goto("/admin");
});
File Organization
playwright-tests/
├── .auth/ # Storage state JSON files (gitignored in CI)
├── enums/ # Constants: handle names, error messages, labels
├── fixtures/ # authFixtures.ts — extended test/expect
├── pageObjects/ # POM classes (BasePage, LoginPage, ...)
│ └── components/ # Reusable: Navigation, FileUpload, FormElements
├── setup/ # auth.setup.ts, cleanup.ts
└── tests/ # Spec files grouped by feature (handle/, ping-files/, ...)
Page Objects
Pages extend BasePage (pageObjects/BasePage.ts), which composes
Navigation, FilterSection, and IdleModal. Specialized pages (e.g.
AppsDetailPage) add components like FileUpload and Iwa. Keep
FormElements.ts under 300 lines — split by element type when it grows.
Enums & Constants
Store UI labels, error messages, and handle identifiers in
playwright-tests/enums/. Never hard-code them in tests.
// enums/handles.ts
export const HandleNames = { PING_FILES: '___ping-files-en', ... };
export const HandleIds = { PING_FILES: 'qsf-handle-ping-files', ... };
// enums/errors.ts
export const Errors = { FILE_TOO_LARGE: 'produced an error while processing', ... };
// enums/elementLabels.ts
export const Buttons = { SUBMIT: 'Submit', UPLOAD: 'Upload files', ... };
Typical Test Pattern
import { test, expect } from "../../fixtures/authFixtures";
import { AppsDetailPage } from "../../pageObjects/AppsDetailPage";
import { MyAppsPage } from "../../pageObjects/MyAppsPage";
import { HandleNames } from "../../enums/handles";
test.describe("Feature Area", () => {
test.beforeEach(async ({ guest }) => {
const myAppsPage = new MyAppsPage(guest);
await guest.goto("/");
await myAppsPage.navigation.openHandle(HandleNames.PING_FILES);
});
test("describes expected behavior", async ({ guest }) => {
const appsDetailPage = new AppsDetailPage(guest);
await appsDetailPage.doSomething();
await appsDetailPage.assertSomething(expected);
});
});
When beforeEach navigation + upload + dashboard assertion is duplicated across
describe blocks, extract a shared helper (e.g.
setupDashboardForTestcase(guest, handleName, testcase)).
Environment-Aware Tests
import { isOpenAmEnv } from "../../src/utils/environment";
test.skip(isOpenAmEnv, "This test is only for Keycloak environments");
Env is loaded via source ./bin/load_env.sh; the ENVIRONMENT variable controls
the target URL (default: test).
Configuration Highlights (playwright.config.ts)
export default defineConfig({
testDir: "./playwright-tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.TEST_HANDLE ? 0 : 1,
workers: process.env.CI ? 1 : 5,
globalTimeout: 30 * 60 * 1000,
timeout: 60000,
expect: { timeout: 10000 },
use: {
ignoreHTTPSErrors: true,
testIdAttribute: "data-test-label",
baseURL: envUrl,
trace: "on-first-retry",
},
projects: [
{ name: "auth-setup", testMatch: "**/setup/auth.setup.ts" },
{
name: "regression-tests",
testMatch: ["**/tests/*.spec.ts", "**/tests/**/*.spec.ts"],
dependencies: process.env.CI ? ["auth-setup"] : [],
use: { trace: "retain-on-failure", headless: true },
},
],
});
QSF Checklist
-
test/expectimported fromauthFixtures, not@playwright/test - Named user fixtures (
guest,testView,testNoRights) used for auth - Browser context closed after each fixture via
context.close() - Handle names, errors, and labels imported from
enums/ -
getByTestIdvalues matchdata-test-labelattributes -
test.skip(isOpenAmEnv, ...)used for environment-specific tests - Env loaded via
source ./bin/load_env.shbefore running - Generic rules from the
test-uiskill also satisfied