Skip to main content

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​

  1. Set E2E_PROJECT_DIR to the project's test/e2e/ directory
  2. Call io-oidis-e2e/bin/e2e.ps1 directly (or via IntelliJ run configuration)
  3. The runner sets NODE_PATH to io-oidis-e2e/node_modules/ so test files can resolve @playwright/test at runtime
  4. playwright.config.ts reads E2E_PROJECT_DIR to locate tests and e2e.conf.jsonp for project settings
  5. 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:

  1. $env:OIDIS_E2E_HOME — explicit path, takes precedence if set
  2. 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:

  1. bin/project/configs/commons.jsonp — defines the structure with empty defaults (committed):
e2eTest: {
email : "",
password : ""
}
  1. private.conf.jsonp — local developer override (gitignored, each developer fills in their test account):
e2eTest: {
email : "test@example.com",
password : "secret123"
}
tip

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.

  1. 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.json
  • test/e2e/tsconfig.local.json — gitignored, generated by e2e-setup, contains the paths alias pointing to the actual io-oidis-e2e/node_modules/@playwright/test location
// 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
note

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​

  1. name, placeholder, href, title attributes — stable, semantic
  2. Text content — button:has-text('Submit'), text=Label
  3. CSS classes — .toast, .spinner-border
  4. Structural selectors — input[type='password'], :nth-child()

What to Avoid​

  • data-oidis-bind values — compiled to runtime IDs, not stable
  • data-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.

OutputPathPurpose
JUnit XMLbuild/reports/e2e-junit.xmlTeamCity test result import
HTML reportbuild/reports/e2e-html/Visual report with trace viewer
Artifactsbuild/reports/e2e-artifacts/Screenshots, videos, traces (failures only)

TeamCity imports the JUnit XML directly — no additional configuration required.

Running Tests​

First-time Setup​

  1. Run setup from wherever io-oidis-e2e is 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
  1. In the project, run oidis install — this triggers the e2e-setup builder module which generates test/e2e/tsconfig.local.json for 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_DIR to an absolute path (required for Docker volume mounts on Windows)
  • Derive E2E_REPORTS_DIR automatically (<project-root>/build/reports/) — override by setting it explicitly before calling the script
  • Create E2E_REPORTS_DIR on 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.

note

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:

SettingValueReason
workers1SPA + shared DB state — no parallel isolation
fullyParallelfalseTests may depend on application state
retries1Catch flaky SPA timing issues (from e2e.conf.jsonp)
screenshotonly-on-failureDebug failed tests
traceon-first-retryFull trace for retry debugging
videoretain-on-failureVideo 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.

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 when e2e.conf.jsonp contains empty values)
  • E2E_REPORTS_DIR — override report output path (default: derived from E2E_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");
Copyright exclusion

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​

AspectStatus
App startup before testsAutomatic (opt-in via autoStart) or manual
Readiness checkPlaywright polls baseUrl until HTTP 2xx
App shutdown after testsAutomatic (child process killed by Playwright)
Playwright webServer configImplemented
Playwright globalSetup hookNot 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​

FieldTypeDefaultDescription
autoStartbooleanfalseEnable automatic app start/stop
startTimeoutnumber120000Max wait time (ms) for app readiness
Environment VariableDescription
E2E_AUTO_STARTSet to "true" to enable auto-start (overrides config)
No ad-hoc scripts

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