Testing
Webswing applications can be tested with the following tools:
- Webswing Test Tool for recording, replaying, and exporting GUI tests. Included in SLA Standard and Professional.
- Selenium Plugin for Selenium browser automation.
- Cypress Plugin for Cypress browser automation.
- Playwright Plugin for Playwright browser automation.
- JMeter Plugin for functional and performance testing with JMeter.
- QF-Test for advanced GUI testing. Licensed separately.
Browser automation for Webswing applications
Selenium, Cypress, and Playwright can test the HTML page surrounding a Webswing application in the usual way. The Swing or JavaFX application itself requires a different approach because Webswing renders it on an HTML canvas. A Swing button, text field, or table cell therefore does not exist as a separate DOM element and cannot be found with an ordinary CSS selector, Selenium findElement(), Cypress cy.get(), or Playwright locator().
Webswing Test Mode solves this by exposing the application component tree. A test uses a Webswing Query Syntax expression to locate a component, receives its current properties and on-screen coordinates, and then performs the action on the canvas.
The recommended workflow is:
- Enable Test Mode for the application.
- Use Pick in Webswing Test Tool to identify a component and generate its Query Syntax.
- Evaluate the query and make sure it identifies the intended component uniquely.
- Use the query with the Webswing helper for Selenium, Cypress, or Playwright.
- Assert the component state or perform an action such as click, type, clear, or drag.
Enable Test Mode
In the tested application's configuration, set Enable Test Mode to ON. The corresponding JSON property is:
"testMode": true
Test Mode exposes the component-tree APIs required by selector-based tests. Reload the application after changing this setting.
Find a component with Pick
Webswing Test Tool provides the easiest way to discover a stable query without inspecting the complete component tree manually:
- Open a recording and start recording the Webswing application.
- Select Create Assertion.
- In the Create Assertion dialog, select Pick.
- Move the pointer over the application. The component under the pointer is highlighted.
- Select the required component. The dialog reopens with the generated expression in Query Syntax.
- Select Evaluate. The query is valid for a single-component assertion when exactly one component matches.
- Edit and evaluate the expression again if no component or multiple components match.
- Select Add Assertion to store the query in the recording.
The same Pick and Evaluate actions are available when editing an assertion. Test Tool starts with stable component properties and generates the shortest query that uniquely identifies the selected component. If those properties are not sufficient, the generated query can include a parent component or :nth-child(...).
Once the recording is complete, Test Tool can replay it directly or export it for Selenium, Cypress, Playwright, or JMeter. See the Test Tool documentation for recording, replay, and export instructions.
Export a recording
Open a recording, select Download Test Case, and choose Selenium, Cypress, Playwright, or JMeter. The dialog preloads values from the recording and shows the fields used by the selected generator:
| Generator | Required export parameters | Generated file |
|---|---|---|
| Selenium | Webswing application URL, login username, login password, timeout | name-selenium.js |
| Cypress | Webswing application URL, login username, login password, timeout | name-cypress.spec.js |
| Playwright | Webswing application URL, login username, login password, startup timeout | name-playwright.spec.js |
| JMeter | WebSocket URL, timeout, execution delay, JWT secret | name-jmeter.jmx |
The Playwright output is a JavaScript test for Playwright Test. It imports the downloaded webswing-playwright-plugin.js helper from the same directory, uses ordinary Playwright locators to open and authenticate to Webswing, waits for the Webswing canvas for the configured startup timeout, and then calls the helper's webswingClick(), webswingType(), and webswingShouldExist() functions for recorded actions and assertions. Playwright closes its page fixture and browser resources after the test.
To prepare a project for the generated Playwright spec, install Playwright Test and Chromium:
npm install --save-dev @playwright/test
npx playwright install chromium
Place webswing-playwright-plugin.js beside the generated .spec.js file and run it with npx playwright test. Enable Test Mode on the target Webswing application before running any generated browser-automation test. Generated Selenium, Cypress, and Playwright files contain the configured login credentials; generated JMeter plans contain the JWT secret, so handle them as sensitive files.
Webswing Query Syntax
Webswing Query Syntax and the requestComponentTreeElement() API are available since Webswing 26.2. This is Webswing's own CSS-like syntax; it is not JSONata and does not require the JSONata library.
A query consists of a component type followed by optional property filters:
JButton[value="Save"]
JTextField[name="searchField"]
JDialog[value="Confirm"] > JButton[value="OK"]
JTable[columnheader="Name"][rownumber="1"]
Prefer properties that remain stable between test runs, such as component type, name, value, columnheader, and rownumber. Avoid coordinates and dimensions unless necessary because they can change with the window size or application layout.
Attribute operators
| Operator | Meaning | Example |
|---|---|---|
= |
equals | JButton[value="Save"] |
!= |
does not equal | JButton[enabled!="false"] |
*= |
contains | JLabel[value*="Welcome"] |
^= |
starts with | JButton[name^="save"] |
$= |
ends with | JButton[name$="Button"] |
>, <, >=, <= |
numeric comparison | JTable[rownumber>="1"] |
Backslashes, double quotes, and closing square brackets inside an attribute value must be escaped with a backslash.
Relationships and state
| Syntax | Meaning | Example |
|---|---|---|
| space | descendant | JDialog JButton |
> |
direct child | JDialog > JButton |
+ |
adjacent sibling | JLabel + JTextField |
~ |
following sibling | JLabel ~ JTextField |
:first-child |
first child | JButton:first-child |
:last-child |
last child | JButton:last-child |
:nth-child(n) |
child at a one-based position | JButton:nth-child(2) |
:enabled, :disabled |
enabled state | JButton:enabled |
:visible, :hidden |
Swing visibility state | JPanel:visible |
Single-component assertions and actions normally require exactly one match. A broader query can be used by a count assertion when multiple matches are intentional.
How component lookup works
requestComponentTreeElement(query) sends the query to the running Webswing application. Webswing builds the current Swing and JavaFX component tree, evaluates the query on the application side, and returns only the matching components. The complete tree does not have to be transferred to and searched in the browser.
The result is a promise containing an array:
window.webswingInstance0
.requestComponentTreeElement('JButton[name="saveButton"]')
.then((components) => console.log(components))
Each match contains information used by the test helpers, including:
| Property | Purpose |
|---|---|
componentType, name, value |
identifies the component and its displayed value |
enabled, visible, selected |
describes component state |
screenX, screenY, width, height |
locates the component on the canvas |
visibleScreenX, visibleScreenY, visibleWidth, visibleHeight |
describes its effective on-screen area after clipping |
columnHeader, rowNumber |
identifies table cells |
The Webswing browser-automation helpers perform a fresh lookup, require the expected number of matches, and use the returned coordinates to interact with the canvas. This lets a test use component identities instead of hard-coded mouse coordinates.
For complete query syntax documentation, see JMeter Query Syntax.
Framework examples
The examples assume that Test Mode is enabled, authentication is complete, the application has started, and the helper file is available in the test project. The default instance name is webswingInstance0; configure the helper when an embedded application uses a different name.
For Selenium, Cypress, and Playwright, use ordinary DOM selectors for the login page, embedding page, and other HTML controls. Use Webswing Query Syntax only for Swing and JavaFX components rendered on the canvas.
Selenium
Use the Webswing Selenium Plugin with a Selenium WebDriver. Pass the driver, followed by the Webswing query, to each helper function:
const { Builder } = require('selenium-webdriver')
const webswing = require('./webswing-selenium-plugin')
async function run() {
const driver = await new Builder().forBrowser('chrome').build()
try {
await driver.get('http://localhost:8080/webswing-demo/')
// Complete login and wait for the Webswing application to start.
await webswing.webswingShouldVisible(driver, 'JTextField[name="username"]')
await webswing.webswingType(driver, 'JTextField[name="username"]', 'alice')
await webswing.webswingClick(driver, 'JButton[name="saveButton"]')
await webswing.webswingCheckValue(driver, 'JTextField[name="username"]', 'alice')
} finally {
await driver.quit()
}
}
run().catch(console.error)
Cypress
The Webswing Cypress Plugin registers commands that begin with cy.webswingGet(query). Chain an assertion or action from each fresh lookup:
describe('Webswing form', () => {
it('enters and saves a value', () => {
cy.visit('http://localhost:8080/webswing-demo/')
// Complete login and wait for the Webswing application to start.
cy.webswingGet('JTextField[name="username"]')
.webswingShouldVisible()
.webswingType('alice')
cy.webswingGet('JButton[name="saveButton"]')
.webswingClick()
cy.webswingGet('JTextField[name="username"]')
.webswingCheckValue('alice')
})
})
Playwright
Use the Webswing Playwright Plugin, distributed as playwright-plugin/webswing-playwright-plugin.js. The helper accepts a Playwright page, followed by the Webswing query:
const { test } = require('@playwright/test')
const webswing = require('./webswing-playwright-plugin')
test('enters and saves a value', async ({ page }) => {
await page.goto('http://localhost:8080/webswing-demo/')
// Complete login and wait for the Webswing application to start.
await webswing.webswingShouldVisible(page, 'JTextField[name="username"]')
await webswing.webswingType(page, 'JTextField[name="username"]', 'alice')
await webswing.webswingClick(page, 'JButton[name="saveButton"]')
await webswing.webswingCheckValue(page, 'JTextField[name="username"]', 'alice')
})
Common problems
requestComponentTreeElement is not available: enable Test Mode and reload the application.- The Webswing instance is not available: wait for application initialization or configure the correct instance name.
- No component matches: use Pick again on the current application screen and check properties that may have changed.
- Multiple components match: add a stable attribute or parent relationship until the query is unique.
- The component exists but cannot be clicked: verify that it is enabled and has an effective visible area; it may be clipped by a scroll pane or outside the canvas.
- A selector is unreliable between runs: prefer a component
namesupplied by the application over text, position, dimensions, or:nth-child(...).
Earlier lookup approaches
Before Webswing 26.2, custom integrations commonly called requestComponentTree(), transferred the complete component tree to the browser, and searched it with a separate library such as JSONata. Some integrations also created temporary HTML overlays over the canvas. These approaches remain relevant to older Webswing versions, but new tests for Webswing 26.2 and later should use Webswing Query Syntax with requestComponentTreeElement() or the framework helpers described above.
Selenium
const { Builder, By, Key } = require('selenium-webdriver');
const jsonata = require('./jsonata-min');
(async function example() {
const driver = await new Builder().forBrowser('chrome').build();
try {
//await driver.get('http://localhost:8080/webswing-server/webswing-demo');
await driver.get('http://localhost:8080/webswing-demo');
await new Promise(resolve => setTimeout(resolve, 2500));
const usernameInput = await driver.findElement(By.name('username'));
await usernameInput.sendKeys('admin');
const passwordInput = await driver.findElement(By.name('password'));
await passwordInput.sendKeys('pwd');
const loginButton = await driver.findElement(By.className('ws-btn-submit'));
await loginButton.click();
await new Promise(resolve => setTimeout(resolve, 5000));
const webswingCanvas = await driver.findElement(By.className('webswing-canvas accessibility-off'));
if (webswingCanvas === null) {
console.log('++++++++++ERROR WHILE START WEBSWING DEMO');
} else {
await webswingClick(driver, '**[value="Webswing API" and componentType="JLabel"]');
await new Promise(resolve => setTimeout(resolve, 2500));
await webswingClick(driver, '**[value="Search" and componentType="JTextField" and name="Search"]');
await new Promise(resolve => setTimeout(resolve, 2500));
await driver.actions().sendKeys('latency').perform();
await new Promise(resolve => setTimeout(resolve, 2500));
await driver.actions().sendKeys(Key.RETURN).perform();
await new Promise(resolve => setTimeout(resolve, 2500));
for (let i = 0; i < 7; i++) {
await driver.actions().sendKeys(Key.BACK_SPACE).perform();
await new Promise(resolve => setTimeout(resolve, 1000));
}
await new Promise(resolve => setTimeout(resolve, 2500));
await driver.actions().sendKeys('user').perform();
await driver.actions().sendKeys(Key.RETURN).perform();
await new Promise(resolve => setTimeout(resolve, 2500));
await webswingExist(driver, '**[value="user" and componentType="JTextField" and name="Search"]');
await webswingCheckValue(driver, '**[value="user" and componentType="JTextField" and name="Search"]', 'user');
await webswingMove(driver, '**[componentType="ScrollBar" and height=322]');
await new Promise(resolve => setTimeout(resolve, 1000));
await webswingClick(driver, '**[componentType="WJMenu" and value="File"]');
await new Promise(resolve => setTimeout(resolve, 1000));
await webswingClick(driver, '**[componentType="WJMenuItem" and value="Exit"]');
console.log('++++++++++Complete.');
}
} catch (e) {
console.log('++++++++++ERROR');
console.log(e);
} finally {
console.log('++++++++++FINALLY QUIT DRIVER');
await driver.quit();
}
})();
async function webswingGet(driver, selector) {
const tree = await driver.executeAsyncScript(
function (done) {
window.webswingInstance0.requestComponentTree().then((tree) => done(tree)).catch((err) => done({ error: err.message }));
}
);
if (tree.error) {
throw new Error('ComponentTree fetch failed: ' + tree.error);
} else {
return jsonata(selector).evaluate(tree).then((element) => {
return element;
});
}
}
async function webswingExist(driver, selector) {
const element = await webswingGet(driver, selector);
if (element === undefined) {
throw new Error('Element for selector ' + selector + ' does not exist.');
} else {
return element;
}
}
async function webswingClick(driver, selector) {
const element = await webswingExist(driver, selector);
await driver.actions().move({ origin: undefined, x: parseInt(element.screenX) + 10, y: parseInt(element.screenY) + 10 }).click().perform();
}
async function webswingCheckValue(driver, selector, inputValue) {
const element = await webswingExist(driver, selector);
if (element.value === inputValue) {
console.log('++++++++++Value ' + inputValue + ' match.');
} else {
throw new Error('Input value ' + inputValue + ' does not match to current element value ' + element.value);
}
}
async function webswingMove(driver, selector) {
const element = await webswingExist(driver, selector);
await driver.actions().move({ origin: undefined, x: parseInt(element.screenX) + 2, y: parseInt(element.screenY) + 2 }).press().perform();
await driver.actions().move({ origin: undefined, x: parseInt(element.screenX) + 2, y: parseInt(element.screenY) + 102 }).click().perform();
}
Cypress
const jsonata = require('./jsonata-min');
describe('Webswing demo', () => {
it('runs test', () => {
//cy.visit('http://localhost:8080/webswing-server/webswing-demo');
cy.visit('http://localhost:8080/webswing-demo');
cy.wait(2500);
cy.get('[name="username"]').type('admin');
cy.get('[name="password"]').type('pwd');
cy.get('.ws-btn-submit').click();
cy.wait(5000);
cy.get('.webswing-canvas.accessibility-off').should('be.visible');
webswingClick('**[value="Webswing API" and componentType="JLabel"]');
cy.wait(2500);
webswingClick('**[value="Search" and componentType="JTextField" and name="Search"]');
cy.wait(2500);
cy.focused().type('latency');
cy.wait(2500);
cy.focused().type('{enter}');
cy.wait(2500);
for (let i = 0; i < 7; i++) {
cy.focused().type('{backspace}');
cy.wait(100);
}
cy.wait(2500);
cy.focused().type('user');
cy.focused().type('{enter}');
cy.wait(2500);
webswingExist('**[value="user" and componentType="JTextField" and name="Search"]');
webswingCheckValue(
'**[value="user" and componentType="JTextField" and name="Search"]',
'user'
);
webswingMove('**[componentType="ScrollBar" and height=322]');
cy.wait(1000);
webswingClick('**[componentType="WJMenu" and value="File"]');
cy.wait(1000);
webswingClick('**[componentType="WJMenuItem" and value="Exit"]');
});
});
function webswingGet(selector) {
return cy.window({ log: false }).then((win) => {
if (!win.webswingInstance0
|| typeof win.webswingInstance0.requestComponentTree !== 'function') {
throw new Error('window.webswingInstance0.requestComponentTree is not available.');
}
return win.webswingInstance0.requestComponentTree();
}).then((tree) => {
if (tree && tree.error) {
throw new Error('ComponentTree fetch failed: ' + tree.error);
}
return Cypress.Promise.resolve(jsonata(selector).evaluate(tree));
});
}
function webswingExist(selector) {
return webswingGet(selector).then((element) => {
if (element === undefined || element === null) {
throw new Error('Element for selector ' + selector + ' does not exist.');
}
return element;
});
}
function webswingClick(selector) {
return webswingExist(selector).then((element) => {
const x = Number(element.screenX) + 10;
const y = Number(element.screenY) + 10;
if (![x, y].every(Number.isFinite)) {
throw new Error(`Cannot click element for selector ${selector}: invalid component coordinates.`);
}
return cy.get('body').click(x, y);
});
}
function webswingCheckValue(selector, inputValue) {
return webswingExist(selector).then((element) => {
expect(element.value).to.equal(inputValue);
return element;
});
}
function webswingMove(selector) {
return webswingExist(selector).then((element) => {
const startX = Number(element.screenX) + 2;
const startY = Number(element.screenY) + 2;
const endX = startX;
const endY = startY + 100;
if (![startX, startY, endX, endY].every(Number.isFinite)) {
throw new Error(`Cannot move element for selector ${selector}: invalid component coordinates.`);
}
return cy.window({ log: false }).then((win) => {
const startBrowserPoint = toBrowserCoordinates(win, startX, startY);
const endBrowserPoint = toBrowserCoordinates(win, endX, endY);
console.log('[webswingMove] browser drag coordinates', {
selector,
startX,
startY,
endX,
endY,
startBrowserPoint,
endBrowserPoint
});
return cy.wrap(null, { log: false })
.then(() => dispatchMouseEvent('mouseMoved', startBrowserPoint.x, startBrowserPoint.y))
.then(() => dispatchMouseEvent('mousePressed', startBrowserPoint.x, startBrowserPoint.y, 'left', 1))
.then(() => dispatchMouseEvent('mouseMoved', endBrowserPoint.x, endBrowserPoint.y, 'left', 1))
.then(() => dispatchMouseEvent('mouseReleased', endBrowserPoint.x, endBrowserPoint.y, 'left', 0))
.then(() => dispatchMouseEvent('mousePressed', endBrowserPoint.x, endBrowserPoint.y, 'left', 1))
.then(() => dispatchMouseEvent('mouseReleased', endBrowserPoint.x, endBrowserPoint.y, 'left', 0));
});
});
}
function getAutIframeRect(win) {
const autIframe = win.top.document.querySelector('iframe.aut-iframe');
if (!autIframe) {
throw new Error('Cypress AUT iframe not found.');
}
return autIframe.getBoundingClientRect();
}
function toBrowserCoordinates(win, x, y) {
const rect = getAutIframeRect(win);
const scaleX = rect.width / win.innerWidth;
const scaleY = rect.height / win.innerHeight;
return {
x: rect.left + x * scaleX,
y: rect.top + y * scaleY
};
}
function dispatchMouseEvent(type, x, y, button = 'none', buttons = 0) {
return Cypress.automation('remote:debugger:protocol', {
command: 'Input.dispatchMouseEvent',
params: {
type,
x,
y,
button,
buttons,
pointerType: 'mouse'
}
});
}
Playwright
const { test, expect } = require('@playwright/test');
const jsonata = require('../jsonata-min.js');
test.use({
browserName: 'chromium'
});
test('Webswing demo', async ({ page }) => {
//await page.goto('http://localhost:8080/webswing-server/webswing-demo');
await page.goto('http://localhost:8080/webswing-demo');
// Login
await page.locator('[name="username"]').fill('admin');
await page.locator('[name="password"]').fill('pwd');
await page.locator('.ws-btn-submit').click();
// Selenium:
// By.className('webswing-canvas accessibility-off')
//
// Playwright CSS selector for an element having BOTH classes:
await expect(
page.locator('.webswing-canvas.accessibility-off')
).toBeVisible({
timeout: 10_000
});
// Wait until Webswing JS API is initialized.
await page.waitForFunction(() =>
window.webswingInstance0 &&
typeof window.webswingInstance0.requestComponentTree === 'function'
);
// Click "Webswing API"
await webswingClick(
page,
'**[value="Webswing API" and componentType="JLabel"]'
);
// Wait until Search field exists and click it.
await webswingWaitFor(
page,
'**[value="Search" and componentType="JTextField" and name="Search"]'
);
await webswingClick(
page,
'**[value="Search" and componentType="JTextField" and name="Search"]'
);
// Type "latency"
await page.keyboard.type('latency');
await page.keyboard.press('Enter');
// Remove "latency"
for (let i = 0; i < 7; i++) {
await page.keyboard.press('Backspace');
}
// Type "user"
await page.keyboard.type('user');
await page.keyboard.press('Enter');
// Verify component exists.
await webswingExist(
page,
'**[value="user" and componentType="JTextField" and name="Search"]'
);
// Verify value.
await webswingCheckValue(
page,
'**[value="user" and componentType="JTextField" and name="Search"]',
'user'
);
// Move scrollbar.
await webswingMove(
page,
'**[componentType="ScrollBar" and height=322]'
);
// Open File menu.
await webswingClick(
page,
'**[componentType="WJMenu" and value="File"]'
);
// Wait until Exit menu item appears.
await webswingWaitFor(
page,
'**[componentType="WJMenuItem" and value="Exit"]'
);
// Click Exit.
await webswingClick(
page,
'**[componentType="WJMenuItem" and value="Exit"]'
);
});
/**
* Get a Webswing component using JSONata.
*/
async function webswingGet(page, selector) {
const tree = await page.evaluate(async () => {
try {
return await window.webswingInstance0.requestComponentTree();
} catch (error) {
return {
__webswingError: error?.message ?? String(error)
};
}
});
if (tree.__webswingError) {
throw new Error(
'ComponentTree fetch failed: ' + tree.__webswingError
);
}
return await jsonata(selector).evaluate(tree);
}
/**
* Check that a Webswing component exists.
*/
async function webswingExist(page, selector) {
const element = await webswingGet(page, selector);
if (element === undefined || element === null) {
throw new Error(
`Element for selector "${selector}" does not exist.`
);
}
return element;
}
/**
* Wait until a Webswing component appears.
*/
async function webswingWaitFor(page, selector, timeout = 10_000) {
await expect.poll(
async () => {
const element = await webswingGet(page, selector);
return element !== undefined && element !== null;
},
{
timeout,
message: `Waiting for Webswing component: ${selector}`
}
).toBe(true);
return await webswingExist(page, selector);
}
/**
* Click a Webswing component.
*/
async function webswingClick(page, selector) {
const element = await webswingWaitFor(page, selector);
const x = Number(element.screenX) + 10;
const y = Number(element.screenY) + 10;
await page.mouse.click(x, y);
}
/**
* Check the value of a Webswing component.
*/
async function webswingCheckValue(
page,
selector,
expectedValue,
timeout = 10_000
) {
await expect.poll(
async () => {
const element = await webswingGet(page, selector);
return element?.value;
},
{
timeout,
message:
`Expected Webswing component "${selector}" ` +
`to have value "${expectedValue}"`
}
).toBe(expectedValue);
}
/**
* Drag a Webswing component.
*/
async function webswingMove(page, selector) {
const element = await webswingWaitFor(page, selector);
const startX = Number(element.screenX) + 2;
const startY = Number(element.screenY) + 2;
const endX = startX;
const endY = startY + 100;
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(endX, endY, {
steps: 10
});
await page.mouse.up();
}