mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-21 19:59:24 +00:00

# Description of Changes Currently, the `tsconfig.json` file enforces strict type checking, but nothing in CI checks that the code is actually correctly typed. [Vite only transpiles TypeScript code](https://vite.dev/guide/features.html#transpile-only) so doesn't ensure that the TS code we're running is correct. This PR adds running of the type checker to CI and fixes the type errors that have already crept into the codebase. Note that many of the changes I've made to 'fix the types' are just using `any` to disable the type checker because the code is under too much churn to fix anything properly at the moment. I still think enabling the type checker now is the best course of action though because otherwise we'll never be able to fix all of them, and it should at least help us not break things when adding new code. Co-authored-by: James <james@crosscourtanalytics.com>
495 lines
18 KiB
TypeScript
495 lines
18 KiB
TypeScript
/**
|
|
* End-to-End Tests for Convert Tool
|
|
*
|
|
* These tests dynamically discover available conversion endpoints and test them.
|
|
* Tests are automatically skipped if the backend endpoint is not available.
|
|
*
|
|
* Run with: npm run test:e2e or npx playwright test
|
|
*/
|
|
|
|
import { test, expect, Page } from '@playwright/test';
|
|
import {
|
|
conversionDiscovery,
|
|
type ConversionEndpoint
|
|
} from '../helpers/conversionEndpointDiscovery';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
|
|
// Test configuration
|
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
|
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080';
|
|
|
|
/**
|
|
* Resolves test fixture paths dynamically based on current working directory.
|
|
* Works from both top-level project directory and frontend subdirectory.
|
|
*/
|
|
function resolveTestFixturePath(filename: string): string {
|
|
const cwd = process.cwd();
|
|
|
|
// Try frontend/src/tests/test-fixtures/ first (from top-level)
|
|
const topLevelPath = path.join(cwd, 'frontend', 'src', 'tests', 'test-fixtures', filename);
|
|
if (fs.existsSync(topLevelPath)) {
|
|
return topLevelPath;
|
|
}
|
|
|
|
// Try src/tests/test-fixtures/ (from frontend directory)
|
|
const frontendPath = path.join(cwd, 'src', 'tests', 'test-fixtures', filename);
|
|
if (fs.existsSync(frontendPath)) {
|
|
return frontendPath;
|
|
}
|
|
|
|
// Try relative path from current test file location
|
|
const relativePath = path.join(__dirname, '..', 'test-fixtures', filename);
|
|
if (fs.existsSync(relativePath)) {
|
|
return relativePath;
|
|
}
|
|
|
|
// Fallback to the original path format (should work from top-level)
|
|
return path.join('.', 'frontend', 'src', 'tests', 'test-fixtures', filename);
|
|
}
|
|
|
|
// Test file paths (dynamically resolved based on current working directory)
|
|
const TEST_FILES = {
|
|
pdf: resolveTestFixturePath('sample.pdf'),
|
|
docx: resolveTestFixturePath('sample.docx'),
|
|
doc: resolveTestFixturePath('sample.doc'),
|
|
pptx: resolveTestFixturePath('sample.pptx'),
|
|
ppt: resolveTestFixturePath('sample.ppt'),
|
|
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('[data-testid="file-upload-modal"]', { timeout: 5000 });
|
|
|
|
// Upload the file through the modal's file input
|
|
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
|
|
*/
|
|
async function testConversion(page: Page, conversion: ConversionEndpoint) {
|
|
const expectedExtension = getExpectedExtension(conversion.toFormat);
|
|
|
|
console.log(`Testing ${conversion.endpoint}: ${conversion.fromFormat} → ${conversion.toFormat}`);
|
|
|
|
// 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 });
|
|
|
|
// 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 allConversions: ConversionEndpoint[] = [];
|
|
let availableConversions: ConversionEndpoint[] = [];
|
|
let unavailableConversions: ConversionEndpoint[] = [];
|
|
|
|
// Pre-populate conversions synchronously for test generation
|
|
(async () => {
|
|
try {
|
|
availableConversions = await conversionDiscovery.getAvailableConversions();
|
|
unavailableConversions = await conversionDiscovery.getUnavailableConversions();
|
|
allConversions = [...availableConversions, ...unavailableConversions];
|
|
} 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}`);
|
|
});
|
|
}
|
|
});
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
// Navigate to the homepage
|
|
await page.goto(`${BASE_URL}`);
|
|
|
|
// 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 });
|
|
});
|
|
|
|
test.describe('Dynamic Conversion Tests', () => {
|
|
|
|
// Generate a test for each potentially available conversion
|
|
// We'll discover all possible conversions and then skip unavailable ones at runtime
|
|
test('PDF to PNG conversion', async ({ page }) => {
|
|
const conversion: ConversionEndpoint = {
|
|
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 that disabled conversions don't appear in dropdowns when they shouldn't
|
|
test('should not show conversion button when no valid conversions available', async ({ page }) => {
|
|
// This test ensures the convert button is disabled when no valid conversion is possible
|
|
await uploadFileViaModal(page, TEST_FILES.pdf);
|
|
|
|
// Click the Convert tool button
|
|
await page.click('[data-testid="tool-convert"]');
|
|
|
|
// Wait for convert mode and select file
|
|
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
|
|
const convertButton = page.locator('[data-testid="convert-button"]');
|
|
await expect(convertButton).toHaveCount(0);
|
|
});
|
|
});
|
|
});
|
|
|