strategy change to TDD

This commit is contained in:
2026-02-17 10:42:32 -05:00
parent 04022b7e0b
commit 54df6018f5
13 changed files with 2620 additions and 334 deletions

View File

@@ -0,0 +1,19 @@
# openspec/changes/
<!-- Explorer: Fill in this section with architectural understanding -->
## Responsibility
<!-- What is this folder's job in the system? -->
## Design
<!-- Key patterns, abstractions, architectural decisions -->
## Flow
<!-- How does data/control flow through this module? -->
## Integration
<!-- How does it connect to other parts of the system? -->

View File

@@ -0,0 +1,19 @@
# openspec/changes/headroom-foundation/
<!-- Explorer: Fill in this section with architectural understanding -->
## Responsibility
<!-- What is this folder's job in the system? -->
## Design
<!-- Key patterns, abstractions, architectural decisions -->
## Flow
<!-- How does data/control flow through this module? -->
## Integration
<!-- How does it connect to other parts of the system? -->

File diff suppressed because it is too large Load Diff

19
openspec/codemap.md Normal file
View File

@@ -0,0 +1,19 @@
# openspec/
<!-- Explorer: Fill in this section with architectural understanding -->
## Responsibility
<!-- What is this folder's job in the system? -->
## Design
<!-- Key patterns, abstractions, architectural decisions -->
## Flow
<!-- How does data/control flow through this module? -->
## Integration
<!-- How does it connect to other parts of the system? -->

View File

