@rimori/playwright-testing 0.3.6 → 0.3.7-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import { Browser, Page } from '@playwright/test';
2
+ import { Onboarding } from '../helpers/e2e/onboarding';
3
+ interface RimoriE2ETestEnvironmentOptions {
4
+ browser: Browser;
5
+ pluginId: string;
6
+ }
7
+ interface Exercise {
8
+ title: string;
9
+ description: string;
10
+ pluginId: string;
11
+ actionKey: string;
12
+ parameters?: Record<string, unknown>;
13
+ }
14
+ interface SetupOptions {
15
+ onboarding?: Onboarding;
16
+ exercises?: Array<Exercise>;
17
+ studyPlan?: {
18
+ complete: boolean;
19
+ };
20
+ }
21
+ export declare class RimoriE2ETestEnvironment {
22
+ private browser;
23
+ private pluginId;
24
+ private persistentUserContext;
25
+ private tempUserContext;
26
+ private testUserEmail;
27
+ private existingUserEmail;
28
+ private authToken;
29
+ constructor(options: RimoriE2ETestEnvironmentOptions);
30
+ setup({ onboarding, exercises, studyPlan }?: SetupOptions): Promise<void>;
31
+ getTempUserPage(): Promise<Page>;
32
+ getPersistUserPage(): Promise<Page>;
33
+ getTempUserEmail(): string;
34
+ getPersistUserEmail(): string;
35
+ private createTestUserViaApi;
36
+ private deleteTestUserViaApi;
37
+ private setupConsoleLogging;
38
+ private setSessionFromMagicLink;
39
+ private completeOnboarding;
40
+ private completeExerciseSetup;
41
+ private completeStudyPlanCreation;
42
+ }
43
+ export {};
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RimoriE2ETestEnvironment = void 0;
4
+ const dotenv_1 = require("dotenv");
5
+ const study_plan_setup_1 = require("../helpers/e2e/study-plan-setup");
6
+ const onboarding_1 = require("../helpers/e2e/onboarding");
7
+ (0, dotenv_1.config)();
8
+ const RIMORI_URL = 'https://dev-app.rimori.se';
9
+ const BACKEND_URL = 'http://localhost:2800';
10
+ class RimoriE2ETestEnvironment {
11
+ constructor(options) {
12
+ this.persistentUserContext = null;
13
+ this.tempUserContext = null;
14
+ this.testUserEmail = null;
15
+ this.existingUserEmail = null;
16
+ this.authToken = null;
17
+ this.browser = options.browser;
18
+ this.pluginId = options.pluginId;
19
+ this.authToken = process.env.RIMORI_TOKEN ?? '';
20
+ if (!this.authToken) {
21
+ throw new Error('RIMORI_TOKEN is not set as an environment variable.');
22
+ }
23
+ }
24
+ async setup({ onboarding, exercises, studyPlan } = {}) {
25
+ const onboardingData = {
26
+ learning_reason: onboarding?.learning_reason ?? 'work',
27
+ target_country: onboarding?.target_country ?? 'SE',
28
+ target_city: onboarding?.target_city ?? 'Malmö',
29
+ interests: onboarding?.interests ?? 'Travel, cooking, and music',
30
+ };
31
+ // Step 1: Create both test users (temp + persist) via API
32
+ const { temp, persist } = await this.createTestUserViaApi();
33
+ this.testUserEmail = temp.email;
34
+ this.existingUserEmail = persist.email;
35
+ console.log(`[E2E] Test user (temp): ${temp.email}`);
36
+ console.log(`[E2E] Existing user (persist): ${persist.email}`);
37
+ this.tempUserContext = await this.browser.newContext({ baseURL: RIMORI_URL });
38
+ this.persistentUserContext = await this.browser.newContext({ baseURL: RIMORI_URL });
39
+ await this.setupConsoleLogging(this.tempUserContext, 'temp');
40
+ await this.setupConsoleLogging(this.persistentUserContext, 'persist');
41
+ console.log(`[E2E] Preparing existing user context`);
42
+ // Step 2: Set up existing user browser context with session via magic link
43
+ await this.setSessionFromMagicLink(this.persistentUserContext, persist.magicLink);
44
+ // Step 3: Run onboarding for existing user
45
+ await this.completeOnboarding(this.persistentUserContext, onboardingData);
46
+ console.log(`[E2E] Setting up test user context`);
47
+ // Step 4: Set up test user browser context with session via magic link
48
+ await this.setSessionFromMagicLink(this.tempUserContext, temp.magicLink);
49
+ // Delete test user when test user context is closed
50
+ this.tempUserContext.on('close', async () => {
51
+ await this.deleteTestUserViaApi(temp.email);
52
+ console.log(`[E2E] Deleted test user: ${temp.email}`);
53
+ });
54
+ // Step 5: Run onboarding for test user with e2e plugin flag
55
+ await this.completeOnboarding(this.tempUserContext, onboardingData, this.pluginId);
56
+ // Step 6: Add exercises if specified
57
+ if (exercises && exercises?.length > 0) {
58
+ console.log(`[E2E] Setting up exercises`);
59
+ await this.completeExerciseSetup(this.tempUserContext, exercises);
60
+ }
61
+ // Step 7: Complete study plan creation if specified
62
+ if (studyPlan?.complete) {
63
+ console.log(`[E2E] Setting up study plan`);
64
+ await this.completeStudyPlanCreation(this.tempUserContext);
65
+ }
66
+ console.log(`[E2E] Setup completed`);
67
+ }
68
+ async getTempUserPage() {
69
+ if (!this.tempUserContext) {
70
+ throw new Error('Test user context not initialized. Call setup() first.');
71
+ }
72
+ return this.tempUserContext.newPage();
73
+ }
74
+ async getPersistUserPage() {
75
+ if (!this.persistentUserContext) {
76
+ throw new Error('Existing user context not initialized. Call setup() first.');
77
+ }
78
+ return this.persistentUserContext.newPage();
79
+ }
80
+ getTempUserEmail() {
81
+ if (!this.testUserEmail) {
82
+ throw new Error('Test user not created. Call setup() first.');
83
+ }
84
+ return this.testUserEmail;
85
+ }
86
+ getPersistUserEmail() {
87
+ if (!this.existingUserEmail) {
88
+ throw new Error('Existing user not created. Call setup() first.');
89
+ }
90
+ return this.existingUserEmail;
91
+ }
92
+ async createTestUserViaApi() {
93
+ const response = await fetch(`${BACKEND_URL}/testing/test-user`, {
94
+ method: 'POST',
95
+ headers: {
96
+ 'Content-Type': 'application/json',
97
+ Authorization: `Bearer ${this.authToken}`,
98
+ },
99
+ body: JSON.stringify({
100
+ plugin_id: this.pluginId,
101
+ }),
102
+ });
103
+ if (!response.ok) {
104
+ const errorText = await response.text();
105
+ throw new Error(`Failed to create test user: ${response.status} ${errorText}`);
106
+ }
107
+ return response.json();
108
+ }
109
+ async deleteTestUserViaApi(email) {
110
+ const response = await fetch(`${BACKEND_URL}/testing/test-user`, {
111
+ method: 'DELETE',
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ Authorization: `Bearer ${this.authToken}`,
115
+ },
116
+ body: JSON.stringify({
117
+ plugin_id: this.pluginId,
118
+ email,
119
+ }),
120
+ });
121
+ if (!response.ok) {
122
+ const errorText = await response.text();
123
+ throw new Error(`Failed to delete test user: ${response.status} ${errorText}`);
124
+ }
125
+ }
126
+ async setupConsoleLogging(context, user) {
127
+ console.log(`[E2E] Setting up console logging for ${user}`);
128
+ context.on('console', (msg) => {
129
+ const logLevel = msg.type();
130
+ const logMessage = msg.text();
131
+ if (logLevel === 'debug')
132
+ return;
133
+ if (logMessage.includes('Download the React DevTools'))
134
+ return;
135
+ if (logMessage.includes('languageChanged en'))
136
+ return;
137
+ if (logMessage.includes('i18next: initialized {debug: true'))
138
+ return;
139
+ console.log(`[browser:${logLevel}] [${user}]`, logMessage);
140
+ });
141
+ }
142
+ async setSessionFromMagicLink(context, magicLink) {
143
+ const page = await context.newPage();
144
+ await page.goto(magicLink, { waitUntil: 'networkidle' });
145
+ await page.waitForTimeout(5000);
146
+ const url = page.url();
147
+ if (!url.includes('/dashboard') && !url.includes('/onboarding')) {
148
+ throw new Error(`Failed to set session from magic link: ${url}`);
149
+ }
150
+ await page.close();
151
+ console.log(`[E2E] Authentication completed`);
152
+ }
153
+ async completeOnboarding(context, onboarding, e2ePluginId) {
154
+ console.log(`[E2E] Starting onboarding`);
155
+ const page = await context.newPage();
156
+ await page.goto('/onboarding');
157
+ await page.waitForTimeout(5000);
158
+ const isOnboaded = page.url().includes('/dashboard');
159
+ if (!isOnboaded) {
160
+ console.log(`[E2E] Onboarding user`);
161
+ await (0, onboarding_1.completeOnboarding)(page, onboarding, e2ePluginId);
162
+ console.log(`[E2E] Onboarding completed`);
163
+ }
164
+ else {
165
+ console.log(`[E2E] User already onboarded`);
166
+ }
167
+ await page.close();
168
+ }
169
+ async completeExerciseSetup(context, exercises) {
170
+ const page = await context.newPage();
171
+ for (const exercise of exercises) {
172
+ const encoded = encodeURIComponent(JSON.stringify(exercise));
173
+ await page.goto(`${RIMORI_URL}/dashboard?flag-e2e-exercise=${encoded}`);
174
+ // Wait for the exercise to be created and the flag to be cleared from URL
175
+ await page.waitForURL((url) => !url.searchParams.has('flag-e2e-exercise'), { timeout: 15000 });
176
+ }
177
+ await page.close();
178
+ }
179
+ async completeStudyPlanCreation(context) {
180
+ const page = await context.newPage();
181
+ await (0, study_plan_setup_1.completeStudyPlanGettingStarted)(page);
182
+ await page.close();
183
+ }
184
+ }
185
+ exports.RimoriE2ETestEnvironment = RimoriE2ETestEnvironment;
@@ -1,8 +1,8 @@
1
1
  import { Page, Request } from '@playwright/test';
