test: add unit tests for timestamp PDF feature

Add 31 tests across 3 test files covering:
- TSA preset configuration (uniqueness, valid URLs, known providers)
- Timestamp PDF page logic (file validation, output naming, UI state, drop zone)
- TimestampNode workflow node (category, icon, description, presets)
This commit is contained in:
InstalZDLL
2026-03-15 15:34:14 +01:00
parent 70f0834fc0
commit 1569b66d5c
3 changed files with 350 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
import { describe, it, expect, vi } from 'vitest';
import { TIMESTAMP_TSA_PRESETS } from '@/js/config/timestamp-tsa';
// Mock external dependencies before importing the node
vi.mock('rete', () => ({
ClassicPreset: {
Node: class {
addInput() {}
addOutput() {}
addControl() {}
controls: Record<string, unknown> = {};
},
Input: class {
constructor(
public socket: unknown,
public label: string
) {}
},
Output: class {
constructor(
public socket: unknown,
public label: string
) {}
},
InputControl: class {
value: string;
constructor(
public type: string,
public options: { initial: string }
) {
this.value = options.initial;
}
},
},
}));
vi.mock('@/js/workflow/sockets', () => ({
pdfSocket: {},
}));
vi.mock('@/js/workflow/nodes/base-node', () => ({
BaseWorkflowNode: class {
addInput() {}
addOutput() {}
addControl() {}
controls: Record<string, unknown> = {};
},
}));
vi.mock('pdf-lib', () => ({
PDFDocument: {
load: vi.fn().mockResolvedValue({}),
},
}));
vi.mock('@/js/logic/digital-sign-pdf', () => ({
timestampPdf: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])),
}));
vi.mock('@/js/workflow/types', () => ({
requirePdfInput: vi.fn((inputs: Record<string, unknown[]>) => inputs['pdf']),
processBatch: vi.fn(
async (
inputs: Array<{ bytes: Uint8Array; filename: string }>,
fn: (input: { bytes: Uint8Array; filename: string }) => Promise<unknown>
) => {
const results = [];
for (const input of inputs) {
results.push(await fn(input));
}
return results;
}
),
}));
import { TimestampNode } from '@/js/workflow/nodes/timestamp-node';
describe('TimestampNode', () => {
it('should be instantiable', () => {
const node = new TimestampNode();
expect(node).toBeDefined();
});
it('should have the correct category', () => {
const node = new TimestampNode();
expect(node.category).toBe('Secure PDF');
});
it('should have the correct icon', () => {
const node = new TimestampNode();
expect(node.icon).toBe('ph-clock');
});
it('should have a description', () => {
const node = new TimestampNode();
expect(node.description).toBe('Add an RFC 3161 document timestamp');
});
it('should return TSA presets', () => {
const node = new TimestampNode();
const presets = node.getTsaPresets();
expect(presets).toBe(TIMESTAMP_TSA_PRESETS);
expect(presets.length).toBeGreaterThan(0);
});
it('should use the first TSA preset as default URL', () => {
const node = new TimestampNode();
const presets = node.getTsaPresets();
expect(presets[0].url).toBe(TIMESTAMP_TSA_PRESETS[0].url);
});
it('should generate _timestamped suffix in output filename', () => {
const input = 'report.pdf';
const output = input.replace(/\.pdf$/i, '_timestamped.pdf');
expect(output).toBe('report_timestamped.pdf');
});
});

View File