@@ -1,20 +1,238 @@
schema: spec-driven
# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform
# Project context - shown to AI when creating artifacts
# This provides essential context about the Headroom project
techstack: |
## Backend (Laravel API)
- **Framework:** Laravel 12 (latest) with PHP 8.4
- **Database:** PostgreSQL (latest, Alpine container)
- **Caching:** Redis (latest, Alpine container) - query + response caching
- **Authentication:** JWT (tymon/jwt-auth package)
- **API Design:** REST with Laravel API Resources
- **API Documentation:** Laravel Scribe (auto-generates SwaggerUI)
- **Testing:** PHPUnit (unit) + Pest (feature)
- **Code Style:** PSR-12, Laravel conventions
- **Container:** Docker (port 3000)
## Frontend (SvelteKit)
- **Framework:** SvelteKit (latest) with Svelte 5
- **Styling:** Tailwind CSS + DaisyUI
- **Charts:** Recharts
- **Tables:** TanStack Table (React Table for Svelte)
- **Forms:** Superforms + Zod + SvelteKit Form Actions
- **State Management:** Svelte stores (minimal - UI state only)
- **HTTP Client:** Native fetch (no Axios)
- **Testing:** Vitest (unit) + Playwright (E2E)
- **Container:** Docker (port 5173)
## Infrastructure
- **Local Dev:** Docker Compose with code-mounted volumes (hot reload)
- **Reverse Proxy:** Nginx Proxy Manager (existing)
- **Database Volume:** Mounted to ./data/postgres
- **Cache Volume:** Mounted to ./data/redis
- **Secrets:** .env files (all environments)
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours
conventions: |
## Code Style Standards
### Backend (Laravel)
- Follow PSR-12 coding standards
- Use Laravel Pint for linting
- Use PHPStan (level 5+) for static analysis
- Use Laravel conventions for naming (camelCase methods, snake_case DB columns)
- Use API Resources for consistent JSON responses
- Use Form Requests for validation
- Use Policies for authorization
- Use UUIDs for primary keys (prevents ID enumeration)
### Frontend (SvelteKit)
- Use Prettier for formatting
- Use ESLint with Svelte plugin
- Use TypeScript strict mode
- Use SvelteKit file-based routing conventions
- Use $lib alias for imports from src/lib
- Use Zod schemas for validation (shared between frontend and API contracts)
## Git Conventions
- **Branch naming:** `feature/opsx-<change-name>` for OpenSpec changes
- **Commit format:**
```
[Type] Brief description (50 chars max)
Detailed explanation (optional, 72 char wrap)
Refs: openspec/changes/<change-name>
```
- **Types:** feat, fix, refactor, test, docs, chore
- **Granular commits:** One fix = one commit
## API Conventions
- RESTful endpoints with standard HTTP verbs
- Response format: `{ "data": {}, "meta": {}, "links": {} }`
- Error format: `{ "message": "...", "errors": {} }`
- Cache keys pattern: `allocations:month:{YYYY-MM}`, `reports:forecast:{from}:{to}:{hash}`
- TTL: 1 hour (allocations), 15 min (reports), 24 hours (master data)
development_strategy:
approach: "Spec-Driven Development (SDD) + Test-Driven Development (TDD) Hybrid"
description: |
Every capability follows a 4-phase cycle:
## Phase 1: SPEC → TEST (Red Phase)
- Read scenarios from specs/<capability>/spec.md
- Write E2E tests (Playwright) - mark as test.fixme()
- Write API tests (Pest) - mark as ->todo()
- Write unit tests (Pest/Vitest) - mark as ->todo() or test.skip()
- Write component tests (Vitest) - mark as test.skip()
- Commit: "test(<capability>): Add pending tests for all scenarios"
## Phase 2: IMPLEMENT (Green Phase)
- Remove skip/todo marker from one test
- Write MINIMAL code to make it pass
- Run test suite (npm run test, php artisan test, npx playwright test)
- Commit when green: "feat(<capability>): Implement <scenario>"
- Repeat for all scenarios
## Phase 3: REFACTOR (Clean Phase)
- Review for code smells, duplication, performance
- Refactor with confidence (tests guard against regression)
- Run full test suite
- Commit: "refactor(<capability>): <improvement description>"
## Phase 4: DOCUMENT
- Generate API docs: php artisan scribe:generate
- Verify all tests pass
- Commit: "docs(<capability>): Update API documentation"
test_granularity: "Every spec scenario gets a test"
test_organization: "Mirror current structure"
test_types:
- E2E: Playwright tests for critical user journeys
- API: Pest Feature tests for all endpoints
- Unit: Pest/Vitest for internal methods and business logic
- Component: Vitest + Testing Library for Svelte components
pending_markers:
php: "->todo()"
playwright: "test.fixme()"
vitest: "test.skip()"
test_naming:
format: "Descriptive - mirror spec scenario intent"
examples:
- "authenticates user with valid credentials and issues JWT tokens"
- "returns 401 when credentials are invalid"
- "rotates refresh token on each refresh request"
coverage_target: ">70%"
regression_strategy: "Every test is a regression test - run full suite on every PR"
scripts:
backend:
test: "pest"
"test:unit": "pest --filter=Unit"
"test:feature": "pest --filter=Feature"
"test:coverage": "pest --coverage --min=70"
"test:todo": "pest --filter=todo"
lint: "pint"
"lint:fix": "pint --fix"
analyse: "phpstan analyse --level=5"
docs: "scribe:generate"
frontend:
test: "vitest run"
"test:watch": "vitest"
"test:ui": "vitest --ui"
"test:e2e": "playwright test"
"test:e2e:ui": "playwright test --ui"
"test:all": "npm run test && npm run test:e2e"
lint: "eslint ."
"lint:fix": "eslint . --fix"
format: "prettier --check ."
"format:fix": "prettier --write ."
rules:
# Project-level standing instructions (from decision-log.md)
all_changes:
- Every change must follow SDD+TDD: specs → pending tests → implementation → refactor
- Every spec scenario must have corresponding tests (E2E, API, Unit, Component)
- Pending tests must be committed before implementation (red phase)
- Changes must end with code review for style, standards, and security
- Verification (/opsx-verify) must check for uncovered code (code not tested)
- Commits must be granular (one scenario = one commit)
- Code coverage must be >70% (enforced in /opsx-verify)
- All tests must pass before merge
- Zero linting errors (Laravel Pint, ESLint, Prettier)
- API documentation must be up-to-date (Scribe generation)
proposal:
- Include clear Goals and Non-Goals sections
- Reference the 4 personas (Superuser, Manager, Developer, Top Brass)
- Align with monthly capacity planning workflow
- Include data validation rules for any new entities
specs:
- Document all validation rules explicitly
- Include RBAC permissions for each operation
- Define error scenarios and expected responses
- Reference existing data model (team_members, projects, allocations, actuals)
- Use YYYY-MM format for month references
- Each scenario must be testable (clear GIVEN/WHEN/THEN)
design:
- Include database schema changes (migrations needed)
- Define API endpoints with request/response examples
- Specify caching strategy (keys, TTL, invalidation rules)
- Include UI/UX considerations for SvelteKit + DaisyUI
- Document any new dependencies
- Document test approach for each capability
tasks:
- Organize by capability (not by layer)
- Each capability has 4 phases: Test (Red) → Implement (Green) → Refactor → Document
- Break implementation into individual scenarios
- Include explicit test tasks (write pending, enable one by one)
- Include API documentation updates as tasks
- Order capabilities by dependency and business priority
# Domain knowledge
context: |
## Project Overview
Headroom is a resource planning and capacity management tool for engineering managers.
It replaces error-prone spreadsheets with structured capacity planning, resource allocation,
and utilization tracking.
## Core Workflow (Monthly Cycle)
1. **Capacity Planning** - Define team availability (holidays, PTO, working days)
2. **Project Setup** - Track projects through lifecycle with approved estimates
3. **Resource Allocation** - Allocate hours per person per project per month
4. **Actuals Tracking** - Log actual hours worked and compare to planned
## Personas & Permissions
- **Superuser:** Full access (setup, config, all projects, all teams)
- **Manager:** Create/edit own projects, allocate own team, view all projects read-only
- **Developer:** View own allocations, log own hours, view assigned projects
- **Top Brass:** View all reports read-only (forecasts, utilization, costs)
## Key Business Rules
- Availability: 0 (unavailable), 0.5 (half day), 1.0 (full day)
- Project allocation indicators: GREEN (100% ±5%), YELLOW (<95%), RED (>105%)
- Monthly aggregate for actuals (not daily)
- Untracked resource for external team time (not billed)
- Validation: Cannot allocate to "Done" or "Cancelled" projects
## Data Model Summary
- team_members: id (UUID), name, role_id, hourly_rate, active
- projects: id (UUID), code, title, status_id, type_id, approved_estimate, forecasted_effort (JSON)
- allocations: id (UUID), project_id, team_member_id, month (YYYY-MM), allocated_hours
- actuals: id (UUID), project_id, team_member_id, month (YYYY-MM), hours_logged
- roles, project_statuses, project_types: Master data tables
- holidays, ptos: Calendar data
## Deferred to Phase 2
- Real-time notifications (WebSocket)
- PDF/CSV exports
- Background jobs (Laravel Queue)
- Audit logging
- Multi-tenancy
- Time-tracking tool integration

