@interopio/iocd-cli 0.0.92 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +16 -0
- package/dist/commands/test.command.js +47 -0
- package/dist/commands/test.command.js.map +1 -1
- package/dist/services/template.service.js +15 -4
- package/dist/services/template.service.js.map +1 -1
- package/dist/services/test-infrastructure.service.js +113 -0
- package/dist/services/test-infrastructure.service.js.map +1 -0
- package/dist/templates/tests/.agents/skills/iocd-wdio-testing/SKILL.md +71 -0
- package/dist/templates/tests/.agents/skills/iocd-wdio-testing/references/authoring.md +243 -0
- package/dist/templates/tests/.agents/skills/iocd-wdio-testing/references/commands.md +24 -0
- package/dist/templates/tests/.agents/skills/iocd-wdio-testing/references/configuration.md +30 -0
- package/dist/templates/tests/.agents/skills/iocd-wdio-testing/references/models.md +52 -0
- package/dist/templates/tests/.agents/skills/iocd-wdio-testing/references/setup.md +62 -0
- package/dist/templates/tests/.agents/skills/iocd-wdio-testing/references/troubleshooting.md +66 -0
- package/dist/templates/tests/tests/README.md +79 -0
- package/dist/templates/tests/tests/config/system.json +36 -0
- package/dist/templates/tests/tests/package-lock.json +3425 -2274
- package/dist/templates/tests/tests/package.json +19 -11
- package/dist/templates/tests/tests/tests/assets/applications/channel-client/index.html +31 -0
- package/dist/templates/tests/tests/tests/assets/applications/interop-responder/index.html +25 -0
- package/dist/templates/tests/tests/tests/assets/applications/people-list/index.html +59 -0
- package/dist/templates/tests/tests/tests/assets/applications/person-details/index.html +26 -0
- package/dist/templates/tests/tests/tests/assets/applications/shared-context-client/index.html +37 -0
- package/dist/templates/tests/tests/tests/assets/applications/web/index.html +31 -0
- package/dist/templates/tests/tests/tests/assets/applications/workspace-widget/index.html +14 -0
- package/dist/templates/tests/tests/tests/channel-selector.spec.ts +80 -0
- package/dist/templates/tests/tests/tests/common/iocd.ts +125 -0
- package/dist/templates/tests/tests/tests/config/apps.json +96 -0
- package/dist/templates/tests/tests/tests/interop.spec.ts +37 -0
- package/dist/templates/tests/tests/tests/notifications.spec.ts +37 -0
- package/dist/templates/tests/tests/tests/shared-contexts.spec.ts +71 -0
- package/dist/templates/tests/tests/tests/single-app.spec.ts +39 -0
- package/dist/templates/tests/tests/tests/workspace-add-app.spec.ts +64 -0
- package/dist/templates/tests/tests/tests/workspace-interop.spec.ts +99 -0
- package/dist/templates/tests/tests/tsconfig.json +7 -5
- package/dist/templates/tests/tests/wdio.config.ts +29 -25
- package/package.json +3 -2
- package/dist/templates/tests/tests/tests/sample.spec.ts +0 -73
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tests",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
5
7
|
"scripts": {
|
|
6
|
-
"test": "
|
|
8
|
+
"test": "wdio run ./wdio.config.ts",
|
|
9
|
+
"typecheck": "tsc --noEmit -p ./tsconfig.json"
|
|
7
10
|
},
|
|
8
11
|
"devDependencies": {
|
|
9
|
-
"@interopio/
|
|
10
|
-
"@
|
|
11
|
-
"@
|
|
12
|
-
"@
|
|
13
|
-
"@
|
|
14
|
-
"@
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
12
|
+
"@interopio/desktop": "^6.21.0",
|
|
13
|
+
"@interopio/wdio-iocd-service": "1.0.0-next.2",
|
|
14
|
+
"@interopio/workspaces-api": "^4.5.0",
|
|
15
|
+
"@types/chai": "^5.2.3",
|
|
16
|
+
"@types/mocha": "^10.0.10",
|
|
17
|
+
"@types/node": "^22.20.1",
|
|
18
|
+
"@wdio/cli": "^9.29.1",
|
|
19
|
+
"@wdio/local-runner": "^9.29.1",
|
|
20
|
+
"@wdio/mocha-framework": "^9.29.1",
|
|
21
|
+
"@wdio/spec-reporter": "^9.29.1",
|
|
22
|
+
"chai": "^5.3.3",
|
|
23
|
+
"tsx": "^4.23.0",
|
|
24
|
+
"typescript": "^6.0.3",
|
|
25
|
+
"webdriverio": "^9.29.1"
|
|
18
26
|
}
|
|
19
27
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Channel Client</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main>
|
|
10
|
+
<h1>Channel Client</h1>
|
|
11
|
+
<p>Status: <strong id="channel-status" aria-live="polite">Connecting...</strong></p>
|
|
12
|
+
<p>Current Channel: <strong id="current-channel" aria-live="polite">None</strong></p>
|
|
13
|
+
</main>
|
|
14
|
+
<script>
|
|
15
|
+
const status = document.querySelector("#channel-status");
|
|
16
|
+
const currentChannel = document.querySelector("#current-channel");
|
|
17
|
+
|
|
18
|
+
IODesktop({ channels: true }).then((io) => {
|
|
19
|
+
window.io = io;
|
|
20
|
+
|
|
21
|
+
const showChannels = (channels) => {
|
|
22
|
+
currentChannel.textContent = channels.join(", ") || "None";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
showChannels(io.channels.myChannels());
|
|
26
|
+
io.channels.onChannelsChanged(showChannels);
|
|
27
|
+
status.textContent = "Ready";
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Interop Responder</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main>
|
|
10
|
+
<h1>Interop Responder</h1>
|
|
11
|
+
<p id="status" aria-live="polite">Registering...</p>
|
|
12
|
+
</main>
|
|
13
|
+
<script>
|
|
14
|
+
const addMethod = "IOCD.Sample.Add";
|
|
15
|
+
const status = document.querySelector("#status");
|
|
16
|
+
|
|
17
|
+
IODesktop().then(async (io) => {
|
|
18
|
+
window.io = io;
|
|
19
|
+
// Register an Interop method the test will invoke from the runner.
|
|
20
|
+
await io.interop.register(addMethod, ({ a, b }) => ({ sum: a + b }));
|
|
21
|
+
status.textContent = "Ready";
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>People</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main>
|
|
10
|
+
<h1>People</h1>
|
|
11
|
+
<p>Select a person to send their name to the details app.</p>
|
|
12
|
+
<ul id="people-list">
|
|
13
|
+
<li><button type="button" data-person-name="Ada Lovelace" disabled>Ada Lovelace</button></li>
|
|
14
|
+
<li><button type="button" data-person-name="Grace Hopper" disabled>Grace Hopper</button></li>
|
|
15
|
+
<li><button type="button" data-person-name="Katherine Johnson" disabled>Katherine Johnson</button></li>
|
|
16
|
+
<li><button type="button" data-person-name="Margaret Hamilton" disabled>Margaret Hamilton</button></li>
|
|
17
|
+
<li><button type="button" data-person-name="Edsger Dijkstra" disabled>Edsger Dijkstra</button></li>
|
|
18
|
+
</ul>
|
|
19
|
+
<p id="selection-status" aria-live="polite">Connecting to the details app...</p>
|
|
20
|
+
</main>
|
|
21
|
+
<script>
|
|
22
|
+
const displayPersonMethod = "IOCD.Sample.DisplayPerson";
|
|
23
|
+
const buttons = Array.from(document.querySelectorAll("[data-person-name]"));
|
|
24
|
+
const status = document.querySelector("#selection-status");
|
|
25
|
+
|
|
26
|
+
IODesktop().then((io) => {
|
|
27
|
+
window.io = io;
|
|
28
|
+
|
|
29
|
+
const enableSelection = () => {
|
|
30
|
+
buttons.forEach((button) => {
|
|
31
|
+
button.disabled = false;
|
|
32
|
+
});
|
|
33
|
+
status.textContent = "Ready";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (io.interop.methods({ name: displayPersonMethod }).length > 0) {
|
|
37
|
+
enableSelection();
|
|
38
|
+
} else {
|
|
39
|
+
let unsubscribe;
|
|
40
|
+
unsubscribe = io.interop.methodAdded((method) => {
|
|
41
|
+
if (method.name === displayPersonMethod) {
|
|
42
|
+
enableSelection();
|
|
43
|
+
unsubscribe?.();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
buttons.forEach((button) => {
|
|
49
|
+
button.addEventListener("click", async () => {
|
|
50
|
+
const name = button.dataset.personName;
|
|
51
|
+
status.textContent = `Sending ${name}...`;
|
|
52
|
+
await io.interop.invoke(displayPersonMethod, { name }, "best");
|
|
53
|
+
status.textContent = `Sent ${name}`;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
</script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Person Details</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main>
|
|
10
|
+
<h1>Person Details</h1>
|
|
11
|
+
<p>Selected person: <strong id="selected-person" aria-live="polite">None</strong></p>
|
|
12
|
+
</main>
|
|
13
|
+
<script>
|
|
14
|
+
const displayPersonMethod = "IOCD.Sample.DisplayPerson";
|
|
15
|
+
const selectedPerson = document.querySelector("#selected-person");
|
|
16
|
+
|
|
17
|
+
IODesktop().then(async (io) => {
|
|
18
|
+
window.io = io;
|
|
19
|
+
await io.interop.register(displayPersonMethod, ({ name }) => {
|
|
20
|
+
selectedPerson.textContent = name;
|
|
21
|
+
return { displayed: name };
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Shared Context Client</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main>
|
|
10
|
+
<h1>Shared Context Client</h1>
|
|
11
|
+
<p>Status: <strong id="context-status" aria-live="polite">Connecting...</strong></p>
|
|
12
|
+
<button id="share-client" type="button" disabled>Share selected client</button>
|
|
13
|
+
<p>Selected client: <strong id="selected-client" aria-live="polite">None</strong></p>
|
|
14
|
+
</main>
|
|
15
|
+
<script>
|
|
16
|
+
const contextName = "IOCD.Sample.SelectedClient";
|
|
17
|
+
const selectedClient = document.querySelector("#selected-client");
|
|
18
|
+
const status = document.querySelector("#context-status");
|
|
19
|
+
const shareClient = document.querySelector("#share-client");
|
|
20
|
+
|
|
21
|
+
IODesktop().then(async (io) => {
|
|
22
|
+
window.io = io;
|
|
23
|
+
|
|
24
|
+
await io.contexts.subscribe(contextName, (data) => {
|
|
25
|
+
selectedClient.textContent = data?.clientName ?? "None";
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
shareClient.addEventListener("click", async () => {
|
|
29
|
+
await io.contexts.set(contextName, { clientName: "Ada Lovelace" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
shareClient.disabled = false;
|
|
33
|
+
status.textContent = "Ready";
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>IOCD Test App</title>
|
|
7
|
+
<script>
|
|
8
|
+
IODesktop().then((desktop) => {
|
|
9
|
+
window.io = desktop;
|
|
10
|
+
console.log("IOCD Desktop API is ready to use.");
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<main>
|
|
16
|
+
<h1>IOCD Test App</h1>
|
|
17
|
+
<button id="increment-counter" type="button">Increment counter</button>
|
|
18
|
+
<p>Count: <span id="counter" aria-live="polite">0</span></p>
|
|
19
|
+
</main>
|
|
20
|
+
<script>
|
|
21
|
+
const incrementButton = document.querySelector("#increment-counter");
|
|
22
|
+
const counter = document.querySelector("#counter");
|
|
23
|
+
let count = 0;
|
|
24
|
+
|
|
25
|
+
incrementButton.addEventListener("click", () => {
|
|
26
|
+
count += 1;
|
|
27
|
+
counter.textContent = String(count);
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Workspace Widget</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main>
|
|
10
|
+
<h1 id="widget-ready">Workspace Widget</h1>
|
|
11
|
+
<p>Added to the workspace through the add-application popup.</p>
|
|
12
|
+
</main>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import type { IOConnectDesktop } from "@interopio/desktop";
|
|
3
|
+
import { Group } from "@interopio/wdio-iocd-service/models";
|
|
4
|
+
import { $, browser } from "@wdio/globals";
|
|
5
|
+
|
|
6
|
+
// Selects a Channel through the Web Group caption UI and verifies that the
|
|
7
|
+
// client app observes the resulting Channel membership through its IO API.
|
|
8
|
+
describe("Channel Selector", function () {
|
|
9
|
+
this.timeout(60_000);
|
|
10
|
+
|
|
11
|
+
let instance: IOConnectDesktop.AppManager.Instance | undefined;
|
|
12
|
+
let windowId: string | undefined;
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
const io = await browser.io();
|
|
16
|
+
if (windowId) {
|
|
17
|
+
await io.channels.leave(windowId).catch(() => undefined);
|
|
18
|
+
}
|
|
19
|
+
await instance?.stop().catch(() => undefined);
|
|
20
|
+
instance = undefined;
|
|
21
|
+
windowId = undefined;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("joins the Channel selected from the app caption", async () => {
|
|
25
|
+
const io = await browser.io();
|
|
26
|
+
const [channelName] = await io.channels.all();
|
|
27
|
+
|
|
28
|
+
expect(channelName, "configured test Channel").to.be.a("string").and.not.be.empty;
|
|
29
|
+
|
|
30
|
+
instance = await io.appManager.application("channel-client")!.start({}, { mode: "flat" });
|
|
31
|
+
const window = await instance.getWindow();
|
|
32
|
+
windowId = window.id;
|
|
33
|
+
|
|
34
|
+
await browser.switchById(window.id);
|
|
35
|
+
await browser.waitUntil(async () => (await $("#channel-status").getText()) === "Ready", {
|
|
36
|
+
timeoutMsg: "Expected the Channel client to initialize its Channels API",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(window.groupId, "Web Group ID").to.be.a("string").and.not.be.empty;
|
|
40
|
+
|
|
41
|
+
const group = new Group(window.groupId!);
|
|
42
|
+
const [frame] = await group.frames.waitForCount(1);
|
|
43
|
+
const channelSelector = frame.captionBar.channelSelector;
|
|
44
|
+
|
|
45
|
+
await group.switchTo();
|
|
46
|
+
await channelSelector.waitForDisplayed();
|
|
47
|
+
await channelSelector.click();
|
|
48
|
+
await switchToChannelSelectorPopup();
|
|
49
|
+
|
|
50
|
+
await $(`[data-testid="channel-selector-channel-${channelName}"]`).waitAndClick();
|
|
51
|
+
|
|
52
|
+
await browser.switchById(window.id);
|
|
53
|
+
const currentChannel = await $("#current-channel");
|
|
54
|
+
await browser.waitUntil(async () => (await currentChannel.getText()) === channelName, {
|
|
55
|
+
timeoutMsg: `Expected the app to join the ${channelName} Channel`,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(await currentChannel.getText()).to.equal(channelName);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
async function switchToChannelSelectorPopup(): Promise<void> {
|
|
63
|
+
await browser.waitUntil(
|
|
64
|
+
async () => {
|
|
65
|
+
for (const handle of await browser.getWindowHandles()) {
|
|
66
|
+
try {
|
|
67
|
+
await browser.switchToWindow(handle);
|
|
68
|
+
if (await $(".io-channel-selector-panel").isDisplayed()) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// The popup can be created or removed while handles are being inspected.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return false;
|
|
77
|
+
},
|
|
78
|
+
{ timeoutMsg: "Expected the Channel Selector popup to open" }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export async function waitFor(
|
|
2
|
+
condition: () => boolean | Promise<boolean>,
|
|
3
|
+
timeoutMs = 5000,
|
|
4
|
+
intervalMs = 100
|
|
5
|
+
): Promise<void> {
|
|
6
|
+
const deadline = Date.now() + timeoutMs;
|
|
7
|
+
while (!(await condition())) {
|
|
8
|
+
if (Date.now() >= deadline) {
|
|
9
|
+
throw new Error(`Condition was not met within ${timeoutMs}ms.`);
|
|
10
|
+
}
|
|
11
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Cleanup = () => void | Promise<void>;
|
|
16
|
+
|
|
17
|
+
export function createCleanupRegistry() {
|
|
18
|
+
const cleanups: Cleanup[] = [];
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
add(cleanup: Cleanup): Cleanup {
|
|
22
|
+
cleanups.push(cleanup);
|
|
23
|
+
return cleanup;
|
|
24
|
+
},
|
|
25
|
+
async run(): Promise<void> {
|
|
26
|
+
const pending = cleanups.splice(0).reverse();
|
|
27
|
+
let firstError: unknown;
|
|
28
|
+
|
|
29
|
+
for (const cleanup of pending) {
|
|
30
|
+
try {
|
|
31
|
+
await cleanup();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
firstError ??= error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (firstError) {
|
|
38
|
+
throw firstError;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface WaitForEventOptions<T> {
|
|
45
|
+
predicate?: (value: T) => boolean;
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
timeoutMsg?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function waitForEvent<T>(
|
|
51
|
+
subscribe: (listener: (value: T) => void) => Cleanup | Promise<Cleanup>,
|
|
52
|
+
options: WaitForEventOptions<T> = {}
|
|
53
|
+
): Promise<{ result: Promise<T>; unsubscribe: Cleanup }> {
|
|
54
|
+
const {
|
|
55
|
+
predicate = () => true,
|
|
56
|
+
timeoutMs = 5000,
|
|
57
|
+
timeoutMsg = `Event was not received within ${timeoutMs}ms.`,
|
|
58
|
+
} = options;
|
|
59
|
+
|
|
60
|
+
let resolveResult!: (value: T) => void;
|
|
61
|
+
let rejectResult!: (error: Error) => void;
|
|
62
|
+
let subscriptionCleanup: Cleanup | undefined;
|
|
63
|
+
let cleanupRequested = false;
|
|
64
|
+
let settled = false;
|
|
65
|
+
|
|
66
|
+
const result = new Promise<T>((resolve, reject) => {
|
|
67
|
+
resolveResult = resolve;
|
|
68
|
+
rejectResult = reject;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const unsubscribe: Cleanup = async () => {
|
|
72
|
+
if (!subscriptionCleanup) {
|
|
73
|
+
cleanupRequested = true;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cleanup = subscriptionCleanup;
|
|
78
|
+
subscriptionCleanup = undefined;
|
|
79
|
+
await cleanup();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const timeout = setTimeout(() => {
|
|
83
|
+
if (settled) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
settled = true;
|
|
88
|
+
void (async () => {
|
|
89
|
+
try {
|
|
90
|
+
await unsubscribe();
|
|
91
|
+
} finally {
|
|
92
|
+
rejectResult(new Error(timeoutMsg));
|
|
93
|
+
}
|
|
94
|
+
})().catch(() => undefined);
|
|
95
|
+
}, timeoutMs);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
subscriptionCleanup = await subscribe((value) => {
|
|
99
|
+
if (settled || !predicate(value)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
settled = true;
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
void (async () => {
|
|
106
|
+
try {
|
|
107
|
+
await unsubscribe();
|
|
108
|
+
resolveResult(value);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
rejectResult(error instanceof Error ? error : new Error(String(error)));
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (cleanupRequested) {
|
|
116
|
+
await unsubscribe();
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
settled = true;
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
rejectResult(error instanceof Error ? error : new Error(String(error)));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { result, unsubscribe };
|
|
125
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "web",
|
|
4
|
+
"title": "IOCD Test App",
|
|
5
|
+
"type": "window",
|
|
6
|
+
"allowScriptExecution": true,
|
|
7
|
+
"details": {
|
|
8
|
+
"left": 100,
|
|
9
|
+
"top": 100,
|
|
10
|
+
"height": 400,
|
|
11
|
+
"width": 400,
|
|
12
|
+
"url": "file://%WDIO_TESTS_ROOT%/assets/applications/web/index.html",
|
|
13
|
+
"allowOverrides": {
|
|
14
|
+
"url": true
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"hidden": true,
|
|
18
|
+
"ignoreSavedLayout": true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "people-list",
|
|
22
|
+
"title": "People",
|
|
23
|
+
"type": "window",
|
|
24
|
+
"details": {
|
|
25
|
+
"height": 500,
|
|
26
|
+
"width": 360,
|
|
27
|
+
"url": "file://%WDIO_TESTS_ROOT%/assets/applications/people-list/index.html"
|
|
28
|
+
},
|
|
29
|
+
"hidden": true,
|
|
30
|
+
"ignoreSavedLayout": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "person-details",
|
|
34
|
+
"title": "Person Details",
|
|
35
|
+
"type": "window",
|
|
36
|
+
"details": {
|
|
37
|
+
"height": 500,
|
|
38
|
+
"width": 480,
|
|
39
|
+
"url": "file://%WDIO_TESTS_ROOT%/assets/applications/person-details/index.html"
|
|
40
|
+
},
|
|
41
|
+
"hidden": true,
|
|
42
|
+
"ignoreSavedLayout": true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "interop-responder",
|
|
46
|
+
"title": "Interop Responder",
|
|
47
|
+
"type": "window",
|
|
48
|
+
"details": {
|
|
49
|
+
"height": 200,
|
|
50
|
+
"width": 320,
|
|
51
|
+
"url": "file://%WDIO_TESTS_ROOT%/assets/applications/interop-responder/index.html"
|
|
52
|
+
},
|
|
53
|
+
"hidden": true,
|
|
54
|
+
"ignoreSavedLayout": true
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "channel-client",
|
|
58
|
+
"title": "Channel Client",
|
|
59
|
+
"type": "window",
|
|
60
|
+
"details": {
|
|
61
|
+
"height": 260,
|
|
62
|
+
"width": 360,
|
|
63
|
+
"url": "file://%WDIO_TESTS_ROOT%/assets/applications/channel-client/index.html",
|
|
64
|
+
"channelSelector": {
|
|
65
|
+
"enabled": true,
|
|
66
|
+
"type": "single"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"ignoreSavedLayout": true
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "shared-context-client",
|
|
73
|
+
"title": "Shared Context Client",
|
|
74
|
+
"type": "window",
|
|
75
|
+
"details": {
|
|
76
|
+
"height": 300,
|
|
77
|
+
"width": 380,
|
|
78
|
+
"url": "file://%WDIO_TESTS_ROOT%/assets/applications/shared-context-client/index.html"
|
|
79
|
+
},
|
|
80
|
+
"ignoreSavedLayout": true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "workspace-widget",
|
|
84
|
+
"title": "Workspace Widget",
|
|
85
|
+
"type": "window",
|
|
86
|
+
"details": {
|
|
87
|
+
"height": 300,
|
|
88
|
+
"width": 360,
|
|
89
|
+
"url": "file://%WDIO_TESTS_ROOT%/assets/applications/workspace-widget/index.html"
|
|
90
|
+
},
|
|
91
|
+
"customProperties": {
|
|
92
|
+
"includeInWorkspaces": true
|
|
93
|
+
},
|
|
94
|
+
"ignoreSavedLayout": true
|
|
95
|
+
}
|
|
96
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import type { IOConnectDesktop } from "@interopio/desktop";
|
|
3
|
+
import { browser } from "@wdio/globals";
|
|
4
|
+
import { waitFor } from "./common/iocd";
|
|
5
|
+
|
|
6
|
+
// Shows the request/response Interop pattern driven from the test itself:
|
|
7
|
+
// the test starts an app that registers a method, then invokes it and asserts
|
|
8
|
+
// the returned value. The existing workspace sample only exercises Interop
|
|
9
|
+
// between two apps; here the test process is the caller.
|
|
10
|
+
describe("Interop request/response", () => {
|
|
11
|
+
let instance: IOConnectDesktop.AppManager.Instance | undefined;
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
// Stop the responder app even if the test failed.
|
|
15
|
+
await instance?.stop().catch(() => undefined);
|
|
16
|
+
instance = undefined;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("invokes an Interop method registered by another app", async () => {
|
|
20
|
+
const io = await browser.io();
|
|
21
|
+
const methodName = "IOCD.Sample.Add";
|
|
22
|
+
|
|
23
|
+
// Start the app that registers the Interop method.
|
|
24
|
+
instance = await io.appManager.application("interop-responder")!.start();
|
|
25
|
+
|
|
26
|
+
// The app registers the method asynchronously once its API is ready, so
|
|
27
|
+
// wait for it to be published before invoking it.
|
|
28
|
+
await waitFor(() =>
|
|
29
|
+
io.interop.methods().some((method) => method.name === methodName)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Invoke the method and assert the app's computed response.
|
|
33
|
+
const result = await io.interop.invoke(methodName, { a: 2, b: 3 });
|
|
34
|
+
|
|
35
|
+
expect((result.returned as { sum: number }).sum).to.equal(5);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { $, browser } from "@wdio/globals";
|
|
3
|
+
|
|
4
|
+
// Raises a notification and asserts it in the Notification Panel. This is the
|
|
5
|
+
// only sample that uses the panel-specific service commands:
|
|
6
|
+
// browser.switchToNotificationPanel() and element.waitForChildrenCount().
|
|
7
|
+
describe("Notifications", () => {
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
// Clear every notification so the next test starts from an empty panel.
|
|
10
|
+
const io = await browser.io();
|
|
11
|
+
await io.notifications.clearAll().catch(() => undefined);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("shows a raised notification in the Notification Panel", async () => {
|
|
15
|
+
const io = await browser.io();
|
|
16
|
+
const title = "Trade executed";
|
|
17
|
+
const body = "AAPL 100 @ 190.25";
|
|
18
|
+
|
|
19
|
+
// Notifications are disabled on a fresh platform; enable before raising.
|
|
20
|
+
await io.notifications.configure({ enable: true });
|
|
21
|
+
await io.notifications.raise({ title, body });
|
|
22
|
+
|
|
23
|
+
// Open the panel through the API, then move the WebdriverIO context to it.
|
|
24
|
+
// switchToNotificationPanel() only switches focus; it doesn't open the panel.
|
|
25
|
+
await io.notifications.panel.show();
|
|
26
|
+
await browser.switchToNotificationPanel();
|
|
27
|
+
|
|
28
|
+
// waitForChildrenCount waits until the list holds exactly one notification.
|
|
29
|
+
const list = await $(".io-notification-list");
|
|
30
|
+
await list.waitForChildrenCount(".io-notification", 1);
|
|
31
|
+
|
|
32
|
+
const notificationTitle = await $(
|
|
33
|
+
".io-notification .io-notification-body-content .io-title"
|
|
34
|
+
);
|
|
35
|
+
expect(await notificationTitle.getText()).to.equal(title);
|
|
36
|
+
});
|
|
37
|
+
});
|