@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
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.RimoriTestEnvironment = void 0;
|
|
4
4
|
const default_user_info_1 = require("../fixtures/default-user-info");
|
|
5
5
|
const MessageChannelSimulator_1 = require("./MessageChannelSimulator");
|
|
6
|
+
const SettingsStateManager_1 = require("./SettingsStateManager");
|
|
6
7
|
class RimoriTestEnvironment {
|
|
7
8
|
constructor(options) {
|
|
8
9
|
this.backendRoutes = {};
|
|
@@ -10,57 +11,45 @@ class RimoriTestEnvironment {
|
|
|
10
11
|
this.messageChannelSimulator = null;
|
|
11
12
|
this.plugin = {
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Manually set the settings state (useful for test setup).
|
|
15
|
+
* This directly modifies the internal settings state.
|
|
16
|
+
* @param settings - The settings to set, or null to clear settings
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
setSettings: (settings) => {
|
|
19
|
+
this.settingsManager.setSettings(settings);
|
|
20
|
+
},
|
|
21
|
+
/**
|
|
22
|
+
* Get the current settings state (useful for assertions).
|
|
23
|
+
* @returns The current settings or null if no settings exist
|
|
24
|
+
*/
|
|
25
|
+
getSettings: () => {
|
|
26
|
+
return this.settingsManager.getSettings();
|
|
25
27
|
},
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* Should include: { id, plugin_id, guild_id, settings, is_guild_setting, user_id }.
|
|
30
|
-
* If null, simulates no settings exist (triggers INSERT flow).
|
|
29
|
+
* Override the GET handler for plugin_settings (rarely needed).
|
|
30
|
+
* By default, GET returns the current state from SettingsStateManager.
|
|
31
31
|
*/
|
|
32
32
|
mockGetSettings: (settingsRow, options) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
this.addSupabaseRoute('plugin_settings', settingsRow, { ...options, method: 'GET' });
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Override the PATCH handler for plugin_settings (rarely needed).
|
|
37
|
+
* By default, PATCH updates the state in SettingsStateManager.
|
|
38
|
+
*/
|
|
39
|
+
mockSetSettings: (response, options) => {
|
|
40
|
+
this.addSupabaseRoute('plugin_settings', response, { ...options, method: 'PATCH' });
|
|
38
41
|
},
|
|
39
42
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
43
|
+
* Override the POST handler for plugin_settings (rarely needed).
|
|
44
|
+
* By default, POST inserts/updates the state in SettingsStateManager.
|
|
42
45
|
*/
|
|
43
46
|
mockInsertSettings: (response, options) => {
|
|
44
|
-
|
|
45
|
-
console.warn('mockInsertSettings is not tested');
|
|
46
|
-
// TODO this function should not exist and possibly be combined with the mockSetSettings function
|
|
47
|
-
// POST request returns the inserted row or success response
|
|
48
|
-
// Default to an object representing successful insert
|
|
49
|
-
const defaultResponse = response ?? {
|
|
50
|
-
id: 'mock-settings-id',
|
|
51
|
-
plugin_id: this.pluginId,
|
|
52
|
-
guild_id: this.rimoriInfo.guild.id,
|
|
53
|
-
};
|
|
54
|
-
this.addSupabaseRoute('plugin_settings', defaultResponse, { ...options, method: 'POST' });
|
|
47
|
+
this.addSupabaseRoute('plugin_settings', response, { ...options, method: 'POST' });
|
|
55
48
|
},
|
|
56
49
|
mockGetUserInfo: (userInfo, options) => {
|
|
57
|
-
console.log('Mocking get user info for mockGetUserInfo', userInfo, options);
|
|
58
|
-
console.warn('mockGetUserInfo is not tested');
|
|
59
50
|
this.addSupabaseRoute('/user-info', { ...this.rimoriInfo.profile, ...userInfo }, { ...options, delay: 0 });
|
|
60
51
|
},
|
|
61
52
|
mockGetPluginInfo: (pluginInfo, options) => {
|
|
62
|
-
console.log('Mocking get plugin info for mockGetPluginInfo', pluginInfo, options);
|
|
63
|
-
console.warn('mockGetPluginInfo is not tested');
|
|
64
53
|
this.addSupabaseRoute('/plugin-info', pluginInfo, options);
|
|
65
54
|
},
|
|
66
55
|
};
|
|
@@ -81,21 +70,87 @@ class RimoriTestEnvironment {
|
|
|
81
70
|
* @param options - Mock options including HTTP method (defaults to 'GET' if not specified)
|
|
82
71
|
*/
|
|
83
72
|
mockFrom: (tableName, value, options) => {
|
|
84
|
-
console.log('Mocking db.from for table:', tableName, 'method:', options?.method ?? 'GET', value, options);
|
|
73
|
+
// console.log('Mocking db.from for table:', tableName, 'method:', options?.method ?? 'GET', value, options);
|
|
85
74
|
const fullTableName = `${this.pluginId}_${tableName}`;
|
|
86
75
|
this.addSupabaseRoute(fullTableName, value, options);
|
|
87
76
|
},
|
|
88
|
-
mockTable: () => { },
|
|
89
77
|
};
|
|
90
78
|
this.event = {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Emit an event into the plugin as if it came from Rimori main or another plugin.
|
|
81
|
+
*
|
|
82
|
+
* Note: This does NOT currently reach worker listeners such as those in
|
|
83
|
+
* `worker/listeners/decks.ts` or `worker/listeners/flascards.ts` – those run in a
|
|
84
|
+
* separate process. This helper is intended for UI‑side events only.
|
|
85
|
+
*/
|
|
86
|
+
mockEmit: async (topic, data, sender = 'test') => {
|
|
87
|
+
if (!this.messageChannelSimulator) {
|
|
88
|
+
throw new Error('MessageChannelSimulator not initialized. Call setup() first.');
|
|
89
|
+
}
|
|
90
|
+
await this.messageChannelSimulator.emit(topic, data, sender);
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* Registers a one-time auto-responder for request/response style events.
|
|
94
|
+
*
|
|
95
|
+
* When the plugin calls `plugin.event.request(topic, data)`, this registered responder
|
|
96
|
+
* will automatically return the provided response value. The responder is automatically
|
|
97
|
+
* removed after the first request, ensuring it only responds once.
|
|
98
|
+
*
|
|
99
|
+
* Example:
|
|
100
|
+
* ```ts
|
|
101
|
+
* // Register a responder that will return deck summaries when requested
|
|
102
|
+
* env.event.mockRequest('deck.requestOpenToday', [
|
|
103
|
+
* { id: 'deck-1', name: 'My Deck', total_new: 5, total_learning: 2, total_review: 10 }
|
|
104
|
+
* ]);
|
|
105
|
+
*
|
|
106
|
+
* // Now when the plugin calls: plugin.event.request('deck.requestOpenToday', {})
|
|
107
|
+
* // It will receive the deck summaries array above
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @param topic - The event topic to respond to (e.g., 'deck.requestOpenToday')
|
|
111
|
+
* @param response - The response value to return, or a function that receives the event and returns the response
|
|
112
|
+
* @returns A function to manually remove the responder before it's used
|
|
113
|
+
*/
|
|
114
|
+
mockRequest: (topic, response) => {
|
|
115
|
+
if (!this.messageChannelSimulator) {
|
|
116
|
+
throw new Error('MessageChannelSimulator not initialized. Call setup() first.');
|
|
117
|
+
}
|
|
118
|
+
return this.messageChannelSimulator.respondOnce(topic, response);
|
|
119
|
+
},
|
|
120
|
+
/**
|
|
121
|
+
* Listen for events emitted by the plugin.
|
|
122
|
+
* @param topic - The event topic to listen for (e.g., 'global.accomplishment.triggerMicro')
|
|
123
|
+
* @param handler - The handler function that receives the event data
|
|
124
|
+
* @returns A function to unsubscribe from the event
|
|
125
|
+
*/
|
|
126
|
+
on: (topic, handler) => {
|
|
127
|
+
if (!this.messageChannelSimulator) {
|
|
128
|
+
throw new Error('MessageChannelSimulator not initialized. Call setup() first.');
|
|
129
|
+
}
|
|
130
|
+
return this.messageChannelSimulator.on(topic, (event) => {
|
|
131
|
+
handler(event.data);
|
|
132
|
+
});
|
|
133
|
+
},
|
|
94
134
|
mockOnce: () => { },
|
|
95
135
|
mockRespond: () => { },
|
|
96
136
|
mockEmitAccomplishment: () => { },
|
|
97
137
|
mockOnAccomplishment: () => { },
|
|
98
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Emits a sidebar action event into the plugin as if Rimori main had triggered it.
|
|
140
|
+
* This is useful for testing sidebar-driven flows like flashcard creation from selected text.
|
|
141
|
+
*
|
|
142
|
+
* It sends a message on the 'global.sidebar.triggerAction' topic, which plugins can listen to via:
|
|
143
|
+
* plugin.event.on<{ action: string; text: string }>('global.sidebar.triggerAction', ...)
|
|
144
|
+
*
|
|
145
|
+
* @param payload - The payload forwarded to the plugin, typically including an `action` key and optional `text`.
|
|
146
|
+
*/
|
|
147
|
+
triggerSidebarAction: async (payload) => {
|
|
148
|
+
if (!this.messageChannelSimulator) {
|
|
149
|
+
throw new Error('MessageChannelSimulator not initialized. Call setup() first.');
|
|
150
|
+
}
|
|
151
|
+
// Simulate Rimori main emitting the sidebar trigger event towards the plugin
|
|
152
|
+
await this.messageChannelSimulator.emit('global.sidebar.triggerAction', payload, 'sidebar');
|
|
153
|
+
},
|
|
99
154
|
/**
|
|
100
155
|
* Triggers a side panel action event as the parent application would.
|
|
101
156
|
* This simulates how rimori-main's SidebarPluginHandler responds to plugin's 'action.requestSidebar' events.
|
|
@@ -156,24 +211,137 @@ class RimoriTestEnvironment {
|
|
|
156
211
|
* @param options - Optional mock options.
|
|
157
212
|
*/
|
|
158
213
|
mockGetSteamedText: (text, options) => {
|
|
159
|
-
console.log('Mocking get steamed text for mockGetSteamedText', text, options);
|
|
160
214
|
this.addBackendRoute('/ai/llm', text, { ...options, isStreaming: true });
|
|
161
215
|
},
|
|
162
216
|
mockGetVoice: (values, options) => {
|
|
163
|
-
console.log('Mocking get voice for mockGetVoice', values, options);
|
|
164
|
-
console.warn('mockGetVoice is not tested');
|
|
165
217
|
this.addBackendRoute('/voice/tts', values, options);
|
|
166
218
|
},
|
|
167
219
|
mockGetTextFromVoice: (text, options) => {
|
|
168
|
-
console.log('Mocking get text from voice for mockGetTextFromVoice', text, options);
|
|
169
|
-
console.warn('mockGetTextFromVoice is not tested');
|
|
170
220
|
this.addBackendRoute('/voice/stt', text, options);
|
|
171
221
|
},
|
|
172
222
|
mockGetObject: (value, options) => {
|
|
173
|
-
console.log('Mocking get object for mockGetObject', value, options);
|
|
174
223
|
this.addBackendRoute('/ai/llm-object', value, { ...options, method: 'POST' });
|
|
175
224
|
},
|
|
176
225
|
};
|
|
226
|
+
/**
|
|
227
|
+
* Helpers for tracking browser audio playback in tests.
|
|
228
|
+
*
|
|
229
|
+
* This is useful for components like the AudioPlayer in @rimori/react-client which:
|
|
230
|
+
* 1) Fetch audio data from the backend (mocked via `env.ai.mockGetVoice`)
|
|
231
|
+
* 2) Create `new Audio(url)` and call `.play()`
|
|
232
|
+
*
|
|
233
|
+
* With tracking enabled you can assert how many times audio playback was attempted:
|
|
234
|
+
*
|
|
235
|
+
* ```ts
|
|
236
|
+
* await env.audio.enableTracking();
|
|
237
|
+
* await env.ai.mockGetVoice(Buffer.from('dummy'), { method: 'POST' });
|
|
238
|
+
* await env.setup();
|
|
239
|
+
* // ...navigate and trigger audio...
|
|
240
|
+
* const counts = await env.audio.getPlayCounts();
|
|
241
|
+
* expect(counts.mediaPlayCalls).toBeGreaterThan(0);
|
|
242
|
+
* ```
|
|
243
|
+
*
|
|
244
|
+
* **Counter Types:**
|
|
245
|
+
* - `mediaPlayCalls`: Tracks calls to `.play()` on any `HTMLMediaElement` instance
|
|
246
|
+
* (including `<audio>`, `<video>` elements, or any element that inherits from `HTMLMediaElement`).
|
|
247
|
+
* This counter increments whenever `HTMLMediaElement.prototype.play()` is invoked.
|
|
248
|
+
* - `audioPlayCalls`: Tracks calls to `.play()` specifically on instances created via the `Audio` constructor
|
|
249
|
+
* (e.g., `new Audio(url).play()`). This is a subset of `mediaPlayCalls` but provides more specific
|
|
250
|
+
* tracking for programmatically created audio elements.
|
|
251
|
+
*
|
|
252
|
+
* **Note**: Since `Audio` instances are also `HTMLMediaElement` instances, calling `.play()` on an
|
|
253
|
+
* `Audio` object will increment **both** counters. For most use cases, checking `mediaPlayCalls`
|
|
254
|
+
* is sufficient as it captures all audio playback attempts.
|
|
255
|
+
*/
|
|
256
|
+
this.audio = {
|
|
257
|
+
/**
|
|
258
|
+
* Injects tracking hooks for HTMLMediaElement.play and the Audio constructor.
|
|
259
|
+
* Must be called before the plugin code runs (ideally before env.setup()).
|
|
260
|
+
*/
|
|
261
|
+
enableTracking: async () => {
|
|
262
|
+
await this.page.addInitScript(() => {
|
|
263
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
264
|
+
const w = window;
|
|
265
|
+
if (!w.__rimoriAudio) {
|
|
266
|
+
w.__rimoriAudio = {
|
|
267
|
+
mediaPlayCalls: 0,
|
|
268
|
+
audioPlayCalls: 0,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const proto = (w.HTMLMediaElement && w.HTMLMediaElement.prototype) || undefined;
|
|
272
|
+
if (proto && !proto.__rimoriPatched) {
|
|
273
|
+
const originalPlay = proto.play;
|
|
274
|
+
proto.play = function (...args) {
|
|
275
|
+
w.__rimoriAudio.mediaPlayCalls += 1;
|
|
276
|
+
return originalPlay.apply(this, args);
|
|
277
|
+
};
|
|
278
|
+
Object.defineProperty(proto, '__rimoriPatched', {
|
|
279
|
+
value: true,
|
|
280
|
+
configurable: false,
|
|
281
|
+
enumerable: false,
|
|
282
|
+
writable: false,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const OriginalAudio = w.Audio;
|
|
286
|
+
if (OriginalAudio && !OriginalAudio.__rimoriPatched) {
|
|
287
|
+
const PatchedAudio = function (...args) {
|
|
288
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
|
+
const audio = new OriginalAudio(...args);
|
|
290
|
+
const originalPlay = audio.play.bind(audio);
|
|
291
|
+
audio.play = () => {
|
|
292
|
+
w.__rimoriAudio.audioPlayCalls += 1;
|
|
293
|
+
return originalPlay();
|
|
294
|
+
};
|
|
295
|
+
return audio;
|
|
296
|
+
};
|
|
297
|
+
PatchedAudio.prototype = OriginalAudio.prototype;
|
|
298
|
+
Object.defineProperty(PatchedAudio, '__rimoriPatched', {
|
|
299
|
+
value: true,
|
|
300
|
+
configurable: false,
|
|
301
|
+
enumerable: false,
|
|
302
|
+
writable: false,
|
|
303
|
+
});
|
|
304
|
+
w.Audio = PatchedAudio;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
/**
|
|
309
|
+
* Returns current audio play counters from the browser context.
|
|
310
|
+
*
|
|
311
|
+
* @returns An object with two counters:
|
|
312
|
+
* - `mediaPlayCalls`: Total number of `.play()` calls on any `HTMLMediaElement` (includes all audio/video elements)
|
|
313
|
+
* - `audioPlayCalls`: Number of `.play()` calls on instances created via `new Audio()` (subset of `mediaPlayCalls`)
|
|
314
|
+
*
|
|
315
|
+
* **Note**: Since `Audio` extends `HTMLMediaElement`, calling `.play()` on an `Audio` instance increments both counters.
|
|
316
|
+
* For general audio playback tracking, use `mediaPlayCalls` as it captures all playback attempts.
|
|
317
|
+
*/
|
|
318
|
+
getPlayCounts: async () => {
|
|
319
|
+
return this.page.evaluate(() => {
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
321
|
+
const w = window;
|
|
322
|
+
if (!w.__rimoriAudio) {
|
|
323
|
+
return { mediaPlayCalls: 0, audioPlayCalls: 0 };
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
mediaPlayCalls: Number(w.__rimoriAudio.mediaPlayCalls || 0),
|
|
327
|
+
audioPlayCalls: Number(w.__rimoriAudio.audioPlayCalls || 0),
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
/**
|
|
332
|
+
* Resets the audio play counters to zero.
|
|
333
|
+
*/
|
|
334
|
+
resetPlayCounts: async () => {
|
|
335
|
+
await this.page.evaluate(() => {
|
|
336
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
337
|
+
const w = window;
|
|
338
|
+
if (w.__rimoriAudio) {
|
|
339
|
+
w.__rimoriAudio.mediaPlayCalls = 0;
|
|
340
|
+
w.__rimoriAudio.audioPlayCalls = 0;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
};
|
|
177
345
|
this.runtime = {
|
|
178
346
|
mockFetchBackend: () => { },
|
|
179
347
|
};
|
|
@@ -199,8 +367,84 @@ class RimoriTestEnvironment {
|
|
|
199
367
|
};
|
|
200
368
|
this.page = options.page;
|
|
201
369
|
this.pluginId = options.pluginId;
|
|
202
|
-
|
|
203
|
-
|
|
370
|
+
this.rimoriInfo = this.getRimoriInfo(options);
|
|
371
|
+
// Initialize settings state manager
|
|
372
|
+
this.settingsManager = new SettingsStateManager_1.SettingsStateManager(options.settings || null, options.pluginId, this.rimoriInfo.guild.id);
|
|
373
|
+
this.interceptRoutes(options.pluginUrl);
|
|
374
|
+
}
|
|
375
|
+
interceptRoutes(pluginUrl) {
|
|
376
|
+
// Intercept all /locales requests and fetch from the dev server
|
|
377
|
+
this.page.route(`${pluginUrl}/locales/**`, async (route) => {
|
|
378
|
+
const request = route.request();
|
|
379
|
+
const url = new URL(request.url());
|
|
380
|
+
const devServerUrl = `http://${url.host}/locales/en.json`;
|
|
381
|
+
// console.log('Fetching locales from: ' + devServerUrl);
|
|
382
|
+
// throw new Error('Test: ' + devServerUrl);
|
|
383
|
+
try {
|
|
384
|
+
// Fetch from the dev server
|
|
385
|
+
const response = await fetch(devServerUrl);
|
|
386
|
+
const body = await response.text();
|
|
387
|
+
await route.fulfill({
|
|
388
|
+
status: response.status,
|
|
389
|
+
headers: { 'Content-Type': 'application/json' },
|
|
390
|
+
body,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
console.error(`Error fetching translation from ${devServerUrl}:`, error);
|
|
395
|
+
await route.fulfill({
|
|
396
|
+
status: 500,
|
|
397
|
+
headers: { 'Content-Type': 'application/json' },
|
|
398
|
+
body: JSON.stringify({ error: 'Failed to load translations' }),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
this.page.route(`${this.rimoriInfo.backendUrl}/**`, (route) => this.handleRoute(route, this.backendRoutes));
|
|
403
|
+
this.page.route(`${this.rimoriInfo.url}/**`, (route) => this.handleRoute(route, this.supabaseRoutes));
|
|
404
|
+
}
|
|
405
|
+
async setup() {
|
|
406
|
+
// console.log('Setting up RimoriTestEnvironment');
|
|
407
|
+
this.page.on('console', (msg) => {
|
|
408
|
+
const logLevel = msg.type();
|
|
409
|
+
const logMessage = msg.text();
|
|
410
|
+
if (logLevel === 'debug')
|
|
411
|
+
return;
|
|
412
|
+
if (logMessage.includes('Download the React DevTools'))
|
|
413
|
+
return;
|
|
414
|
+
if (logMessage.includes('languageChanged en'))
|
|
415
|
+
return;
|
|
416
|
+
if (logMessage.includes('i18next: initialized {debug: true'))
|
|
417
|
+
return;
|
|
418
|
+
console.log(`[browser:${logLevel}]`, logMessage);
|
|
419
|
+
});
|
|
420
|
+
// Set up default handlers for plugin_settings routes using SettingsStateManager
|
|
421
|
+
this.setupSettingsRoutes();
|
|
422
|
+
// Initialize MessageChannelSimulator to simulate parent-iframe communication
|
|
423
|
+
// This makes the plugin think it's running in an iframe (not standalone mode)
|
|
424
|
+
// Convert RimoriInfo from CommunicationHandler format to MessageChannelSimulator format
|
|
425
|
+
this.messageChannelSimulator = new MessageChannelSimulator_1.MessageChannelSimulator({
|
|
426
|
+
page: this.page,
|
|
427
|
+
pluginId: this.pluginId,
|
|
428
|
+
queryParams: {},
|
|
429
|
+
rimoriInfo: this.rimoriInfo,
|
|
430
|
+
});
|
|
431
|
+
// Initialize the simulator - this injects the necessary shims
|
|
432
|
+
// to intercept window.parent.postMessage calls and set up MessageChannel communication
|
|
433
|
+
await this.messageChannelSimulator.initialize();
|
|
434
|
+
// Set up a no-op handler for pl454583483.session.triggerUrlChange
|
|
435
|
+
// This prevents errors if the plugin emits this event
|
|
436
|
+
this.messageChannelSimulator.on(`${this.pluginId}.session.triggerUrlChange`, () => {
|
|
437
|
+
// No-op handler - does nothing
|
|
438
|
+
});
|
|
439
|
+
this.messageChannelSimulator.on('global.accomplishment.triggerMicro', () => {
|
|
440
|
+
// No-op handler - does nothing
|
|
441
|
+
});
|
|
442
|
+
this.messageChannelSimulator.on('global.accomplishment.triggerMacro', () => {
|
|
443
|
+
// No-op handler - does nothing
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
getRimoriInfo(options) {
|
|
447
|
+
return {
|
|
204
448
|
key: 'rimori-testing-key',
|
|
205
449
|
token: 'rimori-testing-token',
|
|
206
450
|
url: 'http://localhost:3500',
|
|
@@ -231,80 +475,54 @@ class RimoriTestEnvironment {
|
|
|
231
475
|
profile: default_user_info_1.DEFAULT_USER_INFO,
|
|
232
476
|
mainPanelPlugin: undefined,
|
|
233
477
|
sidePanelPlugin: undefined,
|
|
478
|
+
interfaceLanguage: default_user_info_1.DEFAULT_USER_INFO.mother_tongue.code, // Set interface language from user's mother tongue
|
|
234
479
|
};
|
|
235
|
-
this.interceptRoutes();
|
|
236
480
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
this.
|
|
244
|
-
|
|
481
|
+
/**
|
|
482
|
+
* Sets up the plugin_settings routes to use the SettingsStateManager.
|
|
483
|
+
* GET returns current state, PATCH updates state, POST creates/updates state.
|
|
484
|
+
*/
|
|
485
|
+
setupSettingsRoutes() {
|
|
486
|
+
// GET: Return current settings state
|
|
487
|
+
this.addSupabaseRoute('plugin_settings', () => this.settingsManager.getSettings(), {
|
|
488
|
+
method: 'GET',
|
|
245
489
|
});
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
description: p.info?.description || '',
|
|
282
|
-
logo: p.info?.logo || '',
|
|
283
|
-
url: p.pages?.external_hosted_url || '',
|
|
284
|
-
})),
|
|
285
|
-
mainPanelPlugin: this.rimoriInfo.mainPanelPlugin
|
|
286
|
-
? {
|
|
287
|
-
id: this.rimoriInfo.mainPanelPlugin.id,
|
|
288
|
-
title: this.rimoriInfo.mainPanelPlugin.info?.title || '',
|
|
289
|
-
description: this.rimoriInfo.mainPanelPlugin.info?.description || '',
|
|
290
|
-
logo: this.rimoriInfo.mainPanelPlugin.info?.logo || '',
|
|
291
|
-
url: this.rimoriInfo.mainPanelPlugin.pages?.external_hosted_url || '',
|
|
292
|
-
}
|
|
293
|
-
: undefined,
|
|
294
|
-
sidePanelPlugin: this.rimoriInfo.sidePanelPlugin
|
|
295
|
-
? {
|
|
296
|
-
id: this.rimoriInfo.sidePanelPlugin.id,
|
|
297
|
-
title: this.rimoriInfo.sidePanelPlugin.info?.title || '',
|
|
298
|
-
description: this.rimoriInfo.sidePanelPlugin.info?.description || '',
|
|
299
|
-
logo: this.rimoriInfo.sidePanelPlugin.info?.logo || '',
|
|
300
|
-
url: this.rimoriInfo.sidePanelPlugin.pages?.external_hosted_url || '',
|
|
301
|
-
}
|
|
302
|
-
: undefined,
|
|
303
|
-
},
|
|
490
|
+
// PATCH: Update settings based on request body
|
|
491
|
+
this.addSupabaseRoute('plugin_settings', async (request) => {
|
|
492
|
+
try {
|
|
493
|
+
const postData = request.postData();
|
|
494
|
+
if (postData) {
|
|
495
|
+
const updates = JSON.parse(postData);
|
|
496
|
+
return this.settingsManager.updateSettings(updates);
|
|
497
|
+
}
|
|
498
|
+
// If no body, return empty array (no update)
|
|
499
|
+
return this.settingsManager.updateSettings({});
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
// If parsing fails, return empty array
|
|
503
|
+
return this.settingsManager.updateSettings({});
|
|
504
|
+
}
|
|
505
|
+
}, {
|
|
506
|
+
method: 'PATCH',
|
|
507
|
+
});
|
|
508
|
+
// POST: Insert/update settings based on request body
|
|
509
|
+
this.addSupabaseRoute('plugin_settings', async (request) => {
|
|
510
|
+
try {
|
|
511
|
+
const postData = request.postData();
|
|
512
|
+
if (postData) {
|
|
513
|
+
const newSettings = JSON.parse(postData);
|
|
514
|
+
return this.settingsManager.insertSettings(newSettings);
|
|
515
|
+
}
|
|
516
|
+
// If no body, insert with defaults
|
|
517
|
+
return this.settingsManager.insertSettings({});
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
// If parsing fails, insert with defaults
|
|
521
|
+
return this.settingsManager.insertSettings({});
|
|
522
|
+
}
|
|
523
|
+
}, {
|
|
524
|
+
method: 'POST',
|
|
304
525
|
});
|
|
305
|
-
// Initialize the simulator - this injects the necessary shims
|
|
306
|
-
// to intercept window.parent.postMessage calls and set up MessageChannel communication
|
|
307
|
-
await this.messageChannelSimulator.initialize();
|
|
308
526
|
}
|
|
309
527
|
/**
|
|
310
528
|
* Formats text as SSE (Server-Sent Events) response.
|
|
@@ -359,15 +577,15 @@ class RimoriTestEnvironment {
|
|
|
359
577
|
}
|
|
360
578
|
}
|
|
361
579
|
async handleRoute(route, routes) {
|
|
362
|
-
console.warn('handleRoute is not tested');
|
|
363
580
|
const request = route.request();
|
|
364
581
|
const requestUrl = request.url();
|
|
365
582
|
const method = request.method().toUpperCase();
|
|
366
583
|
const routeKey = this.createRouteKey(method, requestUrl);
|
|
367
|
-
console.log('Handling route', routeKey);
|
|
584
|
+
// console.log('Handling route', routeKey);
|
|
368
585
|
const mocks = routes[routeKey];
|
|
369
586
|
if (!mocks || mocks.length === 0) {
|
|
370
587
|
console.error('No route handler found for route', routeKey);
|
|
588
|
+
throw new Error('No route handler found for route: ' + routeKey);
|
|
371
589
|
route.abort('not_found');
|
|
372
590
|
return;
|
|
373
591
|
}
|
|
@@ -407,10 +625,15 @@ class RimoriTestEnvironment {
|
|
|
407
625
|
if (options?.error) {
|
|
408
626
|
return await route.abort(options.error);
|
|
409
627
|
}
|
|
628
|
+
// Handle function-based mocks (for stateful responses like settings)
|
|
629
|
+
let responseValue = matchingMock.value;
|
|
630
|
+
if (typeof matchingMock.value === 'function') {
|
|
631
|
+
responseValue = await matchingMock.value(request);
|
|
632
|
+
}
|
|
410
633
|
// Handle streaming responses (for mockGetSteamedText)
|
|
411
634
|
// Since Playwright requires complete body, we format as SSE without delays
|
|
412
|
-
if (matchingMock.isStreaming && typeof
|
|
413
|
-
const body = this.formatAsSSE(
|
|
635
|
+
if (matchingMock.isStreaming && typeof responseValue === 'string') {
|
|
636
|
+
const body = this.formatAsSSE(responseValue);
|
|
414
637
|
return await route.fulfill({
|
|
415
638
|
status: 200,
|
|
416
639
|
headers: { 'Content-Type': 'text/event-stream' },
|
|
@@ -418,7 +641,7 @@ class RimoriTestEnvironment {
|
|
|
418
641
|
});
|
|
419
642
|
}
|
|
420
643
|
// Regular JSON response
|
|
421
|
-
const responseBody = JSON.stringify(
|
|
644
|
+
const responseBody = JSON.stringify(responseValue);
|
|
422
645
|
route.fulfill({
|
|
423
646
|
status: 200,
|
|
424
647
|
body: responseBody,
|
|
@@ -431,7 +654,6 @@ class RimoriTestEnvironment {
|
|
|
431
654
|
* @param options - The options for the route. Method defaults to 'GET' if not specified.
|
|
432
655
|
*/
|
|
433
656
|
addSupabaseRoute(path, values, options) {
|
|
434
|
-
console.warn('addSupabaseRoute is not tested');
|
|
435
657
|
const method = options?.method ?? 'GET';
|
|
436
658
|
const fullPath = `${this.rimoriInfo.url}/rest/v1/${path}`;
|
|
437
659
|
const routeKey = this.createRouteKey(method, fullPath);
|
|
@@ -452,7 +674,6 @@ class RimoriTestEnvironment {
|
|
|
452
674
|
* @param isStreaming - Optional flag to mark this as a streaming response.
|
|
453
675
|
*/
|
|
454
676
|
addBackendRoute(path, values, options) {
|
|
455
|
-
console.warn('addBackendRoute is not tested');
|
|
456
677
|
const method = options?.method ?? 'POST';
|
|
457
678
|
const fullPath = `${this.rimoriInfo.backendUrl}${path.startsWith('/') ? path : '/' + path}`;
|
|
458
679
|
const routeKey = this.createRouteKey(method, fullPath);
|
|
@@ -470,4 +691,3 @@ class RimoriTestEnvironment {
|
|
|
470
691
|
}
|
|
471
692
|
exports.RimoriTestEnvironment = RimoriTestEnvironment;
|
|
472
693
|
// Todo: How to test if the event was received by the parent?
|
|
473
|
-
// TODO: The matcher option of RimoriTestEnvironment v1 might be useful to use
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages plugin settings state for test environment.
|
|
3
|
+
* Provides a single source of truth for settings that can be modified by mocked API calls.
|
|
4
|
+
*/
|
|
5
|
+
export interface PluginSettings {
|
|
6
|
+
id?: string;
|
|
7
|
+
plugin_id?: string;
|
|
8
|
+
guild_id?: string;
|
|
9
|
+
settings?: Record<string, unknown>;
|
|
10
|
+
is_guild_setting?: boolean;
|
|
11
|
+
user_id?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export declare class SettingsStateManager {
|
|
14
|
+
private settings;
|
|
15
|
+
constructor(initialSettings: PluginSettings | null, pluginId: string, guildId: string);
|
|
16
|
+
/**
|
|
17
|
+
* Get current settings state (for GET requests)
|
|
18
|
+
* Returns null if no settings exist, otherwise returns the full settings object
|
|
19
|
+
*/
|
|
20
|
+
getSettings(): PluginSettings | null;
|
|
21
|
+
/**
|
|
22
|
+
* Update settings (for PATCH requests)
|
|
23
|
+
* @param updates - Partial settings to update
|
|
24
|
+
* @returns Array with updated row if settings exist, empty array if no settings exist
|
|
25
|
+
*/
|
|
26
|
+
updateSettings(updates: Partial<PluginSettings>): PluginSettings[];
|
|
27
|
+
/**
|
|
28
|
+
* Insert new settings (for POST requests)
|
|
29
|
+
* @param newSettings - Settings to insert
|
|
30
|
+
* @returns The inserted settings object
|
|
31
|
+
*/
|
|
32
|
+
insertSettings(newSettings: Partial<PluginSettings>): PluginSettings;
|
|
33
|
+
/**
|
|
34
|
+
* Manually set settings (useful for test setup)
|
|
35
|
+
*/
|
|
36
|
+
setSettings(settings: PluginSettings | null): void;
|
|
37
|
+
/**
|
|
38
|
+
* Check if settings exist
|
|
39
|
+
*/
|
|
40
|
+
hasSettings(): boolean;
|
|
41
|
+
}
|