View File

@@ -0,0 +1,100 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\User;
uses()->group('{{capability}}');
/*
* Test Template for Pest Feature Tests
*
* Copy this file and replace:
* - {{capability}} with the capability name (e.g., 'authentication')
* - {{scenario_description}} with the spec scenario description
*
* Mark pending tests with ->todo() during Red Phase
* Remove ->todo() and implement during Green Phase
*/
describe('{{capability}} API', function () {
// Use RefreshDatabase trait for clean state
uses(RefreshDatabase::class);
beforeEach(function () {
// Setup code runs before each test
// e.g., create test user, authenticate, etc.
});
/*
* Scenario: {{scenario_description}}
*
* Spec Reference: specs/{{capability}}/spec.md
* Scenario: {{scenario_number}}
*/
it('{{scenario_description}}', function () {
// Arrange (GIVEN)
// Set up the initial state
// Act (WHEN)
// Perform the action
$response = $this->postJson('/api/{{endpoint}}', [
// Request data
]);
// Assert (THEN)
// Verify the expected outcome
$response->assertStatus(200)
->assertJson([
// Expected response structure
]);
})->todo(); // Remove ->todo() when implementing
/*
* Example: Error scenario
*/
it('returns error when {{error_condition}}', function () {
// Arrange
// Act
$response = $this->postJson('/api/{{endpoint}}', [
'invalid' => 'data'
]);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['field_name']);
})->todo();
/*
* Example: Authorization scenario
*/
it('returns 403 when user lacks permission', function () {
// Arrange: Create user without permission
$user = User::factory()->create(['role' => 'developer']);
// Act
$response = $this->actingAs($user)
->postJson('/api/{{admin_endpoint}}');
// Assert
$response->assertStatus(403);
})->todo();
});
/*
* Helper functions specific to this capability
*/
function create{{model}}(array $attributes = []): {{model}}
{
return {{model}}::factory()->create($attributes);
}
function authenticateUser(string $role = 'manager'): User
{
$user = User::factory()->create(['role' => $role]);
// Return authenticated user or token
return $user;
}

View File

