Fixing everything, making the project structure ready for real code.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=24.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
|
||||
133
frontend/src/test-setup.ts
Normal file
133
frontend/src/test-setup.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Automatic Zod validation setup for test runs
|
||||
* This file sets up global hooks that automatically validate all Zod schemas during testing
|
||||
*/
|
||||
|
||||
import { beforeAll, afterAll } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Store original Zod parse methods
|
||||
const originalParse = z.ZodType.prototype.parse;
|
||||
const originalSafeParse = z.ZodType.prototype.safeParse;
|
||||
|
||||
let validationCount = 0;
|
||||
let validationErrors: string[] = [];
|
||||
|
||||
/**
|
||||
* Enhanced parse method that logs validation attempts during tests
|
||||
* @param data - The data to validate against the schema
|
||||
* @returns The parsed and validated data
|
||||
*/
|
||||
function enhancedParse<T>(this: z.ZodType<T>, data: unknown): T {
|
||||
try {
|
||||
const result = originalParse.call(this, data);
|
||||
validationCount++;
|
||||
|
||||
// Log successful validations in test mode
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
console.log(`✅ Zod validation passed (${validationCount} total)`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
validationErrors.push(`Zod validation failed: ${error}`);
|
||||
|
||||
// Log failed validations in test mode
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
console.error(`❌ Zod validation failed:`, error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced safeParse method that logs validation attempts during tests
|
||||
* @param data - The data to validate against the schema
|
||||
* @returns Safe parse result with success flag and data or error
|
||||
*/
|
||||
function enhancedSafeParse<T>(
|
||||
this: z.ZodType<T>,
|
||||
data: unknown
|
||||
): z.SafeParseReturnType<unknown, T> {
|
||||
const result = originalSafeParse.call(this, data);
|
||||
validationCount++;
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
if (result.success) {
|
||||
console.log(`✅ Zod safe validation passed (${validationCount} total)`);
|
||||
} else {
|
||||
console.warn(`⚠️ Zod safe validation failed but was handled gracefully`);
|
||||
validationErrors.push(`Zod safe validation failed: ${result.error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install automatic Zod validation hooks for testing
|
||||
*/
|
||||
export function installZodTestHooks(): void {
|
||||
// Replace Zod's parse methods with enhanced versions
|
||||
z.ZodType.prototype.parse = enhancedParse;
|
||||
z.ZodType.prototype.safeParse = enhancedSafeParse;
|
||||
|
||||
// Enable global auto-validation flag
|
||||
(globalThis as any).__AUTO_VALIDATE__ = true;
|
||||
|
||||
console.log('🛡️ Automatic Zod validation hooks installed for test run');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove automatic Zod validation hooks
|
||||
*/
|
||||
export function uninstallZodTestHooks(): void {
|
||||
// Restore original methods
|
||||
z.ZodType.prototype.parse = originalParse;
|
||||
z.ZodType.prototype.safeParse = originalSafeParse;
|
||||
|
||||
// Disable global auto-validation flag
|
||||
(globalThis as any).__AUTO_VALIDATE__ = false;
|
||||
|
||||
console.log('🔧 Zod validation hooks removed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation statistics for test reporting
|
||||
* @returns Object containing validation count and errors
|
||||
*/
|
||||
export function getValidationStats(): {
|
||||
count: number;
|
||||
errors: string[];
|
||||
} {
|
||||
return {
|
||||
count: validationCount,
|
||||
errors: [...validationErrors],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset validation statistics
|
||||
*/
|
||||
export function resetValidationStats(): void {
|
||||
validationCount = 0;
|
||||
validationErrors = [];
|
||||
}
|
||||
|
||||
// Automatic setup for Vitest
|
||||
beforeAll(() => {
|
||||
installZodTestHooks();
|
||||
resetValidationStats();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
const stats = getValidationStats();
|
||||
console.log(`📊 Test run completed with ${stats.count} Zod validations`);
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
console.warn(`⚠️ ${stats.errors.length} validation errors encountered during tests`);
|
||||
}
|
||||
|
||||
uninstallZodTestHooks();
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Hypermodern Zod validation setup - automatic runtime validation without explicit .parse() calls
|
||||
* Follows the same philosophy as typeguard for Python
|
||||
* Zod validation setup with automatic test hooks
|
||||
* Schemas are automatically validated during test runs via test-setup.ts
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
@@ -28,99 +28,3 @@ export const UserSchema = z.object({
|
||||
// Inferred types (no manual type definitions needed)
|
||||
export type Playlist = z.infer<typeof PlaylistSchema>;
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
|
||||
/**
|
||||
* Global schema registry for automatic validation
|
||||
*/
|
||||
class SchemaRegistry {
|
||||
private static schemas = new Map<string, z.ZodTypeAny>([
|
||||
['Playlist', PlaylistSchema as z.ZodTypeAny],
|
||||
['User', UserSchema as z.ZodTypeAny],
|
||||
]);
|
||||
|
||||
static register<T extends z.ZodTypeAny>(name: string, schema: T): void {
|
||||
this.schemas.set(name, schema as z.ZodTypeAny);
|
||||
}
|
||||
|
||||
static get(name: string): z.ZodTypeAny | undefined {
|
||||
return this.schemas.get(name);
|
||||
}
|
||||
|
||||
static validate<T>(typeName: string, data: unknown): T {
|
||||
const schema = this.schemas.get(typeName);
|
||||
if (!schema) {
|
||||
throw new Error(`No schema registered for type: ${typeName}`);
|
||||
}
|
||||
return schema.parse(data) as T;
|
||||
}
|
||||
|
||||
static safeValidate<T>(
|
||||
typeName: string,
|
||||
data: unknown
|
||||
): { success: true; data: T } | { success: false; error: z.ZodError } {
|
||||
const schema = this.schemas.get(typeName);
|
||||
if (!schema) {
|
||||
return {
|
||||
success: false,
|
||||
error: new z.ZodError([
|
||||
{
|
||||
code: 'custom',
|
||||
message: `No schema registered for type: ${typeName}`,
|
||||
path: [],
|
||||
},
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
return result.success
|
||||
? { success: true, data: result.data as T }
|
||||
: { success: false, error: result.error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic validation wrapper - works like typeguard for Python
|
||||
* In development/test: validates automatically
|
||||
* In production: can be stripped by build process
|
||||
*/
|
||||
export class AutoValidator {
|
||||
static validate<T>(typeName: string, data: unknown): T {
|
||||
// Check if auto-validation is enabled (development/test mode)
|
||||
if (typeof globalThis !== 'undefined' && (globalThis as any).__AUTO_VALIDATE__) {
|
||||
return SchemaRegistry.validate<T>(typeName, data);
|
||||
}
|
||||
|
||||
// In production, validation is skipped for performance
|
||||
return data as T;
|
||||
}
|
||||
|
||||
static safeValidate<T>(
|
||||
typeName: string,
|
||||
data: unknown
|
||||
): { success: true; data: T } | { success: false; error: z.ZodError } {
|
||||
if (typeof globalThis !== 'undefined' && (globalThis as any).__AUTO_VALIDATE__) {
|
||||
return SchemaRegistry.safeValidate<T>(typeName, data);
|
||||
}
|
||||
|
||||
// In production, assume data is valid
|
||||
return { success: true, data: data as T };
|
||||
}
|
||||
|
||||
static register<T extends z.ZodTypeAny>(name: string, schema: T): void {
|
||||
SchemaRegistry.register(name, schema);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable automatic validation in development/test environments
|
||||
declare global {
|
||||
var __AUTO_VALIDATE__: boolean;
|
||||
}
|
||||
|
||||
// Check environment and log status
|
||||
const isDev = typeof globalThis !== 'undefined' && (globalThis as any).__AUTO_VALIDATE__;
|
||||
if (isDev) {
|
||||
console.log('🛡️ Hypermodern Zod validation enabled (development/test mode)');
|
||||
} else {
|
||||
console.log('🏃♂️ Production mode - Zod validation optimized away');
|
||||
}
|
||||
|
||||
@@ -1,41 +1,6 @@
|
||||
/**
|
||||
* Example unit tests with automatic Zod validation
|
||||
* Uses hypermodern approach where validation happens automatically
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import App from '@/App.vue'
|
||||
import { AutoValidator, type Playlist, type User } from '@/validation'
|
||||
|
||||
/**
|
||||
* Creates a playlist - validation happens automatically in development/test mode
|
||||
* @param name - Playlist name
|
||||
* @param description - Optional description
|
||||
* @returns Playlist object (automatically validated)
|
||||
*/
|
||||
function createPlaylist(name: string, description?: string): Playlist {
|
||||
const rawData = {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
description,
|
||||
tracks: []
|
||||
}
|
||||
|
||||
// In hypermodern setup, this would be handled by build-time transform
|
||||
// For now, we use our auto-validator
|
||||
return AutoValidator.validate<Playlist>('Playlist', rawData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates API response handling with automatic validation
|
||||
* @param apiResponse - Raw API data
|
||||
* @returns Validated user object
|
||||
*/
|
||||
function handleUserApiResponse(apiResponse: unknown): User {
|
||||
// Validation happens automatically - no explicit .parse() needed
|
||||
return AutoValidator.validate<User>('User', apiResponse)
|
||||
}
|
||||
|
||||
describe('App.vue', () => {
|
||||
it('renders properly', () => {
|
||||
@@ -43,81 +8,3 @@ describe('App.vue', () => {
|
||||
expect(wrapper.text()).toContain('Plex Playlist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Automatic Zod validation (hypermodern approach)', () => {
|
||||
it('validates playlist creation automatically', () => {
|
||||
const playlist = createPlaylist('Test Playlist', 'Test description')
|
||||
|
||||
expect(playlist.name).toBe('Test Playlist')
|
||||
expect(playlist.description).toBe('Test description')
|
||||
expect(playlist.tracks).toEqual([])
|
||||
expect(playlist.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
|
||||
})
|
||||
|
||||
it('creates playlist without description automatically', () => {
|
||||
const playlist = createPlaylist('Simple Playlist')
|
||||
|
||||
expect(playlist.name).toBe('Simple Playlist')
|
||||
expect(playlist.description).toBeUndefined()
|
||||
expect(playlist.tracks).toEqual([])
|
||||
})
|
||||
|
||||
it('automatically validates API responses', () => {
|
||||
const apiResponse = {
|
||||
id: 123,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
preferences: {
|
||||
theme: 'dark',
|
||||
notifications: false
|
||||
}
|
||||
}
|
||||
|
||||
const user = handleUserApiResponse(apiResponse)
|
||||
expect(user.username).toBe('testuser')
|
||||
expect(user.preferences.theme).toBe('dark')
|
||||
})
|
||||
|
||||
it('automatically catches validation errors', () => {
|
||||
const invalidPlaylistData = {
|
||||
id: 'not-a-uuid', // Invalid UUID
|
||||
name: '', // Empty name
|
||||
tracks: 'invalid' // Should be array
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
AutoValidator.validate<Playlist>('Playlist', invalidPlaylistData)
|
||||
}).toThrow()
|
||||
})
|
||||
|
||||
it('provides safe validation with error handling', () => {
|
||||
const malformedData = {
|
||||
id: 'not-a-uuid',
|
||||
description: 'Missing name field'
|
||||
}
|
||||
|
||||
const result = AutoValidator.safeValidate<Playlist>('Playlist', malformedData)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
expect(result.error.issues).toBeDefined()
|
||||
expect(result.error.issues.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('handles successful safe validation', () => {
|
||||
const validData = {
|
||||
id: crypto.randomUUID(),
|
||||
name: 'Valid Playlist',
|
||||
tracks: ['track1', 'track2']
|
||||
}
|
||||
|
||||
const result = AutoValidator.safeValidate<Playlist>('Playlist', validData)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.name).toBe('Valid Playlist')
|
||||
expect(result.data.tracks).toEqual(['track1', 'track2'])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,33 +2,10 @@ import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// Custom plugin for automatic Zod validation in development
|
||||
function zodAutoValidation() {
|
||||
return {
|
||||
name: 'zod-auto-validation',
|
||||
transform(code: string, id: string) {
|
||||
// Only apply in development/test environments
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Add automatic validation for functions with Zod schemas
|
||||
if (id.includes('src/') && id.endsWith('.ts')) {
|
||||
// This would normally be a more sophisticated transform
|
||||
// For now, we rely on our AutoValidator pattern
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
zodAutoValidation()
|
||||
vue()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./src/test-setup.ts'], // Automatically install Zod validation hooks
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
@@ -23,7 +24,15 @@ export default defineConfig({
|
||||
'coverage/',
|
||||
'tests/',
|
||||
'playwright.config.ts'
|
||||
]
|
||||
],
|
||||
thresholds: {
|
||||
global: {
|
||||
lines: 85,
|
||||
functions: 85,
|
||||
branches: 85,
|
||||
statements: 85
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user