ultisuite-client/e2e/helpers/migration-onboarding.ts
R3D347HR4Y 6c7278a3aa
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(onboarding): implement claim and migration pages with OAuth support
- Added OnboardClaimPage and OnboardMigrationPage components for user onboarding.
- Integrated OAuth login flow for Google and Microsoft accounts.
- Implemented error handling and user feedback for claim and migration processes.
- Created MigrationStepList and MigrationOnboardingAlerts components for progress tracking.
- Added MailDomainsSection and MigrationProjectsPanel for admin settings.
- Introduced e2e tests for onboarding migration scenarios.
2026-06-13 12:47:03 +02:00

192 lines
5.9 KiB
TypeScript

import { expect, type Page } from "@playwright/test"
export const E2E_INVITE_TOKEN = "e2e-onboard-invite-token"
export const E2E_MIGRATEE_EMAIL = "alice@acme.com"
export const E2E_PROJECT_ID = "11111111-1111-4111-8111-111111111111"
export type MigrationMockState = {
projectStatus?: "draft" | "active" | "cutover"
authMode?: "oauth" | "google_dwd" | "microsoft_app"
sourceProvider?: "google" | "microsoft"
inviteStatus?: "invited" | "claimed"
hasMigrationCredentials?: boolean
waitingForAdmin?: boolean
jobs?: Array<{
service: string
status: string
stats_json?: Record<string, number>
error?: string
}>
}
const DEFAULT_JOBS = [
{ service: "mail", status: "pending", stats_json: { imported: 0 } },
{ service: "contacts", status: "pending", stats_json: { imported: 0 } },
{ service: "calendar", status: "pending", stats_json: { imported: 0 } },
{ service: "drive", status: "pending", stats_json: { imported: 0 } },
]
function buildProject(state: MigrationMockState) {
return {
id: E2E_PROJECT_ID,
name: "Migration E2E ACME",
source_provider: state.sourceProvider ?? "google",
auth_mode: state.authMode ?? "oauth",
status: state.projectStatus ?? "draft",
delta_mode: state.projectStatus === "cutover",
created_at: "2026-01-15T10:00:00Z",
}
}
function buildInvite(state: MigrationMockState) {
return {
id: "22222222-2222-4222-8222-222222222222",
project_id: E2E_PROJECT_ID,
email: E2E_MIGRATEE_EMAIL,
status: state.inviteStatus ?? "invited",
}
}
export function buildOnboardingHints(state: MigrationMockState) {
const projectStatus = state.projectStatus ?? "draft"
const waiting =
state.waitingForAdmin ??
(projectStatus !== "active" && projectStatus !== "cutover")
const authMode = state.authMode ?? "oauth"
const sourceProvider = state.sourceProvider ?? "google"
return {
needs_user_oauth: authMode !== "google_dwd" && authMode !== "microsoft_app",
oauth_provider: sourceProvider,
waiting_for_admin: waiting,
waiting_reason: waiting ? "project_not_activated" : undefined,
has_migration_credentials: state.hasMigrationCredentials ?? false,
needs_microsoft_admin_consent: sourceProvider === "microsoft",
}
}
export async function mockAuthSession(
page: Page,
opts: { authenticated: boolean; email?: string; name?: string } = {
authenticated: true,
}
) {
await page.route("**/api/auth/session", async (route) => {
if (!opts.authenticated) {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ authenticated: false }),
})
return
}
const email = opts.email ?? E2E_MIGRATEE_EMAIL
const name = opts.name ?? "Alice Test"
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
authenticated: true,
accessToken: "e2e-access-token",
refreshToken: "e2e-refresh-token",
expiresAt: Date.now() + 3_600_000,
user: {
sub: "e2e-user-sub",
email,
name,
firstName: name.split(/\s+/)[0] ?? name,
},
}),
})
})
}
export async function installMigrationApiMocks(
page: Page,
state: MigrationMockState = {}
) {
let inviteClaimed = (state.inviteStatus ?? "invited") === "claimed"
const snapshot = () => {
const inviteStatus = inviteClaimed ? "claimed" : "invited"
const merged = { ...state, inviteStatus: inviteStatus as "invited" | "claimed" }
return {
project: buildProject(merged),
invite: buildInvite(merged),
onboarding: buildOnboardingHints(merged),
jobs: state.jobs ?? DEFAULT_JOBS,
}
}
await page.route("**/api/v1/migration/invite**", async (route) => {
const body = snapshot()
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
invite: body.invite,
project: body.project,
onboarding: body.onboarding,
}),
})
})
await page.route("**/api/v1/migration/status**", async (route) => {
const body = snapshot()
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(body),
})
})
await page.route("**/api/v1/migration/claim**", async (route) => {
if (route.request().method() !== "POST") {
await route.continue()
return
}
inviteClaimed = true
const body = snapshot()
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(body),
})
})
await page.route("**/api/v1/migration/oauth/start**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
auth_url: `/onboard/migration?token=${E2E_INVITE_TOKEN}&oauth=success`,
}),
})
})
}
export async function gotoClaimPage(page: Page, token = E2E_INVITE_TOKEN) {
await page.goto(`/onboard/claim?token=${encodeURIComponent(token)}`)
await page.waitForResponse((res) => res.url().includes("/migration/invite"))
}
export async function gotoMigrationPage(page: Page, token = E2E_INVITE_TOKEN) {
await page.goto(`/onboard/migration?token=${encodeURIComponent(token)}`)
}
export async function expectClaimAuthenticatedStep(page: Page) {
await expect(page.getByRole("heading", { name: "Revendiquer votre compte" })).toBeVisible()
await expect(page.getByRole("button", { name: "Revendiquer mon compte" })).toBeVisible()
await expect(
page.getByText(/L'email de connexion doit correspondre exactement/)
).toBeVisible()
}
export async function submitClaim(page: Page) {
const claimResponse = page.waitForResponse(
(res) => res.url().includes("/migration/claim") && res.request().method() === "POST"
)
await page.getByRole("button", { name: "Revendiquer mon compte" }).click()
await claimResponse
await page.waitForURL(new RegExp(`/onboard/migration\\?token=${E2E_INVITE_TOKEN}`))
}