# Testing

Source: https://docs.interop.io/desktop/developers/testing/index.html

## 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](https://webdriver.io/) to create end-to-end automation tests for **io.Connect Desktop** and your interop-enabled apps via the [`@interopio/wdio-iocd-service`](https://www.npmjs.com/package/@interopio/wdio-iocd-service) library.

> ⚠️ *Note that the `@interopio/wdio-iocd-service` library 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](https://webdriver.io/docs/gettingstarted).*

### 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:

```cmd
npm install --save-dev @interopio/wdio-iocd-service
```

To install WebdriverIO with basic test dependencies, execute the following command:

```cmd
npm install --save-dev webdriverio@^8 @wdio/cli@^8 @wdio/local-runner@^8 @wdio/mocha-framework@^8 @wdio/spec-reporter@^8
```

To install TypeScript configurations and specifications, execute the following command:

```cmd
npm install --save-dev ts-node typescript @types/node
```

### Configuration

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**:

```typescript
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](https://docs.interop.io/desktop/developers/configuration/overview/index.md#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:

```json
{
    "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-service` library 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](#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

1. Open the **Run and Debug** panel.
2. Open or create a `launch.json` file, click the "Add Configuration…" button, and choose "Node.js: Attach".
3. Set the `"port"` property to your configured inspect port.
4. Press **F5** to start debugging.

> ℹ️ *For more details on debugging with Visual Studio Code, see the [official Visual Studio Code documentation](https://code.visualstudio.com/docs/nodejs/nodejs-debugging).*

- Chrome DevTools

1. Open `chrome://inspect` in Chrome.
2. Click the "Configure..." button and add your inspect port to the list.
3. Find your process under "Remote Target" and click "inspect".

> ℹ️ *For more details on debugging with Chrome DevTools, see the [Node.js official documentation](https://nodejs.org/en/learn/getting-started/debugging).*

- Inline debugging with WebdriverIO

You can also use the [`browser.debug()`](https://webdriver.io/docs/api/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:

```javascript
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:

```json
{
    "scripts": {
        "test": "npx wdio run ./wdio.config.ts"
    }
}
```

To run the tests, execute the following command:

```cmd
npm test
```

## Playwright

You can use [Playwright](https://playwright.dev/) 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](https://playwright.dev/docs/intro).*

> ℹ️ *See the full io.Connect [Playwright example](https://github.com/InteropIO/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](https://docs.interop.io/desktop/getting-started/how-to/rebrand-io-connect/functionality/index.md#environments__regions-shortcuts)), await the io.Connect launcher to appear, initialize the [`@interopio/desktop`](https://www.npmjs.com/package/@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](https://docs.interop.io/desktop/capabilities/windows/window-management/overview/index.md#window_groups-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](https://docs.interop.io/desktop/developers/configuration/io-connect-windows/index.md#ioconnect_window_properties-classic__web_groups).*

1. Import the required objects and define the **io.Connect Desktop** working directory and the path to the io.Connect executable file:

```javascript
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);
```

2. Create helper functions for initializing the [`@interopio/desktop`](https://www.npmjs.com/package/@interopio/desktop) library, for awaiting an io.Connect app to load, and for retrieving the Web Group App:

```javascript
// 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.
        }
    });
};
```

3. Prepare the conditions for the tests:

```javascript
// 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);
});
```

4. Write the test cases:

- test case for opening and loading apps:

```javascript
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:

```javascript
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:

```javascript
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.

```javascript
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();
});
```
