From ignition-scada
Provides Playwright page objects, gateway API helpers, and Perspective DOM conventions for e2e testing Ignition Perspective views. Use when writing or debugging browser tests.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ignition-scada:ignition-e2eThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This project uses Playwright to test Ignition Perspective views in a real browser. Tests run against a live gateway, authenticate through the Perspective login form, and interact with the actual Perspective SPA.
This project uses Playwright to test Ignition Perspective views in a real browser. Tests run against a live gateway, authenticate through the Perspective login form, and interact with the actual Perspective SPA.
Page for Perspective conventions.auth/user.json for reusePerspective is a React SPA served over WebSocket. The DOM has specific conventions you MUST understand:
Every Perspective component has data-component-path using positional indices, NOT the named paths from view.json.
Prefix convention:
L[n] = left dock (e.g., L[0], L[1])T[n] = top dock (e.g., T[0])C = center (page content)$ separates embedded view boundaries: separates child indicesExamples:
C — the root page content containerC:0:1 — second child of first child of page contentC:0$0:2 — embedded view boundary, then third childT[0]:0:1 — top dock, first container, second childL[0] — left dockThe component type attribute: data-component="ia.display.label", data-component="ia.input.button", etc.
page.goto() after initial session open — it creates a new WebSocket session. Use PerspectivePage.openPage(route) instead. Exception: the very first navigation to the session root (e.g., for dock-only tests) may use page.goto().C prefix — use perspective.pageContent() to exclude docks.T[0]) may be visible before page content (C).$ marks the boundary, child indices restart from 0.onDemand — check toBeAttached() not toBeVisible().import { PerspectivePage } from "../pages/PerspectivePage";
// Available methods:
perspective.openPage("/route") // Open a page route (call once per test)
perspective.waitForPageContent(timeout?) // Wait for C-prefixed content to render
perspective.waitForSession(timeout?) // Wait for any [data-component] elements
perspective.pageContent() // Locator scoped to page content (excludes docks)
perspective.componentByType("ia.display.label") // Find by component type in page content
perspective.pageLabelWithText("Title") // Find label with text in page content
perspective.pageText("some text") // Find visible text in page content
perspective.dismissPopups() // Close popup overlays
perspective.dumpComponentPaths() // Debug: list all component paths in DOM
const comp = new PerspectiveComponent(locator, page);
await comp.isVisible(timeout?) // Returns boolean
await comp.waitForVisible(timeout?) // Throws if not visible
await comp.getText() // Text content
ia.input.button)const btn = new Button(locator, page);
await btn.click() // Wait for visible, then click
await btn.isEnabled() // Checks for "ia_button--disabled" class
ia.display.table)const table = new Table(locator, page);
await table.waitForData(timeout?) // Wait for first row to render
await table.getRowCount() // Number of visible rows
await table.clickRow(index) // Click row by index
await table.getCellText(row, column) // Get cell text content
Row selector: .ia_table__body__row
Cell selector: .ia_table__body__cell
Call WebDev endpoints from test code:
import { readTags, writeTags, readTag, writeTag, callScript, isGatewayReachable, mirrorTags, deleteMirror } from "../helpers/gateway-api";
// Tag operations
const values = await readTags(["[WHK01]Path/To/Tag1", "[WHK01]Path/To/Tag2"]);
// Returns: { "[WHK01]Path/To/Tag1": { value: 42, quality: "Good", good: true } }
const val = await readTag("[WHK01]Path/To/Tag"); // Single tag, returns value or null
await writeTag("[default]Test/Tag", 42); // Single write, returns boolean
await writeTags([{ path: "[default]Test/Tag1", value: "hello" }, { path: "[default]Test/Tag2", value: 123 }]);
// Script invocation — call real Jython scripts on the gateway
const result = await callScript("core.mes.changeover.client.get_state", ["cooker"]);
// Returns: { success: true, result: { current_state: "idle", ... } }
// Tag mirroring — clone OPC tags to memory for testing without PLC
await mirrorTags("[WHK01]Distillery01/Mashing01", "[WHK01]Distillery01/Mashing01_MEM");
// ... run tests against memory tags ...
await deleteMirror("[WHK01]Distillery01/Mashing01_MEM"); // Cleanup
// Health check
const reachable = await isGatewayReachable(); // Quick connectivity check
The fixtures/auth.setup.ts handles authentication:
/data/perspective/client/<PROJECT>input.username-field) or live session ([data-component])IGNITION_USER/IGNITION_PASSWORD env vars.auth/user.json for reuse across testsIf auth fails: Run cd e2e && npx playwright test --project=setup to re-authenticate.
Use the perspective fixture instead of raw page:
import { test, expect } from "../fixtures/perspective";
test("my test", async ({ perspective }) => {
await perspective.openPage("/my-page");
const title = perspective.pageLabelWithText("My Title");
await expect(title.first()).toBeVisible();
});
This auto-wraps page in a PerspectivePage instance.
import { test, expect } from "../../fixtures/perspective";
test.describe("My View smoke tests", () => {
test("page loads with expected title", async ({ perspective }) => {
await perspective.openPage("/my-view");
const title = perspective.pageLabelWithText("Expected Title");
await expect(title.first()).toBeVisible({ timeout: 15_000 });
});
test("table renders with data", async ({ perspective }) => {
await perspective.openPage("/my-view");
const table = perspective.componentByType("ia.display.table");
await expect(table.first()).toBeVisible();
// Check for data rows
await expect(table.locator(".ia_table__body__row").first()).toBeVisible({ timeout: 10_000 });
});
});
import { test, expect } from "../../fixtures/perspective";
import { writeTag, readTag, callScript } from "../../helpers/gateway-api";
test.describe("Changeover integration", () => {
test.beforeAll(async () => {
// Set up test state via gateway API
await writeTag("[default]Test/State", "idle");
});
test.afterAll(async () => {
// Cleanup
await writeTag("[default]Test/State", "");
});
test("state change reflects in UI", async ({ perspective }) => {
await perspective.openPage("/changeover");
// Trigger state change via script
await callScript("core.mes.changeover.client.transition", ["cooker", "start"]);
// Verify UI updates
const label = perspective.pageText("running");
await expect(label).toBeVisible({ timeout: 10_000 });
});
});
test("top dock shows plant data", async ({ perspective }) => {
// Don't use openPage for dock-only tests — just navigate to session root
await perspective.page.goto(`/data/perspective/client/${process.env.PERSPECTIVE_PROJECT}`);
await perspective.waitForSession();
const topDock = perspective.page.locator("[data-component-path^='T[0]']");
await expect(topDock.first()).toBeVisible();
});
test("left dock menu exists", async ({ perspective }) => {
await perspective.openPage("/some-page");
// Left dock is onDemand — check attached, not visible
const menu = perspective.page.locator("[data-component='ia.navigation.menutree']");
await expect(menu).toBeAttached({ timeout: 10_000 });
});
cd e2e
# All tests
npx playwright test
# Specific area
npx playwright test tests/changeover/
# Smoke tests only
npx playwright test tests/smoke/
# Headed mode (see the browser)
npx playwright test --headed
# Single test file
npx playwright test tests/smoke/perspective-loads.spec.ts
# View HTML report after failures
npx playwright show-report
Set in e2e/.env:
| Variable | Purpose | Example |
|---|---|---|
IGNITION_URL | Gateway base URL | https://localhost:9043 |
IGNITION_USER | Login username | admin |
IGNITION_PASSWORD | Login password | password |
PERSPECTIVE_PROJECT | Perspective project name | QSI_WhiskeyHouseKentucky01 |
TAG_PROVIDER | Default tag provider | WHK01 |
page.goto() mid-test. This kills the WebSocket session. Navigate within Perspective using component interactions or openPage() for the initial load.table.waitForData() or wait for .ia_table__body__row specifically.perspective.dismissPopups() after openPage() if needed.npx claudepluginhub thethoughtagen/ignition-ide-plugins --plugin ignition-scadaGuides creating page objects and refactoring Playwright tests using Page Object Model patterns for maintainability, reusability, and scalability. Covers locators, principles, and TypeScript examples.
Guides Playwright end-to-end testing: selectors, assertions, fixtures, auth, parallelism, CI, visual regression, and flake hunting. Activate with playwright/e2e/playwright config topics.
Write Playwright E2E tests using fixtures and best practices. Use when creating E2E tests, writing browser automation tests, or testing user flows.