Add frontend autoformatting and set CI to require formatted code for all languages (#6052)

# Description of Changes
Changes the strategy for autoformatting to reject PRs if they are not
formatted correctly instead of allowing them to merge and then spawning
a new PR to fix the formatting. The old strategy just caused more work
for us because we'd have to manually approve the followup PR and get it
merged, which required 2 reviewers so in practice it rarely got done and
just meant everyone's PRs ended up containing reformatting for unrelated
files, which makes code review unnecessarily difficult. If the PR's code
is not formatted correctly after this PR, a comment will be added
automatically to tell the author how to run the formatter script to fix
their code so it can go in.

This also enables autoformatting for the frontend code, using Prettier.
I've enabled it for pretty much everything in the frontend folder, other
than 3rd party files and files it doesn't make sense for. I also
excluded Markdown because it sounds likely to be more annoying to have
to autoformat the Markdown in the frontend folder but nowhere else. Open
to changing this though if people disagree.

> [!note]
> 
> Advice to reviewers: The first commit contains all of the actual logic
I've introduced (CI changes, Prettier config, etc.)
> The second commit is just the reformatting of the entire frontend
folder.
> The first commit needs proper review, the second one just give it a
spot-check that it's doing what you'd expect.
This commit is contained in:
James Brunton
2026-04-10 17:41:19 +01:00
committed by GitHub
parent 33b2b5827a
commit a3e45bc182
1359 changed files with 57784 additions and 57460 deletions
@@ -1,23 +1,23 @@
import { test, expect, type Page } from '@playwright/test';
import path from 'path';
import { test, expect, type Page } from "@playwright/test";
import path from "path";
// ---------------------------------------------------------------------------
// Test fixtures — pre-generated keystores in test-fixtures/certs/
// ---------------------------------------------------------------------------
const CERTS_DIR = path.join(__dirname, '../test-fixtures/certs');
const VALID_P12 = path.join(CERTS_DIR, 'valid-test.p12');
const EXPIRED_P12 = path.join(CERTS_DIR, 'expired-test.p12');
const NOT_YET_VALID_P12 = path.join(CERTS_DIR, 'not-yet-valid-test.p12');
const VALID_JKS = path.join(CERTS_DIR, 'valid-test.jks');
const CERTS_DIR = path.join(__dirname, "../test-fixtures/certs");
const VALID_P12 = path.join(CERTS_DIR, "valid-test.p12");
const EXPIRED_P12 = path.join(CERTS_DIR, "expired-test.p12");
const NOT_YET_VALID_P12 = path.join(CERTS_DIR, "not-yet-valid-test.p12");
const VALID_JKS = path.join(CERTS_DIR, "valid-test.jks");
// ---------------------------------------------------------------------------
// Stable mock data returned by the mocked backend
// ---------------------------------------------------------------------------
const MOCK_SESSION = {
sessionId: 'test-session-id',
documentName: 'test-document.pdf',
status: 'IN_PROGRESS',
ownerUsername: 'test-owner',
sessionId: "test-session-id",
documentName: "test-document.pdf",
status: "IN_PROGRESS",
ownerUsername: "test-owner",
message: null,
dueDate: null,
participants: [],
@@ -25,10 +25,10 @@ const MOCK_SESSION = {
const MOCK_PARTICIPANT = {
id: 1,
email: '[email protected]',
name: 'Test User',
status: 'PENDING',
shareToken: 'test-token',
email: "[email protected]",
name: "Test User",
status: "PENDING",
shareToken: "test-token",
expiresAt: null,
hasCompleted: false,
isExpired: false,
@@ -40,30 +40,26 @@ const MOCK_PARTICIPANT = {
// ---------------------------------------------------------------------------
async function mockParticipantApis(page: Page) {
// Mock auth so AppProviders/Landing don't redirect to /login
await page.route('**/api/v1/auth/me', (route) =>
await page.route("**/api/v1/auth/me", (route) =>
route.fulfill({
json: {
id: 1,
username: 'testuser',
email: '[email protected]',
roles: ['ROLE_USER'],
username: "testuser",
email: "[email protected]",
roles: ["ROLE_USER"],
},
})
}),
);
await page.route('**/api/v1/workflow/participant/session**', (route) =>
route.fulfill({ json: MOCK_SESSION })
);
await page.route('**/api/v1/workflow/participant/details**', (route) =>
route.fulfill({ json: MOCK_PARTICIPANT })
);
await page.route("**/api/v1/workflow/participant/session**", (route) => route.fulfill({ json: MOCK_SESSION }));
await page.route("**/api/v1/workflow/participant/details**", (route) => route.fulfill({ json: MOCK_PARTICIPANT }));
// Minimal stub so the download-document call doesn't throw
await page.route('**/api/v1/workflow/participant/document**', (route) =>
await page.route("**/api/v1/workflow/participant/document**", (route) =>
route.fulfill({
status: 200,
contentType: 'application/pdf',
contentType: "application/pdf",
body: Buffer.alloc(128),
})
}),
);
}
@@ -72,8 +68,8 @@ async function mockParticipantApis(page: Page) {
// the matching option — Mantine renders a custom combobox, not a native select.
// ---------------------------------------------------------------------------
async function selectCertType(page: Page, label: string) {
await page.getByTestId('cert-type-select').click();
await page.getByRole('option', { name: label }).click();
await page.getByTestId("cert-type-select").click();
await page.getByRole("option", { name: label }).click();
}
// ---------------------------------------------------------------------------
@@ -82,7 +78,7 @@ async function selectCertType(page: Page, label: string) {
async function uploadCertFile(page: Page, filePath: string) {
// Mantine FileInput uses a visually hidden <input type="file">.
// We click the visible button to expose it, then set files via the hidden input.
const certFileInput = page.getByTestId('cert-file-input');
const certFileInput = page.getByTestId("cert-file-input");
await certFileInput.click();
// After click, the file chooser or the hidden input becomes interactive.
// Use the first file input on the page (Mantine places it near the button).
@@ -93,41 +89,41 @@ async function uploadCertFile(page: Page, filePath: string) {
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe('Certificate Validation — ParticipantView', () => {
test.describe("Certificate Validation — ParticipantView", () => {
test.beforeEach(async ({ page }) => {
await mockParticipantApis(page);
});
// 1. Happy path — valid P12
test('valid P12 cert shows green "Certificate valid until" feedback', async ({ page }) => {
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
route.fulfill({
json: {
valid: true,
subjectName: 'Test Signer',
notAfter: '2027-01-01T00:00:00Z',
notBefore: '2025-01-01T00:00:00Z',
subjectName: "Test Signer",
notAfter: "2027-01-01T00:00:00Z",
notBefore: "2025-01-01T00:00:00Z",
selfSigned: true,
error: null,
},
})
}),
);
await page.goto('/workflow/sign/test-token');
await page.goto("/workflow/sign/test-token");
await page.waitForSelector('[data-testid="submit-signature-button"]');
await uploadCertFile(page, VALID_P12);
await page.getByTestId('cert-password-input').fill('testpass');
await page.getByTestId("cert-password-input").fill("testpass");
// Wait for debounce (600 ms) + network round-trip
const feedback = page.getByTestId('cert-validation-feedback');
await expect(feedback).toContainText('Certificate valid until', { timeout: 5000 });
await expect(feedback).toContainText('Test Signer');
const feedback = page.getByTestId("cert-validation-feedback");
await expect(feedback).toContainText("Certificate valid until", { timeout: 5000 });
await expect(feedback).toContainText("Test Signer");
});
// 2. Wrong password — red error
test('wrong password shows red error message', async ({ page }) => {
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
test("wrong password shows red error message", async ({ page }) => {
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
route.fulfill({
json: {
valid: false,
@@ -135,24 +131,24 @@ test.describe('Certificate Validation — ParticipantView', () => {
notAfter: null,
notBefore: null,
selfSigned: false,
error: 'Invalid certificate password or corrupt keystore file',
error: "Invalid certificate password or corrupt keystore file",
},
})
}),
);
await page.goto('/workflow/sign/test-token');
await page.goto("/workflow/sign/test-token");
await page.waitForSelector('[data-testid="submit-signature-button"]');
await uploadCertFile(page, VALID_P12);
await page.getByTestId('cert-password-input').fill('wrongpass');
await page.getByTestId("cert-password-input").fill("wrongpass");
const feedback = page.getByTestId('cert-validation-feedback');
await expect(feedback).toContainText('Invalid certificate password', { timeout: 5000 });
const feedback = page.getByTestId("cert-validation-feedback");
await expect(feedback).toContainText("Invalid certificate password", { timeout: 5000 });
});
// 3. Expired certificate
test('expired cert shows "Certificate has expired" error', async ({ page }) => {
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
route.fulfill({
json: {
valid: false,
@@ -160,24 +156,24 @@ test.describe('Certificate Validation — ParticipantView', () => {
notAfter: null,
notBefore: null,
selfSigned: false,
error: 'Certificate has expired (expired: 2023-01-02 00:00:00 UTC)',
error: "Certificate has expired (expired: 2023-01-02 00:00:00 UTC)",
},
})
}),
);
await page.goto('/workflow/sign/test-token');
await page.goto("/workflow/sign/test-token");
await page.waitForSelector('[data-testid="submit-signature-button"]');
await uploadCertFile(page, EXPIRED_P12);
await page.getByTestId('cert-password-input').fill('testpass');
await page.getByTestId("cert-password-input").fill("testpass");
const feedback = page.getByTestId('cert-validation-feedback');
await expect(feedback).toContainText('Certificate has expired', { timeout: 5000 });
const feedback = page.getByTestId("cert-validation-feedback");
await expect(feedback).toContainText("Certificate has expired", { timeout: 5000 });
});
// 4. Not-yet-valid certificate
test('not-yet-valid cert shows "not yet valid" error', async ({ page }) => {
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
route.fulfill({
json: {
valid: false,
@@ -185,46 +181,46 @@ test.describe('Certificate Validation — ParticipantView', () => {
notAfter: null,
notBefore: null,
selfSigned: false,
error: 'Certificate is not yet valid (valid from: 2027-01-01 00:00:00 UTC)',
error: "Certificate is not yet valid (valid from: 2027-01-01 00:00:00 UTC)",
},
})
}),
);
await page.goto('/workflow/sign/test-token');
await page.goto("/workflow/sign/test-token");
await page.waitForSelector('[data-testid="submit-signature-button"]');
await uploadCertFile(page, NOT_YET_VALID_P12);
await page.getByTestId('cert-password-input').fill('testpass');
await page.getByTestId("cert-password-input").fill("testpass");
const feedback = page.getByTestId('cert-validation-feedback');
await expect(feedback).toContainText('not yet valid', { timeout: 5000 });
const feedback = page.getByTestId("cert-validation-feedback");
await expect(feedback).toContainText("not yet valid", { timeout: 5000 });
});
// 5. Submit button disabled while validating
test('submit button is disabled while validation is in flight', async ({ page }) => {
test("submit button is disabled while validation is in flight", async ({ page }) => {
// Slow response so we can assert the disabled state mid-flight
await page.route('**/api/v1/workflow/participant/validate-certificate', async (route) => {
await page.route("**/api/v1/workflow/participant/validate-certificate", async (route) => {
await new Promise((r) => setTimeout(r, 1500));
await route.fulfill({
json: {
valid: true,
subjectName: 'Test Signer',
notAfter: '2027-01-01T00:00:00Z',
notBefore: '2025-01-01T00:00:00Z',
subjectName: "Test Signer",
notAfter: "2027-01-01T00:00:00Z",
notBefore: "2025-01-01T00:00:00Z",
selfSigned: true,
error: null,
},
});
});
await page.goto('/workflow/sign/test-token');
await page.goto("/workflow/sign/test-token");
await page.waitForSelector('[data-testid="submit-signature-button"]');
await uploadCertFile(page, VALID_P12);
await page.getByTestId('cert-password-input').fill('testpass');
await page.getByTestId("cert-password-input").fill("testpass");
// Shortly after typing, validation is in flight — button must be disabled
const submitBtn = page.getByTestId('submit-signature-button');
const submitBtn = page.getByTestId("submit-signature-button");
await expect(submitBtn).toBeDisabled({ timeout: 3000 });
// After validation completes the button should be re-enabled
@@ -232,49 +228,49 @@ test.describe('Certificate Validation — ParticipantView', () => {
});
// 6. SERVER type — no validation call made, button stays enabled
test('SERVER cert type skips validation and keeps submit enabled', async ({ page }) => {
test("SERVER cert type skips validation and keeps submit enabled", async ({ page }) => {
let validateCalled = false;
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) => {
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) => {
validateCalled = true;
return route.fulfill({ json: { valid: true } });
});
await page.goto('/workflow/sign/test-token');
await page.goto("/workflow/sign/test-token");
await page.waitForSelector('[data-testid="submit-signature-button"]');
await selectCertType(page, 'Server Certificate (if available)');
await selectCertType(page, "Server Certificate (if available)");
// Wait longer than debounce to confirm no call is made
await page.waitForTimeout(1000);
expect(validateCalled).toBe(false);
await expect(page.getByTestId('submit-signature-button')).toBeEnabled();
await expect(page.getByTestId("submit-signature-button")).toBeEnabled();
});
// 7. Bonus — valid JKS keystore
test('valid JKS keystore shows green feedback', async ({ page }) => {
await page.route('**/api/v1/workflow/participant/validate-certificate', (route) =>
test("valid JKS keystore shows green feedback", async ({ page }) => {
await page.route("**/api/v1/workflow/participant/validate-certificate", (route) =>
route.fulfill({
json: {
valid: true,
subjectName: 'JKS Signer',
notAfter: '2027-01-01T00:00:00Z',
notBefore: '2025-01-01T00:00:00Z',
subjectName: "JKS Signer",
notAfter: "2027-01-01T00:00:00Z",
notBefore: "2025-01-01T00:00:00Z",
selfSigned: true,
error: null,
},
})
}),
);
await page.goto('/workflow/sign/test-token');
await page.goto("/workflow/sign/test-token");
await page.waitForSelector('[data-testid="submit-signature-button"]');
await selectCertType(page, 'JKS Keystore');
await selectCertType(page, "JKS Keystore");
await uploadCertFile(page, VALID_JKS);
await page.getByTestId('cert-password-input').fill('jkspass');
await page.getByTestId("cert-password-input").fill("jkspass");
const feedback = page.getByTestId('cert-validation-feedback');
await expect(feedback).toContainText('Certificate valid until', { timeout: 5000 });
await expect(feedback).toContainText('JKS Signer');
const feedback = page.getByTestId("cert-validation-feedback");
await expect(feedback).toContainText("Certificate valid until", { timeout: 5000 });
await expect(feedback).toContainText("JKS Signer");
});
});
@@ -5,23 +5,40 @@
* 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';
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');
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 }])
"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 }]),
);
// ---------------------------------------------------------------------------
@@ -29,48 +46,36 @@ const MOCK_ENDPOINTS_AVAILABILITY = Object.fromEntries(
// ---------------------------------------------------------------------------
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' } })
);
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) =>
await page.route("**/api/v1/config/app-config", (route) =>
route.fulfill({
json: { enableLogin: false, languages: ['en-GB'], defaultLocale: 'en-GB' },
})
json: { enableLogin: false, languages: ["en-GB"], defaultLocale: "en-GB" },
}),
);
// Auth — fallback if anything calls auth/me
await page.route('**/api/v1/auth/me', (route) =>
await page.route("**/api/v1/auth/me", (route) =>
route.fulfill({
json: { id: 1, username: 'testuser', email: '[email protected]', roles: ['ROLE_USER'] },
})
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 })
);
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 })
);
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 })
);
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: {} })
);
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: {} })
);
await page.route("**/api/v1/proprietary/**", (route) => route.fulfill({ json: {} }));
}
// ---------------------------------------------------------------------------
@@ -79,11 +84,11 @@ async function mockAppApis(page: Page) {
// 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.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 });
await page.waitForSelector(".mantine-Modal-overlay", { state: "hidden", timeout: 10000 });
}
// ---------------------------------------------------------------------------
@@ -102,83 +107,80 @@ async function navigateToConvert(page: Page) {
// 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("convert-to-dropdown").click();
await page.getByTestId(`format-option-${toValue}`).click();
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe('Convert Tool', () => {
test.describe("Convert Tool", () => {
test.beforeEach(async ({ page }) => {
await mockAppApis(page);
await page.goto('/?bypassOnboarding=true');
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 }) => {
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');
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 }) => {
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),
]);
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) =>
await page.route("**/api/v1/convert/pdf/img", (route) =>
route.fulfill({
status: 200,
contentType: 'image/png',
headers: { 'Content-Disposition': 'attachment; filename="sample.png"' },
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 selectToFormat(page, "png");
await page.getByTestId("convert-button").click();
await expect(page.getByTestId('download-result-button')).toBeVisible({ timeout: 10000 });
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) =>
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',
})
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();
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 });
await expect(page.getByRole("alert").first()).toBeVisible({ timeout: 5000 });
});
test('convert button becomes enabled after selecting a valid TO format', async ({ page }) => {
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');
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 selectToFormat(page, "png");
await expect(convertBtn).toBeEnabled({ timeout: 3000 });
});
});
File diff suppressed because it is too large Load Diff
@@ -3,37 +3,37 @@
* Tests the complete flow from file upload through auto-detection to API calls
*/
import React from 'react';
import { describe, test, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useConvertOperation } from '@app/hooks/tools/convert/useConvertOperation';
import { useConvertParameters } from '@app/hooks/tools/convert/useConvertParameters';
import { FileContextProvider } from '@app/contexts/FileContext';
import { NavigationProvider } from '@app/contexts/NavigationContext';
import { ToolRegistryProvider } from '@app/contexts/ToolRegistryProvider';
import { PreferencesProvider } from '@app/contexts/PreferencesContext';
import { I18nextProvider } from 'react-i18next';
import i18n from '@app/i18n/config';
import { detectFileExtension } from '@app/utils/fileUtils';
import { FIT_OPTIONS } from '@app/constants/convertConstants';
import { createTestStirlingFile, createTestFilesWithId } from '@app/tests/utils/testFileHelpers';
import { MantineProvider } from '@mantine/core';
import React from "react";
import { describe, test, expect, vi, beforeEach, afterEach, Mock } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useConvertOperation } from "@app/hooks/tools/convert/useConvertOperation";
import { useConvertParameters } from "@app/hooks/tools/convert/useConvertParameters";
import { FileContextProvider } from "@app/contexts/FileContext";
import { NavigationProvider } from "@app/contexts/NavigationContext";
import { ToolRegistryProvider } from "@app/contexts/ToolRegistryProvider";
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
import { I18nextProvider } from "react-i18next";
import i18n from "@app/i18n/config";
import { detectFileExtension } from "@app/utils/fileUtils";
import { FIT_OPTIONS } from "@app/constants/convertConstants";
import { createTestStirlingFile, createTestFilesWithId } from "@app/tests/utils/testFileHelpers";
import { MantineProvider } from "@mantine/core";
// Mock axios (for static methods like CancelToken, isCancel)
vi.mock('axios', () => ({
vi.mock("axios", () => ({
default: {
CancelToken: {
source: vi.fn(() => ({
token: 'mock-cancel-token',
cancel: vi.fn()
}))
token: "mock-cancel-token",
cancel: vi.fn(),
})),
},
isCancel: vi.fn(() => false),
}
},
}));
// Mock our apiClient service
vi.mock('../../services/apiClient', () => ({
vi.mock("../../services/apiClient", () => ({
default: {
post: vi.fn(),
get: vi.fn(),
@@ -41,18 +41,18 @@ vi.mock('../../services/apiClient', () => ({
delete: vi.fn(),
interceptors: {
response: {
use: vi.fn()
}
}
}
use: vi.fn(),
},
},
},
}));
// Import the mocked apiClient
import apiClient from '@app/services/apiClient';
import apiClient from "@app/services/apiClient";
const mockedApiClient = vi.mocked(apiClient);
// Mock only essential services that are actually called by the tests
vi.mock('../../services/fileStorage', () => ({
vi.mock("../../services/fileStorage", () => ({
fileStorage: {
init: vi.fn().mockResolvedValue(undefined),
storeFile: vi.fn().mockImplementation((file, thumbnail) => {
@@ -62,20 +62,20 @@ vi.mock('../../services/fileStorage', () => ({
size: file.size,
type: file.type,
lastModified: file.lastModified,
thumbnail: thumbnail
thumbnail: thumbnail,
});
}),
getAllFileMetadata: vi.fn().mockResolvedValue([]),
cleanup: vi.fn().mockResolvedValue(undefined)
}
cleanup: vi.fn().mockResolvedValue(undefined),
},
}));
vi.mock('../../services/thumbnailGenerationService', () => ({
vi.mock("../../services/thumbnailGenerationService", () => ({
thumbnailGenerationService: {
generateThumbnail: vi.fn().mockResolvedValue('data:image/png;base64,fake-thumbnail'),
generateThumbnail: vi.fn().mockResolvedValue("data:image/png;base64,fake-thumbnail"),
cleanup: vi.fn(),
destroy: vi.fn()
}
destroy: vi.fn(),
},
}));
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
@@ -84,9 +84,7 @@ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<PreferencesProvider>
<ToolRegistryProvider>
<NavigationProvider>
<FileContextProvider>
{children}
</FileContextProvider>
<FileContextProvider>{children}</FileContextProvider>
</NavigationProvider>
</ToolRegistryProvider>
</PreferencesProvider>
@@ -94,14 +92,13 @@ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
</MantineProvider>
);
describe('Convert Tool - Smart Detection Integration Tests', () => {
describe("Convert Tool - Smart Detection Integration Tests", () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock successful API response
(mockedApiClient.post as Mock).mockResolvedValue({
data: new Blob(['fake converted content'], { type: 'application/pdf' })
data: new Blob(["fake converted content"], { type: "application/pdf" }),
});
});
@@ -110,18 +107,22 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
vi.restoreAllMocks();
});
describe('Single File Auto-Detection Flow', () => {
test('should auto-detect PDF from DOCX and convert to PDF', async () => {
describe("Single File Auto-Detection Flow", () => {
test("should auto-detect PDF from DOCX and convert to PDF", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
// Create mock DOCX file
const docxFile = createTestStirlingFile('document.docx', 'docx content', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
const docxFile = createTestStirlingFile(
"document.docx",
"docx content",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
);
// Test auto-detection
act(() => {
@@ -129,35 +130,32 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
await waitFor(() => {
expect(paramsResult.current.parameters.fromExtension).toBe('docx');
expect(paramsResult.current.parameters.toExtension).toBe('pdf');
expect(paramsResult.current.parameters.fromExtension).toBe("docx");
expect(paramsResult.current.parameters.toExtension).toBe("pdf");
expect(paramsResult.current.parameters.isSmartDetection).toBe(false);
});
// Test conversion operation
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
[docxFile]
);
await operationResult.current.executeOperation(paramsResult.current.parameters, [docxFile]);
});
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
responseType: 'blob'
expect(mockedApiClient.post).toHaveBeenCalledWith("/api/v1/convert/file/pdf", expect.any(FormData), {
responseType: "blob",
});
});
test('should handle unknown file type with file-to-pdf fallback', async () => {
test("should handle unknown file type with file-to-pdf fallback", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
// Create mock unknown file
const unknownFile = createTestStirlingFile('document.xyz', 'unknown content', 'application/octet-stream');
const unknownFile = createTestStirlingFile("document.xyz", "unknown content", "application/octet-stream");
// Test auto-detection
act(() => {
@@ -165,41 +163,37 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
await waitFor(() => {
expect(paramsResult.current.parameters.fromExtension).toBe('file-xyz');
expect(paramsResult.current.parameters.toExtension).toBe('pdf'); // Fallback
expect(paramsResult.current.parameters.fromExtension).toBe("file-xyz");
expect(paramsResult.current.parameters.toExtension).toBe("pdf"); // Fallback
expect(paramsResult.current.parameters.isSmartDetection).toBe(false);
});
// Test conversion operation
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
[unknownFile]
);
await operationResult.current.executeOperation(paramsResult.current.parameters, [unknownFile]);
});
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
responseType: 'blob'
expect(mockedApiClient.post).toHaveBeenCalledWith("/api/v1/convert/file/pdf", expect.any(FormData), {
responseType: "blob",
});
});
});
describe('Multi-File Smart Detection Flow', () => {
test('should detect all images and use img-to-pdf endpoint', async () => {
describe("Multi-File Smart Detection Flow", () => {
test("should detect all images and use img-to-pdf endpoint", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
// Create mock image files
const imageFiles = createTestFilesWithId([
{ name: 'photo1.jpg', content: 'jpg content', type: 'image/jpeg' },
{ name: 'photo2.png', content: 'png content', type: 'image/png' },
{ name: 'photo3.gif', content: 'gif content', type: 'image/gif' }
{ name: "photo1.jpg", content: "jpg content", type: "image/jpeg" },
{ name: "photo2.png", content: "png content", type: "image/png" },
{ name: "photo3.gif", content: "gif content", type: "image/gif" },
]);
// Test smart detection for all images
@@ -208,44 +202,49 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
await waitFor(() => {
expect(paramsResult.current.parameters.fromExtension).toBe('image');
expect(paramsResult.current.parameters.toExtension).toBe('pdf');
expect(paramsResult.current.parameters.fromExtension).toBe("image");
expect(paramsResult.current.parameters.toExtension).toBe("pdf");
expect(paramsResult.current.parameters.isSmartDetection).toBe(true);
expect(paramsResult.current.parameters.smartDetectionType).toBe('images');
expect(paramsResult.current.parameters.smartDetectionType).toBe("images");
});
// Test conversion operation
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
imageFiles
);
await operationResult.current.executeOperation(paramsResult.current.parameters, imageFiles);
});
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/img/pdf', expect.any(FormData), {
responseType: 'blob'
expect(mockedApiClient.post).toHaveBeenCalledWith("/api/v1/convert/img/pdf", expect.any(FormData), {
responseType: "blob",
});
// Should send all files in single request
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
const files = formData.getAll('fileInput');
const files = formData.getAll("fileInput");
expect(files).toHaveLength(3);
});
test('should detect mixed file types and use file-to-pdf endpoint', async () => {
test("should detect mixed file types and use file-to-pdf endpoint", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
// Create mixed file types
const mixedFiles = createTestFilesWithId([
{ name: 'document.pdf', content: 'pdf content', type: 'application/pdf' },
{ name: 'spreadsheet.xlsx', content: 'docx content', type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
{ name: 'presentation.pptx', content: 'pptx content', type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }
{ name: "document.pdf", content: "pdf content", type: "application/pdf" },
{
name: "spreadsheet.xlsx",
content: "docx content",
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
{
name: "presentation.pptx",
content: "pptx content",
type: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
},
]);
// Test smart detection for mixed types
@@ -254,38 +253,35 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
await waitFor(() => {
expect(paramsResult.current.parameters.fromExtension).toBe('any');
expect(paramsResult.current.parameters.toExtension).toBe('pdf');
expect(paramsResult.current.parameters.fromExtension).toBe("any");
expect(paramsResult.current.parameters.toExtension).toBe("pdf");
expect(paramsResult.current.parameters.isSmartDetection).toBe(true);
expect(paramsResult.current.parameters.smartDetectionType).toBe('mixed');
expect(paramsResult.current.parameters.smartDetectionType).toBe("mixed");
});
// Test conversion operation
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
mixedFiles
);
await operationResult.current.executeOperation(paramsResult.current.parameters, mixedFiles);
});
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), {
responseType: 'blob'
expect(mockedApiClient.post).toHaveBeenCalledWith("/api/v1/convert/file/pdf", expect.any(FormData), {
responseType: "blob",
});
});
test('should detect all web files and use html-to-pdf endpoint', async () => {
test("should detect all web files and use html-to-pdf endpoint", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
// Create mock web files
const webFiles = createTestFilesWithId([
{ name: 'page1.html', content: '<html>content</html>', type: 'text/html' },
{ name: 'site.zip', content: 'zip content', type: 'application/zip' }
{ name: "page1.html", content: "<html>content</html>", type: "text/html" },
{ name: "site.zip", content: "zip content", type: "application/zip" },
]);
// Test smart detection for web files
@@ -294,22 +290,19 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
await waitFor(() => {
expect(paramsResult.current.parameters.fromExtension).toBe('html');
expect(paramsResult.current.parameters.toExtension).toBe('pdf');
expect(paramsResult.current.parameters.fromExtension).toBe("html");
expect(paramsResult.current.parameters.toExtension).toBe("pdf");
expect(paramsResult.current.parameters.isSmartDetection).toBe(true);
expect(paramsResult.current.parameters.smartDetectionType).toBe('web');
expect(paramsResult.current.parameters.smartDetectionType).toBe("web");
});
// Test conversion operation
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
webFiles
);
await operationResult.current.executeOperation(paramsResult.current.parameters, webFiles);
});
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/html/pdf', expect.any(FormData), {
responseType: 'blob'
expect(mockedApiClient.post).toHaveBeenCalledWith("/api/v1/convert/html/pdf", expect.any(FormData), {
responseType: "blob",
});
// Should process files separately for web files
@@ -317,182 +310,165 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
});
describe('Web and Email Conversion Options Integration', () => {
test('should send correct HTML parameters for web-to-pdf conversion', async () => {
describe("Web and Email Conversion Options Integration", () => {
test("should send correct HTML parameters for web-to-pdf conversion", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const htmlFile = createTestStirlingFile('page.html', '<html>content</html>', 'text/html');
const htmlFile = createTestStirlingFile("page.html", "<html>content</html>", "text/html");
// Set up HTML conversion parameters
act(() => {
paramsResult.current.analyzeFileTypes([htmlFile]);
paramsResult.current.updateParameter('htmlOptions', {
zoomLevel: 1.5
paramsResult.current.updateParameter("htmlOptions", {
zoomLevel: 1.5,
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
[htmlFile]
);
await operationResult.current.executeOperation(paramsResult.current.parameters, [htmlFile]);
});
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('zoom')).toBe('1.5');
expect(formData.get("zoom")).toBe("1.5");
});
test('should send correct email parameters for eml-to-pdf conversion', async () => {
test("should send correct email parameters for eml-to-pdf conversion", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const emlFile = createTestStirlingFile('email.eml', 'email content', 'message/rfc822');
const emlFile = createTestStirlingFile("email.eml", "email content", "message/rfc822");
// Set up email conversion parameters
act(() => {
paramsResult.current.updateParameter('fromExtension', 'eml');
paramsResult.current.updateParameter('toExtension', 'pdf');
paramsResult.current.updateParameter('emailOptions', {
paramsResult.current.updateParameter("fromExtension", "eml");
paramsResult.current.updateParameter("toExtension", "pdf");
paramsResult.current.updateParameter("emailOptions", {
includeAttachments: false,
maxAttachmentSizeMB: 20,
downloadHtml: true,
includeAllRecipients: true
includeAllRecipients: true,
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
[emlFile]
);
await operationResult.current.executeOperation(paramsResult.current.parameters, [emlFile]);
});
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('includeAttachments')).toBe('false');
expect(formData.get('maxAttachmentSizeMB')).toBe('20');
expect(formData.get('downloadHtml')).toBe('true');
expect(formData.get('includeAllRecipients')).toBe('true');
expect(formData.get("includeAttachments")).toBe("false");
expect(formData.get("maxAttachmentSizeMB")).toBe("20");
expect(formData.get("downloadHtml")).toBe("true");
expect(formData.get("includeAllRecipients")).toBe("true");
});
test('should send correct PDF/A parameters for pdf-to-pdfa conversion', async () => {
test("should send correct PDF/A parameters for pdf-to-pdfa conversion", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const pdfFile = createTestStirlingFile('document.pdf', 'pdf content', 'application/pdf');
const pdfFile = createTestStirlingFile("document.pdf", "pdf content", "application/pdf");
// Set up PDF/A conversion parameters
act(() => {
paramsResult.current.updateParameter('fromExtension', 'pdf');
paramsResult.current.updateParameter('toExtension', 'pdfa');
paramsResult.current.updateParameter('pdfaOptions', {
outputFormat: 'pdfa',
strict: false
paramsResult.current.updateParameter("fromExtension", "pdf");
paramsResult.current.updateParameter("toExtension", "pdfa");
paramsResult.current.updateParameter("pdfaOptions", {
outputFormat: "pdfa",
strict: false,
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
[pdfFile]
);
await operationResult.current.executeOperation(paramsResult.current.parameters, [pdfFile]);
});
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('outputFormat')).toBe('pdfa');
expect(formData.get('strict')).toBe('false');
expect(mockedApiClient.post).toHaveBeenCalledWith('/api/v1/convert/pdf/pdfa', expect.any(FormData), {
responseType: 'blob'
expect(formData.get("outputFormat")).toBe("pdfa");
expect(formData.get("strict")).toBe("false");
expect(mockedApiClient.post).toHaveBeenCalledWith("/api/v1/convert/pdf/pdfa", expect.any(FormData), {
responseType: "blob",
});
});
});
describe('Image Conversion Options Integration', () => {
test('should send correct parameters for image-to-pdf conversion', async () => {
describe("Image Conversion Options Integration", () => {
test("should send correct parameters for image-to-pdf conversion", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const imageFiles = createTestFilesWithId([
{ name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' },
{ name: 'photo2.jpg', content: 'jpg2', type: 'image/jpeg' }
{ name: "photo1.jpg", content: "jpg1", type: "image/jpeg" },
{ name: "photo2.jpg", content: "jpg2", type: "image/jpeg" },
]);
// Set up image conversion parameters
act(() => {
paramsResult.current.analyzeFileTypes(imageFiles);
paramsResult.current.updateParameter('imageOptions', {
colorType: 'grayscale',
paramsResult.current.updateParameter("imageOptions", {
colorType: "grayscale",
dpi: 150,
singleOrMultiple: 'single',
singleOrMultiple: "single",
fitOption: FIT_OPTIONS.FIT_PAGE,
autoRotate: false,
combineImages: true
combineImages: true,
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
imageFiles
);
await operationResult.current.executeOperation(paramsResult.current.parameters, imageFiles);
});
const formData = (mockedApiClient.post as Mock).mock.calls[0][1] as FormData;
expect(formData.get('fitOption')).toBe(FIT_OPTIONS.FIT_PAGE);
expect(formData.get('colorType')).toBe('grayscale');
expect(formData.get('autoRotate')).toBe('false');
expect(formData.get("fitOption")).toBe(FIT_OPTIONS.FIT_PAGE);
expect(formData.get("colorType")).toBe("grayscale");
expect(formData.get("autoRotate")).toBe("false");
});
test('should process images separately when combineImages is false', async () => {
test("should process images separately when combineImages is false", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const imageFiles = createTestFilesWithId([
{ name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' },
{ name: 'photo2.jpg', content: 'jpg2', type: 'image/jpeg' }
{ name: "photo1.jpg", content: "jpg1", type: "image/jpeg" },
{ name: "photo2.jpg", content: "jpg2", type: "image/jpeg" },
]);
// Set up for separate processing
act(() => {
paramsResult.current.analyzeFileTypes(imageFiles);
paramsResult.current.updateParameter('imageOptions', {
paramsResult.current.updateParameter("imageOptions", {
...paramsResult.current.parameters.imageOptions,
combineImages: false
combineImages: false,
});
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
imageFiles
);
await operationResult.current.executeOperation(paramsResult.current.parameters, imageFiles);
});
// Should make separate API calls for each file
@@ -500,28 +476,26 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
});
describe('Error Scenarios in Smart Detection', () => {
test('should handle partial failures in multi-file processing', async () => {
describe("Error Scenarios in Smart Detection", () => {
test("should handle partial failures in multi-file processing", async () => {
const { result: paramsResult } = renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const { result: operationResult } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
// Mock one success, one failure
(mockedApiClient.post as Mock)
.mockResolvedValueOnce({
data: new Blob(['converted1'], { type: 'application/pdf' })
data: new Blob(["converted1"], { type: "application/pdf" }),
})
.mockRejectedValueOnce(new Error('File 2 failed'));
.mockRejectedValueOnce(new Error("File 2 failed"));
const mixedFiles = createTestFilesWithId([
{ name: 'doc1.txt', content: 'file1', type: 'text/plain' },
{ name: 'doc2.xyz', content: 'file2', type: 'application/octet-stream' }
{ name: "doc1.txt", content: "file1", type: "text/plain" },
{ name: "doc2.xyz", content: "file2", type: "application/octet-stream" },
]);
// Set up for separate processing (mixed smart detection)
@@ -530,10 +504,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
await act(async () => {
await operationResult.current.executeOperation(
paramsResult.current.parameters,
mixedFiles
);
await operationResult.current.executeOperation(paramsResult.current.parameters, mixedFiles);
});
await waitFor(() => {
@@ -544,21 +515,20 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
});
});
describe('Real File Extension Detection', () => {
test('should correctly detect various file extensions', async () => {
describe("Real File Extension Detection", () => {
test("should correctly detect various file extensions", async () => {
renderHook(() => useConvertParameters(), {
wrapper: TestWrapper
wrapper: TestWrapper,
});
const testCases = [
{ filename: 'document.PDF', expected: 'pdf' },
{ filename: 'image.JPEG', expected: 'jpg' }, // JPEG should normalize to jpg
{ filename: 'photo.jpeg', expected: 'jpg' }, // jpeg should normalize to jpg
{ filename: 'archive.tar.gz', expected: 'gz' },
{ filename: 'file.', expected: '' },
{ filename: '.hidden', expected: 'hidden' },
{ filename: 'noextension', expected: '' }
{ filename: "document.PDF", expected: "pdf" },
{ filename: "image.JPEG", expected: "jpg" }, // JPEG should normalize to jpg
{ filename: "photo.jpeg", expected: "jpg" }, // jpeg should normalize to jpg
{ filename: "archive.tar.gz", expected: "gz" },
{ filename: "file.", expected: "" },
{ filename: ".hidden", expected: "hidden" },
{ filename: "noextension", expected: "" },
];
testCases.forEach(({ filename, expected }) => {
@@ -1,23 +1,15 @@
import fs from 'fs';
import path from 'path';
import ts from 'typescript';
import { describe, expect, test } from 'vitest';
import { parse } from 'smol-toml';
import fs from "fs";
import path from "path";
import ts from "typescript";
import { describe, expect, test } from "vitest";
import { parse } from "smol-toml";
const REPO_ROOT = path.join(__dirname, '../../../..');
const SRC_ROOT = path.join(__dirname, '../..');
const EN_GB_FILE = path.join(__dirname, '../../../public/locales/en-GB/translation.toml');
const REPO_ROOT = path.join(__dirname, "../../../..");
const SRC_ROOT = path.join(__dirname, "../..");
const EN_GB_FILE = path.join(__dirname, "../../../public/locales/en-GB/translation.toml");
const IGNORED_DIRS = new Set([
'tests',
'__mocks__',
]);
const IGNORED_FILE_PATTERNS = [
/\.d\.ts$/,
/\.test\./,
/\.spec\./,
/\.stories\./,
];
const IGNORED_DIRS = new Set(["tests", "__mocks__"]);
const IGNORED_FILE_PATTERNS = [/\.d\.ts$/, /\.test\./, /\.spec\./, /\.stories\./];
const IGNORED_KEYS = new Set<string>([
// If the script has found a false-positive that shouldn't be in the translations, include it here
]);
@@ -30,8 +22,8 @@ type FoundKey = {
column: number;
};
const flattenKeys = (node: unknown, prefix = '', acc = new Set<string>()): Set<string> => {
if (!node || typeof node !== 'object' || Array.isArray(node)) {
const flattenKeys = (node: unknown, prefix = "", acc = new Set<string>()): Set<string> => {
if (!node || typeof node !== "object" || Array.isArray(node)) {
if (prefix) {
acc.add(prefix);
}
@@ -47,9 +39,7 @@ const flattenKeys = (node: unknown, prefix = '', acc = new Set<string>()): Set<s
};
const listSourceFiles = (): string[] => {
const files = ts.sys.readDirectory(SRC_ROOT, ['.ts', '.tsx', '.js', '.jsx'], undefined, [
'**/*',
]);
const files = ts.sys.readDirectory(SRC_ROOT, [".ts", ".tsx", ".js", ".jsx"], undefined, ["**/*"]);
return files
.filter((file) => !file.split(path.sep).some((segment) => IGNORED_DIRS.has(segment)))
@@ -57,15 +47,15 @@ const listSourceFiles = (): string[] => {
};
const getScriptKind = (file: string): ts.ScriptKind => {
if (file.endsWith('.tsx')) {
if (file.endsWith(".tsx")) {
return ts.ScriptKind.TSX;
}
if (file.endsWith('.ts')) {
if (file.endsWith(".ts")) {
return ts.ScriptKind.TS;
}
if (file.endsWith('.jsx')) {
if (file.endsWith(".jsx")) {
return ts.ScriptKind.JSX;
}
@@ -77,14 +67,8 @@ const getScriptKind = (file: string): ts.ScriptKind => {
* Ignores dynamic strings because we can't know what the actual translation key will be.
*/
const extractKeys = (file: string): FoundKey[] => {
const code = fs.readFileSync(file, 'utf8');
const sourceFile = ts.createSourceFile(
file,
code,
ts.ScriptTarget.Latest,
true,
getScriptKind(file),
);
const code = fs.readFileSync(file, "utf8");
const sourceFile = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true, getScriptKind(file));
const found: FoundKey[] = [];
@@ -100,8 +84,8 @@ const extractKeys = (file: string): FoundKey[] => {
const arg1 = node.arguments.at(1);
const isT =
(ts.isIdentifier(callee) && callee.text === 't') ||
(ts.isPropertyAccessExpression(callee) && callee.name.text === 't');
(ts.isIdentifier(callee) && callee.text === "t") ||
(ts.isPropertyAccessExpression(callee) && callee.name.text === "t");
if (isT && arg0 && (ts.isStringLiteral(arg0) || ts.isNoSubstitutionTemplateLiteral(arg0))) {
let arg1Text: string = "";
@@ -114,11 +98,7 @@ const extractKeys = (file: string): FoundKey[] => {
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
for (const attr of node.attributes.properties) {
if (
!ts.isJsxAttribute(attr) ||
attr.name.getText(sourceFile) !== 'i18nKey' ||
!attr.initializer
) {
if (!ts.isJsxAttribute(attr) || attr.name.getText(sourceFile) !== "i18nKey" || !attr.initializer) {
continue;
}
@@ -129,11 +109,7 @@ const extractKeys = (file: string): FoundKey[] => {
continue;
}
if (
ts.isJsxExpression(init) &&
init.expression &&
ts.isStringLiteral(init.expression)
) {
if (ts.isJsxExpression(init) && init.expression && ts.isStringLiteral(init.expression)) {
record(init.expression, init.expression.text);
}
}
@@ -146,11 +122,11 @@ const extractKeys = (file: string): FoundKey[] => {
return found;
};
describe('Missing translation coverage', () => {
test('fails if any en-GB translation key used in source is missing', { timeout: 10000 }, () => {
describe("Missing translation coverage", () => {
test("fails if any en-GB translation key used in source is missing", { timeout: 10000 }, () => {
expect(fs.existsSync(EN_GB_FILE)).toBe(true);
const localeContent = fs.readFileSync(EN_GB_FILE, 'utf8');
const localeContent = fs.readFileSync(EN_GB_FILE, "utf8");
const enGb = parse(localeContent);
const availableKeys = flattenKeys(enGb);
@@ -163,7 +139,7 @@ describe('Missing translation coverage', () => {
const annotations = missingKeys.map(({ key, fallback, file, line, column }) => {
const workspaceRelativeRaw = path.relative(REPO_ROOT, file);
const workspaceRelativeFile = workspaceRelativeRaw.replace(/\\/g, '/');
const workspaceRelativeFile = workspaceRelativeRaw.replace(/\\/g, "/");
return {
key,
@@ -186,7 +162,7 @@ describe('Missing translation coverage', () => {
key,
fallback,
location: `${file}:${line}:${column}`,
}
};
});
expect(neatened).toEqual([]);
+118 -114
View File
@@ -1,125 +1,129 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test HTML Document</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 40px;
color: #333;
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
h2 {
color: #34495e;
margin-top: 30px;
}
table {
border-collapse: collapse;
width: 100%;
margin: 20px 0;
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
.highlight {
background-color: #fff3cd;
padding: 10px;
border-left: 4px solid #ffc107;
margin: 20px 0;
}
code {
background-color: #f8f9fa;
padding: 2px 4px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
</style>
</head>
<body>
<h1>Test HTML Document for Convert Tool</h1>
<p>This is a <strong>test HTML file</strong> for testing the HTML to PDF conversion functionality. It contains various HTML elements to ensure proper conversion.</p>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Test HTML Document</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 40px;
color: #333;
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
h2 {
color: #34495e;
margin-top: 30px;
}
table {
border-collapse: collapse;
width: 100%;
margin: 20px 0;
}
th,
td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
.highlight {
background-color: #fff3cd;
padding: 10px;
border-left: 4px solid #ffc107;
margin: 20px 0;
}
code {
background-color: #f8f9fa;
padding: 2px 4px;
border-radius: 4px;
font-family: "Courier New", monospace;
}
</style>
</head>
<body>
<h1>Test HTML Document for Convert Tool</h1>
<h2>Text Formatting</h2>
<p>This paragraph contains <strong>bold text</strong>, <em>italic text</em>, and <code>inline code</code>.</p>
<div class="highlight">
<p><strong>Important:</strong> This is a highlighted section that should be preserved in the PDF output.</p>
</div>
<p>
This is a <strong>test HTML file</strong> for testing the HTML to PDF conversion functionality. It contains various
HTML elements to ensure proper conversion.
</p>
<h2>Lists</h2>
<h3>Unordered List</h3>
<ul>
<li>First item</li>
<li>Second item with <a href="https://example.com">a link</a></li>
<li>Third item</li>
</ul>
<h2>Text Formatting</h2>
<p>This paragraph contains <strong>bold text</strong>, <em>italic text</em>, and <code>inline code</code>.</p>
<h3>Ordered List</h3>
<ol>
<li>Primary point</li>
<li>Secondary point</li>
<li>Tertiary point</li>
</ol>
<div class="highlight">
<p><strong>Important:</strong> This is a highlighted section that should be preserved in the PDF output.</p>
</div>
<h2>Table</h2>
<table>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data A</td>
<td>Data B</td>
<td>Data C</td>
</tr>
<tr>
<td>Test 1</td>
<td>Test 2</td>
<td>Test 3</td>
</tr>
<tr>
<td>Sample X</td>
<td>Sample Y</td>
<td>Sample Z</td>
</tr>
</tbody>
</table>
<h2>Lists</h2>
<h3>Unordered List</h3>
<ul>
<li>First item</li>
<li>Second item with <a href="https://example.com">a link</a></li>
<li>Third item</li>
</ul>
<h2>Code Block</h2>
<pre><code>function testFunction() {
<h3>Ordered List</h3>
<ol>
<li>Primary point</li>
<li>Secondary point</li>
<li>Tertiary point</li>
</ol>
<h2>Table</h2>
<table>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data A</td>
<td>Data B</td>
<td>Data C</td>
</tr>
<tr>
<td>Test 1</td>
<td>Test 2</td>
<td>Test 3</td>
</tr>
<tr>
<td>Sample X</td>
<td>Sample Y</td>
<td>Sample Z</td>
</tr>
</tbody>
</table>
<h2>Code Block</h2>
<pre><code>function testFunction() {
console.log("This is a test function");
return "Hello from HTML to PDF conversion";
}</code></pre>
<h2>Final Notes</h2>
<p>This HTML document should convert to a well-formatted PDF that preserves:</p>
<ul>
<li>Text formatting (bold, italic)</li>
<li>Headings and hierarchy</li>
<li>Tables with proper borders</li>
<li>Lists (ordered and unordered)</li>
<li>Code formatting</li>
<li>Basic CSS styling</li>
</ul>
<h2>Final Notes</h2>
<p>This HTML document should convert to a well-formatted PDF that preserves:</p>
<ul>
<li>Text formatting (bold, italic)</li>
<li>Headings and hierarchy</li>
<li>Tables with proper borders</li>
<li>Lists (ordered and unordered)</li>
<li>Code formatting</li>
<li>Basic CSS styling</li>
</ul>
<p><small>Generated for Stirling PDF Convert Tool testing purposes.</small></p>
</body>
</html>
<p><small>Generated for Stirling PDF Convert Tool testing purposes.</small></p>
</body>
</html>
@@ -1,106 +1,110 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Test HTML Document</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 40px;
color: #333;
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
h2 {
color: #34495e;
margin-top: 30px;
}
table {
border-collapse: collapse;
width: 100%;
margin: 20px 0;
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
.highlight {
background-color: #fff3cd;
padding: 10px;
border-left: 4px solid #ffc107;
margin: 20px 0;
}
code {
background-color: #f8f9fa;
padding: 2px 4px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 40px;
color: #333;
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
h2 {
color: #34495e;
margin-top: 30px;
}
table {
border-collapse: collapse;
width: 100%;
margin: 20px 0;
}
th,
td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
.highlight {
background-color: #fff3cd;
padding: 10px;
border-left: 4px solid #ffc107;
margin: 20px 0;
}
code {
background-color: #f8f9fa;
padding: 2px 4px;
border-radius: 4px;
font-family: "Courier New", monospace;
}
</style>
</head>
<body>
</head>
<body>
<h1>Test HTML Document for Convert Tool</h1>
<p>This is a <strong>test HTML file</strong> for testing the HTML to PDF conversion functionality. It contains various HTML elements to ensure proper conversion.</p>
<p>
This is a <strong>test HTML file</strong> for testing the HTML to PDF conversion functionality. It contains various HTML
elements to ensure proper conversion.
</p>
<h2>Text Formatting</h2>
<p>This paragraph contains <strong>bold text</strong>, <em>italic text</em>, and <code>inline code</code>.</p>
<div class="highlight">
<p><strong>Important:</strong> This is a highlighted section that should be preserved in the PDF output.</p>
<p><strong>Important:</strong> This is a highlighted section that should be preserved in the PDF output.</p>
</div>
<h2>Lists</h2>
<h3>Unordered List</h3>
<ul>
<li>First item</li>
<li>Second item with <a href="https://example.com">a link</a></li>
<li>Third item</li>
<li>First item</li>
<li>Second item with <a href="https://example.com">a link</a></li>
<li>Third item</li>
</ul>
<h3>Ordered List</h3>
<ol>
<li>Primary point</li>
<li>Secondary point</li>
<li>Tertiary point</li>
<li>Primary point</li>
<li>Secondary point</li>
<li>Tertiary point</li>
</ol>
<h2>Table</h2>
<table>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data A</td>
<td>Data B</td>
<td>Data C</td>
</tr>
<tr>
<td>Test 1</td>
<td>Test 2</td>
<td>Test 3</td>
</tr>
<tr>
<td>Sample X</td>
<td>Sample Y</td>
<td>Sample Z</td>
</tr>
</tbody>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data A</td>
<td>Data B</td>
<td>Data C</td>
</tr>
<tr>
<td>Test 1</td>
<td>Test 2</td>
<td>Test 3</td>
</tr>
<tr>
<td>Sample X</td>
<td>Sample Y</td>
<td>Sample Z</td>
</tr>
</tbody>
</table>
<h2>Code Block</h2>
@@ -112,14 +116,14 @@
<h2>Final Notes</h2>
<p>This HTML document should convert to a well-formatted PDF that preserves:</p>
<ul>
<li>Text formatting (bold, italic)</li>
<li>Headings and hierarchy</li>
<li>Tables with proper borders</li>
<li>Lists (ordered and unordered)</li>
<li>Code formatting</li>
<li>Basic CSS styling</li>
<li>Text formatting (bold, italic)</li>
<li>Headings and hierarchy</li>
<li>Tables with proper borders</li>
<li>Lists (ordered and unordered)</li>
<li>Code formatting</li>
<li>Basic CSS styling</li>
</ul>
<p><small>Generated for Stirling PDF Convert Tool testing purposes.</small></p>
</body>
</html>
</body>
</html>
+17 -16
View File
@@ -1,9 +1,9 @@
import { describe, test, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
import { parse } from 'smol-toml';
import { describe, test, expect } from "vitest";
import fs from "fs";
import path from "path";
import { parse } from "smol-toml";
const LOCALES_DIR = path.join(__dirname, '../../../public/locales');
const LOCALES_DIR = path.join(__dirname, "../../../public/locales");
// Get all locale directories for parameterized tests
const getLocaleDirectories = () => {
@@ -11,31 +11,32 @@ const getLocaleDirectories = () => {
return [];
}
return fs.readdirSync(LOCALES_DIR, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
return fs
.readdirSync(LOCALES_DIR, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
};
const localeDirectories = getLocaleDirectories();
describe('Translation TOML Validation', () => {
test('should find the locales directory', () => {
describe("Translation TOML Validation", () => {
test("should find the locales directory", () => {
expect(fs.existsSync(LOCALES_DIR)).toBe(true);
});
test('should have at least one locale directory', () => {
test("should have at least one locale directory", () => {
expect(localeDirectories.length).toBeGreaterThan(0);
});
test.each(localeDirectories)('should have valid TOML in %s/translation.toml', (localeDir) => {
const translationFile = path.join(LOCALES_DIR, localeDir, 'translation.toml');
test.each(localeDirectories)("should have valid TOML in %s/translation.toml", (localeDir) => {
const translationFile = path.join(LOCALES_DIR, localeDir, "translation.toml");
// Check if file exists
expect(fs.existsSync(translationFile)).toBe(true);
// Read file content
const content = fs.readFileSync(translationFile, 'utf8');
expect(content.trim()).not.toBe('');
const content = fs.readFileSync(translationFile, "utf8");
expect(content.trim()).not.toBe("");
// Parse TOML - this will throw if invalid TOML
let tomlData;
@@ -44,7 +45,7 @@ describe('Translation TOML Validation', () => {
}).not.toThrow();
// Ensure it's an object at root level
expect(typeof tomlData).toBe('object');
expect(typeof tomlData).toBe("object");
expect(tomlData).not.toBeNull();
expect(Array.isArray(tomlData)).toBe(false);
});
@@ -1,29 +1,30 @@
import { describe, test, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
import { parse } from 'smol-toml';
import { describe, test, expect } from "vitest";
import fs from "fs";
import path from "path";
import { parse } from "smol-toml";
const LOCALES_DIR = path.join(__dirname, '../../../public/locales');
const LOCALES_DIR = path.join(__dirname, "../../../public/locales");
const getLocaleDirectories = () => {
if (!fs.existsSync(LOCALES_DIR)) {
return [];
}
return fs.readdirSync(LOCALES_DIR, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
return fs
.readdirSync(LOCALES_DIR, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
};
const findDottedKeys = (node: unknown, segments: string[] = []): string[] => {
if (!node || typeof node !== 'object') {
if (!node || typeof node !== "object") {
return [];
}
const issues: string[] = [];
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
if (key.includes('.')) {
issues.push([...segments, key].join('.'));
if (key.includes(".")) {
issues.push([...segments, key].join("."));
}
issues.push(...findDottedKeys(value, [...segments, key]));
}
@@ -32,21 +33,21 @@ const findDottedKeys = (node: unknown, segments: string[] = []): string[] => {
const localeDirectories = getLocaleDirectories();
describe('Translation key structure', () => {
test('should locate locales directory', () => {
describe("Translation key structure", () => {
test("should locate locales directory", () => {
expect(fs.existsSync(LOCALES_DIR)).toBe(true);
});
test('should have at least one locale directory', () => {
test("should have at least one locale directory", () => {
expect(localeDirectories.length).toBeGreaterThan(0);
});
test.each(localeDirectories)('should not contain dotted keys in %s/translation.toml', (localeDir) => {
const translationFile = path.join(LOCALES_DIR, localeDir, 'translation.toml');
test.each(localeDirectories)("should not contain dotted keys in %s/translation.toml", (localeDir) => {
const translationFile = path.join(LOCALES_DIR, localeDir, "translation.toml");
expect(fs.existsSync(translationFile)).toBe(true);
const data = parse(fs.readFileSync(translationFile, 'utf8'));
const data = parse(fs.readFileSync(translationFile, "utf8"));
const dottedKeys = findDottedKeys(data);
expect(dottedKeys, `Dotted keys found in ${localeDir}: ${dottedKeys.join(', ')}`).toHaveLength(0);
expect(dottedKeys, `Dotted keys found in ${localeDir}: ${dottedKeys.join(", ")}`).toHaveLength(0);
});
});
@@ -2,15 +2,15 @@
* Test utilities for creating StirlingFile objects in tests
*/
import { StirlingFile, createStirlingFile } from '@app/types/fileContext';
import { StirlingFile, createStirlingFile } from "@app/types/fileContext";
/**
* Create a StirlingFile object for testing purposes
*/
export function createTestStirlingFile(
name: string,
content: string = 'test content',
type: string = 'application/pdf'
content: string = "test content",
type: string = "application/pdf",
): StirlingFile {
const file = new File([content], name, { type });
return createStirlingFile(file);
@@ -19,10 +19,8 @@ export function createTestStirlingFile(
/**
* Create multiple StirlingFile objects for testing
*/
export function createTestFilesWithId(
files: Array<{ name: string; content?: string; type?: string }>
): StirlingFile[] {
return files.map(({ name, content = 'test content', type = 'application/pdf' }) =>
createTestStirlingFile(name, content, type)
export function createTestFilesWithId(files: Array<{ name: string; content?: string; type?: string }>): StirlingFile[] {
return files.map(({ name, content = "test content", type = "application/pdf" }) =>
createTestStirlingFile(name, content, type),
);
}
}