blacksmith-cli 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +254 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/templates/frontend/package.json.hbs +9 -1
- package/src/templates/frontend/src/__tests__/setup.ts.hbs +21 -0
- package/src/templates/frontend/src/__tests__/test-utils.tsx.hbs +80 -0
- package/src/templates/frontend/tsconfig.app.json.hbs +1 -0
- package/src/templates/frontend/vite.config.ts.hbs +8 -0
package/dist/index.js
CHANGED
|
@@ -1371,9 +1371,10 @@ var reactSkill = {
|
|
|
1371
1371
|
- Display user-facing errors using the project's feedback components (Alert, Toast)
|
|
1372
1372
|
|
|
1373
1373
|
### Testing
|
|
1374
|
-
-
|
|
1375
|
-
-
|
|
1376
|
-
-
|
|
1374
|
+
- See the \`frontend-testing\` skill for full conventions on test placement, utilities, mocking, and what to test
|
|
1375
|
+
- **Every code change must include corresponding tests** \u2014 see the \`frontend-testing\` skill for the complete rules
|
|
1376
|
+
- Tests use \`.spec.tsx\` / \`.spec.ts\` and live in \`__tests__/\` folders co-located with source code
|
|
1377
|
+
- Always use \`renderWithProviders\` from \`@/__tests__/test-utils\` \u2014 never import \`render\` from \`@testing-library/react\` directly
|
|
1377
1378
|
`;
|
|
1378
1379
|
}
|
|
1379
1380
|
};
|
|
@@ -3427,6 +3428,244 @@ class OrderService:
|
|
|
3427
3428
|
}
|
|
3428
3429
|
};
|
|
3429
3430
|
|
|
3431
|
+
// src/skills/frontend-testing.ts
|
|
3432
|
+
var frontendTestingSkill = {
|
|
3433
|
+
id: "frontend-testing",
|
|
3434
|
+
name: "Frontend Testing Conventions",
|
|
3435
|
+
description: "Test infrastructure, file placement, test utilities, and rules for when and how to write frontend tests.",
|
|
3436
|
+
render(_ctx) {
|
|
3437
|
+
return `## Frontend Testing Conventions
|
|
3438
|
+
|
|
3439
|
+
### Stack
|
|
3440
|
+
- **Vitest** \u2014 test runner (configured in \`vite.config.ts\`)
|
|
3441
|
+
- **jsdom** \u2014 browser environment
|
|
3442
|
+
- **React Testing Library** \u2014 component rendering and queries
|
|
3443
|
+
- **\`@testing-library/user-event\`** \u2014 user interaction simulation
|
|
3444
|
+
- **\`@testing-library/jest-dom\`** \u2014 DOM assertion matchers (e.g. \`toBeInTheDocument\`)
|
|
3445
|
+
|
|
3446
|
+
### Running Tests
|
|
3447
|
+
- Run all tests: \`cd frontend && npm test\`
|
|
3448
|
+
- Watch mode: \`cd frontend && npm run test:watch\`
|
|
3449
|
+
- Run a specific file: \`cd frontend && npx vitest run src/pages/home/__tests__/home.spec.tsx\`
|
|
3450
|
+
- Coverage: \`cd frontend && npm run test:coverage\`
|
|
3451
|
+
|
|
3452
|
+
### File Placement \u2014 Tests Live Next to the Code
|
|
3453
|
+
|
|
3454
|
+
> **RULE: Every test file goes in a \`__tests__/\` folder co-located with the code it tests. Never put tests in a top-level \`tests/\` directory.**
|
|
3455
|
+
|
|
3456
|
+
\`\`\`
|
|
3457
|
+
pages/customers/
|
|
3458
|
+
\u251C\u2500\u2500 customers-page.tsx
|
|
3459
|
+
\u251C\u2500\u2500 customer-detail-page.tsx
|
|
3460
|
+
\u251C\u2500\u2500 __tests__/ # Page integration tests
|
|
3461
|
+
\u2502 \u251C\u2500\u2500 customers-page.spec.tsx
|
|
3462
|
+
\u2502 \u2514\u2500\u2500 customer-detail-page.spec.tsx
|
|
3463
|
+
\u251C\u2500\u2500 components/
|
|
3464
|
+
\u2502 \u251C\u2500\u2500 customer-card.tsx
|
|
3465
|
+
\u2502 \u251C\u2500\u2500 customer-list.tsx
|
|
3466
|
+
\u2502 \u251C\u2500\u2500 customer-form.tsx
|
|
3467
|
+
\u2502 \u2514\u2500\u2500 __tests__/ # Component unit tests
|
|
3468
|
+
\u2502 \u251C\u2500\u2500 customer-card.spec.tsx
|
|
3469
|
+
\u2502 \u251C\u2500\u2500 customer-list.spec.tsx
|
|
3470
|
+
\u2502 \u2514\u2500\u2500 customer-form.spec.tsx
|
|
3471
|
+
\u2514\u2500\u2500 hooks/
|
|
3472
|
+
\u251C\u2500\u2500 use-customers-page.ts
|
|
3473
|
+
\u2514\u2500\u2500 __tests__/ # Hook tests
|
|
3474
|
+
\u2514\u2500\u2500 use-customers-page.spec.ts
|
|
3475
|
+
\`\`\`
|
|
3476
|
+
|
|
3477
|
+
The same pattern applies to \`features/\`, \`shared/\`, and \`router/\`:
|
|
3478
|
+
\`\`\`
|
|
3479
|
+
features/auth/
|
|
3480
|
+
\u251C\u2500\u2500 pages/
|
|
3481
|
+
\u2502 \u251C\u2500\u2500 login-page.tsx
|
|
3482
|
+
\u2502 \u2514\u2500\u2500 __tests__/
|
|
3483
|
+
\u2502 \u2514\u2500\u2500 login-page.spec.tsx
|
|
3484
|
+
\u251C\u2500\u2500 hooks/
|
|
3485
|
+
\u2502 \u2514\u2500\u2500 __tests__/
|
|
3486
|
+
shared/
|
|
3487
|
+
\u251C\u2500\u2500 components/
|
|
3488
|
+
\u2502 \u2514\u2500\u2500 __tests__/
|
|
3489
|
+
\u251C\u2500\u2500 hooks/
|
|
3490
|
+
\u2502 \u2514\u2500\u2500 __tests__/
|
|
3491
|
+
router/
|
|
3492
|
+
\u251C\u2500\u2500 __tests__/
|
|
3493
|
+
\u2502 \u251C\u2500\u2500 paths.spec.ts
|
|
3494
|
+
\u2502 \u2514\u2500\u2500 auth-guard.spec.tsx
|
|
3495
|
+
\`\`\`
|
|
3496
|
+
|
|
3497
|
+
### Test File Naming
|
|
3498
|
+
- Use \`.spec.tsx\` for component/page tests (JSX)
|
|
3499
|
+
- Use \`.spec.ts\` for pure logic tests (hooks, utilities, no JSX)
|
|
3500
|
+
- Name matches the source file: \`customer-card.tsx\` \u2192 \`customer-card.spec.tsx\`
|
|
3501
|
+
|
|
3502
|
+
### Always Use \`renderWithProviders\`
|
|
3503
|
+
|
|
3504
|
+
> **RULE: Never import \`render\` from \`@testing-library/react\` directly. Always use \`renderWithProviders\` from \`@/__tests__/test-utils\`.**
|
|
3505
|
+
|
|
3506
|
+
\`renderWithProviders\` wraps components with all app providers (ThemeProvider, QueryClientProvider, MemoryRouter) so tests match the real app environment.
|
|
3507
|
+
|
|
3508
|
+
\`\`\`tsx
|
|
3509
|
+
import { screen } from '@/__tests__/test-utils'
|
|
3510
|
+
import { renderWithProviders } from '@/__tests__/test-utils'
|
|
3511
|
+
import { MyComponent } from '../my-component'
|
|
3512
|
+
|
|
3513
|
+
describe('MyComponent', () => {
|
|
3514
|
+
it('renders correctly', () => {
|
|
3515
|
+
renderWithProviders(<MyComponent />)
|
|
3516
|
+
expect(screen.getByText('Hello')).toBeInTheDocument()
|
|
3517
|
+
})
|
|
3518
|
+
})
|
|
3519
|
+
\`\`\`
|
|
3520
|
+
|
|
3521
|
+
**Options:**
|
|
3522
|
+
\`\`\`tsx
|
|
3523
|
+
renderWithProviders(<MyComponent />, {
|
|
3524
|
+
routerEntries: ['/customers/1'], // Set initial route
|
|
3525
|
+
queryClient: customQueryClient, // Custom query client
|
|
3526
|
+
})
|
|
3527
|
+
\`\`\`
|
|
3528
|
+
|
|
3529
|
+
**User interactions:**
|
|
3530
|
+
\`\`\`tsx
|
|
3531
|
+
const { user } = renderWithProviders(<MyComponent />)
|
|
3532
|
+
await user.click(screen.getByRole('button', { name: 'Submit' }))
|
|
3533
|
+
await user.type(screen.getByLabelText('Email'), 'test@example.com')
|
|
3534
|
+
\`\`\`
|
|
3535
|
+
|
|
3536
|
+
### When to Write Tests
|
|
3537
|
+
|
|
3538
|
+
> **RULE: Every code change that touches pages, components, hooks, or utilities must include corresponding test updates.**
|
|
3539
|
+
|
|
3540
|
+
| What changed | Test required |
|
|
3541
|
+
|---|---|
|
|
3542
|
+
| New page | Add \`__tests__/<page>.spec.tsx\` with integration test |
|
|
3543
|
+
| New component | Add \`__tests__/<component>.spec.tsx\` |
|
|
3544
|
+
| New hook (with logic) | Add \`__tests__/<hook>.spec.ts\` |
|
|
3545
|
+
| New utility function | Add \`__tests__/<util>.spec.ts\` |
|
|
3546
|
+
| Modified page/component | Update existing test or add new test cases |
|
|
3547
|
+
| Bug fix | Add regression test that would have caught the bug |
|
|
3548
|
+
| Deleted page/component | Delete corresponding test file |
|
|
3549
|
+
|
|
3550
|
+
### What to Test
|
|
3551
|
+
|
|
3552
|
+
**Page integration tests** \u2014 test the page as a whole:
|
|
3553
|
+
- Renders correct heading/title
|
|
3554
|
+
- Loading state shows skeleton or spinner
|
|
3555
|
+
- Error state shows error message
|
|
3556
|
+
- Data renders correctly (mock the API hooks)
|
|
3557
|
+
- User interactions (navigation, form submission, delete confirmation)
|
|
3558
|
+
|
|
3559
|
+
**Component unit tests** \u2014 test the component in isolation:
|
|
3560
|
+
- Renders with required props
|
|
3561
|
+
- Handles optional props correctly (present vs absent)
|
|
3562
|
+
- Displays correct content based on props
|
|
3563
|
+
- User interactions trigger correct callbacks
|
|
3564
|
+
- Conditional rendering (empty state, loading state)
|
|
3565
|
+
|
|
3566
|
+
**Hook tests** \u2014 test custom hooks with logic:
|
|
3567
|
+
- Returns correct initial state
|
|
3568
|
+
- Transforms data correctly
|
|
3569
|
+
- Side effects fire as expected
|
|
3570
|
+
|
|
3571
|
+
**Utility/pure function tests** \u2014 test input/output:
|
|
3572
|
+
- Happy path
|
|
3573
|
+
- Edge cases (empty input, null, special characters)
|
|
3574
|
+
- Error cases
|
|
3575
|
+
|
|
3576
|
+
### Mocking Patterns
|
|
3577
|
+
|
|
3578
|
+
**Mock hooks (for page tests):**
|
|
3579
|
+
\`\`\`tsx
|
|
3580
|
+
vi.mock('@/api/hooks/customers')
|
|
3581
|
+
vi.mock('@/features/auth/hooks/use-auth')
|
|
3582
|
+
|
|
3583
|
+
import { useCustomers } from '@/api/hooks/customers'
|
|
3584
|
+
|
|
3585
|
+
beforeEach(() => {
|
|
3586
|
+
vi.mocked(useCustomers).mockReturnValue({
|
|
3587
|
+
data: { customers: mockCustomers, total: 2 },
|
|
3588
|
+
isLoading: false,
|
|
3589
|
+
errorMessage: null,
|
|
3590
|
+
} as any)
|
|
3591
|
+
})
|
|
3592
|
+
\`\`\`
|
|
3593
|
+
|
|
3594
|
+
**Mock external UI libraries (for auth page tests):**
|
|
3595
|
+
\`\`\`tsx
|
|
3596
|
+
vi.mock('@blacksmith-ui/auth', () => ({
|
|
3597
|
+
LoginForm: ({ onSubmit, error, loading }: any) => (
|
|
3598
|
+
<form onSubmit={(e: any) => { e.preventDefault(); onSubmit({ email: 'test@test.com', password: 'pass' }) }}>
|
|
3599
|
+
{error && <div data-testid="error">{error.message}</div>}
|
|
3600
|
+
<button type="submit">Sign In</button>
|
|
3601
|
+
</form>
|
|
3602
|
+
),
|
|
3603
|
+
}))
|
|
3604
|
+
\`\`\`
|
|
3605
|
+
|
|
3606
|
+
**Mock react-router-dom hooks (for detail pages):**
|
|
3607
|
+
\`\`\`tsx
|
|
3608
|
+
const mockNavigate = vi.fn()
|
|
3609
|
+
vi.mock('react-router-dom', async () => {
|
|
3610
|
+
const actual = await vi.importActual('react-router-dom')
|
|
3611
|
+
return { ...actual, useParams: () => ({ id: '1' }), useNavigate: () => mockNavigate }
|
|
3612
|
+
})
|
|
3613
|
+
\`\`\`
|
|
3614
|
+
|
|
3615
|
+
### Test Structure
|
|
3616
|
+
\`\`\`tsx
|
|
3617
|
+
import { screen, waitFor } from '@/__tests__/test-utils'
|
|
3618
|
+
import { renderWithProviders } from '@/__tests__/test-utils'
|
|
3619
|
+
|
|
3620
|
+
// Mocks at the top, before imports of modules that use them
|
|
3621
|
+
vi.mock('@/api/hooks/customers')
|
|
3622
|
+
|
|
3623
|
+
import { useCustomers } from '@/api/hooks/customers'
|
|
3624
|
+
import CustomersPage from '../customers-page'
|
|
3625
|
+
|
|
3626
|
+
const mockCustomers = [
|
|
3627
|
+
{ id: '1', title: 'Acme Corp', created_at: '2024-01-15T10:00:00Z' },
|
|
3628
|
+
]
|
|
3629
|
+
|
|
3630
|
+
describe('CustomersPage', () => {
|
|
3631
|
+
beforeEach(() => {
|
|
3632
|
+
vi.mocked(useCustomers).mockReturnValue({ ... } as any)
|
|
3633
|
+
})
|
|
3634
|
+
|
|
3635
|
+
it('renders page heading', () => {
|
|
3636
|
+
renderWithProviders(<CustomersPage />)
|
|
3637
|
+
expect(screen.getByText('Customers')).toBeInTheDocument()
|
|
3638
|
+
})
|
|
3639
|
+
|
|
3640
|
+
it('shows error message when API fails', () => {
|
|
3641
|
+
vi.mocked(useCustomers).mockReturnValue({
|
|
3642
|
+
data: undefined,
|
|
3643
|
+
isLoading: false,
|
|
3644
|
+
errorMessage: 'Failed to load',
|
|
3645
|
+
} as any)
|
|
3646
|
+
|
|
3647
|
+
renderWithProviders(<CustomersPage />)
|
|
3648
|
+
expect(screen.getByText('Failed to load')).toBeInTheDocument()
|
|
3649
|
+
})
|
|
3650
|
+
})
|
|
3651
|
+
\`\`\`
|
|
3652
|
+
|
|
3653
|
+
### Key Rules
|
|
3654
|
+
|
|
3655
|
+
1. **Tests live next to code** \u2014 \`__tests__/\` folder alongside the source, not in a separate top-level directory
|
|
3656
|
+
2. **Always use \`renderWithProviders\`** \u2014 never import render from \`@testing-library/react\` directly
|
|
3657
|
+
3. **Every page gets an integration test** \u2014 at minimum: renders heading, handles loading, handles errors
|
|
3658
|
+
4. **Every component gets a unit test** \u2014 at minimum: renders with required props, handles optional props
|
|
3659
|
+
5. **Mock at the hook level** \u2014 mock \`useCustomers\`, not \`fetch\`. Mock \`useAuth\`, not the auth adapter
|
|
3660
|
+
6. **Test behavior, not implementation** \u2014 query by role, text, or label, not by class names or internal state
|
|
3661
|
+
7. **No test-only IDs unless necessary** \u2014 prefer \`getByRole\`, \`getByText\`, \`getByLabelText\` over \`getByTestId\`
|
|
3662
|
+
8. **Keep tests focused** \u2014 each \`it()\` tests one behavior. Don't assert 10 things in one test
|
|
3663
|
+
9. **Clean up mocks** \u2014 use \`beforeEach\` to reset mock return values so tests don't leak state
|
|
3664
|
+
10. **Update tests when code changes** \u2014 if you modify a component, update its tests. If you delete a component, delete its tests
|
|
3665
|
+
`;
|
|
3666
|
+
}
|
|
3667
|
+
};
|
|
3668
|
+
|
|
3430
3669
|
// src/skills/clean-code.ts
|
|
3431
3670
|
var cleanCodeSkill = {
|
|
3432
3671
|
id: "clean-code",
|
|
@@ -3563,14 +3802,16 @@ var aiGuidelinesSkill = {
|
|
|
3563
3802
|
|
|
3564
3803
|
### Checklist Before Finishing a Task
|
|
3565
3804
|
1. Backend tests pass: \`cd backend && ./venv/bin/python manage.py test\`
|
|
3566
|
-
2. Frontend
|
|
3567
|
-
3.
|
|
3568
|
-
4.
|
|
3569
|
-
5.
|
|
3570
|
-
6.
|
|
3571
|
-
7.
|
|
3572
|
-
8.
|
|
3573
|
-
9.
|
|
3805
|
+
2. Frontend tests pass: \`cd frontend && npm test\`
|
|
3806
|
+
3. Frontend builds: \`cd frontend && npm run build\`
|
|
3807
|
+
4. API types are in sync: \`blacksmith sync\`
|
|
3808
|
+
5. No lint errors in modified files
|
|
3809
|
+
6. All UI uses \`@blacksmith-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
|
|
3810
|
+
7. Pages are modular \u2014 page file is a thin orchestrator, sections are in \`components/\`, logic in \`hooks/\`
|
|
3811
|
+
8. Logic is in hooks \u2014 no \`useApiQuery\`, \`useApiMutation\`, \`useEffect\`, or multi-\`useState\` in component bodies
|
|
3812
|
+
9. No hardcoded route paths \u2014 all paths use the \`Path\` enum from \`@/router/paths\`
|
|
3813
|
+
10. New routes have a corresponding \`Path\` enum entry
|
|
3814
|
+
11. **Tests are co-located** \u2014 every new or modified page, component, or hook has a corresponding \`.spec.tsx\` / \`.spec.ts\` in a \`__tests__/\` folder next to the source file (see the \`frontend-testing\` skill)
|
|
3574
3815
|
`;
|
|
3575
3816
|
}
|
|
3576
3817
|
};
|
|
@@ -3597,6 +3838,7 @@ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill })
|
|
|
3597
3838
|
skills.push(uiDesignSkill);
|
|
3598
3839
|
}
|
|
3599
3840
|
skills.push(blacksmithCliSkill);
|
|
3841
|
+
skills.push(frontendTestingSkill);
|
|
3600
3842
|
skills.push(programmingParadigmsSkill);
|
|
3601
3843
|
skills.push(cleanCodeSkill);
|
|
3602
3844
|
skills.push(aiGuidelinesSkill);
|
|
@@ -4299,6 +4541,7 @@ var allSkills = [
|
|
|
4299
4541
|
blacksmithUiAuthSkill,
|
|
4300
4542
|
blacksmithHooksSkill,
|
|
4301
4543
|
blacksmithCliSkill,
|
|
4544
|
+
frontendTestingSkill,
|
|
4302
4545
|
cleanCodeSkill,
|
|
4303
4546
|
aiGuidelinesSkill
|
|
4304
4547
|
];
|