Skip to main content

SAMO E2E Tests β€” Implementation Guide

A complete, step-by-step guide for partners and implementers who want to build an end-to-end (E2E) test suite for their own SAMO-based environment.

This guide assumes no prior experience with E2E testing. It walks you from installing Node.js to writing, running, and maintaining a full test suite for your deployment.


Table of Contents​

  1. What is this and who is it for
  2. Architecture overview
  3. Prerequisites
  4. First run β€” getting the reference suite working
  5. How tests are structured
  6. The test lifecycle
  7. Writing a feature file (Gherkin / Cucumber)
  8. Writing step definitions (TypeScript / Playwright)
  9. Factory bots deep dive
  10. Customizing for your own environment
  11. Running tests β€” all the commands
  12. Reading test reports
  13. Debugging failing tests
  14. Git workflow and contribution
  15. Housekeeping
  16. Troubleshooting common errors
  17. Further reading
  18. Appendix A β€” Environment variables reference
  19. Appendix B β€” Commands cheat sheet

1. What is this and who is it for​

If you are implementing SAMO for a customer, or you are a customer maintaining your own SAMO instance, and you need automated browser-level tests that verify the whole system works end-to-end, this guide is for you.

You will learn how to:

  • Run an existing, reference E2E test suite (this repository, e2e-samo) to see how everything fits together.
  • Understand the architecture: three cooperating projects that you can compose for your needs.
  • Write your own tests using Gherkin (readable English-like syntax) and Playwright (real browser automation).
  • Create factory-bot templates that generate test data (entities) matching your own feature types.
  • Customize the whole stack for your customer's SAMO deployment.

The five projects this guide covers are:

  • samo-factory-bot β€” the upstream test-data library. You depend on it as a package; you don't normally modify it.
  • samo-template-factory-bot β€” the starter scaffold for factory-bots. Partners fork this and register their own templates.
  • samo-demo-factory-bot β€” the reference implementation for the SAMO Demo environment. Do not fork β€” use as a pattern library to copy structural ideas from.
  • e2e-samo-template β€” the starter scaffold for E2E test suites. Partners fork this and add their own Gherkin scenarios.
  • e2e-samo β€” the reference E2E suite (this repo). Do not fork β€” use as a pattern library for step definitions and feature-file structure.

2. Architecture overview​

The SAMO Demo stack β€” three cooperating projects, top to bottom:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ samo-factory-bot β”‚
β”‚ (upstream library) β”‚
β”‚ β”‚
β”‚ β€’ LASFactory β€” create/delete features β”‚
β”‚ β€’ SecurityManagerFactory β€” create users and groups β”‚
β”‚ β€’ UserServiceFactory β€” create profiles and messages β”‚
β”‚ β€’ FeatureInstance β€” represents a created feature β”‚
β”‚ β€’ RollbackManager β€” automatic cleanup via checkpointsβ”‚
β”‚ β€’ Codelist APIs, metadata APIs, geometry helpers β”‚
β”‚ β”‚
β”‚ Published as npm package @samo-tools/samo-factory-bot β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β–³
β”‚ extends
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ samo-demo-factory-bot β”‚
β”‚ (SAMO Demo data layer β€” ~50 templates) β”‚
β”‚ β”‚
β”‚ β€’ LASDemoFactory extends LASFactory β”‚
β”‚ β€’ registerTemplates() for employee, building, β”‚
β”‚ defect_lightning, cable_low_voltage, … β”‚
β”‚ β€’ Types, constants, codelist fixtures for SAMO Demo β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β–³
β”‚ imports
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ e2e-samo β”‚
β”‚ (reference E2E suite for SAMO Demo) β”‚
β”‚ β”‚
β”‚ β€’ Cucumber feature files (Gherkin scenarios) β”‚
β”‚ β€’ TypeScript step definitions driving Playwright β”‚
β”‚ β€’ Per-scenario factory checkpoint and rollback β”‚
β”‚ β€’ HTML reports with screenshots, videos, traces β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This is what is deployed against SAMO Demo today. The middle and bottom boxes are SAMO Demo-specific β€” they hard-code Demo feature types (ft_employee, ft_5050000, …), Demo codelists, and Demo address bindings.

⚠ For partner adoption, do NOT fork samo-demo-factory-bot or e2e-samo. Instead, fork the minimal starter templates:

The starters ship one example template plus two demo scenarios with a phased adoption checklist. Treat samo-demo-factory-bot and this repo as pattern libraries β€” open them in your editor to copy structural ideas (linked records, geometry helpers, the _with_<suffix> variant convention), but do not fork them. See Section 10 for the full adoption flow.


3. Prerequisites​

3.1 Software​

ToolMinimum versionWhere to get it
Node.js18 LTS or newer (Node 20 or 22 LTS recommended)nodejs.org
npmships with Node.jsβ€”
Gitany recentgit-scm.com
Code editorany; VS Code is what most of the team usescode.visualstudio.com

πŸ’‘ On Windows, the commands in this guide are tested in Git Bash (installed with Git for Windows) or WSL. PowerShell works too, but some shell commands (mkdir -p, backtick strings) behave differently.

3.2 A running SAMO environment​

