Project

General

Profile

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 - If true Lyra is enabled server side. This is mandatory to activate Lyra.
  • debug - Enables server-side debug instrumentation. Only to be used for debug purposes - set to true to activate. When enabled, the generated tests include snapshotInfo with 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:

  1. Install Lyra:
    sudo npm install -g path/to/fwd-lyra-1.0.0.tgz
  2. Install Browsers & Dependencies:
    npx playwright install-deps && npx playwright install firefox
  3. Record a Test:
    lyra record --url https://your-server.com/app --output my-test.js
  4. 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:

  1. Start Recording: Launch Lyra with the target URL.
  2. Perform Actions: Interact with the application (click, type, navigate) to perform the workflow you want to test.
  3. Pause at Key Points: When you reach a critical state (e.g., after logging in, or after a search results load), Pause the recording.
  4. Add Assertions: While paused, use the Selection Assert tool to verify the UI state. This ensures the test checks for correctness, not just completion.
  5. 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.
  6. Resume: Continue recording the next steps.
  7. 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 the lyra-test-helpers.js file 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.

  1. Click the 🎯 icon.
  2. Click and drag to draw a box around a specific part of the UI (e.g., a form, a table, or a specific button).
  3. 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.

  1. Press Insert key (to create a custom event).
  2. Type startTimer('My Transaction').
  3. Perform the actions you want to measure (e.g., clicking 'Save').
  4. Press Insert key again.
  5. Type endTimer('My Transaction').
  6. 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., btn for Button, txt for 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 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 the captures/ 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-threshold is used and an operation takes longer than the limit you defined in the endTimer call.

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

  1. 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.
  2. Descendant queries (leading //) can match at any depth. //btn:OK matches btn:OK regardless of how many parent segments precede it.
  3. The query must consume the entire stable ID. //alert:* matches win:Main/alert:About but not win:Main/alert:About/btn:OK -- the button has segments after the alert.
  4. Case-sensitive. //BTN:OK does not match btn:OK.
  5. Glob wildcards (*) match zero or more characters within a single type or name. They do not cross segment boundaries.
  6. 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_ID environment 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.