@girardmedia/bootspring 3.3.2 → 3.4.0
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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: terraform-patterns
|
|
3
|
+
description: Terraform and IaC patterns for modules, state management, workspaces, drift detection, and import.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Terraform / IaC Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when managing cloud infrastructure with Terraform 1.5+. Use
|
|
11
|
+
this skill for structuring reusable modules, managing state safely, isolating
|
|
12
|
+
environments with workspaces, detecting configuration drift, and importing
|
|
13
|
+
existing resources under Terraform management.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### Module Structure
|
|
18
|
+
|
|
19
|
+
Create reusable modules with clear inputs, outputs, and documentation. A module
|
|
20
|
+
should represent one logical unit (VPC, ECS service, RDS instance).
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
modules/
|
|
24
|
+
vpc/
|
|
25
|
+
main.tf # resources
|
|
26
|
+
variables.tf # input variables
|
|
27
|
+
outputs.tf # output values
|
|
28
|
+
versions.tf # required providers and versions
|
|
29
|
+
README.md # usage documentation
|
|
30
|
+
ecs-service/
|
|
31
|
+
main.tf
|
|
32
|
+
variables.tf
|
|
33
|
+
outputs.tf
|
|
34
|
+
iam.tf # IAM roles/policies for this module
|
|
35
|
+
versions.tf
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```hcl
|
|
39
|
+
# modules/vpc/variables.tf
|
|
40
|
+
variable "name" {
|
|
41
|
+
type = string
|
|
42
|
+
description = "Name prefix for all VPC resources"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
variable "cidr" {
|
|
46
|
+
type = string
|
|
47
|
+
description = "CIDR block for the VPC"
|
|
48
|
+
default = "10.0.0.0/16"
|
|
49
|
+
|
|
50
|
+
validation {
|
|
51
|
+
condition = can(cidrhost(var.cidr, 0))
|
|
52
|
+
error_message = "Must be a valid CIDR block."
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
variable "availability_zones" {
|
|
57
|
+
type = list(string)
|
|
58
|
+
description = "List of AZs to deploy subnets into"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# modules/vpc/outputs.tf
|
|
62
|
+
output "vpc_id" {
|
|
63
|
+
value = aws_vpc.main.id
|
|
64
|
+
description = "ID of the created VPC"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
output "private_subnet_ids" {
|
|
68
|
+
value = aws_subnet.private[*].id
|
|
69
|
+
description = "IDs of private subnets"
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Root Module Composition
|
|
74
|
+
|
|
75
|
+
The root module wires together child modules and passes configuration. Keep
|
|
76
|
+
resource definitions in modules; the root should be mostly module calls.
|
|
77
|
+
|
|
78
|
+
```hcl
|
|
79
|
+
# environments/production/main.tf
|
|
80
|
+
module "vpc" {
|
|
81
|
+
source = "../../modules/vpc"
|
|
82
|
+
name = "prod"
|
|
83
|
+
cidr = "10.0.0.0/16"
|
|
84
|
+
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module "database" {
|
|
88
|
+
source = "../../modules/rds"
|
|
89
|
+
name = "prod-db"
|
|
90
|
+
vpc_id = module.vpc.vpc_id
|
|
91
|
+
subnet_ids = module.vpc.private_subnet_ids
|
|
92
|
+
instance_class = "db.r6g.large"
|
|
93
|
+
allocated_storage = 100
|
|
94
|
+
multi_az = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module "api" {
|
|
98
|
+
source = "../../modules/ecs-service"
|
|
99
|
+
name = "api"
|
|
100
|
+
vpc_id = module.vpc.vpc_id
|
|
101
|
+
subnet_ids = module.vpc.private_subnet_ids
|
|
102
|
+
image = "${var.ecr_repo}:${var.image_tag}"
|
|
103
|
+
cpu = 512
|
|
104
|
+
memory = 1024
|
|
105
|
+
desired_count = 3
|
|
106
|
+
|
|
107
|
+
environment = {
|
|
108
|
+
DATABASE_URL = module.database.connection_string
|
|
109
|
+
NODE_ENV = "production"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### State Management
|
|
115
|
+
|
|
116
|
+
Remote state prevents conflicts. Use S3 + DynamoDB for locking. Never store
|
|
117
|
+
state locally in production. Separate state per environment.
|
|
118
|
+
|
|
119
|
+
```hcl
|
|
120
|
+
# environments/production/backend.tf
|
|
121
|
+
terraform {
|
|
122
|
+
backend "s3" {
|
|
123
|
+
bucket = "mycompany-terraform-state"
|
|
124
|
+
key = "production/terraform.tfstate"
|
|
125
|
+
region = "us-east-1"
|
|
126
|
+
dynamodb_table = "terraform-locks"
|
|
127
|
+
encrypt = true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```hcl
|
|
133
|
+
# State locking table (create once, outside Terraform)
|
|
134
|
+
resource "aws_dynamodb_table" "terraform_locks" {
|
|
135
|
+
name = "terraform-locks"
|
|
136
|
+
billing_mode = "PAY_PER_REQUEST"
|
|
137
|
+
hash_key = "LockID"
|
|
138
|
+
|
|
139
|
+
attribute {
|
|
140
|
+
name = "LockID"
|
|
141
|
+
type = "S"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Workspaces vs. Directory Structure
|
|
147
|
+
|
|
148
|
+
Use **directory-per-environment** for significantly different configs. Use
|
|
149
|
+
**workspaces** only when environments share identical structure and differ
|
|
150
|
+
only in variable values.
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
# Directory approach (recommended for most teams)
|
|
154
|
+
environments/
|
|
155
|
+
production/
|
|
156
|
+
main.tf
|
|
157
|
+
terraform.tfvars
|
|
158
|
+
backend.tf
|
|
159
|
+
staging/
|
|
160
|
+
main.tf
|
|
161
|
+
terraform.tfvars
|
|
162
|
+
backend.tf
|
|
163
|
+
|
|
164
|
+
# Workspace approach (same structure, different values)
|
|
165
|
+
terraform workspace select production
|
|
166
|
+
terraform apply -var-file="production.tfvars"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Drift Detection
|
|
170
|
+
|
|
171
|
+
Run `terraform plan` on a schedule (CI/CD) to detect manual changes. Alert on
|
|
172
|
+
any planned changes when none are expected.
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
# .github/workflows/drift-detection.yml
|
|
176
|
+
name: Terraform Drift Detection
|
|
177
|
+
on:
|
|
178
|
+
schedule:
|
|
179
|
+
- cron: '0 8 * * 1-5' # weekday mornings
|
|
180
|
+
|
|
181
|
+
jobs:
|
|
182
|
+
detect-drift:
|
|
183
|
+
runs-on: ubuntu-latest
|
|
184
|
+
steps:
|
|
185
|
+
- uses: actions/checkout@v4
|
|
186
|
+
- uses: hashicorp/setup-terraform@v3
|
|
187
|
+
- run: terraform init
|
|
188
|
+
working-directory: environments/production
|
|
189
|
+
- run: terraform plan -detailed-exitcode -out=plan.tfplan
|
|
190
|
+
working-directory: environments/production
|
|
191
|
+
id: plan
|
|
192
|
+
continue-on-error: true
|
|
193
|
+
- if: steps.plan.outcome == 'failure'
|
|
194
|
+
run: |
|
|
195
|
+
echo "Drift detected in production!"
|
|
196
|
+
# Send alert to Slack/PagerDuty
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Import Existing Resources
|
|
200
|
+
|
|
201
|
+
Use `import` blocks (Terraform 1.5+) to bring existing resources under management
|
|
202
|
+
without destroying and recreating them.
|
|
203
|
+
|
|
204
|
+
```hcl
|
|
205
|
+
# Import an existing S3 bucket
|
|
206
|
+
import {
|
|
207
|
+
to = aws_s3_bucket.existing_logs
|
|
208
|
+
id = "my-existing-log-bucket"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
resource "aws_s3_bucket" "existing_logs" {
|
|
212
|
+
bucket = "my-existing-log-bucket"
|
|
213
|
+
# Configuration matching the existing resource
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Generate config for imported resources
|
|
219
|
+
terraform plan -generate-config-out=generated.tf
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Variable Validation and Locals
|
|
223
|
+
|
|
224
|
+
Use `validation` blocks on variables. Use `locals` for computed values to
|
|
225
|
+
keep resource blocks clean.
|
|
226
|
+
|
|
227
|
+
```hcl
|
|
228
|
+
variable "environment" {
|
|
229
|
+
type = string
|
|
230
|
+
validation {
|
|
231
|
+
condition = contains(["dev", "staging", "production"], var.environment)
|
|
232
|
+
error_message = "Environment must be dev, staging, or production."
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
locals {
|
|
237
|
+
common_tags = {
|
|
238
|
+
Environment = var.environment
|
|
239
|
+
ManagedBy = "terraform"
|
|
240
|
+
Project = var.project_name
|
|
241
|
+
}
|
|
242
|
+
name_prefix = "${var.project_name}-${var.environment}"
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Examples
|
|
247
|
+
|
|
248
|
+
**Pattern: Conditional resource creation**
|
|
249
|
+
```hcl
|
|
250
|
+
resource "aws_cloudwatch_metric_alarm" "cpu" {
|
|
251
|
+
count = var.enable_alarms ? 1 : 0
|
|
252
|
+
alarm_name = "${local.name_prefix}-cpu-high"
|
|
253
|
+
comparison_operator = "GreaterThanThreshold"
|
|
254
|
+
threshold = 80
|
|
255
|
+
# ...
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Checklist
|
|
260
|
+
|
|
261
|
+
- [ ] One logical unit per module (VPC, service, database)
|
|
262
|
+
- [ ] All variables have `type`, `description`, and `validation` where applicable
|
|
263
|
+
- [ ] All outputs have `description`
|
|
264
|
+
- [ ] Remote state with S3 + DynamoDB locking, encrypted at rest
|
|
265
|
+
- [ ] Separate state files per environment
|
|
266
|
+
- [ ] `terraform fmt` and `terraform validate` in CI
|
|
267
|
+
- [ ] Drift detection runs on a schedule, alerts on unexpected changes
|
|
268
|
+
- [ ] `import` blocks for existing resources (Terraform 1.5+)
|
|
269
|
+
- [ ] `locals` for computed values and common tags
|
|
270
|
+
- [ ] Provider versions pinned with `~>` in `versions.tf`
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-react
|
|
3
|
+
description: React Testing Library patterns including render, screen queries, userEvent, async testing, and custom hooks testing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Testing Library Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use React Testing Library when testing React components from the user's perspective. These patterns focus on testing behavior rather than implementation details, querying by accessible roles and text rather than CSS selectors or component internals. Apply these patterns for unit tests of individual components, integration tests of connected components, and tests of custom hooks.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Basic Component Rendering
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { render, screen } from '@testing-library/react';
|
|
18
|
+
import { Badge } from '../src/components/Badge';
|
|
19
|
+
|
|
20
|
+
describe('Badge', () => {
|
|
21
|
+
it('renders the label text', () => {
|
|
22
|
+
render(<Badge label="New" variant="info" />);
|
|
23
|
+
|
|
24
|
+
expect(screen.getByText('New')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('applies the correct variant class', () => {
|
|
28
|
+
render(<Badge label="Error" variant="danger" />);
|
|
29
|
+
|
|
30
|
+
const badge = screen.getByText('Error');
|
|
31
|
+
expect(badge).toHaveClass('badge-danger');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders nothing when label is empty', () => {
|
|
35
|
+
const { container } = render(<Badge label="" variant="info" />);
|
|
36
|
+
expect(container.firstChild).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### User Interactions with userEvent
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
45
|
+
import { render, screen } from '@testing-library/react';
|
|
46
|
+
import userEvent from '@testing-library/user-event';
|
|
47
|
+
import { LoginForm } from '../src/components/LoginForm';
|
|
48
|
+
|
|
49
|
+
describe('LoginForm', () => {
|
|
50
|
+
it('submits with email and password', async () => {
|
|
51
|
+
const user = userEvent.setup();
|
|
52
|
+
const onSubmit = vi.fn();
|
|
53
|
+
|
|
54
|
+
render(<LoginForm onSubmit={onSubmit} />);
|
|
55
|
+
|
|
56
|
+
await user.type(screen.getByLabelText('Email'), 'alice@example.com');
|
|
57
|
+
await user.type(screen.getByLabelText('Password'), 'secret123');
|
|
58
|
+
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
59
|
+
|
|
60
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
61
|
+
email: 'alice@example.com',
|
|
62
|
+
password: 'secret123',
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('disables submit when fields are empty', async () => {
|
|
67
|
+
render(<LoginForm onSubmit={vi.fn()} />);
|
|
68
|
+
|
|
69
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
70
|
+
expect(submitButton).toBeDisabled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('shows validation error for invalid email', async () => {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
render(<LoginForm onSubmit={vi.fn()} />);
|
|
76
|
+
|
|
77
|
+
await user.type(screen.getByLabelText('Email'), 'not-an-email');
|
|
78
|
+
await user.tab();
|
|
79
|
+
|
|
80
|
+
expect(screen.getByText('Please enter a valid email')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Async Testing with waitFor and findBy
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
89
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
90
|
+
import { UserList } from '../src/components/UserList';
|
|
91
|
+
import * as api from '../src/api';
|
|
92
|
+
|
|
93
|
+
vi.mock('../src/api');
|
|
94
|
+
|
|
95
|
+
describe('UserList', () => {
|
|
96
|
+
it('displays users after loading', async () => {
|
|
97
|
+
vi.mocked(api.fetchUsers).mockResolvedValueOnce([
|
|
98
|
+
{ id: '1', name: 'Alice' },
|
|
99
|
+
{ id: '2', name: 'Bob' },
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
render(<UserList />);
|
|
103
|
+
|
|
104
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
105
|
+
|
|
106
|
+
const alice = await screen.findByText('Alice');
|
|
107
|
+
expect(alice).toBeInTheDocument();
|
|
108
|
+
expect(screen.getByText('Bob')).toBeInTheDocument();
|
|
109
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('shows error message on fetch failure', async () => {
|
|
113
|
+
vi.mocked(api.fetchUsers).mockRejectedValueOnce(new Error('Server error'));
|
|
114
|
+
|
|
115
|
+
render(<UserList />);
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Failed to load users');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Testing Custom Hooks
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
128
|
+
import { renderHook, act } from '@testing-library/react';
|
|
129
|
+
import { useDebounce } from '../src/hooks/useDebounce';
|
|
130
|
+
|
|
131
|
+
describe('useDebounce', () => {
|
|
132
|
+
it('returns debounced value after delay', async () => {
|
|
133
|
+
vi.useFakeTimers();
|
|
134
|
+
|
|
135
|
+
const { result, rerender } = renderHook(
|
|
136
|
+
({ value }) => useDebounce(value, 500),
|
|
137
|
+
{ initialProps: { value: 'hello' } },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(result.current).toBe('hello');
|
|
141
|
+
|
|
142
|
+
rerender({ value: 'hello world' });
|
|
143
|
+
expect(result.current).toBe('hello');
|
|
144
|
+
|
|
145
|
+
act(() => {
|
|
146
|
+
vi.advanceTimersByTime(500);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(result.current).toBe('hello world');
|
|
150
|
+
vi.useRealTimers();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Testing with Providers (Router, Theme, Store)
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { describe, it, expect } from 'vitest';
|
|
159
|
+
import { render, screen } from '@testing-library/react';
|
|
160
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
161
|
+
import { ThemeProvider } from '../src/context/ThemeContext';
|
|
162
|
+
import { Navbar } from '../src/components/Navbar';
|
|
163
|
+
|
|
164
|
+
function renderWithProviders(ui: React.ReactElement, { route = '/' } = {}) {
|
|
165
|
+
return render(
|
|
166
|
+
<MemoryRouter initialEntries={[route]}>
|
|
167
|
+
<ThemeProvider defaultTheme="light">
|
|
168
|
+
{ui}
|
|
169
|
+
</ThemeProvider>
|
|
170
|
+
</MemoryRouter>,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describe('Navbar', () => {
|
|
175
|
+
it('highlights the active link', () => {
|
|
176
|
+
renderWithProviders(<Navbar />, { route: '/settings' });
|
|
177
|
+
|
|
178
|
+
const settingsLink = screen.getByRole('link', { name: /settings/i });
|
|
179
|
+
expect(settingsLink).toHaveAttribute('aria-current', 'page');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('renders all navigation items', () => {
|
|
183
|
+
renderWithProviders(<Navbar />);
|
|
184
|
+
|
|
185
|
+
expect(screen.getByRole('link', { name: /dashboard/i })).toBeInTheDocument();
|
|
186
|
+
expect(screen.getByRole('link', { name: /projects/i })).toBeInTheDocument();
|
|
187
|
+
expect(screen.getByRole('link', { name: /settings/i })).toBeInTheDocument();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Accessibility Queries Priority
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { describe, it, expect } from 'vitest';
|
|
196
|
+
import { render, screen } from '@testing-library/react';
|
|
197
|
+
import { SearchBar } from '../src/components/SearchBar';
|
|
198
|
+
|
|
199
|
+
describe('SearchBar accessibility', () => {
|
|
200
|
+
it('uses accessible queries in priority order', () => {
|
|
201
|
+
render(<SearchBar placeholder="Search projects..." />);
|
|
202
|
+
|
|
203
|
+
// 1. getByRole - best, matches ARIA roles
|
|
204
|
+
screen.getByRole('searchbox');
|
|
205
|
+
|
|
206
|
+
// 2. getByLabelText - for form fields
|
|
207
|
+
screen.getByLabelText('Search');
|
|
208
|
+
|
|
209
|
+
// 3. getByPlaceholderText - when no label exists
|
|
210
|
+
screen.getByPlaceholderText('Search projects...');
|
|
211
|
+
|
|
212
|
+
// 4. getByText - for non-interactive elements
|
|
213
|
+
screen.getByText('Search');
|
|
214
|
+
|
|
215
|
+
// 5. getByTestId - last resort
|
|
216
|
+
screen.getByTestId('search-bar');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Examples
|
|
222
|
+
|
|
223
|
+
| Query | When to Use | Example |
|
|
224
|
+
|-------|------------|---------|
|
|
225
|
+
| `getByRole` | Buttons, links, headings, form controls | `getByRole('button', { name: /save/i })` |
|
|
226
|
+
| `getByLabelText` | Form inputs with associated labels | `getByLabelText('Email address')` |
|
|
227
|
+
| `getByText` | Static text content | `getByText(/no results found/i)` |
|
|
228
|
+
| `findByText` | Elements that appear asynchronously | `await findByText('Loaded!')` |
|
|
229
|
+
| `queryByText` | Asserting an element does NOT exist | `expect(queryByText('Error')).toBeNull()` |
|
|
230
|
+
| `getAllByRole` | Multiple matching elements | `getAllByRole('listitem')` |
|
|
231
|
+
|
|
232
|
+
## Checklist
|
|
233
|
+
- [ ] Use `userEvent.setup()` instead of `fireEvent` for realistic interactions
|
|
234
|
+
- [ ] Prefer `getByRole` and `getByLabelText` over `getByTestId`
|
|
235
|
+
- [ ] Use `findBy*` (async) for elements that appear after loading or state changes
|
|
236
|
+
- [ ] Use `queryBy*` only when asserting an element is absent
|
|
237
|
+
- [ ] Wrap state updates in `act()` only when not already handled by RTL utilities
|
|
238
|
+
- [ ] Provide wrapper components (router, theme, store) via a shared `renderWithProviders` helper
|
|
239
|
+
- [ ] Never test internal state or implementation details (no `component.state` access)
|
|
240
|
+
- [ ] Run with `jsdom` or `happy-dom` environment configured in vitest.config.ts
|