Testing Your App

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.

For end-to-end automation tests, it is recommended to use the Playwright testing tool.

Testing with Playwright

Playwright allows you 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/desktop library 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.

  1. 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}\\interop.io\\io.Connect Desktop\\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);
  1. Create helper functions for initializing the @interopio/desktop library, 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.
        };
    });
};
  1. 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(page);
});
  1. 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();
});