@@ -0,0 +1,92 @@
<?php
use App\Services\{{service}};
uses()->group('{{capability}}', 'unit');
/*
* Test Template for Pest Unit Tests
*
* Copy this file and replace:
* - {{capability}} with the capability name
* - {{service}} with the class being tested
*
* Mark pending tests with ->todo() during Red Phase
* Remove ->todo() and implement during Green Phase
*/
describe('{{service}}', function () {
beforeEach(function () {
$this->service = new {{service}}();
});
/*
* Test: {{method_description}}
*
* Tests the core logic of {{method_name}}
*/
it('{{method_description}}', function () {
// Arrange
$input = [
// Test input data
];
$expected = [
// Expected output
];
// Act
$result = $this->service->{{method_name}}($input);
// Assert
expect($result)->toBe($expected);
})->todo();
/*
* Test: Edge case handling
*/
it('handles {{edge_case}} gracefully', function () {
// Arrange
$edgeCaseInput = null;
// Act & Assert
expect(fn () => $this->service->{{method_name}}($edgeCaseInput))
->toThrow(\InvalidArgumentException::class);
})->todo();
/*
* Test: Calculation accuracy
*/
it('calculates {{calculation}} correctly', function () {
// Arrange
$a = 10;
$b = 20;
// Act
$result = $this->service->calculate($a, $b);
// Assert
expect($result)->toBe(30);
})->todo();
});
/*
* Data providers for parameterized tests
*/
dataset('valid_inputs', function () {
return [
'case 1' => [['input' => 'value1'], 'expected1'],
'case 2' => [['input' => 'value2'], 'expected2'],
'case 3' => [['input' => 'value3'], 'expected3'],
];
});
/*
* Example parameterized test
*/
it('processes valid inputs correctly', function ($input, $expected) {
$result = $this->service->process($input);
expect($result)->toBe($expected);
})->with('valid_inputs')->todo();

View File

@@ -0,0 +1,137 @@
import { test, expect } from '@playwright/test';
/*
* Test Template for Playwright E2E Tests
*
* Copy this file and replace:
* - {{capability}} with the capability name (e.g., 'authentication')
* - {{scenario_description}} with the spec scenario description
*
* Mark pending tests with test.fixme() during Red Phase
* Remove test.fixme() and implement during Green Phase
*/
test.describe('{{capability}} Flow', () => {
test.beforeEach(async ({ page }) => {
// Setup: Navigate to base URL, login, etc.
await page.goto('/');
});
/*
* Scenario: {{scenario_description}}
*
* Spec Reference: specs/{{capability}}/spec.md
* Scenario: {{scenario_number}}
*/
test('{{scenario_description}}', async ({ page }) => {
// Arrange (GIVEN)
// Set up initial state
await page.goto('/{{route}}');
// Act (WHEN)
// Perform user actions
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Assert (THEN)
// Verify expected outcome
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page.locator('[data-testid="user-name"]')).toContainText('Test User');
});
/*
* Example: Error scenario
*/
test('displays error when {{error_condition}}', async ({ page }) => {
// Arrange
await page.goto('/{{route}}');
// Act
await page.fill('[name="email"]', 'invalid@example.com');
await page.fill('[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
// Assert
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText('Invalid credentials');
await expect(page).toHaveURL('/{{route}}');
});
/*
* Example: Form validation
*/
test('validates required fields', async ({ page }) => {
// Arrange
await page.goto('/{{route}}');
// Act
await page.click('button[type="submit"]');
// Assert
await expect(page.locator('[data-testid="error-email"]')).toContainText('Email is required');
await expect(page.locator('[data-testid="error-password"]')).toContainText('Password is required');
});
/*
* Example: CRUD operation
*/
test('completes full {{resource}} CRUD flow', async ({ page }) => {
// Create
await page.goto('/{{resource}}/new');
await page.fill('[name="name"]', 'Test {{resource}}');
await page.click('button[type="submit"]');
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
// Read
await page.goto('/{{resource}}');
await expect(page.locator('text=Test {{resource}}')).toBeVisible();
// Update
await page.click('[data-testid="edit-{{resource}}"]');
await page.fill('[name="name"]', 'Updated {{resource}}');
await page.click('button[type="submit"]');
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
// Delete
await page.click('[data-testid="delete-{{resource}}"]');
await page.click('[data-testid="confirm-delete"]');
await expect(page.locator('text=Updated {{resource}}')).not.toBeVisible();
});
/*
* Example: RBAC test
*/
test('restricts access based on role', async ({ page }) => {
// Login as developer (limited permissions)
await loginAs(page, 'developer');
// Try to access admin page
await page.goto('/admin/{{resource}}');
// Should be redirected or show access denied
await expect(page.locator('[data-testid="access-denied"]')).toBeVisible();
});
});
/*
* Helper functions
*/
async function loginAs(page, role: string) {
await page.goto('/login');
await page.fill('[name="email"]', `${role}@example.com`);
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
}
async function create{{model}}(page, data: object) {
await page.goto('/{{resource}}/new');
for (const [key, value] of Object.entries(data)) {
await page.fill(`[name="${key}"]`, value);
}
await page.click('button[type="submit"]');
}

