E2E Testing with Playwright
Oidis supports end-to-end (E2E) browser testing using Playwright. Unlike unit tests and Selenium tests which are managed by the Oidis Builder, Playwright E2E tests use the io-oidis-e2e runtime package — a self-contained tooling layer that keeps all Playwright infrastructure out of the project's source tree.
Why Playwright (alongside Selenium)
Oidis Builder includes Selenium-based runtime tests (see Testing). Playwright complements this with:
- Modern async API — auto-waits, no explicit sleep calls
- Built-in reporters — HTML, JUnit XML (TeamCity), trace viewer
- Docker-native — official Microsoft images for headless CI
- Developer UX — UI mode, step-through debugger, headed mode
Selenium tests remain the framework-integrated option; Playwright is a solution-level addition for projects that need richer E2E coverage.
Architecture
The E2E setup separates tooling (io-oidis-e2e — standalone CORE package, cloned separately) from test code (project):
io-oidis-e2e/ # E2E tooling package — cloned independently
├── package.json # @playwright/test, typescript
├── playwright.config.ts # Dynamic — reads E2E_PROJECT_DIR + e2e.conf.jsonp
├── tsconfig.json # Base TS config
├── .dockerignore # Excludes node/, node_modules/ from Docker build context
├── bin/
│ ├── setup.ps1 # Windows: portable Node.js installer + npm install
│ ├── setup.sh # Linux/macOS: portable Node.js installer + npm install
│ ├── e2e.ps1 # Windows CLI runner (portable Node.js)
│ ├── e2e.sh # Linux/macOS CLI runner (portable Node.js)
│ ├── e2e-docker.ps1 # Windows Docker runner
│ └── e2e-docker.sh # Linux/macOS / CI Docker runner
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
├── node/ # Portable Node.js (gitignored)
└── node_modules/ # Playwright etc. (gitignored)
<project>/ # Project — test code only
├── test/
│ ├── resource/
│ │ └── data/[Namespace]/ # Test data files — namespace mirrors resource/data/
│ └── e2e/ # Test code only (committed to git)
│ ├── Specs/
│ │ └── Auth/
│ │ └── Login.spec.ts
│ ├── Pages/
│ │ ├── BasePage.ts
│ │ └── LoginPage.ts
│ ├── Helpers/
│ │ ├── Selectors.ts
│ │ └── WaitHelpers.ts
│ ├── Fixtures/
│ │ └── BaseFixture.ts
│ ├── tsconfig.json # Extends tsconfig.local.json
│ ├── tsconfig.local.json # Generated by e2e-setup — gitignored
│ └── e2e.conf.jsonp # Generated by builder from template — gitignored
│
└── resource/
├── scripts/
│ └── e2e-setup.js # Builder module — generates tsconfig.local.json
└── configs/
├── e2e.conf.template.jsonp # E2E config template (baseUrl, timeout, credentials via <? @var ?>)
└── modules.register.json # Registers e2e-setup to run after oidis install
The two repositories can be cloned anywhere — their relative location on disk is not prescribed.
How It Works
- Set
E2E_PROJECT_DIRto the project'stest/e2e/directory - Call
io-oidis-e2e/bin/e2e.ps1directly (or via IntelliJ run configuration) - The runner sets
NODE_PATHtoio-oidis-e2e/node_modules/so test files can resolve@playwright/testat runtime playwright.config.tsreadsE2E_PROJECT_DIRto locate tests ande2e.conf.jsonpfor project settings- All reports output to
build/reports/(consistent with unit and coverage test outputs)
io-oidis-e2e Location
The runner resolves io-oidis-e2e in this order:
$env:OIDIS_E2E_HOME— explicit path, takes precedence if set- A local path convention — configurable per team setup
Naming Conventions
E2E tests follow standard Oidis naming conventions:
- Directories: PascalCase (
Pages/,Helpers/,Fixtures/,Specs/) - Files: PascalCase matching the class or module name (
BasePage.ts,LoginPage.ts,WaitHelpers.ts) - Classes: PascalCase (
BasePage,LoginPage,Selectors) - Public methods: PascalCase with
$prefix on parameters (Goto(),Login($email, $password),NavigateTo($hash)) - Exported functions: PascalCase with
$prefix on parameters (WaitForPanelLoad($page)) - Type annotations: space before and after
:(page : Page,$hash : string) - File headers: Oidis
/*!...*/banner format
Exception: Playwright API parameters (fixture destructuring { page }, use) cannot be renamed — they are part of the framework's API contract.
Project Configuration
test/e2e/e2e.conf.jsonp is generated by the builder from a template — it is not committed to git. It contains runtime configuration (base URL, timeout) and test credentials (email, password) resolved from the standard Oidis config chain.
Template and Generation
The template lives in resource/configs/e2e.conf.template.jsonp and uses <? @var ?> string replacement:
/*! Oidis banner */
Io.Oidis.E2e.DAO.Resources.Data({
"$interface" : "Io.Oidis.E2e.Config",
"baseUrl" : "http://localhost:9306",
"browser" : "chromium",
"timeout" : 30000,
"retries" : 1,
"testEmail" : "<? @var project.e2eTest.email ?>",
"testPassword" : "<? @var project.e2eTest.password ?>"
})
The builder processes this via target.resources in package.conf.jsonp:
target: {
resources: {
e2eConf: {
input : "resource/configs/e2e.conf.template.jsonp",
output: "../../test/e2e/e2e.conf.jsonp" // relative to build/target/ → traverses to project root
}
}
}
skipReplace is not set (default false) — the builder applies <? @var ?> substitution using the merged project configuration (commons.jsonp + private.conf.jsonp override).
Test Credentials Configuration
Test account credentials follow the standard Oidis config chain:
bin/project/configs/commons.jsonp— defines the structure with empty defaults (committed):
e2eTest: {
email : "",
password : ""
}
private.conf.jsonp— local developer override (gitignored, each developer fills in their test account):
e2eTest: {
email : "test@example.com",
password : "secret123"
}
Projects may add additional fields (e.g., group name, tenant ID) to the e2eTest config section as needed. Only email and password are required by the io-oidis-e2e runtime.
bin/project/configs/package.conf.schema.jsonp— new config sections must be declared here (the builder validates the config against this schema):
e2eTest: {
type: "object",
properties: {
email : { type: "string" },
password : { type: "string" }
}
}
After oidis compile:dev the builder generates test/e2e/e2e.conf.jsonp with credentials substituted. The io-oidis-e2e playwright.config.ts reads this file and injects credentials into test processes as process.env.E2E_TEST_EMAIL / E2E_TEST_PASSWORD, with fallback to CI environment variables for pipeline runs.
Runtime URL Override
Override baseUrl at runtime without editing the template:
$env:BASE_URL = "https://your-dev-instance.example.com"
$env:E2E_PROJECT_DIR = "path\to\project\test\e2e"
& <io-oidis-e2e>\bin\e2e.ps1
IDE Type Resolution
@playwright/test lives in io-oidis-e2e/node_modules/ — outside the project tree. The IDE's TypeScript language service needs to be told where to find it.
tsconfig.local.json
Each project uses a two-file TypeScript config:
test/e2e/tsconfig.json— committed, contains all compiler options, extends./tsconfig.local.jsontest/e2e/tsconfig.local.json— gitignored, generated bye2e-setup, contains thepathsalias pointing to the actualio-oidis-e2e/node_modules/@playwright/testlocation
// tsconfig.local.json — generated, not committed
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@playwright/test": ["<relative-path-to-io-oidis-e2e>/node_modules/@playwright/test"]
}
}
}
// tsconfig.json — committed
{
"extends": "./tsconfig.local.json",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": false,
"outDir": "./dist",
"rootDir": "."
},
"include": ["**/*.ts"],
"exclude": ["dist"]
}
e2e-setup Builder Module
resource/scripts/e2e-setup.js is registered as a builder module that runs automatically after oidis install:
// resource/configs/modules.register.json
{
"e2e-setup": {
"script": "resource/scripts/e2e-setup.js",
"events": {
"after": "install"
}
}
}
The script resolves the io-oidis-e2e location (via OIDIS_E2E_HOME or convention), computes the correct relative path from test/e2e/ to node_modules/@playwright/test, and writes tsconfig.local.json. It is a no-op if the file already contains the correct path.
Can also be run directly:
oidis task e2e-setup
tsconfig.local.json does not exist on fresh clone — it is generated on first oidis install. Until then, IntelliJ will report a TypeScript error on the extends. This is expected; run oidis install to resolve it.
Selector Strategy
Oidis SPA compiles data-oidis-bind values to runtime-generated IDs — these are not stable and must never be used in E2E selectors.
Priority Order
name,placeholder,href,titleattributes — stable, semantic- Text content —
button:has-text('Submit'),text=Label - CSS classes —
.toast,.spinner-border - Structural selectors —
input[type='password'],:nth-child()
What to Avoid
data-oidis-bindvalues — compiled to runtime IDs, not stabledata-testid— not present in Oidis framework output- Generated DOM IDs — framework-assigned, change between builds
Oidis Component Selectors
Oidis UserControl components render specific HTML that differs from plain Bootstrap markup. Always use the actual rendered output, not the template source.
Table UserControl — two <table> elements
The Oidis Table component renders two <table> elements: the data table (with class table-sm) and a loading placeholder (without it). Using a plain table selector causes Playwright strict mode violations ("found N elements"). Always target the data table explicitly:
// Wrong — matches both tables, strict mode violation
table: "table"
// Correct — targets data table only
table: "table.table-sm"
DropDownListItem renders as <button>, not <a>
DropDownListItem from io-oidis-usercontrols generates <button class="dropdown-item">. Panels that use inline HTML for a loading placeholder (e.g. <a class="dropdown-item">načítám...</a>) replace it with DropDownListItem instances on FetchData — the result is <button> elements.
Using a.dropdown-item in selectors finds nothing after data loads. Use the class-only selector to match both:
// Wrong — matches only the static loading placeholder <a>, not real items
".dropdown-menu a.dropdown-item"
// Correct — matches DropDownListItem <button> elements
".dropdown-menu .dropdown-item"
Bootstrap 5 tooltip removes title attribute
Bootstrap 5 moves title="..." to data-bs-original-title when it initialises a tooltip. Selectors like span[title='Edit'] stop working as soon as Bootstrap runs. Use structural or icon-based selectors instead:
// Wrong — Bootstrap removes title attribute at runtime
rowEditButton: "span[title='Edit']"
// Correct — targets the SVG icon class (stable)
rowEditButton: "span:has(svg.bi-pencil)"
rowDeleteButton: "span:has(svg.bi-trash)"
Centralized Selectors
All selectors live in Helpers/Selectors.ts as a class with static members:
export class Selectors {
public static readonly login = {
emailInput: "input[name='email']",
passwordInput: "input[type='password'][name='current-password']",
submitButton: "button.btn-primary:has-text('Přihlásit se')",
signUpLink: "a[href='/#/signUp']"
};
public static readonly header = {
groupsLink: "a[href='/#/groups']",
eansLink: "a[href='/#/eans']"
};
public static readonly toast = {
container: ".toast",
body: ".toast-body"
};
}
SPA Wait Helpers
Oidis uses hash-based routing (/#/path) with lazy-loaded panels. Standard Playwright navigation waits don't work because the URL change is a hash change (no full navigation). Custom helpers handle this:
// Wait for hash route change + panel render
export async function WaitForHashRoute($page : Page, $hash : string) : Promise<void> {
await $page.waitForURL(`**/#/${$hash}`, { timeout: 10_000 });
await WaitForPanelLoad($page);
}
// Wait for loading indicators to disappear
export async function WaitForPanelLoad($page : Page) : Promise<void> {
const spinner = $page.locator(".spinner-border, .loading-overlay");
if (await spinner.count() > 0) {
await spinner.first().waitFor({ state: "hidden", timeout: 10_000 });
}
await $page.waitForLoadState("networkidle");
}
// Wait for toast notification
export async function WaitForToast($page : Page, $expectedText? : string) : Promise<void> {
const toast = $page.locator(Selectors.toast.container);
await expect(toast).toBeVisible({ timeout: 5_000 });
if ($expectedText) {
await expect($page.locator(Selectors.toast.body)).toContainText($expectedText);
}
}
FetchData and Async Data Loading
WaitForPanelLoad waits for the panel UI to render (spinners, networkidle), but not for FetchData() to complete. FetchData is an application-level async call — the panel is fully rendered before it finishes.
Tests that interact with data populated by FetchData (dropdown items, table rows loaded from the backend) must account for this:
// FetchData populates the dropdown asynchronously after the panel renders.
// WaitForPanelLoad does not wait for it — extend the test timeout and wait
// for the actual items to appear before interacting with the dropdown.
test("should select group", async ({ page }) => {
test.setTimeout(30_000); // FetchData may take several seconds
await page.click(Selectors.allocations.groupDropdown);
const items = page.locator(".dropdown-menu .dropdown-item:not(:has-text('načítám'))");
await items.first().waitFor({ state: "visible", timeout: 15_000 });
await items.first().click();
});
The test.setTimeout() call must be inside the test body — it cannot be set at describe level for individual tests. Only extend it for tests that call FetchData-dependent interactions; leave the global default for fast UI-only tests.
Page Object Pattern
Each page/section of the application has a corresponding Page Object class:
export abstract class BasePage {
constructor(protected readonly page : Page) {}
async NavigateTo($hash : string) : Promise<void> {
await this.page.goto(`/#/${$hash}`);
await WaitForPanelLoad(this.page);
}
async WaitForSection($hash : string) : Promise<void> {
await WaitForHashRoute(this.page, $hash);
}
}
export class LoginPage extends BasePage {
constructor($page : Page) {
super($page);
}
async Goto() : Promise<void> {
await this.NavigateTo("login");
}
async Login($email : string, $password : string) : Promise<void> {
await this.page.fill(Selectors.login.emailInput, $email);
await this.page.fill(Selectors.login.passwordInput, $password);
await this.page.click(Selectors.login.submitButton);
}
}
Page objects are injected via Playwright fixtures (Fixtures/BaseFixture.ts):
import { test as base } from "@playwright/test";
import { LoginPage } from "../Pages/LoginPage";
export const test = base.extend<{ loginPage: LoginPage }>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
}
});
Build Reports
All test outputs go to build/reports/ — consistent with unit test and coverage report locations. The /build/ directory is already covered by the project's auto-generated .gitignore.
| Output | Path | Purpose |
|---|---|---|
| JUnit XML | build/reports/e2e-junit.xml | TeamCity test result import |
| HTML report | build/reports/e2e-html/ | Visual report with trace viewer |
| Artifacts | build/reports/e2e-artifacts/ | Screenshots, videos, traces (failures only) |
TeamCity imports the JUnit XML directly — no additional configuration required.
Running Tests
First-time Setup
- Run setup from wherever
io-oidis-e2eis cloned on your machine:
Windows (PowerShell):
# Install portable Node.js + npm packages
powershell -ExecutionPolicy Bypass -File <io-oidis-e2e>\bin\setup.ps1
# Install Playwright browsers (once)
<io-oidis-e2e>\node\npm.cmd run install:browsers
Linux / macOS (bash):
# Install portable Node.js + npm packages
bash <io-oidis-e2e>/bin/setup.sh
# Install Playwright browsers (once)
<io-oidis-e2e>/node/bin/npm run install:browsers
- In the project, run
oidis install— this triggers thee2e-setupbuilder module which generatestest/e2e/tsconfig.local.jsonfor IDE type resolution:
oidis install
The node/ and node_modules/ directories are gitignored in io-oidis-e2e and must be set up on each machine.
Running Tests
Set E2E_PROJECT_DIR to the project's test/e2e/ directory and call the runner directly:
Windows (PowerShell):
$env:E2E_PROJECT_DIR = "path\to\project\test\e2e"
& <io-oidis-e2e>\bin\e2e.ps1 # headless
& <io-oidis-e2e>\bin\e2e.ps1 --headed # headed (visual)
& <io-oidis-e2e>\bin\e2e.ps1 --ui # Playwright UI mode
& <io-oidis-e2e>\bin\e2e.ps1 --debug # step-through debugger
Linux / macOS (bash):
export E2E_PROJECT_DIR="path/to/project/test/e2e"
<io-oidis-e2e>/bin/e2e.sh # headless
<io-oidis-e2e>/bin/e2e.sh --headed # headed (visual)
<io-oidis-e2e>/bin/e2e.sh --ui # Playwright UI mode
<io-oidis-e2e>/bin/e2e.sh --debug # step-through debugger
The application must be running locally (default http://localhost:9306). Override with BASE_URL:
$env:BASE_URL = "https://your-dev-instance.example.com"
$env:E2E_PROJECT_DIR = "path\to\project\test\e2e"
& <io-oidis-e2e>\bin\e2e.ps1
export BASE_URL="https://your-dev-instance.example.com"
export E2E_PROJECT_DIR="path/to/project/test/e2e"
<io-oidis-e2e>/bin/e2e.sh
IntelliJ IDEA Run Configurations
Projects can provide IntelliJ run configurations in .idea/runConfigurations/ (tracked in git via .gitignore exception). The configurations set E2E_PROJECT_DIR inline and call the relevant runner script using the PowerShell -Command interpreter option.
Typical configurations: E2E Tests (headless), E2E Tests Headed, E2E Tests UI Mode, E2E Tests (Docker).
Docker (CI / Headless)
Use the dedicated Docker runner scripts — they resolve absolute paths and set E2E_REPORTS_DIR automatically:
Windows (PowerShell):
$env:E2E_PROJECT_DIR = "path\to\project\test\e2e"
& <io-oidis-e2e>\bin\e2e-docker.ps1 # headless
& <io-oidis-e2e>\bin\e2e-docker.ps1 --no-cache # force Docker image rebuild
Linux / macOS / CI (bash):
export E2E_PROJECT_DIR="path/to/project/test/e2e"
<io-oidis-e2e>/bin/e2e-docker.sh # headless
<io-oidis-e2e>/bin/e2e-docker.sh --no-cache # force Docker image rebuild
The runner scripts:
- Resolve
E2E_PROJECT_DIRto an absolute path (required for Docker volume mounts on Windows) - Derive
E2E_REPORTS_DIRautomatically (<project-root>/build/reports/) — override by setting it explicitly before calling the script - Create
E2E_REPORTS_DIRon the host if it does not exist - Call
docker compose up --build --exit-code-from e2e-tests --abort-on-container-exit - Clean up the stopped container after the run (image stays cached)
The Docker setup uses the official mcr.microsoft.com/playwright image. Test code is mounted read-only; reports are written to build/reports/ on the host. No local Node.js installation required — all dependencies are baked into the image.
E2E_TEST_EMAIL, E2E_TEST_PASSWORD, and E2E_TEST_GROUP_NAME can be passed as environment variables to supply CI credentials without an e2e.conf.jsonp file. playwright.config.ts falls back to these when the config file is absent or contains empty values.
Configuration
Key settings in io-oidis-e2e/playwright.config.ts:
| Setting | Value | Reason |
|---|---|---|
workers | 1 | SPA + shared DB state — no parallel isolation |
fullyParallel | false | Tests may depend on application state |
retries | 1 | Catch flaky SPA timing issues (from e2e.conf.jsonp) |
screenshot | only-on-failure | Debug failed tests |
trace | on-first-retry | Full trace for retry debugging |
video | retain-on-failure | Video evidence for failures |
CI Pipeline Integration
E2E tests are intentionally isolated from the Oidis Builder (oidis test runs unit + Selenium only). E2E tests require a running application instance, making them unsuitable for pre-commit or per-commit execution.
Recommended Integration Levels
Level 1 — Local development
Run directly by setting E2E_PROJECT_DIR and calling io-oidis-e2e/bin/e2e.ps1, or via IntelliJ run configurations.
Level 2 — CI pipeline step (opt-in)
Add to build.sh as an optional post-deploy step:
if [ "$RUN_E2E" = "1" ] && [ "$DEPLOY_DISABLED" != "1" ]; then
export E2E_PROJECT_DIR="$(pwd)/test/e2e"
export BASE_URL="https://your-dev-instance.example.com"
bash <io-oidis-e2e>/bin/e2e-docker.sh
fi
Environment variables:
BASE_URL— URL of the deployed DEV instance (default:http://host.docker.internal:9306)E2E_TEST_EMAIL,E2E_TEST_PASSWORD,E2E_TEST_GROUP_NAME— CI secret variables (fallback whene2e.conf.jsonpcontains empty values)E2E_REPORTS_DIR— override report output path (default: derived fromE2E_PROJECT_DIR)RUN_E2E=1— opt-in flag (not run on every commit)
Level 3 — GitLab CI stage (long-term)
e2e-tests:
stage: e2e
image: mcr.microsoft.com/playwright:v1.50.0-noble
variables:
BASE_URL: https://your-dev-instance.example.com
script:
- cd <io-oidis-e2e> && npm ci && E2E_PROJECT_DIR=<project>/test/e2e npm test
when: manual
artifacts:
reports:
junit: build/reports/e2e-junit.xml
paths:
- build/reports/e2e-html/
- build/reports/e2e-artifacts/
when: always
What NOT to Do
- Do not add E2E to
oidis test— E2E tests require a live running application, which the builder test pipeline does not provide - Do not run E2E on every commit — it is too slow and requires a deployed application
- Do not add E2E to pre-commit hooks — impractical, takes minutes
Credentials for Login Tests
Test credentials are configured through the standard Oidis config chain — see Test Credentials Configuration above. Set e2eTest.email and e2eTest.password in your local private.conf.jsonp, then run oidis compile:dev to regenerate test/e2e/e2e.conf.jsonp.
In CI pipelines, pass credentials as environment variables — playwright.config.ts automatically falls back to process.env.E2E_TEST_EMAIL / E2E_TEST_PASSWORD when e2e.conf.jsonp contains empty values.
CRUD Test Data
CRUD tests that create, edit, and delete records share a real database with other test runs. Stale data from previous failed runs causes strict mode violations (has-text substring matching finds multiple rows).
Timestamp-Based Unique Names
Use Date.now() in test data names so each run creates unique records:
const TS = Date.now();
const TEST_GROUP_NAME = `[E2E] ORIG-${TS}`;
const TEST_GROUP_NAME_EDITED = `[E2E] EDIT-${TS}`;
The [E2E] prefix makes test records easy to identify and clean up manually if needed.
Non-Overlapping Name Prefixes
Playwright's :has-text() and tr:has-text() do substring matching. If the original name is a substring of the edited name, both assertions match the same row:
// Problematic — "Test Group" is a substring of "Test Group Edited"
const TEST_NAME = "[E2E] Test Group";
const TEST_NAME_EDITED = "[E2E] Test Group Edited"; // matches both rows!
// Correct — use distinct prefixes that are not substrings of each other
const TEST_NAME = "[E2E] ORIG-${TS}";
const TEST_NAME_EDITED = "[E2E] EDIT-${TS}"; // ORIG ≠ substring of EDIT
Static Test Data Files
Static test data files (CSV, JSON, etc.) belong in test/resource/data/[Namespace]/ at the project level — not inside test/e2e/. The namespace path follows the same convention as production resources (resource/data/[Namespace]/). This is required because the builder copies and merges resources from all dependencies into build/target/ — the namespace prevents file collisions and enables intentional overrides (dependencies are copied first, then the project itself).
test/resource/data/Io/Oidis/Irma/edcReport.csv ← CSV fixture for upload tests
The E2E engine automatically exposes test/resource/data/ as process.env.E2E_DATA_DIR. Reference files from Page Objects using the namespace path:
import { resolve } from "path";
const filePath : string = resolve(process.env["E2E_DATA_DIR"] ?? "", "Io/Oidis/Irma/edcReport.csv");
Binary or data files that cannot contain a copyright header comment (CSV, images) must be added to bin/project/configs/oidislint.conf.json exclude list — e.g., "*.csv". The standard exclude entry test/resource/data/* is already present in the builder's default config.
Sequential CRUD Tests
CRUD tests are designed to run sequentially and depend on each other (create → edit → delete). Playwright's serial execution (workers: 1) ensures this. Each test must leave the database in the state expected by the next test, and the final test must clean up all created records.
App Lifecycle (Start / Readiness / Stop)
E2E tests require a running application instance. The io-oidis-e2e runtime supports automatic app lifecycle management via Playwright's webServer configuration — opt-in, with graceful fallback to manual start.
Status
| Aspect | Status |
|---|---|
| App startup before tests | Automatic (opt-in via autoStart) or manual |
| Readiness check | Playwright polls baseUrl until HTTP 2xx |
| App shutdown after tests | Automatic (child process killed by Playwright) |
Playwright webServer config | Implemented |
Playwright globalSetup hook | Not needed |
Enabling Auto-Start
Set autoStart: true in e2e.conf.jsonp:
Io.Oidis.E2e.DAO.Resources.Data({
baseUrl : "http://localhost:9306",
// ...existing fields...
autoStart : true,
startTimeout : 120000
});
Or via environment variable (useful for CI): E2E_AUTO_START=true.
How It Works
When autoStart is enabled, playwright.config.ts auto-detects the start command from the standard Oidis project structure:
{projectBase}/dependencies/nodejs/node[.exe] {projectBase}/build/target/resource/javascript/loader.min.js start
The application is spawned as a child process (non-detached). Playwright polls baseUrl until it responds with HTTP 2xx, then runs the tests. When tests finish, the child process is killed automatically.
reuseExistingServer: true is always set — if the app is already running (started manually via IntelliJ, oidis dev, or otherwise), Playwright skips spawning and runs tests against the existing instance. This preserves the local development workflow.
Graceful Fallback
If autoStart is enabled but required paths are missing (node.exe or loader.min.js), the runtime prints a warning and continues without webServer. Tests then behave as before — expecting a manually started app.
Configuration Reference
| Field | Type | Default | Description |
|---|---|---|---|
autoStart | boolean | false | Enable automatic app start/stop |
startTimeout | number | 120000 | Max wait time (ms) for app readiness |
| Environment Variable | Description |
|---|---|
E2E_AUTO_START | Set to "true" to enable auto-start (overrides config) |
Projects must not create ad-hoc readiness scripts (e.g., wait-app-ready.ps1 polling localhost). App lifecycle management belongs in io-oidis-e2e — shared infrastructure that goes through code review.
.gitignore
Add to the "Project individual ignore patterns" section:
test/e2e/e2e.conf.jsonp
test/e2e/tsconfig.local.json
test/e2e/test-results/
test/e2e/playwright-report/
test/e2e/e2e.conf.jsonp is gitignored because it is generated by the builder and may contain test credentials from private.conf.jsonp. The template (resource/configs/e2e.conf.template.jsonp) is committed in its place.
Note: build/reports/e2e-* does not need to be listed — /build/ in the auto-generated section already covers it. The io-oidis-e2e package itself handles its own gitignore for node/, node_modules/, and dist/.
See also: Testing | Docker & CI | Module System