Lyra, Automated Testing Evolved¶
- Lyra, Automated Testing Evolved
Lyra is a powerful tool for recording and replaying web application tests with perfect fidelity. It allows you to capture user interactions and replay them either visually or headlessly, making it ideal for regression testing and performance monitoring.
This document is the official documentation for Lyra. See also: Lyra Testing Best Practices.
Prerequisites¶
Lyra requires Node.js v20 or higher.
Installing Node.js¶
Mac / Linux (Recommended via nvm):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash source ~/.bashrc nvm install 20 nvm use 20
Windows:
Download the "LTS" installer from nodejs.org and follow the setup wizard.
Enabling Lyra Support in FWD¶
Lyra support must be enabled in the FWD server directory (directory.xml); by default it is disabled. Without this, the server will not send widget stable IDs to the JS client and the p2j.lyra.js script will not be injected into the page, so recording and playback will not work.
The container with the Lyra options is at ""/server/default/webClient/lyra:
<node class="container" name="server">
<node class="container" name="default">
<node class="container" name="webClient">
<node class="container" name="lyra">
<node class="boolean" name="enabled">
<node-attribute name="value" value="TRUE"/>
</node>
<node class="boolean" name="debug">
<node-attribute name="value" value="FALSE"/>
</node>
</node>
</node>
</node>
</node>
Options:
enabled- IftrueLyra is enabled server side. This is mandatory to activate Lyra.debug- Enables server-side debug instrumentation. Only to be used for debug purposes - set totrueto activate. When enabled, the generated tests includesnapshotInfowith additional widget information captured at recording time (widget class, parent frame, window), useful for linking legacy 4GL components to captured widgets.
The FWD server must be restarted for the change to take effect.
Quick Start¶
To get started, follow these simple steps:
- Install Lyra:
sudo npm install -g path/to/fwd-lyra-1.0.0.tgz - Install Browsers & Dependencies:
npx playwright install-deps && npx playwright install firefox - Record a Test:
lyra record --url https://your-server.com/app --output my-test.js - Play a Test:
lyra play my-test.js
Recording Tests¶
The Lyra Recorder captures your interactions with a web application and converts them into a robust Playwright test script.
The Recording Process¶
Recording a robust test involves more than just clicking through the application. A typical workflow looks like this:
- Start Recording: Launch Lyra with the target URL.
- Perform Actions: Interact with the application (click, type, navigate) to perform the workflow you want to test.
- Pause at Key Points: When you reach a critical state (e.g., after logging in, or after a search results load), Pause the recording.
- Add Assertions: While paused, use the Selection Assert tool to verify the UI state. This ensures the test checks for correctness, not just completion.
- Add Timers: If you want to measure performance of a specific step (e.g., "Search"), insert a Start Timer event before the action and an End Timer event after it.
- Resume: Continue recording the next steps.
- Stop: Close the browser or press Ctrl+C to finish and generate the script.
Starting the Recorder¶
Run the record command to launch the browser and start recording:
lyra record --url <TARGET_URL> --output <OUTPUT_FILE>
Arguments:
--url: The full URL to the application you want to test.--output: The path where the generated test script will be saved (e.g.,tests/login-flow.js).--browser: (Optional) Specify chromium, firefox, or webkit. Default is firefox.--devtools: (Optional) Automatically open browser Developer Tools when recording starts.--helpers-only: (Optional) Generates only thelyra-test-helpers.jsfile in the output directory and exits. Useful for regenerating helpers without recording a test.
The Control Panel¶
When recording starts, a Control Panel window will open alongside the browser. This panel is your command center for the recording session.
- Status Indicator: A pulsing red dot indicates that recording is active. A grey pause symbol means recording is paused.
- Event List: Displays a real-time log of every captured action (clicks, keystrokes, etc.).
Control Panel Tools¶
| Icon | Action | Description |
|---|---|---|
| ⏸/● | Pause/Resume | Toggles recording on and off. Use this when you need to perform setup steps that shouldn't be part of the test. |
| 🎯 | Assert Selection | Enters Selection Mode. detailed below. |
| ✗ | Cancel | Cancels the current selection operation. |
| ✓ | Confirm | Confirms the current selection or edit. |
Selection Assert (Target Icon)¶
The Selection Assert tool (🎯) allows you to verify the state of the application without writing code.
- Click the 🎯 icon.
- Click and drag to draw a box around a specific part of the UI (e.g., a form, a table, or a specific button).
- Click the Confirm (✓) button.
What it does:
Lyra captures the semantic structure (Widget Tree) of the elements within the selected area. During playback, it verifies that:
- The exact same widgets exist in that area.
- They have the same key properties (text, visibility, enabled state, etc.).
Additionally, when you create an assertion, Lyra saves a screenshot of the selected area in the test directory (under `captures/`). This serves as a visual reference for what was verified.
Why use it?- Robustness: It checks the meaning of the UI, not just pixels. It won't fail if fonts rendering slightly differently, but will fail if a "Save" button changes to "Submit".
- Ease of Use: You don't need to manually inspect HTML or write complex CSS selectors to verify data. Just draw a box around a grid to ensure it loaded the correct data.
Managing Events¶
- Delete: Select one or more events in the list and press the Delete key to remove them.
- Multi-select: Hold Ctrl (or Cmd) to select multiple events.
- Range Select: Hold Shift and click to select a range of events.
- Edit Custom Events: Double-click a custom event (like a timer) to edit its text.
- Insert Custom Event: Press the Insert key to add a custom event definition. This is primarily used for Timers.
Performance Timers¶
You can measure how long specific workflows take by inserting Start and End timers.
- Press Insert key (to create a custom event).
- Type
startTimer('My Transaction'). - Perform the actions you want to measure (e.g., clicking 'Save').
- Press Insert key again.
- Type
endTimer('My Transaction'). - You can optionally add a failure threshold (in milliseconds):
endTimer('My Transaction', 2000).
If using a threshold (e.g., 2000ms), you must run the player with --fail-timer-threshold to make the test fail if the action is too slow.
Timer results will appear in the Console output and the generated HTML/JSON reports.
Concept: Stable IDs¶
Lyra understands the FWD application structure. Instead of relying on (unstable) numeric Widget IDs, it captures the Stable ID of the widget you interact with.
- Format:
<segment>/<segment>/... - Example:
win:Hotel Demo/frame:fmain/btn:Search
Segment Structure¶
Each segment in the path represents a widget in the hierarchy and follows this format:<abbreviation>:<identifier>[:<ordinal>]
- Abbreviation: A short code representing the widget class (e.g.,
btnfor Button,txtfor Text). - Identifier: A meaningful name derived from the widget's properties:
- Windows/Frames: The window title.
- Buttons/Labels: The visible text.
- Fields: The field label.
- Ordinal: (Optional) A zero-based index used to distinguish between identical siblings (e.g., two "Save" buttons in the same panel). It is omitted if the widget is unique among its siblings.
- Escaping: Special characters (
:and/) in names are escaped with a backslash.
Stable ID Abbreviations¶
The following table lists the abbreviations used in Stable IDs and the corresponding widget types:
| Abbr | Widget Type |
|---|---|
| alert | Alert Box |
| border | Bordered Panel |
| brcol | Browse Column |
| brconf | Browse Confirm Panel |
| brfilt | Browse Filter Entry Field |
| browse | Browse / Grid |
| btn | Button |
| btnlist | Button List |
| cal | Calendar |
| calpop | Calendar Popup |
| capbtn | Caption Button |
| combo | Combo Box |
| dialog | Dialog Box |
| drop | Drop Down |
| edit | Editor |
| editpop | Editor Popup |
| entry | Entry Field |
| fill | Fill-In |
| font | Font Chooser |
| frame | Frame |
| fover | Frame Overlay |
| html | HTML Browser |
| link | Hyperlink |
| img | Image |
| imglist | Image List |
| input | Input Dialog |
| lbl | Label |
| line | Line Editor |
| menu | Menu |
| item | Menu Item |
| moven | Menu Overlay Window |
| msg | Message Area |
| modal | Modal Window |
| over | Overlay Window |
| Print Setup Dialog | |
| prog | Progress Bar |
| radio | Radio Button |
| rset | Radio Set |
| rect | Rectangle |
| sclist | Scrollable List |
| scsellist | Scrollable Selection List |
| sbbtn | Scroll Bar Button |
| sbar | Scroll Bar |
| spane | Scroll Pane |
| spop | Scroll Popup |
| slbody | Selection List Body |
| slist | Selection List |
| sig | Signature Client |
| imgbtn | Simple Image Button |
| slbl | Simple Label |
| skip | Skip Button |
| slider | Slider |
| sheet | Spreadsheet |
| status | Status Line |
| sub | Sub Menu |
| tab | Tab Set |
| txt | Text |
| toggle | Toggle Box |
| tip | Tool Tip |
| tlbody | Tree List Body |
| tlcap | Tree List Caption |
| tlitem | Tree List Caption Item |
| tlist | Tree List |
| tvbody | Tree View Body |
| tview | Tree View |
| win | Window |
| winbar | Window Title Bar |
| winpop | Window Title Popup |
| work | Workspace |
Mouse-down Screenshots¶
During recording, Lyra automatically captures a screenshot of the page's viewport every time a mouse down occurs. Each screenshot includes a visual marker (red target with crosshairs) showing the exact mouse down location.
- Filename:
mousedown-<eventId>.png, saved in thecaptures/directory alongside assertion screenshots. - Script Comment: A reference to the screenshot file is added as a comment in the generated test script above the corresponding click action:
// Screenshot: test/captures/click-5.png // click (stableId: win:Hotel Demo/btn:OK) await clickAtWidget('win:Hotel Demo/btn:OK');
These screenshots provide a visual record of every mouse down interaction, making it easier to debug test failures and understand recorded test flows.
Playing Tests¶
Replay your recorded tests to verify application functionality.
lyra play <TEST_FILE> [options]
CLI Options¶
--headless: Runs the test in the background without opening a visible browser window. Faster and ideal for CI/CD.--browser <type>: Overrides the recorded browser type (chromium, firefox, or webkit).--debug(or-d): Enables Playwright Inspector for step-by-step debugging.--skip-waits: Ignores all recorded wait times (think time) to run the test as fast as possible.--keep-browser-open: Keeps the browser window open after the test finishes (useful for debugging failed states).--fail-timer-threshold: Causes the test to fail if any named performance timers exceed their defined duration.--load-profile <file>: Runs the test in Load Testing mode using the specified JSON profile.
Test Failures¶
A test replay can fail for several reasons. Lyra provides detailed logs to help you diagnose the issue.
- Assertion Failure: The UI state at playback time did not match the state captured during recording. (e.g., a field was missing, or had different text).
- Widget Not Found: Lyra attempted to click a widget using its Stable ID, but the application could not find a widget with that ID. This usually means the widget was removed or renamed in the application code or an unexpected UI state was reached.
- Load Metric Failure: In a load test, if error rates exceed the configured
failureThreshold, the test aborts. - Timer Threshold: If
--fail-timer-thresholdis used and an operation takes longer than the limit you defined in theendTimercall.
When a test fails, Lyra automatically captures a full-page screenshot of the application state at the moment of failure. This screenshot is saved in the test run's artifact directory as failure-screenshot.png.
Logs and Artifacts¶
Every test run creates a timestamped artifact directory (e.g., test_run_my-test_2026-01-01_12-00-00).
- console.log: The full output of the test runner.
- report.html: (Load Tests) A graphical report of the run.
- report.json: (Load Tests) Raw data.
- user_<ID>.log: (Load Tests) Individual logs for each Virtual User.
The Helper Library (lyra-test-helpers.js)¶
Lyra generates a shared library file named lyra-test-helpers.js in your output directory. This file contains utility functions (like clickAtWidget, assertUiState, etc.) that your test scripts rely on.
- Automatic Generation: Lyra creates this file automatically when you record a test if it doesn't already exist.
- Versioning: The helper library is versioned to match your installed Lyra version. The generated test scripts include a check to ensure they are compatible with the helpers file.
- Regeneration: If you upgrade Lyra, you may need to update this file. You can regenerate it without recording a new test by running:
lyra record --helpers-only --output .
Lyra Query Language (LQL)¶
Lyra test scripts can query whether widgets are currently visible on screen using a glob-like pattern language over stable ID paths. This enables tests like "is there an alert dialog visible?" or "are all buttons in the main frame present?" without hardcoding widget IDs and allows dynamic test execution branching based on the results.
Four helper functions are available in every Lyra-generated test script:
| Function | Returns | Description |
|---|---|---|
isWidgetVisible(pattern) |
Promise<boolean> |
True if at least one visible widget matches |
queryVisibleWidgets(pattern) |
Promise<string[]> |
All matching stable ID strings |
getAllVisibleWidgets() |
Promise<string[]> |
All visible stable IDs (no filtering) |
countVisibleWidgets(pattern) |
Promise<number> |
Count of matches (all visible if no pattern) |
Stable ID Structure¶
Every visible widget is assigned a stable ID -- a hierarchical path that identifies it by its position in the widget tree. Stable IDs are built by StableIdBuilder.java and have the form: win:Menu & Form Demo/frame:fmain/btn:Greet
Each path consists of segments separated by /. Each segment has the form: type:name[:ordinal]
- type -- abbreviated widget class
- name -- widget name, label text, or title (may be empty, may contain escaped
\:or\/) - ordinal -- optional 0-based disambiguator when sibling widgets share the same type:name
For more information on stable IDs see the section Concept: Stable IDs.
Example Stable IDs¶
win:Menu & Form Demo win:Menu & Form Demo/frame:fmain win:Menu & Form Demo/frame:fmain/btn:Greet win:Menu & Form Demo/frame:fmain/fill:Name win:Menu & Form Demo/frame:fmain/lbl:Name\: win:Menu & Form Demo/frame:fmain/btn:Save win:Menu & Form Demo/frame:fmain/btn:Save:1 win:Menu & Form Demo/menu: win:Menu & Form Demo/menu:/sub:mFile win:Menu & Form Demo/menu:/sub:mFile/item:New win:Menu & Form Demo/alert:About win:Menu & Form Demo/alert:About/btn:OK
Note that menu: has empty name (the colon is present but nothing follows it). The btn:Save:1 has an ordinal 1 to disambiguate it from the first btn:Save.
Query Pattern Syntax¶
Segment Patterns¶
Each segment in a query matches against a segment in a stable ID:
| Pattern | Matches | Example |
|---|---|---|
btn:OK |
exact type + exact name | only btn:OK |
btn:* |
exact type, any name | any button |
*:Name |
any type, exact name | anything named "Name" |
* or *:* |
any single segment | any widget |
btn:Save* |
type=btn, name starts with "Save" | btn:Save, btn:SaveAs |
btn:*ave |
type=btn, name ends with "ave" | btn:Save |
btn:*av* |
type=btn, name contains "av" | btn:Save, btn:Navigate |
btn:OK:0 |
exact type + name + ordinal | first btn:OK among siblings |
The * character is a glob wildcard matching zero or more characters. It can be used in both the type and name parts.
Path Operators¶
| Operator | Meaning | Example |
|---|---|---|
/ |
direct child (next segment) | win:*/frame:* -- frame directly in a window |
// |
descendant (zero or more segments between) | win:Main//btn:OK -- btn:OK anywhere under window Main |
A query starting with // means "match anywhere from root". A query starting with a segment (no leading //) is anchored to the root of the stable ID path.
Grammar¶
query := '//' segPattern ( ('/' | '//') segPattern )*
| segPattern ( ('/' | '//') segPattern )*
segPattern := typePattern ':' namePattern (':' ordinal)?
| '*'
typePattern := '*' | literal | glob
namePattern := '*' | literal | glob
glob := characters with '*' as wildcard
literal := characters (escaped \: \/ \\ honored)
ordinal := non-negative integer
Query Examples¶
Finding widgets by type¶
| Query | Meaning |
|---|---|
//btn:* |
All buttons, anywhere in the widget tree |
//fill:* |
All fill-in fields |
//alert:* |
Any alert dialog |
//combo:* |
Any combo box |
//menu:* |
Any menu bar |
//sub:* |
All submenus |
//item:* |
All menu items |
Scoped queries¶
| Query | Meaning |
|---|---|
win:*/frame:* |
Any frame directly inside any window |
win:Menu & Form Demo/frame:fmain/btn:* |
All buttons directly in frame fmain |
//frame:fmain/fill:* |
All fill-ins directly in any frame named fmain |
//sub:mFile/item:* |
All items directly in submenu mFile |
win:Settings//toggle:* |
All toggles anywhere under the Settings window |
Exact match¶
| Query | Meaning |
|---|---|
win:Menu & Form Demo/frame:fmain/btn:Greet |
Exact full-path match |
win:Menu & Form Demo/alert:About/btn:OK |
The OK button in the About alert |
Glob patterns in names¶
| Query | Meaning |
|---|---|
//btn:Save* |
Buttons whose name starts with "Save" |
//btn:*eet |
Buttons whose name ends with "eet" |
//frame:f* |
Frames whose name starts with "f" |
//*:Name |
Any widget type named "Name" |
Descendant chains¶
| Query | Meaning |
|---|---|
win:Menu & Form Demo//* |
Everything under this window |
//menu://item:* |
All items anywhere under any menu bar |
Ordinals¶
| Query | Meaning |
|---|---|
//btn:Save:1 |
The second button named "Save" (0-indexed) |
Usage in Lyra Test Scripts¶
The query functions are available via the standard require at the top of every generated test:
const { isWidgetVisible, queryVisibleWidgets, getAllVisibleWidgets, countVisibleWidgets,
/* ...other helpers... */ } = require('./lyra-test-helpers-1.1.0.js');
Checking if a widget is visible¶
// Wait for app to load
await waitForP2JReady();
// Is the main window up?
const mainVisible = await isWidgetVisible('win:Menu & Form Demo');
// Is there an alert on screen?
const hasAlert = await isWidgetVisible('//alert:*');
// Is a specific button present?
const okVisible = await isWidgetVisible('win:Menu & Form Demo/alert:About/btn:OK');
Querying visible widgets¶
// Find all buttons in the main frame
const buttons = await queryVisibleWidgets('//frame:fmain/btn:*');
// buttons = ['win:Menu & Form Demo/frame:fmain/btn:Greet', 'win:Menu & Form Demo/frame:fmain/btn:Save', ...]
// Find all fill-in fields
const fillIns = await queryVisibleWidgets('//fill:*');
// Find everything under the Settings window
const settingsWidgets = await queryVisibleWidgets('win:Settings//*');
Counting widgets¶
// How many buttons are visible?
const buttonCount = await countVisibleWidgets('//btn:*');
// Total visible widgets
const totalCount = await countVisibleWidgets();
Listing all visible widgets (debugging)¶
const all = await getAllVisibleWidgets();
all.forEach(sid => log(' ' + sid));
Matching Rules¶
- Anchored queries (no leading
//) must match from the root of the stable ID path.win:*/frame:*only matches 2-segment IDs whose first segment is a window and second is a frame. - Descendant queries (leading
//) can match at any depth.//btn:OKmatchesbtn:OKregardless of how many parent segments precede it. - The query must consume the entire stable ID.
//alert:*matcheswin:Main/alert:Aboutbut notwin:Main/alert:About/btn:OK-- the button has segments after the alert. - Case-sensitive.
//BTN:OKdoes not matchbtn:OK. - Glob wildcards (
*) match zero or more characters within a single type or name. They do not cross segment boundaries. - Escaped characters. Colons in widget names appear as
\:in stable IDs (e.g.,lbl:Name\:). Use the same escaping in query patterns.
Load Testing¶
Lyra features a robust load testing engine capable of simulating high user traffic. Load tests are defined using Load Profiles (JSON files) that dictate how many virtual users are active over time.
To run a load test:
lyra play my-test.js --load-profile profiles/my-load-profile.json
Virtual Users¶
In a load test, Lyra spawns multiple Virtual Users (VUs).
- Process Isolation: Each VU is a separate Node.js process running your test script.
- Independent Browser: In headed mode, each VU opens its own browser window. In headless mode, they run invisibly in the background.
- Logging: Each VU writes its own log file (
user_1.log,user_2.log, etc.) in the artifact directory, allowing you to debug specific failures in individual user sessions.
Data Sets¶
Lyra allows test scripts to use dynamic input data based on the current Virtual User (VU) context. This prevents race conditions and enables realistic load testing.
These functions are provided by the generated lyra-test-helpers.js library and are designed to be used within your generated test scripts.
Loading Datasets:
// Load from a JSON file
loadDataset('users', 'users.json');
// Load from a JSON file with 'random' strategy
loadDataset('cities', 'cities.json', { strategy: 'random' });
// Load from an inline array
loadDataset('roles', ['admin', 'editor', 'viewer']);
Using Data:
// Get data for the current VU
const user = getData('users');
const city = getData('cities');
// Use in test
log(`Logging in as ${user.username}`);
Strategies:
- Sequence (Default): Assigns data based on the
LYRA_USER_IDenvironment variable (round-robin).- User 0 -> Index 0
- User 1 -> Index 1
- User N -> Index
N % length
- Random: Returns a random item from the dataset for every call.
Load Profile Types¶
You can configure several types of load patterns to simulate real-world scenarios.
1. Constant Load¶
Simulates a stable number of users for a fixed period. Good for baseline performance testing.
{
"name": "Steady State",
"profile": {
"type": "constant",
"users": 20,
"duration": "5m"
},
"execution": { "mode": "loop" }
}
2. Ramp Up¶
Gradually adds users to the system, allowing you to find the "breaking point" or measure scalability.
{
"profile": {
"type": "ramp-up",
"from": 0,
"to": 100,
"duration": "2m"
}
}
3. Ramp Down¶
Gradually removes users. Useful for testing safe shutdown or recovery.
{
"profile": {
"type": "ramp-down",
"from": 100,
"to": 0,
"duration": "1m"
}
}
4. Spike¶
Instantly jumps to a high user count, holds it, and then drops off. detailed tests sudden bursts of traffic.
{
"profile": {
"type": "spike",
"users": 500,
"duration": "30s"
}
}
5. Step¶
Increases load in defined increments (steps).
{
"profile": {
"type": "step",
"steps": [
{ "users": 10, "duration": "1m" },
{ "users": 50, "duration": "1m" },
{ "users": 100, "duration": "1m" }
]
}
}
6. Combined¶
Chains multiple other profile types together for complex scenarios (e.g., Ramp Up -> Constant -> Ramp Down).
{
"profile": {
"type": "combined",
"phases": [
{ "type": "ramp-up", "from": 0, "to": 50, "duration": "1m" },
{ "type": "constant", "users": 50, "duration": "10m" },
{ "type": "ramp-down", "from": 50, "to": 0, "duration": "1m" }
]
}
}
Execution Configuration¶
The execution section of the profile controls how users behave:
- mode:
single: Run the test script once and stop.loop: Run the test script repeatedly until the profile duration ends.
- thinkTime: (Loop mode only) delay between iterations (e.g.,
"2s","500ms").
Failure Thresholds¶
You can configure the test to abort automatically, if error rates get too high, saving time and resources.
"failureThreshold": {
"maxFailureRate": 0.2, // Abort if > 20% of runs fail
"windowSize": "30s" // Measured over a sliding 30-second window
}
Reporting¶
After a load test, Lyra generates detailed reports:
- Console Real-time: Shows active users, throughput (ops/sec), and current failure rate.
- HTML Report: An interactive dashboard with graphs for response times, concurrency, and error rates.
- JSON Report: Raw data for integration with other analysis tools.
Lyra Architecture Overview¶
Lyra Recorder¶
Records user interactions with a FWD web application and generates a standalone Playwright test script.
┌─────────────────────────────────────────────────────────────────────────┐
│ Lyra CLI (Node.js process) │
│ │
│ ┌───────────────┐ ┌──────────────┐ ┌───────────────────────────┐ │
│ │ cli.ts │──▶│ recorder/ │──▶│ codeGenerator.ts │ │
│ │ (commander) │ │ index.ts │ │ Converts captured events │ │
│ └───────────────┘ │ │ │ into a .js test file + │ │
│ │ Orchestrates│ │ lyra-test-helpers.js │ │
│ │ recording │ └──────────┬────────────────┘ │
│ └──────┬───────┘ │ │
│ │ ▼ │
│ │ ┌──────────────┐ │
│ │ │ my-test.js │ (output) │
│ │ │ helpers.js │ │
│ │ └──────────────┘ │
│ │ │
│ │ Playwright API │
│ │ (launch, goto, │
│ │ exposeFunction, │
│ │ addInitScript) │
│ │ │
└─────────────────────────────┬───────────────────────────────────────────┘
│
CDP / Playwright Wire Protocol
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Browser (Firefox / Chromium / WebKit) │
│ │
│ ┌─ Main Page ────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌───────────────────────┐ window.postMessage ┌────────────┐ │ │
│ │ │ Control Panel │◀════════════════════════▶│ Custom │ │ │
│ │ │ (popup window) │ │ Event │ │ │
│ │ │ - event log │ │ Input │ │ │
│ │ │ - pause/resume │ │ (popup) │ │ │
│ │ │ - delete events │ └────────────┘ │ │
│ │ │ - insert custom code │ │ │
│ │ └───────────────────────┘ │ │
│ │ │ │
│ │ ┌─ Injected Scripts (via addInitScript) ────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ eventCapture.ts │ │ │
│ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Listens on window for: │ │ │ │
│ │ │ │ - keydown / keyup (keyboard events) │ │ │ │
│ │ │ │ - click / dblclick / mousedown / mouseup (mouse) │ │ │ │
│ │ │ │ - input / compositionstart / compositionend (IME) │ │ │ │
│ │ │ │ - focus / blur (input tracking) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ Resolves P2J stable IDs for widget identification │ │ │ │
│ │ │ │ Calls window.__lyra_recordEvent(JSON) | | | |
│ │ │ └──────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ controlPanel.ts — launches & syncs control panel popup │ │ │
│ │ │ selectionOverlay.ts — visual region selection for assertions │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─ FWD Application (iframe #p2j-main) ──────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ P2J Web GUI client (canvas-based) │ │ │
│ │ │ - p2j.screen.getHoveredWindowXy() │ │ │
│ │ │ - p2j.screen.getStableIdForWidgetId() │ │ │
│ │ │ - p2j.screen.getWidgetIdAt() │ │ │
│ │ │ │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
▲ │
│ Playwright exposeFunction bridge │
└───────────────────────────────────────────────────────────────────┘
(browser→Node.js function call)
Recorder Data Flow¶
User interacts ──▶ Browser DOM event
│
▼
eventCapture.ts
(in-browser JS)
│
│ Resolves CSS selector + P2J stable ID
│ Builds event JSON
│
▼
window.__lyra_recordEvent(json)
│
│ Playwright exposeFunction
│ (browser ──▶ Node.js IPC)
│
▼
recorder/index.ts
(Node.js process)
│
│ Appends to events[]
│ Pushes to control panel
│
▼
┌─── Ctrl+C / close browser ────┐
│ │
▼ │
codeGenerator.ts │
│ │
│ Generates standalone .js │
│ with Playwright calls │
│ │
▼ │
my-test.js + lyra-test-helpers.js │
(output files on disk) │
└───────────────────────────────┘
Lyra Player — Single Execution¶
Replays a recorded test by spawning the generated script as a child process.
┌───────────────────────────────────────────────────────────────┐
│ Lyra CLI (Node.js process) │
│ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ cli.ts │────▶│ player/index.ts │ │
│ └──────────┘ │ │ │
│ │ spawn('node', │ │
│ │ [testFile]) │ │
│ └────────┬─────────┘ │
│ │ │
│ env: LYRA_HEADLESS, LYRA_BROWSER, │
│ LYRA_SKIP_WAITS, LYRA_LOG_DIR, ... │
│ │ │
│ child_process.spawn │
│ │ │
│ ┌────────▼─────────┐ │
│ │ stdout / stderr │──▶ console + log file │
│ └──────────────────┘ │
└───────────────────────────┬───────────────────────────────────┘
│
│ Node.js child process
▼
┌───────────────────────────────────────────────────────────────┐
│ my-test.js (child Node.js process) │
│ │
│ const { firefox } = require('playwright'); │
│ │
│ 1. Launch browser │
│ 2. Navigate to URL │
│ 3. Wait for P2J input ready │
│ 4. Replay events via page.evaluate() / dispatchEvent() │
│ 5. Assert screenshots / timers │
│ 6. Close browser │
│ │
│ Uses: lyra-test-helpers.js │
│ (waitForInputReady, startTimer, endTimer, loadDataset, ...) │
└───────────────────────────┬───────────────────────────────────┘
│
CDP / Playwright Wire Protocol
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Browser (Firefox / Chromium / WebKit) │
│ │
│ ┌─ FWD Application ───────────────────────────────────────┐ │
│ │ │ │
│ │ P2J Web GUI client processes events as if user-driven │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
Lyra Player — Load Testing (Multi-Process)¶
The orchestrator spawns multiple virtual users, each running the test script in its own process and browser instance.
┌────────────────────────────────────────────────────────────────────────────┐
│ Lyra CLI (Node.js process — Orchestrator) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ cli.ts │───▶│ player/ │───▶│ scheduler.ts│───▶│ orchestrator │ │
│ └──────────┘ │ index.ts │ │ │ │ .ts │ │
│ └──────────────┘ │ Calculates │ │ │ │
│ │ start/stop │ │ Spawns and │ │
│ │ times per │ │ manages VU │ │
│ │ load profile│ │ processes │ │
│ └─────────────┘ └──────┬───────┘ │
│ │ │
│ ┌─────────────┐ ┌──────────────────┐ │ │
│ │ metrics.ts │◀───│ Console Reporter │ child_process.spawn │
│ │ (aggregate) │ │ (live dashboard) │ (per virtual user) │
│ └──────┬──────┘ └──────────────────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────────────┐ │ │
│ │ report.json │ │ │
│ │ report.html │ │ │
│ │ user_N.log (per VU) │ │ │
│ └──────────────────────┘ │ │
└────────────────────────────────────────────────────────────┬───┘───────────┘
│
┌────────────────────────────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌───────────┬──────┐
│ VU 0 (Node.js) │ │ VU 1 (Node.js) │ │ VU N (Node.js) │
│ my-test.js │ │ my-test.js │ │ my-test.js │
│ │ │ │ │ │
│ env: │ │ env: │ │ env: │
│ LYRA_USER_ID=0 │ │ LYRA_USER_ID=1 │ │ LYRA_USER_ID=N │
│ LYRA_EXEC_MODE= │ │ LYRA_EXEC_MODE= │ │ ... │
│ loop │ │ loop │ │ │
│ LYRA_THINK_TIME=1s │ │ LYRA_THINK_TIME=1s │ │ │
└──────────┬───────────┘ └──────────┬───────────┘ └────────┬─────────┘
│ │ │
Playwright Wire Protocol Playwright Wire Protocol Playwright Wire Protocol
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Browser 0 │ │ Browser 1 │ │ Browser N │
│ (separate │ │ (separate │ │ (separate │
│ instance) │ │ instance) │ │ instance) │
│ │ │ │ │ │
│ FWD App │ │ FWD App │ │ FWD App │
└──────────────┘ └──────────────┘ └──────────────┘
Load Test Lifecycle¶
Load Profile JSON Scheduler Orchestrator
───────────────── ────────────────────────── ────────────────────────
{ For each VU, calculate: Spawn VU at startTime
type: "ramp-up", - startTime Kill VU at stopTime
from: 1, - stopTime Respawn on exit (loop)
to: 10, based on profile type Collect exit codes
duration: "30s" and duration Record metrics
} Monitor failure rate
│ │ │
└────────▶────────────────┘────────────▶───────────────┘
│
▼
┌─────────────────────┐
│ Reports │
│ - HTML dashboard │
│ - JSON data │
│ - Console summary │
│ - Timer stats │
└─────────────────────┘
Key Protocols & Communication¶
| Connection | Protocol / Mechanism | Direction |
|---|---|---|
| Lyra CLI ↔ Browser | CDP (Chrome DevTools Protocol) via Playwright | Bidirectional |
| Injected JS → Node.js | page.exposeFunction() (Playwright bridge) |
Browser → Node |
| Node.js → Injected JS | page.evaluate() / page.addInitScript() |
Node → Browser |
| Control Panel ↔ Page | window.postMessage + window.open() |
Bidirectional |
| Orchestrator → VU processes | child_process.spawn + env vars |
Parent → Child |
| VU processes → Orchestrator | Exit codes + stdout/stderr pipes + timer JSON files | Child → Parent |
| Generated test → Browser | Playwright API (page.evaluate, dispatchEvent) |
Node → Browser |
© 2026 Golden Code Development Corporation. ALL RIGHTS RESERVED.