mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
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
This commit is contained in:
@@ -236,10 +236,12 @@ jobs:
|
|||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: cd frontend && npm ci
|
run: cd frontend && npm ci
|
||||||
|
- name: Generate icons
|
||||||
|
run: cd frontend && node scripts/generate-icons.js
|
||||||
- name: Install Playwright (chromium only)
|
- name: Install Playwright (chromium only)
|
||||||
run: cd frontend && npx playwright install chromium --with-deps
|
run: cd frontend && npx playwright install chromium --with-deps
|
||||||
- name: Run E2E tests (chromium)
|
- name: Run E2E tests (chromium)
|
||||||
run: cd frontend && npx playwright test src/core/tests/certValidation --project=chromium
|
run: cd frontend && npx playwright test --project=chromium
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
|
|||||||
@@ -35,11 +35,14 @@ jobs:
|
|||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: cd frontend && npm ci
|
run: cd frontend && npm ci
|
||||||
|
|
||||||
|
- name: Generate icons
|
||||||
|
run: cd frontend && node scripts/generate-icons.js
|
||||||
|
|
||||||
- name: Install all Playwright browsers
|
- name: Install all Playwright browsers
|
||||||
run: cd frontend && npx playwright install --with-deps
|
run: cd frontend && npx playwright install --with-deps
|
||||||
|
|
||||||
- name: Run E2E tests (all browsers)
|
- name: Run E2E tests (all browsers)
|
||||||
run: cd frontend && npx playwright test src/core/tests/certValidation
|
run: cd frontend && npx playwright test
|
||||||
|
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ function ReviewStepContent<TParams = unknown>({
|
|||||||
)}
|
)}
|
||||||
{operation.downloadUrl && (
|
{operation.downloadUrl && (
|
||||||
<Button
|
<Button
|
||||||
|
data-testid="download-result-button"
|
||||||
leftSection={<DownloadIcon />}
|
leftSection={<DownloadIcon />}
|
||||||
color="blue"
|
color="blue"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -1,491 +1,184 @@
|
|||||||
/**
|
/**
|
||||||
* End-to-End Tests for Convert Tool
|
* End-to-End Tests for Convert Tool
|
||||||
*
|
*
|
||||||
* These tests dynamically discover available conversion endpoints and test them.
|
* All backend API calls are mocked via page.route() — no real backend required.
|
||||||
* Tests are automatically skipped if the backend endpoint is not available.
|
* The Vite dev server must be running (handled by playwright.config.ts webServer).
|
||||||
*
|
|
||||||
* Run with: npm run test:e2e or npx playwright test
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect, Page } from '@playwright/test';
|
import { test, expect, type Page } from '@playwright/test';
|
||||||
import {
|
import path from 'path';
|
||||||
conversionDiscovery,
|
|
||||||
type ConversionEndpoint
|
|
||||||
} from '@app/tests/helpers/conversionEndpointDiscovery';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
// Test configuration
|
const FIXTURES_DIR = path.join(__dirname, '../test-fixtures');
|
||||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
const SAMPLE_PDF = path.join(FIXTURES_DIR, 'sample.pdf');
|
||||||
|
|
||||||
/**
|
// ---------------------------------------------------------------------------
|
||||||
* Resolves test fixture paths dynamically based on current working directory.
|
// Endpoint availability map — all conversion endpoints enabled
|
||||||
* Works from both top-level project directory and frontend subdirectory.
|
// ---------------------------------------------------------------------------
|
||||||
*/
|
const MOCK_ENDPOINTS_AVAILABILITY = Object.fromEntries(
|
||||||
function resolveTestFixturePath(filename: string): string {
|
[
|
||||||
const cwd = process.cwd();
|
'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 }])
|
||||||
|
);
|
||||||
|
|
||||||
// Try frontend/src/tests/test-fixtures/ first (from top-level)
|
// ---------------------------------------------------------------------------
|
||||||
const topLevelPath = path.join(cwd, 'frontend', 'src', 'tests', 'test-fixtures', filename);
|
// Helper: mock all standard app APIs needed to load the main UI
|
||||||
if (fs.existsSync(topLevelPath)) {
|
// ---------------------------------------------------------------------------
|
||||||
return topLevelPath;
|
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' } })
|
||||||
|
);
|
||||||
|
|
||||||
// Try src/tests/test-fixtures/ (from frontend directory)
|
// App config — enableLogin:false puts the app in anonymous mode
|
||||||
const frontendPath = path.join(cwd, 'src', 'tests', 'test-fixtures', filename);
|
await page.route('**/api/v1/config/app-config', (route) =>
|
||||||
if (fs.existsSync(frontendPath)) {
|
route.fulfill({
|
||||||
return frontendPath;
|
json: { enableLogin: false, languages: ['en-GB'], defaultLocale: 'en-GB' },
|
||||||
}
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Try relative path from current test file location
|
// Auth — fallback if anything calls auth/me
|
||||||
const relativePath = path.join(__dirname, '..', 'test-fixtures', filename);
|
await page.route('**/api/v1/auth/me', (route) =>
|
||||||
if (fs.existsSync(relativePath)) {
|
route.fulfill({
|
||||||
return relativePath;
|
json: { id: 1, username: 'testuser', email: '[email protected]', roles: ['ROLE_USER'] },
|
||||||
}
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Fallback to the original path format (should work from top-level)
|
// Endpoint availability — queried by ConvertSettings
|
||||||
return path.join('.', 'frontend', 'src', 'tests', 'test-fixtures', filename);
|
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: {} })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test file paths (dynamically resolved based on current working directory)
|
// ---------------------------------------------------------------------------
|
||||||
const TEST_FILES = {
|
// Helper: upload a file through the Files modal
|
||||||
pdf: resolveTestFixturePath('sample.pdf'),
|
// Uses the HiddenFileInput (data-testid="file-input") which has the correct
|
||||||
docx: resolveTestFixturePath('sample.docx'),
|
// onChange handler. Waits for the modal to auto-close after upload.
|
||||||
doc: resolveTestFixturePath('sample.doc'),
|
// ---------------------------------------------------------------------------
|
||||||
pptx: resolveTestFixturePath('sample.pptx'),
|
async function uploadFile(page: Page, filePath: string) {
|
||||||
ppt: resolveTestFixturePath('sample.ppt'),
|
await page.getByTestId('files-button').click();
|
||||||
xlsx: resolveTestFixturePath('sample.xlsx'),
|
|
||||||
xls: resolveTestFixturePath('sample.xls'),
|
|
||||||
png: resolveTestFixturePath('sample.png'),
|
|
||||||
jpg: resolveTestFixturePath('sample.jpg'),
|
|
||||||
jpeg: resolveTestFixturePath('sample.jpeg'),
|
|
||||||
gif: resolveTestFixturePath('sample.gif'),
|
|
||||||
bmp: resolveTestFixturePath('sample.bmp'),
|
|
||||||
tiff: resolveTestFixturePath('sample.tiff'),
|
|
||||||
webp: resolveTestFixturePath('sample.webp'),
|
|
||||||
md: resolveTestFixturePath('sample.md'),
|
|
||||||
eml: resolveTestFixturePath('sample.eml'),
|
|
||||||
html: resolveTestFixturePath('sample.html'),
|
|
||||||
txt: resolveTestFixturePath('sample.txt'),
|
|
||||||
xml: resolveTestFixturePath('sample.xml'),
|
|
||||||
csv: resolveTestFixturePath('sample.csv')
|
|
||||||
};
|
|
||||||
|
|
||||||
// File format to test file mapping
|
|
||||||
const getTestFileForFormat = (format: string): string => {
|
|
||||||
const formatMap: Record<string, string> = {
|
|
||||||
'pdf': TEST_FILES.pdf,
|
|
||||||
'docx': TEST_FILES.docx,
|
|
||||||
'doc': TEST_FILES.doc,
|
|
||||||
'pptx': TEST_FILES.pptx,
|
|
||||||
'ppt': TEST_FILES.ppt,
|
|
||||||
'xlsx': TEST_FILES.xlsx,
|
|
||||||
'xls': TEST_FILES.xls,
|
|
||||||
'office': TEST_FILES.docx, // Default office file
|
|
||||||
'image': TEST_FILES.png, // Default image file
|
|
||||||
'png': TEST_FILES.png,
|
|
||||||
'jpg': TEST_FILES.jpg,
|
|
||||||
'jpeg': TEST_FILES.jpeg,
|
|
||||||
'gif': TEST_FILES.gif,
|
|
||||||
'bmp': TEST_FILES.bmp,
|
|
||||||
'tiff': TEST_FILES.tiff,
|
|
||||||
'webp': TEST_FILES.webp,
|
|
||||||
'md': TEST_FILES.md,
|
|
||||||
'eml': TEST_FILES.eml,
|
|
||||||
'html': TEST_FILES.html,
|
|
||||||
'txt': TEST_FILES.txt,
|
|
||||||
'xml': TEST_FILES.xml,
|
|
||||||
'csv': TEST_FILES.csv
|
|
||||||
};
|
|
||||||
|
|
||||||
return formatMap[format] || TEST_FILES.pdf; // Fallback to PDF
|
|
||||||
};
|
|
||||||
|
|
||||||
// Expected file extensions for target formats
|
|
||||||
const getExpectedExtension = (toFormat: string): string => {
|
|
||||||
const extensionMap: Record<string, string> = {
|
|
||||||
'pdf': '.pdf',
|
|
||||||
'docx': '.docx',
|
|
||||||
'pptx': '.pptx',
|
|
||||||
'txt': '.txt',
|
|
||||||
'html': '.zip', // HTML is zipped
|
|
||||||
'xml': '.xml',
|
|
||||||
'csv': '.csv',
|
|
||||||
'md': '.md',
|
|
||||||
'image': '.png', // Default for image conversion
|
|
||||||
'png': '.png',
|
|
||||||
'jpg': '.jpg',
|
|
||||||
'jpeg': '.jpeg',
|
|
||||||
'gif': '.gif',
|
|
||||||
'bmp': '.bmp',
|
|
||||||
'tiff': '.tiff',
|
|
||||||
'webp': '.webp',
|
|
||||||
'pdfa': '.pdf'
|
|
||||||
};
|
|
||||||
|
|
||||||
return extensionMap[toFormat] || '.pdf';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to upload files through the modal system
|
|
||||||
*/
|
|
||||||
async function uploadFileViaModal(page: Page, filePath: string) {
|
|
||||||
// Click the Files button in the QuickAccessBar to open the modal
|
|
||||||
await page.click('[data-testid="files-button"]');
|
|
||||||
|
|
||||||
// Wait for the modal to open
|
|
||||||
await page.waitForSelector('.mantine-Modal-overlay', { state: 'visible', timeout: 5000 });
|
await page.waitForSelector('.mantine-Modal-overlay', { state: 'visible', timeout: 5000 });
|
||||||
//await page.waitForSelector('[data-testid="file-upload-modal"]', { timeout: 5000 });
|
await page.locator('[data-testid="file-input"]').setInputFiles(filePath);
|
||||||
|
// Modal auto-closes after file is selected
|
||||||
// Upload the file through the modal's file input
|
await page.waitForSelector('.mantine-Modal-overlay', { state: 'hidden', timeout: 10000 });
|
||||||
await page.setInputFiles('input[type="file"]', filePath);
|
|
||||||
|
|
||||||
// Wait for the file to be processed and the modal to close
|
|
||||||
await page.waitForSelector('[data-testid="file-upload-modal"]', { state: 'hidden' });
|
|
||||||
|
|
||||||
// Wait for the file thumbnail to appear in the main interface
|
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ---------------------------------------------------------------------------
|
||||||
* Generic test function for any conversion
|
// Helper: navigate to the Convert tool panel
|
||||||
*/
|
// Tools use data-tour="tool-button-{key}" anchors in the ToolPanel.
|
||||||
async function testConversion(page: Page, conversion: ConversionEndpoint) {
|
// After clicking, the URL changes to /convert and the settings appear.
|
||||||
const expectedExtension = getExpectedExtension(conversion.toFormat);
|
// ---------------------------------------------------------------------------
|
||||||
|
async function navigateToConvert(page: Page) {
|
||||||
console.log(`Testing ${conversion.endpoint}: ${conversion.fromFormat} → ${conversion.toFormat}`);
|
await page.locator('[data-tour="tool-button-convert"]').click();
|
||||||
|
|
||||||
// File should already be uploaded, click the Convert tool button
|
|
||||||
await page.click('[data-testid="tool-convert"]');
|
|
||||||
|
|
||||||
// Wait for the FileEditor to load in convert mode with file thumbnails
|
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
|
||||||
|
|
||||||
// Click the file thumbnail checkbox to select it in the FileEditor
|
|
||||||
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
|
||||||
|
|
||||||
// Wait for the conversion settings to appear after file selection
|
|
||||||
await page.waitForSelector('[data-testid="convert-from-dropdown"]', { timeout: 5000 });
|
await page.waitForSelector('[data-testid="convert-from-dropdown"]', { timeout: 5000 });
|
||||||
|
|
||||||
// Select FROM format
|
|
||||||
await page.click('[data-testid="convert-from-dropdown"]');
|
|
||||||
const fromFormatOption = page.locator(`[data-testid="format-option-${conversion.fromFormat}"]`);
|
|
||||||
await fromFormatOption.scrollIntoViewIfNeeded();
|
|
||||||
await fromFormatOption.click();
|
|
||||||
|
|
||||||
// Select TO format
|
|
||||||
await page.click('[data-testid="convert-to-dropdown"]');
|
|
||||||
const toFormatOption = page.locator(`[data-testid="format-option-${conversion.toFormat}"]`);
|
|
||||||
await toFormatOption.scrollIntoViewIfNeeded();
|
|
||||||
await toFormatOption.click();
|
|
||||||
|
|
||||||
// Handle format-specific options
|
|
||||||
if (conversion.toFormat === 'image' || ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'webp'].includes(conversion.toFormat)) {
|
|
||||||
// Set image conversion options if they appear
|
|
||||||
const imageOptionsVisible = await page.locator('[data-testid="image-options-section"]').isVisible().catch(() => false);
|
|
||||||
if (imageOptionsVisible) {
|
|
||||||
// Click the color type dropdown and select "Color"
|
|
||||||
await page.click('[data-testid="color-type-select"]');
|
|
||||||
await page.getByRole('option', { name: 'Color' }).click();
|
|
||||||
|
|
||||||
// Set DPI value
|
|
||||||
await page.fill('[data-testid="dpi-input"]', '150');
|
|
||||||
|
|
||||||
// Click the output type dropdown and select "Multiple"
|
|
||||||
await page.click('[data-testid="output-type-select"]');
|
|
||||||
|
|
||||||
await page.getByRole('option', { name: 'single' }).click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversion.fromFormat === 'image' && conversion.toFormat === 'pdf') {
|
|
||||||
// Set PDF creation options if they appear
|
|
||||||
const pdfOptionsVisible = await page.locator('[data-testid="pdf-options-section"]').isVisible().catch(() => false);
|
|
||||||
if (pdfOptionsVisible) {
|
|
||||||
// Click the color type dropdown and select "Color"
|
|
||||||
await page.click('[data-testid="color-type-select"]');
|
|
||||||
await page.locator('[data-value="color"]').click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversion.fromFormat === 'pdf' && conversion.toFormat === 'csv') {
|
|
||||||
// Set CSV extraction options if they appear
|
|
||||||
const csvOptionsVisible = await page.locator('[data-testid="csv-options-section"]').isVisible().catch(() => false);
|
|
||||||
if (csvOptionsVisible) {
|
|
||||||
// Set specific page numbers for testing (test pages 1-2)
|
|
||||||
await page.fill('[data-testid="page-numbers-input"]', '1-2');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start conversion
|
|
||||||
await page.click('[data-testid="convert-button"]');
|
|
||||||
|
|
||||||
// Wait for conversion to complete (with generous timeout)
|
|
||||||
await page.waitForSelector('[data-testid="download-button"]', { timeout: 60000 });
|
|
||||||
|
|
||||||
// Verify download is available
|
|
||||||
const downloadButton = page.locator('[data-testid="download-button"]');
|
|
||||||
await expect(downloadButton).toBeVisible();
|
|
||||||
|
|
||||||
// Start download and verify file
|
|
||||||
const downloadPromise = page.waitForEvent('download');
|
|
||||||
await downloadButton.click();
|
|
||||||
const download = await downloadPromise;
|
|
||||||
|
|
||||||
// Verify file extension
|
|
||||||
expect(download.suggestedFilename()).toMatch(new RegExp(`\\${expectedExtension}$`));
|
|
||||||
|
|
||||||
// Save and verify file is not empty
|
|
||||||
const path = await download.path();
|
|
||||||
if (path) {
|
|
||||||
const fs = require('fs');
|
|
||||||
const stats = fs.statSync(path);
|
|
||||||
expect(stats.size).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Format-specific validations
|
|
||||||
if (conversion.toFormat === 'pdf' || conversion.toFormat === 'pdfa') {
|
|
||||||
// Verify PDF header
|
|
||||||
const buffer = fs.readFileSync(path);
|
|
||||||
const header = buffer.toString('utf8', 0, 4);
|
|
||||||
expect(header).toBe('%PDF');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversion.toFormat === 'txt') {
|
|
||||||
// Verify text content exists
|
|
||||||
const content = fs.readFileSync(path, 'utf8');
|
|
||||||
expect(content.length).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversion.toFormat === 'csv') {
|
|
||||||
// Verify CSV content contains separators
|
|
||||||
const content = fs.readFileSync(path, 'utf8');
|
|
||||||
expect(content).toContain(',');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discover conversions at module level before tests are defined
|
// ---------------------------------------------------------------------------
|
||||||
let availableConversions: ConversionEndpoint[] = [];
|
// Helper: select the TO format in the convert dropdown
|
||||||
let unavailableConversions: ConversionEndpoint[] = [];
|
// 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.
|
||||||
// Pre-populate conversions synchronously for test generation
|
// ---------------------------------------------------------------------------
|
||||||
(async () => {
|
async function selectToFormat(page: Page, toValue: string) {
|
||||||
try {
|
await page.getByTestId('convert-to-dropdown').click();
|
||||||
availableConversions = await conversionDiscovery.getAvailableConversions();
|
await page.getByTestId(`format-option-${toValue}`).click();
|
||||||
unavailableConversions = await conversionDiscovery.getUnavailableConversions();
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to discover conversions during module load:', error);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
test.describe('Convert Tool E2E Tests', () => {
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
// Re-discover to ensure fresh data at test time
|
|
||||||
console.log('Re-discovering available conversion endpoints...');
|
|
||||||
availableConversions = await conversionDiscovery.getAvailableConversions();
|
|
||||||
unavailableConversions = await conversionDiscovery.getUnavailableConversions();
|
|
||||||
|
|
||||||
console.log(`Found ${availableConversions.length} available conversions:`);
|
|
||||||
availableConversions.forEach(conv => {
|
|
||||||
console.log(` ✓ ${conv.endpoint}: ${conv.fromFormat} → ${conv.toFormat}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (unavailableConversions.length > 0) {
|
|
||||||
console.log(`Found ${unavailableConversions.length} unavailable conversions:`);
|
|
||||||
unavailableConversions.forEach(conv => {
|
|
||||||
console.log(` ✗ ${conv.endpoint}: ${conv.fromFormat} → ${conv.toFormat}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
test.describe('Convert Tool', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
// Navigate to the homepage
|
await mockAppApis(page);
|
||||||
await page.goto(`${BASE_URL}`);
|
await page.goto('/?bypassOnboarding=true');
|
||||||
|
|
||||||
// Wait for the page to load
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Wait for the QuickAccessBar to appear
|
|
||||||
await page.waitForSelector('[data-testid="files-button"]', { timeout: 10000 });
|
await page.waitForSelector('[data-testid="files-button"]', { timeout: 10000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Dynamic Conversion Tests', () => {
|
test('convert button is disabled before a TO format is selected', async ({ page }) => {
|
||||||
|
await uploadFile(page, SAMPLE_PDF);
|
||||||
|
await navigateToConvert(page);
|
||||||
|
|
||||||
// Generate a test for each potentially available conversion
|
// FROM is auto-detected as PDF; TO not selected → button visible but disabled
|
||||||
// We'll discover all possible conversions and then skip unavailable ones at runtime
|
const convertBtn = page.getByTestId('convert-button');
|
||||||
test('PDF to PNG conversion', async ({ page }) => {
|
await expect(convertBtn).toBeVisible({ timeout: 3000 });
|
||||||
const conversion: ConversionEndpoint = {
|
await expect(convertBtn).toBeDisabled();
|
||||||
endpoint: '/api/v1/convert/pdf/img',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'png',
|
|
||||||
description: '',
|
|
||||||
apiPath: ''
|
|
||||||
};
|
|
||||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
|
||||||
|
|
||||||
const testFile = getTestFileForFormat(conversion.fromFormat);
|
|
||||||
await uploadFileViaModal(page, testFile);
|
|
||||||
|
|
||||||
await testConversion(page, conversion);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('PDF to DOCX conversion', async ({ page }) => {
|
|
||||||
const conversion: ConversionEndpoint = {
|
|
||||||
endpoint: '/api/v1/convert/pdf/word',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'docx',
|
|
||||||
description: '',
|
|
||||||
apiPath: ''
|
|
||||||
};
|
|
||||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
|
||||||
|
|
||||||
const testFile = getTestFileForFormat(conversion.fromFormat);
|
|
||||||
await uploadFileViaModal(page, testFile);
|
|
||||||
|
|
||||||
await testConversion(page, conversion);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('DOCX to PDF conversion', async ({ page }) => {
|
|
||||||
const conversion: ConversionEndpoint = {
|
|
||||||
endpoint: '/api/v1/convert/file/pdf',
|
|
||||||
fromFormat: 'docx',
|
|
||||||
toFormat: 'pdf',
|
|
||||||
description: '',
|
|
||||||
apiPath: ''
|
|
||||||
};
|
|
||||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
|
||||||
|
|
||||||
const testFile = getTestFileForFormat(conversion.fromFormat);
|
|
||||||
await uploadFileViaModal(page, testFile);
|
|
||||||
|
|
||||||
await testConversion(page, conversion);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Image to PDF conversion', async ({ page }) => {
|
|
||||||
const conversion: ConversionEndpoint = {
|
|
||||||
endpoint: '/api/v1/convert/img/pdf',
|
|
||||||
fromFormat: 'png',
|
|
||||||
toFormat: 'pdf',
|
|
||||||
description: '',
|
|
||||||
apiPath: ''
|
|
||||||
};
|
|
||||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
|
||||||
|
|
||||||
const testFile = getTestFileForFormat(conversion.fromFormat);
|
|
||||||
await uploadFileViaModal(page, testFile);
|
|
||||||
|
|
||||||
await testConversion(page, conversion);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('PDF to TXT conversion', async ({ page }) => {
|
|
||||||
const conversion: ConversionEndpoint = {
|
|
||||||
endpoint: '/api/v1/convert/pdf/text',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'txt',
|
|
||||||
description: '',
|
|
||||||
apiPath: ''
|
|
||||||
};
|
|
||||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
|
||||||
|
|
||||||
const testFile = getTestFileForFormat(conversion.fromFormat);
|
|
||||||
await uploadFileViaModal(page, testFile);
|
|
||||||
|
|
||||||
await testConversion(page, conversion);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('PDF to HTML conversion', async ({ page }) => {
|
|
||||||
const conversion: ConversionEndpoint = {
|
|
||||||
endpoint: '/api/v1/convert/pdf/html',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'html',
|
|
||||||
description: '',
|
|
||||||
apiPath: ''
|
|
||||||
};
|
|
||||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
|
||||||
|
|
||||||
const testFile = getTestFileForFormat(conversion.fromFormat);
|
|
||||||
await uploadFileViaModal(page, testFile);
|
|
||||||
|
|
||||||
await testConversion(page, conversion);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('PDF to XML conversion', async ({ page }) => {
|
|
||||||
const conversion: ConversionEndpoint = {
|
|
||||||
endpoint: '/api/v1/convert/pdf/xml',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'xml',
|
|
||||||
description: '',
|
|
||||||
apiPath: ''
|
|
||||||
};
|
|
||||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
|
||||||
|
|
||||||
const testFile = getTestFileForFormat(conversion.fromFormat);
|
|
||||||
await uploadFileViaModal(page, testFile);
|
|
||||||
|
|
||||||
await testConversion(page, conversion);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('PDF to CSV conversion', async ({ page }) => {
|
|
||||||
const conversion: ConversionEndpoint = {
|
|
||||||
endpoint: '/api/v1/convert/pdf/csv',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'csv',
|
|
||||||
description: '',
|
|
||||||
apiPath: ''
|
|
||||||
};
|
|
||||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
|
||||||
|
|
||||||
const testFile = getTestFileForFormat(conversion.fromFormat);
|
|
||||||
await uploadFileViaModal(page, testFile);
|
|
||||||
|
|
||||||
await testConversion(page, conversion);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('PDF to PDFA conversion', async ({ page }) => {
|
|
||||||
const conversion: ConversionEndpoint = {
|
|
||||||
endpoint: '/api/v1/convert/pdf/pdfa',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'pdfa',
|
|
||||||
description: '',
|
|
||||||
apiPath: ''
|
|
||||||
};
|
|
||||||
const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint);
|
|
||||||
test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`);
|
|
||||||
|
|
||||||
const testFile = getTestFileForFormat(conversion.fromFormat);
|
|
||||||
await uploadFileViaModal(page, testFile);
|
|
||||||
|
|
||||||
await testConversion(page, conversion);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Static Tests', () => {
|
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),
|
||||||
|
]);
|
||||||
|
|
||||||
// Test that disabled conversions don't appear in dropdowns when they shouldn't
|
await page.route('**/api/v1/convert/pdf/img', (route) =>
|
||||||
test('should not show conversion button when no valid conversions available', async ({ page }) => {
|
route.fulfill({
|
||||||
// This test ensures the convert button is disabled when no valid conversion is possible
|
status: 200,
|
||||||
await uploadFileViaModal(page, TEST_FILES.pdf);
|
contentType: 'image/png',
|
||||||
|
headers: { 'Content-Disposition': 'attachment; filename="sample.png"' },
|
||||||
|
body: fakePng,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Click the Convert tool button
|
await uploadFile(page, SAMPLE_PDF);
|
||||||
await page.click('[data-testid="tool-convert"]');
|
await navigateToConvert(page);
|
||||||
|
await selectToFormat(page, 'png');
|
||||||
|
await page.getByTestId('convert-button').click();
|
||||||
|
|
||||||
// Wait for convert mode and select file
|
await expect(page.getByTestId('download-result-button')).toBeVisible({ timeout: 10000 });
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
});
|
||||||
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
|
||||||
|
|
||||||
// Don't select any formats - convert button should not exist
|
test('conversion API error shows error notification', async ({ page }) => {
|
||||||
const convertButton = page.locator('[data-testid="convert-button"]');
|
await page.route('**/api/v1/convert/pdf/img', (route) =>
|
||||||
await expect(convertButton).toHaveCount(0);
|
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 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,325 +0,0 @@
|
|||||||
/**
|
|
||||||
* Conversion Endpoint Discovery for E2E Testing
|
|
||||||
*
|
|
||||||
* Uses the backend's endpoint configuration API to discover available conversions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useMultipleEndpointsEnabled } from '@app/hooks/useEndpointConfig';
|
|
||||||
|
|
||||||
export interface ConversionEndpoint {
|
|
||||||
endpoint: string;
|
|
||||||
fromFormat: string;
|
|
||||||
toFormat: string;
|
|
||||||
description: string;
|
|
||||||
apiPath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete list of conversion endpoints based on EndpointConfiguration.java
|
|
||||||
const ALL_CONVERSION_ENDPOINTS: ConversionEndpoint[] = [
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-img',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'image',
|
|
||||||
description: 'Convert PDF to images (PNG, JPG, GIF, etc.)',
|
|
||||||
apiPath: '/api/v1/convert/pdf/img'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'img-to-pdf',
|
|
||||||
fromFormat: 'image',
|
|
||||||
toFormat: 'pdf',
|
|
||||||
description: 'Convert images to PDF',
|
|
||||||
apiPath: '/api/v1/convert/img/pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-pdfa',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'pdfa',
|
|
||||||
description: 'Convert PDF to PDF/A',
|
|
||||||
apiPath: '/api/v1/convert/pdf/pdfa'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'file-to-pdf',
|
|
||||||
fromFormat: 'office',
|
|
||||||
toFormat: 'pdf',
|
|
||||||
description: 'Convert office files to PDF',
|
|
||||||
apiPath: '/api/v1/convert/file/pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-word',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'docx',
|
|
||||||
description: 'Convert PDF to Word document',
|
|
||||||
apiPath: '/api/v1/convert/pdf/word'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-presentation',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'pptx',
|
|
||||||
description: 'Convert PDF to PowerPoint presentation',
|
|
||||||
apiPath: '/api/v1/convert/pdf/presentation'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-text',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'txt',
|
|
||||||
description: 'Convert PDF to plain text',
|
|
||||||
apiPath: '/api/v1/convert/pdf/text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-html',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'html',
|
|
||||||
description: 'Convert PDF to HTML',
|
|
||||||
apiPath: '/api/v1/convert/pdf/html'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-xml',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'xml',
|
|
||||||
description: 'Convert PDF to XML',
|
|
||||||
apiPath: '/api/v1/convert/pdf/xml'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'html-to-pdf',
|
|
||||||
fromFormat: 'html',
|
|
||||||
toFormat: 'pdf',
|
|
||||||
description: 'Convert HTML to PDF',
|
|
||||||
apiPath: '/api/v1/convert/html/pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'url-to-pdf',
|
|
||||||
fromFormat: 'url',
|
|
||||||
toFormat: 'pdf',
|
|
||||||
description: 'Convert web page to PDF',
|
|
||||||
apiPath: '/api/v1/convert/url/pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'markdown-to-pdf',
|
|
||||||
fromFormat: 'md',
|
|
||||||
toFormat: 'pdf',
|
|
||||||
description: 'Convert Markdown to PDF',
|
|
||||||
apiPath: '/api/v1/convert/markdown/pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-csv',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'csv',
|
|
||||||
description: 'Extract CSV data from PDF',
|
|
||||||
apiPath: '/api/v1/convert/pdf/csv'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-xlsx',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'xlsx',
|
|
||||||
description: 'Extract Excel spreadsheet from PDF',
|
|
||||||
apiPath: '/api/v1/convert/pdf/xlsx'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-markdown',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'md',
|
|
||||||
description: 'Convert PDF to Markdown',
|
|
||||||
apiPath: '/api/v1/convert/pdf/markdown'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'eml-to-pdf',
|
|
||||||
fromFormat: 'eml',
|
|
||||||
toFormat: 'pdf',
|
|
||||||
description: 'Convert email (EML) to PDF',
|
|
||||||
apiPath: '/api/v1/convert/eml/pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'pdf-to-epub',
|
|
||||||
fromFormat: 'pdf',
|
|
||||||
toFormat: 'epub',
|
|
||||||
description: 'Convert PDF to EPUB/AZW3',
|
|
||||||
apiPath: '/api/v1/convert/pdf/epub'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: 'eml-to-pdf', // MSG uses same endpoint as EML
|
|
||||||
fromFormat: 'msg',
|
|
||||||
toFormat: 'pdf',
|
|
||||||
description: 'Convert Outlook email (MSG) to PDF',
|
|
||||||
apiPath: '/api/v1/convert/eml/pdf'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export class ConversionEndpointDiscovery {
|
|
||||||
private baseUrl: string;
|
|
||||||
private cache: Map<string, boolean> | null = null;
|
|
||||||
private cacheExpiry: number = 0;
|
|
||||||
private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
constructor(baseUrl: string = process.env.BACKEND_URL || 'http://localhost:8080') {
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all available conversion endpoints by checking with backend
|
|
||||||
*/
|
|
||||||
async getAvailableConversions(): Promise<ConversionEndpoint[]> {
|
|
||||||
const endpointStatuses = await this.getEndpointStatuses();
|
|
||||||
|
|
||||||
return ALL_CONVERSION_ENDPOINTS.filter(conversion =>
|
|
||||||
endpointStatuses.get(conversion.endpoint) === true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all unavailable conversion endpoints
|
|
||||||
*/
|
|
||||||
async getUnavailableConversions(): Promise<ConversionEndpoint[]> {
|
|
||||||
const endpointStatuses = await this.getEndpointStatuses();
|
|
||||||
|
|
||||||
return ALL_CONVERSION_ENDPOINTS.filter(conversion =>
|
|
||||||
endpointStatuses.get(conversion.endpoint) === false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a specific conversion is available
|
|
||||||
*/
|
|
||||||
async isConversionAvailable(endpoint: string): Promise<boolean> {
|
|
||||||
const endpointStatuses = await this.getEndpointStatuses();
|
|
||||||
return endpointStatuses.get(endpoint) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get available conversions grouped by source format
|
|
||||||
*/
|
|
||||||
async getConversionsByFormat(): Promise<Record<string, ConversionEndpoint[]>> {
|
|
||||||
const availableConversions = await this.getAvailableConversions();
|
|
||||||
|
|
||||||
const grouped: Record<string, ConversionEndpoint[]> = {};
|
|
||||||
|
|
||||||
availableConversions.forEach(conversion => {
|
|
||||||
if (!grouped[conversion.fromFormat]) {
|
|
||||||
grouped[conversion.fromFormat] = [];
|
|
||||||
}
|
|
||||||
grouped[conversion.fromFormat].push(conversion);
|
|
||||||
});
|
|
||||||
|
|
||||||
return grouped;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get supported target formats for a given source format
|
|
||||||
*/
|
|
||||||
async getSupportedTargetFormats(fromFormat: string): Promise<string[]> {
|
|
||||||
const availableConversions = await this.getAvailableConversions();
|
|
||||||
|
|
||||||
return availableConversions
|
|
||||||
.filter(conversion => conversion.fromFormat === fromFormat)
|
|
||||||
.map(conversion => conversion.toFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all supported source formats
|
|
||||||
*/
|
|
||||||
async getSupportedSourceFormats(): Promise<string[]> {
|
|
||||||
const availableConversions = await this.getAvailableConversions();
|
|
||||||
|
|
||||||
const sourceFormats = new Set(
|
|
||||||
availableConversions.map(conversion => conversion.fromFormat)
|
|
||||||
);
|
|
||||||
|
|
||||||
return Array.from(sourceFormats);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get endpoint statuses from backend using batch API
|
|
||||||
*/
|
|
||||||
private async getEndpointStatuses(): Promise<Map<string, boolean>> {
|
|
||||||
// Return cached result if still valid
|
|
||||||
if (this.cache && Date.now() < this.cacheExpiry) {
|
|
||||||
return this.cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const endpointNames = ALL_CONVERSION_ENDPOINTS.map(conv => conv.endpoint);
|
|
||||||
const endpointsParam = endpointNames.join(',');
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`${this.baseUrl}/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch endpoint statuses: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusMap: Record<string, boolean> = await response.json();
|
|
||||||
|
|
||||||
// Convert to Map and cache
|
|
||||||
this.cache = new Map(Object.entries(statusMap));
|
|
||||||
this.cacheExpiry = Date.now() + this.CACHE_DURATION;
|
|
||||||
|
|
||||||
console.log(`Retrieved status for ${Object.keys(statusMap).length} conversion endpoints`);
|
|
||||||
return this.cache;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to get endpoint statuses:', error);
|
|
||||||
|
|
||||||
// Fallback: assume all endpoints are disabled
|
|
||||||
const fallbackMap = new Map<string, boolean>();
|
|
||||||
ALL_CONVERSION_ENDPOINTS.forEach(conv => {
|
|
||||||
fallbackMap.set(conv.endpoint, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
return fallbackMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to create a skipping condition for tests
|
|
||||||
*/
|
|
||||||
static createSkipCondition(endpoint: string, discovery: ConversionEndpointDiscovery) {
|
|
||||||
return async () => {
|
|
||||||
const available = await discovery.isConversionAvailable(endpoint);
|
|
||||||
return !available;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get detailed conversion info by endpoint name
|
|
||||||
*/
|
|
||||||
getConversionInfo(endpoint: string): ConversionEndpoint | undefined {
|
|
||||||
return ALL_CONVERSION_ENDPOINTS.find(conv => conv.endpoint === endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all conversion endpoints (regardless of availability)
|
|
||||||
*/
|
|
||||||
getAllConversions(): ConversionEndpoint[] {
|
|
||||||
return [...ALL_CONVERSION_ENDPOINTS];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export singleton instance for reuse across tests
|
|
||||||
export const conversionDiscovery = new ConversionEndpointDiscovery();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React hook version for use in components (wraps the class)
|
|
||||||
*/
|
|
||||||
export function useConversionEndpoints() {
|
|
||||||
const endpointNames = ALL_CONVERSION_ENDPOINTS.map(conv => conv.endpoint);
|
|
||||||
const { endpointStatus, loading, error, refetch } = useMultipleEndpointsEnabled(endpointNames);
|
|
||||||
|
|
||||||
const availableConversions = ALL_CONVERSION_ENDPOINTS.filter(
|
|
||||||
conv => endpointStatus[conv.endpoint] === true
|
|
||||||
);
|
|
||||||
|
|
||||||
const unavailableConversions = ALL_CONVERSION_ENDPOINTS.filter(
|
|
||||||
conv => endpointStatus[conv.endpoint] === false
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
availableConversions,
|
|
||||||
unavailableConversions,
|
|
||||||
allConversions: ALL_CONVERSION_ENDPOINTS,
|
|
||||||
endpointStatus,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
isConversionAvailable: (endpoint: string) => endpointStatus[endpoint] === true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,7 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasFiles = selectedFiles.length > 0;
|
const hasFiles = selectedFiles.length > 0;
|
||||||
const hasResults = convertOperation.files.length > 0 || convertOperation.downloadUrl !== null;
|
const hasResults = convertOperation.files.length > 0 || convertOperation.downloadUrl !== null || !!convertOperation.errorMessage;
|
||||||
const settingsCollapsed = hasResults;
|
const settingsCollapsed = hasResults;
|
||||||
|
|
||||||
// When operation completes, flag the next selection change to skip reset
|
// When operation completes, flag the next selection change to skip reset
|
||||||
|
|||||||
Reference in New Issue
Block a user