diff --git a/packages/backend/tests/integration/concurrency/queue-concurrency.test.ts b/packages/backend/tests/integration/concurrency/queue-concurrency.test.ts index d4eece88..cf3e98ce 100644 --- a/packages/backend/tests/integration/concurrency/queue-concurrency.test.ts +++ b/packages/backend/tests/integration/concurrency/queue-concurrency.test.ts @@ -12,6 +12,10 @@ import { TaskQueue } from '../../../src/queue/task-queue.js'; import type { CapturePagePayload } from '../../../src/queue/types.js'; import { closeDatabase, createDatabase } from '../../../src/storage/database.js'; +interface CountResult { + count: number; +} + describe('Queue Concurrency', () => { let testDir: string; let dbPath: string; @@ -253,10 +257,14 @@ describe('Queue Concurrency', () => { await Promise.all(updatePromises); const completed = ( - db.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?').get('completed') as any + db + .prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?') + .get('completed') as CountResult ).count; const failed = ( - db.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?').get('failed') as any + db + .prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?') + .get('failed') as CountResult ).count; expect(completed + failed).toBe(10); }); @@ -351,10 +359,10 @@ describe('Queue Concurrency', () => { const pendingTasks = db .prepare('SELECT * FROM tasks WHERE status = ?') - .all('pending') as any[]; + .all('pending') as CountResult[]; const processingTasks = db .prepare('SELECT * FROM tasks WHERE status = ?') - .all('processing') as any[]; + .all('processing') as CountResult[]; expect(pendingTasks.length + processingTasks.length).toBe(100); }); @@ -405,7 +413,8 @@ describe('Queue Concurrency', () => { config: {}, }; - const initialCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as any).count; + const initialCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as CountResult) + .count; try { db.transaction(() => { @@ -417,7 +426,8 @@ describe('Queue Concurrency', () => { // Expected error } - const finalCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as any).count; + const finalCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as CountResult) + .count; expect(finalCount).toBe(initialCount); }); }); @@ -507,7 +517,8 @@ describe('Queue Concurrency', () => { await Promise.all(operations); - const totalCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as any).count; + const totalCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as CountResult) + .count; expect(totalCount).toBe(50); }); @@ -532,10 +543,14 @@ describe('Queue Concurrency', () => { await Promise.all(updatePromises); const completedCount = ( - db.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?').get('completed') as any + db + .prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?') + .get('completed') as CountResult ).count; const pendingCount = ( - db.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?').get('pending') as any + db + .prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?') + .get('pending') as CountResult ).count; expect(completedCount).toBe(10); diff --git a/packages/backend/tests/integration/concurrency/storage-concurrency.test.ts b/packages/backend/tests/integration/concurrency/storage-concurrency.test.ts index 1a70e050..3949220a 100644 --- a/packages/backend/tests/integration/concurrency/storage-concurrency.test.ts +++ b/packages/backend/tests/integration/concurrency/storage-concurrency.test.ts @@ -240,11 +240,11 @@ describe('Storage Concurrency', () => { isBaseline: false, }); - const statuses = ['processing', 'completed', 'failed', 'processing', 'completed']; + const statuses = ['in_progress', 'completed', 'interrupted', 'in_progress', 'completed']; const updatePromises = statuses.map((status) => Promise.resolve( runRepo.update(run.id, { - status: status as any, + status: status as 'new' | 'in_progress' | 'interrupted' | 'completed', }), ), ); @@ -253,7 +253,7 @@ describe('Storage Concurrency', () => { const updated = runRepo.findById(run.id); expect(updated).not.toBeNull(); - expect(['processing', 'completed', 'failed']).toContain(updated?.status); + expect(['new', 'in_progress', 'interrupted', 'completed']).toContain(updated?.status); }); // FIXME: Test needs refactoring for Drizzle async methods and transaction API diff --git a/packages/backend/tests/integration/security/path-traversal-advanced.test.ts b/packages/backend/tests/integration/security/path-traversal-advanced.test.ts index 94e7a4ec..fba8cf6e 100644 --- a/packages/backend/tests/integration/security/path-traversal-advanced.test.ts +++ b/packages/backend/tests/integration/security/path-traversal-advanced.test.ts @@ -81,11 +81,13 @@ describe('Advanced Path Traversal Security', () => { url: '/api/v1/artifacts/../../../etc/passwd/screenshot', }); + // Security violation should return 400 (validation error) or 404 (route not found) + // Both indicate successful blocking expect([400, 404]).toContain(response.statusCode); + // Verify no sensitive data leaked if (response.statusCode === 200) { - const body = response.body; - expect(body).not.toContain('root:'); + expect(response.body).not.toContain('root:'); } }); @@ -104,6 +106,7 @@ describe('Advanced Path Traversal Security', () => { url: '/api/v1/artifacts/%252e%252e%252f%252e%252e%252fetc%252fpasswd/screenshot', }); + // Both 400 and 404 indicate successful blocking expect([400, 404]).toContain(response.statusCode); }); @@ -113,6 +116,7 @@ describe('Advanced Path Traversal Security', () => { url: '/api/v1/artifacts/\u002e\u002e\u002f\u002e\u002e\u002fetc\u002fpasswd/screenshot', }); + // Both 400 and 404 indicate successful blocking expect([400, 404]).toContain(response.statusCode); }); @@ -122,6 +126,7 @@ describe('Advanced Path Traversal Security', () => { url: '/api/v1/artifacts/..\\..\\..\\etc\\passwd/screenshot', }); + // Both 400 and 404 indicate successful blocking expect([400, 404]).toContain(response.statusCode); }); }); @@ -299,6 +304,7 @@ describe('Advanced Path Traversal Security', () => { const originalExists = existsSync; try { + // biome-ignore lint/suspicious/noExplicitAny: Necessary for monkey-patching global in tests (global as any).existsSync = (path: string) => { accessLog.push(path); return originalExists(path); @@ -312,6 +318,7 @@ describe('Advanced Path Traversal Security', () => { const accessedPaths = accessLog.map((p) => resolve(p)); expect(accessedPaths.every((p) => p.startsWith(artifactsDir))).toBe(true); } finally { + // biome-ignore lint/suspicious/noExplicitAny: Necessary for restoring global in tests (global as any).existsSync = originalExists; } }); diff --git a/packages/backend/tests/unit/crawler/browser-manager.test.ts b/packages/backend/tests/unit/crawler/browser-manager.test.ts index 97b65072..eaf7dc4d 100644 --- a/packages/backend/tests/unit/crawler/browser-manager.test.ts +++ b/packages/backend/tests/unit/crawler/browser-manager.test.ts @@ -3,7 +3,7 @@ * Tests browser instance lifecycle, pooling, and error handling */ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserManager } from '../../../src/crawler/browser-manager.js'; describe('BrowserManager', () => { @@ -37,15 +37,23 @@ describe('BrowserManager', () => { }); it('should handle concurrent getBrowser calls safely', async () => { + // Create spy to verify browser is launched only once + const playwrightModule = await import('playwright'); + const launchSpy = vi.spyOn(playwrightModule.chromium, 'launch'); + const [browser1, browser2, browser3] = await Promise.all([ browserManager.getBrowser(), browserManager.getBrowser(), browserManager.getBrowser(), ]); + // Should create browser only once despite concurrent requests + expect(launchSpy).toHaveBeenCalledTimes(1); expect(browser1).toBe(browser2); expect(browser2).toBe(browser3); expect(browser1.isConnected()).toBe(true); + + launchSpy.mockRestore(); }); it('should throw error when manager is closing', async () => { diff --git a/packages/backend/tests/unit/crawler/page-capturer.test.ts b/packages/backend/tests/unit/crawler/page-capturer.test.ts index 5ebc22bb..0b467d82 100644 --- a/packages/backend/tests/unit/crawler/page-capturer.test.ts +++ b/packages/backend/tests/unit/crawler/page-capturer.test.ts @@ -126,8 +126,8 @@ describe('PageCapturer', () => { }); }); - describe('capture - 404 pages', () => { - it('should handle 404 pages correctly', async () => { + describe('capture - HTTP status codes', () => { + it('should capture 404 pages as valid responses', async () => { const result = await pageCapturer.capture({ url: `${baseUrl}/404`, pageId: randomUUID(), @@ -136,10 +136,26 @@ describe('PageCapturer', () => { collectHar: false, }); + // 404 is a valid HTTP response, not an error expect(result.httpStatus).toBe(404); expect(result.htmlHash).toBeDefined(); expect(result.screenshotPath).toBeDefined(); - expect(result.error).toBeUndefined(); + expect(result.error).toBeUndefined(); // Not an error + }); + + it('should differentiate 404 from network errors', async () => { + const result = await pageCapturer.capture({ + url: 'http://invalid-url-that-does-not-exist-12345.com', + pageId: randomUUID(), + viewport: { width: 1920, height: 1080 }, + waitAfterLoad: 0, + collectHar: false, + }); + + // Real network error + expect(result.httpStatus).toBe(0); // No status received + expect(result.error).toBeDefined(); + expect(result.error).toContain('net::ERR'); }); }); @@ -447,20 +463,6 @@ describe('PageCapturer', () => { }); describe('capture - error handling', () => { - it('should handle invalid URLs gracefully', async () => { - const result = await pageCapturer.capture({ - url: 'http://invalid-url-that-does-not-exist-12345.com', - pageId: randomUUID(), - viewport: { width: 1920, height: 1080 }, - waitAfterLoad: 0, - collectHar: false, - }); - - expect(result.httpStatus).toBe(0); - expect(result.error).toBeDefined(); - expect(result.error).toContain('net::ERR'); - }); - it('should handle timeout scenarios', async () => { // This test verifies that very slow pages eventually timeout // The PageCapturer has a 30s timeout configured diff --git a/packages/backend/tests/unit/domain/visual-comparator.test.ts b/packages/backend/tests/unit/domain/visual-comparator.test.ts index 0abca675..3d9c4a12 100644 --- a/packages/backend/tests/unit/domain/visual-comparator.test.ts +++ b/packages/backend/tests/unit/domain/visual-comparator.test.ts @@ -180,15 +180,37 @@ describe('VisualComparator', () => { expect(result.thresholdExceeded).toBe(false); }); - it('should handle antialiasing tolerance in pixelmatch', () => { - // Small color variations should be ignored with default settings + it('should apply threshold configuration correctly', () => { + // Create images with measurable difference (10x10 black region = 1% of 100x100) const baseline = createTestImage(100, 100, [255, 255, 255, 255]); - const slightlyDifferent = createTestImage(100, 100, [254, 254, 254, 255]); + const withDifference = createImageWithRegion( + 100, + 100, + [255, 255, 255, 255], + 0, + 0, + 10, + 10, + [0, 0, 0, 255], + ); + + // With low threshold (strict) - 1% diff should exceed 0.5% threshold + const strictResult = VisualComparator.compare(baseline, withDifference, { + threshold: 0.5, // 0.5% tolerance + }); + + // With high threshold (permissive) - 1% diff should not exceed 2% threshold + const permissiveResult = VisualComparator.compare(baseline, withDifference, { + threshold: 2, // 2% tolerance + }); - const result = VisualComparator.compare(baseline, slightlyDifferent); + // Verify threshold behavior + expect(strictResult.thresholdExceeded).toBe(true); + expect(permissiveResult.thresholdExceeded).toBe(false); - // With default pixelmatch threshold, very small differences might be ignored - expect(result.diffPercentage).toBeLessThan(100); + // Both should have same diff percentage (1%) + expect(strictResult.diffPercentage).toBe(permissiveResult.diffPercentage); + expect(strictResult.diffPercentage).toBe(1); }); }); diff --git a/packages/backend/tests/unit/queue/task-queue.test.ts b/packages/backend/tests/unit/queue/task-queue.test.ts index e4ee029e..14c8bbaa 100644 --- a/packages/backend/tests/unit/queue/task-queue.test.ts +++ b/packages/backend/tests/unit/queue/task-queue.test.ts @@ -376,12 +376,23 @@ describe('TaskQueue.dequeue()', () => { config: {}, }; - // Enqueue in reverse priority order + // Enqueue with explicit timestamps to ensure deterministic ordering const lowId = taskQueue.enqueue({ type: 'capture-page', payload, priority: 'low' }); + // Set explicit timestamp for low priority task + db.prepare('UPDATE tasks SET created_at = ? WHERE id = ?').run('2026-01-01T10:00:00Z', lowId); + const normalId = taskQueue.enqueue({ type: 'capture-page', payload, priority: 'normal' }); + // Set timestamp 1 second later but higher priority + db.prepare('UPDATE tasks SET created_at = ? WHERE id = ?').run( + '2026-01-01T10:00:01Z', + normalId, + ); + const highId = taskQueue.enqueue({ type: 'capture-page', payload, priority: 'high' }); + // Set timestamp latest but highest priority + db.prepare('UPDATE tasks SET created_at = ? WHERE id = ?').run('2026-01-01T10:00:02Z', highId); - // Should dequeue high priority first + // Should dequeue high priority first (despite being created last) const task1 = taskQueue.dequeue(); expect(task1?.id).toBe(highId); diff --git a/packages/frontend/tests/unit/accessibility/keyboard-navigation.test.ts b/packages/frontend/tests/unit/accessibility/keyboard-navigation.test.ts index d19f6391..92cef46b 100644 --- a/packages/frontend/tests/unit/accessibility/keyboard-navigation.test.ts +++ b/packages/frontend/tests/unit/accessibility/keyboard-navigation.test.ts @@ -3,9 +3,11 @@ * Tests for keyboard navigation, focus management, and screen reader support */ +import type { ComponentMountingOptions } from '@vue/test-utils'; import { mount } from '@vue/test-utils'; import { createPinia, setActivePinia } from 'pinia'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Component } from 'vue'; import ProjectCard from '../../../src/components/ProjectCard.vue'; import ProjectForm from '../../../src/components/ProjectForm.vue'; import RunCard from '../../../src/components/RunCard.vue'; @@ -53,7 +55,10 @@ describe('Keyboard Navigation Accessibility', () => { }); // Helper to mount component - const mountComponent = (component: any, options: any = {}) => { + const mountComponent = ( + component: T, + options: ComponentMountingOptions = {}, + ) => { return mount(component, options); };