View File

@@ -0,0 +1,174 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import {{component}} from './{{component}}.svelte';
/*
* Test Template for Vitest Component Tests
*
* Copy this file and replace:
* - {{component}} with the Svelte component name
* - {{capability}} with the capability name
*
* Mark pending tests with test.skip() during Red Phase
* Remove .skip and implement during Green Phase
*/
describe('{{component}}', () => {
beforeEach(() => {
// Reset mocks and state
vi.clearAllMocks();
});
/*
* Test: Component renders correctly
*/
it.skip('renders with default props', () => {
const { container } = render({{component}});
expect(container.querySelector('[data-testid="{{component}}"]')).toBeInTheDocument();
});
/*
* Test: Component displays data correctly
*/
it.skip('displays {{data_type}} data', () => {
const mockData = {
id: '123',
name: 'Test Name',
value: 100
};
render({{component}}, { props: { data: mockData } });
expect(screen.getByText('Test Name')).toBeInTheDocument();
expect(screen.getByText('100')).toBeInTheDocument();
});
/*
* Test: User interaction
*/
it.skip('handles {{action}} click', async () => {
const mockHandler = vi.fn();
render({{component}}, {
props: {
on{{action}}: mockHandler
}
});
const button = screen.getByTestId('{{action}}-button');
await fireEvent.click(button);
expect(mockHandler).toHaveBeenCalledTimes(1);
});
/*
* Test: Form submission
*/
it.skip('submits form with correct data', async () => {
const mockSubmit = vi.fn();
render({{component}}, {
props: {
onSubmit: mockSubmit
}
});
// Fill form
await fireEvent.input(screen.getByLabelText('Name'), {
target: { value: 'Test Name' }
});
await fireEvent.input(screen.getByLabelText('Email'), {
target: { value: 'test@example.com' }
});
// Submit
await fireEvent.click(screen.getByText('Submit'));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
name: 'Test Name',
email: 'test@example.com'
});
});
});
/*
* Test: Validation errors
*/
it.skip('displays validation errors', async () => {
render({{component}});
// Submit empty form
await fireEvent.click(screen.getByText('Submit'));
await waitFor(() => {
expect(screen.getByText('Name is required')).toBeInTheDocument();
expect(screen.getByText('Email is required')).toBeInTheDocument();
});
});
/*
* Test: Loading state
*/
it.skip('shows loading state', () => {
render({{component}}, {
props: {
loading: true
}
});
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
/*
* Test: Empty state
*/
it.skip('displays empty state when no data', () => {
render({{component}}, {
props: {
data: []
}
});
expect(screen.getByText('No data available')).toBeInTheDocument();
});
/*
* Test: Error state
*/
it.skip('displays error message', () => {
render({{component}}, {
props: {
error: 'Failed to load data'
}
});
expect(screen.getByText('Failed to load data')).toBeInTheDocument();
});
/*
* Test: Accessibility
*/
it.skip('has accessible attributes', () => {
render({{component}});
const element = screen.getByRole('button');
expect(element).toHaveAttribute('aria-label');
});
});
/*
* Helper functions
*/
function createMock{{model}}(overrides = {}) {
return {
id: '123',
name: 'Test {{model}}',
createdAt: new Date().toISOString(),
...overrides
};
}

View File