You need a SAMO instance that the tests can connect to. That means:

  • A reachable HTTPS URL (for example https://your-sandbox.samo-kube.internal/).
  • Valid credentials (a dev user and a test user).
  • The LIDS, UserService, and Codelist endpoints exposed by the SAMO backend.

If you do not yet have an environment, follow your organization's SAMO deployment guide β€” typically a Kubernetes namespace with your own name (e.g. alice-test). When idle, please remove the environment to save resources.

3.3 Access to the three repositories​

You will need git clone access to:

  • samo-factory-bot (if you plan to read or contribute to core changes; most partners just consume it as an npm package).
  • samo-demo-factory-bot (you fork this to create your own factory bot).
  • e2e-samo (you fork this to create your own test suite).

The npm packages are hosted on a samo-npm registry:

http://nexus/repository/samo-npm/

Ask your SAMO contact for credentials if you cannot reach the registry.


4. First run β€” getting the reference suite working​

Before customizing anything, get the reference suite running. This validates your setup end-to-end.

4.1 Clone and install​

git clone <e2e-samo-repository-url> e2e-samo
cd e2e-samo
npm install

npm install downloads all dependencies, including @samo-tools/samo-factory-bot and @samo-tools/samo-demo-factory-bot from the samo-npm registry. If you get 401/403 errors, your .npmrc is missing credentials for that registry β€” fix that and re-run.

4.2 Install the browser​

Playwright ships with its own browser binaries (not the Chrome you have installed). Download them once:

npx playwright install

This downloads Chromium, Firefox, and WebKit binaries into a cache directory. For E2E-Samo tests we only use Chromium.

4.3 Configure .env​

Copy the example and fill in real values:

cp .env.example .env

Open .env in your editor and set each variable. The values come from your SAMO environment. The full reference is in Appendix A, but the minimum is:

SAMO_DEV_USERNAME="alice"
SAMO_DEV_PASSWORD="your-password"
SAMO_TEST_USERNAME="alice-test"
SAMO_TEST_PASSWORD="your-test-password"

LIDS_URL_API="https://alice-test.samo-kube.internal/lids"
USER_SERVICE_URL_API="https://alice-test.samo-kube.internal/userService"
PATH_TO_APP="/samo/a/demo/c/demo-dynamic-client"
GTW_BASE_URL="https://alice-test.samo-kube.internal"
PLAYWRIGHT_ALLOWED_PORTS="8080,10080"
APPLICATION="demo"

⚠️ Never commit .env. It contains credentials. The repository's .gitignore already excludes it.

4.4 Run a test​

The simplest smoke test: run all scenarios that touch the login flow.

npm run testLocal

A Chromium window opens, navigates through SAMO, and closes. At the end you see a pass/fail summary and the reports are generated under reports/.

Open reports/index.html in your browser β€” this is the HTML report with step-level detail, screenshots (on failure), and links to videos and traces.

If the smoke test passes, your setup is correct. If it fails, jump to section 16 β€” Troubleshooting.


5. How tests are structured​

e2e-samo/
β”œβ”€β”€ features/
β”‚ β”œβ”€β”€ scenarios/ # Gherkin .feature files β€” what is tested
β”‚ β”‚ β”œβ”€β”€ samo-browse.feature
β”‚ β”‚ β”œβ”€β”€ samo-entity-documents.feature
β”‚ β”‚ └── ...
β”‚ β”œβ”€β”€ step_definitions/ # TypeScript β€” how each Given/When/Then is implemented
β”‚ β”‚ β”œβ”€β”€ user.ts
β”‚ β”‚ β”œβ”€β”€ dataPrepare.ts
β”‚ β”‚ β”œβ”€β”€ navigation.ts
β”‚ β”‚ β”œβ”€β”€ infoDetail.ts
β”‚ β”‚ └── ...
β”‚ └── support/ # World, hooks, helpers
β”‚ β”œβ”€β”€ world.ts # ICustomWorld β€” per-scenario state
β”‚ β”œβ”€β”€ hooks.ts # BeforeAll / Before / After / AfterAll
β”‚ β”œβ”€β”€ factory.ts # Factory bot initialization
β”‚ └── attachment.ts # Screenshot / video / trace attaching
β”œβ”€β”€ fixtures/ # Binary test data (images, documents)
β”œβ”€β”€ scripts/ # Runner scripts and report generators
β”œβ”€β”€ reports/ # Generated HTML / JSON reports (gitignored)
β”œβ”€β”€ cucumber.json # Cucumber configuration
β”œβ”€β”€ package.json # npm scripts and dependencies
└── .env # Your local configuration (gitignored)

Rule of thumb: one .feature file β†’ one matching .ts step-definitions file, where possible. The mapping is convention, not enforced. Shared step definitions (e.g. login, navigation) live in topic-named files that multiple features reuse.

5.1 What's in features/support/ β€” the plumbing layer​

The support/ folder is the glue that wires Cucumber, Playwright, and the factory-bots together. You will touch these files when you customize the framework for your environment. Here's each one, what it does, and when you'd change it.

FileRole
world.tsPer-scenario state container (CustomWorld)
hooks.tsCucumber lifecycle hooks (BeforeAll / Before / After / AfterAll)
factory.tsFactory-bot initialization wrappers
support-mixin.tsSmall helpers that read .env values (auth, base URL)
attachment.tsPaths + HTML link generators for report attachments
customParameterTypeDefinition.tsCucumber {parameter} type definitions
types.tsShared TypeScript types
imageDiff.tsVisual-regression helpers (pixel-diff)

Each file is described in detail below, including when partners typically customize it.

world.ts β€” the per-scenario context​

Defines CustomWorld, the object you access as this inside every step. It carries:

  • page β€” the current Playwright Page (browser tab).
  • context, browser β€” the Playwright context and browser instance.
  • factory, userServiceFactory β€” the factory-bot instances initialized in the Before hook.
  • scenarioUniqueId β€” a short unique string ({workerId}-{timestamp}-{random}) generated in the constructor. This is what replaces {SCENARIO_UNIQUE_ID} in step parameters. It is the cornerstone of parallel-safe isolation.
  • entity, previouslyCreatedEntity, relation β€” slots where "prepare entity" / "prepare relation" steps cache the last created thing so subsequent steps can refer to it.
  • customOptions β€” a free-form bag for any scratch data you want to pass between steps in the same scenario.
  • downloads, lastDownload β€” tracked by the page.on('download', …) listener in Before, so the "Then file X is downloaded" step can verify completion.

world.ts also:

  • Sets the Cucumber default timeout to 40 seconds (setDefaultTimeout(40000)). Do not remove this β€” tests timeout at 5 s by default and it crashes the pipeline in surprising ways.
  • Configures Chromium launch flags (--no-sandbox, --disable-dev-shm-usage, font rendering flags, popup blocking). Reading through _getBrowser() is instructive β€” most flags exist for Docker/CI compatibility and for consistent screenshot rendering across machines.
  • Opens browser context with ignoreHTTPSErrors: true (required for self-signed sandboxes), recordVideo, and tracing.start().

When partners customize this: usually only the viewport resolution, or to add a new slot to ICustomWorld (e.g. myData: SomeType).

hooks.ts β€” the lifecycle​

Four hooks, in chronological order:

  • BeforeAll β€” runs once per worker (not per scenario). Loads CustomWorld.allMetadata via getMetadata(lidsUrl, auth). This is an expensive API call returning all feature types and their attributes β€” loading it once and reusing it saves several seconds per scenario.
  • Before β€” runs before each scenario. Initializes this.factory (reusing the cached metadata) and this.userServiceFactory, calls factory.checkpoint() so the later rollbackSequentially(1) only deletes things this scenario created, opens a browser page, and wires up a page.on('download', …) listener.
  • After β€” runs after each scenario. On failure (or if FORCE_VIDEOS_SAVE=true): attaches a full-page screenshot, the video, and the Playwright trace to the Cucumber report. Always: calls factory.rollbackSequentially(1) and userServiceFactory.rollbackSequentially(1) to delete created entities, then closes the browser.
  • AfterAll β€” runs once per worker at the end. Only logging β€” the rollbacks already happened in the per-scenario After.

When partners customize this: adding a new tagged hook (Before({tags: '@mySetup'}, …)) for scenarios that need extra setup; adjusting what goes into the report on failure; or wiring a second factory-bot.

⚠️ Do not call rollbackAll() in AfterAll when running in parallel. Workers share a cleanup pool and you'll get race conditions. Per-scenario rollbackSequentially(1) is the right granularity.

factory.ts β€” where the factory-bot is plugged in​

A thin wrapper around LASDemoFactory.init() and UserServiceDemoFactory.init(). Exposes:

  • initFactory(apiUrl, auth, metadata?) β€” called from Before hook.
  • initUserServiceFactory(apiUrl, auth) β€” called from Before hook.
  • getFactory() / getUserServiceFactory() β€” accessors (rarely used; most code uses this.factory on CustomWorld).

When partners customize this: this is the #1 file you change when you fork e2e-samo. Replace the import from @samo-tools/samo-demo-factory-bot with @yourcustomer/samo-factory-bot, and rename the types accordingly. Mirror the same rename in world.ts and hooks.ts.

support-mixin.ts β€” environment helpers​

Small pure functions that read .env and return meaningful values:

  • getSamoAuth() β€” builds a Basic <base64> auth header from SAMO_DEV_USERNAME + SAMO_DEV_PASSWORD.
  • getOwnSamoAuth(user: UserType) β€” same but for an arbitrary user (used in multi-user scenarios).
  • getBaseUrl() β€” concatenates GTW_BASE_URL + PATH_TO_APP, the root URL Playwright navigates to.
  • getLidsUrl(), getUserServiceUrl() β€” plain getters for the backend service URLs.

When partners customize this: adding helpers for new env variables you introduce (e.g. getMapServiceUrl()), or changing the auth scheme if your SAMO deployment uses something other than Basic auth.

attachment.ts β€” report attachments​

Exports a paths object with absolute paths to reports/videos/, reports/traces/, reports/downloads/, and HTML link generators used by world.ts#attachTrace(), attachVideo(), and attachDownloads(). The generated HTML is embedded into the Cucumber HTML report so you can click through to artifacts.

Also worth knowing: createTraceLink() inserts two links β€” one to download the trace zip, and one to open the public Playwright trace viewer (you then drag-drop the downloaded zip onto it).

When partners customize this: rarely β€” only if you want to change report styling or add a new artifact type (e.g. HAR files).

customParameterTypeDefinition.ts β€” typed Cucumber parameters​

Cucumber expressions support custom parameter types. This file defines {user}: a word matching samo or test, transformed into a UserType object carrying the corresponding credentials from .env.

That's why the step Given the user samo is logged into SAMO works without hardcoding credentials β€” the {user} parameter resolves to { userInfo: { name: 'alice', password: '...' } }.

When partners customize this: adding more typed parameters β€” e.g. {priority} that maps low|medium|high to numeric ids, or {role} that maps names to group ids. Centralizing these here keeps your step definitions clean.

types.ts β€” shared type declarations​

Currently contains only UserType. This is the file to grow when you add custom parameter types (their transformer return types live here), or any TypeScript type referenced by multiple support files or step definitions.

imageDiff.ts β€” visual regression helpers​

Two helpers for tests that compare a rendered element (typically a canvas) to a reference image on disk:

  • saveComponentScreenshot(page, selector, name) β€” saves the current render to features/screenshots/{name}.png. You run this once when you have a known-good state to capture a baseline.
  • isComponentScreenshotSimilar(page, selector, name, maxDiffPixels = 4000) β€” captures the current render into reports/screenshots/{name}_now.png, compares it against the baseline pixel-by-pixel (using pixelmatch with threshold 0.1), and writes a diff-…png into reports/screenshots/ when the diff exceeds maxDiffPixels.

Use this for map renders, chart renders, and other canvas-based visualizations that can't be asserted through the DOM. Be aware that visual diffs are sensitive to OS-level font rendering, GPU differences, and browser versions β€” this is why world.ts disables subpixel positioning and font hinting (for consistency across machines).

When partners customize this: tuning maxDiffPixels defaults or the threshold, or adding helpers for element-level (not component-level) visual diffs.


6. The test lifecycle​

Understanding the lifecycle helps you reason about what runs when.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ BeforeAll β”‚ once per worker β€” loads SAMO metadata (codelists, feature types)
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ For each scenario: β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Before β”‚ init factory (uses cached metadata) β”‚
β”‚ β”‚ β”‚ factory.checkpoint() β”‚
β”‚ β”‚ β”‚ open browser page β”‚
β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚
β”‚ ↓ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Scenario body β”‚ Given / When / Then steps β”‚
β”‚ β”‚ β”‚ (entities created via factory) β”‚
β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ ↓ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ After β”‚ screenshot on failure β”‚
β”‚ β”‚ β”‚ factory.rollbackSequentially(1) β”‚
β”‚ β”‚ β”‚ close browser β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ AfterAll β”‚ per-worker cleanup (logging only)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key points:

  • Metadata is loaded once per worker, not per scenario β€” it's an expensive network round trip and it does not change during the run.
  • Every scenario gets a fresh LASDemoFactory, a fresh checkpoint(), and a fresh browser page. Scenarios are isolated from each other.
  • On After, factory.rollbackSequentially(1) deletes every entity the scenario created (including associated attachments). If the test passed, you get a clean environment; if it failed, you still get a clean environment.
  • Parallel execution: by default Cucumber runs 3 workers in parallel. Each worker runs its own scenarios, each with its own metadata cache, browser, and factory. See cucumber.json for the parallel count.

7. Writing a feature file (Gherkin / Cucumber)​

Gherkin is the natural-language syntax that Cucumber understands. One feature file describes one component or user flow.

7.1 Structure​

@samoEntityDocuments
Feature: Entity documents component
Component that shows documents attached to an entity.
Users can attach files, preview them, and delete them.

Background:
Given the user samo is logged into SAMO

Scenario: Attach a PDF document to an entity
Given prepare 1 entity type "legal_right" with attributes:
| at_prop_agreem_mark | e2e-docs-{SCENARIO_UNIQUE_ID} |
Given open section "Contracts" and subsection "Other Legal Rights"
Given add to the end of URL "!search=at_prop_agreem_mark:\"e2e-docs-{SCENARIO_UNIQUE_ID}\""
When open 1 entity from list
And click on add button in component "samo-entity-documents" at position 1
And upload file "example.pdf" as attachment
Then the list in component "samo-entity-documents" at position 1 contains value "example.pdf"

Sections:

  • Tag @samoEntityDocuments β€” attached to every scenario in the file. Use it to run this feature: npm run testTag @samoEntityDocuments.
  • Feature: line β€” human-readable name.
  • Description β€” the lines immediately after the Feature: line (indented). Any free text.
  • Background: β€” steps that run before every scenario in the file. Keep it minimal (login, global setup).
  • Scenario: β€” one test case. Reads like a short story: preconditions (Given), actions (When), expectations (Then). Use And to chain when the previous keyword still applies.

⚠️ Background is a tradeoff. It reduces duplication but has real downsides:

  • A reader looking at a scenario deep in the file has to scroll to the top to see what already ran.
  • A maintainer adding a scenario at the bottom can forget the Background exists and add redundant steps.
  • When you move a scenario to a different feature file, you have to re-check what Background it relied on.

Rule of thumb: use Background only for steps that are truly shared across every scenario in the file (like login). If only half the scenarios need the step, move it back into the scenarios themselves.

7.2 BDD best practices (the BRIEF heuristics)​

Six rules for readable scenarios:

  • Business language β€” use domain terms your business stakeholders understand.
  • Real data β€” concrete examples over abstract "some value".
  • Intention revealing β€” describe the why, not the mechanics.
  • Essential β€” remove details that do not change the outcome of the rule being tested.
  • Focused β€” one scenario tests one rule.
  • Brief β€” aim for ≀ 5 steps. Long scenarios hide their intent.

7.3 Tags​

Tags organize and filter tests.

TagPurpose
@featureName (e.g. @samoEntityDocuments)Group all scenarios in one file. Required on every feature.
@WIPWork in progress. Skipped in CI. Use while you are still iterating.
@ToDoScenario planned but not implemented. Skipped in CI.
@singleThreadScenario that must run alone (cannot run in parallel with others).

Custom tags for ad-hoc selection are fine: @smoke, @regression, @critical, etc.

7.4 {SCENARIO_UNIQUE_ID} β€” mandatory parallel isolation​

Every scenario must create entities with a unique marker. This prevents scenarios that run in parallel from seeing each other's data.

The string {SCENARIO_UNIQUE_ID} appearing in any step parameter is replaced at runtime by a unique per-scenario identifier (see features/support/world.ts).

Given prepare 1 entity type "work_order" with attributes:
| at_boWorkRealiz_description | e2e-wo-{SCENARIO_UNIQUE_ID} |
Given add to the end of URL "!search=at_boWorkRealiz_description:e2e-wo-{SCENARIO_UNIQUE_ID}"

Both steps get the same unique id in one scenario. Another scenario running in parallel gets a different id. This guarantees that open 1 entity from list finds your entity, not a stranger's.

7.5 Gherkin expression tricks​

Cucumber expressions let you write steps in natural English with two very useful shortcuts.

Optional (s) for singular and plural​

Wrap a suffix in parentheses to make it optional β€” one step matches both forms:

When(
'enables {string} in the creation dialog(s)',
async function (this: CustomWorld, name: string) {
// Matches BOTH:
// When enables "Subscribe" in the creation dialog
// When enables "Show count" in the creation dialogs
}
);

This keeps your step definition count down while preserving readable English in the .feature file.

Alternates with / for synonyms​

A slash matches either alternative β€” one step for multiple natural phrasings:

When(
'enables/allow/permit {string} in the creation dialog',
async function (this: CustomWorld, name: string) {
// Matches "enables ...", "allow ...", and "permit ..."
}
);

Use this when the action is the same but the business-language term varies by context.

DataTable helpers​

When a step uses a DataTable:, you can transform it inside the step. The two common helpers:

Given('prepare entity with attributes:', async function (this: CustomWorld, table: DataTable) {
// rowsHash() β€” when the table has two columns: key and value
const attrs = table.rowsHash(); // { name: 'Alice', age: '30' }

// hashes() β€” when the first row is a header and each subsequent row is a record
const records = table.hashes(); // [{ name: 'Alice', age: '30' }, ...]

// raw() β€” 2D array as-is
const raw = table.raw(); // [['name', 'age'], ['Alice', '30']]

// transpose() β€” swap rows and columns; useful for turning a horizontal table into vertical
const transposed = table.transpose();
});

Use transpose() when your .feature reads more naturally one way but your step-definition code needs the other shape.


8. Writing step definitions (TypeScript / Playwright)​

Each Given/When/Then in a feature file is backed by a TypeScript function that uses Playwright to drive a real browser.

8.1 Minimal skeleton​

// features/step_definitions/myFeature.ts
import { When, Then } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../support/world';

When('click on the save button', async function (this: CustomWorld) {
const page = this.page!;
await page.getByRole('button', { name: 'Save' }).click();
});

Then('the entity is marked as saved', async function (this: CustomWorld) {
const page = this.page!;
await expect(page.getByText('Saved')).toBeVisible();
});

8.2 What's on this (CustomWorld)​

The first argument on every step function is this, typed as CustomWorld. It carries per-scenario state:

PropertyTypePurpose
this.pagePage | nullThe Playwright page (browser tab).
this.factoryLASDemoFactory | nullThe factory for creating test entities.
this.userServiceFactoryUserServiceDemoFactory | nullUser-service factory (messages, profiles).
this.scenarioUniqueIdstringThe unique marker for this scenario.
this.entityobjectThe last entity created via prepare … step.
this.previouslyCreatedEntityobjectThe entity created before the last one. Used to chain creates.
this.customOptionsRecord<string, any>Scratch bag for anything you need to pass between steps.

πŸ’‘ this is shared across all steps of a single scenario, but fresh for each new scenario. That's the key pattern for passing data between steps: save something in step A (this.customOptions.someValue = …), use it in step B later in the same scenario. When the scenario ends, this is thrown away and the next scenario starts clean. Never rely on this values persisting across scenarios β€” use the factory-bot's rollback mechanism for shared test-data setup, not this.

8.3 Selectors β€” shadow DOM everywhere​

SAMO's UI is built as web components with shadow DOM. That matters for how you locate elements:

  • page.locator('samo-entity-detail button.save') works β€” Playwright's locator auto-pierces shadow DOM.
  • page.evaluate(() => document.querySelector('samo-…')) does NOT work β€” the native querySelector does not pierce shadow DOM. If you need to reach into a custom element via page.evaluate, start from the app root: document.querySelector('dynamic-app').shadowRoot.querySelector('samo-…').
  • locator.textContent() and innerHTML() return empty for shadow-DOM elements. Use locator.evaluate(el => el.someProperty) or attribute checks instead.

8.4 Waits​

  • await locator.waitFor({ state: 'visible', timeout: 10000 }) β€” wait for an element.
  • await page.waitForLoadState('networkidle') β€” wait for network activity to settle. Useful after navigation or an action that triggers a fetch.
  • await expect(locator).toBeVisible({ timeout: 10000 }) β€” Playwright's expectation auto-retries.
  • Avoid page.waitForTimeout(N) unless you are waiting for a documented app-side timer. Fixed sleeps are brittle.

8.5 Parameter types​

Cucumber expressions match Gherkin text to step-definition parameters:

Given(
'prepare {int} entity type {string}',
async function (this: CustomWorld, count: number, entityType: string) {
// …
}
);

Built-in types: {int}, {float}, {word}, {string} (quoted). You can define custom types in features/support/customParameterTypeDefinition.ts.

8.6 Always replace {SCENARIO_UNIQUE_ID}​

If a step accepts a string that might contain {SCENARIO_UNIQUE_ID}, you must substitute it in the step body:

When('add to the end of URL {string}', async function (this: CustomWorld, suffix: string) {
const processed = suffix.replace(/{SCENARIO_UNIQUE_ID}/g, this.scenarioUniqueId);
await this.page!.goto(this.page!.url() + processed);
});

If you forget, the literal text {SCENARIO_UNIQUE_ID} ends up in URLs and search filters and nothing matches.

8.7 Playwright actions cheat sheet​

A condensed reference for the most common Playwright calls you'll reach for in step definitions. For the full API, see the Playwright docs.

// ──────────────────────────── Navigation ────────────────────────────
await page.goto('/absolute-path'); // absolute path on base URL
await page.goto(process.env.GTW_BASE_URL + '/…'); // full URL
await page.goBack();
await page.reload();

// ──────────────────────────── Waiting ───────────────────────────────
await locator.waitFor({ state: 'visible', timeout: 10000 });
await locator.waitFor({ state: 'attached' }); // just in the DOM, may be invisible
await locator.waitFor({ state: 'detached' }); // gone from the DOM
await page.waitForLoadState('networkidle'); // network activity has settled
await page.waitForLoadState('domcontentloaded'); // DOM is parsed (rarely needed)
await page.waitForURL(/\/detail\//); // URL matches pattern
await page.waitForTimeout(500); // ⚠️ fixed wait β€” use sparingly

// ──────────────────────────── Locating ──────────────────────────────
page.getByRole('button', { name: 'Save' }); // preferred β€” matches accessibility tree
page.getByText('Saved'); // by visible text
page.getByLabel('Email'); // associated with a label
page.getByPlaceholder('Enter name'); // by placeholder attribute
page.getByTestId('save-button'); // by data-testid attribute
page.locator('samo-entity-detail button.save'); // CSS selector (pierces shadow DOM)
page.locator('#id').first(); // disambiguate when multiple match
page.locator('div').nth(2); // positional

// ──────────────────────────── Interactions ──────────────────────────
await locator.click();
await locator.dblclick();
await locator.fill('some text'); // input / textarea
await locator.clear();
await locator.press('Enter'); // single key
await locator.selectOption('optionValue'); // <select>
await locator.check(); // checkbox / radio
await locator.hover();
await locator.setInputFiles('fixtures/sample.pdf'); // file input

// ──────────────────────────── Assertions ────────────────────────────
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toHaveText('exact match');
await expect(locator).toContainText('substring');
await expect(locator).toHaveAttribute('href', /\/detail/);
await expect(locator).toHaveClass(/active/);
await expect(locator).toHaveCount(3); // N matches
await expect(page).toHaveURL(/\/dashboard$/);
await expect(page).toHaveTitle('My Title');

// ──────────────────────────── Polling ───────────────────────────────
await expect
.poll(async () => locator.evaluate((el: any) => el.ready), {
timeout: 10000,
message: 'element never reached ready state',
})
.toBe(true);

// ──────────────────────────── Screenshots & recording ───────────────
await page.screenshot({ path: 'reports/debug.png', fullPage: true });
await locator.screenshot({ path: 'reports/element.png' });

General rule: prefer role/label/text locators (getByRole, getByText) over CSS selectors. They read better, survive refactors, and encourage accessibility. Fall back to page.locator('…') only when the component has no useful ARIA attributes β€” most SAMO custom elements fit this case, so you'll use CSS selectors often in practice.


9. Factory bots deep dive​

The factory bots are how tests get their data. Instead of clicking through the UI to create 20 test entities, a test calls factory.create('employee') and the entity appears in the backend via API. When the scenario finishes, factory.rollback() deletes it.

9.1 What samo-factory-bot provides (core)​

@samo-tools/samo-factory-bot exposes three factories:

FactoryWhat it creates
LASFactoryFeatures (entities) of any feature type β€” the main one for most tests. Also codelists, settings, object-of-interest.
SecurityManagerFactoryUsers, groups, group memberships.
UserServiceFactoryProfile resources, messages, favorites.

All three support checkpoint() and rollback() for automatic cleanup. See node_modules/@samo-tools/samo-factory-bot/README.md for the full API reference.

Initialization pattern:

import { LASFactory } from '@samo-tools/samo-factory-bot';

const factory = await LASFactory.init(
'https://sandbox.samo-kube.internal/lids',
'Basic ' + Buffer.from('user:password').toString('base64')
);

9.2 Templates​

A template is a named recipe for creating a feature. Instead of passing 10 attributes every time, you register a template once and call it by name.

factory.registerTemplate('employee', 'ft_employee', {
at_employee__personalNo: ({ seq }) => (15000 + seq()).toString(),
at_employee__firstname: () => faker.person.firstName(),
at_employee__surname: () => faker.person.lastName(),
at_employee__email: ({ attr }) =>
faker.internet.email({
firstName: attr.at_employee__firstname as string,
lastName: attr.at_employee__surname as string,
}),
});

const employee = await factory.create('employee');

Template attributes can be:

  • Static values: at_building_floors_c: 3.
  • Functions: at_building_floors_c: () => Math.floor(Math.random() * 10).
  • Attribute-context functions with helpers:
    • { seq } β€” auto-incrementing counter per feature type (starts at 1).
    • { attr } β€” access to previously resolved attributes in the same template (so later attributes can depend on earlier ones).
    • { transient } β€” temporary values with keys starting __ that are computed but not sent to the backend. Useful when two attributes need the same random value (e.g. first name used in both firstname and email).

Templates can extend other templates:

factory.registerTemplate('big_building', 'ft_building', { at_building_floors_c: 7 });
factory.registerTemplate('modern_building', 'big_building', { at_building_floors_c: 9 });
// modern_building inherits all of big_building's attributes, overrides floors.

9.3 Hooks​

Templates can run code at lifecycle points:

factory.registerTemplate(
'building_with_owner',
'ft_owner',
{ at_owner_name: () => faker.person.fullName() },
{
hooks: {
after_create: async (owner) => {
// After the owner is created, also create 3 buildings linked to them.
await factory.createList(3, 'ft_building', {
at_building_owner: { type: 'ft_owner', id: owner?.id },
});
},
},
}
);
  • before_create(dataToCreate) β€” mutate the payload right before the POST.
  • after_create(created) β€” run after the feature exists; the argument is the FeatureInstance.
  • after_all(features) β€” only for createList, runs once after all have been created.

9.4 Checkpoints and rollback​

factory.checkpoint(); // marker 0
await factory.create('employee'); // A
await factory.create('building'); // B
factory.checkpoint(); // marker 1
await factory.create('work_order'); // C

// What e2e-samo's After hook actually uses:
await factory.rollbackSequentially(1); // deletes C (back to marker 1), one at a time

// To wipe everything in reverse creation order:
await factory.rollbackAllSequentially(); // deletes C, B, A β€” safe for linked records

Prefer the sequential variants by default. rollbackSequentially(n) and rollbackAllSequentially() delete entities one at a time in reverse creation order. This is safer in three ways:

  • Order safety β€” children created via after_create hooks are deleted before their parents.
  • Predictable failure β€” if one delete fails, the rest of the stack is still alive on the stack; with parallel rollback (Promise.all) you can't tell which deletes ran.
  • Backend-friendliness β€” avoids overwhelming the backend with concurrent delete requests.

The parallel variants rollback(n) / rollbackAll() exist as an edge-case optimization for resources without order dependencies on a tolerant backend β€” not the default. See the upstream samo-factory-bot README β†’ Checkpoints and Rollbacks β†’ Mental model for the full rationale.

9.5 Codelists and bindings​

SAMO uses codelists (drop-down sources) heavily. The factory caches codelists so you don't hit the backend repeatedly:

const cities = await factory.getCodelist('cl_taiCity');
const randomCity = await factory.getRandomCodelistItem('cl_taiCity');

// Binding filter β€” "get a street in Prague, not in any other city"
const street = await factory.getRandomCodelistItem('cl_taiStreet', {
bindingsFilter: [
{
codelist: 'cl_taiCity',
binding: 'bc_city_cityPart_street_o',
attribute: 'ca_taiStreet__c_city',
values: [10], // Prague
},
],
});

9.6 Geometry helpers​

For features that live on the map:

import { genPoint, genLineString } from '@samo-tools/samo-factory-bot';

const point = genPoint(); // a random point
const line = genLineString(); // a random polyline

Use them via the graphics helper in your template:

import { buildGraphics } from '../utils'; // see samo-demo-factory-bot/src/utils.ts

factory.registerTemplate(
'mast_with_geometry',
'ft_mast',
{ at_mast_type: () => faker.helpers.arrayElement(['A', 'B', 'C']) },
{ featureData: { graphics: buildGraphics({ geometry: () => genPoint() }) } }
);

9.7 What samo-demo-factory-bot adds (partner-customizable layer)​

@samo-tools/samo-demo-factory-bot extends LASFactory:

// samo-demo-factory-bot/src/models/las-demo-factory.ts
import { LASFactory } from '@samo-tools/samo-factory-bot';

export class LASDemoFactory extends LASFactory {
static async init(apiUrl, auth, metadata?) {
const instance = new LASDemoFactory(apiUrl, loadedMetadata, auth);
await instance.registerTemplates(); // ← registers ~50 demo-specific templates
return instance;
}

async registerTemplates() {
this.registerTemplate('employee', 'ft_employee', {
/* … */
});
this.registerTemplate('building', 'ft_building', {
/* … */
});
this.registerTemplate('defect_lightning', 'ft_defDefectElectro', {
/* … */
});
// … ~50 more templates
}
}

The demo factory also provides:

  • Types for each feature (defect-lightning.ts, cable-low-voltage.ts, …) β€” strongly-typed attribute shapes.
  • Constants (CODE_LIST, DEFAULT_CITY) β€” shared values across templates.
  • Utilities (buildGraphics, buildAttachments) β€” helpers that make templates concise.

See node_modules/@samo-tools/samo-demo-factory-bot/README.md for the full list of templates. When building your own factory-bot for a non-Demo deployment, fork samo-template-factory-bot (the minimal starter) rather than this demo factory-bot β€” see Section 10 below.

9.8 Further factory-bot capabilities​

This guide covers the APIs you will use every day. The factory-bot library also provides more specialized features β€” consult node_modules/@samo-tools/samo-factory-bot/README.md when you need them:

  • Bulk operations on features (bulkOperation, bulk assignments, bulk relations) β€” faster than creating one at a time when you need dozens of entities.
  • Settings and Objects-of-Interest (OOI) β€” registerSettingTemplate, createSetting, createOOI, createOOIList, listSettings, updateSettingById, deleteSettingById. Useful for test scenarios that assert on saved views, filters, or OOI configurations.
  • UserServiceFactory templates β€” profile and message templates with dynamic attributes, same pattern as LASFactory templates.
  • SecurityManagerFactory β€” full user and group management (create, delete, list, add/remove memberships, bulk additions).
  • FeatureInstance attachment methods β€” addAttachments, getAssociatedAttachments, deleteAssociatedAttachments for tests that exercise document-upload flows.
  • importCodelist / dumpAllCodelists / getCodelistXML β€” for scenarios that depend on specific codelist entries your fixtures need to seed.

10. Customizing for your own environment​

This is the core of partner work. You create two new repositories, both forked from the starter templates β€” not from this reference suite or the demo factory-bot:

  1. samo-<yourcustomer>-factory-bot β€” fork of samo-template-factory-bot (minimal starter) with your feature types and templates.
  2. e2e-<yourcustomer> β€” fork of e2e-samo-template (minimal E2E starter) with your Gherkin scenarios.

The starter templates ship as minimal scaffolds (one example template, two demo scenarios) plus rename β†’ register β†’ publish checklists. Use samo-demo-factory-bot and e2e-samo (this repo) as pattern libraries β€” open them in your editor, copy structural ideas (linked records, geometry helpers, codelist binding filters, the _with_<suffix> variant convention), but do not fork them. They carry ~50 SAMO Demo-specific templates and ~20 feature files you would have to strip.

10.1 Create your factory-bot​

Fork the minimal starter (not the demo factory-bot β€” see introduction above):

git clone <samo-template-factory-bot-url> samo-acme-factory-bot
cd samo-acme-factory-bot

# Update package.json: name, description, repository.
# Change "name" to "@acme/samo-factory-bot" (or whatever convention your org uses).
# Point publishConfig.registry at your npm registry.

The starter ships with one example template (typ_pristroja) and three commented-out reference patterns (employee, building_with_owner, hydrant_full). Its README.md and AGENTS.md walk you through the full rename β†’ register β†’ publish checklist in three phases.

Then edit src/models/las-template-factory.ts (rename the file and the class to match your org):

export class ACMELASFactory extends LASFactory {
async registerTemplates() {
// Register templates for YOUR feature types.
this.registerTemplate('customer', 'ft_acme_customer', {
at_customer_name: () => faker.company.name(),
at_customer_vat: () => faker.finance.accountNumber(9),
});

this.registerTemplate('order', 'ft_acme_order', {
at_order_number: ({ seq }) => `ORD-${1000 + seq()}`,
at_order_amount: () => faker.number.float({ min: 100, max: 10000 }),
});

// … one registerTemplate call per feature type you want usable in tests
}
}

Build and publish to your npm registry (or use npm link during development β€” see 10.4).

10.2 Create your E2E test project​

Fork the minimal E2E starter (not this reference suite):

git clone <e2e-samo-template-url> e2e-acme
cd e2e-acme

# Replace the factory dependency
# "@samo-tools/samo-template-factory-bot": "^1.0.0"
# with
# "@acme/samo-factory-bot": "^1.0.0"
# in package.json.

# Update imports in features/support/factory.ts and features/support/hooks.ts:
# from '@samo-tools/samo-template-factory-bot'
# to
# from '@acme/samo-factory-bot'

npm install

The starter ships with two demo scenarios β€” @smoke for a credentials-free framework wiring check (verifies the SAMO login page is reachable without any test user) and @login for UI + API login. Its README.md walks you through Phase A/B/C adoption: factory-bot fork β†’ scenario rewrite β†’ CI integration.

Adjust .env for your SAMO sandbox URL and credentials (see Appendix A).

10.3 Write your first custom scenario​

Create features/scenarios/customers.feature:

@acmeCustomers
Feature: Customer list

Background:
Given the user samo is logged into SAMO

Scenario: Open a customer detail from the list
Given prepare 1 entity type "customer" with attributes:
| at_customer_name | E2E-{SCENARIO_UNIQUE_ID} |
Given open section "Customers" and subsection "All customers"
Given add to the end of URL "!search=at_customer_name:E2E-{SCENARIO_UNIQUE_ID}"
When open 1 entity from list
Then info detail was displayed

Most existing step definitions from e2e-samo are domain-agnostic (login, navigation, info-detail), so you can reuse them without changes. Write new step definitions only for UI components unique to your customer's SAMO configuration.

Run it:

npm run testTag @acmeCustomers

When iterating on both repos simultaneously, avoid the publish/install cycle:

# In your factory-bot repo
cd samo-acme-factory-bot
npm run build # produces dist/
npm link # makes it globally available

# In your E2E repo
cd ../e2e-acme
npm link @acme/samo-factory-bot

# Now any change in samo-acme-factory-bot's dist/ is picked up immediately by e2e-acme.

To revert:

npm unlink @acme/samo-factory-bot
npm install

11. Running tests β€” all the commands​

All commands from the project root.

CommandWhat it does
npm run testLocalRuns everything in a headed browser (you see it). Generates HTML report.
npm run testLocalHeadlessSame, but without opening a window. Faster.
npm run testTag @myTagRuns only scenarios tagged @myTag. Headed.
npm run testTagHeadless @myTagSame, headless. Closest to how CI runs.
npm run testToDoRuns only scenarios tagged @ToDo. Useful for checking planned but unimplemented tests.
npm run testKubeRuns against a Kubernetes environment (uses different credentials/URL logic).
npx cucumber-jsRaw Cucumber invocation, useful for debugging wiring.
npx cucumber-js --dry-runChecks that every step in every .feature has a matching step definition. No browser is launched.

11.1 Before opening a Merge Request​

Always run both testLocal and testLocalHeadless. A test can pass headed but fail headless because of window timing differences.

11.2 Recording new interactions​

If you are writing a brand new step and don't know which selector to use, Playwright's codegen opens a recording session:

npx playwright codegen https://your-sandbox.samo-kube.internal/ --ignore-https-errors

Click through the UI; the generated TypeScript appears in a side panel. Copy the relevant selectors into your step definition. Refine them afterwards β€” auto-generated selectors are often more brittle than hand-written ones.


12. Reading test reports​

After a run, you get:

reports/
β”œβ”€β”€ index.html # Main HTML report β€” open this
β”œβ”€β”€ cucumber-report-parallel.json # Raw Cucumber JSON
β”œβ”€β”€ cucumber-report-serial.json # Same, for @singleThread scenarios
β”œβ”€β”€ traces/ # Playwright .zip traces (on failure or FORCE_VIDEOS_SAVE=true)
β”œβ”€β”€ videos/ # Scenario videos
β”œβ”€β”€ downloads/ # Files the scenarios downloaded
└── ...

12.1 HTML report​

Open reports/index.html. For each scenario:

  • Pass/fail status and total duration.
  • Each step with its duration.
  • On failure: a full-page screenshot is embedded in the report.
  • Links to the video (entire scenario) and the trace.

12.2 Playwright traces​

A trace is the richest debugging artifact: a step-by-step replay of every action, every selector match, every network call.

npx playwright show-trace reports/traces/trace-<timestamp>.zip

Opens an interactive viewer. Use it to step through a failed scenario, see the DOM at each moment, and inspect network requests.

12.3 Forcing video and trace saves for all runs​

By default, video and trace are kept only on failure (to save disk space). For debugging flaky tests, set in .env:

FORCE_VIDEOS_SAVE="true"

This saves traces and videos for every scenario. Report sizes grow to hundreds of megabytes β€” do not commit them.


13. Debugging failing tests​

Follow this order. Skipping step 1 costs you time.

13.1 Look at the screenshot​

The After hook attaches a full-page screenshot to the report on every failure. 80 % of failures are obvious from the screenshot alone β€” a missing button, a dialog that never opened, a typo in a search field.

13.2 Analyze the trace​

If the screenshot is not enough, open the Playwright trace (npx playwright show-trace …). Step through actions and watch:

  • Which selector didn't match?
  • Was there a pending network request?
  • Did a dialog appear and immediately disappear?

13.3 Run the single scenario headed​

# Temporarily tag the scenario with a custom tag:
@debugThis
Scenario: ...

# Then:
npm run testTag @debugThis

Watching the headed run in real time often reveals timing issues that are invisible in the trace.

13.4 Add targeted diagnostics​

If still unclear, add temporary logging inside the step:

// console.log is swallowed by Cucumber parallel workers β€” write to a file instead:
require('fs').writeFileSync('reports/debug.txt', JSON.stringify(something));

// Or capture a screenshot at a specific point:
await page.screenshot({ path: 'reports/debug-mid-scenario.png', fullPage: true });

13.5 Interactively reproduce with Playwright codegen​

If the scenario is long, reproduce just the failing step using codegen (see 11.2) to find a better selector.


14. Git workflow and contribution​

14.1 Branches​

Create a personal branch named after yourself or the feature you are testing:

git checkout -b alice/add-customer-tests

Commit often with small, focused changes. Commit subjects usually reference a ticket:

test: SAMO-12345 add customer list scenarios

14.2 Merge requests​

  • Push your branch: git push -u origin alice/add-customer-tests.
  • Open a Merge Request in GitLab.
  • In the MR description, mention which environment and which tags you used to validate. Example: "validated against alice-test sandbox, ran npm run testTag @acmeCustomers + full testLocalHeadless."
  • If your E2E changes depend on changes in the tested application (Dynamic App), create the same branch name in the Dynamic App repo. The CI pipeline wires them together by branch name.

14.3 Do not commit​

  • .env (credentials).
  • reports/ (generated and often huge).
  • node_modules/ (already in .gitignore).
  • Screenshots, videos, traces from local runs.

15. Housekeeping​

15.1 Clean state after a cancelled run​

Scenarios clean up after themselves via factory.rollback in the After hook. If you stop a run with Ctrl+C, that hook does not fire and test entities stay in the backend.

To clean up manually, either:

  • Re-run the test (it will recreate its own entities but the stray ones remain).
  • Delete the stray entities through the SAMO UI.
  • For heavy leaks, recreate your Kubernetes environment (see 15.2).

15.2 Kubernetes environment hygiene​

  • Name your environment clearly (alice-test, not testing) so everyone knows whose it is.
  • Delete your environment when you know you will not use it for a while. It consumes cluster resources.
  • Documentation for deploying / deleting is in your organization's SAMO deployment guide.

15.3 {SCENARIO_UNIQUE_ID} β€” do not reuse​

If you hard-code a value instead of using {SCENARIO_UNIQUE_ID}, two parallel scenarios will collide and one will fail non-deterministically. This is the most common cause of flaky tests in this framework. Always use the placeholder.

15.4 Verify scenario independence with random order​

Scenarios are supposed to be fully independent β€” no scenario should rely on another scenario running before it, or on any state left behind. The reference suite does NOT enable random order by default (the cucumber.json in this repo has parallel: 3 but no order field), but you can turn it on temporarily in your fork to catch hidden dependencies:

{
"default": {
"paths": ["features/"],
"parallel": 3,
"order": "random"
}
}

Run the whole suite several times with that setting. If results differ between runs with no code changes, two scenarios are coupled β€” usually one scenario left data behind that the other expected to find (or vice versa). Fix the offender so each scenario creates its own data via the factory-bot.

Consider leaving random order on in CI if your runtime allows β€” it's the cheapest guard against this class of bug.


16. Troubleshooting common errors​

npm install fails with 401/403​

Your .npmrc is not authenticated for the samo-npm registry. Ask your SAMO contact for the correct _auth or _authToken entry, add it to ~/.npmrc or project .npmrc, and retry.

npx playwright install fails behind a corporate proxy​

Set HTTPS_PROXY / HTTP_PROXY environment variables before running the command. See Playwright docs for details.

Error: Factory is not initialized for this scenario​

The Before hook did not run (or it threw). Usually .env values are missing or the SAMO sandbox is unreachable. Check LIDS_URL_API and that you can curl it from your machine.

UNABLE_TO_VERIFY_LEAF_SIGNATURE / self-signed certificate errors​

Internal SAMO environments often use self-signed certs. The project sets NODE_TLS_REJECT_UNAUTHORIZED=0 inside hooks.ts to bypass verification. If you hit this error outside the E2E pipeline (e.g. running a factory-bot script directly), prefix your command:

NODE_TLS_REJECT_UNAUTHORIZED=0 npx ts-node myscript.ts

Never use this in production code.

"Element not found" even though it's visible​

Two most common causes:

  1. The element is inside a shadow root and you used page.evaluate(() => document.querySelector(...)) instead of page.locator(...). Use page.locator or start your traversal from dynamic-app.shadowRoot.
  2. You clicked too early β€” some other async work had to finish first. Add await page.waitForLoadState('networkidle') before the action, or await locator.waitFor({ state: 'visible', timeout: 10000 }).

Tests pass headed but fail headless in CI​

Common reasons:

  • The headless viewport is different from your local one. Headless runs in a fixed size (1366x768 by default β€” see world.ts).
  • Timing differs in headless (sometimes faster). Assertions need proper waits (not waitForTimeout).
  • Popups and new windows behave differently in headless. The test scaffolding disables popup blocking and sets --no-sandbox for Docker compatibility; see world.ts if you need to tune further.

Factory rollback complains about already-deleted entities​

Usually means another scenario (running in parallel without {SCENARIO_UNIQUE_ID}) already deleted your entity, or you deleted it manually in the UI. Fix the test to use {SCENARIO_UNIQUE_ID} consistently.

Flaky "dialog never opens" after clicking a button​

Some SAMO components fetch data asynchronously and the click-to-open sequence races with an internal load. Wait for the component's specific ready signal before clicking (e.g. expect.poll on a JS property), not just for the button to be visible. There is no generic fix β€” inspect the component source to find a ready signal.


17. Further reading​


Appendix A β€” Environment variables reference​

All variables live in .env at the project root (or are set in the environment for CI).

Required​

  • SAMO_DEV_USERNAME β€” Dev user for login via UI. Example: alice.
  • SAMO_DEV_PASSWORD β€” Dev user password.
  • SAMO_TEST_USERNAME β€” Test user (used for scenarios that need a second user). Example: alice-test.
  • SAMO_TEST_PASSWORD β€” Test user password.
  • LIDS_URL_API β€” URL of the LIDS service (feature backend). Example: https://alice-test.samo-kube.internal/lids.
  • USER_SERVICE_URL_API β€” URL of the user service. Example: https://alice-test.samo-kube.internal/userService.
  • PATH_TO_APP β€” Path under GTW_BASE_URL where the SAMO dynamic client lives. Example: /samo/a/demo/c/demo-dynamic-client.
  • GTW_BASE_URL β€” Base URL of the SAMO gateway. Example: https://alice-test.samo-kube.internal.
  • PLAYWRIGHT_ALLOWED_PORTS β€” Comma-separated list of ports Chrome will consider safe (e.g. for Kubernetes-style URLs on non-standard ports). Example: 8080,10080.

Optional​

  • APPLICATION β€” Used only by object-of-interest scenarios (ObjectOfInterest.ts); defaults to empty when unset. Keep demo for the reference suite. Example: demo.
  • FORCE_VIDEOS_SAVE β€” If true, saves videos and traces for all scenarios, not just failing ones. Local-only β€” never enable in CI. Example: true.
  • PLAYWRIGHT_HEADLESS β€” If true, forces headless mode. Set automatically by testLocalHeadless / testTagHeadless β€” only set manually if you want testLocal / testTag to also run headless. Example: true.

Appendix B β€” Commands cheat sheet​

# Install
npm install
npx playwright install

# First-time setup
cp .env.example .env
# …edit .env…

# Run all tests (headed)
npm run testLocal

# Run all tests (headless)
npm run testLocalHeadless

# Run only tests with a specific tag
npm run testTag @myFeature
npm run testTagHeadless @myFeature

# Dry run β€” validates every step has a definition, no browser
npx cucumber-js --dry-run

# Record a new interaction
npx playwright codegen https://your-sandbox.samo-kube.internal/ --ignore-https-errors

# Open a trace from a failure
npx playwright show-trace reports/traces/trace-<timestamp>.zip

# Run @ToDo scenarios only (useful to see what's pending)
npm run testToDo

Questions, corrections, improvements to this guide are welcome β€” open a Merge Request against this file, or contact your SAMO implementation partner.