test(routes): rewrite DropZone test with behavioral assertions
Replaces 5 setTimeout sleeps with vi.waitFor on the actual class transition, and converts 6 .not.toThrow smoke tests into assertions that the validation guard surfaces the expected error message (or absence thereof). Tightens the dragging-state regex to bg-accent-bg so it cannot match the idle hover:border-primary substring. Runtime: faster + deterministic. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -47,16 +47,17 @@ describe('DropZone', () => {
|
|||||||
await expect.element(page.getByText(/Dateiformat nicht unterstützt/i)).toBeVisible();
|
await expect.element(page.getByText(/Dateiformat nicht unterstützt/i)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts a PDF file as a valid type', async () => {
|
it('accepts a PDF file as a valid type and renders no "invalid type" message', async () => {
|
||||||
const onUploadComplete = vi.fn();
|
const onUploadComplete = vi.fn();
|
||||||
render(DropZone, { props: { onUploadComplete } });
|
render(DropZone, { props: { onUploadComplete } });
|
||||||
|
|
||||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||||
const pdfFile = new File(['%PDF'], 'brief.pdf', { type: 'application/pdf' });
|
const pdfFile = new File(['%PDF'], 'brief.pdf', { type: 'application/pdf' });
|
||||||
Object.defineProperty(input, 'files', { value: [pdfFile], writable: false });
|
Object.defineProperty(input, 'files', { value: [pdfFile], writable: false });
|
||||||
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
||||||
// Just verify the change event triggers without throwing
|
// The validation guard never raises an invalid-type error for application/pdf.
|
||||||
expect(() => input.dispatchEvent(new Event('change', { bubbles: true }))).not.toThrow();
|
expect(document.body.textContent).not.toMatch(/Dateiformat nicht unterstützt/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns early when no files are selected', async () => {
|
it('returns early when no files are selected', async () => {
|
||||||
@@ -66,7 +67,6 @@ describe('DropZone', () => {
|
|||||||
Object.defineProperty(input, 'files', { value: [], writable: false });
|
Object.defineProperty(input, 'files', { value: [], writable: false });
|
||||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
||||||
// No error message rendered
|
|
||||||
const errors = document.querySelectorAll('.text-red-600');
|
const errors = document.querySelectorAll('.text-red-600');
|
||||||
expect(errors.length).toBe(0);
|
expect(errors.length).toBe(0);
|
||||||
});
|
});
|
||||||
@@ -105,9 +105,10 @@ describe('DropZone', () => {
|
|||||||
const dropZone = document.querySelector('div[role="button"]') as HTMLElement;
|
const dropZone = document.querySelector('div[role="button"]') as HTMLElement;
|
||||||
dropZone.dispatchEvent(new DragEvent('dragover', { bubbles: true }));
|
dropZone.dispatchEvent(new DragEvent('dragover', { bubbles: true }));
|
||||||
|
|
||||||
// The dragging class is applied via reactive isDragging
|
// isDragging=true switches the class to border-primary bg-accent-bg — wait for the next paint.
|
||||||
await new Promise((r) => setTimeout(r, 30));
|
await vi.waitFor(() => {
|
||||||
expect(dropZone.className).toMatch(/border-primary|bg-accent-bg/);
|
expect(dropZone.className).toMatch(/bg-accent-bg/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('drops dragging style on dragleave', async () => {
|
it('drops dragging style on dragleave', async () => {
|
||||||
@@ -115,22 +116,27 @@ describe('DropZone', () => {
|
|||||||
|
|
||||||
const dropZone = document.querySelector('div[role="button"]') as HTMLElement;
|
const dropZone = document.querySelector('div[role="button"]') as HTMLElement;
|
||||||
dropZone.dispatchEvent(new DragEvent('dragover', { bubbles: true }));
|
dropZone.dispatchEvent(new DragEvent('dragover', { bubbles: true }));
|
||||||
await new Promise((r) => setTimeout(r, 10));
|
await vi.waitFor(() => {
|
||||||
|
expect(dropZone.className).toMatch(/bg-accent-bg/);
|
||||||
|
});
|
||||||
dropZone.dispatchEvent(new DragEvent('dragleave', { bubbles: true }));
|
dropZone.dispatchEvent(new DragEvent('dragleave', { bubbles: true }));
|
||||||
await new Promise((r) => setTimeout(r, 30));
|
await vi.waitFor(() => {
|
||||||
expect(dropZone.className).not.toMatch(/^border-primary bg-accent-bg/);
|
expect(dropZone.className).not.toMatch(/bg-accent-bg/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles drop event without files (no-op)', async () => {
|
it('drop event with no files is a no-op (no error message)', async () => {
|
||||||
render(DropZone, { props: {} });
|
render(DropZone, { props: {} });
|
||||||
|
|
||||||
const dropZone = document.querySelector('div[role="button"]') as HTMLElement;
|
const dropZone = document.querySelector('div[role="button"]') as HTMLElement;
|
||||||
const dropEvent = new DragEvent('drop', { bubbles: true });
|
const dropEvent = new DragEvent('drop', { bubbles: true });
|
||||||
Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [] }, writable: false });
|
Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [] }, writable: false });
|
||||||
expect(() => dropZone.dispatchEvent(dropEvent)).not.toThrow();
|
dropZone.dispatchEvent(dropEvent);
|
||||||
|
|
||||||
|
expect(document.querySelectorAll('.text-red-600')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects multiple invalid files and lists them all', async () => {
|
it('rejects multiple invalid files and lists one error message per file', async () => {
|
||||||
render(DropZone, { props: {} });
|
render(DropZone, { props: {} });
|
||||||
|
|
||||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||||
@@ -139,19 +145,21 @@ describe('DropZone', () => {
|
|||||||
Object.defineProperty(input, 'files', { value: [f1, f2], writable: false });
|
Object.defineProperty(input, 'files', { value: [f1, f2], writable: false });
|
||||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 30));
|
await vi.waitFor(() => {
|
||||||
const errors = document.querySelectorAll('.text-red-600');
|
expect(document.querySelectorAll('.text-red-600')).toHaveLength(2);
|
||||||
expect(errors.length).toBe(2);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mixes valid and invalid files without throwing during upload', async () => {
|
it('mixed valid+invalid files raises an error only for the invalid one', async () => {
|
||||||
render(DropZone, { props: {} });
|
render(DropZone, { props: {} });
|
||||||
|
|
||||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||||
const f1 = new File(['x'], 'a.docx', { type: 'application/x-bad' });
|
const f1 = new File(['x'], 'a.docx', { type: 'application/x-bad' });
|
||||||
const f2 = new File(['%PDF'], 'b.pdf', { type: 'application/pdf' });
|
const f2 = new File(['%PDF'], 'b.pdf', { type: 'application/pdf' });
|
||||||
Object.defineProperty(input, 'files', { value: [f1, f2], writable: false });
|
Object.defineProperty(input, 'files', { value: [f1, f2], writable: false });
|
||||||
expect(() => input.dispatchEvent(new Event('change', { bubbles: true }))).not.toThrow();
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
||||||
|
await expect.element(page.getByText(/Dateiformat nicht unterstützt/i)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Enter handler ignores other keys', async () => {
|
it('Enter handler ignores other keys', async () => {
|
||||||
@@ -167,26 +175,35 @@ describe('DropZone', () => {
|
|||||||
it('responds to window-level dragenter only when dataTransfer.types includes "Files"', async () => {
|
it('responds to window-level dragenter only when dataTransfer.types includes "Files"', async () => {
|
||||||
render(DropZone, { props: {} });
|
render(DropZone, { props: {} });
|
||||||
|
|
||||||
// Non-files dragenter should not trigger windowDragging
|
const dropZone = document.querySelector('div[role="button"]') as HTMLElement;
|
||||||
|
|
||||||
|
// Non-files dragenter should not trigger the windowDragging style.
|
||||||
const evt1 = new DragEvent('dragenter', { bubbles: true });
|
const evt1 = new DragEvent('dragenter', { bubbles: true });
|
||||||
Object.defineProperty(evt1, 'dataTransfer', { value: { types: ['text/html'] } });
|
Object.defineProperty(evt1, 'dataTransfer', { value: { types: ['text/html'] } });
|
||||||
window.dispatchEvent(evt1);
|
window.dispatchEvent(evt1);
|
||||||
await new Promise((r) => setTimeout(r, 10));
|
expect(dropZone.className).not.toMatch(/bg-accent-bg/);
|
||||||
|
|
||||||
// Files dragenter should — we just confirm no error
|
// Files dragenter flips windowDragging=true → drop-zone gains the border-primary style.
|
||||||
const evt2 = new DragEvent('dragenter', { bubbles: true });
|
const evt2 = new DragEvent('dragenter', { bubbles: true });
|
||||||
Object.defineProperty(evt2, 'dataTransfer', { value: { types: ['Files'] } });
|
Object.defineProperty(evt2, 'dataTransfer', { value: { types: ['Files'] } });
|
||||||
expect(() => window.dispatchEvent(evt2)).not.toThrow();
|
window.dispatchEvent(evt2);
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(dropZone.className).toMatch(/bg-accent-bg/);
|
||||||
|
});
|
||||||
|
|
||||||
// Trailing window drop to clean up
|
// Trailing window drop to clean up.
|
||||||
window.dispatchEvent(new DragEvent('drop', { bubbles: true }));
|
window.dispatchEvent(new DragEvent('drop', { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('window-level dragleave decrements counter without going negative', async () => {
|
it('window-level dragleave without prior dragenter is safe (counter does not go negative)', async () => {
|
||||||
render(DropZone, { props: {} });
|
render(DropZone, { props: {} });
|
||||||
|
|
||||||
// Multiple dragleaves without dragenters should not throw
|
const dropZone = document.querySelector('div[role="button"]') as HTMLElement;
|
||||||
expect(() => window.dispatchEvent(new DragEvent('dragleave', { bubbles: true }))).not.toThrow();
|
|
||||||
expect(() => window.dispatchEvent(new DragEvent('dragleave', { bubbles: true }))).not.toThrow();
|
// Two consecutive dragleaves on the window without dragenters should leave the drop-zone
|
||||||
|
// in its idle (non-highlighted) state.
|
||||||
|
window.dispatchEvent(new DragEvent('dragleave', { bubbles: true }));
|
||||||
|
window.dispatchEvent(new DragEvent('dragleave', { bubbles: true }));
|
||||||
|
expect(dropZone.className).not.toMatch(/bg-accent-bg/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user