test(ui): stabilize onboarding/modal dismissal in e2e helpers
Refactor test helpers to reliably dismiss onboarding and migration modals during UI flows. Replace ad-hoc Settings-only dismissal with a unified dismissOnboardingModals routine that targets privacy, migration, and settings dialogs via testids, interacts with visible controls (checkbox, continue/skip/save buttons) and waits for dialogs to hide. Increase resilience by repeating checks, adding short settle delays, and tightening timeouts. Also add data-testid attributes to PrivacyModal, SettingsModal and DexieMigrationModal to make the selectors deterministic and reduce flakiness in export/upload tests. No production behavior changes.
This commit is contained in:
parent
dbd202569f
commit
7d9d8de967
4 changed files with 61 additions and 67 deletions
|
|
@ -96,7 +96,7 @@ export function PrivacyModal({ onAccept }: PrivacyModalProps) {
|
|||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
|
||||
<DialogPanel data-testid="privacy-modal" className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
|
||||
<DialogTitle
|
||||
as="h3"
|
||||
className="text-lg font-semibold leading-6 text-foreground"
|
||||
|
|
@ -110,6 +110,7 @@ export function PrivacyModal({ onAccept }: PrivacyModalProps) {
|
|||
<div className="flex items-start gap-3 rounded-lg border border-offbase p-3 bg-offbase/20">
|
||||
<div className="flex h-6 items-center">
|
||||
<input
|
||||
data-testid="privacy-agree-checkbox"
|
||||
id="privacy-agree"
|
||||
type="checkbox"
|
||||
checked={agreed}
|
||||
|
|
@ -129,6 +130,7 @@ export function PrivacyModal({ onAccept }: PrivacyModalProps) {
|
|||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
data-testid="privacy-continue-button"
|
||||
type="button"
|
||||
disabled={!agreed}
|
||||
className="inline-flex justify-center rounded-lg bg-accent px-4 py-2 text-sm
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
|||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel className="w-full max-w-3xl transform rounded-2xl bg-base text-left align-middle shadow-xl transition-all overflow-hidden">
|
||||
<DialogPanel data-testid="settings-modal" className="w-full max-w-3xl transform rounded-2xl bg-base text-left align-middle shadow-xl transition-all overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-offbase">
|
||||
<DialogTitle as="h3" className="text-lg font-semibold leading-6 text-foreground">
|
||||
|
|
@ -660,6 +660,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
|||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="settings-save-button"
|
||||
type="button"
|
||||
className={`${btnPrimary} px-4 py-2`}
|
||||
disabled={!canSubmit}
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ export function DexieMigrationModal() {
|
|||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
|
||||
<DialogPanel data-testid="migration-modal" className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
|
||||
<DialogTitle as="h3" className="text-lg font-semibold leading-6 text-foreground mb-4">
|
||||
{title}
|
||||
</DialogTitle>
|
||||
|
|
@ -222,6 +222,7 @@ export function DexieMigrationModal() {
|
|||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<Button
|
||||
data-testid="migration-skip-button"
|
||||
onClick={handleSkip}
|
||||
disabled={isUploading}
|
||||
className="inline-flex justify-center rounded-lg bg-background px-3 py-1.5 text-sm
|
||||
|
|
|
|||
118
tests/helpers.ts
118
tests/helpers.ts
|
|
@ -82,6 +82,7 @@ export async function uploadAndDisplay(page: Page, fileName: string) {
|
|||
const expectedId = sha256HexOfFile(fixturePath(fileName));
|
||||
const targetLink = page.locator(`a[href$="/pdf/${expectedId}"]`).first();
|
||||
await expect(targetLink).toBeVisible({ timeout: 15000 });
|
||||
await dismissOnboardingModals(page);
|
||||
await targetLink.click();
|
||||
await page.waitForSelector('.react-pdf__Document', { timeout: 15000 });
|
||||
return;
|
||||
|
|
@ -89,14 +90,12 @@ export async function uploadAndDisplay(page: Page, fileName: string) {
|
|||
|
||||
const targetLink = page.getByRole('link', { name: new RegExp(escapeRegExp(fileName), 'i') }).first();
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
await dismissSettingsModalIfVisible(page);
|
||||
await dismissOnboardingModals(page);
|
||||
try {
|
||||
await targetLink.click({ timeout: 10000 });
|
||||
break;
|
||||
} catch (error) {
|
||||
if (attempt === 2) throw error;
|
||||
const message = String(error);
|
||||
if (!message.includes('intercepts pointer events')) throw error;
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
}
|
||||
|
|
@ -110,23 +109,58 @@ export async function uploadAndDisplay(page: Page, fileName: string) {
|
|||
}
|
||||
}
|
||||
|
||||
async function dismissSettingsModalIfVisible(page: Page): Promise<void> {
|
||||
const settingsDialog = page.getByRole('dialog', { name: 'Settings' });
|
||||
const visible = await settingsDialog.isVisible().catch(() => false);
|
||||
if (!visible) return;
|
||||
async function dismissOnboardingModals(page: Page): Promise<void> {
|
||||
const privacyDialog = page.getByTestId('privacy-modal');
|
||||
const migrationDialog = page.getByTestId('migration-modal');
|
||||
const settingsDialog = page.getByTestId('settings-modal');
|
||||
|
||||
const saveBtn = settingsDialog.getByRole('button', { name: 'Save' });
|
||||
const saveVisible = await saveBtn.isVisible().catch(() => false);
|
||||
if (saveVisible) {
|
||||
await expect(saveBtn).toBeEnabled({ timeout: 10000 }).catch(() => { });
|
||||
await saveBtn.click({ force: true }).catch(async () => {
|
||||
await page.keyboard.press('Escape').catch(() => { });
|
||||
});
|
||||
} else {
|
||||
await page.keyboard.press('Escape').catch(() => { });
|
||||
const maxSteps = 12;
|
||||
const settleChecks = 3;
|
||||
let settledWithoutDialog = 0;
|
||||
|
||||
for (let step = 0; step < maxSteps; step += 1) {
|
||||
if (await privacyDialog.isVisible().catch(() => false)) {
|
||||
const privacyAgree = page.getByTestId('privacy-agree-checkbox');
|
||||
if (await privacyAgree.isVisible().catch(() => false)) {
|
||||
if (!(await privacyAgree.isChecked())) {
|
||||
await privacyAgree.check();
|
||||
}
|
||||
}
|
||||
const continueBtn = page.getByTestId('privacy-continue-button');
|
||||
await expect(continueBtn).toBeEnabled({ timeout: 10000 });
|
||||
await continueBtn.click();
|
||||
await privacyDialog.waitFor({ state: 'hidden', timeout: 15000 });
|
||||
await page.waitForTimeout(100);
|
||||
settledWithoutDialog = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await migrationDialog.isVisible().catch(() => false)) {
|
||||
const skipBtn = page.getByTestId('migration-skip-button');
|
||||
await expect(skipBtn).toBeEnabled({ timeout: 10000 });
|
||||
await skipBtn.click();
|
||||
await migrationDialog.waitFor({ state: 'hidden', timeout: 15000 });
|
||||
await page.waitForTimeout(100);
|
||||
settledWithoutDialog = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await settingsDialog.isVisible().catch(() => false)) {
|
||||
const saveBtn = page.getByTestId('settings-save-button');
|
||||
await expect(saveBtn).toBeEnabled({ timeout: 15000 });
|
||||
await saveBtn.click();
|
||||
await settingsDialog.waitFor({ state: 'hidden', timeout: 15000 });
|
||||
await page.waitForTimeout(100);
|
||||
settledWithoutDialog = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
settledWithoutDialog += 1;
|
||||
if (settledWithoutDialog >= settleChecks) {
|
||||
return;
|
||||
}
|
||||
await page.waitForTimeout(150);
|
||||
}
|
||||
|
||||
await settingsDialog.waitFor({ state: 'hidden', timeout: 15000 }).catch(() => { });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -237,58 +271,14 @@ export async function setupTest(page: Page, testInfo?: TestInfo) {
|
|||
.waitForSelector('.fixed.inset-0.bg-base.z-50', { state: 'detached', timeout: 15_000 })
|
||||
.catch(() => { });
|
||||
|
||||
// Privacy modal should come first in onboarding.
|
||||
// Be tolerant if it's already accepted (e.g., reused context).
|
||||
const privacyBtn = page.getByRole('button', { name: /Continue|I Understand/i });
|
||||
try {
|
||||
await expect(privacyBtn).toBeVisible({ timeout: 5000 });
|
||||
const privacyAgree = page.locator('#privacy-agree');
|
||||
if ((await privacyAgree.count()) > 0) {
|
||||
await privacyAgree.check();
|
||||
}
|
||||
await expect(privacyBtn).toBeEnabled({ timeout: 5000 });
|
||||
await privacyBtn.click();
|
||||
// HeadlessUI keeps dialogs in the DOM during leave transitions; "hidden" is enough
|
||||
// (we mainly need to ensure it no longer blocks pointer events).
|
||||
await page.getByRole('dialog', { name: /privacy/i }).waitFor({ state: 'hidden', timeout: 15000 });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Fallback: if the banner still appears, dismiss it before continuing.
|
||||
const cookieAcceptBtn = page.getByRole('button', { name: 'Accept All' });
|
||||
if (await cookieAcceptBtn.isVisible().catch(() => false)) {
|
||||
await cookieAcceptBtn.click();
|
||||
}
|
||||
|
||||
// Settings modal should appear on first visit. In non-auth mode there is no
|
||||
// privacy modal, so open Settings explicitly if onboarding did not auto-open.
|
||||
const settingsDialog = page.getByRole('dialog', { name: 'Settings' });
|
||||
const saveBtn = settingsDialog.getByRole('button', { name: 'Save' });
|
||||
|
||||
// On some local runs, Settings opens automatically but the Save button is not yet
|
||||
// visible during enter transition. Prefer waiting for it before trying to click.
|
||||
await saveBtn.waitFor({ state: 'visible', timeout: 2500 }).catch(async () => {
|
||||
const settingsBtn = page.getByRole('button', { name: 'Settings' });
|
||||
if (await settingsBtn.isVisible().catch(() => false)) {
|
||||
// Force avoids occasional pointer interception by in-flight dialog overlays.
|
||||
await settingsBtn.click({ force: true });
|
||||
}
|
||||
});
|
||||
|
||||
await expect(saveBtn).toBeVisible({ timeout: 10000 });
|
||||
// SettingsModal can briefly disable Save while it mirrors a custom model into the input field.
|
||||
await expect(saveBtn).toBeEnabled({ timeout: 15000 });
|
||||
|
||||
// If running in CI, select the "Custom OpenAI-Like" model and "Deepinfra" provider
|
||||
if (process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Custom OpenAI-Like' }).click();
|
||||
await page.getByText('Deepinfra').click();
|
||||
}
|
||||
|
||||
// Click the "done" button to dismiss the welcome message
|
||||
await saveBtn.click();
|
||||
await settingsDialog.waitFor({ state: 'hidden', timeout: 15000 });
|
||||
// Close first-run dialogs (when present). These do not always appear.
|
||||
await dismissOnboardingModals(page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue