Testing
Overview
There are two general approaches for testing interop-enabled apps depending on whether you are an individual app owner or a team within your organization responsible for the io.Connect integration flow.
App owners can test their io.Connect apps by mocking the io.Connect methods used in them with the help of any familiar testing framework.
WebdriverIO
Available since io.Connect Desktop 10.0
You can use WebdriverIO to create end-to-end automation tests for io.Connect Desktop and your interop-enabled apps via the @interopio/wdio-iocd-service library.
⚠️ Note that the
@interopio/wdio-iocd-servicelibrary currently supports only WebdriverIO v8.
The @interopio/wdio-iocd-service library can be used to launch io.Connect Desktop via ChromeDriver, enabling you to write standard WebdriverIO tests for your interop-enabled apps.
ℹ️ For more in-depth information on using WebdriverIO, see the WebdriverIO official documentation.
Requirements
To use the @interopio/wdio-iocd-service library, the following requirements must be met:
- Node.js v20 or later;
- npm v9 or later;
- io.Connect Desktop installed;
Installation
To install the service, execute the following command:
npm install --save-dev @interopio/wdio-iocd-serviceTo install WebdriverIO with basic test dependencies, execute the following command:
npm install --save-dev webdriverio@^8 @wdio/cli@^8 @wdio/local-runner@^8 @wdio/mocha-framework@^8 @wdio/spec-reporter@^8To install TypeScript configurations and specifications, execute the following command:
npm install --save-dev ts-node typescript @types/nodeConfiguration
The @interopio/wdio-iocd-service library exports an IOCDBaseConfiguration object and an IOCDService() factory function to facilitate setting up the WebdriverIO service for io.Connect Desktop.
The IOCDBaseConfiguration object provides the base settings required for the service to work with io.Connect Desktop. It sets the runner to "local", limits the capability instances to 1, sets the automation protocol to "webdriver", and configures the connection to localhost on the default 61111 port.
The IOCDService() factory registers both the launcher and worker services with WebdriverIO.
To configure WebdriverIO, create a wdio.config.ts file in your project root. The following example demonstrates how to set up the service with the base configuration and a single capability for io.Connect Desktop:
import { IOCDBaseConfiguration, IOCDService } from "@interopio/wdio-iocd-service";
export const config: WebdriverIO.Config = {
// Base WebdriverIO configuration for io.Connect Desktop.
...IOCDBaseConfiguration,
// Configuring the services for io.Connect Desktop.
services: IOCDService({
// Optional video recording for failing tests.
videoRecording: {
enabled: true,
fps: 20,
outputDir: "./artifacts/videos"
}
}),
capabilities: [
{
// Must be set to "iocd" for the service to activate.
browserName: "iocd",
// Providing the test specifications for this capability.
"wdio:specs": ["./tests/**/*.spec.ts"],
// Settings for launching io.Connect Desktop.
"iocd:options": {
binary: "%LocalAppData%/interop.io/io.Connect Desktop/Desktop/io-connect-desktop.exe"
}
}
],
// WebdriverIO options.
// The integration is agnostic in regard to frameworks and reporters.
// You can use Mocha, Jasmine, Cucumber, or any other supported WebdriverIO framework and reporter combination.
logLevel: "warn",
framework: "mocha",
reporters: ["spec"],
// TypeScript auto compile options.
autoCompileOpts: {
autoCompile: true,
tsNodeOpts: {
transpileOnly: true,
project: "./tsconfig.json"
}
}
};The following sections describe the available service and capability configuration options.
Service Options
The IOCDService() factory function accepts as an argument an object with the following properties:
| Property | Type | Description |
|---|---|---|
videoRecording |
object |
Configuration for video recording. If enabled, videos will be recorded for each test. Videos will be kept only for the failing tests. |
The videoRecording object has the following properties:
| Property | Type | Description |
|---|---|---|
enabled |
boolean |
If true, will enable video recording. Defaults to false. |
fps |
number |
Frames per second for the video recording. Defaults to 60. |
outputDir |
string |
Output directory for storing videos of failed tests. Can be an absolute path or a path relative to the wdio.config file. Defaults to "./artifacts/videos". |
Capability Options
Each capability object in the capabilities array has the following properties:
| Property | Type | Description |
|---|---|---|
browserName |
string |
Required. Must be set to "iocd" for the service to activate. |
"iocd:options" |
object |
Configuration for io.Connect Desktop. |
"wdio:specs" |
string[] |
Array of glob patterns specifying which test specifications to run for the capability. If not provided, the service will use the default WebdriverIO specification patterns. |
The "iocd:options" object has the following properties:
| Property | Type | Description |
|---|---|---|
binary |
string |
Required. Absolute or relative path to the io.Connect Desktop executable file. You can use environment variables. |
configOverrides |
string[] |
Array of absolute or relative paths to override files for the system.json file. You can use environment variables. May be useful for different capabilities or environments. |
enableLogging |
boolean |
If true, will enable the Electron logging for the platform process. |
inspect |
number |
Node.js inspect port for debugging your tests. The port must be a valid integer between 1024 and 65535. |
systemConfig |
string |
Path to a system.json file for the test run. If not provided, the service will attempt to find a system.json file relative to the location of the executable file. |
TypeScript Configuration
The following example demonstrates a minimal tsconfig.json configuration for your project when using the @interopio/wdio-iocd-service library:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "dist",
"esModuleInterop": true,
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"]
},
"include": ["wdio.config.ts", "tests/**/*.ts"]
}⚠️ Note that the
@interopio/wdio-iocd-servicelibrary currently supports WebdriverIO v8 and uses"module": "CommonJS"to ensure compatibility with the WebdriverIO v8 runtime. When the library is updated to support WebdriverIO v9, this will be adjusted to"module": "NodeNext".
Inspect Port
Specifying an inspect port in the capability options enables you to attach a debugger to your running WebdriverIO test process and step through them in real time.
The following instructions describe how to attach a debugger to the WebdriverIO test process for different debugging tools:
- Visual Studio Code
- Open the Run and Debug panel.
- Open or create a
launch.jsonfile, click the "Add Configuration…" button, and choose "Node.js: Attach". - Set the
"port"property to your configured inspect port. - Press F5 to start debugging.
ℹ️ For more details on debugging with Visual Studio Code, see the official Visual Studio Code documentation.
- Chrome DevTools
- Open
chrome://inspectin Chrome. - Click the "Configure..." button and add your inspect port to the list.
- Find your process under "Remote Target" and click "inspect".
ℹ️ For more details on debugging with Chrome DevTools, see the Node.js official documentation.
- Inline debugging with WebdriverIO
You can also use the browser.debug() method directly inside your specifications to pause test execution at any point. This opens an interactive REPL in your terminal, allowing you to inspect the current browser state, execute commands, and resume when ready.
Custom Commands
The @interopio/wdio-iocd-service library extends the WebdriverIO browser and element objects with additional io.Connect Desktop methods.
Browser Commands
The following methods are available via the WebdriverIO browser object:
| Method | Description |
|---|---|
createIOConnectInstance() |
Function with the following signature: createIOConnectInstance(appDefinition: IOConnectDesktop.AppManager.Definition, config: IOConnectDesktop.Config) => Promise<IOConnectDesktop.API>. Uses the @interopio/desktop library to create a new isolated io.Connect API instance for a specific interop-enabled app. Imports the provided app definition into the platform in-memory store and initializes a new io.Connect API instance scoped to the specified app. |
io() |
Function with the following signature: () => Promise<IOConnectDesktop.API>. Resolves with a shared platform-level io.Connect API instance via which you can access the various io.Connect APIs (Interop, Shared Contexts, Channels, and more). |
restoreTime() |
Function with the following signature: () => void. Restores the original browser time after using travelTime(). |
switchById() |
Function with the following signature: (windowId: string) => Promise<void>. Switches the active browser context to an app frame by using the ID of an io.Connect Window instance. |
switchToLaunchpad() |
Function with the following signature: () => Promise<void>. Switches the active browser context to the Launchpad window. |
switchToNotificationPanel() |
Function with the following signature: () => Promise<void>. Switches the active browser context to the Notification Panel window. |
travelTime() |
Function with the following signature: (timeMs: number) => void. Advances the browser time by the given offset in milliseconds by patching Date and Date.now(). |
Element Commands
The following methods are available via the WebdriverIO element object:
| Method | Description |
|---|---|
waitAndClick() |
Function with the following signature: (opts?: { timeout?: number }) => Promise<void>. Waits for the element to exist, be visible, and be clickable, and then performs a click. The timeout argument is in milliseconds and defaults to 5000. |
waitForChildrenCount() |
Function with the following signature: (selector: string, count: number, opts?: { timeout?: number }) => Promise<void>. Waits until the element has the exact number of children specified via count which match the provided selector. The timeout argument is in milliseconds and defaults to 5000. |
Example Test
The following example demonstrates how to use the @interopio/wdio-iocd-service library in combination with standard WebdriverIO functionality:
describe("Example usage of @interopio/wdio-iocd-service", () => {
it("Should have access to `browser.io()`.", async () => {
// Fully initialized io.Connect API instance.
const io = await browser.io();
// Assert that the API instance has the expected API namespaces.
expect(io).toHaveAttribute("appManager");
expect(io).toHaveAttribute("contexts");
expect(io).toHaveAttribute("channels");
});
it("Should launch an app and interact with it.", async () => {
const io = await browser.io();
// Start an app via the App Management API.
const instance = await io.appManager.application("MyApp").start();
// Retrieve the io.Connect Window instance of the app.
const window = await instance.getWindow();
// Switch the WebdriverIO context to the started app instance.
await browser.switchById(window.id);
// Use standard WebdriverIO commands to interact with the app.
const input = await $("input[name='username']");
await input.setValue("John Doe");
const submit = await $("button[type='submit']");
await submit.click();
const message = await $(".welcome");
await message.waitForDisplayed({ timeout: 5000 });
expect(await message.getText()).toContain("Welcome, John Doe");
});
});Add a script command to your package.json file for executing the tests:
{
"scripts": {
"test": "npx wdio run ./wdio.config.ts"
}
}To run the tests, execute the following command:
npm testPlaywright
You can use Playwright to create end-to-end automation tests for io.Connect Desktop and your interop-enabled apps.
ℹ️ For more in-depth information on using Playwright, see the Playwright official documentation.
ℹ️ See the full io.Connect Playwright example on GitHub.
⚠️ Note that while Playwright offers experimental support for Electron apps, it's primarily designed for web app testing. For native apps (.NET, Java, etc.), consider using established UI testing frameworks like Selenium, WinAppDriver, or Appium.
The following test example contains two test cases and demonstrates how to:
prepare the necessary conditions for executing the test cases: launch io.Connect Desktop (you can also specify command line arguments for the io.Connect Desktop executable - e.g., if you want to launch it with custom configuration), await the io.Connect launcher to appear, initialize the
@interopio/desktoplibrary in order to use it in the tests;execute two test cases: one for launching and loading apps, the other for manipulating web groups;
⚠️ Note that in order for the test for manipulating web groups to succeed, io.Connect Desktop must be configured to use web groups.
- Import the required objects and define the io.Connect Desktop working directory and the path to the io.Connect executable file:
const { _electron: electron } = require("playwright");
const { test, expect } = require("@playwright/test");
const { setDefaultResultOrder } = require("dns");
const path = require("path");
const IODesktop = require("@interopio/desktop");
setDefaultResultOrder("ipv4first");
// These paths must correspond to your io.Connect Desktop deployment.
const platformDir = `${process.env.LocalAppData}\\io-connect-desktop`;
const executablePath = path.join(platformDir, "io-connect-desktop.exe");
// Variables that will hold the Electron app and the `@interopio/desktop` library instances.
let electronApp;
let io;
let workspacesPage;
test.setTimeout(40000);- Create helper functions for initializing the
@interopio/desktoplibrary, for awaiting an io.Connect app to load, and for retrieving the Web Group App:
// Helper for initializing the `@interopio/desktop` library so that it can be used in the tests.
const initDesktop = async (page) => {
// Using the page of the first started shell app to obtain a Gateway token
// for the library to be able to connect to the io.Connect Gateway.
const gwToken = await page.evaluate("iodesktop.getGWToken()");
// Initializing the library with the Layouts API and passing the Gateway token.
const io = await IODesktop({ layouts: "full", auth: { gatewayToken: gwToken } });
return io;
};
// Helper for awaiting an io.Connect app to load.
// The function will receive the io.Connect app name and the Electron app object as arguments.
const waitForAppToLoad = (appName, electronApp) => {
return new Promise((resolve, reject) => {
electronApp.on("window", async (page) => {
try {
// Check for the `iodesktop` service object injected in the page.
const iodesktop = await page.evaluate("window.iodesktop");
// Check the app name against the name contained in the `iodekstop` service object.
if (iodesktop && appName === iodesktop.applicationName) {
page.on("load", () => {
resolve({
app: iodesktop.applicationName,
instance: iodesktop.instance,
iodesktop,
page
});
})
}
} catch (error) {
// Add proper logging.
}
});
});
};
// Helper function to wait for apps to load.
const waitForAppsToLoad = async (appNames, appInstance) => {
return Promise.all(appNames.map(appName => waitForAppToLoad(appName, appInstance)));
};
// Helper for retrieving the io.Connect Web Group App.
// The function will receive window group ID and the Electron app object as arguments.
const getWebGroup = async (groupId, electronApp) => {
return new Promise(async (resolve, reject) => {
try {
const windows = electronApp.windows();
// Search for the Web Group App.
for (let index = 0; index < windows.length; index++) {
const page = windows[index];
// Check for the `iodesktop` service object injected in the page.
const iodesktop = await page.evaluate("window.iodesktop");
// Check the window group ID against the window ID contained in the `iodesktop` service object.
if (iodesktop && groupId === iodesktop.windowId) {
resolve(page);
break;
}
}
} catch (error) {
// Add proper logging.
}
});
};- Prepare the conditions for the tests:
// Start io.Connect Desktop, wait for the io.Connect launcher to load,
// and initialize the `@interopio/desktop` library before the tests.
test.beforeAll(async () => {
// Start io.Connect Desktop.
electronApp = await electron.launch({
executablePath: executablePath,
cwd: platformDir
// You can also specify command line arguments for the io.Connect Desktop executable as an array of strings:
// args: ["config=config/system.json", "configOverrides", "config0=config/system-PROD-EMEA.json"]
});
// List of apps to await.
const appNames = ["io-connect-desktop-toolbar", "workspaces-demo"];
// Wait for the specified apps to appear.
const [toolbarApp, workspacesApp] = await waitForAppsToLoad(appNames, electronApp);
// Wait for the Workspaces App to initialize its io.Connect library and the Workspaces API.
await workspacesApp.page.waitForFunction("window.io && window.io.workspaces !== undefined");
// Set the Workspaces App page globally so it can be used in the following tests.
workspacesPage = workspacesApp.page;
// Initialize the `@interopio/desktop` library.
io = await initDesktop(toolbarApp.page);
});- Write the test cases:
- test case for opening and loading apps:
test("Launch Client List and click the button to open Client Portfolio.", async () => {
// Open the "Client List" app using the `@interopio/desktop` library and wait for it to appear.
io.appManager.application("channelsclientlist").start();
const { page } = await waitForAppToLoad("channelsclientlist", electronApp);
// Click on the "Open Client Portfolio" button.
page.locator("button.btn.btn-icon.btn-primary.btn-borderless").click();
// Wait for the "Client Portfolio" app to appear.
await waitForAppToLoad("channelsclientportfolio", electronApp);
});- test case for manipulating web groups:
test("Open two windows, snap them together, and manipulate the window group via its frame buttons.", async () => {
// Open two windows using the `@interopio/desktop` library.
const url = "https://docs.interop.io/";
const win1 = await io.windows.open("win1", url);
const win2 = await io.windows.open("win2", url);
// Snap the opened windows to each other to create a window group.
await win2.snap(win1.id, "right");
// Get the `groupId` of the windows and retrieve the Web Group App.
const groupId = win1.groupId;
const webGroup = await getWebGroup(groupId, electronApp);
// Maximize, restore and close the Wb Group App via the standard frame buttons.
await webGroup.locator(`#t42-group-caption-bar-standard-buttons-maximize-${groupId}`).click();
await webGroup.waitForSelector(`#t42-group-caption-bar-standard-buttons-restore-${groupId}`);
await webGroup.locator(`#t42-group-caption-bar-standard-buttons-restore-${groupId}`).click();
await webGroup.locator(`#t42-group-caption-bar-standard-buttons-close-${groupId}`).click();
});- test case for manipulating a Workspace window:
test("Launch the Client Workspace and manipulate a window inside.", async () => {
const ordersWorkspaceWindowContext = await workspacesPage.evaluate(async () => {
// Restore the "Client" Workspace.
await io.workspaces.restoreWorkspace("Client");
// Wrap `onWindowLoaded()` in a `Promise` to wait for the "Client View" app to load as a Workspace window.
function waitForClientViewWindow() {
return new Promise((resolve, reject) => {
window.io.workspaces.onWindowLoaded((win) => {
if (win.appName === "client-view") {
// Resolve with the `WorkspaceWindow` object.
resolve(win);
}
});
});
};
// Get the "Client View" app as a `WorkspaceWindow` object.
const clientViewWindow = await waitForClientViewWindow();
if (clientViewWindow) {
// Get the underlying `IOconnectWindow` object.
const ioConnectWindow = clientViewWindow.getGdWindow();
// Update the context of the io.Connect Window.
await ioConnectWindow.updateContext({ testKey: "testValue" });
// Return the updated context.
const windowContext = await ioConnectWindow.getContext();
return windowContext;
}
});
// Assert against the updated context property.
await expect(ordersWorkspaceWindowContext.testKey).toEqual("testValue");
});- test case for restoring a Workspace using the Layouts API and testing an app inside the Workspace.
test("Restore the Client Workspace and check whether the Client View app has loaded.", async () => {
// Restore the "Client" Workspace using the Layouts API.
await io.layouts.restore({ name: "Client", type: "Workspace"});
// Locate the "Client View" app in the Workspace.
const { page } = await waitForAppToLoad("client-view", electronApp);
// Check whether the content of the "Client View" app is visible.
await expect(page.locator("div.col-12.ng-scope")).toBeVisible();
});