test(ui,tests): stabilize audiobook export tests and harden settings modal interactions
- Add waitForBackendDownloadReady helper to tests/export.spec.ts to wait for backend chapter metadata and enabled full-download UI before attempting combines/downloads. Use it in two export tests to reduce flakiness related to eventual backend storage visibility. - Improve uploadAndDisplay helper in tests/helpers.ts to retry link click when transient "intercepts pointer events" errors occur, ensuring test clicks succeed under flaky UI conditions. Add dismissSettingsModalIfVisible to detect and close the Settings dialog before interacting with page links (click Save or press Escape, wait for dialog to hide). - Update SettingsModal.tsx useEffect dependencies to avoid referencing checkFirstVist in the local state sync effect and add separate effect for checkFirstVist for clearer lifecycle. Why: Reduce end-to-end test flakiness by waiting for backend readiness and making UI interactions more robust; simplify SettingsModal effect dependencies to prevent unnecessary re-runs. Breaking changes: none.
This commit is contained in:
parent
d6ae2baa6f
commit
52e512f17d
3 changed files with 62 additions and 2 deletions
|
|
@ -147,12 +147,15 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
|||
checkFirstVist().catch((err) => {
|
||||
console.error('First visit check failed:', err);
|
||||
});
|
||||
}, [checkFirstVist]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalApiKey(apiKey);
|
||||
setLocalBaseUrl(baseUrl);
|
||||
setLocalTTSProvider(ttsProvider);
|
||||
setModelValue(ttsModel);
|
||||
setLocalTTSInstructions(ttsInstructions);
|
||||
}, [apiKey, baseUrl, ttsProvider, ttsModel, ttsInstructions, checkFirstVist]);
|
||||
}, [apiKey, baseUrl, ttsProvider, ttsModel, ttsInstructions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authEnabled) {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,26 @@ async function expectChaptersBackendState(page: Page, bookId: string) {
|
|||
return json;
|
||||
}
|
||||
|
||||
async function waitForBackendDownloadReady(
|
||||
page: Page,
|
||||
bookId: string,
|
||||
{ minChapters = 1, timeoutMs = 120_000 }: { minChapters?: number; timeoutMs?: number } = {}
|
||||
) {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const json = await expectChaptersBackendState(page, bookId);
|
||||
if (!json?.exists) return false;
|
||||
if (!Array.isArray(json?.chapters)) return false;
|
||||
if (json.chapters.length < minChapters) return false;
|
||||
return json.chapters.every((chapter: { duration?: number }) => Number(chapter.duration ?? 0) > 0);
|
||||
}, { timeout: timeoutMs })
|
||||
.toBe(true);
|
||||
|
||||
const fullDownloadButton = page.getByRole('button', { name: /Full Download/i });
|
||||
await expect(fullDownloadButton).toBeVisible({ timeout: timeoutMs });
|
||||
await expect(fullDownloadButton).toBeEnabled({ timeout: timeoutMs });
|
||||
}
|
||||
|
||||
async function withDownloadedFullAudiobook<T>(
|
||||
page: Page,
|
||||
fn: (args: { filePath: string; suggestedFilename: string }) => Promise<T>,
|
||||
|
|
@ -311,6 +331,9 @@ test('exports a single MP3 audiobook PDF page via chapters menu', async ({ page
|
|||
const chapterActionsButtons = page.getByRole('button', { name: 'Chapter actions' });
|
||||
await expect(chapterActionsButtons.first()).toBeVisible({ timeout: 90_000 });
|
||||
|
||||
// Readiness gate: chapter row visibility can lead backend storage consistency by a small window.
|
||||
await waitForBackendDownloadReady(page, bookId, { minChapters: 1 });
|
||||
|
||||
// Download via frontend button
|
||||
await withDownloadedFullAudiobook(page, async ({ filePath }) => {
|
||||
const durationSeconds = await getAudioDurationSeconds(filePath);
|
||||
|
|
@ -492,6 +515,9 @@ test('resumes audiobook when a chapter is missing and full download succeeds (PD
|
|||
// UI should also stop showing a missing placeholder after resume completes.
|
||||
await expect(page.getByText(/Missing •/)).toHaveCount(0, { timeout: 120_000 });
|
||||
|
||||
// Ensure backend chapter metadata/object visibility is settled before combine/download.
|
||||
await waitForBackendDownloadReady(page, bookId, { minChapters: 2 });
|
||||
|
||||
await withDownloadedFullAudiobook(page, async ({ filePath }) => {
|
||||
const durationSeconds = await getAudioDurationSeconds(filePath);
|
||||
expect(durationSeconds).toBeGreaterThan(18);
|
||||
|
|
|
|||
|
|
@ -87,7 +87,19 @@ export async function uploadAndDisplay(page: Page, fileName: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
await page.getByRole('link', { name: new RegExp(escapeRegExp(fileName), 'i') }).first().click();
|
||||
const targetLink = page.getByRole('link', { name: new RegExp(escapeRegExp(fileName), 'i') }).first();
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
await dismissSettingsModalIfVisible(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);
|
||||
}
|
||||
}
|
||||
|
||||
if (lower.endsWith('.pdf')) {
|
||||
await page.waitForSelector('.react-pdf__Document', { timeout: 10000 });
|
||||
|
|
@ -98,6 +110,25 @@ 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;
|
||||
|
||||
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(() => { });
|
||||
}
|
||||
|
||||
await settingsDialog.waitFor({ state: 'hidden', timeout: 15000 }).catch(() => { });
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the play button to be clickable and click it
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue