diff --git a/src/app/(app)/pdf/[id]/page.tsx b/src/app/(app)/pdf/[id]/page.tsx
index 631af46..30a6a6f 100644
--- a/src/app/(app)/pdf/[id]/page.tsx
+++ b/src/app/(app)/pdf/[id]/page.tsx
@@ -443,7 +443,7 @@ export default function PDFViewerPage() {
) : null}
{isLoading || !isParseReady || !isPdfViewerReady ? (
-
+
{renderPdfStatusLoader()}
) : null}
diff --git a/tests/accessibility.spec.ts b/tests/accessibility.spec.ts
index f3e4504..21337f2 100644
--- a/tests/accessibility.spec.ts
+++ b/tests/accessibility.spec.ts
@@ -53,6 +53,7 @@ test.describe('Accessibility smoke', () => {
});
test('TTS controls expose aria labels and are keyboard focusable', async ({ page }) => {
+ test.setTimeout(120_000);
await uploadAndDisplay(page, 'sample.pdf');
// TTS bar present
diff --git a/tests/export.spec.ts b/tests/export.spec.ts
index 00e6153..ce64437 100644
--- a/tests/export.spec.ts
+++ b/tests/export.spec.ts
@@ -271,7 +271,7 @@ async function resetAudiobookIfPresent(page: Page, bookId?: string) {
}
test('exports full MP3 audiobook for PDF using mocked 10s TTS sample', async ({ page }, testInfo) => {
- test.setTimeout(60_000);
+ test.setTimeout(120_000);
// Ensure TTS is mocked and app is ready
await setupTest(page, testInfo);
@@ -382,7 +382,7 @@ test('exports partial MP3 audiobook for EPUB using mocked 10s TTS sample', async
});
test('exports a single MP3 audiobook PDF page via chapters menu', async ({ page }, testInfo) => {
- test.setTimeout(60_000);
+ test.setTimeout(120_000);
await setupTest(page, testInfo);
await uploadAndDisplay(page, 'sample.pdf');
@@ -413,6 +413,7 @@ test('exports a single MP3 audiobook PDF page via chapters menu', async ({ page
});
test('resets all MP3 audiobook PDF pages', async ({ page }, testInfo) => {
+ test.setTimeout(120_000);
await setupTest(page, testInfo);
await uploadAndDisplay(page, 'sample.pdf');
@@ -451,7 +452,7 @@ test('resets all MP3 audiobook PDF pages', async ({ page }, testInfo) => {
});
test('regenerates a single MP3 audiobook PDF page and exports full audiobook', async ({ page }, testInfo) => {
- test.setTimeout(60_000);
+ test.setTimeout(120_000);
await setupTest(page, testInfo);
await uploadAndDisplay(page, 'sample.pdf');
@@ -525,7 +526,7 @@ test('regenerates a single MP3 audiobook PDF page and exports full audiobook', a
});
test('resumes audiobook when a chapter is missing and full download succeeds (PDF)', async ({ page }, testInfo) => {
- test.setTimeout(60_000);
+ test.setTimeout(120_000);
await setupTest(page, testInfo);
await uploadAndDisplay(page, 'sample.pdf');
diff --git a/tests/helpers.ts b/tests/helpers.ts
index c5b6da5..500b3dc 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -24,17 +24,23 @@ function sha256HexOfFile(filePath: string) {
async function waitForPdfViewerReady(page: Page, timeout = 60000) {
await expect(page).toHaveURL(/\/pdf\/[A-Za-z0-9._%-]+$/, { timeout: Math.min(timeout, 20000) });
+ const loader = page.getByTestId('pdf-status-loader').first();
+ const parseFailedBanner = page.getByText('PDF parsing failed. Retry to continue.').first();
await expect
.poll(async () => {
+ const parseFailed = await parseFailedBanner.isVisible().catch(() => false);
+ if (parseFailed) return 'failed';
+
const documentVisible = await page.locator('.react-pdf__Document').first().isVisible().catch(() => false);
- if (documentVisible) return true;
const pageVisible = await page.locator('.react-pdf__Page').first().isVisible().catch(() => false);
- if (pageVisible) return true;
const canvasVisible = await page.locator('.react-pdf__Page canvas').first().isVisible().catch(() => false);
- if (canvasVisible) return true;
- return false;
+
+ const hasRenderedPdf = documentVisible || pageVisible || canvasVisible;
+ const loaderVisible = await loader.isVisible().catch(() => false);
+ if (hasRenderedPdf && !loaderVisible) return 'ready';
+ return 'loading';
}, { timeout })
- .toBe(true);
+ .toBe('ready');
}
/**
@@ -396,7 +402,7 @@ export async function expectViewerForFile(page: Page, fileName: string) {
if (lower.endsWith('.pdf') || lower.endsWith('.docx')) {
// DOCX converts to PDF, so viewer expectations are PDF
await expect(page).toHaveURL(/\/pdf\/[A-Za-z0-9._%-]+$/);
- await expect(page.locator('.react-pdf__Document')).toBeVisible({ timeout: 15000 });
+ await waitForPdfViewerReady(page, 60000);
return;
}
if (lower.endsWith('.epub')) {
diff --git a/tests/landing-routing.spec.ts b/tests/landing-routing.spec.ts
index f9b4bb4..ca9347c 100644
--- a/tests/landing-routing.spec.ts
+++ b/tests/landing-routing.spec.ts
@@ -37,6 +37,7 @@ test.describe('Landing and app routing', () => {
});
test('documents back link returns to /app', async ({ page }, testInfo) => {
+ test.setTimeout(120_000);
await setupTest(page, testInfo);
await uploadAndDisplay(page, 'sample.pdf');
diff --git a/tests/navigation.spec.ts b/tests/navigation.spec.ts
index 173a0c1..b642b20 100644
--- a/tests/navigation.spec.ts
+++ b/tests/navigation.spec.ts
@@ -37,6 +37,7 @@ test.describe('Document link navigation by type', () => {
});
test('navigates to /pdf, /epub, /html and renders correct viewers', async ({ page }) => {
+ test.setTimeout(120_000);
// Upload documents
await uploadFiles(page, 'sample.pdf', 'sample.epub', 'sample.txt');
@@ -65,10 +66,9 @@ test.describe('PDF view modes and Navigator', () => {
});
test('switches Single/Dual/Scroll modes and uses Navigator to change page', async ({ page }) => {
- test.setTimeout(60_000);
+ test.setTimeout(120_000);
// Open PDF viewer
await uploadAndDisplay(page, 'sample.pdf');
- await expect(page.locator('.react-pdf__Document')).toBeVisible({ timeout: 15000 });
// Open document settings (page-level settings)
await page.getByRole('button', { name: 'Open settings' }).click();
diff --git a/tests/play.spec.ts b/tests/play.spec.ts
index e9a6b3a..aeaaf68 100644
--- a/tests/play.spec.ts
+++ b/tests/play.spec.ts
@@ -45,6 +45,7 @@ test.describe('Play/Pause Tests', () => {
test.describe.configure({ mode: 'serial', timeout: 60000 });
test('plays and pauses TTS for a PDF document', async ({ page }) => {
+ test.setTimeout(120_000);
// Play TTS for the PDF document
await playTTSAndWaitForASecond(page, 'sample.pdf');
@@ -61,6 +62,7 @@ test.describe('Play/Pause Tests', () => {
});
test('plays and pauses TTS for an DOCX document', async ({ page }) => {
+ test.setTimeout(120_000);
// Play TTS for the DOCX document
await playTTSAndWaitForASecond(page, 'sample.docx');
@@ -77,6 +79,7 @@ test.describe('Play/Pause Tests', () => {
});
test('switches to a single voice and resumes playing', async ({ page }) => {
+ test.setTimeout(120_000);
// Start playback
await playTTSAndWaitForASecond(page, 'sample.pdf');
@@ -96,6 +99,7 @@ test.describe('Play/Pause Tests', () => {
});
test('keeps selected single voice instead of resetting to first option', async ({ page }) => {
+ test.setTimeout(120_000);
await playTTSAndWaitForASecond(page, 'sample.pdf');
await openVoicesMenu(page);
@@ -122,6 +126,7 @@ test.describe('Play/Pause Tests', () => {
});
if (!process.env.CI) test('selects multiple Kokoro voices and resumes playing', async ({ page }) => {
+ test.setTimeout(120_000);
// Start playback
await playTTSAndWaitForASecond(page, 'sample.pdf');
@@ -142,6 +147,7 @@ test.describe('Play/Pause Tests', () => {
});
test('changing TTS native speed toggles processing and returns to playing', async ({ page }) => {
+ test.setTimeout(120_000);
await playTTSAndWaitForASecond(page, 'sample.pdf');
await changeNativeSpeedAndAssert(page, 1.5);
await expectMediaState(page, 'playing');
diff --git a/tests/upload.spec.ts b/tests/upload.spec.ts
index c43c7aa..7c66b73 100644
--- a/tests/upload.spec.ts
+++ b/tests/upload.spec.ts
@@ -88,10 +88,10 @@ test.describe('Document Upload Tests', () => {
});
test('displays a PDF document', async ({ page }) => {
+ test.setTimeout(120_000);
await uploadAndDisplay(page, 'sample.pdf');
await expectViewerForFile(page, 'sample.pdf');
// Additional content checks specific to the sample PDF
- await expect(page.locator('.react-pdf__Page')).toBeVisible();
await expect(page.getByRole('heading', { level: 1, name: 'sample.pdf' })).toBeVisible();
await expect(page.getByRole('button', { name: /1\s*\/\s*2/ })).toBeVisible();
});
@@ -105,10 +105,10 @@ test.describe('Document Upload Tests', () => {
});
test('displays a DOCX document as PDF after conversion', async ({ page }) => {
+ test.setTimeout(120_000);
await uploadAndDisplay(page, 'sample.docx');
await expectViewerForFile(page, 'sample.docx'); // DOCX converts to PDF
// Keep specific content checks
- await expect(page.locator('.react-pdf__Page')).toBeVisible();
await expect(page.getByText('Demonstration of DOCX')).toBeVisible();
});
@@ -119,6 +119,7 @@ test.describe('Document Upload Tests', () => {
});
test('uploads PDF/EPUB/TXT and opens correct viewer for each', async ({ page }) => {
+ test.setTimeout(120_000);
// Upload multiple files
await uploadFiles(page, 'sample.pdf', 'sample.epub', 'sample.txt');