@@ -0,0 +1,184 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TIMESTAMP_TSA_PRESETS } from '@/js/config/timestamp-tsa';
/**
* Tests for the Timestamp PDF page logic.
*
* These tests validate DOM interactions, state management, and UI behavior
* of the timestamp-pdf-page module without calling real TSA servers.
*/
function buildPageHtml(): string {
return `
<input type="file" id="file-input" accept=".pdf" />
<div id="drop-zone"></div>
<div id="file-display-area"></div>
<div id="tsa-section" class="hidden">
<select id="tsa-preset"></select>
</div>
<button id="process-btn" class="hidden" disabled></button>
<button id="back-to-tools"></button>
`;
}
describe('Timestamp PDF Page', () => {
beforeEach(() => {
document.body.innerHTML = buildPageHtml();
});
describe('TSA Preset Population', () => {
it('should populate the TSA preset select element with all presets', () => {
const select = document.getElementById('tsa-preset') as HTMLSelectElement;
for (const preset of TIMESTAMP_TSA_PRESETS) {
const option = document.createElement('option');
option.value = preset.url;
option.textContent = preset.label;
select.append(option);
}
expect(select.options.length).toBe(TIMESTAMP_TSA_PRESETS.length);
});
it('should set option values to TSA URLs', () => {
const select = document.getElementById('tsa-preset') as HTMLSelectElement;
for (const preset of TIMESTAMP_TSA_PRESETS) {
const option = document.createElement('option');
option.value = preset.url;
option.textContent = preset.label;
select.append(option);
}
for (let i = 0; i < TIMESTAMP_TSA_PRESETS.length; i++) {
expect(select.options[i].value).toBe(TIMESTAMP_TSA_PRESETS[i].url);
expect(select.options[i].textContent).toBe(
TIMESTAMP_TSA_PRESETS[i].label
);
}
});
});
describe('File Validation', () => {
it('should reject non-PDF files based on type', () => {
const file = new File(['content'], 'image.png', { type: 'image/png' });
const isValidPdf =
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf');
expect(isValidPdf).toBe(false);
});
it('should accept files with application/pdf type', () => {
const file = new File(['content'], 'document.pdf', {
type: 'application/pdf',
});
const isValidPdf =
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf');
expect(isValidPdf).toBe(true);
});
it('should accept files with .pdf extension regardless of MIME type', () => {
const file = new File(['content'], 'document.pdf', {
type: 'application/octet-stream',
});
const isValidPdf =
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf');
expect(isValidPdf).toBe(true);
});
it('should handle case-insensitive PDF extension', () => {
const file = new File(['content'], 'document.PDF', {
type: 'application/octet-stream',
});
const isValidPdf =
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf');
expect(isValidPdf).toBe(true);
});
});
describe('Output Filename', () => {
it('should append _timestamped before .pdf extension', () => {
const inputName = 'document.pdf';
const outputName = inputName.replace(/\.pdf$/i, '_timestamped.pdf');
expect(outputName).toBe('document_timestamped.pdf');
});
it('should handle uppercase .PDF extension', () => {
const inputName = 'document.PDF';
const outputName = inputName.replace(/\.pdf$/i, '_timestamped.pdf');
expect(outputName).toBe('document_timestamped.pdf');
});
it('should handle filenames with multiple dots', () => {
const inputName = 'my.report.2024.pdf';
const outputName = inputName.replace(/\.pdf$/i, '_timestamped.pdf');
expect(outputName).toBe('my.report.2024_timestamped.pdf');
});
});
describe('UI State Management', () => {
it('should have TSA section hidden initially', () => {
const tsaSection = document.getElementById('tsa-section');
expect(tsaSection?.classList.contains('hidden')).toBe(true);
});
it('should have process button hidden initially', () => {
const processBtn = document.getElementById('process-btn');
expect(processBtn?.classList.contains('hidden')).toBe(true);
});
it('should have process button disabled initially', () => {
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
expect(processBtn.disabled).toBe(true);
});
it('should show TSA section when hidden class is removed', () => {
const tsaSection = document.getElementById('tsa-section')!;
tsaSection.classList.remove('hidden');
expect(tsaSection.classList.contains('hidden')).toBe(false);
});
it('should enable process button when PDF is loaded', () => {
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
processBtn.classList.remove('hidden');
processBtn.disabled = false;
expect(processBtn.disabled).toBe(false);
expect(processBtn.classList.contains('hidden')).toBe(false);
});
});
describe('Drop Zone', () => {
it('should exist in the DOM', () => {
const dropZone = document.getElementById('drop-zone');
expect(dropZone).not.toBeNull();
});
it('should add highlight class on dragover', () => {
const dropZone = document.getElementById('drop-zone')!;
dropZone.classList.add('bg-gray-700');
expect(dropZone.classList.contains('bg-gray-700')).toBe(true);
});
it('should remove highlight class on dragleave', () => {
const dropZone = document.getElementById('drop-zone')!;
dropZone.classList.add('bg-gray-700');
dropZone.classList.remove('bg-gray-700');
expect(dropZone.classList.contains('bg-gray-700')).toBe(false);
});
});
});

View File

@@ -0,0 +1,49 @@
import { describe, it, expect } from 'vitest';
import {
TIMESTAMP_TSA_PRESETS,
type TimestampTsaPreset,
} from '@/js/config/timestamp-tsa';
describe('Timestamp TSA Presets', () => {
it('should be a non-empty array', () => {
expect(Array.isArray(TIMESTAMP_TSA_PRESETS)).toBe(true);
expect(TIMESTAMP_TSA_PRESETS.length).toBeGreaterThan(0);
});
it('should contain only objects with label and url strings', () => {
for (const preset of TIMESTAMP_TSA_PRESETS) {
expect(typeof preset.label).toBe('string');
expect(preset.label.length).toBeGreaterThan(0);
expect(typeof preset.url).toBe('string');
expect(preset.url.length).toBeGreaterThan(0);
}
});
it('should have unique labels', () => {
const labels = TIMESTAMP_TSA_PRESETS.map((p) => p.label);
expect(new Set(labels).size).toBe(labels.length);
});
it('should have unique URLs', () => {
const urls = TIMESTAMP_TSA_PRESETS.map((p) => p.url);
expect(new Set(urls).size).toBe(urls.length);
});
it('should have valid URL formats', () => {
for (const preset of TIMESTAMP_TSA_PRESETS) {
expect(() => new URL(preset.url)).not.toThrow();
}
});
it('should include well-known TSA providers', () => {
const labels = TIMESTAMP_TSA_PRESETS.map((p) => p.label);
expect(labels).toContain('DigiCert');
expect(labels).toContain('Sectigo');
});
it('should satisfy the TimestampTsaPreset interface', () => {
const preset: TimestampTsaPreset = TIMESTAMP_TSA_PRESETS[0];
expect(preset).toHaveProperty('label');
expect(preset).toHaveProperty('url');
});
});