Files
Stirling-PDF/frontend/src/core/tests/convert/ConvertE2E.spec.ts
T
ConnorYohandGitHub 0e29640766 fix: get all Playwright E2E tests loading and expand CI to run full suite (#6009)
## Fix Playwright E2E tests and expand CI to run full suite

### Problem

The full Playwright suite was broken in two ways:

1. **`ConvertE2E.spec.ts` crashed at import time** —
`conversionEndpointDiscovery.ts` imported a React hook at the top level,
which pulled in the entire component tree. That chain eventually
required `material-symbols-icons.json` (a generated file that didn't
exist), crashing module resolution before any tests ran.

2. **CI only ran cert validation tests** — both `build.yml` and
`nightly.yml` hardcoded `src/core/tests/certValidation` as the test
path, silently ignoring everything else.

### Changes

**`ConvertE2E.spec.ts` — complete rewrite**
The old tests were useless in practice: all 9 dynamic conversion tests
were permanently skipped unless a real Spring Boot backend was running
(they called a live `/api/v1/config/endpoints-enabled` endpoint at
module load time). Replaced with 4 focused tests that use `page.route()`
mocking — no backend required, same pattern as
`CertificateValidationE2E`.

New tests cover:
- Convert button absent before a format pair is selected
- Successful PDF→PNG conversion shows a download button (mocked API
response)
- API error surfaces as an error notification
- Convert button appears and is enabled after selecting valid formats

**`conversionEndpointDiscovery.ts` — deleted**
Only existed to support the old tests. The `useConversionEndpoints`
React hook it exported was never imported anywhere else.

**`ReviewToolStep.tsx`**
Added `data-testid="download-result-button"` to the download button —
required for the happy-path test assertion.

**CI workflows (`build.yml`, `nightly.yml`)**
- Added a `Generate icons` step before Playwright runs (`node
scripts/generate-icons.js`) — the icon JSON is generated by `npm run
dev` locally but skipped by `npm ci` in CI
- Removed the `src/core/tests/certValidation` path filter so the full
suite runs
2026-03-30 11:27:55 +01:00

185 lines
7.4 KiB
TypeScript

/**
* End-to-End Tests for Convert Tool
*
* All backend API calls are mocked via page.route() — no real backend required.
* The Vite dev server must be running (handled by playwright.config.ts webServer).
*/
import { test, expect, type Page } from '@playwright/test';
import path from 'path';
const FIXTURES_DIR = path.join(__dirname, '../test-fixtures');
const SAMPLE_PDF = path.join(FIXTURES_DIR, 'sample.pdf');
// ---------------------------------------------------------------------------
// Endpoint availability map — all conversion endpoints enabled
// ---------------------------------------------------------------------------
const MOCK_ENDPOINTS_AVAILABILITY = Object.fromEntries(
[
'pdf-to-img', 'img-to-pdf', 'pdf-to-word', 'file-to-pdf', 'pdf-to-text',
'pdf-to-html', 'pdf-to-xml', 'pdf-to-csv', 'pdf-to-xlsx', 'pdf-to-pdfa',
'pdf-to-pdfx', 'pdf-to-presentation', 'pdf-to-markdown', 'pdf-to-cbz',
'pdf-to-cbr', 'pdf-to-epub', 'html-to-pdf', 'svg-to-pdf', 'markdown-to-pdf',
'eml-to-pdf', 'cbz-to-pdf', 'cbr-to-pdf',
].map((k) => [k, { enabled: true }])
);
// ---------------------------------------------------------------------------
// Helper: mock all standard app APIs needed to load the main UI
// ---------------------------------------------------------------------------
async function mockAppApis(page: Page) {
// Backend probe — must return UP so Landing shows app in anonymous mode
await page.route('**/api/v1/info/status', (route) =>
route.fulfill({ json: { status: 'UP' } })
);
// App config — enableLogin:false puts the app in anonymous mode
await page.route('**/api/v1/config/app-config', (route) =>
route.fulfill({
json: { enableLogin: false, languages: ['en-GB'], defaultLocale: 'en-GB' },
})
);
// Auth — fallback if anything calls auth/me
await page.route('**/api/v1/auth/me', (route) =>
route.fulfill({
json: { id: 1, username: 'testuser', email: '[email protected]', roles: ['ROLE_USER'] },
})
);
// Endpoint availability — queried by ConvertSettings
await page.route('**/api/v1/config/endpoints-availability', (route) =>
route.fulfill({ json: MOCK_ENDPOINTS_AVAILABILITY })
);
// Single-endpoint check — queried by Convert.tsx for the execute button
await page.route('**/api/v1/config/endpoint-enabled*', (route) =>
route.fulfill({ json: true })
);
// Group-enabled check
await page.route('**/api/v1/config/group-enabled*', (route) =>
route.fulfill({ json: true })
);
// Footer info — non-critical
await page.route('**/api/v1/ui-data/footer-info', (route) =>
route.fulfill({ json: {} })
);
// Proprietary endpoints — silence proxy errors in the Vite dev server
await page.route('**/api/v1/proprietary/**', (route) =>
route.fulfill({ json: {} })
);
}
// ---------------------------------------------------------------------------
// Helper: upload a file through the Files modal
// Uses the HiddenFileInput (data-testid="file-input") which has the correct
// onChange handler. Waits for the modal to auto-close after upload.
// ---------------------------------------------------------------------------
async function uploadFile(page: Page, filePath: string) {
await page.getByTestId('files-button').click();
await page.waitForSelector('.mantine-Modal-overlay', { state: 'visible', timeout: 5000 });
await page.locator('[data-testid="file-input"]').setInputFiles(filePath);
// Modal auto-closes after file is selected
await page.waitForSelector('.mantine-Modal-overlay', { state: 'hidden', timeout: 10000 });
}
// ---------------------------------------------------------------------------
// Helper: navigate to the Convert tool panel
// Tools use data-tour="tool-button-{key}" anchors in the ToolPanel.
// After clicking, the URL changes to /convert and the settings appear.
// ---------------------------------------------------------------------------
async function navigateToConvert(page: Page) {
await page.locator('[data-tour="tool-button-convert"]').click();
await page.waitForSelector('[data-testid="convert-from-dropdown"]', { timeout: 5000 });
}
// ---------------------------------------------------------------------------
// Helper: select the TO format in the convert dropdown
// The FROM format is auto-detected from the uploaded file (e.g. PDF → "Document (PDF)").
// Opening the TO dropdown renders format-option-{value} buttons in a portal.
// ---------------------------------------------------------------------------
async function selectToFormat(page: Page, toValue: string) {
await page.getByTestId('convert-to-dropdown').click();
await page.getByTestId(`format-option-${toValue}`).click();
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe('Convert Tool', () => {
test.beforeEach(async ({ page }) => {
await mockAppApis(page);
await page.goto('/?bypassOnboarding=true');
await page.waitForSelector('[data-testid="files-button"]', { timeout: 10000 });
});
test('convert button is disabled before a TO format is selected', async ({ page }) => {
await uploadFile(page, SAMPLE_PDF);
await navigateToConvert(page);
// FROM is auto-detected as PDF; TO not selected → button visible but disabled
const convertBtn = page.getByTestId('convert-button');
await expect(convertBtn).toBeVisible({ timeout: 3000 });
await expect(convertBtn).toBeDisabled();
});
test('successful PDF to PNG conversion shows download option', async ({ page }) => {
// Minimal valid PNG header (8 bytes signature + padding)
const fakePng = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
...Array(504).fill(0),
]);
await page.route('**/api/v1/convert/pdf/img', (route) =>
route.fulfill({
status: 200,
contentType: 'image/png',
headers: { 'Content-Disposition': 'attachment; filename="sample.png"' },
body: fakePng,
})
);
await uploadFile(page, SAMPLE_PDF);
await navigateToConvert(page);
await selectToFormat(page, 'png');
await page.getByTestId('convert-button').click();
await expect(page.getByTestId('download-result-button')).toBeVisible({ timeout: 10000 });
});
test('conversion API error shows error notification', async ({ page }) => {
await page.route('**/api/v1/convert/pdf/img', (route) =>
route.fulfill({
status: 500,
contentType: 'text/plain',
body: 'Internal server error: conversion failed',
})
);
await uploadFile(page, SAMPLE_PDF);
await navigateToConvert(page);
await selectToFormat(page, 'png');
await page.getByTestId('convert-button').click();
// Mantine Notification renders as role="alert"
await expect(page.getByRole('alert').first()).toBeVisible({ timeout: 5000 });
});
test('convert button becomes enabled after selecting a valid TO format', async ({ page }) => {
await uploadFile(page, SAMPLE_PDF);
await navigateToConvert(page);
// Before selecting TO format — button visible but disabled
const convertBtn = page.getByTestId('convert-button');
await expect(convertBtn).toBeVisible({ timeout: 3000 });
await expect(convertBtn).toBeDisabled();
// After selecting PNG as TO format — button enabled
await selectToFormat(page, 'png');
await expect(convertBtn).toBeEnabled({ timeout: 3000 });
});
});