mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
## 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
185 lines
7.4 KiB
TypeScript
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 });
|
|
});
|
|
});
|