test-casebook 1.0.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/.claude/agents/test-reviewer.md +29 -0
- package/.claude/agents/test-writer.md +29 -0
- package/.claude/skills/test-casebook/SKILL.md +30 -0
- package/.claude/skills/test-task/SKILL.md +50 -0
- package/AGENTS.md +392 -0
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/bin/test-casebook.mjs +93 -0
- package/docs/conventions.md +127 -0
- package/docs/strategy.md +100 -0
- package/docs/testing-guide/README.md +2568 -0
- package/package.json +36 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const pkgRoot = dirname(here);
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const command = args[0];
|
|
11
|
+
const force = args.includes("--force");
|
|
12
|
+
|
|
13
|
+
const ASSETS = ["AGENTS.md", "docs", ".claude"];
|
|
14
|
+
|
|
15
|
+
function walk(dir) {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const entry of readdirSync(dir)) {
|
|
18
|
+
const full = join(dir, entry);
|
|
19
|
+
if (statSync(full).isDirectory()) out.push(...walk(full));
|
|
20
|
+
else out.push(full);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function copyAsset(name) {
|
|
26
|
+
const src = join(pkgRoot, name);
|
|
27
|
+
if (!existsSync(src)) return { copied: [], skipped: [] };
|
|
28
|
+
const copied = [];
|
|
29
|
+
const skipped = [];
|
|
30
|
+
const files = statSync(src).isDirectory() ? walk(src) : [src];
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const rel = relative(pkgRoot, file);
|
|
33
|
+
const dest = join(cwd, rel);
|
|
34
|
+
if (existsSync(dest) && !force) {
|
|
35
|
+
skipped.push(rel);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
39
|
+
cpSync(file, dest);
|
|
40
|
+
copied.push(rel);
|
|
41
|
+
}
|
|
42
|
+
return { copied, skipped };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function init() {
|
|
46
|
+
const copied = [];
|
|
47
|
+
const skipped = [];
|
|
48
|
+
for (const asset of ASSETS) {
|
|
49
|
+
const result = copyAsset(asset);
|
|
50
|
+
copied.push(...result.copied);
|
|
51
|
+
skipped.push(...result.skipped);
|
|
52
|
+
}
|
|
53
|
+
console.log(`\ntest-casebook — scaffolded into ${cwd}\n`);
|
|
54
|
+
if (copied.length) {
|
|
55
|
+
console.log(`Added ${copied.length} file(s):`);
|
|
56
|
+
for (const file of copied) console.log(` + ${file}`);
|
|
57
|
+
}
|
|
58
|
+
if (skipped.length) {
|
|
59
|
+
console.log(
|
|
60
|
+
`\nSkipped ${skipped.length} existing file(s) (use --force to overwrite):`,
|
|
61
|
+
);
|
|
62
|
+
for (const file of skipped) console.log(` = ${file}`);
|
|
63
|
+
}
|
|
64
|
+
console.log(`\nNext steps:`);
|
|
65
|
+
console.log(
|
|
66
|
+
` 1. Open this project in Claude Code and invoke the "test-casebook" skill.`,
|
|
67
|
+
);
|
|
68
|
+
console.log(
|
|
69
|
+
` 2. env-attr-cleaner (the cleaner) is OPTIONAL and never auto-installed.`,
|
|
70
|
+
);
|
|
71
|
+
console.log(
|
|
72
|
+
` To strip data-test-* from production builds, install it yourself:`,
|
|
73
|
+
);
|
|
74
|
+
console.log(
|
|
75
|
+
` npm i -D env-attr-cleaner (or pnpm add -D / yarn add -D)`,
|
|
76
|
+
);
|
|
77
|
+
console.log(
|
|
78
|
+
` then wire it per the env-attr-cleaner repo's docs/frameworks.\n`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function usage() {
|
|
83
|
+
console.log(`test-casebook — testing methodology scaffolder\n`);
|
|
84
|
+
console.log(`Usage:`);
|
|
85
|
+
console.log(` npx test-casebook init [--force]\n`);
|
|
86
|
+
console.log(
|
|
87
|
+
` init copy AGENTS.md, docs/ and .claude/ into the current project`,
|
|
88
|
+
);
|
|
89
|
+
console.log(` --force overwrite files that already exist\n`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (command === "init") init();
|
|
93
|
+
else usage();
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
|
|
2
|
+
# Conventions - Data Attributes
|
|
3
|
+
|
|
4
|
+
## Test Attributes
|
|
5
|
+
|
|
6
|
+
### `data-test-id`
|
|
7
|
+
|
|
8
|
+
Unique identifier for a specific element.
|
|
9
|
+
|
|
10
|
+
```html
|
|
11
|
+
<button data-test-id="login-submit">Connexion</button>
|
|
12
|
+
<input data-test-id="login-email" type="email">
|
|
13
|
+
<form data-test-id="login-form">
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Naming**: `context-element` or `context-action`
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
login-submit
|
|
20
|
+
login-email
|
|
21
|
+
cart-checkout-button
|
|
22
|
+
product-123-add-to-cart
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### `data-test-class`
|
|
26
|
+
|
|
27
|
+
Group similar elements for counting or filtering.
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<div data-test-class="product-card">...</div>
|
|
31
|
+
<div data-test-class="product-card">...</div>
|
|
32
|
+
<div data-test-class="product-card featured">...</div>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Usage in tests**:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// Compter
|
|
39
|
+
const cards = page.locator('[data-test-class~="product-card"]')
|
|
40
|
+
await expect(cards).toHaveCount(3)
|
|
41
|
+
|
|
42
|
+
// Filtrer
|
|
43
|
+
const featured = page.locator('[data-test-class~="featured"]')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Debug Attributes (Phase 2)
|
|
47
|
+
|
|
48
|
+
### `data-state`
|
|
49
|
+
|
|
50
|
+
Current business state of a component.
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<form data-state="idle">
|
|
54
|
+
<form data-state="loading">
|
|
55
|
+
<form data-state="success">
|
|
56
|
+
<form data-state="error">
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `data-error`
|
|
60
|
+
|
|
61
|
+
Error context for debugging.
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<div data-error="validation-failed">
|
|
65
|
+
<div data-error="network-timeout">
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `data-last-action`
|
|
69
|
+
|
|
70
|
+
Last user action for debugging.
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<div data-last-action="submit-clicked">
|
|
74
|
+
<div data-last-action="field-focused">
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Feature Flags (Phase 3)
|
|
78
|
+
|
|
79
|
+
### `data-present`
|
|
80
|
+
|
|
81
|
+
Track if an element should be present.
|
|
82
|
+
|
|
83
|
+
```html
|
|
84
|
+
<div data-present="premium-feature">
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `data-experiment` / `data-variant`
|
|
88
|
+
|
|
89
|
+
A/B test tracking.
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<button data-experiment="checkout-flow" data-variant="one-click">
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Analytics (Phase 4)
|
|
96
|
+
|
|
97
|
+
### `data-analytics-*`
|
|
98
|
+
|
|
99
|
+
Analytics event tracking.
|
|
100
|
+
|
|
101
|
+
```html
|
|
102
|
+
<button
|
|
103
|
+
data-analytics-event="click"
|
|
104
|
+
data-analytics-category="checkout"
|
|
105
|
+
data-analytics-label="buy-now"
|
|
106
|
+
>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Configuration by Environment
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
envAttrCleaner({
|
|
113
|
+
environments: {
|
|
114
|
+
// Tous les attributs en developpement
|
|
115
|
+
development: ['data-test-*', 'data-debug-*', 'data-analytics-*'],
|
|
116
|
+
|
|
117
|
+
// Uniquement les attributs de test en CI
|
|
118
|
+
test: ['data-test-*'],
|
|
119
|
+
|
|
120
|
+
// Test + analytics en staging
|
|
121
|
+
staging: ['data-test-*', 'data-analytics-*'],
|
|
122
|
+
|
|
123
|
+
// Uniquement analytics en production (ou rien)
|
|
124
|
+
production: ['data-analytics-*']
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
```
|
package/docs/strategy.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
# Strategy - Why Data Attributes for Testing
|
|
3
|
+
|
|
4
|
+
## The Problem
|
|
5
|
+
|
|
6
|
+
Traditional selectors are **fragile**:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// Breaks when CSS changes
|
|
10
|
+
cy.get('.btn-primary')
|
|
11
|
+
|
|
12
|
+
// Breaks when structure changes
|
|
13
|
+
cy.get('form > div:nth-child(2) > button')
|
|
14
|
+
|
|
15
|
+
// Breaks when text changes
|
|
16
|
+
cy.get('button').contains('Envoyer')
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## The Solution
|
|
20
|
+
|
|
21
|
+
Data attributes are **stable**:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// Never breaks unless intentionally changed
|
|
25
|
+
cy.get('[data-test-id="submit-button"]')
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why It Works
|
|
29
|
+
|
|
30
|
+
| Selector type | Stability | Readability |
|
|
31
|
+
|---------------|-----------|-------------|
|
|
32
|
+
| CSS classes | Low | Medium |
|
|
33
|
+
| DOM structure | Very low | Low |
|
|
34
|
+
| Text content | Low | High |
|
|
35
|
+
| `data-test-*` | **High** | **High** |
|
|
36
|
+
|
|
37
|
+
## Conventions
|
|
38
|
+
|
|
39
|
+
### `data-test-id` - Target a unique element
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<button data-test-id="login-submit">Connexion</button>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
page.locator('[data-test-id="login-submit"]')
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `data-test-class` - Group elements
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<div data-test-class="product-card">Produit 1</div>
|
|
53
|
+
<div data-test-class="product-card">Produit 2</div>
|
|
54
|
+
<div data-test-class="product-card">Produit 3</div>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const cards = page.locator('[data-test-class="product-card"]')
|
|
59
|
+
await expect(cards).toHaveCount(3)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Adoption Roadmap
|
|
63
|
+
|
|
64
|
+
### Phase 1 - Testing (immediate value)
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
data-test-id -> unique element targeting
|
|
68
|
+
data-test-class -> group counting and filtering
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Phase 2 - Debug (developer productivity)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
data-state -> visible business state
|
|
75
|
+
data-error -> error context
|
|
76
|
+
data-last-action -> last user action
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Phase 3 - Feature Flags / A/B Testing
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
data-present -> element presence tracking
|
|
83
|
+
data-experiment -> A/B test name
|
|
84
|
+
data-variant -> active variant
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Phase 4 - Analytics
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
data-analytics-event -> event name
|
|
91
|
+
data-analytics-category -> category
|
|
92
|
+
data-analytics-label -> custom label
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Key Message
|
|
96
|
+
|
|
97
|
+
> Start by testing better.
|
|
98
|
+
> End up understanding and controlling your application.
|
|
99
|
+
|
|
100
|
+
Data attributes become a **common language** for the entire frontend: testing, debug, observability, analytics.
|