@nextsparkjs/ai-workflow 0.1.0-beta.100
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/LICENSE +21 -0
- package/README.md +115 -0
- package/claude/_docs/workflows-optimizations.md +359 -0
- package/claude/agents/api-tester.md +634 -0
- package/claude/agents/architecture-supervisor.md +1351 -0
- package/claude/agents/backend-developer.md +997 -0
- package/claude/agents/backend-validator.md +417 -0
- package/claude/agents/bdd-docs-writer.md +737 -0
- package/claude/agents/block-developer.md +677 -0
- package/claude/agents/code-reviewer.md +1432 -0
- package/claude/agents/db-developer.md +721 -0
- package/claude/agents/db-validator.md +407 -0
- package/claude/agents/demo-video-generator.md +493 -0
- package/claude/agents/documentation-writer.md +1268 -0
- package/claude/agents/frontend-developer.md +1234 -0
- package/claude/agents/frontend-validator.md +777 -0
- package/claude/agents/functional-validator.md +630 -0
- package/claude/agents/mock-analyst.md +387 -0
- package/claude/agents/product-manager.md +963 -0
- package/claude/agents/qa-automation.md +1762 -0
- package/claude/agents/release-manager.md +634 -0
- package/claude/agents/selectors-translator.md +262 -0
- package/claude/agents/unit-test-writer.md +785 -0
- package/claude/agents/visual-comparator.md +329 -0
- package/claude/agents/workflow-maintainer.md +352 -0
- package/claude/commands/do/README.md +88 -0
- package/claude/commands/do/create-api.md +64 -0
- package/claude/commands/do/create-entity.md +66 -0
- package/claude/commands/do/create-migration.md +64 -0
- package/claude/commands/do/create-plugin.md +56 -0
- package/claude/commands/do/create-theme.md +70 -0
- package/claude/commands/do/mock-data.md +67 -0
- package/claude/commands/do/reset-db.md +71 -0
- package/claude/commands/do/setup-scheduled-action.md +75 -0
- package/claude/commands/do/sync-code-review.md +117 -0
- package/claude/commands/do/update-selectors.md +112 -0
- package/claude/commands/do/use-skills.md +90 -0
- package/claude/commands/do/validate-blocks.md +69 -0
- package/claude/commands/how-to/README.md +261 -0
- package/claude/commands/how-to/add-metadata.md +692 -0
- package/claude/commands/how-to/add-taxonomies.md +806 -0
- package/claude/commands/how-to/add-translations.md +571 -0
- package/claude/commands/how-to/create-api.md +577 -0
- package/claude/commands/how-to/create-block.md +575 -0
- package/claude/commands/how-to/create-child-entities.md +771 -0
- package/claude/commands/how-to/create-entity.md +597 -0
- package/claude/commands/how-to/create-migrations.md +605 -0
- package/claude/commands/how-to/create-plugin.md +654 -0
- package/claude/commands/how-to/customize-app.md +481 -0
- package/claude/commands/how-to/customize-dashboard.md +553 -0
- package/claude/commands/how-to/customize-theme.md +438 -0
- package/claude/commands/how-to/define-features-flows.md +632 -0
- package/claude/commands/how-to/deploy.md +507 -0
- package/claude/commands/how-to/handle-file-uploads.md +746 -0
- package/claude/commands/how-to/implement-search.md +1001 -0
- package/claude/commands/how-to/install-plugins.md +352 -0
- package/claude/commands/how-to/manage-test-coverage.md +984 -0
- package/claude/commands/how-to/run-tests.md +400 -0
- package/claude/commands/how-to/set-app-languages.md +601 -0
- package/claude/commands/how-to/set-plans-and-permissions.md +575 -0
- package/claude/commands/how-to/set-scheduled-actions.md +527 -0
- package/claude/commands/how-to/set-user-roles-and-permissions.md +550 -0
- package/claude/commands/how-to/setup-authentication.md +388 -0
- package/claude/commands/how-to/setup-claude-code.md +440 -0
- package/claude/commands/how-to/setup-database.md +274 -0
- package/claude/commands/how-to/setup-email-providers.md +598 -0
- package/claude/commands/how-to/setup-mobile-dev.md +627 -0
- package/claude/commands/how-to/start.md +500 -0
- package/claude/commands/how-to/use-devtools.md +639 -0
- package/claude/commands/how-to/use-superadmin.md +622 -0
- package/claude/commands/session/README.md +193 -0
- package/claude/commands/session/block-create.md +190 -0
- package/claude/commands/session/block-list.md +203 -0
- package/claude/commands/session/block-update.md +192 -0
- package/claude/commands/session/block-validate.md +218 -0
- package/claude/commands/session/changelog.md +115 -0
- package/claude/commands/session/close.md +225 -0
- package/claude/commands/session/commit.md +174 -0
- package/claude/commands/session/db-entity.md +206 -0
- package/claude/commands/session/db-fix.md +212 -0
- package/claude/commands/session/db-sample.md +206 -0
- package/claude/commands/session/demo.md +178 -0
- package/claude/commands/session/doc-bdd.md +207 -0
- package/claude/commands/session/doc-feature.md +218 -0
- package/claude/commands/session/doc-read.md +225 -0
- package/claude/commands/session/execute.md +204 -0
- package/claude/commands/session/explain.md +202 -0
- package/claude/commands/session/fix-bug.md +210 -0
- package/claude/commands/session/fix-build.md +182 -0
- package/claude/commands/session/fix-test.md +189 -0
- package/claude/commands/session/pending.md +232 -0
- package/claude/commands/session/refine.md +188 -0
- package/claude/commands/session/resume.md +192 -0
- package/claude/commands/session/review.md +192 -0
- package/claude/commands/session/scope-change.md +181 -0
- package/claude/commands/session/start-blocks.md +347 -0
- package/claude/commands/session/start.md +604 -0
- package/claude/commands/session/status.md +169 -0
- package/claude/commands/session/test-fix.md +221 -0
- package/claude/commands/session/test-run.md +203 -0
- package/claude/commands/session/test-write.md +242 -0
- package/claude/commands/session/validate.md +162 -0
- package/claude/config/context.json +40 -0
- package/claude/config/github.json +69 -0
- package/claude/config/github.schema.json +106 -0
- package/claude/config/team.json +46 -0
- package/claude/config/team.schema.json +106 -0
- package/claude/config/workspace.json +43 -0
- package/claude/config/workspace.schema.json +75 -0
- package/claude/skills/README.md +228 -0
- package/claude/skills/accessibility/SKILL.md +573 -0
- package/claude/skills/api-bypass-layers/SKILL.md +550 -0
- package/claude/skills/asana-integration/SKILL.md +499 -0
- package/claude/skills/better-auth/SKILL.md +666 -0
- package/claude/skills/billing-subscriptions/SKILL.md +660 -0
- package/claude/skills/block-decision-matrix/SKILL.md +359 -0
- package/claude/skills/clickup-integration/SKILL.md +434 -0
- package/claude/skills/core-theme-responsibilities/SKILL.md +485 -0
- package/claude/skills/create-plugin/SKILL.md +425 -0
- package/claude/skills/create-theme/SKILL.md +331 -0
- package/claude/skills/cypress-api/SKILL.md +511 -0
- package/claude/skills/cypress-api/scripts/generate-api-controller.py +329 -0
- package/claude/skills/cypress-api/scripts/generate-api-test.py +930 -0
- package/claude/skills/cypress-e2e/SKILL.md +526 -0
- package/claude/skills/cypress-e2e/scripts/extract-selectors.py +383 -0
- package/claude/skills/cypress-e2e/scripts/generate-uat-test.py +788 -0
- package/claude/skills/cypress-selectors/SKILL.md +309 -0
- package/claude/skills/cypress-selectors/scripts/extract-missing.py +243 -0
- package/claude/skills/cypress-selectors/scripts/generate-block-selectors.py +283 -0
- package/claude/skills/cypress-selectors/scripts/validate-selectors.py +145 -0
- package/claude/skills/database-migrations/SKILL.md +335 -0
- package/claude/skills/database-migrations/scripts/generate-sample-data.py +284 -0
- package/claude/skills/database-migrations/scripts/validate-migration.py +323 -0
- package/claude/skills/design-system/SKILL.md +682 -0
- package/claude/skills/documentation/SKILL.md +540 -0
- package/claude/skills/entity-api/SKILL.md +482 -0
- package/claude/skills/entity-system/SKILL.md +635 -0
- package/claude/skills/entity-system/scripts/generate-child-migration.py +298 -0
- package/claude/skills/entity-system/scripts/generate-metas-migration.py +233 -0
- package/claude/skills/entity-system/scripts/generate-migration.py +382 -0
- package/claude/skills/entity-system/scripts/generate-sample-data.py +418 -0
- package/claude/skills/entity-system/scripts/scaffold-entity.py +661 -0
- package/claude/skills/github/SKILL.md +467 -0
- package/claude/skills/i18n-nextintl/SKILL.md +302 -0
- package/claude/skills/i18n-nextintl/scripts/add-translation.py +243 -0
- package/claude/skills/i18n-nextintl/scripts/extract-hardcoded.py +246 -0
- package/claude/skills/i18n-nextintl/scripts/validate-translations.py +260 -0
- package/claude/skills/impact-analysis/SKILL.md +203 -0
- package/claude/skills/jest-unit/SKILL.md +306 -0
- package/claude/skills/jest-unit/references/component-testing.md +371 -0
- package/claude/skills/jest-unit/references/mocking-patterns.md +380 -0
- package/claude/skills/jest-unit/references/service-hook-testing.md +454 -0
- package/claude/skills/jira-integration/SKILL.md +539 -0
- package/claude/skills/media-library/SKILL.md +743 -0
- package/claude/skills/mock-analysis/SKILL.md +276 -0
- package/claude/skills/monorepo-architecture/SKILL.md +162 -0
- package/claude/skills/nextjs-api-development/SKILL.md +364 -0
- package/claude/skills/nextjs-api-development/scripts/generate-crud-tests.py +456 -0
- package/claude/skills/nextjs-api-development/scripts/scaffold-endpoint.py +481 -0
- package/claude/skills/nextjs-api-development/scripts/validate-api.py +283 -0
- package/claude/skills/notion-integration/SKILL.md +641 -0
- package/claude/skills/npm-development-workflow/SKILL.md +480 -0
- package/claude/skills/page-builder-blocks/SKILL.md +530 -0
- package/claude/skills/page-builder-blocks/scripts/scaffold-block.py +444 -0
- package/claude/skills/permissions-system/SKILL.md +619 -0
- package/claude/skills/plugins/SKILL.md +340 -0
- package/claude/skills/plugins/references/plugin-templates.md +414 -0
- package/claude/skills/plugins/references/plugin-testing.md +353 -0
- package/claude/skills/plugins/references/plugin-types.md +198 -0
- package/claude/skills/plugins/scripts/scaffold-plugin.py +443 -0
- package/claude/skills/pom-patterns/SKILL.md +452 -0
- package/claude/skills/pom-patterns/scripts/generate-pom.py +392 -0
- package/claude/skills/rate-limiting/SKILL.md +342 -0
- package/claude/skills/react-best-practices/AGENTS.md +2410 -0
- package/claude/skills/react-best-practices/README.md +123 -0
- package/claude/skills/react-best-practices/SKILL.md +125 -0
- package/claude/skills/react-best-practices/metadata.json +15 -0
- package/claude/skills/react-best-practices/rules/_sections.md +46 -0
- package/claude/skills/react-best-practices/rules/_template.md +28 -0
- package/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/claude/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
- package/claude/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/claude/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/claude/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/claude/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/claude/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/claude/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/claude/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/claude/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/claude/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/claude/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/claude/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/claude/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/claude/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/claude/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/claude/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/claude/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/claude/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/claude/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/claude/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/claude/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/claude/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/claude/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/claude/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/claude/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/claude/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/claude/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/claude/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/claude/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/claude/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/claude/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/claude/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/claude/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/claude/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/claude/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/claude/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/claude/skills/react-patterns/SKILL.md +688 -0
- package/claude/skills/registry-system/SKILL.md +331 -0
- package/claude/skills/scheduled-actions/SKILL.md +671 -0
- package/claude/skills/scope-enforcement/SKILL.md +542 -0
- package/claude/skills/scope-enforcement/scripts/validate-scope.py +357 -0
- package/claude/skills/server-actions/SKILL.md +493 -0
- package/claude/skills/service-layer/SKILL.md +587 -0
- package/claude/skills/session-management/SKILL.md +266 -0
- package/claude/skills/session-management/scripts/create-session.py +166 -0
- package/claude/skills/session-management/scripts/iteration-close.sh +105 -0
- package/claude/skills/session-management/scripts/iteration-init.sh +180 -0
- package/claude/skills/session-management/scripts/session-archive.sh +87 -0
- package/claude/skills/session-management/scripts/session-close.sh +133 -0
- package/claude/skills/session-management/scripts/session-init.sh +225 -0
- package/claude/skills/session-management/scripts/session-list.sh +163 -0
- package/claude/skills/session-management/scripts/split-plan.sh +116 -0
- package/claude/skills/shadcn-components/SKILL.md +586 -0
- package/claude/skills/shadcn-theming/SKILL.md +446 -0
- package/claude/skills/suspense-loading/SKILL.md +280 -0
- package/claude/skills/tailwind-theming/SKILL.md +507 -0
- package/claude/skills/tanstack-query/SKILL.md +608 -0
- package/claude/skills/test-coverage/SKILL.md +239 -0
- package/claude/skills/web-design-guidelines/SKILL.md +39 -0
- package/claude/skills/zod-validation/SKILL.md +537 -0
- package/claude/templates/blocks/progress.md +86 -0
- package/claude/templates/iteration/changes.md +61 -0
- package/claude/templates/iteration/progress.md +55 -0
- package/claude/templates/log.md +31 -0
- package/claude/templates/story/context.md +77 -0
- package/claude/templates/story/pendings.md +37 -0
- package/claude/templates/story/plan.md +299 -0
- package/claude/templates/story/requirements.md +109 -0
- package/claude/templates/story/scope.json +10 -0
- package/claude/templates/story/tests.md +91 -0
- package/claude/templates/task/progress.md +58 -0
- package/claude/templates/task/requirements.md +54 -0
- package/claude/workflows/README.md +154 -0
- package/claude/workflows/blocks.md +614 -0
- package/claude/workflows/story.md +1207 -0
- package/claude/workflows/task.md +927 -0
- package/claude/workflows/tweak.md +527 -0
- package/cursor/.gitkeep +0 -0
- package/package.json +35 -0
- package/scripts/postinstall.mjs +198 -0
- package/scripts/setup.mjs +282 -0
- package/scripts/sync.mjs +209 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jest-unit
|
|
3
|
+
description: |
|
|
4
|
+
Jest unit testing patterns for this Next.js application.
|
|
5
|
+
Covers test structure, mocking strategies, React component testing, hook testing, and coverage targets.
|
|
6
|
+
Use this skill when writing unit tests for services, hooks, utilities, or components.
|
|
7
|
+
allowed-tools: Read, Glob, Grep
|
|
8
|
+
version: 1.1.0
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Jest Unit Testing Skill
|
|
12
|
+
|
|
13
|
+
Patterns for writing effective unit tests with Jest and React Testing Library.
|
|
14
|
+
|
|
15
|
+
## Architecture Overview
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
TEST FILE STRUCTURE:
|
|
19
|
+
|
|
20
|
+
core/tests/jest/
|
|
21
|
+
├── api/ # API route tests
|
|
22
|
+
├── hooks/ # React hooks tests
|
|
23
|
+
├── lib/ # Utility tests
|
|
24
|
+
├── components/ # Component tests
|
|
25
|
+
├── services/ # Service tests
|
|
26
|
+
├── __mocks__/ # Mock utilities
|
|
27
|
+
│ ├── db-mocks.ts
|
|
28
|
+
│ ├── better-auth.js
|
|
29
|
+
│ └── next-server.js
|
|
30
|
+
└── setup.ts # Global configuration
|
|
31
|
+
|
|
32
|
+
contents/themes/default/tests/jest/
|
|
33
|
+
└── ... # Theme-specific tests
|
|
34
|
+
|
|
35
|
+
contents/plugins/*/__tests__/
|
|
36
|
+
└── ... # Plugin-specific tests
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## When to Use This Skill
|
|
40
|
+
|
|
41
|
+
- Writing unit tests for services
|
|
42
|
+
- Testing React hooks
|
|
43
|
+
- Testing React components
|
|
44
|
+
- Mocking database operations
|
|
45
|
+
- Setting up test coverage
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Test File Structure
|
|
50
|
+
|
|
51
|
+
### Naming Conventions
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
*.test.ts - TypeScript unit tests
|
|
55
|
+
*.test.tsx - React component tests
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Standard Test Structure
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals'
|
|
62
|
+
import { functionToTest } from '@/core/lib/module'
|
|
63
|
+
|
|
64
|
+
describe('ModuleName', () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
jest.clearAllMocks()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
jest.clearAllMocks()
|
|
71
|
+
jest.resetModules()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('Feature Group', () => {
|
|
75
|
+
test('should do expected behavior when condition met', () => {
|
|
76
|
+
// Arrange
|
|
77
|
+
const input = 'test-data'
|
|
78
|
+
|
|
79
|
+
// Act
|
|
80
|
+
const result = functionToTest(input)
|
|
81
|
+
|
|
82
|
+
// Assert
|
|
83
|
+
expect(result).toBe('expected-output')
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Jest Configuration
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// jest.config.ts
|
|
95
|
+
import type { Config } from 'jest'
|
|
96
|
+
|
|
97
|
+
export const baseConfig: Partial<Config> = {
|
|
98
|
+
preset: 'ts-jest',
|
|
99
|
+
testEnvironment: 'jsdom',
|
|
100
|
+
|
|
101
|
+
moduleNameMapper: {
|
|
102
|
+
'^@/(.*)$': '<rootDir>/$1',
|
|
103
|
+
'^@/core/(.*)$': '<rootDir>/core/$1',
|
|
104
|
+
'next/server': '<rootDir>/core/tests/jest/__mocks__/next-server.js',
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
setupFilesAfterEnv: ['<rootDir>/core/tests/setup.ts'],
|
|
108
|
+
testTimeout: 10000,
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Test Scripts
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pnpm test # Run all core tests
|
|
116
|
+
pnpm test:theme # Run theme-specific tests
|
|
117
|
+
pnpm test:coverage # Generate coverage reports
|
|
118
|
+
pnpm test:watch # Watch mode
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Essential Mocking (Quick Reference)
|
|
124
|
+
|
|
125
|
+
### Database (MANDATORY)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
jest.mock('@/core/lib/db', () => ({
|
|
129
|
+
queryWithRLS: jest.fn(),
|
|
130
|
+
queryOneWithRLS: jest.fn(),
|
|
131
|
+
mutateWithRLS: jest.fn(),
|
|
132
|
+
}))
|
|
133
|
+
|
|
134
|
+
const mockQueryWithRLS = queryWithRLS as jest.MockedFunction<typeof queryWithRLS>
|
|
135
|
+
|
|
136
|
+
test('example', async () => {
|
|
137
|
+
mockQueryWithRLS.mockResolvedValue([{ id: '123' }])
|
|
138
|
+
// ...
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Next.js Navigation
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
jest.mock('next/navigation', () => ({
|
|
146
|
+
useRouter: () => ({ push: jest.fn(), replace: jest.fn() }),
|
|
147
|
+
usePathname: () => '/',
|
|
148
|
+
useSearchParams: () => new URLSearchParams(),
|
|
149
|
+
}))
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Translations
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
jest.mock('next-intl', () => ({
|
|
156
|
+
useTranslations: () => (key: string) => key,
|
|
157
|
+
useLocale: () => 'en',
|
|
158
|
+
}))
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
→ See `references/mocking-patterns.md` for complete mocking strategies
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Quick Testing Patterns
|
|
166
|
+
|
|
167
|
+
### Component Test
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { render, screen } from '@testing-library/react'
|
|
171
|
+
import userEvent from '@testing-library/user-event'
|
|
172
|
+
|
|
173
|
+
test('form submission', async () => {
|
|
174
|
+
const user = userEvent.setup()
|
|
175
|
+
render(<MyForm />)
|
|
176
|
+
|
|
177
|
+
await user.type(screen.getByLabelText('Email'), 'test@example.com')
|
|
178
|
+
await user.click(screen.getByRole('button', { name: /submit/i }))
|
|
179
|
+
|
|
180
|
+
expect(await screen.findByText('Success')).toBeInTheDocument()
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
→ See `references/component-testing.md` for complete patterns
|
|
185
|
+
|
|
186
|
+
### Hook Test
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { renderHook, act } from '@testing-library/react'
|
|
190
|
+
|
|
191
|
+
test('hook state update', () => {
|
|
192
|
+
const { result } = renderHook(() => useCounter())
|
|
193
|
+
|
|
194
|
+
act(() => {
|
|
195
|
+
result.current.increment()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(result.current.count).toBe(1)
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
→ See `references/service-hook-testing.md` for complete patterns
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Coverage Targets
|
|
207
|
+
|
|
208
|
+
| Category | Target | Notes |
|
|
209
|
+
|----------|--------|-------|
|
|
210
|
+
| **Critical Paths** | 90%+ | Auth, API, Validation |
|
|
211
|
+
| **Features** | 80%+ | UI, Business Logic |
|
|
212
|
+
| **Utilities** | 80%+ | Helpers, Services |
|
|
213
|
+
| **Components** | 70%+ | UI Components |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Common Assertions
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Equality
|
|
221
|
+
expect(result).toBe('expected')
|
|
222
|
+
expect(result).toEqual({ id: '123' })
|
|
223
|
+
|
|
224
|
+
// Truthiness
|
|
225
|
+
expect(result).toBeTruthy()
|
|
226
|
+
expect(result).toBeNull()
|
|
227
|
+
expect(result).toBeDefined()
|
|
228
|
+
|
|
229
|
+
// Collections
|
|
230
|
+
expect(array).toHaveLength(2)
|
|
231
|
+
expect(array).toContain('item')
|
|
232
|
+
expect(obj).toHaveProperty('key')
|
|
233
|
+
|
|
234
|
+
// DOM (Testing Library)
|
|
235
|
+
expect(element).toBeInTheDocument()
|
|
236
|
+
expect(element).toHaveClass('className')
|
|
237
|
+
expect(element).toBeVisible()
|
|
238
|
+
expect(element).toBeDisabled()
|
|
239
|
+
|
|
240
|
+
// Mocks
|
|
241
|
+
expect(mockFn).toHaveBeenCalled()
|
|
242
|
+
expect(mockFn).toHaveBeenCalledWith('arg')
|
|
243
|
+
expect(mockFn).toHaveBeenCalledTimes(1)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Anti-Patterns
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// ❌ NEVER: Test implementation details
|
|
252
|
+
expect(result.current._internalState).toBe('value')
|
|
253
|
+
|
|
254
|
+
// ✅ CORRECT: Test observable behavior
|
|
255
|
+
expect(result.current.displayValue).toBe('value')
|
|
256
|
+
|
|
257
|
+
// ❌ NEVER: Skip cleanup
|
|
258
|
+
// No afterEach → leaks between tests
|
|
259
|
+
|
|
260
|
+
// ✅ CORRECT: Always cleanup
|
|
261
|
+
afterEach(() => jest.clearAllMocks())
|
|
262
|
+
|
|
263
|
+
// ❌ NEVER: Async without await
|
|
264
|
+
fetchData().then(data => expect(data).toBeDefined())
|
|
265
|
+
|
|
266
|
+
// ✅ CORRECT: Properly await
|
|
267
|
+
const data = await fetchData()
|
|
268
|
+
expect(data).toBeDefined()
|
|
269
|
+
|
|
270
|
+
// ❌ NEVER: Use fireEvent for user actions
|
|
271
|
+
fireEvent.change(input, { target: { value: 'text' } })
|
|
272
|
+
|
|
273
|
+
// ✅ CORRECT: Use userEvent
|
|
274
|
+
await user.type(input, 'text')
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Checklist
|
|
280
|
+
|
|
281
|
+
Before finalizing unit tests:
|
|
282
|
+
|
|
283
|
+
- [ ] All database calls mocked (queryWithRLS, mutateWithRLS)
|
|
284
|
+
- [ ] External APIs mocked (fetch)
|
|
285
|
+
- [ ] Next.js functions mocked (useRouter, useSearchParams)
|
|
286
|
+
- [ ] afterEach cleanup with jest.clearAllMocks()
|
|
287
|
+
- [ ] Async tests properly awaited
|
|
288
|
+
- [ ] Test both success and error paths
|
|
289
|
+
- [ ] Edge cases covered (null, empty, invalid input)
|
|
290
|
+
- [ ] Coverage targets met (90%+ critical, 80%+ features)
|
|
291
|
+
- [ ] Test names are descriptive
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## References
|
|
296
|
+
|
|
297
|
+
- `references/mocking-patterns.md` - Complete mocking strategies
|
|
298
|
+
- `references/component-testing.md` - React component testing patterns
|
|
299
|
+
- `references/service-hook-testing.md` - Service and hook testing patterns
|
|
300
|
+
|
|
301
|
+
## Related Skills
|
|
302
|
+
|
|
303
|
+
- `cypress-e2e` - Integration/E2E testing
|
|
304
|
+
- `cypress-api` - API testing with Cypress
|
|
305
|
+
- `zod-validation` - Schema validation testing
|
|
306
|
+
- `service-layer` - Service patterns to test
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Component Testing Reference
|
|
2
|
+
|
|
3
|
+
React component testing patterns with Testing Library and Jest.
|
|
4
|
+
|
|
5
|
+
## Basic Component Test Structure
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { describe, test, expect, beforeEach, jest } from '@jest/globals'
|
|
9
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
10
|
+
import userEvent from '@testing-library/user-event'
|
|
11
|
+
import { LoginForm } from '@/core/components/auth/forms/LoginForm'
|
|
12
|
+
|
|
13
|
+
describe('LoginForm Component', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('Rendering', () => {
|
|
19
|
+
test('should render login form with all essential elements', () => {
|
|
20
|
+
render(<LoginForm />)
|
|
21
|
+
|
|
22
|
+
expect(screen.getByText('Sign In')).toBeInTheDocument()
|
|
23
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument()
|
|
24
|
+
expect(screen.getByLabelText('Password')).toBeInTheDocument()
|
|
25
|
+
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument()
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('User Interactions', () => {
|
|
30
|
+
test('should call signIn with correct credentials', async () => {
|
|
31
|
+
const user = userEvent.setup()
|
|
32
|
+
|
|
33
|
+
render(<LoginForm />)
|
|
34
|
+
|
|
35
|
+
await user.type(screen.getByLabelText('Email'), 'test@example.com')
|
|
36
|
+
await user.type(screen.getByLabelText('Password'), 'password123')
|
|
37
|
+
await user.click(screen.getByRole('button', { name: /sign in/i }))
|
|
38
|
+
|
|
39
|
+
await waitFor(() => {
|
|
40
|
+
expect(mockSignIn).toHaveBeenCalledWith({
|
|
41
|
+
email: 'test@example.com',
|
|
42
|
+
password: 'password123'
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('Error Handling', () => {
|
|
49
|
+
test('should display error message on failed login', async () => {
|
|
50
|
+
const user = userEvent.setup()
|
|
51
|
+
mockSignIn.mockRejectedValue(new Error('Invalid credentials'))
|
|
52
|
+
|
|
53
|
+
render(<LoginForm />)
|
|
54
|
+
|
|
55
|
+
await user.type(screen.getByLabelText('Email'), 'test@example.com')
|
|
56
|
+
await user.type(screen.getByLabelText('Password'), 'wrong')
|
|
57
|
+
await user.click(screen.getByRole('button', { name: /sign in/i }))
|
|
58
|
+
|
|
59
|
+
await waitFor(() => {
|
|
60
|
+
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('Accessibility', () => {
|
|
66
|
+
test('should have proper accessibility attributes', () => {
|
|
67
|
+
render(<LoginForm />)
|
|
68
|
+
|
|
69
|
+
const emailInput = screen.getByLabelText('Email')
|
|
70
|
+
expect(emailInput).toHaveAttribute('type', 'email')
|
|
71
|
+
expect(emailInput).toHaveAttribute('aria-required', 'true')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## userEvent vs fireEvent
|
|
80
|
+
|
|
81
|
+
### userEvent (Preferred)
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import userEvent from '@testing-library/user-event'
|
|
85
|
+
|
|
86
|
+
// ✅ CORRECT - Realistic user interactions
|
|
87
|
+
test('user types in input field', async () => {
|
|
88
|
+
const user = userEvent.setup()
|
|
89
|
+
|
|
90
|
+
render(<MyComponent />)
|
|
91
|
+
|
|
92
|
+
await user.type(screen.getByRole('textbox'), 'Hello')
|
|
93
|
+
await user.click(screen.getByRole('button'))
|
|
94
|
+
await user.keyboard('{Enter}')
|
|
95
|
+
await user.tab()
|
|
96
|
+
|
|
97
|
+
expect(screen.getByText('Hello')).toBeInTheDocument()
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### fireEvent (Only for Edge Cases)
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Use fireEvent only when userEvent doesn't work
|
|
105
|
+
// (e.g., custom events, scroll, resize)
|
|
106
|
+
test('handles scroll event', () => {
|
|
107
|
+
render(<ScrollComponent />)
|
|
108
|
+
|
|
109
|
+
const container = screen.getByTestId('scroll-container')
|
|
110
|
+
fireEvent.scroll(container, { target: { scrollY: 100 } })
|
|
111
|
+
|
|
112
|
+
expect(screen.getByText('Scrolled')).toBeInTheDocument()
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Common userEvent Actions
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const user = userEvent.setup()
|
|
122
|
+
|
|
123
|
+
// Typing
|
|
124
|
+
await user.type(input, 'text') // Type text
|
|
125
|
+
await user.clear(input) // Clear input
|
|
126
|
+
await user.type(input, 'text{Enter}') // Type and submit
|
|
127
|
+
|
|
128
|
+
// Clicking
|
|
129
|
+
await user.click(element) // Single click
|
|
130
|
+
await user.dblClick(element) // Double click
|
|
131
|
+
await user.tripleClick(element) // Triple click (select all)
|
|
132
|
+
|
|
133
|
+
// Keyboard
|
|
134
|
+
await user.keyboard('{Enter}') // Press Enter
|
|
135
|
+
await user.keyboard('{Tab}') // Press Tab
|
|
136
|
+
await user.keyboard('{Escape}') // Press Escape
|
|
137
|
+
await user.keyboard('{ArrowDown}') // Arrow keys
|
|
138
|
+
await user.keyboard('{Shift>}A{/Shift}') // Shift+A
|
|
139
|
+
|
|
140
|
+
// Selection
|
|
141
|
+
await user.selectOptions(select, 'option-value')
|
|
142
|
+
await user.selectOptions(select, ['opt1', 'opt2']) // Multi-select
|
|
143
|
+
|
|
144
|
+
// Hover
|
|
145
|
+
await user.hover(element)
|
|
146
|
+
await user.unhover(element)
|
|
147
|
+
|
|
148
|
+
// Clipboard
|
|
149
|
+
await user.copy()
|
|
150
|
+
await user.paste()
|
|
151
|
+
await user.cut()
|
|
152
|
+
|
|
153
|
+
// Focus
|
|
154
|
+
await user.tab() // Move focus forward
|
|
155
|
+
await user.tab({ shift: true }) // Move focus backward
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Query Priority
|
|
161
|
+
|
|
162
|
+
Testing Library recommends queries in this priority order:
|
|
163
|
+
|
|
164
|
+
### 1. Accessible to Everyone (Preferred)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// By Role (best)
|
|
168
|
+
screen.getByRole('button', { name: /submit/i })
|
|
169
|
+
screen.getByRole('textbox', { name: /email/i })
|
|
170
|
+
screen.getByRole('heading', { level: 1 })
|
|
171
|
+
|
|
172
|
+
// By Label Text
|
|
173
|
+
screen.getByLabelText('Email')
|
|
174
|
+
|
|
175
|
+
// By Placeholder Text
|
|
176
|
+
screen.getByPlaceholderText('Enter email...')
|
|
177
|
+
|
|
178
|
+
// By Text Content
|
|
179
|
+
screen.getByText('Submit')
|
|
180
|
+
screen.getByText(/submit/i) // Case insensitive
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 2. Semantic Queries
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
screen.getByAltText('Profile photo')
|
|
187
|
+
screen.getByTitle('Close dialog')
|
|
188
|
+
screen.getByDisplayValue('current value')
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 3. Test IDs (Last Resort)
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// Only when other queries don't work
|
|
195
|
+
screen.getByTestId('complex-component')
|
|
196
|
+
screen.getByTestId('data-cy-selector')
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Async Testing Patterns
|
|
202
|
+
|
|
203
|
+
### waitFor
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
test('should show loading then data', async () => {
|
|
207
|
+
render(<DataComponent />)
|
|
208
|
+
|
|
209
|
+
// Initially loading
|
|
210
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
|
211
|
+
|
|
212
|
+
// Wait for data
|
|
213
|
+
await waitFor(() => {
|
|
214
|
+
expect(screen.getByText('Data loaded')).toBeInTheDocument()
|
|
215
|
+
}, { timeout: 3000 })
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### findBy (Combines getBy + waitFor)
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
test('should load async content', async () => {
|
|
223
|
+
render(<AsyncComponent />)
|
|
224
|
+
|
|
225
|
+
// findBy waits for element to appear
|
|
226
|
+
const element = await screen.findByText('Loaded content', {}, { timeout: 5000 })
|
|
227
|
+
expect(element).toBeInTheDocument()
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### waitForElementToBeRemoved
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
test('should hide loading spinner', async () => {
|
|
235
|
+
render(<DataComponent />)
|
|
236
|
+
|
|
237
|
+
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'))
|
|
238
|
+
|
|
239
|
+
expect(screen.getByText('Data')).toBeInTheDocument()
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Form Testing
|
|
246
|
+
|
|
247
|
+
### Basic Form
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
test('submits form with correct data', async () => {
|
|
251
|
+
const onSubmit = jest.fn()
|
|
252
|
+
const user = userEvent.setup()
|
|
253
|
+
|
|
254
|
+
render(<ContactForm onSubmit={onSubmit} />)
|
|
255
|
+
|
|
256
|
+
await user.type(screen.getByLabelText('Name'), 'John Doe')
|
|
257
|
+
await user.type(screen.getByLabelText('Email'), 'john@example.com')
|
|
258
|
+
await user.type(screen.getByLabelText('Message'), 'Hello!')
|
|
259
|
+
await user.click(screen.getByRole('button', { name: /submit/i }))
|
|
260
|
+
|
|
261
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
262
|
+
name: 'John Doe',
|
|
263
|
+
email: 'john@example.com',
|
|
264
|
+
message: 'Hello!'
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Form Validation
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
test('shows validation errors', async () => {
|
|
273
|
+
const user = userEvent.setup()
|
|
274
|
+
|
|
275
|
+
render(<ContactForm />)
|
|
276
|
+
|
|
277
|
+
// Submit empty form
|
|
278
|
+
await user.click(screen.getByRole('button', { name: /submit/i }))
|
|
279
|
+
|
|
280
|
+
// Check for validation errors
|
|
281
|
+
expect(await screen.findByText('Name is required')).toBeInTheDocument()
|
|
282
|
+
expect(await screen.findByText('Email is required')).toBeInTheDocument()
|
|
283
|
+
})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Testing with Context/Providers
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const renderWithProviders = (ui: React.ReactElement, options = {}) => {
|
|
292
|
+
const AllProviders = ({ children }: { children: React.ReactNode }) => (
|
|
293
|
+
<QueryClientProvider client={queryClient}>
|
|
294
|
+
<ThemeProvider>
|
|
295
|
+
<AuthProvider>
|
|
296
|
+
{children}
|
|
297
|
+
</AuthProvider>
|
|
298
|
+
</ThemeProvider>
|
|
299
|
+
</QueryClientProvider>
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return render(ui, { wrapper: AllProviders, ...options })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Usage
|
|
306
|
+
test('renders with all providers', () => {
|
|
307
|
+
renderWithProviders(<MyComponent />)
|
|
308
|
+
// ...
|
|
309
|
+
})
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Testing Modals/Dialogs
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
test('opens and closes modal', async () => {
|
|
318
|
+
const user = userEvent.setup()
|
|
319
|
+
|
|
320
|
+
render(<ModalComponent />)
|
|
321
|
+
|
|
322
|
+
// Open modal
|
|
323
|
+
await user.click(screen.getByRole('button', { name: /open/i }))
|
|
324
|
+
|
|
325
|
+
// Check modal is visible
|
|
326
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
327
|
+
|
|
328
|
+
// Close modal
|
|
329
|
+
await user.click(screen.getByRole('button', { name: /close/i }))
|
|
330
|
+
|
|
331
|
+
// Check modal is gone
|
|
332
|
+
await waitFor(() => {
|
|
333
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Common Assertions (DOM)
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// Presence
|
|
344
|
+
expect(element).toBeInTheDocument()
|
|
345
|
+
expect(element).not.toBeInTheDocument()
|
|
346
|
+
|
|
347
|
+
// Visibility
|
|
348
|
+
expect(element).toBeVisible()
|
|
349
|
+
expect(element).not.toBeVisible()
|
|
350
|
+
|
|
351
|
+
// State
|
|
352
|
+
expect(element).toBeDisabled()
|
|
353
|
+
expect(element).toBeEnabled()
|
|
354
|
+
expect(element).toBeChecked()
|
|
355
|
+
expect(element).toHaveFocus()
|
|
356
|
+
|
|
357
|
+
// Content
|
|
358
|
+
expect(element).toHaveTextContent('text')
|
|
359
|
+
expect(element).toHaveTextContent(/regex/i)
|
|
360
|
+
expect(element).toBeEmpty()
|
|
361
|
+
|
|
362
|
+
// Attributes
|
|
363
|
+
expect(element).toHaveAttribute('href', '/path')
|
|
364
|
+
expect(element).toHaveClass('active')
|
|
365
|
+
expect(element).toHaveStyle({ color: 'red' })
|
|
366
|
+
|
|
367
|
+
// Form
|
|
368
|
+
expect(input).toHaveValue('value')
|
|
369
|
+
expect(input).toHaveDisplayValue('displayed')
|
|
370
|
+
expect(select).toHaveValue('option-value')
|
|
371
|
+
```
|