@interopio/iocd-cli 1.1.2 → 1.1.3

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 CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.3](https://github.com/InteropIO/iocd-cli/compare/iocd-cli-v1.1.2...iocd-cli-v1.1.3) (2026-10-06)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **test:** harden seed testing skill and examples ([#124](https://github.com/InteropIO/iocd-cli/issues/124)) ([0ea68f4](https://github.com/InteropIO/iocd-cli/commit/0ea68f41079284feef624569b4da57cb9663c538))
14
+
8
15
  ## [1.1.2](https://github.com/InteropIO/iocd-cli/compare/iocd-cli-v1.1.1...iocd-cli-v1.1.2) (2026-09-23)
9
16
 
10
17
 
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: iocd-wdio-testing
3
3
  description: 'Set up, write, run, and debug end-to-end WebdriverIO tests for an io.Connect Desktop (IOCD) seed project with @interopio/wdio-iocd-service. Use when adding or running IOCD/seed E2E tests, or testing apps, windows, Web Groups, Workspaces, Interop, Channels, Shared Contexts, Layouts, or Notifications in a running io.Connect Desktop platform.'
4
- argument-hint: 'Describe the IOCD behavior to test, or ask to set up tests'
5
4
  ---
6
5
 
7
6
  # io.Connect Desktop WebdriverIO Testing
@@ -175,14 +175,20 @@ only when the target app permits it.
175
175
  Some renderer calls return a promise that settles only after the user answers a dialog or completes
176
176
  another UI workflow. Awaiting that promise while issuing the request blocks the test before it can
177
177
  perform the action that resolves the promise. Split the workflow into **start**, **interact**, and
178
- **collect** phases:
178
+ **collect** phases. This example uses `ModalsDialog`; verify that the installed service exports it
179
+ (see [models](./models.md)). Otherwise, implement the interaction in a local page object using the
180
+ actual dialog DOM. `config`, `requesterWindow`, and `expectedResponse` are test-owned inputs.
179
181
 
180
182
  ```ts
183
+ import { ModalsDialog } from "@interopio/wdio-iocd-service/models";
184
+
185
+ type ModalOutcome = { settled: boolean; response?: unknown; error?: string };
186
+
181
187
  // In the app-window helper: start the request and return immediately.
182
188
  await browser.execute((requestConfig) => {
183
189
  const appWindow = window as typeof window & {
184
190
  io: { modals: { dialogs: { request(config: unknown): Promise<unknown> } } };
185
- __modalOutcome?: { settled: boolean; response?: unknown; error?: string };
191
+ __modalOutcome?: ModalOutcome;
186
192
  };
187
193
 
188
194
  appWindow.__modalOutcome = { settled: false };
@@ -195,16 +201,24 @@ await browser.execute((requestConfig) => {
195
201
  });
196
202
  }, config);
197
203
 
198
- // Switch to the dialog and answer it through WebdriverIO or a service model.
204
+ // Address the dialog raised by this requester and answer it.
205
+ const dialog = ModalsDialog.forTarget(requesterWindow.id);
199
206
  await dialog.waitUntilOpen();
200
- await dialog.clickAction("confirm");
207
+ await dialog.action("confirm").click();
201
208
 
202
209
  // Switch back to the requester, wait for settlement, then collect/assert the outcome.
203
210
  await browser.switchById(requesterWindow.id);
204
211
  await browser.waitUntil(
205
- () => browser.execute(() => Boolean((window as any).__modalOutcome?.settled)),
212
+ () => browser.execute(() =>
213
+ Boolean((window as typeof window & { __modalOutcome?: ModalOutcome }).__modalOutcome?.settled)
214
+ ),
206
215
  { timeoutMsg: "Expected the modal request to settle" }
207
216
  );
217
+ const outcome = await browser.execute(() =>
218
+ (window as typeof window & { __modalOutcome?: ModalOutcome }).__modalOutcome
219
+ );
220
+ expect(outcome?.error).to.equal(undefined);
221
+ expect(outcome?.response).to.deep.equal(expectedResponse);
208
222
  ```
209
223
 
210
224
  Store both resolved and rejected outcomes, clean any app-owned request state after the test, and use
@@ -233,10 +247,13 @@ command only switches focus, it doesn't open the panel. See `notifications.spec.
233
247
  - **Wait for state, not time** — events, `browser.waitUntil`, element waits, or `waitFor`. Subscribe
234
248
  before triggering the action. For event APIs, use `waitForEvent` from `tests/tests/common/iocd.ts`:
235
249
  it filters events, times out with a useful message, and unsubscribes on success or timeout. Track
236
- its idempotent `unsubscribe` in a cleanup registry so test failure can't leak the subscription.
250
+ its idempotent `unsubscribe` in a cleanup registry so test failure cancels the pending wait and
251
+ removes its timer and subscription. Always await `result` on the success path to assert delivery.
237
252
  Don't replace an unknown readiness condition with a fixed pause.
238
- - **Isolate** — create state in `beforeEach` or the test; clean everything in `afterEach`. Catch
239
- cleanup errors only when the resource may already be gone.
253
+ - **Isolate** — register cleanup immediately after each resource is created, before the next
254
+ fallible operation. Put all cleanup in one registry so a failed unsubscribe cannot prevent apps
255
+ from stopping or contexts from being destroyed. Catch cleanup errors only when the resource may
256
+ already be gone.
240
257
  - **Don't bypass the behavior under test** — drive the first app as a user would and observe the
241
258
  outcome in the receiver, rather than invoking application business logic directly from the test.
242
259
  - **One behavior per test**; run the new spec alone while iterating.
@@ -29,7 +29,11 @@ platform the seed ships — you'd be testing something else. Behavior changes be
29
29
  `modifications/`; another *supported* configuration belongs in its own capability (below).
30
30
 
31
31
  The generated `config/system.json` registers `tests/tests/config/apps.json` as a path app store and
32
- isolates `folders.userData` to `tests/user-data` (disposable — never commit it or reuse its state).
32
+ isolates `folders.userData` to `tests/user-data`. The test package's `.gitignore` excludes this
33
+ generated profile; check older packages as described in [setup](./setup.md). It persists across
34
+ launches, including cached SSO sessions, so tests must arrange their required state. Use separate
35
+ profile directories for capabilities that require different authenticated users or independent
36
+ persistent state. Never commit the profile or include it in CI artifacts.
33
37
 
34
38
  ## Tests that need extra config
35
39
 
@@ -81,9 +85,10 @@ Rules:
81
85
  - **One launch per capability, run sequentially** (`IOCDBaseConfiguration` sets `maxInstances: 1`).
82
86
  Budget a full platform startup + shutdown for each — group specs by configuration; never add a
83
87
  capability per spec.
84
- - **`npm test -- --spec <file>` overrides capability spec patterns**, so a single-spec run executes
85
- that spec under *every* capability. Config-dependent specs must read the live config and skip when
86
- it doesn't apply (below).
88
+ - **`npm test -- --spec <file>` overrides capability spec patterns, but preserves exclusions.**
89
+ A selected spec runs under each capability that does not exclude it. In the example above, a
90
+ multi-Channel spec runs only under its variant, while a default spec also runs under the variant.
91
+ Config-dependent specs must read the live config and skip when it doesn't apply (below).
87
92
 
88
93
  ### Read the live config in a spec
89
94
 
@@ -114,8 +119,10 @@ the configuration that was actually found.
114
119
  A seed configured with an interactive login (`ssoAuth` in the system config) reports readiness only
115
120
  **after** someone signs in — without automation, `npm test` sits on the login window until the
116
121
  startup timeout and no spec runs. The service's `login` option drives that window while the
117
- platform boots. Requires `@interopio/wdio-iocd-service` 1.0.0-next.3+ (the version currently
118
- generated) and io.Connect Desktop 10.4+; older test packages must update `tests/package.json` first.
122
+ platform boots. The option was introduced in `@interopio/wdio-iocd-service` 1.0.0-next.3; the handler
123
+ API and supported platform versions vary by service release. Check the installed service's README
124
+ and `dist/services/loginOptions.d.ts` before adapting a login flow. The example below uses the
125
+ `LoginBrowser` API in 1.0.0-next.7, whose login discovery also supports platforms before 10.4.
119
126
 
120
127
  ```ts
121
128
  // tests/wdio.config.ts
@@ -125,8 +132,14 @@ services: IOCDService({
125
132
  handler: async (browser) => {
126
133
  const username = browser.$("#username");
127
134
  const password = browser.$("#password");
128
- await username.waitForExist({ timeout: 30000 });
129
- await password.waitForExist({ timeout: 30000 });
135
+ await browser.waitUntil(() => username.isExisting(), {
136
+ timeout: 30000,
137
+ timeoutMsg: "The login username field did not appear",
138
+ });
139
+ await browser.waitUntil(() => password.isExisting(), {
140
+ timeout: 30000,
141
+ timeoutMsg: "The login password field did not appear",
142
+ });
130
143
  await username.setValue(process.env.IOCD_TEST_USER!);
131
144
  await password.setValue(process.env.IOCD_TEST_PASSWORD!);
132
145
  await browser.$("button[type=submit]").click();
@@ -137,9 +150,12 @@ services: IOCDService({
137
150
 
138
151
  Rules:
139
152
 
140
- - **The handler runs before the platform is ready.** No `browser.io()`, no models — plain
141
- WebdriverIO against the login page only. The WDIO globals (`browser`, `$`, `$$`) are set while it
142
- runs, so page objects work.
153
+ - **The handler runs before the platform is ready.** No `browser.io()` or models. In next.7 it
154
+ receives `LoginBrowser`, a subset of WebdriverIO driven over CDP. Use `$`, `getUrl`, `getTitle`,
155
+ `waitUntil`, and `execute`; its elements support `setValue`, `click`, `getText`, `getValue`,
156
+ `isExisting`, and `waitUntil`. Check the installed `dist/services/loginBrowser.d.ts` for the
157
+ supported surface. WDIO globals resolve during the handler, but `$$` throws and element methods
158
+ such as `waitForExist` are unavailable. Page objects must use only the supported subset.
143
159
  - **The signed-in session belongs to the platform process**, so the apps under test are
144
160
  authenticated exactly as in production.
145
161
  - **Credentials come from env vars / CI secrets** — never from specs or config files.
@@ -155,10 +171,17 @@ Rules:
155
171
 
156
172
  ## Add an optional IO library
157
173
 
158
- 1. Add the package to `tests/package.json`.
174
+ For libraries that support the worker's Node context:
175
+
176
+ 1. Add the package to `tests/package.json` and install it in the test package.
159
177
  2. Add its module specifier to `iocd:options.ioInitOptions.libraries` in `tests/wdio.config.ts`.
160
178
  3. Use it via `browser.io()` (optional sub-APIs need `!`).
161
179
 
180
+ Renderer-scoped libraries, such as a Modals client that requires an app window, must be installed
181
+ and initialized in a real fixture or application window. Adding them to the worker's `libraries`
182
+ does not supply that window context. See [authoring](./authoring.md#worker-api-vs-app-window-apis)
183
+ for the readiness and interaction pattern.
184
+
162
185
  ## Runners
163
186
 
164
187
  `platform-wdio` is the default (specs run Node-side and drive the app remotely). `platform-window`,
@@ -1,27 +1,29 @@
1
1
  # Built-in IOCD UI Models
2
2
 
3
3
  `@interopio/wdio-iocd-service/models` exports page objects for built-in web UI owned by IOCD. Use
4
- them for Web Groups, captions, tabs, Channel Selectors, and similar platform surfaces. Use normal
5
- WebdriverIO selectors for UI owned by the client app.
4
+ available models for Web Groups, captions, tabs, Channel Selectors, dialogs, and other platform
5
+ surfaces. Use normal WebdriverIO selectors for UI owned by the client app.
6
6
 
7
- Models hide generated element IDs and DOM structure behind semantic operations such as
8
- `group.switchTo()`, `group.frames.waitForCount()`, `tab.close()`, and `waitForDisplayed()`. They also
9
- handle the WebDriver context and waits expected by the built-in surface. Prefer an available model
10
- over copying internal IOCD selectors into a spec.
7
+ ## Inspect the installed version first
11
8
 
12
- ## Current Generated Version
9
+ The generated test package requests the service's `next` tag, so its available models can change
10
+ between installations. The installed package and lockfile are the source of truth. Before choosing
11
+ or importing a model, read these files from the test package:
13
12
 
14
- The generated test package currently pins `@interopio/wdio-iocd-service@1.0.0-next.4`. Its
15
- `@interopio/wdio-iocd-service/models` subpath exports:
13
+ ```text
14
+ tests/node_modules/@interopio/wdio-iocd-service/package.json
15
+ tests/node_modules/@interopio/wdio-iocd-service/dist/models/index.d.ts
16
+ ```
16
17
 
17
- | Area | Models and types |
18
- | --- | --- |
19
- | Group and frames | `Group`, `Frame`, `WebGroupElement` |
20
- | Captions and tabs | `CaptionBar`, `Caption`, `Editor`, `MoveArea`, `TabBar`, `TabHeader`, `Tab`, `Tabs`, `PinnedTabs` |
21
- | Caption controls | `ChannelSelector`, `CloseButton`, `HtmlButtons`, `ButtonsContainer`, `StandardButton`, `StandardButtons`, `CustomButton`, `CustomButtons` |
22
- | Sizing and snapping | `SizingBorderGroup`, `SizingBorderOrientation`, `SizingBorder`, `SizingBorderType`, `ApproachingMarker`, and the `NeighborFrames` type |
18
+ Then follow the export to its declaration file for constructor arguments, methods, waits, and
19
+ window-context behavior. Check for a suitable popup or dialog model before writing handle-scanning
20
+ helpers or copying internal selectors. Do not assume that a model shown in another repository or
21
+ service version exists locally, or that a missing model in an older version is still unavailable.
23
22
 
24
- For example:
23
+ ## Web Group example
24
+
25
+ The generated Channel Selector spec demonstrates the `Group` model. After verifying its installed
26
+ signature:
25
27
 
26
28
  ```ts
27
29
  import { Group } from "@interopio/wdio-iocd-service/models";
@@ -34,19 +36,10 @@ await frame.captionBar.channelSelector.waitForDisplayed();
34
36
  await frame.captionBar.channelSelector.click();
35
37
  ```
36
38
 
37
- ## Version Rule
38
-
39
- The installed package is the source of truth. Before using a model, read:
40
-
41
- ```text
42
- tests/node_modules/@interopio/wdio-iocd-service/dist/models/index.d.ts
43
- ```
44
-
45
- Do not copy a model import from another repository or a newer service release without checking that
46
- it exists locally. Newer versions may add models such as `ChannelSelectorPopup`, `PlatformDialog`,
47
- `ModalsDialog`, or `Alert`; those names aren't exported by the version currently generated in the
48
- seed.
39
+ Models hide generated IDs and DOM structure behind semantic operations and provide the waits and
40
+ context switching supported by that version. Read their declarations before mixing model calls
41
+ with raw `browser.switchById()` or `browser.switchToWindow()` calls.
49
42
 
50
- Models reduce direct coupling to built-in markup, but remain coupled to the matching Desktop and
51
- service versions. If no suitable model is exported, prefer a stable `data-testid`, role, or other
52
- semantic selector and keep the version-sensitive selector in one local page object.
43
+ Models remain coupled to the matching Desktop and service versions. If no suitable model is
44
+ exported, prefer a stable `data-testid`, role, or other semantic selector and keep any
45
+ version-sensitive selector in one local page object.
@@ -13,9 +13,9 @@ npm test
13
13
  ```
14
14
 
15
15
  On the first run the CLI installs the nested test package's dependencies, resolves the executable
16
- from the configured `iocd` component, and passes its path to WebdriverIO via `BINARY_PATH`. If the
17
- executable isn't available yet, start the seed once with `npm run dev`, close it, then run `npm test`
18
- (the service launches and manages its own platform instance). The CLI also makes
16
+ from the configured `iocd` component, and passes its path to WebdriverIO via `BINARY_PATH`. The CLI
17
+ applies the seed's dev modifications before the service launches its own platform instance. If the
18
+ component or executable is missing, run `npm run setup` to install it. The CLI also makes
19
19
  `config/iocd.license.key` available to the test platform — keep the license out of specs, config,
20
20
  logs, and source control.
21
21
 
@@ -32,6 +32,18 @@ version. Fall back to `npx @interopio/iocd-cli@latest test setup` only when the
32
32
  on the CLI. Setup creates the test package and the agent skill, installs the test dependencies, and validates the
33
33
  setup with the TypeScript compiler. On failure it removes the files it created.
34
34
 
35
+ ## Existing test package
36
+
37
+ Check that the test package's `.gitignore` contains `user-data/`. Add that entry if it is missing;
38
+ older scaffolds did not include it. From the seed root, verify it with
39
+ `git check-ignore tests/user-data/Default/Cookies` (substitute the configured `testsDirectory`).
40
+ This path can be checked without creating or reading a cookie file. If the profile is already
41
+ tracked, an ignore rule does not untrack it: remove only that generated directory from the Git index
42
+ with `git rm -r --cached -- tests/user-data`, substituting the configured `testsDirectory` here too
43
+ and keeping its local files. For example, `testsDirectory: "quality/e2e"` requires
44
+ `git rm -r --cached -- quality/e2e/user-data`. Never include profile data in commits or CI artifacts;
45
+ it can contain authenticated sessions.
46
+
35
47
  ## Replacing an existing test package
36
48
 
37
49
  The CLI never overwrites or migrates an existing test directory. To replace one:
@@ -46,6 +58,7 @@ The CLI never overwrites or migrates an existing test directory. To replace one:
46
58
 
47
59
  ```text
48
60
  tests/ # test package (Node.js)
61
+ ├── .gitignore # excludes the generated user-data profile
49
62
  ├── package.json
50
63
  ├── tsconfig.json
51
64
  ├── wdio.config.ts # WebdriverIO + IOCDService config (platform-wdio runner)
@@ -0,0 +1 @@
1
+ user-data/
@@ -30,6 +30,7 @@ npm run typecheck
30
30
 
31
31
  - `wdio.config.ts` — WebdriverIO + `IOCDService` configuration (the `platform-wdio` runner).
32
32
  - `config/system.json` — test-only overrides applied to the test process.
33
+ - `.gitignore` — excludes the disposable `user-data/` browser profile from version control.
33
34
  - `tests/common/iocd.ts` — polling, event-waiting, and cleanup-registry helpers.
34
35
  - `tests/config/apps.json` — fixture app definitions.
35
36
  - `tests/assets/applications/` — static fixture app HTML.
@@ -75,7 +76,8 @@ platform, so group specifications by configuration rather than adding a capabili
75
76
  the variant specifications from the default capability so they don't run twice, keep the default
76
77
  capability on the configuration the seed ships, and let a configuration-dependent specification read
77
78
  `globalThis.ioCDPlatformConfig` at `describe` time to skip when it isn't running under its own
78
- capability (`npm test -- --spec` overrides the capability spec patterns).
79
+ capability (`npm test -- --spec` overrides capability spec patterns, but capability exclusions
80
+ still apply).
79
81
 
80
82
  ## Sample specifications
81
83
 
@@ -117,5 +119,5 @@ package. Use ordinary WebdriverIO selectors for UI owned by your app.
117
119
 
118
120
  ## More
119
121
 
120
- - Full guide: the **Testing** section of the io.Connect Desktop seed-project documentation.
122
+ - Full guide: [Testing in the io.Connect Desktop seed-project documentation](https://docs.interop.io/desktop/developers/seed-project/index.html#testing).
121
123
  - Coding-agent guidance: `.agents/skills/iocd-wdio-testing/` at the seed root.
@@ -25,16 +25,20 @@ export function createCleanupRegistry() {
25
25
  async run(): Promise<void> {
26
26
  const pending = cleanups.splice(0).reverse();
27
27
  let firstError: unknown;
28
+ let failed = false;
28
29
 
29
30
  for (const cleanup of pending) {
30
31
  try {
31
32
  await cleanup();
32
33
  } catch (error) {
33
- firstError ??= error;
34
+ if (!failed) {
35
+ firstError = error;
36
+ failed = true;
37
+ }
34
38
  }
35
39
  }
36
40
 
37
- if (firstError) {
41
+ if (failed) {
38
42
  throw firstError;
39
43
  }
40
44
  },
@@ -59,67 +63,83 @@ export async function waitForEvent<T>(
59
63
 
60
64
  let resolveResult!: (value: T) => void;
61
65
  let rejectResult!: (error: Error) => void;
62
- let subscriptionCleanup: Cleanup | undefined;
63
- let cleanupRequested = false;
66
+ let cleanupPromise: Promise<void> | undefined;
64
67
  let settled = false;
65
68
 
66
69
  const result = new Promise<T>((resolve, reject) => {
67
70
  resolveResult = resolve;
68
71
  rejectResult = reject;
69
72
  });
73
+ // The trigger can fail or outlast the timeout before the caller awaits result.
74
+ // Observe rejection immediately, without changing what awaiting result throws.
75
+ void result.catch(() => undefined);
70
76
 
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();
77
+ const cleanupSubscription = (): Promise<void> => {
78
+ cleanupPromise ??= subscription.then(cleanup => cleanup());
79
+ return cleanupPromise;
80
80
  };
81
81
 
82
- const timeout = setTimeout(() => {
82
+ const finish = (outcome: { value: T } | { error: Error }): void => {
83
83
  if (settled) {
84
84
  return;
85
85
  }
86
86
 
87
87
  settled = true;
88
- void (async () => {
89
- try {
90
- await unsubscribe();
91
- } finally {
92
- rejectResult(new Error(timeoutMsg));
88
+ clearTimeout(timeout);
89
+ void cleanupSubscription().then(
90
+ () => {
91
+ if ("error" in outcome) {
92
+ rejectResult(outcome.error);
93
+ } else {
94
+ resolveResult(outcome.value);
95
+ }
96
+ },
97
+ (error: unknown) => {
98
+ rejectResult("error" in outcome ? outcome.error : toError(error));
93
99
  }
94
- })().catch(() => undefined);
95
- }, timeoutMs);
100
+ );
101
+ };
96
102
 
97
- try {
98
- subscriptionCleanup = await subscribe((value) => {
99
- if (settled || !predicate(value)) {
103
+ const timeout = setTimeout(
104
+ () => finish({ error: new Error(timeoutMsg) }),
105
+ timeoutMs
106
+ );
107
+ // Start in a microtask so even a synchronous callback can wait for the returned
108
+ // unsubscribe function. Success must not hide an error from delayed cleanup.
109
+ const subscription = Promise.resolve().then(() =>
110
+ subscribe(value => {
111
+ if (settled) {
100
112
  return;
101
113
  }
102
114
 
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)));
115
+ try {
116
+ if (predicate(value)) {
117
+ finish({ value });
111
118
  }
112
- })();
113
- });
119
+ } catch (error) {
120
+ finish({ error: toError(error) });
121
+ }
122
+ })
123
+ );
114
124
 
115
- if (cleanupRequested) {
116
- await unsubscribe();
125
+ const unsubscribe: Cleanup = async () => {
126
+ if (!settled) {
127
+ settled = true;
128
+ clearTimeout(timeout);
129
+ rejectResult(new Error("Event wait was cancelled."));
117
130
  }
131
+ await cleanupSubscription();
132
+ };
133
+
134
+ try {
135
+ await subscription;
118
136
  } catch (error) {
119
- settled = true;
120
- clearTimeout(timeout);
121
- rejectResult(error instanceof Error ? error : new Error(String(error)));
137
+ finish({ error: toError(error) });
122
138
  }
123
139
 
124
140
  return { result, unsubscribe };
125
- }
141
+ }
142
+
143
+ function toError(error: unknown): Error {
144
+ return error instanceof Error ? error : new Error(String(error));
145
+ }
@@ -1,5 +1,4 @@
1
1
  import { expect } from "chai";
2
- import type { IOConnectDesktop } from "@interopio/desktop";
3
2
  import { $, browser } from "@wdio/globals";
4
3
  import { createCleanupRegistry, waitForEvent } from "./common/iocd";
5
4
 
@@ -8,21 +7,19 @@ import { createCleanupRegistry, waitForEvent } from "./common/iocd";
8
7
  describe("Shared Contexts", () => {
9
8
  const contextName = "IOCD.Sample.SelectedClient";
10
9
  const clientName = "Ada Lovelace";
11
- let instances: IOConnectDesktop.AppManager.Instance[] = [];
12
10
  let cleanups = createCleanupRegistry();
13
11
 
14
12
  beforeEach(async () => {
15
13
  const io = await browser.io();
16
14
  cleanups = createCleanupRegistry();
15
+ // Register first so the context is removed after subscriptions and apps.
16
+ cleanups.add(() => io.contexts.destroy(contextName).catch(() => undefined));
17
17
  await io.contexts.destroy(contextName).catch(() => undefined);
18
18
  });
19
19
 
20
20
  afterEach(async () => {
21
- const io = await browser.io();
21
+ // The registry attempts every cleanup, even when an earlier one fails.
22
22
  await cleanups.run();
23
- await Promise.all(instances.map((instance) => instance.stop().catch(() => undefined)));
24
- instances = [];
25
- await io.contexts.destroy(contextName).catch(() => undefined);
26
23
  });
27
24
 
28
25
  it("delivers an update from one app instance to another", async () => {
@@ -30,25 +27,30 @@ describe("Shared Contexts", () => {
30
27
  const app = io.appManager.application("shared-context-client")!;
31
28
 
32
29
  const publisher = await app.start({}, { title: "Context Publisher" });
30
+ cleanups.add(() => publisher.stop());
33
31
  const subscriber = await app.start({}, { title: "Context Subscriber" });
34
- instances = [publisher, subscriber];
32
+ cleanups.add(() => subscriber.stop());
35
33
 
36
34
  const publisherWindow = await publisher.getWindow();
37
35
  const subscriberWindow = await subscriber.getWindow();
38
36
 
39
37
  // Wait until the receiving app has subscribed before publishing the update.
40
38
  await browser.switchById(subscriberWindow.id);
41
- await browser.waitUntil(async () => (await $("#context-status").getText()) === "Ready", {
42
- timeoutMsg: "Expected the subscriber app to initialize its Shared Context subscription",
43
- });
39
+ await browser.waitUntil(
40
+ async () => (await $("#context-status").getText()) === "Ready",
41
+ {
42
+ timeoutMsg:
43
+ "Expected the subscriber app to initialize its Shared Context subscription",
44
+ }
45
+ );
44
46
 
45
47
  const contextUpdate = await waitForEvent<{ clientName?: string }>(
46
- (listener) =>
47
- io.contexts.subscribe(contextName, (data) => {
48
+ listener =>
49
+ io.contexts.subscribe(contextName, data => {
48
50
  listener(data as { clientName?: string });
49
51
  }),
50
52
  {
51
- predicate: (data) => data.clientName === clientName,
53
+ predicate: data => data.clientName === clientName,
52
54
  timeoutMsg: `Expected the ${contextName} context to select ${clientName}`,
53
55
  }
54
56
  );
@@ -62,10 +64,13 @@ describe("Shared Contexts", () => {
62
64
 
63
65
  await browser.switchById(subscriberWindow.id);
64
66
  const selectedClient = await $("#selected-client");
65
- await browser.waitUntil(async () => (await selectedClient.getText()) === clientName, {
66
- timeoutMsg: `Expected the subscriber app to receive ${clientName}`,
67
- });
67
+ await browser.waitUntil(
68
+ async () => (await selectedClient.getText()) === clientName,
69
+ {
70
+ timeoutMsg: `Expected the subscriber app to receive ${clientName}`,
71
+ }
72
+ );
68
73
 
69
74
  expect(await selectedClient.getText()).to.equal(clientName);
70
75
  });
71
- });
76
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interopio/iocd-cli",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "CLI tool for setting up, building and packaging io.Connect Desktop platforms",
5
5
  "engines": {
6
6
  "node": ">=22.0.0",