2
- import { UserInfo } from '@rimori/client/dist/controller/SettingsController';
3
- import { MainPanelAction, Plugin } from '@rimori/client/dist/fromRimori/PluginTypes';
2
+ import { UserInfo } from '@rimori/client';
3
+ import { MainPanelAction, Plugin } from '@rimori/client';
4
4
  import { PluginSettings } from './SettingsStateManager';
5
- import { EventPayload } from '@rimori/client/dist/fromRimori/EventBus';
5
+ import { EventPayload } from '@rimori/client';
6
6
  import { LanguageLevel } from '@rimori/client';
7
7
  interface RimoriTestEnvironmentOptions {
8
8
  page: Page;
@@ -23,8 +23,8 @@ exports.DEFAULT_USER_INFO = {
23
23
  skill_level_speaking: 'Pre-A1',
24
24
  skill_level_listening: 'Pre-A1',
25
25
  skill_level_understanding: 'Pre-A1',
26
- goal_longterm: 'Learn Swedish',
27
- goal_weekly: 'Practice daily',
26
+ learning_reason: 'growth',
27
+ personal_interests: 'Travel and cooking',
28
28
  study_buddy: {
29
29
  id: 'buddy-1',
30
30
  name: 'Test Buddy',
@@ -33,9 +33,7 @@ exports.DEFAULT_USER_INFO = {
33
33
  voiceId: 'alloy',
34
34
  aiPersonality: 'friendly',
35
35
  },
36
- story_genre: 'fiction',
37
36
  study_duration: 30,
38
- motivation_type: 'career',
39
37
  onboarding_completed: true,
40
38
  context_menu_on_select: true,
41
39
  user_name: 'Test User',
@@ -0,0 +1,21 @@
1
+ import { Page } from "@playwright/test";
2
+ export interface Onboarding {
3
+ learning_reason?: keyof typeof learningReasonMap;
4
+ target_country?: string;
5
+ target_city?: string;
6
+ interests?: string;
7
+ }
8
+ declare const learningReasonMap: {
9
+ work: string;
10
+ relationship: string;
11
+ friends_family: string;
12
+ education: string;
13
+ moving: string;
14
+ culture_travel: string;
15
+ self_improvement: string;
16
+ citizenship: string;
17
+ other: string;
18
+ speaking: string;
19
+ };
20
+ export declare function completeOnboarding(page: Page, onboarding: Required<Onboarding>, e2ePluginId?: string): Promise<void>;
21
+ export {};
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.completeOnboarding = completeOnboarding;
4
+ const test_1 = require("@playwright/test");
5
+ const learningReasonMap = {
6
+ work: 'For my job',
7
+ relationship: 'For my relationship',
8
+ friends_family: 'For friends and family',
9
+ education: 'For education',
10
+ moving: 'Moving or living abroad',
11
+ culture_travel: 'For culture and travel',
12
+ self_improvement: 'Self-improvement',
13
+ citizenship: 'For citizenship or residency',
14
+ other: 'Other reason',
15
+ speaking: 'What is your main motivation to learn Swedish?',
16
+ };
17
+ async function completeOnboarding(page, onboarding, e2ePluginId) {
18
+ console.log(`[E2E] Onboarding user`);
19
+ console.log(`[E2E] E2E plugin ID: ${e2ePluginId}`);
20
+ console.log(`[E2E] Onboarding: ${JSON.stringify(onboarding)}`);
21
+ const url = e2ePluginId ? `/onboarding?flag-e2e-plugin-id=${e2ePluginId}` : `/onboarding`;
22
+ await page.goto(url, { waitUntil: 'networkidle' });
23
+ page.setDefaultTimeout(60000);
24
+ page.setDefaultNavigationTimeout(60000);
25
+ // Ensure we're on onboarding page
26
+ await (0, test_1.expect)(page).toHaveURL(/\/onboarding/);
27
+ // Step 1: Learning Reason (radio select, auto-advances after selection)
28
+ const learningReasonOption = page.getByText(learningReasonMap[onboarding.learning_reason]);
29
+ await (0, test_1.expect)(learningReasonOption).toBeVisible({ timeout: 10000 });
30
+ await learningReasonOption.click();
31
+ // Step 2: Interests (textarea with continue button)
32
+ await page.waitForTimeout(1000);
33
+ const interestsTextarea = page.locator('textarea');
34
+ await (0, test_1.expect)(interestsTextarea).toBeVisible({ timeout: 10000 });
35
+ await interestsTextarea.click();
36
+ await interestsTextarea.fill(onboarding.interests);
37
+ const interestsContinue = page.getByRole('button', { name: /continue/i });
38
+ await (0, test_1.expect)(interestsContinue).toBeEnabled({ timeout: 10000 });
39
+ await interestsContinue.click();
40
+ // Step 3: Location
41
+ const countrySelect = page.getByLabel('Country');
42
+ await (0, test_1.expect)(countrySelect).toBeVisible({ timeout: 10000 });
43
+ await countrySelect.selectOption(onboarding.target_country);
44
+ await page.getByLabel('City (optional)').selectOption(onboarding.target_city);
45
+ await page.getByRole('button', { name: 'Continue' }).click();
46
+ // Step 4: Study Buddy (card select, auto-advances after selection)
47
+ // Wait for study buddy step and click the first buddy card
48
+ await page.waitForTimeout(1000);
49
+ const buddyCard = page
50
+ .locator('button')
51
+ .filter({ has: page.locator('img') })
52
+ .first();
53
+ await (0, test_1.expect)(buddyCard).toBeVisible({ timeout: 10000 });
54
+ await buddyCard.click();
55
+ // Step 5: Wait for setup completion
56
+ await (0, test_1.expect)(page.getByRole('heading', { name: 'Almost there!' })).toBeVisible({ timeout: 10000 });
57
+ await page.waitForURL('**/dashboard', { timeout: 120000 });
58
+ await (0, test_1.expect)(page.getByRole('heading', { name: "Today's Mission" })).toBeVisible({ timeout: 30000 });
59
+ await (0, test_1.expect)(page.getByRole('button', { name: 'Grammar', exact: true })).toBeVisible({ timeout: 60000 });
60
+ await (0, test_1.expect)(page.getByRole('heading', { name: 'Getting Started: Create your first study plan' })).toBeVisible({
61
+ timeout: 60000,
62
+ });
63
+ await (0, test_1.expect)(page.getByText('Train your first flashcard deck', { exact: true })).toBeVisible({ timeout: 200000 });
64
+ await (0, test_1.expect)(page.locator('iframe').contentFrame().getByText('Getting Started', { exact: true })).toBeVisible({
65
+ timeout: 250000,
66
+ });
67
+ }
@@ -0,0 +1,11 @@
1
+ import { Page } from '@playwright/test';
2
+ /**
3
+ * Navigates through the study plan getting-started flow on the dashboard.
4
+ * This clicks through the real UI: milestone planning (Submit Topics) and
5
+ * exercise creation (Save Exercises).
6
+ *
7
+ * Expects the page to already be on the dashboard with a "Getting Started" exercise visible.
8
+ *
9
+ * @param page - Playwright page instance, should be on the dashboard
10
+ */
11
+ export declare function completeStudyPlanGettingStarted(page: Page): Promise<void>;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.completeStudyPlanGettingStarted = completeStudyPlanGettingStarted;
4
+ const test_1 = require("@playwright/test");
5
+ /**
6
+ * Navigates through the study plan getting-started flow on the dashboard.
7
+ * This clicks through the real UI: milestone planning (Submit Topics) and
8
+ * exercise creation (Save Exercises).
9
+ *
10
+ * Expects the page to already be on the dashboard with a "Getting Started" exercise visible.
11
+ *
12
+ * @param page - Playwright page instance, should be on the dashboard
13
+ */
14
+ async function completeStudyPlanGettingStarted(page) {
15
+ page.goto('/dashboard');
16
+ await page.waitForTimeout(2000);
17
+ // Step 1: Find and click the Getting Started exercise card
18
+ const card = page.getByText('Getting Started: Create your first study plan', { exact: false });
19
+ await card.waitFor({ timeout: 10000, state: 'visible' }).catch(() => {
20
+ /* not visible within 10s, continue to early return below */
21
+ });
22
+ if (!(await card.isVisible())) {
23
+ page.close();
24
+ console.warn(`[E2E] Getting Started card not found, skipping study plan setup`);
25
+ return;
26
+ }
27
+ const gettingStartedCard = page.getByText('Start Exercise', { exact: false }).first();
28
+ await (0, test_1.expect)(gettingStartedCard).toBeVisible({ timeout: 30000 });
29
+ await gettingStartedCard.click();
30
+ // Wait for the study plan plugin iframe to load
31
+ const iframe = page.locator('iframe').first();
32
+ await (0, test_1.expect)(iframe).toBeVisible({ timeout: 30000 });
33
+ const frame = iframe.contentFrame();
34
+ // Step 2: Milestone Planning Stage
35
+ // Wait for the 3 milestone cards to appear (AI generates them)
36
+ await (0, test_1.expect)(frame.getByText('Week 1', { exact: false })).toBeVisible({ timeout: 180000 });
37
+ await (0, test_1.expect)(frame.getByText('Week 2', { exact: false })).toBeVisible({ timeout: 10000 });
38
+ await (0, test_1.expect)(frame.getByText('Week 3', { exact: false })).toBeVisible({ timeout: 10000 });
39
+ // Wait for "Submit Topics" button to be enabled and click it
40
+ const submitTopicsButton = frame.getByRole('button', { name: /submit topics/i });
41
+ await (0, test_1.expect)(submitTopicsButton).toBeEnabled({ timeout: 180000 });
42
+ await submitTopicsButton.click();
43
+ // Step 3: Exercise Creation Stage
44
+ // Wait for "Save Exercises" button to appear (AI generates all exercises)
45
+ const saveExercisesButton = frame.getByRole('button', { name: /save exercises/i });
46
+ await (0, test_1.expect)(saveExercisesButton).toBeVisible({ timeout: 300000 });
47
+ await (0, test_1.expect)(saveExercisesButton).toBeEnabled({ timeout: 30000 });
48
+ await saveExercisesButton.click();
49
+ // Wait for save to complete (button should disappear or page navigates)
50
+ await (0, test_1.expect)(saveExercisesButton).toBeHidden({ timeout: 30000 });
51
+ // Step 4: Verify completion - should be back on dashboard
52
+ // The "Getting Started" card should be gone and exercises should appear
53
+ await (0, test_1.expect)(page.getByText("Today's Mission", { exact: false })).toBeVisible({ timeout: 30000 });
54
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './core/RimoriTestEnvironment';
2
+ export * from './core/RimoriE2ETestEnvironment';
package/dist/index.js CHANGED
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./core/RimoriTestEnvironment"), exports);
18
+ __exportStar(require("./core/RimoriE2ETestEnvironment"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/playwright-testing",
3
- "version": "0.3.6",
3
+ "version": "0.3.7-next.1",
4
4
  "description": "Playwright testing utilities for Rimori plugins and workers",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -21,13 +21,16 @@
21
21
  "test:ui": "playwright test --ui",
22
22
  "test:headed:debug": "playwright test --headed --debug"
23
23
  },
24
+ "dependencies": {
25
+ "dotenv": "^16.4.5"
26
+ },
24
27
  "peerDependencies": {
25
28
  "@playwright/test": "^1.40.0",
26
- "@rimori/client": "^2.5.12"
29
+ "@rimori/client": "2.5.16-next.0"
27
30
  },
28
31
  "devDependencies": {
29
32
  "@playwright/test": "^1.40.0",
30
- "@rimori/client": "^2.5.12",
33
+ "@rimori/client": "2.5.16-next.0",
31
34
  "@types/node": "^20.12.7",
32
35
  "rimraf": "^5.0.7",
33
36
  "typescript": "^5.7.2"