@partylayer/testing 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PartyLayer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # @partylayer/testing
2
+
3
+ Offline test foundation for PartyLayer: a **mock CIP-0103 wallet provider** with
4
+ configurable failure scenarios, a **controllable transaction lifecycle**, a
5
+ **session-lifecycle harness** over the real `@partylayer/session` store, **TanStack
6
+ Query** test utilities, and **browser/e2e primitives** — so unit, integration, and
7
+ real-browser tests run with no DevNet or live-wallet dependency.
8
+
9
+ The TanStack Query utilities live in the `@partylayer/testing/query` subpath
10
+ (`@tanstack/query-core` is an optional peer) so the main entry stays
11
+ dependency-free for non-Query consumers.
12
+
13
+ ## A. Mock CIP-0103 wallet — `createMockWallet(config?)`
14
+
15
+ Returns a real `CIP0103Provider`, built by wrapping a configurable in-memory
16
+ client in the repo's canonical `createProviderBridge`. **The default/happy
17
+ config passes `runCIP0103ConformanceTests` by construction** (it is the
18
+ conformance reference implementation with a mock backend).
19
+
20
+ ```ts
21
+ import { createMockWallet } from '@partylayer/testing';
22
+
23
+ const provider = createMockWallet(); // happy path
24
+ await provider.request({ method: 'connect' }); // { isConnected: true }
25
+
26
+ // connect succeeds but submission fails:
27
+ const flaky = createMockWallet({ scenarios: { submitTransaction: 'synchronizerError' } });
28
+ ```
29
+
30
+ ### Failure scenarios (per-method, existing error codes only)
31
+
32
+ Scenarios are toggled per method. Every named scenario maps to a code that
33
+ **already exists** in `@partylayer/provider`'s error model — no new codes are
34
+ invented. You may also pass a raw `ProviderRpcError` or a `{ code, message }`.
35
+
36
+ | scenario name | code | constructor |
37
+ |---|---|---|
38
+ | `userRejected` | `4001` (USER_REJECTED) | `userRejected()` |
39
+ | `insufficientTraffic` | `-32002` (RESOURCE_UNAVAILABLE) | `resourceUnavailable()` |
40
+ | `synchronizerError` | `4901` (CHAIN_DISCONNECTED) | `chainDisconnected()` |
41
+ | `transactionTimeout` | `-32003` (TRANSACTION_REJECTED) | `transactionRejected()` |
42
+ | `genericError` | `-32603` (INTERNAL_ERROR) | `internalError()` |
43
+
44
+ `createMockWalletClient(config?)` exposes the underlying `BridgeableClient` as
45
+ an extension point for advanced wrapping/inspection.
46
+
47
+ ## B. Simulated transaction lifecycle — `createTransactionLifecycle(config?)`
48
+
49
+ A controllable lifecycle with phase flags
50
+ `isPreparing → isSubmitting → isConfirming → isFinalized` plus a `failed`
51
+ terminal, emitting the same CIP-0103 `txChanged` events the real provider does.
52
+
53
+ ```ts
54
+ import { createTransactionLifecycle } from '@partylayer/testing';
55
+
56
+ // manual stepping — deterministic, phase by phase
57
+ const lc = createTransactionLifecycle({ commandId: 'cmd-1' });
58
+ lc.on('txChanged', (e) => console.log(e.status));
59
+ lc.advance(); // → 'preparing' emits { status: 'pending' }
60
+ lc.advance(); // → 'submitting' emits { status: 'signed', payload }
61
+ lc.advance(); // → 'confirming' (no CIP-0103 event — see below)
62
+ lc.advance(); // → 'finalized' emits { status: 'executed', payload }
63
+ // or lc.fail() at any point → emits { status: 'failed' }
64
+
65
+ // auto mode — fake-timer friendly
66
+ const auto = createTransactionLifecycle({ delays: { preparing: 10, finalized: 50 } });
67
+ await auto.start(); // walks every phase using the delays
68
+ ```
69
+
70
+ Phase → `txChanged.status`: `preparing→pending`, `submitting→signed`,
71
+ `confirming→`(none)`, `finalized→executed`, `failed→failed`. CIP-0103 has no
72
+ "confirming" status (the union goes signed → executed); `isConfirming` is the
73
+ post-signed waiting flag the session layer surfaces.
74
+
75
+ ## C. Offline helpers
76
+
77
+ ```ts
78
+ import { createMockWallet, recordTxEvents, connectMock } from '@partylayer/testing';
79
+
80
+ const provider = createMockWallet();
81
+ const rec = recordTxEvents(provider); // collect txChanged
82
+ await connectMock(provider);
83
+ await provider.request({ method: 'prepareExecute', params: { tx: {} } });
84
+ rec.statuses(); // ['pending', 'signed', 'executed']
85
+ rec.stop();
86
+ ```
87
+
88
+ Optional `delays` use `setTimeout`, so `vi.useFakeTimers()` +
89
+ `vi.advanceTimersByTimeAsync()` give tests full control over time.
90
+
91
+ ## D. Session-lifecycle harness — `createSessionHarness(config?)`
92
+
93
+ Drives a **real** `@partylayer/session` store through a controllable provider, so
94
+ each scenario exercises the store's own machinery (no synthetic shortcuts).
95
+
96
+ ```ts
97
+ import { createSessionHarness } from '@partylayer/testing';
98
+ import { vi } from 'vitest';
99
+
100
+ vi.useFakeTimers();
101
+ const h = createSessionHarness({ ttlMs: 30_000, onReauthRequired, advanceTimers: vi.advanceTimersByTimeAsync });
102
+ await h.connect();
103
+ await h.expire(); // advances the store's REAL expiry timer → session:expired
104
+ h.switchParty('party::b'); // real accountsChanged → party:changed
105
+ h.dropConnection(); // real statusChanged(false) → transient reconnect
106
+ const tabB = h.openTab(); // a 2nd store sharing the broadcast hub (multi-tab)
107
+ h.destroy(); tabB.destroy(); // per-harness teardown (children are separate)
108
+ ```
109
+
110
+ `expire()` advances the store's real `setTimeout`-based expiry — it never emits a
111
+ fake `session:expired`, so pass `advanceTimers` (e.g. `vi.advanceTimersByTimeAsync`)
112
+ and install fake timers.
113
+
114
+ ## E. Offline composition — `createOfflineHarness({ wallet?, session? })`
115
+
116
+ Wires a mock wallet to a real session store, fully offline:
117
+
118
+ ```ts
119
+ import { createOfflineHarness } from '@partylayer/testing';
120
+ const { provider, store, destroy } = createOfflineHarness({ wallet: { partyId: 'party::a' } });
121
+ ```
122
+
123
+ ## F. TanStack Query utilities — `@partylayer/testing/query`
124
+
125
+ ```ts
126
+ import {
127
+ createTestQueryClient, getQueryState, expectInvalidated, trackOptimisticRollback, createQueryHarness,
128
+ } from '@partylayer/testing/query';
129
+
130
+ const qc = createTestQueryClient(); // no retries, gcTime 0
131
+ const t = trackOptimisticRollback<number>(qc, ['count']);
132
+ t.apply(99); /* assert */ t.rollback(); /* assert restore */
133
+ const h = createQueryHarness({ wallet, session, query }); // offline harness + QueryClient
134
+ ```
135
+
136
+ ## G. Browser / e2e primitives
137
+
138
+ Framework-agnostic script strings (no Playwright dependency) for a real-browser
139
+ smoke (`mockWalletInjectionScript()`, `idbEntryCountScript(db)`, `sessionKeyDbName(origin)`),
140
+ injected via Playwright's `page.addInitScript` / `page.evaluate`. The smoke itself
141
+ lives in `apps/demo/e2e` and runs nightly.
@@ -0,0 +1,177 @@
1
+ export { M as MOCK_SCENARIO_NAMES, a as MockMethod, b as MockScenario, c as MockScenarioName, d as MockWalletClient, e as MockWalletConfig, O as OfflineHarness, T as TxEventRecorder, f as connectMock, g as createMockWallet, h as createMockWalletClient, i as createOfflineHarness, r as recordTxEvents, s as scenarioToError } from './offline-CELeTEq9.mjs';
2
+ import { CIP0103Provider, CIP0103TxChangedEvent } from '@partylayer/core';
3
+ import { ChannelFactory, SessionStore, SessionState, ExpiryOptions, RetryPolicy } from '@partylayer/session';
4
+ import '@partylayer/provider';
5
+
6
+ /**
7
+ * Simulated, controllable transaction lifecycle.
8
+ *
9
+ * Exposes the session-layer view (boolean phase flags
10
+ * isPreparing → isSubmitting → isConfirming → isFinalized, plus a `failed`
11
+ * terminal) AND emits the SAME CIP-0103 `txChanged` events the real provider
12
+ * emits, so tests can assert against either view.
13
+ *
14
+ * Two drive modes:
15
+ * - manual: `advance()` steps one phase at a time; `fail()` terminates.
16
+ * - auto: `start()` walks all phases using configurable per-phase delays
17
+ * (uses setTimeout — fake-timer friendly).
18
+ *
19
+ * Phase → CIP-0103 `txChanged.status` mapping:
20
+ * preparing → 'pending'
21
+ * submitting → 'signed' (payload: { signature, signedBy, party })
22
+ * confirming → (no CIP-0103 status — see note)
23
+ * finalized → 'executed' (payload: { updateId, completionOffset })
24
+ * failed → 'failed'
25
+ *
26
+ * NOTE — 'confirming' has no CIP-0103 `txChanged` status: the spec's tx union
27
+ * goes signed → executed with no intermediate "confirming" state. We still
28
+ * model `isConfirming` as the post-signed waiting window because the session
29
+ * layer surfaces it as a UI flag. The session-lifecycle harness
30
+ * (`createSessionHarness`) and the TanStack Query utilities
31
+ * (`@partylayer/testing/query`) build on top of this controller.
32
+ */
33
+
34
+ type LifecyclePhase = 'idle' | 'preparing' | 'submitting' | 'confirming' | 'finalized' | 'failed';
35
+ type LifecycleDelays = Partial<Record<'preparing' | 'submitting' | 'confirming' | 'finalized', number>>;
36
+ interface LifecycleConfig {
37
+ /** Command id stamped on every emitted event. */
38
+ commandId?: string;
39
+ /** Party id used in the 'signed' payload. */
40
+ party?: string;
41
+ /** Signature used in the 'signed' payload. */
42
+ signature?: string;
43
+ /** Update id used in the 'executed' payload. */
44
+ updateId?: string;
45
+ /**
46
+ * Optional provider to ALSO emit `txChanged` onto (in addition to this
47
+ * controller's own listeners), so events surface on a mock wallet's bus.
48
+ */
49
+ provider?: CIP0103Provider;
50
+ /** Per-phase delays for auto mode (`start()`). Default 0. */
51
+ delays?: LifecycleDelays;
52
+ }
53
+ interface TransactionLifecycle {
54
+ readonly commandId: string;
55
+ readonly phase: LifecyclePhase;
56
+ readonly isPreparing: boolean;
57
+ readonly isSubmitting: boolean;
58
+ readonly isConfirming: boolean;
59
+ readonly isFinalized: boolean;
60
+ readonly isFailed: boolean;
61
+ /** Subscribe to `txChanged`. Returns an unsubscribe function. */
62
+ on(event: string, listener: (event: CIP0103TxChangedEvent) => void): () => void;
63
+ /** Manual step to the next phase; emits the mapped event. Returns the new phase. */
64
+ advance(): LifecyclePhase;
65
+ /** Terminal failure: emits `txChanged` `{ status: 'failed' }`. */
66
+ fail(): void;
67
+ /** Auto mode: walk every phase with configured delays. Resolves at 'finalized'. */
68
+ start(): Promise<void>;
69
+ /** Reset back to 'idle' (does not emit). */
70
+ reset(): void;
71
+ }
72
+ declare function createTransactionLifecycle(config?: LifecycleConfig): TransactionLifecycle;
73
+
74
+ /**
75
+ * Session-lifecycle simulation harness.
76
+ *
77
+ * Drives a REAL `@partylayer/session` store through a controllable CIP-0103
78
+ * provider, so every scenario exercises the store's own machinery — not
79
+ * synthetic shortcuts:
80
+ * - `expire()` advances the store's REAL expiry timer (the
81
+ * `session:expired` / `onReauthRequired` path); it never
82
+ * emits a fake `session:expired`. Requires fake-timer
83
+ * control via `advanceTimers` (the store arms expiry
84
+ * with `setTimeout`).
85
+ * - `dropConnection()` / emit the provider's real `statusChanged` CIP-0103
86
+ * `restoreConnection()` event — the same signal a live wallet sends — driving
87
+ * the store's transient-disconnect / reconnect path.
88
+ * - `switchParty()` emits a real `accountsChanged` with a new primary,
89
+ * driving the store's `party:changed` detection.
90
+ * - `openTab()` returns a second harness whose store shares this
91
+ * harness's in-memory BroadcastChannel hub, so a
92
+ * disconnect in one tab propagates to the other.
93
+ *
94
+ * Lifecycle: each harness OWNS its own store; `destroy()` is per-harness (a
95
+ * child from `openTab()` must be destroyed by the caller — it is not torn down
96
+ * with its parent).
97
+ */
98
+
99
+ interface SessionHarnessConfig {
100
+ /** Initial primary party id. Default `party::harness-1`. */
101
+ partyId?: string;
102
+ /** CAIP-2 network id reported by the provider. Default `canton:da-devnet`. */
103
+ networkId?: string;
104
+ /** Expiry TTL (ms) armed on connect. Default `60_000`. Drive it with `expire()`. */
105
+ ttlMs?: number;
106
+ /** App re-auth hook invoked by the store's real expiry path. */
107
+ onReauthRequired?: ExpiryOptions['onReauthRequired'];
108
+ /** Reconnect policy passed straight to the store (default: store default). */
109
+ reconnect?: RetryPolicy | false;
110
+ /**
111
+ * Advance timers to fire the store's REAL `setTimeout`-based expiry/backoff —
112
+ * e.g. `vi.advanceTimersByTimeAsync`. Required for `expire()` and for
113
+ * deterministic reconnect timing. Omit only if you don't call `expire()`.
114
+ */
115
+ advanceTimers?: (ms: number) => void | Promise<void>;
116
+ /** @internal shared multi-tab hub (set by `openTab`). */
117
+ _hub?: ChannelHub;
118
+ }
119
+ interface SessionHarness {
120
+ /** The live session store under test. */
121
+ readonly store: SessionStore;
122
+ /** The controllable CIP-0103 provider backing the store. */
123
+ readonly provider: CIP0103Provider;
124
+ /** Connect via the real store flow (arms the expiry timer). */
125
+ connect(): Promise<SessionState>;
126
+ /** Fire a real transient `statusChanged(false)` (NOT an explicit disconnect). */
127
+ dropConnection(): void;
128
+ /** Fire a real `statusChanged(true)` to restore the connection. */
129
+ restoreConnection(): void;
130
+ /** Fire a real `accountsChanged` with a new primary → `party:changed`. */
131
+ switchParty(partyId: string): void;
132
+ /** Advance the store's REAL expiry timer past its TTL (no synthetic emit). */
133
+ expire(): Promise<void>;
134
+ /** A second harness sharing this one's broadcast hub (simulates another tab). */
135
+ openTab(config?: Omit<SessionHarnessConfig, '_hub'>): SessionHarness;
136
+ /** Tear down THIS harness's store/provider (per-harness; children are separate). */
137
+ destroy(): void;
138
+ }
139
+ /** A synchronous in-memory BroadcastChannel hub shared across tabs. */
140
+ interface ChannelHub {
141
+ factory: ChannelFactory;
142
+ }
143
+ /** Build a hub whose channels deliver to OTHER instances only (no echo to sender). */
144
+ declare function createChannelHub(): ChannelHub;
145
+ declare function createSessionHarness(config?: SessionHarnessConfig): SessionHarness;
146
+
147
+ /**
148
+ * Reusable browser-test primitives for a real-browser (Playwright) smoke —
149
+ * framework-agnostic: every helper returns a STRING of JS to run via
150
+ * `page.addInitScript(...)` / `page.evaluate(...)`, so this package takes NO
151
+ * dependency on Playwright. The actual smoke lives in `apps/demo/e2e`.
152
+ */
153
+ interface MockWalletInjectionOptions {
154
+ /** Key on `window.canton` the wallet registers under. Default `mock`. */
155
+ walletId?: string;
156
+ /** Primary party id the mock reports. Default `party::e2e-1`. */
157
+ partyId?: string;
158
+ /** CAIP-2 network id. Default `canton:da-devnet`. */
159
+ networkId?: string;
160
+ }
161
+ /**
162
+ * An init script that installs a minimal CIP-0103-shaped provider at
163
+ * `window.canton[walletId]` BEFORE the app loads, so a real-browser test can
164
+ * drive the connect flow with no extension/live wallet. Inject via
165
+ * `page.addInitScript({ content: mockWalletInjectionScript() })`.
166
+ */
167
+ declare function mockWalletInjectionScript(options?: MockWalletInjectionOptions): string;
168
+ /**
169
+ * A script returning the number of object stores' total entries for an IndexedDB
170
+ * database (or -1 if the DB does not exist) — used to assert encrypted-session
171
+ * persistence engaged after a connect. Run via `page.evaluate(idbEntryCountScript(db))`.
172
+ */
173
+ declare function idbEntryCountScript(dbName: string): string;
174
+ /** The origin-bound IndexedDB name the session key store uses for a given origin. */
175
+ declare function sessionKeyDbName(origin: string): string;
176
+
177
+ export { type ChannelHub, type LifecycleConfig, type LifecycleDelays, type LifecyclePhase, type MockWalletInjectionOptions, type SessionHarness, type SessionHarnessConfig, type TransactionLifecycle, createChannelHub, createSessionHarness, createTransactionLifecycle, idbEntryCountScript, mockWalletInjectionScript, sessionKeyDbName };
@@ -0,0 +1,177 @@
1
+ export { M as MOCK_SCENARIO_NAMES, a as MockMethod, b as MockScenario, c as MockScenarioName, d as MockWalletClient, e as MockWalletConfig, O as OfflineHarness, T as TxEventRecorder, f as connectMock, g as createMockWallet, h as createMockWalletClient, i as createOfflineHarness, r as recordTxEvents, s as scenarioToError } from './offline-CELeTEq9.js';
2
+ import { CIP0103Provider, CIP0103TxChangedEvent } from '@partylayer/core';
3
+ import { ChannelFactory, SessionStore, SessionState, ExpiryOptions, RetryPolicy } from '@partylayer/session';
4
+ import '@partylayer/provider';
5
+
6
+ /**
7
+ * Simulated, controllable transaction lifecycle.
8
+ *
9
+ * Exposes the session-layer view (boolean phase flags
10
+ * isPreparing → isSubmitting → isConfirming → isFinalized, plus a `failed`
11
+ * terminal) AND emits the SAME CIP-0103 `txChanged` events the real provider
12
+ * emits, so tests can assert against either view.
13
+ *
14
+ * Two drive modes:
15
+ * - manual: `advance()` steps one phase at a time; `fail()` terminates.
16
+ * - auto: `start()` walks all phases using configurable per-phase delays
17
+ * (uses setTimeout — fake-timer friendly).
18
+ *
19
+ * Phase → CIP-0103 `txChanged.status` mapping:
20
+ * preparing → 'pending'
21
+ * submitting → 'signed' (payload: { signature, signedBy, party })
22
+ * confirming → (no CIP-0103 status — see note)
23
+ * finalized → 'executed' (payload: { updateId, completionOffset })
24
+ * failed → 'failed'
25
+ *
26
+ * NOTE — 'confirming' has no CIP-0103 `txChanged` status: the spec's tx union
27
+ * goes signed → executed with no intermediate "confirming" state. We still
28
+ * model `isConfirming` as the post-signed waiting window because the session
29
+ * layer surfaces it as a UI flag. The session-lifecycle harness
30
+ * (`createSessionHarness`) and the TanStack Query utilities
31
+ * (`@partylayer/testing/query`) build on top of this controller.
32
+ */
33
+
34
+ type LifecyclePhase = 'idle' | 'preparing' | 'submitting' | 'confirming' | 'finalized' | 'failed';
35
+ type LifecycleDelays = Partial<Record<'preparing' | 'submitting' | 'confirming' | 'finalized', number>>;
36
+ interface LifecycleConfig {
37
+ /** Command id stamped on every emitted event. */
38
+ commandId?: string;
39
+ /** Party id used in the 'signed' payload. */
40
+ party?: string;
41
+ /** Signature used in the 'signed' payload. */
42
+ signature?: string;
43
+ /** Update id used in the 'executed' payload. */
44
+ updateId?: string;
45
+ /**
46
+ * Optional provider to ALSO emit `txChanged` onto (in addition to this
47
+ * controller's own listeners), so events surface on a mock wallet's bus.
48
+ */
49
+ provider?: CIP0103Provider;
50
+ /** Per-phase delays for auto mode (`start()`). Default 0. */
51
+ delays?: LifecycleDelays;
52
+ }
53
+ interface TransactionLifecycle {
54
+ readonly commandId: string;
55
+ readonly phase: LifecyclePhase;
56
+ readonly isPreparing: boolean;
57
+ readonly isSubmitting: boolean;
58
+ readonly isConfirming: boolean;
59
+ readonly isFinalized: boolean;
60
+ readonly isFailed: boolean;
61
+ /** Subscribe to `txChanged`. Returns an unsubscribe function. */
62
+ on(event: string, listener: (event: CIP0103TxChangedEvent) => void): () => void;
63
+ /** Manual step to the next phase; emits the mapped event. Returns the new phase. */
64
+ advance(): LifecyclePhase;
65
+ /** Terminal failure: emits `txChanged` `{ status: 'failed' }`. */
66
+ fail(): void;
67
+ /** Auto mode: walk every phase with configured delays. Resolves at 'finalized'. */
68
+ start(): Promise<void>;
69
+ /** Reset back to 'idle' (does not emit). */
70
+ reset(): void;
71
+ }
72
+ declare function createTransactionLifecycle(config?: LifecycleConfig): TransactionLifecycle;
73
+
74
+ /**
75
+ * Session-lifecycle simulation harness.
76
+ *
77
+ * Drives a REAL `@partylayer/session` store through a controllable CIP-0103
78
+ * provider, so every scenario exercises the store's own machinery — not
79
+ * synthetic shortcuts:
80
+ * - `expire()` advances the store's REAL expiry timer (the
81
+ * `session:expired` / `onReauthRequired` path); it never
82
+ * emits a fake `session:expired`. Requires fake-timer
83
+ * control via `advanceTimers` (the store arms expiry
84
+ * with `setTimeout`).
85
+ * - `dropConnection()` / emit the provider's real `statusChanged` CIP-0103
86
+ * `restoreConnection()` event — the same signal a live wallet sends — driving
87
+ * the store's transient-disconnect / reconnect path.
88
+ * - `switchParty()` emits a real `accountsChanged` with a new primary,
89
+ * driving the store's `party:changed` detection.
90
+ * - `openTab()` returns a second harness whose store shares this
91
+ * harness's in-memory BroadcastChannel hub, so a
92
+ * disconnect in one tab propagates to the other.
93
+ *
94
+ * Lifecycle: each harness OWNS its own store; `destroy()` is per-harness (a
95
+ * child from `openTab()` must be destroyed by the caller — it is not torn down
96
+ * with its parent).
97
+ */
98
+
99
+ interface SessionHarnessConfig {
100
+ /** Initial primary party id. Default `party::harness-1`. */
101
+ partyId?: string;
102
+ /** CAIP-2 network id reported by the provider. Default `canton:da-devnet`. */
103
+ networkId?: string;
104
+ /** Expiry TTL (ms) armed on connect. Default `60_000`. Drive it with `expire()`. */
105
+ ttlMs?: number;
106
+ /** App re-auth hook invoked by the store's real expiry path. */
107
+ onReauthRequired?: ExpiryOptions['onReauthRequired'];
108
+ /** Reconnect policy passed straight to the store (default: store default). */
109
+ reconnect?: RetryPolicy | false;
110
+ /**
111
+ * Advance timers to fire the store's REAL `setTimeout`-based expiry/backoff —
112
+ * e.g. `vi.advanceTimersByTimeAsync`. Required for `expire()` and for
113
+ * deterministic reconnect timing. Omit only if you don't call `expire()`.
114
+ */
115
+ advanceTimers?: (ms: number) => void | Promise<void>;
116
+ /** @internal shared multi-tab hub (set by `openTab`). */
117
+ _hub?: ChannelHub;
118
+ }
119
+ interface SessionHarness {
120
+ /** The live session store under test. */
121
+ readonly store: SessionStore;
122
+ /** The controllable CIP-0103 provider backing the store. */
123
+ readonly provider: CIP0103Provider;
124
+ /** Connect via the real store flow (arms the expiry timer). */
125
+ connect(): Promise<SessionState>;
126
+ /** Fire a real transient `statusChanged(false)` (NOT an explicit disconnect). */
127
+ dropConnection(): void;
128
+ /** Fire a real `statusChanged(true)` to restore the connection. */
129
+ restoreConnection(): void;
130
+ /** Fire a real `accountsChanged` with a new primary → `party:changed`. */
131
+ switchParty(partyId: string): void;
132
+ /** Advance the store's REAL expiry timer past its TTL (no synthetic emit). */
133
+ expire(): Promise<void>;
134
+ /** A second harness sharing this one's broadcast hub (simulates another tab). */
135
+ openTab(config?: Omit<SessionHarnessConfig, '_hub'>): SessionHarness;
136
+ /** Tear down THIS harness's store/provider (per-harness; children are separate). */
137
+ destroy(): void;
138
+ }
139
+ /** A synchronous in-memory BroadcastChannel hub shared across tabs. */
140
+ interface ChannelHub {
141
+ factory: ChannelFactory;
142
+ }
143
+ /** Build a hub whose channels deliver to OTHER instances only (no echo to sender). */
144
+ declare function createChannelHub(): ChannelHub;
145
+ declare function createSessionHarness(config?: SessionHarnessConfig): SessionHarness;
146
+
147
+ /**
148
+ * Reusable browser-test primitives for a real-browser (Playwright) smoke —
149
+ * framework-agnostic: every helper returns a STRING of JS to run via
150
+ * `page.addInitScript(...)` / `page.evaluate(...)`, so this package takes NO
151
+ * dependency on Playwright. The actual smoke lives in `apps/demo/e2e`.
152
+ */
153
+ interface MockWalletInjectionOptions {
154
+ /** Key on `window.canton` the wallet registers under. Default `mock`. */
155
+ walletId?: string;
156
+ /** Primary party id the mock reports. Default `party::e2e-1`. */
157
+ partyId?: string;
158
+ /** CAIP-2 network id. Default `canton:da-devnet`. */
159
+ networkId?: string;
160
+ }
161
+ /**
162
+ * An init script that installs a minimal CIP-0103-shaped provider at
163
+ * `window.canton[walletId]` BEFORE the app loads, so a real-browser test can
164
+ * drive the connect flow with no extension/live wallet. Inject via
165
+ * `page.addInitScript({ content: mockWalletInjectionScript() })`.
166
+ */
167
+ declare function mockWalletInjectionScript(options?: MockWalletInjectionOptions): string;
168
+ /**
169
+ * A script returning the number of object stores' total entries for an IndexedDB
170
+ * database (or -1 if the DB does not exist) — used to assert encrypted-session
171
+ * persistence engaged after a connect. Run via `page.evaluate(idbEntryCountScript(db))`.
172
+ */
173
+ declare function idbEntryCountScript(dbName: string): string;
174
+ /** The origin-bound IndexedDB name the session key store uses for a given origin. */
175
+ declare function sessionKeyDbName(origin: string): string;
176
+
177
+ export { type ChannelHub, type LifecycleConfig, type LifecycleDelays, type LifecyclePhase, type MockWalletInjectionOptions, type SessionHarness, type SessionHarnessConfig, type TransactionLifecycle, createChannelHub, createSessionHarness, createTransactionLifecycle, idbEntryCountScript, mockWalletInjectionScript, sessionKeyDbName };