@@ -0,0 +1,205 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
/*
* Test Template for Vitest Unit Tests (Utilities, Stores, API Clients)
*
* Copy this file and replace:
* - {{module}} with the module name
* - {{capability}} with the capability name
*
* Mark pending tests with test.skip() during Red Phase
* Remove .skip and implement during Green Phase
*/
describe('{{module}}', () => {
beforeEach(() => {
// Reset state before each test
vi.clearAllMocks();
});
/*
* Test: Core functionality
*/
it.skip('{{functionality_description}}', () => {
// Arrange
const input = {
// Test input
};
const expected = {
// Expected output
};
// Act
const result = {{module}}.{{method}}(input);
// Assert
expect(result).toEqual(expected);
});
/*
* Test: Edge cases
*/
it.skip('handles {{edge_case}}', () => {
// Arrange
const edgeCaseInput = null;
// Act & Assert
expect(() => {{module}}.{{method}}(edgeCaseInput))
.toThrow('Invalid input');
});
/*
* Test: Async operations
*/
it.skip('{{async_description}}', async () => {
// Arrange
const mockResponse = { data: 'test' };
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockResponse)
});
// Act
const result = await {{module}}.{{async_method}}();
// Assert
expect(result).toEqual(mockResponse);
expect(fetch).toHaveBeenCalledWith('/api/{{endpoint}}');
});
/*
* Test: Error handling
*/
it.skip('handles API errors', async () => {
// Arrange
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
json: () => Promise.resolve({ message: 'Server error' })
});
// Act & Assert
await expect({{module}}.{{async_method}}())
.rejects.toThrow('Server error');
});
/*
* Test: Store behavior (if testing Svelte stores)
*/
it.skip('updates store value', () => {
// Arrange
const store = {{module}}.{{store_name}};
// Act
store.set({ value: 'new value' });
// Assert
let value;
store.subscribe(v => { value = v; })();
expect(value).toEqual({ value: 'new value' });
});
/*
* Test: Store derived values
*/
it.skip('computes derived value correctly', () => {
// Arrange
const store = {{module}}.{{store_name}};
store.set({ items: [1, 2, 3] });
// Act
let derivedValue;
{{module}}.{{derived_store}}.subscribe(v => { derivedValue = v; })();
// Assert
expect(derivedValue).toBe(6); // sum of items
});
/*
* Test: Validation logic
*/
it.skip('validates {{field}} correctly', () => {
// Valid cases
expect({{module}}.validate{{field}}('valid')).toBe(true);
expect({{module}}.validate{{field}}('another-valid')).toBe(true);
// Invalid cases
expect({{module}}.validate{{field}}('')).toBe(false);
expect({{module}}.validate{{field}}(null)).toBe(false);
expect({{module}}.validate{{field}}('invalid!')).toBe(false);
});
/*
* Test: Calculation accuracy
*/
it.skip('calculates {{calculation}} correctly', () => {
expect({{module}}.calculate(10, 20)).toBe(30);
expect({{module}}.calculate(0, 0)).toBe(0);
expect({{module}}.calculate(-5, 5)).toBe(0);
});
/*
* Test: Formatting functions
*/
it.skip('formats {{data_type}} correctly', () => {
expect({{module}}.format{{data_type}}(1000)).toBe('$1,000.00');
expect({{module}}.format{{data_type}}(0)).toBe('$0.00');
expect({{module}}.format{{data_type}}(-500)).toBe('-$500.00');
});
/*
* Test: Date/time utilities
*/
it.skip('parses date correctly', () => {
const result = {{module}}.parseDate('2024-01-15');
expect(result).toBeInstanceOf(Date);
expect(result.getFullYear()).toBe(2024);
expect(result.getMonth()).toBe(0); // January is 0
expect(result.getDate()).toBe(15);
});
it.skip('formats date as YYYY-MM', () => {
const date = new Date(2024, 0, 15); // January 15, 2024
expect({{module}}.formatMonth(date)).toBe('2024-01');
});
/*
* Test: Local storage operations
*/
it.skip('saves to localStorage', () => {
const data = { key: 'value' };
{{module}}.saveToStorage('test-key', data);
expect(localStorage.setItem).toHaveBeenCalledWith(
'test-key',
JSON.stringify(data)
);
});
it.skip('loads from localStorage', () => {
const data = { key: 'value' };
localStorage.getItem.mockReturnValue(JSON.stringify(data));
const result = {{module}}.loadFromStorage('test-key');
expect(result).toEqual(data);
});
});
/*
* Data providers for parameterized tests
*/
const testCases = [
{ input: 'case1', expected: 'result1' },
{ input: 'case2', expected: 'result2' },
{ input: 'case3', expected: 'result3' },
];
it.skip('processes multiple cases correctly', () => {
testCases.forEach(({ input, expected }) => {
expect({{module}}.process(input)).toBe(expected);
});
});