@rimori/playwright-testing 0.2.3 → 0.2.4-next.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/dist/core/MessageChannelSimulator.d.ts +7 -82
- package/dist/core/MessageChannelSimulator.js +37 -106
- package/dist/core/RimoriTestEnvironment.d.ts +74 -28
- package/dist/core/RimoriTestEnvironment.js +258 -130
- 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 +9 -5
|
@@ -1,79 +1,10 @@
|
|
|
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>;
|
|
79
10
|
type AutoResponder = (event: EventBusMessage) => unknown | Promise<unknown>;
|
|
@@ -81,13 +12,10 @@ export declare class MessageChannelSimulator {
|
|
|
81
12
|
private readonly page;
|
|
82
13
|
private readonly pluginId;
|
|
83
14
|
private readonly queryParams;
|
|
84
|
-
private readonly
|
|
85
|
-
private readonly providedInfo?;
|
|
15
|
+
private readonly rimoriInfo;
|
|
86
16
|
private readonly listeners;
|
|
87
17
|
private readonly autoResponders;
|
|
88
18
|
private readonly pendingOutbound;
|
|
89
|
-
private currentUserInfo;
|
|
90
|
-
private currentRimoriInfo;
|
|
91
19
|
private isReady;
|
|
92
20
|
private instanceId;
|
|
93
21
|
/**
|
|
@@ -107,7 +35,7 @@ export declare class MessageChannelSimulator {
|
|
|
107
35
|
/**
|
|
108
36
|
* Sends an event into the plugin as though the Rimori parent emitted it.
|
|
109
37
|
*/
|
|
110
|
-
emit(topic: string, data:
|
|
38
|
+
emit(topic: string, data: EventPayload, sender?: string): Promise<void>;
|
|
111
39
|
/**
|
|
112
40
|
* Registers a handler for events emitted from the plugin.
|
|
113
41
|
*/
|
|
@@ -123,20 +51,17 @@ export declare class MessageChannelSimulator {
|
|
|
123
51
|
*/
|
|
124
52
|
respondOnce(topic: string, responder: AutoResponder | unknown): () => void;
|
|
125
53
|
/**
|
|
126
|
-
* Overrides the
|
|
54
|
+
* Overrides the user info.
|
|
127
55
|
*/
|
|
128
|
-
setUserInfo(
|
|
129
|
-
getRimoriInfo(): RimoriInfo
|
|
56
|
+
setUserInfo(userInfo: UserInfo): void;
|
|
57
|
+
getRimoriInfo(): RimoriInfo;
|
|
130
58
|
private setupMessageChannel;
|
|
131
59
|
private sendToPlugin;
|
|
132
60
|
private flushPending;
|
|
133
61
|
private handlePortMessage;
|
|
134
62
|
private dispatchEvent;
|
|
135
63
|
private maybeRespond;
|
|
136
|
-
private buildRimoriInfo;
|
|
137
|
-
private serializeRimoriInfo;
|
|
138
64
|
private cloneUserInfo;
|
|
139
|
-
private mergeUserInfo;
|
|
140
65
|
private registerAutoResponders;
|
|
141
66
|
private cloneRimoriInfo;
|
|
142
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,
|
|
@@ -176,25 +173,19 @@ class MessageChannelSimulator {
|
|
|
176
173
|
};
|
|
177
174
|
}
|
|
178
175
|
/**
|
|
179
|
-
* Overrides the
|
|
176
|
+
* Overrides the user info.
|
|
180
177
|
*/
|
|
181
|
-
setUserInfo(
|
|
182
|
-
this.
|
|
183
|
-
if (this.currentRimoriInfo) {
|
|
184
|
-
this.currentRimoriInfo.profile = this.cloneUserInfo(this.currentUserInfo);
|
|
185
|
-
}
|
|
178
|
+
setUserInfo(userInfo) {
|
|
179
|
+
this.rimoriInfo.profile = userInfo;
|
|
186
180
|
}
|
|
187
181
|
getRimoriInfo() {
|
|
188
|
-
return this.
|
|
182
|
+
return this.cloneRimoriInfo(this.rimoriInfo);
|
|
189
183
|
}
|
|
190
184
|
async setupMessageChannel() {
|
|
191
185
|
if (this.isReady) {
|
|
192
186
|
return;
|
|
193
187
|
}
|
|
194
|
-
|
|
195
|
-
this.currentRimoriInfo = rimoriInfo;
|
|
196
|
-
const serialized = this.serializeRimoriInfo(rimoriInfo);
|
|
197
|
-
await this.page.evaluate(({ pluginId, queryParams, instanceId, rimoriInfo: info, }) => {
|
|
188
|
+
await this.page.evaluate(({ pluginId, queryParams, instanceId, rimoriInfo, }) => {
|
|
198
189
|
const channel = new MessageChannel();
|
|
199
190
|
channel.port1.onmessage = (event) => {
|
|
200
191
|
// @ts-expect-error binding injected via exposeBinding
|
|
@@ -209,10 +200,7 @@ class MessageChannelSimulator {
|
|
|
209
200
|
pluginId,
|
|
210
201
|
instanceId,
|
|
211
202
|
queryParams,
|
|
212
|
-
rimoriInfo
|
|
213
|
-
...info,
|
|
214
|
-
expiration: new Date(info.expiration),
|
|
215
|
-
},
|
|
203
|
+
rimoriInfo,
|
|
216
204
|
},
|
|
217
205
|
ports: [channel.port2],
|
|
218
206
|
});
|
|
@@ -221,7 +209,7 @@ class MessageChannelSimulator {
|
|
|
221
209
|
pluginId: this.pluginId,
|
|
222
210
|
queryParams: this.queryParams,
|
|
223
211
|
instanceId: this.instanceId,
|
|
224
|
-
rimoriInfo:
|
|
212
|
+
rimoriInfo: this.rimoriInfo,
|
|
225
213
|
});
|
|
226
214
|
this.isReady = true;
|
|
227
215
|
await this.flushPending();
|
|
@@ -248,21 +236,38 @@ class MessageChannelSimulator {
|
|
|
248
236
|
return;
|
|
249
237
|
}
|
|
250
238
|
if ('event' in payload && payload.event) {
|
|
251
|
-
console.log(
|
|
239
|
+
// console.log(
|
|
240
|
+
// '[MessageChannelSimulator] handlePortMessage - received event:',
|
|
241
|
+
// payload.event.topic,
|
|
242
|
+
// 'from:',
|
|
243
|
+
// payload.event.sender,
|
|
244
|
+
// );
|
|
252
245
|
await this.dispatchEvent(payload.event);
|
|
253
246
|
await this.maybeRespond(payload.event);
|
|
254
247
|
return;
|
|
255
248
|
}
|
|
256
249
|
}
|
|
257
250
|
async dispatchEvent(event) {
|
|
258
|
-
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
|
+
// );
|
|
259
259
|
const handlers = this.listeners.get(event.topic);
|
|
260
260
|
if (!handlers?.size) {
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
// Don't log an error if this is a request/response event with an auto-responder
|
|
262
|
+
// (auto-responders handle request/response patterns, not listeners)
|
|
263
|
+
const hasAutoResponder = event.eventId && this.autoResponders.has(event.topic);
|
|
264
|
+
if (!hasAutoResponder) {
|
|
265
|
+
console.log('[MessageChannelSimulator] No handlers found for topic:', event.topic);
|
|
266
|
+
console.log('[MessageChannelSimulator] Available topics:', Array.from(this.listeners.keys()));
|
|
267
|
+
}
|
|
263
268
|
return;
|
|
264
269
|
}
|
|
265
|
-
console.log('[MessageChannelSimulator] Calling', handlers.size, 'handler(s) for topic:', event.topic);
|
|
270
|
+
// console.log('[MessageChannelSimulator] Calling', handlers.size, 'handler(s) for topic:', event.topic);
|
|
266
271
|
for (const handler of handlers) {
|
|
267
272
|
await handler(event);
|
|
268
273
|
}
|
|
@@ -285,90 +290,16 @@ class MessageChannelSimulator {
|
|
|
285
290
|
},
|
|
286
291
|
});
|
|
287
292
|
}
|
|
288
|
-
buildRimoriInfo() {
|
|
289
|
-
if (this.providedInfo) {
|
|
290
|
-
const clone = this.cloneRimoriInfo(this.providedInfo);
|
|
291
|
-
clone.profile = this.cloneUserInfo(this.currentUserInfo);
|
|
292
|
-
clone.pluginId = this.pluginId;
|
|
293
|
-
clone.tablePrefix = clone.tablePrefix || `${this.pluginId}_`;
|
|
294
|
-
return clone;
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
url: 'http://localhost:3500',
|
|
298
|
-
key: 'rimori-sdk-key',
|
|
299
|
-
backendUrl: 'http://localhost:3501',
|
|
300
|
-
token: 'rimori-token',
|
|
301
|
-
expiration: new Date(Date.now() + 60 * 60 * 1000),
|
|
302
|
-
tablePrefix: `${this.pluginId}_`,
|
|
303
|
-
pluginId: this.pluginId,
|
|
304
|
-
guild: {
|
|
305
|
-
id: 'guild-test',
|
|
306
|
-
longTermGoalOverride: '',
|
|
307
|
-
allowUserPluginSettings: true,
|
|
308
|
-
},
|
|
309
|
-
installedPlugins: [
|
|
310
|
-
{
|
|
311
|
-
id: this.pluginId,
|
|
312
|
-
title: 'Test Plugin',
|
|
313
|
-
description: 'Playwright testing plugin',
|
|
314
|
-
logo: '',
|
|
315
|
-
url: 'https://plugins.rimori.localhost',
|
|
316
|
-
},
|
|
317
|
-
],
|
|
318
|
-
profile: this.cloneUserInfo(this.currentUserInfo),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
serializeRimoriInfo(info) {
|
|
322
|
-
return {
|
|
323
|
-
...info,
|
|
324
|
-
expiration: info.expiration.toISOString(),
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
293
|
cloneUserInfo(input) {
|
|
328
294
|
return JSON.parse(JSON.stringify(input));
|
|
329
295
|
}
|
|
330
|
-
mergeUserInfo(current, overrides) {
|
|
331
|
-
const clone = this.cloneUserInfo(current);
|
|
332
|
-
if (overrides.mother_tongue) {
|
|
333
|
-
clone.mother_tongue = {
|
|
334
|
-
...clone.mother_tongue,
|
|
335
|
-
...overrides.mother_tongue,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
if (overrides.target_language) {
|
|
339
|
-
clone.target_language = {
|
|
340
|
-
...clone.target_language,
|
|
341
|
-
...overrides.target_language,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
if (overrides.study_buddy) {
|
|
345
|
-
clone.study_buddy = {
|
|
346
|
-
...clone.study_buddy,
|
|
347
|
-
...overrides.study_buddy,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
const { mother_tongue, target_language, study_buddy, ...rest } = overrides;
|
|
351
|
-
for (const [key, value] of Object.entries(rest)) {
|
|
352
|
-
if (value === undefined) {
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
clone[key] = value;
|
|
356
|
-
}
|
|
357
|
-
return clone;
|
|
358
|
-
}
|
|
359
296
|
registerAutoResponders() {
|
|
360
|
-
this.autoResponders.set('global.supabase.requestAccess', () => this.
|
|
361
|
-
this.autoResponders.set('global.profile.requestUserInfo', () => this.cloneUserInfo(this.
|
|
362
|
-
this.autoResponders.set('global.profile.getUserInfo', () => this.cloneUserInfo(this.
|
|
297
|
+
this.autoResponders.set('global.supabase.requestAccess', () => this.cloneRimoriInfo(this.rimoriInfo));
|
|
298
|
+
this.autoResponders.set('global.profile.requestUserInfo', () => this.cloneUserInfo(this.rimoriInfo.profile));
|
|
299
|
+
this.autoResponders.set('global.profile.getUserInfo', () => this.cloneUserInfo(this.rimoriInfo.profile));
|
|
363
300
|
}
|
|
364
301
|
cloneRimoriInfo(info) {
|
|
365
|
-
return
|
|
366
|
-
...info,
|
|
367
|
-
expiration: new Date(info.expiration),
|
|
368
|
-
guild: { ...info.guild },
|
|
369
|
-
installedPlugins: info.installedPlugins.map((plugin) => ({ ...plugin })),
|
|
370
|
-
profile: this.cloneUserInfo(info.profile),
|
|
371
|
-
};
|
|
302
|
+
return JSON.parse(JSON.stringify(info));
|
|
372
303
|
}
|
|
373
304
|
}
|
|
374
305
|
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,21 @@ 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;
|
|
57
|
+
/**
|
|
58
|
+
* Sets up default handlers for shared_content and shared_content_completed routes.
|
|
59
|
+
* These provide sensible defaults so tests don't need to mock every shared content call.
|
|
60
|
+
*/
|
|
61
|
+
private setupSharedContentRoutes;
|
|
46
62
|
/**
|
|
47
63
|
* Formats text as SSE (Server-Sent Events) response.
|
|
48
64
|
* Since Playwright's route.fulfill() requires complete body, we format as SSE without delays.
|
|
@@ -78,30 +94,31 @@ export declare class RimoriTestEnvironment {
|
|
|
78
94
|
private addBackendRoute;
|
|
79
95
|
readonly plugin: {
|
|
80
96
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
97
|
+
* Manually set the settings state (useful for test setup).
|
|
98
|
+
* This directly modifies the internal settings state.
|
|
99
|
+
* @param settings - The settings to set, or null to clear settings
|
|
100
|
+
*/
|
|
101
|
+
setSettings: (settings: PluginSettings | null) => void;
|
|
102
|
+
/**
|
|
103
|
+
* Get the current settings state (useful for assertions).
|
|
104
|
+
* @returns The current settings or null if no settings exist
|
|
105
|
+
*/
|
|
106
|
+
getSettings: () => PluginSettings | null;
|
|
107
|
+
/**
|
|
108
|
+
* Override the GET handler for plugin_settings (rarely needed).
|
|
109
|
+
* By default, GET returns the current state from SettingsStateManager.
|
|
84
110
|
*/
|
|
85
|
-
|
|
111
|
+
mockGetSettings: (settingsRow: PluginSettings | null, options?: MockOptions) => void;
|
|
86
112
|
/**
|
|
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).
|
|
113
|
+
* Override the PATCH handler for plugin_settings (rarely needed).
|
|
114
|
+
* By default, PATCH updates the state in SettingsStateManager.
|
|
91
115
|
*/
|
|
92
|
-
|
|
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;
|
|
116
|
+
mockSetSettings: (response: unknown, options?: MockOptions) => void;
|
|
100
117
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
118
|
+
* Override the POST handler for plugin_settings (rarely needed).
|
|
119
|
+
* By default, POST inserts/updates the state in SettingsStateManager.
|
|
103
120
|
*/
|
|
104
|
-
mockInsertSettings: (response
|
|
121
|
+
mockInsertSettings: (response: unknown, options?: MockOptions) => void;
|
|
105
122
|
mockGetUserInfo: (userInfo: Partial<UserInfo>, options?: MockOptions) => void;
|
|
106
123
|
mockGetPluginInfo: (pluginInfo: Plugin, options?: MockOptions) => void;
|
|
107
124
|
};
|
|
@@ -131,7 +148,7 @@ export declare class RimoriTestEnvironment {
|
|
|
131
148
|
* `worker/listeners/decks.ts` or `worker/listeners/flascards.ts` – those run in a
|
|
132
149
|
* separate process. This helper is intended for UI‑side events only.
|
|
133
150
|
*/
|
|
134
|
-
mockEmit: (topic: string, data:
|
|
151
|
+
mockEmit: (topic: string, data: EventPayload, sender?: string) => Promise<void>;
|
|
135
152
|
/**
|
|
136
153
|
* Registers a one-time auto-responder for request/response style events.
|
|
137
154
|
*
|
|
@@ -272,14 +289,43 @@ export declare class RimoriTestEnvironment {
|
|
|
272
289
|
};
|
|
273
290
|
readonly community: {
|
|
274
291
|
sharedContent: {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Mock the shared_content GET endpoint for fetching a single item.
|
|
294
|
+
* Used by SharedContentController.getSharedContent()
|
|
295
|
+
*/
|
|
296
|
+
mockGet: (value: unknown, options?: MockOptions) => void;
|
|
297
|
+
/**
|
|
298
|
+
* Mock the shared_content GET endpoint for fetching multiple items.
|
|
299
|
+
* Used by SharedContentController.getSharedContentList() and getCompletedTopics()
|
|
300
|
+
*/
|
|
301
|
+
mockGetList: (value: unknown[], options?: MockOptions) => void;
|
|
302
|
+
/**
|
|
303
|
+
* Mock the shared_content POST endpoint for creating new content.
|
|
304
|
+
* Used by SharedContentController.createSharedContent() after AI generation.
|
|
305
|
+
* Note: getNewSharedContent() first calls ai.getObject() (mock via env.ai.mockGetObject),
|
|
306
|
+
* then calls createSharedContent() which hits this endpoint.
|
|
307
|
+
*/
|
|
308
|
+
mockCreate: (value: unknown, options?: MockOptions) => void;
|
|
309
|
+
/**
|
|
310
|
+
* Mock the shared_content PATCH endpoint for updating content.
|
|
311
|
+
* Used by SharedContentController.updateSharedContent() and removeSharedContent() (soft delete)
|
|
312
|
+
*/
|
|
313
|
+
mockUpdate: (value: unknown, options?: MockOptions) => void;
|
|
314
|
+
/**
|
|
315
|
+
* Mock the shared_content_completed POST endpoint (upsert).
|
|
316
|
+
* Used by SharedContentController.completeSharedContent() and updateSharedContentState()
|
|
317
|
+
*/
|
|
318
|
+
mockComplete: (value?: unknown, options?: MockOptions) => void;
|
|
319
|
+
/**
|
|
320
|
+
* Mock the shared_content_completed POST endpoint for state updates.
|
|
321
|
+
* Alias for mockComplete since both use upsert via POST.
|
|
322
|
+
*/
|
|
323
|
+
mockUpdateState: (value?: unknown, options?: MockOptions) => void;
|
|
324
|
+
/**
|
|
325
|
+
* Mock removing shared content (soft delete via PATCH).
|
|
326
|
+
* Alias for mockUpdate since removeSharedContent uses PATCH to set deleted_at.
|
|
327
|
+
*/
|
|
328
|
+
mockRemove: (value: unknown, options?: MockOptions) => void;
|
|
283
329
|
};
|
|
284
330
|
};
|
|
285
331
|
readonly exercise: {
|
|
@@ -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
|
-
console.warn('mockGetSettings is not tested');
|
|
35
|
-
// GET request returns the full row or null (from maybeSingle())
|
|
36
|
-
// null means no settings exist, which triggers setSettings() -> INSERT
|
|
37
|
-
this.addSupabaseRoute('plugin_settings', settingsRow, options);
|
|
33
|
+
this.addSupabaseRoute('plugin_settings', settingsRow, { ...options, method: 'GET' });
|
|
38
34
|
},
|
|
39
35
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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' });
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
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,7 +70,7 @@ 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
|
},
|
|
@@ -222,21 +211,15 @@ class RimoriTestEnvironment {
|
|
|
222
211
|
* @param options - Optional mock options.
|
|
223
212
|
*/
|
|
224
213
|
mockGetSteamedText: (text, options) => {
|
|
225
|
-
console.log('Mocking get steamed text for mockGetSteamedText', text, options);
|
|
226
214
|
this.addBackendRoute('/ai/llm', text, { ...options, isStreaming: true });
|
|
227
215
|
},
|
|
228
216
|
mockGetVoice: (values, options) => {
|
|
229
|
-
console.log('Mocking get voice for mockGetVoice', values, options);
|
|
230
|
-
console.warn('mockGetVoice is not tested');
|
|
231
217
|
this.addBackendRoute('/voice/tts', values, options);
|
|
232
218
|
},
|
|
233
219
|
mockGetTextFromVoice: (text, options) => {
|
|
234
|
-
console.log('Mocking get text from voice for mockGetTextFromVoice', text, options);
|
|
235
|
-
console.warn('mockGetTextFromVoice is not tested');
|
|
236
220
|
this.addBackendRoute('/voice/stt', text, options);
|
|
237
221
|
},
|
|
238
222
|
mockGetObject: (value, options) => {
|
|
239
|
-
console.log('Mocking get object for mockGetObject', value, options);
|
|
240
223
|
this.addBackendRoute('/ai/llm-object', value, { ...options, method: 'POST' });
|
|
241
224
|
},
|
|
242
225
|
};
|
|
@@ -364,14 +347,57 @@ class RimoriTestEnvironment {
|
|
|
364
347
|
};
|
|
365
348
|
this.community = {
|
|
366
349
|
sharedContent: {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Mock the shared_content GET endpoint for fetching a single item.
|
|
352
|
+
* Used by SharedContentController.getSharedContent()
|
|
353
|
+
*/
|
|
354
|
+
mockGet: (value, options) => {
|
|
355
|
+
this.addSupabaseRoute('shared_content', value, { ...options, method: 'GET' });
|
|
356
|
+
},
|
|
357
|
+
/**
|
|
358
|
+
* Mock the shared_content GET endpoint for fetching multiple items.
|
|
359
|
+
* Used by SharedContentController.getSharedContentList() and getCompletedTopics()
|
|
360
|
+
*/
|
|
361
|
+
mockGetList: (value, options) => {
|
|
362
|
+
this.addSupabaseRoute('shared_content', value, { ...options, method: 'GET' });
|
|
363
|
+
},
|
|
364
|
+
/**
|
|
365
|
+
* Mock the shared_content POST endpoint for creating new content.
|
|
366
|
+
* Used by SharedContentController.createSharedContent() after AI generation.
|
|
367
|
+
* Note: getNewSharedContent() first calls ai.getObject() (mock via env.ai.mockGetObject),
|
|
368
|
+
* then calls createSharedContent() which hits this endpoint.
|
|
369
|
+
*/
|
|
370
|
+
mockCreate: (value, options) => {
|
|
371
|
+
this.addSupabaseRoute('shared_content', value, { ...options, method: 'POST' });
|
|
372
|
+
},
|
|
373
|
+
/**
|
|
374
|
+
* Mock the shared_content PATCH endpoint for updating content.
|
|
375
|
+
* Used by SharedContentController.updateSharedContent() and removeSharedContent() (soft delete)
|
|
376
|
+
*/
|
|
377
|
+
mockUpdate: (value, options) => {
|
|
378
|
+
this.addSupabaseRoute('shared_content', value, { ...options, method: 'PATCH' });
|
|
379
|
+
},
|
|
380
|
+
/**
|
|
381
|
+
* Mock the shared_content_completed POST endpoint (upsert).
|
|
382
|
+
* Used by SharedContentController.completeSharedContent() and updateSharedContentState()
|
|
383
|
+
*/
|
|
384
|
+
mockComplete: (value = {}, options) => {
|
|
385
|
+
this.addSupabaseRoute('shared_content_completed', value, { ...options, method: 'POST' });
|
|
386
|
+
},
|
|
387
|
+
/**
|
|
388
|
+
* Mock the shared_content_completed POST endpoint for state updates.
|
|
389
|
+
* Alias for mockComplete since both use upsert via POST.
|
|
390
|
+
*/
|
|
391
|
+
mockUpdateState: (value = {}, options) => {
|
|
392
|
+
this.addSupabaseRoute('shared_content_completed', value, { ...options, method: 'POST' });
|
|
393
|
+
},
|
|
394
|
+
/**
|
|
395
|
+
* Mock removing shared content (soft delete via PATCH).
|
|
396
|
+
* Alias for mockUpdate since removeSharedContent uses PATCH to set deleted_at.
|
|
397
|
+
*/
|
|
398
|
+
mockRemove: (value, options) => {
|
|
399
|
+
this.addSupabaseRoute('shared_content', value, { ...options, method: 'PATCH' });
|
|
400
|
+
},
|
|
375
401
|
},
|
|
376
402
|
};
|
|
377
403
|
this.exercise = {
|
|
@@ -384,8 +410,86 @@ class RimoriTestEnvironment {
|
|
|
384
410
|
};
|
|
385
411
|
this.page = options.page;
|
|
386
412
|
this.pluginId = options.pluginId;
|
|
387
|
-
|
|
388
|
-
|
|
413
|
+
this.rimoriInfo = this.getRimoriInfo(options);
|
|
414
|
+
// Initialize settings state manager
|
|
415
|
+
this.settingsManager = new SettingsStateManager_1.SettingsStateManager(options.settings || null, options.pluginId, this.rimoriInfo.guild.id);
|
|
416
|
+
this.interceptRoutes(options.pluginUrl);
|
|
417
|
+
}
|
|
418
|
+
interceptRoutes(pluginUrl) {
|
|
419
|
+
// Intercept all /locales requests and fetch from the dev server
|
|
420
|
+
this.page.route(`${pluginUrl}/locales/**`, async (route) => {
|
|
421
|
+
const request = route.request();
|
|
422
|
+
const url = new URL(request.url());
|
|
423
|
+
const devServerUrl = `http://${url.host}/locales/en.json`;
|
|
424
|
+
// console.log('Fetching locales from: ' + devServerUrl);
|
|
425
|
+
// throw new Error('Test: ' + devServerUrl);
|
|
426
|
+
try {
|
|
427
|
+
// Fetch from the dev server
|
|
428
|
+
const response = await fetch(devServerUrl);
|
|
429
|
+
const body = await response.text();
|
|
430
|
+
await route.fulfill({
|
|
431
|
+
status: response.status,
|
|
432
|
+
headers: { 'Content-Type': 'application/json' },
|
|
433
|
+
body,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
console.error(`Error fetching translation from ${devServerUrl}:`, error);
|
|
438
|
+
await route.fulfill({
|
|
439
|
+
status: 500,
|
|
440
|
+
headers: { 'Content-Type': 'application/json' },
|
|
441
|
+
body: JSON.stringify({ error: 'Failed to load translations' }),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
this.page.route(`${this.rimoriInfo.backendUrl}/**`, (route) => this.handleRoute(route, this.backendRoutes));
|
|
446
|
+
this.page.route(`${this.rimoriInfo.url}/**`, (route) => this.handleRoute(route, this.supabaseRoutes));
|
|
447
|
+
}
|
|
448
|
+
async setup() {
|
|
449
|
+
// console.log('Setting up RimoriTestEnvironment');
|
|
450
|
+
this.page.on('console', (msg) => {
|
|
451
|
+
const logLevel = msg.type();
|
|
452
|
+
const logMessage = msg.text();
|
|
453
|
+
if (logLevel === 'debug')
|
|
454
|
+
return;
|
|
455
|
+
if (logMessage.includes('Download the React DevTools'))
|
|
456
|
+
return;
|
|
457
|
+
if (logMessage.includes('languageChanged en'))
|
|
458
|
+
return;
|
|
459
|
+
if (logMessage.includes('i18next: initialized {debug: true'))
|
|
460
|
+
return;
|
|
461
|
+
console.log(`[browser:${logLevel}]`, logMessage);
|
|
462
|
+
});
|
|
463
|
+
// Set up default handlers for plugin_settings routes using SettingsStateManager
|
|
464
|
+
this.setupSettingsRoutes();
|
|
465
|
+
// Set up default handlers for shared_content routes
|
|
466
|
+
this.setupSharedContentRoutes();
|
|
467
|
+
// Initialize MessageChannelSimulator to simulate parent-iframe communication
|
|
468
|
+
// This makes the plugin think it's running in an iframe (not standalone mode)
|
|
469
|
+
// Convert RimoriInfo from CommunicationHandler format to MessageChannelSimulator format
|
|
470
|
+
this.messageChannelSimulator = new MessageChannelSimulator_1.MessageChannelSimulator({
|
|
471
|
+
page: this.page,
|
|
472
|
+
pluginId: this.pluginId,
|
|
473
|
+
queryParams: {},
|
|
474
|
+
rimoriInfo: this.rimoriInfo,
|
|
475
|
+
});
|
|
476
|
+
// Initialize the simulator - this injects the necessary shims
|
|
477
|
+
// to intercept window.parent.postMessage calls and set up MessageChannel communication
|
|
478
|
+
await this.messageChannelSimulator.initialize();
|
|
479
|
+
// Set up a no-op handler for pl454583483.session.triggerUrlChange
|
|
480
|
+
// This prevents errors if the plugin emits this event
|
|
481
|
+
this.messageChannelSimulator.on(`${this.pluginId}.session.triggerUrlChange`, () => {
|
|
482
|
+
// No-op handler - does nothing
|
|
483
|
+
});
|
|
484
|
+
this.messageChannelSimulator.on('global.accomplishment.triggerMicro', () => {
|
|
485
|
+
// No-op handler - does nothing
|
|
486
|
+
});
|
|
487
|
+
this.messageChannelSimulator.on('global.accomplishment.triggerMacro', () => {
|
|
488
|
+
// No-op handler - does nothing
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
getRimoriInfo(options) {
|
|
492
|
+
return {
|
|
389
493
|
key: 'rimori-testing-key',
|
|
390
494
|
token: 'rimori-testing-token',
|
|
391
495
|
url: 'http://localhost:3500',
|
|
@@ -416,80 +520,99 @@ class RimoriTestEnvironment {
|
|
|
416
520
|
profile: default_user_info_1.DEFAULT_USER_INFO,
|
|
417
521
|
mainPanelPlugin: undefined,
|
|
418
522
|
sidePanelPlugin: undefined,
|
|
523
|
+
interfaceLanguage: default_user_info_1.DEFAULT_USER_INFO.mother_tongue.code, // Set interface language from user's mother tongue
|
|
419
524
|
};
|
|
420
|
-
this.interceptRoutes();
|
|
421
525
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
this.
|
|
429
|
-
|
|
526
|
+
/**
|
|
527
|
+
* Sets up the plugin_settings routes to use the SettingsStateManager.
|
|
528
|
+
* GET returns current state, PATCH updates state, POST creates/updates state.
|
|
529
|
+
*/
|
|
530
|
+
setupSettingsRoutes() {
|
|
531
|
+
// GET: Return current settings state
|
|
532
|
+
this.addSupabaseRoute('plugin_settings', () => this.settingsManager.getSettings(), {
|
|
533
|
+
method: 'GET',
|
|
430
534
|
});
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
description: p.info?.description || '',
|
|
467
|
-
logo: p.info?.logo || '',
|
|
468
|
-
url: p.pages?.external_hosted_url || '',
|
|
469
|
-
})),
|
|
470
|
-
mainPanelPlugin: this.rimoriInfo.mainPanelPlugin
|
|
471
|
-
? {
|
|
472
|
-
id: this.rimoriInfo.mainPanelPlugin.id,
|
|
473
|
-
title: this.rimoriInfo.mainPanelPlugin.info?.title || '',
|
|
474
|
-
description: this.rimoriInfo.mainPanelPlugin.info?.description || '',
|
|
475
|
-
logo: this.rimoriInfo.mainPanelPlugin.info?.logo || '',
|
|
476
|
-
url: this.rimoriInfo.mainPanelPlugin.pages?.external_hosted_url || '',
|
|
477
|
-
}
|
|
478
|
-
: undefined,
|
|
479
|
-
sidePanelPlugin: this.rimoriInfo.sidePanelPlugin
|
|
480
|
-
? {
|
|
481
|
-
id: this.rimoriInfo.sidePanelPlugin.id,
|
|
482
|
-
title: this.rimoriInfo.sidePanelPlugin.info?.title || '',
|
|
483
|
-
description: this.rimoriInfo.sidePanelPlugin.info?.description || '',
|
|
484
|
-
logo: this.rimoriInfo.sidePanelPlugin.info?.logo || '',
|
|
485
|
-
url: this.rimoriInfo.sidePanelPlugin.pages?.external_hosted_url || '',
|
|
486
|
-
}
|
|
487
|
-
: undefined,
|
|
488
|
-
},
|
|
535
|
+
// PATCH: Update settings based on request body
|
|
536
|
+
this.addSupabaseRoute('plugin_settings', async (request) => {
|
|
537
|
+
try {
|
|
538
|
+
const postData = request.postData();
|
|
539
|
+
if (postData) {
|
|
540
|
+
const updates = JSON.parse(postData);
|
|
541
|
+
return this.settingsManager.updateSettings(updates);
|
|
542
|
+
}
|
|
543
|
+
// If no body, return empty array (no update)
|
|
544
|
+
return this.settingsManager.updateSettings({});
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
// If parsing fails, return empty array
|
|
548
|
+
return this.settingsManager.updateSettings({});
|
|
549
|
+
}
|
|
550
|
+
}, {
|
|
551
|
+
method: 'PATCH',
|
|
552
|
+
});
|
|
553
|
+
// POST: Insert/update settings based on request body
|
|
554
|
+
this.addSupabaseRoute('plugin_settings', async (request) => {
|
|
555
|
+
try {
|
|
556
|
+
const postData = request.postData();
|
|
557
|
+
if (postData) {
|
|
558
|
+
const newSettings = JSON.parse(postData);
|
|
559
|
+
return this.settingsManager.insertSettings(newSettings);
|
|
560
|
+
}
|
|
561
|
+
// If no body, insert with defaults
|
|
562
|
+
return this.settingsManager.insertSettings({});
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// If parsing fails, insert with defaults
|
|
566
|
+
return this.settingsManager.insertSettings({});
|
|
567
|
+
}
|
|
568
|
+
}, {
|
|
569
|
+
method: 'POST',
|
|
489
570
|
});
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Sets up default handlers for shared_content and shared_content_completed routes.
|
|
574
|
+
* These provide sensible defaults so tests don't need to mock every shared content call.
|
|
575
|
+
*/
|
|
576
|
+
setupSharedContentRoutes() {
|
|
577
|
+
// GET: Return empty array for getCompletedTopics and getSharedContentList
|
|
578
|
+
this.addSupabaseRoute('shared_content', [], { method: 'GET' });
|
|
579
|
+
// POST: Return created content with generated ID for createSharedContent
|
|
580
|
+
this.addSupabaseRoute('shared_content', async (request) => {
|
|
581
|
+
try {
|
|
582
|
+
const postData = request.postData();
|
|
583
|
+
if (postData) {
|
|
584
|
+
const content = JSON.parse(postData);
|
|
585
|
+
// Return the content with a generated ID
|
|
586
|
+
return [
|
|
587
|
+
{
|
|
588
|
+
id: `shared-content-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
589
|
+
...content,
|
|
590
|
+
created_at: new Date().toISOString(),
|
|
591
|
+
},
|
|
592
|
+
];
|
|
593
|
+
}
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
}, { method: 'POST' });
|
|
600
|
+
// PATCH: Return updated content for updateSharedContent and removeSharedContent
|
|
601
|
+
this.addSupabaseRoute('shared_content', async (request) => {
|
|
602
|
+
try {
|
|
603
|
+
const postData = request.postData();
|
|
604
|
+
if (postData) {
|
|
605
|
+
const updates = JSON.parse(postData);
|
|
606
|
+
return [{ id: 'updated-content', ...updates }];
|
|
607
|
+
}
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
return [];
|
|
612
|
+
}
|
|
613
|
+
}, { method: 'PATCH' });
|
|
614
|
+
// POST: Handle shared_content_completed upserts
|
|
615
|
+
this.addSupabaseRoute('shared_content_completed', {}, { method: 'POST' });
|
|
493
616
|
}
|
|
494
617
|
/**
|
|
495
618
|
* Formats text as SSE (Server-Sent Events) response.
|
|
@@ -548,10 +671,11 @@ class RimoriTestEnvironment {
|
|
|
548
671
|
const requestUrl = request.url();
|
|
549
672
|
const method = request.method().toUpperCase();
|
|
550
673
|
const routeKey = this.createRouteKey(method, requestUrl);
|
|
551
|
-
console.log('Handling route', routeKey);
|
|
674
|
+
// console.log('Handling route', routeKey);
|
|
552
675
|
const mocks = routes[routeKey];
|
|
553
676
|
if (!mocks || mocks.length === 0) {
|
|
554
677
|
console.error('No route handler found for route', routeKey);
|
|
678
|
+
throw new Error('No route handler found for route: ' + routeKey);
|
|
555
679
|
route.abort('not_found');
|
|
556
680
|
return;
|
|
557
681
|
}
|
|
@@ -591,10 +715,15 @@ class RimoriTestEnvironment {
|
|
|
591
715
|
if (options?.error) {
|
|
592
716
|
return await route.abort(options.error);
|
|
593
717
|
}
|
|
718
|
+
// Handle function-based mocks (for stateful responses like settings)
|
|
719
|
+
let responseValue = matchingMock.value;
|
|
720
|
+
if (typeof matchingMock.value === 'function') {
|
|
721
|
+
responseValue = await matchingMock.value(request);
|
|
722
|
+
}
|
|
594
723
|
// Handle streaming responses (for mockGetSteamedText)
|
|
595
724
|
// Since Playwright requires complete body, we format as SSE without delays
|
|
596
|
-
if (matchingMock.isStreaming && typeof
|
|
597
|
-
const body = this.formatAsSSE(
|
|
725
|
+
if (matchingMock.isStreaming && typeof responseValue === 'string') {
|
|
726
|
+
const body = this.formatAsSSE(responseValue);
|
|
598
727
|
return await route.fulfill({
|
|
599
728
|
status: 200,
|
|
600
729
|
headers: { 'Content-Type': 'text/event-stream' },
|
|
@@ -602,7 +731,7 @@ class RimoriTestEnvironment {
|
|
|
602
731
|
});
|
|
603
732
|
}
|
|
604
733
|
// Regular JSON response
|
|
605
|
-
const responseBody = JSON.stringify(
|
|
734
|
+
const responseBody = JSON.stringify(responseValue);
|
|
606
735
|
route.fulfill({
|
|
607
736
|
status: 200,
|
|
608
737
|
body: responseBody,
|
|
@@ -618,6 +747,7 @@ class RimoriTestEnvironment {
|
|
|
618
747
|
const method = options?.method ?? 'GET';
|
|
619
748
|
const fullPath = `${this.rimoriInfo.url}/rest/v1/${path}`;
|
|
620
749
|
const routeKey = this.createRouteKey(method, fullPath);
|
|
750
|
+
// console.log('Registering supabase route:', routeKey);
|
|
621
751
|
if (!this.supabaseRoutes[routeKey]) {
|
|
622
752
|
this.supabaseRoutes[routeKey] = [];
|
|
623
753
|
}
|
|
@@ -635,7 +765,6 @@ class RimoriTestEnvironment {
|
|
|
635
765
|
* @param isStreaming - Optional flag to mark this as a streaming response.
|
|
636
766
|
*/
|
|
637
767
|
addBackendRoute(path, values, options) {
|
|
638
|
-
console.warn('addBackendRoute is not tested');
|
|
639
768
|
const method = options?.method ?? 'POST';
|
|
640
769
|
const fullPath = `${this.rimoriInfo.backendUrl}${path.startsWith('/') ? path : '/' + path}`;
|
|
641
770
|
const routeKey = this.createRouteKey(method, fullPath);
|
|
@@ -653,4 +782,3 @@ class RimoriTestEnvironment {
|
|
|
653
782
|
}
|
|
654
783
|
exports.RimoriTestEnvironment = RimoriTestEnvironment;
|
|
655
784
|
// Todo: How to test if the event was received by the parent?
|
|
656
|
-
// 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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Manages plugin settings state for test environment.
|
|
4
|
+
* Provides a single source of truth for settings that can be modified by mocked API calls.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SettingsStateManager = void 0;
|
|
8
|
+
class SettingsStateManager {
|
|
9
|
+
constructor(initialSettings, pluginId, guildId) {
|
|
10
|
+
this.settings = {
|
|
11
|
+
id: initialSettings?.id ?? 'settings-id',
|
|
12
|
+
plugin_id: initialSettings?.plugin_id ?? pluginId,
|
|
13
|
+
guild_id: initialSettings?.guild_id ?? guildId,
|
|
14
|
+
settings: initialSettings?.settings ?? {},
|
|
15
|
+
is_guild_setting: initialSettings?.is_guild_setting ?? false,
|
|
16
|
+
user_id: initialSettings?.user_id ?? null,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get current settings state (for GET requests)
|
|
21
|
+
* Returns null if no settings exist, otherwise returns the full settings object
|
|
22
|
+
*/
|
|
23
|
+
getSettings() {
|
|
24
|
+
return this.settings;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Update settings (for PATCH requests)
|
|
28
|
+
* @param updates - Partial settings to update
|
|
29
|
+
* @returns Array with updated row if settings exist, empty array if no settings exist
|
|
30
|
+
*/
|
|
31
|
+
updateSettings(updates) {
|
|
32
|
+
if (this.settings === null) {
|
|
33
|
+
// No settings exist - PATCH returns empty array (triggers INSERT flow)
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
// Update existing settings
|
|
37
|
+
this.settings = {
|
|
38
|
+
...this.settings,
|
|
39
|
+
...updates,
|
|
40
|
+
// Ensure these fields are preserved
|
|
41
|
+
id: this.settings.id,
|
|
42
|
+
plugin_id: this.settings.plugin_id,
|
|
43
|
+
guild_id: this.settings.guild_id,
|
|
44
|
+
};
|
|
45
|
+
// PATCH returns array with updated row
|
|
46
|
+
return [this.settings];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Insert new settings (for POST requests)
|
|
50
|
+
* @param newSettings - Settings to insert
|
|
51
|
+
* @returns The inserted settings object
|
|
52
|
+
*/
|
|
53
|
+
insertSettings(newSettings) {
|
|
54
|
+
// Update existing settings with new values
|
|
55
|
+
this.settings = {
|
|
56
|
+
...this.settings,
|
|
57
|
+
...newSettings,
|
|
58
|
+
};
|
|
59
|
+
return this.settings;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Manually set settings (useful for test setup)
|
|
63
|
+
*/
|
|
64
|
+
setSettings(settings) {
|
|
65
|
+
this.settings = settings;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if settings exist
|
|
69
|
+
*/
|
|
70
|
+
hasSettings() {
|
|
71
|
+
return this.settings !== null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.SettingsStateManager = SettingsStateManager;
|
|
@@ -7,7 +7,7 @@ const pluginUrl = 'http://localhost:3009';
|
|
|
7
7
|
test_1.test.describe('Translator Plugin', () => {
|
|
8
8
|
let env;
|
|
9
9
|
test_1.test.beforeEach(async ({ page }) => {
|
|
10
|
-
env = new RimoriTestEnvironment_1.RimoriTestEnvironment({ page, pluginId });
|
|
10
|
+
env = new RimoriTestEnvironment_1.RimoriTestEnvironment({ page, pluginId, pluginUrl });
|
|
11
11
|
env.ai.mockGetObject({
|
|
12
12
|
gramatically_corrected_input_text: 'tree',
|
|
13
13
|
detected_language: 'English',
|
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimori/playwright-testing",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4-next.0",
|
|
4
4
|
"description": "Playwright testing utilities for Rimori plugins and workers",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/rimori-org/playwright-testing.git"
|
|
9
|
+
},
|
|
6
10
|
"main": "dist/index.js",
|
|
7
11
|
"types": "dist/index.d.ts",
|
|
8
12
|
"source": "src/index.ts",
|
|
9
13
|
"scripts": {
|
|
10
14
|
"build": "tsc -p tsconfig.json",
|
|
15
|
+
"dev": "tsc -w --preserveWatchOutput -p tsconfig.json",
|
|
11
16
|
"clean": "rimraf dist",
|
|
12
17
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
13
18
|
"test": "playwright test",
|
|
@@ -17,13 +22,12 @@
|
|
|
17
22
|
"test:headed:debug": "playwright test --headed --debug"
|
|
18
23
|
},
|
|
19
24
|
"peerDependencies": {
|
|
20
|
-
"@playwright/test": "^1.40.0"
|
|
21
|
-
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@rimori/client": "^2.1.7"
|
|
25
|
+
"@playwright/test": "^1.40.0",
|
|
26
|
+
"@rimori/client": "^2.2.0"
|
|
24
27
|
},
|
|
25
28
|
"devDependencies": {
|
|
26
29
|
"@playwright/test": "^1.40.0",
|
|
30
|
+
"@rimori/client": "^2.2.0",
|
|
27
31
|
"@types/node": "^20.12.7",
|
|
28
32
|
"rimraf": "^5.0.7",
|
|
29
33
|
"typescript": "^5.7.2"
|