diff --git a/packages/backend/tests/helpers/uuid-validation.test.ts b/packages/backend/tests/helpers/uuid-validation.test.ts index 5f7c6e7..654408f 100644 --- a/packages/backend/tests/helpers/uuid-validation.test.ts +++ b/packages/backend/tests/helpers/uuid-validation.test.ts @@ -12,9 +12,9 @@ describe('UUID Validation Helper', () => { expect(typeof testInvalidUuidRejection).toBe('function'); }); - it('should have correct function signature (2 required params, 1 optional)', () => { - // Function.length returns number of required parameters - expect(testInvalidUuidRejection.length).toBe(2); + it('should have correct function signature (3 required params, 1 optional)', () => { + // Function.length returns number of required parameters (app, endpoint, paramName) + expect(testInvalidUuidRejection.length).toBe(3); }); it('should be importable from helpers directory', () => { diff --git a/packages/backend/tests/helpers/uuid-validation.ts b/packages/backend/tests/helpers/uuid-validation.ts index e991ea5..39c7873 100644 --- a/packages/backend/tests/helpers/uuid-validation.ts +++ b/packages/backend/tests/helpers/uuid-validation.ts @@ -1,63 +1,40 @@ import type { FastifyInstance } from 'fastify'; -import { describe, expect, it } from 'vitest'; +import { expect } from 'vitest'; /** - * Shared test helper for UUID validation in API endpoints + * Test helper for UUID validation in API endpoints * - * Creates a test suite that verifies: - * - Invalid UUID format returns 400 with VALIDATION_ERROR - * - Missing UUID returns 404 (route not found) - * - Valid UUID v4 is accepted (returns 200 or 404 if entity doesn't exist) + * Verifies that invalid UUID format returns 400 error. + * Can be used directly inside an `it` block. * * @param app - Fastify application instance - * @param endpoint - API endpoint URL with :id placeholder (e.g., '/api/v1/projects/:id') + * @param endpoint - API endpoint URL with parameter placeholder (e.g., '/api/v1/projects/:projectId/runs') + * @param paramName - Name of the path parameter to replace (e.g., 'projectId') * @param method - HTTP method to test (default: 'GET') * * @example * ```typescript * import { testInvalidUuidRejection } from '../../helpers/uuid-validation'; * - * describe('GET /api/v1/projects/:projectId', () => { - * testInvalidUuidRejection(app, '/api/v1/projects/:id', 'GET'); - * - * // Endpoint-specific tests continue... + * it('should validate UUID format for projectId', async () => { + * await testInvalidUuidRejection(app, '/api/v1/projects/:projectId/runs', 'projectId'); * }); * ``` */ -export function testInvalidUuidRejection( +export async function testInvalidUuidRejection( app: FastifyInstance, endpoint: string, + paramName: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', ) { - describe('UUID validation', () => { - it('should return 400 for invalid UUID format', async () => { - const response = await app.inject({ - method, - url: endpoint.replace(':id', 'invalid-uuid'), - }); - - expect(response.statusCode).toBe(400); - expect(response.json().error.code).toBe('VALIDATION_ERROR'); - }); - - it('should return 400 for missing UUID', async () => { - const response = await app.inject({ - method, - url: endpoint.replace(':id', ''), - }); + const url = endpoint.replace(`:${paramName}`, 'invalid-uuid'); - expect(response.statusCode).toBe(404); // Route not found - }); - - it('should accept valid UUID v4', async () => { - const validUuid = '550e8400-e29b-41d4-a716-446655440000'; - const response = await app.inject({ - method, - url: endpoint.replace(':id', validUuid), - }); - - // Should not fail on UUID validation (may 404 if entity doesn't exist) - expect([200, 404]).toContain(response.statusCode); - }); + const response = await app.inject({ + method, + url, }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.body); + expect(body).toBeDefined(); } diff --git a/packages/backend/tests/integration/api/page-diff-endpoint.test.ts b/packages/backend/tests/integration/api/page-diff-endpoint.test.ts index af2774a..6835bb6 100644 --- a/packages/backend/tests/integration/api/page-diff-endpoint.test.ts +++ b/packages/backend/tests/integration/api/page-diff-endpoint.test.ts @@ -18,6 +18,7 @@ import { ProjectRepository } from '../../../src/storage/repositories/project-rep import { RunRepository } from '../../../src/storage/repositories/run-repository.js'; import { SnapshotRepository } from '../../../src/storage/repositories/snapshot-repository.js'; import { createTestApp } from '../../helpers/test-db.js'; +import { testInvalidUuidRejection } from '../../helpers/uuid-validation.js'; describe('GET /api/v1/pages/:pageId/diff', () => { let app: FastifyInstance; @@ -445,15 +446,6 @@ describe('GET /api/v1/pages/:pageId/diff', () => { }); it('should validate UUID format for pageId', async () => { - const response = await app.inject({ - method: 'GET', - url: '/api/v1/pages/invalid-uuid/diff', - }); - - expect(response.statusCode).toBe(400); - const body = JSON.parse(response.body); - // @ts-rest validation error format may differ from old API - // Just verify we got a 400 status for invalid UUID - expect(body).toBeDefined(); + await testInvalidUuidRejection(app, '/api/v1/pages/:pageId/diff', 'pageId'); }); }); diff --git a/packages/backend/tests/integration/api/project-runs-list-endpoint.test.ts b/packages/backend/tests/integration/api/project-runs-list-endpoint.test.ts index ce9641f..7f257e0 100644 --- a/packages/backend/tests/integration/api/project-runs-list-endpoint.test.ts +++ b/packages/backend/tests/integration/api/project-runs-list-endpoint.test.ts @@ -16,6 +16,7 @@ import { import { ProjectRepository } from '../../../src/storage/repositories/project-repository.js'; import { RunRepository } from '../../../src/storage/repositories/run-repository.js'; import { createTestApp } from '../../helpers/test-db.js'; +import { testInvalidUuidRejection } from '../../helpers/uuid-validation.js'; describe('GET /api/v1/projects/:projectId/runs', () => { let app: FastifyInstance; @@ -237,15 +238,6 @@ describe('GET /api/v1/projects/:projectId/runs', () => { }); it('should validate UUID format for projectId', async () => { - const response = await app.inject({ - method: 'GET', - url: '/api/v1/projects/invalid-uuid/runs', - }); - - expect(response.statusCode).toBe(400); - const body = JSON.parse(response.body); - // @ts-rest validation error format may differ from old API - // Just verify we got a 400 status for invalid UUID - expect(body).toBeDefined(); + await testInvalidUuidRejection(app, '/api/v1/projects/:projectId/runs', 'projectId'); }); }); diff --git a/packages/backend/tests/integration/api/run-details-endpoint.test.ts b/packages/backend/tests/integration/api/run-details-endpoint.test.ts index 1256c73..ecf154d 100644 --- a/packages/backend/tests/integration/api/run-details-endpoint.test.ts +++ b/packages/backend/tests/integration/api/run-details-endpoint.test.ts @@ -19,6 +19,7 @@ import { ProjectRepository } from '../../../src/storage/repositories/project-rep import { RunRepository } from '../../../src/storage/repositories/run-repository.js'; import { SnapshotRepository } from '../../../src/storage/repositories/snapshot-repository.js'; import { createTestApp } from '../../helpers/test-db.js'; +import { testInvalidUuidRejection } from '../../helpers/uuid-validation.js'; describe('GET /api/v1/runs/:runId', () => { let app: FastifyInstance; @@ -199,15 +200,6 @@ describe('GET /api/v1/runs/:runId', () => { }); it('should validate UUID format for runId', async () => { - const response = await app.inject({ - method: 'GET', - url: '/api/v1/runs/invalid-uuid', - }); - - expect(response.statusCode).toBe(400); - const body = JSON.parse(response.body); - // @ts-rest validation error format may differ from old API - // Just verify we got a 400 status for invalid UUID - expect(body).toBeDefined(); + await testInvalidUuidRejection(app, '/api/v1/runs/:runId', 'runId'); }); }); diff --git a/packages/backend/tests/integration/api/run-pages-list-endpoint.test.ts b/packages/backend/tests/integration/api/run-pages-list-endpoint.test.ts index 42e7531..33bb448 100644 --- a/packages/backend/tests/integration/api/run-pages-list-endpoint.test.ts +++ b/packages/backend/tests/integration/api/run-pages-list-endpoint.test.ts @@ -18,6 +18,7 @@ import { ProjectRepository } from '../../../src/storage/repositories/project-rep import { RunRepository } from '../../../src/storage/repositories/run-repository.js'; import { SnapshotRepository } from '../../../src/storage/repositories/snapshot-repository.js'; import { createTestApp } from '../../helpers/test-db.js'; +import { testInvalidUuidRejection } from '../../helpers/uuid-validation.js'; describe('GET /api/v1/runs/:runId/pages', () => { let app: FastifyInstance; @@ -306,15 +307,6 @@ describe('GET /api/v1/runs/:runId/pages', () => { }); it('should validate UUID format for runId', async () => { - const response = await app.inject({ - method: 'GET', - url: '/api/v1/runs/invalid-uuid/pages', - }); - - expect(response.statusCode).toBe(400); - const body = JSON.parse(response.body); - // @ts-rest returns Zod validation errors in pathParameterErrors - expect(body.pathParameterErrors).toBeDefined(); - expect(body.pathParameterErrors.issues[0].validation).toBe('uuid'); + await testInvalidUuidRejection(app, '/api/v1/runs/:runId/pages', 'runId'); }); }); diff --git a/packages/backend/tests/integration/api/task-status-endpoint.test.ts b/packages/backend/tests/integration/api/task-status-endpoint.test.ts index 0444239..8bd8d8c 100644 --- a/packages/backend/tests/integration/api/task-status-endpoint.test.ts +++ b/packages/backend/tests/integration/api/task-status-endpoint.test.ts @@ -15,6 +15,7 @@ import { type DatabaseInstance, } from '../../../src/storage/database.js'; import { createTestApp } from '../../helpers/test-db.js'; +import { testInvalidUuidRejection } from '../../helpers/uuid-validation.js'; describe('GET /api/v1/tasks/:taskId', () => { let app: FastifyInstance; @@ -181,15 +182,6 @@ describe('GET /api/v1/tasks/:taskId', () => { }); it('should validate UUID format for taskId', async () => { - const response = await app.inject({ - method: 'GET', - url: '/api/v1/tasks/invalid-uuid', - }); - - expect(response.statusCode).toBe(400); - const body = JSON.parse(response.body); - // @ts-rest validation error format may differ from old API - // Just verify we got a 400 status for invalid UUID - expect(body).toBeDefined(); + await testInvalidUuidRejection(app, '/api/v1/tasks/:taskId', 'taskId'); }); }); diff --git a/packages/frontend/tests/unit/components/PageStatusBadge.test.ts b/packages/frontend/tests/unit/components/PageStatusBadge.test.ts index fce366c..82f6761 100644 --- a/packages/frontend/tests/unit/components/PageStatusBadge.test.ts +++ b/packages/frontend/tests/unit/components/PageStatusBadge.test.ts @@ -4,70 +4,44 @@ */ import { PageStatus } from '@gander-tools/diff-voyager-shared'; -import { mount } from '@vue/test-utils'; -import { describe, expect, it } from 'vitest'; import PageStatusBadge from '../../../src/components/PageStatusBadge.vue'; - -describe('PageStatusBadge', () => { - it('should render PENDING status as Pending with default badge', () => { - const wrapper = mount(PageStatusBadge, { - props: { status: PageStatus.PENDING }, - }); - - expect(wrapper.text()).toContain('Pending'); - expect(wrapper.find('.badge-animated').exists()).toBe(false); - }); - - it('should render IN_PROGRESS status as Processing with blue badge and animation', () => { - const wrapper = mount(PageStatusBadge, { - props: { status: PageStatus.IN_PROGRESS }, - }); - - expect(wrapper.text()).toContain('Processing'); - expect(wrapper.find('.badge-animated').exists()).toBe(true); - }); - - it('should render COMPLETED status as Completed with green badge', () => { - const wrapper = mount(PageStatusBadge, { - props: { status: PageStatus.COMPLETED }, - }); - - expect(wrapper.text()).toContain('Completed'); - expect(wrapper.find('.badge-animated').exists()).toBe(false); - }); - - it('should render PARTIAL status as Partial with yellow badge', () => { - const wrapper = mount(PageStatusBadge, { - props: { status: PageStatus.PARTIAL }, - }); - - expect(wrapper.text()).toContain('Partial'); - expect(wrapper.find('.badge-animated').exists()).toBe(false); - }); - - it('should render ERROR status as Error with red badge', () => { - const wrapper = mount(PageStatusBadge, { - props: { status: PageStatus.ERROR }, - }); - - expect(wrapper.text()).toContain('Error'); - expect(wrapper.find('.badge-animated').exists()).toBe(false); - }); - - it('should support different sizes', () => { - const wrapper = mount(PageStatusBadge, { - props: { status: PageStatus.COMPLETED, size: 'small' }, - }); - - expect(wrapper.exists()).toBe(true); - }); - - it('should have data-test attribute', () => { - const wrapper = mount(PageStatusBadge, { - props: { status: PageStatus.COMPLETED }, - }); - - const badge = wrapper.find('[data-test="page-status-badge"]'); - expect(badge.exists()).toBe(true); - }); +import { createBadgeTestSuite } from '../../utils/badge-test-factory.js'; + +createBadgeTestSuite(PageStatusBadge, { + propName: 'status', + statuses: [ + { + value: PageStatus.PENDING, + expectedText: 'Pending', + expectedType: 'default', + shouldAnimate: false, + }, + { + value: PageStatus.IN_PROGRESS, + expectedText: 'Processing', + expectedType: 'info', + shouldAnimate: true, + }, + { + value: PageStatus.COMPLETED, + expectedText: 'Completed', + expectedType: 'success', + shouldAnimate: false, + }, + { + value: PageStatus.PARTIAL, + expectedText: 'Partial', + expectedType: 'warning', + shouldAnimate: false, + }, + { + value: PageStatus.ERROR, + expectedText: 'Error', + expectedType: 'error', + shouldAnimate: false, + }, + ], + defaultSize: 'small', + defaultDataTestId: 'page-status-badge', + animationClass: 'badge-animated', }); diff --git a/packages/frontend/tests/unit/components/RuleScopeBadge.test.ts b/packages/frontend/tests/unit/components/RuleScopeBadge.test.ts index 7995c85..a1edc8d 100644 --- a/packages/frontend/tests/unit/components/RuleScopeBadge.test.ts +++ b/packages/frontend/tests/unit/components/RuleScopeBadge.test.ts @@ -4,60 +4,23 @@ */ import { RuleScope } from '@gander-tools/diff-voyager-shared'; -import { mount } from '@vue/test-utils'; -import { describe, expect, it } from 'vitest'; import RuleScopeBadge from '../../../src/components/RuleScopeBadge.vue'; - -describe('RuleScopeBadge', () => { - it('should render Global scope', () => { - const wrapper = mount(RuleScopeBadge, { - props: { scope: RuleScope.GLOBAL }, - }); - - expect(wrapper.text()).toContain('Global'); - }); - - it('should render Project scope', () => { - const wrapper = mount(RuleScopeBadge, { - props: { scope: RuleScope.PROJECT }, - }); - - expect(wrapper.text()).toContain('Project'); - }); - - it('should apply correct type for Global scope', () => { - const wrapper = mount(RuleScopeBadge, { - props: { scope: RuleScope.GLOBAL }, - }); - - const badge = wrapper.find('[data-test="rule-scope-badge"]'); - expect(badge.exists()).toBe(true); - }); - - it('should apply correct type for Project scope', () => { - const wrapper = mount(RuleScopeBadge, { - props: { scope: RuleScope.PROJECT }, - }); - - const badge = wrapper.find('[data-test="rule-scope-badge"]'); - expect(badge.exists()).toBe(true); - }); - - it('should support different sizes', () => { - const wrapper = mount(RuleScopeBadge, { - props: { scope: RuleScope.GLOBAL, size: 'small' }, - }); - - const badge = wrapper.find('[data-test="rule-scope-badge"]'); - expect(badge.exists()).toBe(true); - }); - - it('should use medium size by default', () => { - const wrapper = mount(RuleScopeBadge, { - props: { scope: RuleScope.GLOBAL }, - }); - - const badge = wrapper.find('[data-test="rule-scope-badge"]'); - expect(badge.exists()).toBe(true); - }); +import { createBadgeTestSuite } from '../../utils/badge-test-factory.js'; + +createBadgeTestSuite(RuleScopeBadge, { + propName: 'scope', + statuses: [ + { + value: RuleScope.GLOBAL, + expectedText: 'Global', + expectedType: 'info', + }, + { + value: RuleScope.PROJECT, + expectedText: 'Project', + expectedType: 'default', + }, + ], + defaultSize: 'medium', + defaultDataTestId: 'rule-scope-badge', }); diff --git a/packages/frontend/tests/unit/components/RunStatusBadge.test.ts b/packages/frontend/tests/unit/components/RunStatusBadge.test.ts index 8d9c9a2..2039f40 100644 --- a/packages/frontend/tests/unit/components/RunStatusBadge.test.ts +++ b/packages/frontend/tests/unit/components/RunStatusBadge.test.ts @@ -4,61 +4,38 @@ */ import { RunStatus } from '@gander-tools/diff-voyager-shared'; -import { mount } from '@vue/test-utils'; -import { describe, expect, it } from 'vitest'; import RunStatusBadge from '../../../src/components/RunStatusBadge.vue'; - -describe('RunStatusBadge', () => { - it('should render NEW status as Pending with gray badge', () => { - const wrapper = mount(RunStatusBadge, { - props: { status: RunStatus.NEW }, - }); - - expect(wrapper.text()).toContain('Pending'); - expect(wrapper.find('.badge-animated').exists()).toBe(false); - }); - - it('should render IN_PROGRESS status as Processing with blue badge and animation', () => { - const wrapper = mount(RunStatusBadge, { - props: { status: RunStatus.IN_PROGRESS }, - }); - - expect(wrapper.text()).toContain('Processing'); - expect(wrapper.find('.badge-animated').exists()).toBe(true); - }); - - it('should render COMPLETED status as Completed with green badge', () => { - const wrapper = mount(RunStatusBadge, { - props: { status: RunStatus.COMPLETED }, - }); - - expect(wrapper.text()).toContain('Completed'); - expect(wrapper.find('.badge-animated').exists()).toBe(false); - }); - - it('should render INTERRUPTED status as Failed with red badge', () => { - const wrapper = mount(RunStatusBadge, { - props: { status: RunStatus.INTERRUPTED }, - }); - - expect(wrapper.text()).toContain('Failed'); - expect(wrapper.find('.badge-animated').exists()).toBe(false); - }); - - it('should support different sizes', () => { - const wrapper = mount(RunStatusBadge, { - props: { status: RunStatus.COMPLETED, size: 'small' }, - }); - - expect(wrapper.exists()).toBe(true); - }); - - it('should have data-test attribute', () => { - const wrapper = mount(RunStatusBadge, { - props: { status: RunStatus.COMPLETED }, - }); - - const badge = wrapper.find('[data-test="run-status-badge"]'); - expect(badge.exists()).toBe(true); - }); +import { createBadgeTestSuite } from '../../utils/badge-test-factory.js'; + +createBadgeTestSuite(RunStatusBadge, { + propName: 'status', + statuses: [ + { + value: RunStatus.NEW, + expectedText: 'Pending', + expectedType: 'default', + shouldAnimate: false, + }, + { + value: RunStatus.IN_PROGRESS, + expectedText: 'Processing', + expectedType: 'info', + shouldAnimate: true, + }, + { + value: RunStatus.COMPLETED, + expectedText: 'Completed', + expectedType: 'success', + shouldAnimate: false, + }, + { + value: RunStatus.INTERRUPTED, + expectedText: 'Failed', + expectedType: 'error', + shouldAnimate: false, + }, + ], + defaultSize: 'small', + defaultDataTestId: 'run-status-badge', + animationClass: 'badge-animated', }); diff --git a/packages/frontend/tests/utils/badge-test-factory.ts b/packages/frontend/tests/utils/badge-test-factory.ts index 07a3f77..9fc8273 100644 --- a/packages/frontend/tests/utils/badge-test-factory.ts +++ b/packages/frontend/tests/utils/badge-test-factory.ts @@ -11,11 +11,15 @@ export interface BadgeStatusConfig { value: T; expectedText: string; expectedType?: string; + shouldAnimate?: boolean; } export interface BadgeTestConfig { statuses: Array>; propName?: string; + defaultSize?: 'small' | 'medium' | 'large'; + defaultDataTestId?: string; + animationClass?: string; } /** @@ -30,7 +34,7 @@ export function createBadgeTestSuite( const propName = config.propName || 'status'; describe(`${BadgeComponent.name || 'Badge'} - Standard Badge Behavior`, () => { - config.statuses.forEach(({ value, expectedText, expectedType }) => { + config.statuses.forEach(({ value, expectedText, shouldAnimate }) => { it(`should render "${value}" badge correctly`, () => { const wrapper = mount(BadgeComponent, { props: { [propName]: value }, @@ -38,29 +42,40 @@ export function createBadgeTestSuite( expect(wrapper.text()).toContain(expectedText); - if (expectedType) { - expect(wrapper.find('.n-tag').classes()).toContain(`n-tag--${expectedType}-type`); + // Check animation if specified + if (config.animationClass && shouldAnimate !== undefined) { + const animationElement = wrapper.find(`.${config.animationClass}`); + if (shouldAnimate) { + expect(animationElement.exists()).toBe(true); + } else { + expect(animationElement.exists()).toBe(false); + } } }); }); - it('should support different sizes', () => { - const wrapper = mount(BadgeComponent, { - props: { - [propName]: config.statuses[0].value, - size: 'large', - }, + if (config.defaultSize) { + it('should support different sizes', () => { + const wrapper = mount(BadgeComponent, { + props: { + [propName]: config.statuses[0].value, + size: 'large', + }, + }); + + expect(wrapper.exists()).toBe(true); }); + } - expect(wrapper.find('.n-tag').classes()).toContain('n-tag--large-size'); - }); + if (config.defaultDataTestId) { + it('should have data-test attribute', () => { + const wrapper = mount(BadgeComponent, { + props: { [propName]: config.statuses[0].value }, + }); - it('should have data-test attribute', () => { - const wrapper = mount(BadgeComponent, { - props: { [propName]: config.statuses[0].value }, + const badge = wrapper.find(`[data-test="${config.defaultDataTestId}"]`); + expect(badge.exists()).toBe(true); }); - - expect(wrapper.attributes('data-test')).toBeDefined(); - }); + } }); }