@rimori/playwright-testing 0.2.2 → 0.2.3-next.1
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/dist/core/MessageChannelSimulator.d.ts +18 -82
- package/dist/core/MessageChannelSimulator.js +63 -104
- package/dist/core/RimoriTestEnvironment.d.ts +137 -24
- package/dist/core/RimoriTestEnvironment.js +349 -129
- package/dist/core/SettingsStateManager.d.ts +41 -0
- package/dist/core/SettingsStateManager.js +74 -0
- package/dist/fixtures/default-user-info.js +1 -0
- package/dist/test/translator.test.js +1 -1
- package/package.json +7 -2
|
@@ -1,92 +1,21 @@
|
|
|
1
1
|
import type { Page } from '@playwright/test';
|
|
2
|
-
|
|
3
|
-
code: string;
|
|
4
|
-
name: string;
|
|
5
|
-
native: string;
|
|
6
|
-
capitalized: string;
|
|
7
|
-
uppercase: string;
|
|
8
|
-
};
|
|
9
|
-
type StudyBuddy = {
|
|
10
|
-
id: string;
|
|
11
|
-
name: string;
|
|
12
|
-
description: string;
|
|
13
|
-
avatarUrl: string;
|
|
14
|
-
voiceId: string;
|
|
15
|
-
aiPersonality: string;
|
|
16
|
-
};
|
|
17
|
-
export type UserInfo = {
|
|
18
|
-
mother_tongue: Language;
|
|
19
|
-
target_language: Language;
|
|
20
|
-
skill_level_reading: string;
|
|
21
|
-
skill_level_writing: string;
|
|
22
|
-
skill_level_grammar: string;
|
|
23
|
-
skill_level_speaking: string;
|
|
24
|
-
skill_level_listening: string;
|
|
25
|
-
skill_level_understanding: string;
|
|
26
|
-
goal_longterm: string;
|
|
27
|
-
goal_weekly: string;
|
|
28
|
-
study_buddy: StudyBuddy;
|
|
29
|
-
story_genre: string;
|
|
30
|
-
study_duration: number;
|
|
31
|
-
motivation_type: string;
|
|
32
|
-
onboarding_completed: boolean;
|
|
33
|
-
context_menu_on_select: boolean;
|
|
34
|
-
user_name?: string;
|
|
35
|
-
target_country: string;
|
|
36
|
-
target_city?: string;
|
|
37
|
-
};
|
|
38
|
-
type RimoriGuild = {
|
|
39
|
-
id: string;
|
|
40
|
-
longTermGoalOverride: string;
|
|
41
|
-
allowUserPluginSettings: boolean;
|
|
42
|
-
};
|
|
43
|
-
type PluginInfo = {
|
|
44
|
-
id: string;
|
|
45
|
-
title: string;
|
|
46
|
-
description: string;
|
|
47
|
-
logo: string;
|
|
48
|
-
url: string;
|
|
49
|
-
};
|
|
50
|
-
type RimoriInfo = {
|
|
51
|
-
url: string;
|
|
52
|
-
key: string;
|
|
53
|
-
backendUrl: string;
|
|
54
|
-
token: string;
|
|
55
|
-
expiration: Date;
|
|
56
|
-
tablePrefix: string;
|
|
57
|
-
pluginId: string;
|
|
58
|
-
guild: RimoriGuild;
|
|
59
|
-
installedPlugins: PluginInfo[];
|
|
60
|
-
profile: UserInfo;
|
|
61
|
-
mainPanelPlugin?: PluginInfo;
|
|
62
|
-
sidePanelPlugin?: PluginInfo;
|
|
63
|
-
};
|
|
64
|
-
type EventBusMessage = {
|
|
65
|
-
timestamp: string;
|
|
66
|
-
sender: string;
|
|
67
|
-
topic: string;
|
|
68
|
-
data: unknown;
|
|
69
|
-
debug: boolean;
|
|
70
|
-
eventId?: number;
|
|
71
|
-
};
|
|
2
|
+
import { UserInfo, RimoriInfo, EventBusMessage, EventPayload } from '@rimori/client';
|
|
72
3
|
type MessageChannelSimulatorArgs = {
|
|
73
4
|
page: Page;
|
|
74
5
|
pluginId: string;
|
|
6
|
+
rimoriInfo: RimoriInfo;
|
|
75
7
|
queryParams?: Record<string, string>;
|
|
76
|
-
rimoriInfo?: RimoriInfo;
|
|
77
8
|
};
|
|
78
9
|
type EventListener = (event: EventBusMessage) => void | Promise<void>;
|
|
10
|
+
type AutoResponder = (event: EventBusMessage) => unknown | Promise<unknown>;
|
|
79
11
|
export declare class MessageChannelSimulator {
|
|
80
12
|
private readonly page;
|
|
81
13
|
private readonly pluginId;
|
|
82
14
|
private readonly queryParams;
|
|
83
|
-
private readonly
|
|
84
|
-
private readonly providedInfo?;
|
|
15
|
+
private readonly rimoriInfo;
|
|
85
16
|
private readonly listeners;
|
|
86
17
|
private readonly autoResponders;
|
|
87
18
|
private readonly pendingOutbound;
|
|
88
|
-
private currentUserInfo;
|
|
89
|
-
private currentRimoriInfo;
|
|
90
19
|
private isReady;
|
|
91
20
|
private instanceId;
|
|
92
21
|
/**
|
|
@@ -106,26 +35,33 @@ export declare class MessageChannelSimulator {
|
|
|
106
35
|
/**
|
|
107
36
|
* Sends an event into the plugin as though the Rimori parent emitted it.
|
|
108
37
|
*/
|
|
109
|
-
emit(topic: string, data:
|
|
38
|
+
emit(topic: string, data: EventPayload, sender?: string): Promise<void>;
|
|
110
39
|
/**
|
|
111
40
|
* Registers a handler for events emitted from the plugin.
|
|
112
41
|
*/
|
|
113
42
|
on(topic: string, handler: EventListener): () => void;
|
|
114
43
|
/**
|
|
115
|
-
*
|
|
44
|
+
* Registers a one-time auto-responder for a request/response topic.
|
|
45
|
+
* When a request with an eventId comes in for this topic, the responder will
|
|
46
|
+
* be called once and then automatically removed.
|
|
47
|
+
*
|
|
48
|
+
* @param topic - The event topic to respond to
|
|
49
|
+
* @param responder - A function that returns the response data, or a value to return directly
|
|
50
|
+
* @returns A function to manually remove the responder before it's used
|
|
51
|
+
*/
|
|
52
|
+
respondOnce(topic: string, responder: AutoResponder | unknown): () => void;
|
|
53
|
+
/**
|
|
54
|
+
* Overrides the user info.
|
|
116
55
|
*/
|
|
117
|
-
setUserInfo(
|
|
118
|
-
getRimoriInfo(): RimoriInfo
|
|
56
|
+
setUserInfo(userInfo: UserInfo): void;
|
|
57
|
+
getRimoriInfo(): RimoriInfo;
|
|
119
58
|
private setupMessageChannel;
|
|
120
59
|
private sendToPlugin;
|
|
121
60
|
private flushPending;
|
|
122
61
|
private handlePortMessage;
|
|
123
62
|
private dispatchEvent;
|
|
124
63
|
private maybeRespond;
|
|
125
|
-
private buildRimoriInfo;
|
|
126
|
-
private serializeRimoriInfo;
|
|
127
64
|
private cloneUserInfo;
|
|
128
|
-
private mergeUserInfo;
|
|
129
65
|
private registerAutoResponders;
|
|
130
66
|
private cloneRimoriInfo;
|
|
131
67
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MessageChannelSimulator = void 0;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
|
-
const default_user_info_1 = require("../fixtures/default-user-info");
|
|
6
5
|
class MessageChannelSimulator {
|
|
7
6
|
/**
|
|
8
7
|
* Creates a simulator that mimics the Rimori host for plugin tests.
|
|
@@ -15,22 +14,19 @@ class MessageChannelSimulator {
|
|
|
15
14
|
this.listeners = new Map();
|
|
16
15
|
this.autoResponders = new Map();
|
|
17
16
|
this.pendingOutbound = [];
|
|
18
|
-
this.currentRimoriInfo = null;
|
|
19
17
|
this.isReady = false;
|
|
20
18
|
this.instanceId = (0, node_crypto_1.randomUUID)();
|
|
21
19
|
this.page = page;
|
|
22
20
|
this.pluginId = pluginId;
|
|
23
21
|
this.queryParams = queryParams ?? {};
|
|
24
|
-
this.
|
|
25
|
-
this.currentUserInfo = this.cloneUserInfo(default_user_info_1.DEFAULT_USER_INFO);
|
|
26
|
-
this.providedInfo = rimoriInfo ? this.cloneRimoriInfo(rimoriInfo) : undefined;
|
|
22
|
+
this.rimoriInfo = this.cloneRimoriInfo(rimoriInfo);
|
|
27
23
|
this.registerAutoResponders();
|
|
28
24
|
}
|
|
29
25
|
get defaultUserInfo() {
|
|
30
|
-
return this.cloneUserInfo(this.
|
|
26
|
+
return this.cloneUserInfo(this.rimoriInfo.profile);
|
|
31
27
|
}
|
|
32
28
|
get userInfo() {
|
|
33
|
-
return this.cloneUserInfo(this.
|
|
29
|
+
return this.cloneUserInfo(this.rimoriInfo.profile);
|
|
34
30
|
}
|
|
35
31
|
/**
|
|
36
32
|
* Injects the handshake shims so the plugin talks to this simulator.
|
|
@@ -112,6 +108,7 @@ class MessageChannelSimulator {
|
|
|
112
108
|
const message = {
|
|
113
109
|
event: {
|
|
114
110
|
timestamp: new Date().toISOString(),
|
|
111
|
+
eventId: Math.floor(Math.random() * 1000000),
|
|
115
112
|
sender,
|
|
116
113
|
topic,
|
|
117
114
|
data,
|
|
@@ -143,25 +140,52 @@ class MessageChannelSimulator {
|
|
|
143
140
|
};
|
|
144
141
|
}
|
|
145
142
|
/**
|
|
146
|
-
*
|
|
143
|
+
* Registers a one-time auto-responder for a request/response topic.
|
|
144
|
+
* When a request with an eventId comes in for this topic, the responder will
|
|
145
|
+
* be called once and then automatically removed.
|
|
146
|
+
*
|
|
147
|
+
* @param topic - The event topic to respond to
|
|
148
|
+
* @param responder - A function that returns the response data, or a value to return directly
|
|
149
|
+
* @returns A function to manually remove the responder before it's used
|
|
147
150
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
respondOnce(topic, responder) {
|
|
152
|
+
let used = false;
|
|
153
|
+
const wrappedResponder = (event) => {
|
|
154
|
+
if (used) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
used = true;
|
|
158
|
+
// Remove from autoResponders after first use
|
|
159
|
+
this.autoResponders.delete(topic);
|
|
160
|
+
// If responder is a function, call it with the event, otherwise return the value directly
|
|
161
|
+
if (typeof responder === 'function') {
|
|
162
|
+
return responder(event);
|
|
163
|
+
}
|
|
164
|
+
return responder;
|
|
165
|
+
};
|
|
166
|
+
this.autoResponders.set(topic, wrappedResponder);
|
|
167
|
+
// Return a function to manually remove the responder
|
|
168
|
+
return () => {
|
|
169
|
+
if (!used) {
|
|
170
|
+
this.autoResponders.delete(topic);
|
|
171
|
+
used = true;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Overrides the user info.
|
|
177
|
+
*/
|
|
178
|
+
setUserInfo(userInfo) {
|
|
179
|
+
this.rimoriInfo.profile = userInfo;
|
|
153
180
|
}
|
|
154
181
|
getRimoriInfo() {
|
|
155
|
-
return this.
|
|
182
|
+
return this.cloneRimoriInfo(this.rimoriInfo);
|
|
156
183
|
}
|
|
157
184
|
async setupMessageChannel() {
|
|
158
185
|
if (this.isReady) {
|
|
159
186
|
return;
|
|
160
187
|
}
|
|
161
|
-
|
|
162
|
-
this.currentRimoriInfo = rimoriInfo;
|
|
163
|
-
const serialized = this.serializeRimoriInfo(rimoriInfo);
|
|
164
|
-
await this.page.evaluate(({ pluginId, queryParams, instanceId, rimoriInfo: info, }) => {
|
|
188
|
+
await this.page.evaluate(({ pluginId, queryParams, instanceId, rimoriInfo, }) => {
|
|
165
189
|
const channel = new MessageChannel();
|
|
166
190
|
channel.port1.onmessage = (event) => {
|
|
167
191
|
// @ts-expect-error binding injected via exposeBinding
|
|
@@ -176,10 +200,7 @@ class MessageChannelSimulator {
|
|
|
176
200
|
pluginId,
|
|
177
201
|
instanceId,
|
|
178
202
|
queryParams,
|
|
179
|
-
rimoriInfo
|
|
180
|
-
...info,
|
|
181
|
-
expiration: new Date(info.expiration),
|
|
182
|
-
},
|
|
203
|
+
rimoriInfo,
|
|
183
204
|
},
|
|
184
205
|
ports: [channel.port2],
|
|
185
206
|
});
|
|
@@ -188,7 +209,7 @@ class MessageChannelSimulator {
|
|
|
188
209
|
pluginId: this.pluginId,
|
|
189
210
|
queryParams: this.queryParams,
|
|
190
211
|
instanceId: this.instanceId,
|
|
191
|
-
rimoriInfo:
|
|
212
|
+
rimoriInfo: this.rimoriInfo,
|
|
192
213
|
});
|
|
193
214
|
this.isReady = true;
|
|
194
215
|
await this.flushPending();
|
|
@@ -215,21 +236,33 @@ class MessageChannelSimulator {
|
|
|
215
236
|
return;
|
|
216
237
|
}
|
|
217
238
|
if ('event' in payload && payload.event) {
|
|
218
|
-
console.log(
|
|
239
|
+
// console.log(
|
|
240
|
+
// '[MessageChannelSimulator] handlePortMessage - received event:',
|
|
241
|
+
// payload.event.topic,
|
|
242
|
+
// 'from:',
|
|
243
|
+
// payload.event.sender,
|
|
244
|
+
// );
|
|
219
245
|
await this.dispatchEvent(payload.event);
|
|
220
246
|
await this.maybeRespond(payload.event);
|
|
221
247
|
return;
|
|
222
248
|
}
|
|
223
249
|
}
|
|
224
250
|
async dispatchEvent(event) {
|
|
225
|
-
console.log(
|
|
251
|
+
// console.log(
|
|
252
|
+
// '[MessageChannelSimulator] dispatchEvent - topic:',
|
|
253
|
+
// event.topic,
|
|
254
|
+
// 'sender:',
|
|
255
|
+
// event.sender,
|
|
256
|
+
// 'listeners:',
|
|
257
|
+
// this.listeners.has(event.topic) ? this.listeners.get(event.topic)?.size : 0,
|
|
258
|
+
// );
|
|
226
259
|
const handlers = this.listeners.get(event.topic);
|
|
227
260
|
if (!handlers?.size) {
|
|
228
261
|
console.log('[MessageChannelSimulator] No handlers found for topic:', event.topic);
|
|
229
262
|
console.log('[MessageChannelSimulator] Available topics:', Array.from(this.listeners.keys()));
|
|
230
263
|
return;
|
|
231
264
|
}
|
|
232
|
-
console.log('[MessageChannelSimulator] Calling', handlers.size, 'handler(s) for topic:', event.topic);
|
|
265
|
+
// console.log('[MessageChannelSimulator] Calling', handlers.size, 'handler(s) for topic:', event.topic);
|
|
233
266
|
for (const handler of handlers) {
|
|
234
267
|
await handler(event);
|
|
235
268
|
}
|
|
@@ -252,90 +285,16 @@ class MessageChannelSimulator {
|
|
|
252
285
|
},
|
|
253
286
|
});
|
|
254
287
|
}
|
|
255
|
-
buildRimoriInfo() {
|
|
256
|
-
if (this.providedInfo) {
|
|
257
|
-
const clone = this.cloneRimoriInfo(this.providedInfo);
|
|
258
|
-
clone.profile = this.cloneUserInfo(this.currentUserInfo);
|
|
259
|
-
clone.pluginId = this.pluginId;
|
|
260
|
-
clone.tablePrefix = clone.tablePrefix || `${this.pluginId}_`;
|
|
261
|
-
return clone;
|
|
262
|
-
}
|
|
263
|
-
return {
|
|
264
|
-
url: 'http://localhost:3500',
|
|
265
|
-
key: 'rimori-sdk-key',
|
|
266
|
-
backendUrl: 'http://localhost:3501',
|
|
267
|
-
token: 'rimori-token',
|
|
268
|
-
expiration: new Date(Date.now() + 60 * 60 * 1000),
|
|
269
|
-
tablePrefix: `${this.pluginId}_`,
|
|
270
|
-
pluginId: this.pluginId,
|
|
271
|
-
guild: {
|
|
272
|
-
id: 'guild-test',
|
|
273
|
-
longTermGoalOverride: '',
|
|
274
|
-
allowUserPluginSettings: true,
|
|
275
|
-
},
|
|
276
|
-
installedPlugins: [
|
|
277
|
-
{
|
|
278
|
-
id: this.pluginId,
|
|
279
|
-
title: 'Test Plugin',
|
|
280
|
-
description: 'Playwright testing plugin',
|
|
281
|
-
logo: '',
|
|
282
|
-
url: 'https://plugins.rimori.localhost',
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
profile: this.cloneUserInfo(this.currentUserInfo),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
serializeRimoriInfo(info) {
|
|
289
|
-
return {
|
|
290
|
-
...info,
|
|
291
|
-
expiration: info.expiration.toISOString(),
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
288
|
cloneUserInfo(input) {
|
|
295
289
|
return JSON.parse(JSON.stringify(input));
|
|
296
290
|
}
|
|
297
|
-
mergeUserInfo(current, overrides) {
|
|
298
|
-
const clone = this.cloneUserInfo(current);
|
|
299
|
-
if (overrides.mother_tongue) {
|
|
300
|
-
clone.mother_tongue = {
|
|
301
|
-
...clone.mother_tongue,
|
|
302
|
-
...overrides.mother_tongue,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
if (overrides.target_language) {
|
|
306
|
-
clone.target_language = {
|
|
307
|
-
...clone.target_language,
|
|
308
|
-
...overrides.target_language,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
if (overrides.study_buddy) {
|
|
312
|
-
clone.study_buddy = {
|
|
313
|
-
...clone.study_buddy,
|
|
314
|
-
...overrides.study_buddy,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
const { mother_tongue, target_language, study_buddy, ...rest } = overrides;
|
|
318
|
-
for (const [key, value] of Object.entries(rest)) {
|
|
319
|
-
if (value === undefined) {
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
clone[key] = value;
|
|
323
|
-
}
|
|
324
|
-
return clone;
|
|
325
|
-
}
|
|
326
291
|
registerAutoResponders() {
|
|
327
|
-
this.autoResponders.set('global.supabase.requestAccess', () => this.
|
|
328
|
-
this.autoResponders.set('global.profile.requestUserInfo', () => this.cloneUserInfo(this.
|
|
329
|
-
this.autoResponders.set('global.profile.getUserInfo', () => this.cloneUserInfo(this.
|
|
292
|
+
this.autoResponders.set('global.supabase.requestAccess', () => this.cloneRimoriInfo(this.rimoriInfo));
|
|
293
|
+
this.autoResponders.set('global.profile.requestUserInfo', () => this.cloneUserInfo(this.rimoriInfo.profile));
|
|
294
|
+
this.autoResponders.set('global.profile.getUserInfo', () => this.cloneUserInfo(this.rimoriInfo.profile));
|
|
330
295
|
}
|
|
331
296
|
cloneRimoriInfo(info) {
|
|
332
|
-
return
|
|
333
|
-
...info,
|
|
334
|
-
expiration: new Date(info.expiration),
|
|
335
|
-
guild: { ...info.guild },
|
|
336
|
-
installedPlugins: info.installedPlugins.map((plugin) => ({ ...plugin })),
|
|
337
|
-
profile: this.cloneUserInfo(info.profile),
|
|
338
|
-
};
|
|
297
|
+
return JSON.parse(JSON.stringify(info));
|
|
339
298
|
}
|
|
340
299
|
}
|
|
341
300
|
exports.MessageChannelSimulator = MessageChannelSimulator;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Page, Request } from '@playwright/test';
|
|
2
2
|
import { UserInfo } from '@rimori/client/dist/controller/SettingsController';
|
|
3
3
|
import { MainPanelAction, Plugin } from '@rimori/client/dist/fromRimori/PluginTypes';
|
|
4
|
+
import { PluginSettings } from './SettingsStateManager';
|
|
5
|
+
import { EventPayload } from '@rimori/client/dist/fromRimori/EventBus';
|
|
4
6
|
interface RimoriTestEnvironmentOptions {
|
|
5
7
|
page: Page;
|
|
6
8
|
pluginId: string;
|
|
9
|
+
pluginUrl: string;
|
|
10
|
+
settings?: PluginSettings;
|
|
7
11
|
queryParams?: Record<string, string>;
|
|
8
12
|
userInfo?: Record<string, unknown>;
|
|
9
13
|
installedPlugins?: Plugin[];
|
|
@@ -40,9 +44,16 @@ export declare class RimoriTestEnvironment {
|
|
|
40
44
|
private backendRoutes;
|
|
41
45
|
private supabaseRoutes;
|
|
42
46
|
private messageChannelSimulator;
|
|
47
|
+
private settingsManager;
|
|
43
48
|
constructor(options: RimoriTestEnvironmentOptions);
|
|
44
49
|
private interceptRoutes;
|
|
45
50
|
setup(): Promise<void>;
|
|
51
|
+
private getRimoriInfo;
|
|
52
|
+
/**
|
|
53
|
+
* Sets up the plugin_settings routes to use the SettingsStateManager.
|
|
54
|
+
* GET returns current state, PATCH updates state, POST creates/updates state.
|
|
55
|
+
*/
|
|
56
|
+
private setupSettingsRoutes;
|
|
46
57
|
/**
|
|
47
58
|
* Formats text as SSE (Server-Sent Events) response.
|
|
48
59
|
* Since Playwright's route.fulfill() requires complete body, we format as SSE without delays.
|
|
@@ -78,30 +89,31 @@ export declare class RimoriTestEnvironment {
|
|
|
78
89
|
private addBackendRoute;
|
|
79
90
|
readonly plugin: {
|
|
80
91
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
92
|
+
* Manually set the settings state (useful for test setup).
|
|
93
|
+
* This directly modifies the internal settings state.
|
|
94
|
+
* @param settings - The settings to set, or null to clear settings
|
|
95
|
+
*/
|
|
96
|
+
setSettings: (settings: PluginSettings | null) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Get the current settings state (useful for assertions).
|
|
99
|
+
* @returns The current settings or null if no settings exist
|
|
84
100
|
*/
|
|
85
|
-
|
|
101
|
+
getSettings: () => PluginSettings | null;
|
|
86
102
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* Should include: { id, plugin_id, guild_id, settings, is_guild_setting, user_id }.
|
|
90
|
-
* If null, simulates no settings exist (triggers INSERT flow).
|
|
103
|
+
* Override the GET handler for plugin_settings (rarely needed).
|
|
104
|
+
* By default, GET returns the current state from SettingsStateManager.
|
|
91
105
|
*/
|
|
92
|
-
mockGetSettings: (settingsRow:
|
|
93
|
-
id?: string;
|
|
94
|
-
plugin_id?: string;
|
|
95
|
-
guild_id?: string;
|
|
96
|
-
settings?: Record<string, unknown>;
|
|
97
|
-
is_guild_setting?: boolean;
|
|
98
|
-
user_id?: string | null;
|
|
99
|
-
} | null, options?: MockOptions) => void;
|
|
106
|
+
mockGetSettings: (settingsRow: PluginSettings | null, options?: MockOptions) => void;
|
|
100
107
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
108
|
+
* Override the PATCH handler for plugin_settings (rarely needed).
|
|
109
|
+
* By default, PATCH updates the state in SettingsStateManager.
|
|
103
110
|
*/
|
|
104
|
-
|
|
111
|
+
mockSetSettings: (response: unknown, options?: MockOptions) => void;
|
|
112
|
+
/**
|
|
113
|
+
* Override the POST handler for plugin_settings (rarely needed).
|
|
114
|
+
* By default, POST inserts/updates the state in SettingsStateManager.
|
|
115
|
+
*/
|
|
116
|
+
mockInsertSettings: (response: unknown, options?: MockOptions) => void;
|
|
105
117
|
mockGetUserInfo: (userInfo: Partial<UserInfo>, options?: MockOptions) => void;
|
|
106
118
|
mockGetPluginInfo: (pluginInfo: Plugin, options?: MockOptions) => void;
|
|
107
119
|
};
|
|
@@ -122,17 +134,63 @@ export declare class RimoriTestEnvironment {
|
|
|
122
134
|
* @param options - Mock options including HTTP method (defaults to 'GET' if not specified)
|
|
123
135
|
*/
|
|
124
136
|
mockFrom: (tableName: string, value: unknown, options?: MockOptions) => void;
|
|
125
|
-
mockTable: () => void;
|
|
126
137
|
};
|
|
127
138
|
readonly event: {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Emit an event into the plugin as if it came from Rimori main or another plugin.
|
|
141
|
+
*
|
|
142
|
+
* Note: This does NOT currently reach worker listeners such as those in
|
|
143
|
+
* `worker/listeners/decks.ts` or `worker/listeners/flascards.ts` – those run in a
|
|
144
|
+
* separate process. This helper is intended for UI‑side events only.
|
|
145
|
+
*/
|
|
146
|
+
mockEmit: (topic: string, data: EventPayload, sender?: string) => Promise<void>;
|
|
147
|
+
/**
|
|
148
|
+
* Registers a one-time auto-responder for request/response style events.
|
|
149
|
+
*
|
|
150
|
+
* When the plugin calls `plugin.event.request(topic, data)`, this registered responder
|
|
151
|
+
* will automatically return the provided response value. The responder is automatically
|
|
152
|
+
* removed after the first request, ensuring it only responds once.
|
|
153
|
+
*
|
|
154
|
+
* Example:
|
|
155
|
+
* ```ts
|
|
156
|
+
* // Register a responder that will return deck summaries when requested
|
|
157
|
+
* env.event.mockRequest('deck.requestOpenToday', [
|
|
158
|
+
* { id: 'deck-1', name: 'My Deck', total_new: 5, total_learning: 2, total_review: 10 }
|
|
159
|
+
* ]);
|
|
160
|
+
*
|
|
161
|
+
* // Now when the plugin calls: plugin.event.request('deck.requestOpenToday', {})
|
|
162
|
+
* // It will receive the deck summaries array above
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* @param topic - The event topic to respond to (e.g., 'deck.requestOpenToday')
|
|
166
|
+
* @param response - The response value to return, or a function that receives the event and returns the response
|
|
167
|
+
* @returns A function to manually remove the responder before it's used
|
|
168
|
+
*/
|
|
169
|
+
mockRequest: (topic: string, response: unknown | ((event: unknown) => unknown)) => () => void;
|
|
170
|
+
/**
|
|
171
|
+
* Listen for events emitted by the plugin.
|
|
172
|
+
* @param topic - The event topic to listen for (e.g., 'global.accomplishment.triggerMicro')
|
|
173
|
+
* @param handler - The handler function that receives the event data
|
|
174
|
+
* @returns A function to unsubscribe from the event
|
|
175
|
+
*/
|
|
176
|
+
on: (topic: string, handler: (data: unknown) => void) => (() => void);
|
|
131
177
|
mockOnce: () => void;
|
|
132
178
|
mockRespond: () => void;
|
|
133
179
|
mockEmitAccomplishment: () => void;
|
|
134
180
|
mockOnAccomplishment: () => void;
|
|
135
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Emits a sidebar action event into the plugin as if Rimori main had triggered it.
|
|
183
|
+
* This is useful for testing sidebar-driven flows like flashcard creation from selected text.
|
|
184
|
+
*
|
|
185
|
+
* It sends a message on the 'global.sidebar.triggerAction' topic, which plugins can listen to via:
|
|
186
|
+
* plugin.event.on<{ action: string; text: string }>('global.sidebar.triggerAction', ...)
|
|
187
|
+
*
|
|
188
|
+
* @param payload - The payload forwarded to the plugin, typically including an `action` key and optional `text`.
|
|
189
|
+
*/
|
|
190
|
+
triggerSidebarAction: (payload: {
|
|
191
|
+
action: string;
|
|
192
|
+
text?: string;
|
|
193
|
+
}) => Promise<void>;
|
|
136
194
|
/**
|
|
137
195
|
* Triggers a side panel action event as the parent application would.
|
|
138
196
|
* This simulates how rimori-main's SidebarPluginHandler responds to plugin's 'action.requestSidebar' events.
|
|
@@ -166,6 +224,61 @@ export declare class RimoriTestEnvironment {
|
|
|
166
224
|
mockGetTextFromVoice: (text: string, options?: MockOptions) => void;
|
|
167
225
|
mockGetObject: (value: unknown, options?: MockOptions) => void;
|
|
168
226
|
};
|
|
227
|
+
/**
|
|
228
|
+
* Helpers for tracking browser audio playback in tests.
|
|
229
|
+
*
|
|
230
|
+
* This is useful for components like the AudioPlayer in @rimori/react-client which:
|
|
231
|
+
* 1) Fetch audio data from the backend (mocked via `env.ai.mockGetVoice`)
|
|
232
|
+
* 2) Create `new Audio(url)` and call `.play()`
|
|
233
|
+
*
|
|
234
|
+
* With tracking enabled you can assert how many times audio playback was attempted:
|
|
235
|
+
*
|
|
236
|
+
* ```ts
|
|
237
|
+
* await env.audio.enableTracking();
|
|
238
|
+
* await env.ai.mockGetVoice(Buffer.from('dummy'), { method: 'POST' });
|
|
239
|
+
* await env.setup();
|
|
240
|
+
* // ...navigate and trigger audio...
|
|
241
|
+
* const counts = await env.audio.getPlayCounts();
|
|
242
|
+
* expect(counts.mediaPlayCalls).toBeGreaterThan(0);
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* **Counter Types:**
|
|
246
|
+
* - `mediaPlayCalls`: Tracks calls to `.play()` on any `HTMLMediaElement` instance
|
|
247
|
+
* (including `<audio>`, `<video>` elements, or any element that inherits from `HTMLMediaElement`).
|
|
248
|
+
* This counter increments whenever `HTMLMediaElement.prototype.play()` is invoked.
|
|
249
|
+
* - `audioPlayCalls`: Tracks calls to `.play()` specifically on instances created via the `Audio` constructor
|
|
250
|
+
* (e.g., `new Audio(url).play()`). This is a subset of `mediaPlayCalls` but provides more specific
|
|
251
|
+
* tracking for programmatically created audio elements.
|
|
252
|
+
*
|
|
253
|
+
* **Note**: Since `Audio` instances are also `HTMLMediaElement` instances, calling `.play()` on an
|
|
254
|
+
* `Audio` object will increment **both** counters. For most use cases, checking `mediaPlayCalls`
|
|
255
|
+
* is sufficient as it captures all audio playback attempts.
|
|
256
|
+
*/
|
|
257
|
+
readonly audio: {
|
|
258
|
+
/**
|
|
259
|
+
* Injects tracking hooks for HTMLMediaElement.play and the Audio constructor.
|
|
260
|
+
* Must be called before the plugin code runs (ideally before env.setup()).
|
|
261
|
+
*/
|
|
262
|
+
enableTracking: () => Promise<void>;
|
|
263
|
+
/**
|
|
264
|
+
* Returns current audio play counters from the browser context.
|
|
265
|
+
*
|
|
266
|
+
* @returns An object with two counters:
|
|
267
|
+
* - `mediaPlayCalls`: Total number of `.play()` calls on any `HTMLMediaElement` (includes all audio/video elements)
|
|
268
|
+
* - `audioPlayCalls`: Number of `.play()` calls on instances created via `new Audio()` (subset of `mediaPlayCalls`)
|
|
269
|
+
*
|
|
270
|
+
* **Note**: Since `Audio` extends `HTMLMediaElement`, calling `.play()` on an `Audio` instance increments both counters.
|
|
271
|
+
* For general audio playback tracking, use `mediaPlayCalls` as it captures all playback attempts.
|
|
272
|
+
*/
|
|
273
|
+
getPlayCounts: () => Promise<{
|
|
274
|
+
mediaPlayCalls: number;
|
|
275
|
+
audioPlayCalls: number;
|
|
276
|
+
}>;
|
|
277
|
+
/**
|
|
278
|
+
* Resets the audio play counters to zero.
|
|
279
|
+
*/
|
|
280
|
+
resetPlayCounts: () => Promise<void>;
|
|
281
|
+
};
|
|
169
282
|
readonly runtime: {
|
|
170
283
|
mockFetchBackend: () => void;
|
|
171
284
|
};
|