@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 +21 -0
- package/README.md +141 -0
- package/dist/index.d.mts +177 -0
- package/dist/index.d.ts +177 -0
- package/dist/index.js +481 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +467 -0
- package/dist/index.mjs.map +1 -0
- package/dist/offline-CELeTEq9.d.mts +146 -0
- package/dist/offline-CELeTEq9.d.ts +146 -0
- package/dist/query.d.mts +53 -0
- package/dist/query.d.ts +53 -0
- package/dist/query.js +206 -0
- package/dist/query.js.map +1 -0
- package/dist/query.mjs +200 -0
- package/dist/query.mjs.map +1 -0
- package/package.json +70 -0
package/dist/query.mjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
2
|
+
import '@partylayer/core';
|
|
3
|
+
import { createSessionStore, createMemoryStorage } from '@partylayer/session';
|
|
4
|
+
import { createProviderBridge, ProviderRpcError, internalError, transactionRejected, chainDisconnected, resourceUnavailable, userRejected } from '@partylayer/provider';
|
|
5
|
+
|
|
6
|
+
// src/query.ts
|
|
7
|
+
var PRESETS = {
|
|
8
|
+
userRejected: () => userRejected("User rejected the request"),
|
|
9
|
+
insufficientTraffic: () => resourceUnavailable("Insufficient traffic credits to submit the transaction"),
|
|
10
|
+
synchronizerError: () => chainDisconnected("Synchronizer error"),
|
|
11
|
+
transactionTimeout: () => transactionRejected("Transaction timed out"),
|
|
12
|
+
genericError: () => internalError("RPC handler error")
|
|
13
|
+
};
|
|
14
|
+
function scenarioToError(scenario) {
|
|
15
|
+
if (scenario instanceof ProviderRpcError) return scenario;
|
|
16
|
+
if (typeof scenario === "string") return PRESETS[scenario]();
|
|
17
|
+
return new ProviderRpcError(scenario.message, scenario.code);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/mock-wallet.ts
|
|
21
|
+
var DEFAULT_PARTY = "party::mock-1";
|
|
22
|
+
var DEFAULT_NETWORK = "devnet";
|
|
23
|
+
function wait(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
function createMockWalletClient(config = {}) {
|
|
27
|
+
const partyId = config.partyId ?? DEFAULT_PARTY;
|
|
28
|
+
const network = config.network ?? DEFAULT_NETWORK;
|
|
29
|
+
let connected = config.connected ?? false;
|
|
30
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
31
|
+
function makeSession() {
|
|
32
|
+
return {
|
|
33
|
+
sessionId: "sess-mock-1",
|
|
34
|
+
walletId: "mock",
|
|
35
|
+
partyId,
|
|
36
|
+
network,
|
|
37
|
+
expiresAt: Number.MAX_SAFE_INTEGER,
|
|
38
|
+
capabilitiesSnapshot: [
|
|
39
|
+
"connect",
|
|
40
|
+
"signMessage",
|
|
41
|
+
"signTransaction",
|
|
42
|
+
"submitTransaction",
|
|
43
|
+
"ledgerApi"
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function gate(method) {
|
|
48
|
+
const delay = config.delays?.[method];
|
|
49
|
+
if (delay && delay > 0) await wait(delay);
|
|
50
|
+
const scenario = config.scenarios?.[method];
|
|
51
|
+
if (scenario) throw scenarioToError(scenario);
|
|
52
|
+
}
|
|
53
|
+
function fire(event, payload) {
|
|
54
|
+
handlers.get(event)?.forEach((handler) => handler(payload));
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
async connect() {
|
|
58
|
+
await gate("connect");
|
|
59
|
+
connected = true;
|
|
60
|
+
const session = makeSession();
|
|
61
|
+
fire("session:connected", { type: "session:connected", session });
|
|
62
|
+
return session;
|
|
63
|
+
},
|
|
64
|
+
async disconnect() {
|
|
65
|
+
await gate("disconnect");
|
|
66
|
+
connected = false;
|
|
67
|
+
fire("session:disconnected", { type: "session:disconnected" });
|
|
68
|
+
},
|
|
69
|
+
async getActiveSession() {
|
|
70
|
+
await gate("getActiveSession");
|
|
71
|
+
return connected ? makeSession() : null;
|
|
72
|
+
},
|
|
73
|
+
async signMessage() {
|
|
74
|
+
await gate("signMessage");
|
|
75
|
+
return { signature: "mock-signature" };
|
|
76
|
+
},
|
|
77
|
+
async signTransaction() {
|
|
78
|
+
await gate("signTransaction");
|
|
79
|
+
return {
|
|
80
|
+
transactionHash: "mock-tx-hash",
|
|
81
|
+
signedTx: { data: "mock-signed-payload" },
|
|
82
|
+
partyId
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
async submitTransaction() {
|
|
86
|
+
await gate("submitTransaction");
|
|
87
|
+
return {
|
|
88
|
+
transactionHash: "mock-tx-hash",
|
|
89
|
+
submittedAt: 0,
|
|
90
|
+
commandId: "mock-command-1",
|
|
91
|
+
updateId: "mock-update-1"
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
async ledgerApi(params) {
|
|
95
|
+
await gate("ledgerApi");
|
|
96
|
+
return {
|
|
97
|
+
response: JSON.stringify({
|
|
98
|
+
requestMethod: params.requestMethod,
|
|
99
|
+
resource: params.resource
|
|
100
|
+
})
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
getRegistryStatus() {
|
|
104
|
+
return null;
|
|
105
|
+
},
|
|
106
|
+
on(event, handler) {
|
|
107
|
+
let set = handlers.get(event);
|
|
108
|
+
if (!set) {
|
|
109
|
+
set = /* @__PURE__ */ new Set();
|
|
110
|
+
handlers.set(event, set);
|
|
111
|
+
}
|
|
112
|
+
set.add(handler);
|
|
113
|
+
return () => {
|
|
114
|
+
handlers.get(event)?.delete(handler);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function createMockWallet(config = {}) {
|
|
120
|
+
return createProviderBridge(createMockWalletClient(config));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/offline.ts
|
|
124
|
+
function createOfflineHarness(config = {}) {
|
|
125
|
+
const provider = createMockWallet(config.wallet);
|
|
126
|
+
const store = createSessionStore(provider, {
|
|
127
|
+
storage: createMemoryStorage(),
|
|
128
|
+
...config.session
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
provider,
|
|
132
|
+
store,
|
|
133
|
+
destroy() {
|
|
134
|
+
store.destroy();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/query.ts
|
|
140
|
+
function createTestQueryClient(overrides) {
|
|
141
|
+
return new QueryClient({
|
|
142
|
+
...overrides,
|
|
143
|
+
defaultOptions: {
|
|
144
|
+
...overrides?.defaultOptions,
|
|
145
|
+
queries: {
|
|
146
|
+
retry: false,
|
|
147
|
+
gcTime: 0,
|
|
148
|
+
staleTime: 0,
|
|
149
|
+
...overrides?.defaultOptions?.queries
|
|
150
|
+
},
|
|
151
|
+
mutations: {
|
|
152
|
+
retry: false,
|
|
153
|
+
...overrides?.defaultOptions?.mutations
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function getQueryState(client, key) {
|
|
159
|
+
const state = client.getQueryState(key);
|
|
160
|
+
return {
|
|
161
|
+
data: state?.data,
|
|
162
|
+
status: state?.status ?? "pending",
|
|
163
|
+
fetchStatus: state?.fetchStatus ?? "idle",
|
|
164
|
+
isInvalidated: state?.isInvalidated ?? false
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function expectInvalidated(client, key) {
|
|
168
|
+
return client.getQueryState(key)?.isInvalidated === true;
|
|
169
|
+
}
|
|
170
|
+
function trackOptimisticRollback(client, key) {
|
|
171
|
+
const snapshot = client.getQueryData(key);
|
|
172
|
+
return {
|
|
173
|
+
snapshot,
|
|
174
|
+
apply(next) {
|
|
175
|
+
client.setQueryData(key, next);
|
|
176
|
+
},
|
|
177
|
+
rollback() {
|
|
178
|
+
client.setQueryData(key, snapshot);
|
|
179
|
+
},
|
|
180
|
+
current() {
|
|
181
|
+
return client.getQueryData(key);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function createQueryHarness(config = {}) {
|
|
186
|
+
const base = createOfflineHarness({ wallet: config.wallet, session: config.session });
|
|
187
|
+
const queryClient = createTestQueryClient(config.query);
|
|
188
|
+
return {
|
|
189
|
+
...base,
|
|
190
|
+
queryClient,
|
|
191
|
+
destroy() {
|
|
192
|
+
queryClient.clear();
|
|
193
|
+
base.destroy();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { createQueryHarness, createTestQueryClient, expectInvalidated, getQueryState, trackOptimisticRollback };
|
|
199
|
+
//# sourceMappingURL=query.mjs.map
|
|
200
|
+
//# sourceMappingURL=query.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scenarios.ts","../src/mock-wallet.ts","../src/offline.ts","../src/query.ts"],"names":[],"mappings":"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","file":"query.mjs","sourcesContent":["/**\n * Failure scenarios for the mock CIP-0103 wallet.\n *\n * IMPORTANT: every scenario maps to an EXISTING code in the repo's error\n * model (`@partylayer/provider` ProviderRpcError + RPC_ERRORS / JSON_RPC_ERRORS).\n * No new error codes are invented here. The named presets below are a\n * convenience layer over the existing convenience constructors; you can also\n * pass a raw `ProviderRpcError` or a `{ code, message }` pair to model any\n * other failure the provider error model already supports.\n *\n * Scenario → code mapping (all pre-existing codes):\n * userRejected → 4001 (RPC_ERRORS.USER_REJECTED) via userRejected()\n * insufficientTraffic → -32002 (JSON_RPC_ERRORS.RESOURCE_UNAVAILABLE) via resourceUnavailable()\n * synchronizerError → 4901 (RPC_ERRORS.CHAIN_DISCONNECTED) via chainDisconnected()\n * transactionTimeout → -32003 (JSON_RPC_ERRORS.TRANSACTION_REJECTED) via transactionRejected()\n * genericError → -32603 (JSON_RPC_ERRORS.INTERNAL_ERROR) via internalError()\n */\n\nimport {\n ProviderRpcError,\n chainDisconnected,\n internalError,\n resourceUnavailable,\n transactionRejected,\n userRejected,\n} from '@partylayer/provider';\n\n/** Built-in named failure scenarios. */\nexport type MockScenarioName =\n | 'userRejected'\n | 'insufficientTraffic'\n | 'synchronizerError'\n | 'transactionTimeout'\n | 'genericError';\n\n/**\n * A scenario is either a built-in name, a fully-formed `ProviderRpcError`, or\n * a `{ code, message }` pair (which must use an existing numeric code).\n */\nexport type MockScenario =\n | MockScenarioName\n | ProviderRpcError\n | { code: number; message: string };\n\nconst PRESETS: Record<MockScenarioName, () => ProviderRpcError> = {\n userRejected: () => userRejected('User rejected the request'),\n insufficientTraffic: () =>\n resourceUnavailable('Insufficient traffic credits to submit the transaction'),\n synchronizerError: () => chainDisconnected('Synchronizer error'),\n transactionTimeout: () => transactionRejected('Transaction timed out'),\n genericError: () => internalError('RPC handler error'),\n};\n\n/** All built-in scenario names (useful for table-driven tests). */\nexport const MOCK_SCENARIO_NAMES = Object.keys(PRESETS) as MockScenarioName[];\n\n/** Resolve a `MockScenario` into the `ProviderRpcError` the mock will throw. */\nexport function scenarioToError(scenario: MockScenario): ProviderRpcError {\n if (scenario instanceof ProviderRpcError) return scenario;\n if (typeof scenario === 'string') return PRESETS[scenario]();\n return new ProviderRpcError(scenario.message, scenario.code);\n}\n","/**\n * Mock CIP-0103 wallet provider.\n *\n * `createMockWallet(config)` returns a real, CIP-0103-compliant\n * `CIP0103Provider`. It is built by wrapping a configurable in-memory client\n * in the repo's canonical `createProviderBridge` from `@partylayer/provider`,\n * so the default/happy config passes `runCIP0103ConformanceTests` by\n * construction (it IS the conformance reference implementation, just with a\n * mock backend instead of a live wallet).\n *\n * Failure scenarios are toggled per-method (see ./scenarios). A test can make\n * `connect` succeed while `submitTransaction` fails, etc.\n *\n * Everything here is in-memory and synchronous-by-default — no DevNet, no live\n * wallet, no network. Optional per-method `delays` use `setTimeout` and are\n * fake-timer friendly (see ./offline).\n */\n\nimport type { CIP0103Provider } from '@partylayer/core';\nimport { createProviderBridge } from '@partylayer/provider';\nimport { scenarioToError, type MockScenario } from './scenarios';\n\n/**\n * The client shape `createProviderBridge` accepts. `@partylayer/provider` does\n * not export its `BridgeableClient` type publicly, so we derive it from the\n * factory signature — this stays correct automatically if the bridge's\n * contract changes, and contextually types the mock object literal below.\n */\nexport type MockWalletClient = Parameters<typeof createProviderBridge>[0];\n\n/** Methods on the mock client that can carry a scenario / delay. */\nexport type MockMethod =\n | 'connect'\n | 'disconnect'\n | 'getActiveSession'\n | 'signMessage'\n | 'signTransaction'\n | 'submitTransaction'\n | 'ledgerApi';\n\nexport interface MockWalletConfig {\n /** Party id reported by the mock session/accounts. */\n partyId?: string;\n /** Network id the bridge maps to CAIP-2 (e.g. 'devnet'). */\n network?: string;\n /** Whether a session is already active before `connect()` is called. */\n connected?: boolean;\n /** Per-method failure scenarios. Absent ⇒ that method succeeds. */\n scenarios?: Partial<Record<MockMethod, MockScenario>>;\n /** Per-method artificial delay in ms (fake-timer friendly). Default 0. */\n delays?: Partial<Record<MockMethod, number>>;\n}\n\nconst DEFAULT_PARTY = 'party::mock-1';\nconst DEFAULT_NETWORK = 'devnet';\n\nfunction wait(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Build the underlying `BridgeableClient`. Exposed as an extension point for\n * tests that want to wrap it differently or inspect it; most callers should\n * use `createMockWallet` instead.\n */\nexport function createMockWalletClient(config: MockWalletConfig = {}): MockWalletClient {\n const partyId = config.partyId ?? DEFAULT_PARTY;\n const network = config.network ?? DEFAULT_NETWORK;\n let connected = config.connected ?? false;\n\n const handlers = new Map<string, Set<(event: unknown) => void>>();\n\n function makeSession() {\n return {\n sessionId: 'sess-mock-1',\n walletId: 'mock',\n partyId,\n network,\n expiresAt: Number.MAX_SAFE_INTEGER,\n capabilitiesSnapshot: [\n 'connect',\n 'signMessage',\n 'signTransaction',\n 'submitTransaction',\n 'ledgerApi',\n ],\n };\n }\n\n /** Apply the configured delay, then throw the configured scenario (if any). */\n async function gate(method: MockMethod): Promise<void> {\n const delay = config.delays?.[method];\n if (delay && delay > 0) await wait(delay);\n const scenario = config.scenarios?.[method];\n if (scenario) throw scenarioToError(scenario);\n }\n\n function fire(event: string, payload: unknown): void {\n handlers.get(event)?.forEach((handler) => handler(payload));\n }\n\n return {\n async connect() {\n await gate('connect');\n connected = true;\n const session = makeSession();\n fire('session:connected', { type: 'session:connected', session });\n return session;\n },\n async disconnect() {\n await gate('disconnect');\n connected = false;\n fire('session:disconnected', { type: 'session:disconnected' });\n },\n async getActiveSession() {\n await gate('getActiveSession');\n return connected ? makeSession() : null;\n },\n async signMessage() {\n await gate('signMessage');\n return { signature: 'mock-signature' };\n },\n async signTransaction() {\n await gate('signTransaction');\n return {\n transactionHash: 'mock-tx-hash',\n signedTx: { data: 'mock-signed-payload' },\n partyId,\n };\n },\n async submitTransaction() {\n await gate('submitTransaction');\n return {\n transactionHash: 'mock-tx-hash',\n submittedAt: 0,\n commandId: 'mock-command-1',\n updateId: 'mock-update-1',\n };\n },\n async ledgerApi(params) {\n await gate('ledgerApi');\n return {\n response: JSON.stringify({\n requestMethod: params.requestMethod,\n resource: params.resource,\n }),\n };\n },\n getRegistryStatus() {\n return null;\n },\n on(event, handler) {\n let set = handlers.get(event);\n if (!set) {\n set = new Set();\n handlers.set(event, set);\n }\n set.add(handler as (event: unknown) => void);\n return () => {\n handlers.get(event)?.delete(handler as (event: unknown) => void);\n };\n },\n };\n}\n\n/**\n * Create a CIP-0103-compliant mock provider.\n *\n * Default config ⇒ a fully conformant happy-path provider. Pass `scenarios`\n * to make specific methods fail with the repo's existing error codes.\n *\n * @example\n * const provider = createMockWallet(); // happy path\n * const provider = createMockWallet({ // connect ok, submit fails\n * scenarios: { submitTransaction: 'synchronizerError' },\n * });\n */\nexport function createMockWallet(config: MockWalletConfig = {}): CIP0103Provider {\n return createProviderBridge(createMockWalletClient(config));\n}\n","/**\n * Offline test helpers.\n *\n * These let unit/integration tests run with NO DevNet / live-wallet\n * dependency. Everything is deterministic and fake-timer friendly: the mock\n * wallet and lifecycle use `setTimeout` only for optional configured delays,\n * so `vi.useFakeTimers()` + `vi.advanceTimersByTimeAsync()` fully control time.\n *\n * See ./__tests__/offline-example.test.ts for a full connect → submit →\n * finalize assertion against the mock with zero network access.\n */\n\nimport { CIP0103_EVENTS } from '@partylayer/core';\nimport type { CIP0103Provider, CIP0103TxChangedEvent } from '@partylayer/core';\nimport {\n createSessionStore,\n createMemoryStorage,\n type SessionStore,\n type SessionStoreOptions,\n} from '@partylayer/session';\nimport { createMockWallet, type MockWalletConfig } from './mock-wallet';\n\nexport interface TxEventRecorder {\n /** All `txChanged` events captured, in emission order. */\n readonly events: CIP0103TxChangedEvent[];\n /** Just the `status` field of each captured event, in order. */\n statuses(): CIP0103TxChangedEvent['status'][];\n /** Stop recording (removes the listener). */\n stop(): void;\n}\n\n/**\n * Subscribe to a provider's `txChanged` stream and collect every event.\n * Returns a recorder whose `events` array fills as events fire.\n */\nexport function recordTxEvents(provider: CIP0103Provider): TxEventRecorder {\n const events: CIP0103TxChangedEvent[] = [];\n const listener = (event: CIP0103TxChangedEvent): void => {\n events.push(event);\n };\n provider.on(CIP0103_EVENTS.TX_CHANGED, listener);\n return {\n events,\n statuses() {\n return events.map((e) => e.status);\n },\n stop() {\n provider.removeListener(CIP0103_EVENTS.TX_CHANGED, listener);\n },\n };\n}\n\n/**\n * Convenience: connect a mock provider via the CIP-0103 `connect` method.\n * Returns the `CIP0103ConnectResult`-shaped response.\n */\nexport async function connectMock(\n provider: CIP0103Provider,\n): Promise<{ isConnected: boolean }> {\n return provider.request<{ isConnected: boolean }>({ method: 'connect' });\n}\n\n/** A fully offline mock wallet + session store, wired together. */\nexport interface OfflineHarness {\n readonly provider: CIP0103Provider;\n readonly store: SessionStore;\n destroy(): void;\n}\n\n/**\n * Compose a mock CIP-0103 wallet and a real `@partylayer/session` store with NO\n * network/DevNet. `wallet` configures the mock (failure scenarios, delays,\n * party); `session` overrides store options (storage defaults to in-memory).\n * For TanStack Query-inclusive composition, see `@partylayer/testing/query`.\n */\nexport function createOfflineHarness(\n config: { wallet?: MockWalletConfig; session?: Partial<SessionStoreOptions> } = {},\n): OfflineHarness {\n const provider = createMockWallet(config.wallet);\n const store = createSessionStore(provider, {\n storage: createMemoryStorage(),\n ...config.session,\n });\n return {\n provider,\n store,\n destroy() {\n store.destroy();\n },\n };\n}\n","/**\n * TanStack Query test utilities — `@partylayer/testing/query`.\n *\n * Subpath entry so the main package stays dependency-free for non-Query\n * consumers. `@tanstack/query-core` is an OPTIONAL peer; install it only if you\n * import from here.\n */\nimport { QueryClient, type QueryClientConfig, type QueryKey } from '@tanstack/query-core';\nimport {\n createOfflineHarness,\n type OfflineHarness,\n} from './offline';\nimport type { MockWalletConfig } from './mock-wallet';\nimport type { SessionStoreOptions } from '@partylayer/session';\n\n/** A QueryClient with test-friendly defaults (no retries, no caching window). */\nexport function createTestQueryClient(overrides?: QueryClientConfig): QueryClient {\n return new QueryClient({\n ...overrides,\n defaultOptions: {\n ...overrides?.defaultOptions,\n queries: {\n retry: false,\n gcTime: 0,\n staleTime: 0,\n ...overrides?.defaultOptions?.queries,\n },\n mutations: {\n retry: false,\n ...overrides?.defaultOptions?.mutations,\n },\n },\n });\n}\n\nexport interface QueryStateView<T> {\n readonly data: T | undefined;\n readonly status: 'pending' | 'error' | 'success';\n readonly fetchStatus: 'fetching' | 'paused' | 'idle';\n readonly isInvalidated: boolean;\n}\n\n/** Read a query's current cache state (data + status + invalidation flag). */\nexport function getQueryState<T>(client: QueryClient, key: QueryKey): QueryStateView<T> {\n const state = client.getQueryState<T>(key);\n return {\n data: state?.data,\n status: state?.status ?? 'pending',\n fetchStatus: state?.fetchStatus ?? 'idle',\n isInvalidated: state?.isInvalidated ?? false,\n };\n}\n\n/** True when the query at `key` is currently marked invalidated. */\nexport function expectInvalidated(client: QueryClient, key: QueryKey): boolean {\n return client.getQueryState(key)?.isInvalidated === true;\n}\n\nexport interface OptimisticRollback<T> {\n /** The cache value captured before the optimistic write. */\n readonly snapshot: T | undefined;\n /** Apply an optimistic value to the cache. */\n apply(next: T): void;\n /** Restore the captured snapshot (rollback). */\n rollback(): void;\n /** Current cache value at the key. */\n current(): T | undefined;\n}\n\n/**\n * Capture a key's value, then drive an optimistic apply → rollback so a test can\n * assert both the optimistic write and that rollback restored the snapshot.\n */\nexport function trackOptimisticRollback<T>(client: QueryClient, key: QueryKey): OptimisticRollback<T> {\n const snapshot = client.getQueryData<T>(key);\n return {\n snapshot,\n apply(next: T) {\n client.setQueryData<T>(key, next);\n },\n rollback() {\n client.setQueryData<T>(key, snapshot);\n },\n current() {\n return client.getQueryData<T>(key);\n },\n };\n}\n\n/** Offline harness (mock wallet + session store) plus a test QueryClient. */\nexport interface QueryHarness extends OfflineHarness {\n readonly queryClient: QueryClient;\n}\n\n/** Query-inclusive composition of {@link createOfflineHarness}. */\nexport function createQueryHarness(\n config: {\n wallet?: MockWalletConfig;\n session?: Partial<SessionStoreOptions>;\n query?: QueryClientConfig;\n } = {},\n): QueryHarness {\n const base = createOfflineHarness({ wallet: config.wallet, session: config.session });\n const queryClient = createTestQueryClient(config.query);\n return {\n ...base,\n queryClient,\n destroy() {\n queryClient.clear();\n base.destroy();\n },\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@partylayer/testing",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Offline test utilities for PartyLayer: mock CIP-0103 provider with failure scenarios, transaction + session lifecycle simulation, TanStack Query helpers, and browser/e2e primitives",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./query": {
|
|
15
|
+
"types": "./dist/query.d.ts",
|
|
16
|
+
"import": "./dist/query.mjs",
|
|
17
|
+
"require": "./dist/query.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"canton",
|
|
25
|
+
"wallet",
|
|
26
|
+
"cip-0103",
|
|
27
|
+
"testing",
|
|
28
|
+
"mock"
|
|
29
|
+
],
|
|
30
|
+
"author": "PartyLayer Contributors",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/PartyLayer/PartyLayer.git",
|
|
35
|
+
"directory": "packages/testing"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://partylayer.xyz",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/PartyLayer/PartyLayer/issues"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@partylayer/core": "^0.5.0",
|
|
43
|
+
"@partylayer/provider": "^0.2.2",
|
|
44
|
+
"@partylayer/session": "^1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@tanstack/query-core": ">=5.0.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@tanstack/query-core": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@tanstack/query-core": "^5.0.0",
|
|
56
|
+
"@types/node": "^20.11.0",
|
|
57
|
+
"tsup": "^8.0.0",
|
|
58
|
+
"typescript": "^5.3.3",
|
|
59
|
+
"vitest": "^1.2.0",
|
|
60
|
+
"@vitest/ui": "^1.2.0",
|
|
61
|
+
"@partylayer/conformance-runner": "^0.2.0"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsup",
|
|
65
|
+
"clean": "rm -rf dist",
|
|
66
|
+
"lint": "eslint src --ext .ts",
|
|
67
|
+
"typecheck": "tsc --noEmit",
|
|
68
|
+
"test": "vitest run"
|
|
69
|
+
}
|
|
70
|
+
}
|