@rimori/playwright-testing 0.2.3 → 0.2.4

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.
@@ -1,79 +1,10 @@
1
1
  import type { Page } from '@playwright/test';
2
- type Language = {
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 baseUserInfo;
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: unknown, sender?: string): Promise<void>;
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 default profile returned by the auto responders.
54
+ * Overrides the user info.
127
55
  */
128
- setUserInfo(overrides: Partial<UserInfo>): void;
129
- getRimoriInfo(): RimoriInfo | null;
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.baseUserInfo = this.cloneUserInfo(default_user_info_1.DEFAULT_USER_INFO);
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.baseUserInfo);
26
+ return this.cloneUserInfo(this.rimoriInfo.profile);
31
27
  }
32
28
  get userInfo() {
33
- return this.cloneUserInfo(this.currentUserInfo);
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 default profile returned by the auto responders.
176
+ * Overrides the user info.
180
177
  */
181
- setUserInfo(overrides) {
182
- this.currentUserInfo = this.mergeUserInfo(this.currentUserInfo, overrides);
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.currentRimoriInfo ? this.cloneRimoriInfo(this.currentRimoriInfo) : null;
182
+ return this.cloneRimoriInfo(this.rimoriInfo);
189
183
  }
190
184
  async setupMessageChannel() {
191
185
  if (this.isReady) {
192
186
  return;
193
187
  }
194
- const rimoriInfo = this.buildRimoriInfo();
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: serialized,
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('[MessageChannelSimulator] handlePortMessage - received event:', payload.event.topic, 'from:', payload.event.sender);
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('[MessageChannelSimulator] dispatchEvent - topic:', event.topic, 'sender:', event.sender, 'listeners:', this.listeners.has(event.topic) ? this.listeners.get(event.topic)?.size : 0);
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
- console.log('[MessageChannelSimulator] No handlers found for topic:', event.topic);
262
- console.log('[MessageChannelSimulator] Available topics:', Array.from(this.listeners.keys()));
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.buildRimoriInfo());
361
- this.autoResponders.set('global.profile.requestUserInfo', () => this.cloneUserInfo(this.currentUserInfo));
362
- this.autoResponders.set('global.profile.getUserInfo', () => this.cloneUserInfo(this.currentUserInfo));
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
- * Mocks PATCH request for updating plugin_settings.
82
- * @param response - The response for PATCH. Defaults to empty array (no rows updated).
83
- * Should return array with updated row(s) like [{ id: '...' }] if update succeeds.
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
- mockSetSettings: (response?: unknown, options?: MockOptions) => void;
111
+ mockGetSettings: (settingsRow: PluginSettings | null, options?: MockOptions) => void;
86
112
  /**
87
- * Mocks GET request for fetching plugin_settings.
88
- * @param settingsRow - The full row object from plugin_settings table, or null if not found.
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
- 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;
116
+ mockSetSettings: (response: unknown, options?: MockOptions) => void;
100
117
  /**
101
- * Mocks POST request for inserting plugin_settings.
102
- * @param response - The response for POST. Defaults to success response with inserted row.
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?: unknown, options?: MockOptions) => void;
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: unknown, sender?: string) => Promise<void>;
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
- mockGet: () => void;
276
- mockGetList: () => void;
277
- mockGetNew: () => void;
278
- mockCreate: () => void;
279
- mockUpdate: () => void;
280
- mockComplete: () => void;
281
- mockUpdateState: () => void;
282
- mockRemove: () => void;
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
- * Mocks PATCH request for updating plugin_settings.
14
- * @param response - The response for PATCH. Defaults to empty array (no rows updated).
15
- * Should return array with updated row(s) like [{ id: '...' }] if update succeeds.
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
- mockSetSettings: (response, options) => {
18
- console.log('Mocking set settings for mockSetSettings', response, options);
19
- console.warn('mockSetSettings is not tested');
20
- // PATCH request returns array of updated rows
21
- // Empty array means no rows matched (will trigger INSERT)
22
- // Array with items means update succeeded
23
- const defaultResponse = response ?? [];
24
- this.addSupabaseRoute('plugin_settings', defaultResponse, { ...options, method: 'PATCH' });
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
- * Mocks GET request for fetching plugin_settings.
28
- * @param settingsRow - The full row object from plugin_settings table, or null if not found.
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
- console.log('Mocking get settings for mockGetSettings', settingsRow, options);
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
- * Mocks POST request for inserting plugin_settings.
41
- * @param response - The response for POST. Defaults to success response with inserted row.
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
- console.log('Mocking insert settings for mockInsertSettings', response, options);
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
- mockGet: () => { },
368
- mockGetList: () => { },
369
- mockGetNew: () => { },
370
- mockCreate: () => { },
371
- mockUpdate: () => { },
372
- mockComplete: () => { },
373
- mockUpdateState: () => { },
374
- mockRemove: () => { },
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
- // TODO move to a function
388
- this.rimoriInfo = {
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
- interceptRoutes() {
423
- this.page.route(`${this.rimoriInfo.backendUrl}/**`, (route) => this.handleRoute(route, this.backendRoutes));
424
- this.page.route(`${this.rimoriInfo.url}/**`, (route) => this.handleRoute(route, this.supabaseRoutes));
425
- }
426
- async setup() {
427
- console.log('Setting up RimoriTestEnvironment');
428
- this.page.on('console', (msg) => {
429
- console.log(`[browser:${msg.type()}]`, msg.text());
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
- // Add default handlers for common routes that plugins typically access
432
- // These can be overridden by explicit mock calls
433
- if (!this.supabaseRoutes[this.createRouteKey('GET', `${this.rimoriInfo.url}/rest/v1/plugin_settings`)]) {
434
- // Default: no settings exist (null) - triggers INSERT flow
435
- // Can be overridden with mockGetSettings() to return existing settings
436
- this.plugin.mockGetSettings(null);
437
- }
438
- if (!this.supabaseRoutes[this.createRouteKey('PATCH', `${this.rimoriInfo.url}/rest/v1/plugin_settings`)]) {
439
- // Default PATCH handler for plugin_settings - returns empty array (no rows updated)
440
- // This triggers INSERT (POST) flow
441
- // Can be overridden with mockSetSettings() to simulate successful update
442
- this.plugin.mockSetSettings([]);
443
- }
444
- if (!this.supabaseRoutes[this.createRouteKey('POST', `${this.rimoriInfo.url}/rest/v1/plugin_settings`)]) {
445
- // Default POST handler for plugin_settings - simulates successful insert
446
- // Can be overridden with mockInsertSettings() to customize response
447
- this.plugin.mockInsertSettings();
448
- }
449
- // Initialize MessageChannelSimulator to simulate parent-iframe communication
450
- // This makes the plugin think it's running in an iframe (not standalone mode)
451
- // Convert RimoriInfo from CommunicationHandler format to MessageChannelSimulator format
452
- this.messageChannelSimulator = new MessageChannelSimulator_1.MessageChannelSimulator({
453
- page: this.page,
454
- pluginId: this.pluginId,
455
- queryParams: {},
456
- rimoriInfo: {
457
- ...this.rimoriInfo,
458
- guild: {
459
- id: this.rimoriInfo.guild.id,
460
- longTermGoalOverride: 'longTermGoalOverride' in this.rimoriInfo.guild ? this.rimoriInfo.guild.longTermGoalOverride : '',
461
- allowUserPluginSettings: this.rimoriInfo.guild.allowUserPluginSettings,
462
- },
463
- installedPlugins: this.rimoriInfo.installedPlugins.map((p) => ({
464
- id: p.id,
465
- title: p.info?.title || '',
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
- // Initialize the simulator - this injects the necessary shims
491
- // to intercept window.parent.postMessage calls and set up MessageChannel communication
492
- await this.messageChannelSimulator.initialize();
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 matchingMock.value === 'string') {
597
- const body = this.formatAsSSE(matchingMock.value);
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(matchingMock.value);
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;
@@ -40,4 +40,5 @@ exports.DEFAULT_USER_INFO = {
40
40
  user_name: 'Test User',
41
41
  target_country: 'SE',
42
42
  target_city: 'Stockholm',
43
+ user_role: 'user',
43
44
  };
@@ -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",
3
+ "version": "0.2.4",
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"