@skilly-hand/skilly-hand 0.6.1 → 0.8.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/CHANGELOG.md +32 -0
- package/README.md +1 -0
- package/catalog/README.md +4 -0
- package/catalog/catalog-index.json +4 -0
- package/catalog/skills/accessibility-audit/SKILL.md +154 -0
- package/catalog/skills/accessibility-audit/manifest.json +37 -0
- package/catalog/skills/accessibility-audit/references/w3c-wcag22-checklist.md +80 -0
- package/catalog/skills/accessibility-audit/scripts/audit-a11y.sh +372 -0
- package/catalog/skills/frontend-design/SKILL.md +237 -0
- package/catalog/skills/frontend-design/agents/component-designer.md +95 -0
- package/catalog/skills/frontend-design/agents/stack-detector.md +154 -0
- package/catalog/skills/frontend-design/assets/stack-scan-checklist.md +58 -0
- package/catalog/skills/frontend-design/manifest.json +27 -0
- package/catalog/skills/life-guard/SKILL.md +180 -0
- package/catalog/skills/life-guard/assets/committee-member-template.md +44 -0
- package/catalog/skills/life-guard/assets/safety-guard-template.md +47 -0
- package/catalog/skills/life-guard/manifest.json +33 -0
- package/catalog/skills/test-driven-development/SKILL.md +159 -0
- package/catalog/skills/test-driven-development/assets/tdd-cycle.md +487 -0
- package/catalog/skills/test-driven-development/manifest.json +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Test-Driven Development Guide
|
|
2
|
+
|
|
3
|
+
## When to Use
|
|
4
|
+
|
|
5
|
+
Use this skill when:
|
|
6
|
+
|
|
7
|
+
- Implementing a new feature, service, component, or function from scratch.
|
|
8
|
+
- Adding behavior to existing code where the expected outcome can be defined upfront.
|
|
9
|
+
- Debugging a regression by writing a failing test that reproduces the bug first.
|
|
10
|
+
- Reviewing or pair-programming on code where test-first discipline is required.
|
|
11
|
+
|
|
12
|
+
Do not use this skill for:
|
|
13
|
+
|
|
14
|
+
- Exploratory prototyping where requirements are entirely undefined.
|
|
15
|
+
- Snapshot or visual regression tests driven by existing UI.
|
|
16
|
+
- Infrastructure or environment setup with no testable behavior.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Critical Patterns
|
|
21
|
+
|
|
22
|
+
### Pattern 1: RED First, Always
|
|
23
|
+
|
|
24
|
+
Write a failing test before writing any implementation code. This proves:
|
|
25
|
+
|
|
26
|
+
- The test is meaningful (not passing by accident).
|
|
27
|
+
- The feature is actually needed.
|
|
28
|
+
- You understand the requirements before touching implementation.
|
|
29
|
+
|
|
30
|
+
Never write implementation code without a failing test that demands it.
|
|
31
|
+
|
|
32
|
+
### Pattern 2: Minimum Code to GREEN
|
|
33
|
+
|
|
34
|
+
Write the **smallest possible code** to make the test pass:
|
|
35
|
+
|
|
36
|
+
- No extra features beyond what the test requires.
|
|
37
|
+
- No premature optimization or defensive handling.
|
|
38
|
+
- No "while I'm here, let me add…" additions.
|
|
39
|
+
|
|
40
|
+
The goal is to satisfy the test, nothing more.
|
|
41
|
+
|
|
42
|
+
### Pattern 3: REFACTOR With Tests GREEN
|
|
43
|
+
|
|
44
|
+
Only improve code structure **after** all tests pass:
|
|
45
|
+
|
|
46
|
+
- Extract constants, improve naming, simplify logic.
|
|
47
|
+
- Tests must stay green throughout every refactoring step.
|
|
48
|
+
- If a refactor breaks a test, revert — the refactor was wrong.
|
|
49
|
+
|
|
50
|
+
### Pattern 4: One Scenario Per Test
|
|
51
|
+
|
|
52
|
+
Each test must validate exactly one behavior:
|
|
53
|
+
|
|
54
|
+
- Use explicit GIVEN / WHEN / THEN structure in test bodies.
|
|
55
|
+
- A test name should complete the sentence: *"it should ___"*.
|
|
56
|
+
- If a test asserts two behaviors, split it into two tests.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Decision Tree
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
Starting a new feature or function? -> Write failing test first (RED)
|
|
64
|
+
Test is failing as expected? -> Write minimum code to pass (GREEN)
|
|
65
|
+
Test is passing? -> Improve code structure without changing behavior (REFACTOR)
|
|
66
|
+
Refactor broke a test? -> Revert — refactor introduced a behavior change
|
|
67
|
+
Test is already passing before writing code? -> Test is not meaningful; redesign it
|
|
68
|
+
Fixing a bug? -> Write failing test that reproduces the bug first
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Code Examples
|
|
74
|
+
|
|
75
|
+
### Example 1: GIVEN / WHEN / THEN Structure
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
it('should return the sum of two numbers', () => {
|
|
79
|
+
// GIVEN: Two positive integers
|
|
80
|
+
const a = 3;
|
|
81
|
+
const b = 4;
|
|
82
|
+
|
|
83
|
+
// WHEN: Sum is computed
|
|
84
|
+
const result = add(a, b);
|
|
85
|
+
|
|
86
|
+
// THEN: Result equals their sum
|
|
87
|
+
expect(result).toBe(7);
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Example 2: RED — Write Failing Test First
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// calculator.test.ts
|
|
95
|
+
import { divide } from './calculator';
|
|
96
|
+
|
|
97
|
+
it('should throw when dividing by zero', () => {
|
|
98
|
+
// GIVEN / WHEN / THEN
|
|
99
|
+
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Run the test — it **must** fail before writing any implementation.
|
|
104
|
+
|
|
105
|
+
### Example 3: GREEN — Write Minimum Implementation
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// calculator.ts
|
|
109
|
+
export function divide(a: number, b: number): number {
|
|
110
|
+
if (b === 0) throw new Error('Cannot divide by zero');
|
|
111
|
+
return a / b;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Run the test — it should now pass. No additional logic yet.
|
|
116
|
+
|
|
117
|
+
### Example 4: REFACTOR — Improve Without Changing Behavior
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// calculator.ts (refactored)
|
|
121
|
+
const DIVIDE_BY_ZERO_MESSAGE = 'Cannot divide by zero';
|
|
122
|
+
|
|
123
|
+
export function divide(numerator: number, denominator: number): number {
|
|
124
|
+
if (denominator === 0) throw new Error(DIVIDE_BY_ZERO_MESSAGE);
|
|
125
|
+
return numerator / denominator;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Run the test — it must still pass after renaming and extracting the constant.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Commands
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Run a single test file
|
|
137
|
+
npm test -- {test-file}
|
|
138
|
+
|
|
139
|
+
# Run all tests
|
|
140
|
+
npm test
|
|
141
|
+
|
|
142
|
+
# Run tests in watch mode
|
|
143
|
+
npm test -- --watch
|
|
144
|
+
|
|
145
|
+
# Type check without emitting
|
|
146
|
+
npx tsc --noEmit
|
|
147
|
+
|
|
148
|
+
# Lint check
|
|
149
|
+
npm run lint
|
|
150
|
+
|
|
151
|
+
# Full verify (tests + lint + type check + build)
|
|
152
|
+
npm test && npm run lint && npx tsc --noEmit && npm run build
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Resources
|
|
158
|
+
|
|
159
|
+
- Full cycle examples with Angular: [assets/tdd-cycle.md](assets/tdd-cycle.md)
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# TDD Templates: RED → GREEN → REFACTOR Cycles
|
|
2
|
+
|
|
3
|
+
Real examples of the RED → GREEN → REFACTOR TDD cycle for implementing components, services, and pipes.
|
|
4
|
+
|
|
5
|
+
**Pattern**: Write failing test → Write minimum code to pass → Refactor while tests stay green
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Example 1: Simple Pipe Component
|
|
10
|
+
|
|
11
|
+
### Scenario: AsyncCachePipe
|
|
12
|
+
Cache hot observables to prevent resubscription overhead.
|
|
13
|
+
|
|
14
|
+
#### Step 1: RED — Write Failing Test
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// async-cache.pipe.test.ts
|
|
18
|
+
import { TestBed } from '@angular/core/testing';
|
|
19
|
+
import { Subject, take } from 'rxjs';
|
|
20
|
+
import { AsyncCachePipe } from './async-cache.pipe';
|
|
21
|
+
|
|
22
|
+
describe('AsyncCachePipe', () => {
|
|
23
|
+
let pipe: AsyncCachePipe;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
TestBed.configureTestingModule({});
|
|
27
|
+
pipe = TestBed.runInInjectionContext(() => new AsyncCachePipe());
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should cache async values and replay on late subscriptions', (done) => {
|
|
31
|
+
// GIVEN: An observable that emits after 100ms
|
|
32
|
+
let emitCount = 0;
|
|
33
|
+
const source$ = new Subject<number>();
|
|
34
|
+
|
|
35
|
+
// WHEN: First subscription
|
|
36
|
+
const result$ = pipe.transform(source$);
|
|
37
|
+
const values: number[] = [];
|
|
38
|
+
|
|
39
|
+
const sub1 = result$.subscribe((val) => {
|
|
40
|
+
emitCount++;
|
|
41
|
+
values.push(val);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
source$.next(42);
|
|
46
|
+
}, 100);
|
|
47
|
+
|
|
48
|
+
// WHEN: Second subscription arrives after value is cached
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
const sub2 = result$.pipe(take(1)).subscribe(() => {
|
|
51
|
+
// THEN: Second subscriber gets cached value immediately
|
|
52
|
+
expect(emitCount).toBe(1);
|
|
53
|
+
expect(values[0]).toBe(42);
|
|
54
|
+
|
|
55
|
+
sub1.unsubscribe();
|
|
56
|
+
sub2.unsubscribe();
|
|
57
|
+
source$.complete();
|
|
58
|
+
done();
|
|
59
|
+
});
|
|
60
|
+
}, 200);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ❌ TEST FAILS HERE - transform() not implemented yet
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Run**: `npm test -- async-cache.pipe.test.ts`
|
|
68
|
+
```
|
|
69
|
+
FAIL async-cache.pipe.test.ts
|
|
70
|
+
should cache async values and replay on late subscriptions
|
|
71
|
+
TypeError: pipe.transform is not a function
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
✅ Test fails as expected (proves test is meaningful)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
#### Step 2: GREEN — Write Minimum Code
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// async-cache.pipe.ts
|
|
82
|
+
import { Pipe, PipeTransform } from '@angular/core';
|
|
83
|
+
import { Observable, ReplaySubject } from 'rxjs';
|
|
84
|
+
import { tap } from 'rxjs/operators';
|
|
85
|
+
|
|
86
|
+
@Pipe({
|
|
87
|
+
name: 'asyncCache',
|
|
88
|
+
standalone: true,
|
|
89
|
+
})
|
|
90
|
+
export class AsyncCachePipe implements PipeTransform {
|
|
91
|
+
// ✅ MINIMUM: Just make the test pass
|
|
92
|
+
transform<T>(source: Observable<T>): Observable<T> {
|
|
93
|
+
const cached$ = new ReplaySubject<T>(1);
|
|
94
|
+
source.pipe(tap((val) => cached$.next(val))).subscribe();
|
|
95
|
+
return cached$.asObservable();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Run**: `npm test -- async-cache.pipe.test.ts`
|
|
101
|
+
```
|
|
102
|
+
PASS async-cache.pipe.test.ts
|
|
103
|
+
should cache async values and replay on late subscriptions ✓
|
|
104
|
+
|
|
105
|
+
Tests: 1 passed, 1 total
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
✅ Test passes (behavior is correct, but code needs cleanup)
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
#### Step 3: REFACTOR — Improve Without Changing Behavior
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// async-cache.pipe.ts (refactored)
|
|
116
|
+
import { Pipe, PipeTransform } from '@angular/core';
|
|
117
|
+
import { Observable, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
|
118
|
+
import { tap } from 'rxjs/operators';
|
|
119
|
+
|
|
120
|
+
@Pipe({
|
|
121
|
+
name: 'asyncCache',
|
|
122
|
+
standalone: true,
|
|
123
|
+
})
|
|
124
|
+
export class AsyncCachePipe implements PipeTransform {
|
|
125
|
+
private destroy$ = new Subject<void>();
|
|
126
|
+
|
|
127
|
+
// ✅ REFACTORED: Cleaner, adds cleanup
|
|
128
|
+
transform<T>(source: Observable<T>): Observable<T> {
|
|
129
|
+
const cacheSize = 1;
|
|
130
|
+
const cached$ = new ReplaySubject<T>(cacheSize);
|
|
131
|
+
|
|
132
|
+
source
|
|
133
|
+
.pipe(
|
|
134
|
+
tap((val) => cached$.next(val)),
|
|
135
|
+
takeUntil(this.destroy$)
|
|
136
|
+
)
|
|
137
|
+
.subscribe();
|
|
138
|
+
|
|
139
|
+
return cached$.asObservable();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ngOnDestroy(): void {
|
|
143
|
+
this.destroy$.next();
|
|
144
|
+
this.destroy$.complete();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Run**: `npm test -- async-cache.pipe.test.ts`
|
|
150
|
+
```
|
|
151
|
+
PASS async-cache.pipe.test.ts
|
|
152
|
+
should cache async values and replay on late subscriptions ✓
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
✅ Test still passes
|
|
156
|
+
|
|
157
|
+
**What improved**:
|
|
158
|
+
- Constants extracted (`cacheSize`)
|
|
159
|
+
- Cleanup logic added (`takeUntil`, `ngOnDestroy`)
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Example 2: Component with Input/Output
|
|
164
|
+
|
|
165
|
+
### Scenario: AlertComponent
|
|
166
|
+
Display a dismissible alert with a message.
|
|
167
|
+
|
|
168
|
+
#### Step 1: RED — Write Failing Test
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// alert.component.test.ts
|
|
172
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
173
|
+
import { AlertComponent } from './alert.component';
|
|
174
|
+
|
|
175
|
+
describe('AlertComponent', () => {
|
|
176
|
+
let component: AlertComponent;
|
|
177
|
+
let fixture: ComponentFixture<AlertComponent>;
|
|
178
|
+
|
|
179
|
+
beforeEach(async () => {
|
|
180
|
+
await TestBed.configureTestingModule({
|
|
181
|
+
imports: [AlertComponent],
|
|
182
|
+
}).compileComponents();
|
|
183
|
+
|
|
184
|
+
fixture = TestBed.createComponent(AlertComponent);
|
|
185
|
+
component = fixture.componentInstance;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should display the alert message', () => {
|
|
189
|
+
// GIVEN: Component with message input
|
|
190
|
+
component.message = 'Error: Invalid input';
|
|
191
|
+
fixture.detectChanges();
|
|
192
|
+
|
|
193
|
+
// WHEN: Component renders
|
|
194
|
+
const messageEl = fixture.debugElement.nativeElement.querySelector('[data-testid="alert-message"]');
|
|
195
|
+
|
|
196
|
+
// THEN: Message is visible
|
|
197
|
+
expect(messageEl?.textContent).toBe('Error: Invalid input');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ❌ FAILS - Component doesn't exist yet
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Run**: `npm test -- alert.component.test.ts`
|
|
205
|
+
```
|
|
206
|
+
FAIL alert.component.test.ts
|
|
207
|
+
Cannot find component 'AlertComponent'
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
#### Step 2: GREEN — Write Minimum Implementation
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// alert.component.ts
|
|
216
|
+
import { Component, input } from '@angular/core';
|
|
217
|
+
|
|
218
|
+
@Component({
|
|
219
|
+
selector: 'app-alert',
|
|
220
|
+
standalone: true,
|
|
221
|
+
template: `<div data-testid="alert-message">{{ message() }}</div>`,
|
|
222
|
+
})
|
|
223
|
+
export class AlertComponent {
|
|
224
|
+
// ✅ MINIMUM: Just the message input
|
|
225
|
+
message = input<string>('');
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Run**: `npm test -- alert.component.test.ts`
|
|
230
|
+
```
|
|
231
|
+
PASS alert.component.test.ts
|
|
232
|
+
should display the alert message ✓
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
#### Step 3: REFACTOR — Add Accessibility and Dismiss
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// alert.component.ts (refactored)
|
|
241
|
+
import { Component, input, output, ChangeDetectionStrategy } from '@angular/core';
|
|
242
|
+
|
|
243
|
+
@Component({
|
|
244
|
+
selector: 'app-alert',
|
|
245
|
+
standalone: true,
|
|
246
|
+
template: `
|
|
247
|
+
<div
|
|
248
|
+
class="alert"
|
|
249
|
+
role="alert"
|
|
250
|
+
aria-live="polite"
|
|
251
|
+
[attr.data-testid]="'alert-message'"
|
|
252
|
+
[class]="'alert--' + severity()">
|
|
253
|
+
<p class="alert__message">{{ message() }}</p>
|
|
254
|
+
<button class="alert__close" aria-label="Dismiss alert" (click)="onDismiss()">✕</button>
|
|
255
|
+
</div>
|
|
256
|
+
`,
|
|
257
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
258
|
+
})
|
|
259
|
+
export class AlertComponent {
|
|
260
|
+
message = input<string>('');
|
|
261
|
+
severity = input<'success' | 'error' | 'warning' | 'info'>('info');
|
|
262
|
+
dismissed = output<void>();
|
|
263
|
+
|
|
264
|
+
onDismiss(): void {
|
|
265
|
+
this.dismissed.emit();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Run**: `npm test -- alert.component.test.ts`
|
|
271
|
+
```
|
|
272
|
+
PASS alert.component.test.ts
|
|
273
|
+
should display the alert message ✓
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
✅ Test still passes
|
|
277
|
+
|
|
278
|
+
**What improved**:
|
|
279
|
+
- Accessibility attributes (`role="alert"`, `aria-live`, `aria-label`)
|
|
280
|
+
- Dismiss output and handler
|
|
281
|
+
- Severity levels
|
|
282
|
+
- `ChangeDetectionStrategy.OnPush`
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Example 3: Service with State Management
|
|
287
|
+
|
|
288
|
+
### Scenario: TodoService
|
|
289
|
+
Manage a todo list with add and complete operations.
|
|
290
|
+
|
|
291
|
+
#### Step 1: RED — Multiple Scenarios
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// todo.service.test.ts
|
|
295
|
+
import { TestBed } from '@angular/core/testing';
|
|
296
|
+
import { TodoService } from './todo.service';
|
|
297
|
+
|
|
298
|
+
describe('TodoService', () => {
|
|
299
|
+
let service: TodoService;
|
|
300
|
+
|
|
301
|
+
beforeEach(() => {
|
|
302
|
+
TestBed.configureTestingModule({ providers: [TodoService] });
|
|
303
|
+
service = TestBed.inject(TodoService);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should add a todo and update the list', () => {
|
|
307
|
+
// GIVEN: Empty list
|
|
308
|
+
// WHEN: Add todo
|
|
309
|
+
service.addTodo('Buy milk');
|
|
310
|
+
|
|
311
|
+
// THEN: Todo appears
|
|
312
|
+
expect(service.todos()).toContainEqual(
|
|
313
|
+
jasmine.objectContaining({ text: 'Buy milk', done: false })
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should mark a todo as complete', () => {
|
|
318
|
+
// GIVEN: A todo in the list
|
|
319
|
+
service.addTodo('Buy milk');
|
|
320
|
+
const firstTodo = service.todos()[0];
|
|
321
|
+
|
|
322
|
+
// WHEN: Mark as complete
|
|
323
|
+
service.completeTodo(firstTodo.id);
|
|
324
|
+
|
|
325
|
+
// THEN: Todo is done
|
|
326
|
+
expect(service.todos()[0].done).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ❌ FAILS - Service methods don't exist
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Run**: `npm test -- todo.service.test.ts`
|
|
334
|
+
```
|
|
335
|
+
FAIL todo.service.test.ts
|
|
336
|
+
service.addTodo is not a function
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
#### Step 2: GREEN — Write Minimum Implementation
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// todo.service.ts
|
|
345
|
+
import { Injectable, signal } from '@angular/core';
|
|
346
|
+
|
|
347
|
+
interface Todo { id: number; text: string; done: boolean; }
|
|
348
|
+
|
|
349
|
+
@Injectable({ providedIn: 'root' })
|
|
350
|
+
export class TodoService {
|
|
351
|
+
todos = signal<Todo[]>([]);
|
|
352
|
+
private nextId = 1;
|
|
353
|
+
|
|
354
|
+
addTodo(text: string): void {
|
|
355
|
+
this.todos.update((current) => [...current, { id: this.nextId++, text, done: false }]);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
completeTodo(id: number): void {
|
|
359
|
+
this.todos.update((current) =>
|
|
360
|
+
current.map((todo) => todo.id === id ? { ...todo, done: true } : todo)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Run**: `npm test -- todo.service.test.ts`
|
|
367
|
+
```
|
|
368
|
+
PASS todo.service.test.ts
|
|
369
|
+
should add a todo and update the list ✓
|
|
370
|
+
should mark a todo as complete ✓
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
#### Step 3: REFACTOR — Add Validation and Persistence
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
// todo.service.ts (refactored)
|
|
379
|
+
import { Injectable, signal } from '@angular/core';
|
|
380
|
+
|
|
381
|
+
interface Todo {
|
|
382
|
+
id: number;
|
|
383
|
+
text: string;
|
|
384
|
+
done: boolean;
|
|
385
|
+
createdAt: Date;
|
|
386
|
+
completedAt?: Date;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@Injectable({ providedIn: 'root' })
|
|
390
|
+
export class TodoService {
|
|
391
|
+
todos = signal<Todo[]>([]);
|
|
392
|
+
private nextId = 1;
|
|
393
|
+
private readonly MAX_TODOS = 100;
|
|
394
|
+
|
|
395
|
+
constructor() {
|
|
396
|
+
this.loadFromStorage();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
addTodo(text: string): void {
|
|
400
|
+
if (!text?.trim()) throw new Error('Todo text cannot be empty');
|
|
401
|
+
if (this.todos().length >= this.MAX_TODOS) throw new Error(`Cannot exceed ${this.MAX_TODOS} todos`);
|
|
402
|
+
|
|
403
|
+
this.todos.update((current) => [
|
|
404
|
+
...current,
|
|
405
|
+
{ id: this.nextId++, text: text.trim(), done: false, createdAt: new Date() },
|
|
406
|
+
]);
|
|
407
|
+
this.saveToStorage();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
completeTodo(id: number): void {
|
|
411
|
+
if (!this.todos().find((t) => t.id === id)) throw new Error(`Todo with id ${id} not found`);
|
|
412
|
+
|
|
413
|
+
this.todos.update((current) =>
|
|
414
|
+
current.map((t) => t.id === id ? { ...t, done: true, completedAt: new Date() } : t)
|
|
415
|
+
);
|
|
416
|
+
this.saveToStorage();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
deleteTodo(id: number): void {
|
|
420
|
+
this.todos.update((current) => current.filter((t) => t.id !== id));
|
|
421
|
+
this.saveToStorage();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private loadFromStorage(): void {
|
|
425
|
+
try {
|
|
426
|
+
const stored = localStorage.getItem('todos');
|
|
427
|
+
if (stored) this.todos.set(JSON.parse(stored));
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.error('Failed to load todos from storage', e);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private saveToStorage(): void {
|
|
434
|
+
try {
|
|
435
|
+
localStorage.setItem('todos', JSON.stringify(this.todos()));
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.error('Failed to save todos to storage', e);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Run**: `npm test -- todo.service.test.ts`
|
|
444
|
+
```
|
|
445
|
+
PASS todo.service.test.ts
|
|
446
|
+
should add a todo and update the list ✓
|
|
447
|
+
should mark a todo as complete ✓
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
✅ Tests still pass
|
|
451
|
+
|
|
452
|
+
**What improved**:
|
|
453
|
+
- Input validation (empty check, max limit)
|
|
454
|
+
- Timestamps (`createdAt`, `completedAt`)
|
|
455
|
+
- Persistence via `localStorage`
|
|
456
|
+
- `deleteTodo` added for completeness
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Quick Checklist: RED → GREEN → REFACTOR
|
|
461
|
+
|
|
462
|
+
```markdown
|
|
463
|
+
### For Each Task Using TDD
|
|
464
|
+
|
|
465
|
+
#### RED Phase
|
|
466
|
+
- [ ] Write test that describes the feature
|
|
467
|
+
- [ ] Test has explicit GIVEN / WHEN / THEN
|
|
468
|
+
- [ ] Run tests — FAILS as expected
|
|
469
|
+
- [ ] Failure proves test is meaningful
|
|
470
|
+
|
|
471
|
+
#### GREEN Phase
|
|
472
|
+
- [ ] Write minimum code to pass
|
|
473
|
+
- [ ] No extra features beyond test requirement
|
|
474
|
+
- [ ] Run tests — PASSES
|
|
475
|
+
|
|
476
|
+
#### REFACTOR Phase
|
|
477
|
+
- [ ] Improve code structure / naming
|
|
478
|
+
- [ ] Extract constants, simplify logic
|
|
479
|
+
- [ ] Run tests — STILL PASSES
|
|
480
|
+
- [ ] No behavior changes, only improvements
|
|
481
|
+
|
|
482
|
+
#### Verify
|
|
483
|
+
- [ ] All tests pass: `npm test`
|
|
484
|
+
- [ ] Lint passes: `npm run lint`
|
|
485
|
+
- [ ] Type check: `npx tsc --noEmit`
|
|
486
|
+
- [ ] Build succeeds: `npm run build`
|
|
487
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "test-driven-development",
|
|
3
|
+
"title": "Test-Driven Development",
|
|
4
|
+
"description": "Guide implementation using the RED → GREEN → REFACTOR TDD cycle: write a failing test first, write the minimum code to pass, then refactor while tests stay green.",
|
|
5
|
+
"portable": true,
|
|
6
|
+
"tags": ["testing", "workflow", "quality", "core"],
|
|
7
|
+
"detectors": ["always"],
|
|
8
|
+
"detectionTriggers": ["manual"],
|
|
9
|
+
"installsFor": ["all"],
|
|
10
|
+
"agentSupport": ["codex", "claude", "cursor", "gemini", "copilot"],
|
|
11
|
+
"skillMetadata": {
|
|
12
|
+
"author": "skilly-hand",
|
|
13
|
+
"last-edit": "2026-04-04",
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"version": "1.0.0",
|
|
16
|
+
"changelog": "Initial TDD skill ported from legacy scannlab-sdd tdd-templates; enables RED→GREEN→REFACTOR workflow across any stack; affects catalog skill coverage for test-first development",
|
|
17
|
+
"auto-invoke": "Implementing features, services, or components using test-driven development (TDD) or RED→GREEN→REFACTOR cycles",
|
|
18
|
+
"allowed-tools": ["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{ "path": "SKILL.md", "kind": "instruction" },
|
|
22
|
+
{ "path": "assets/tdd-cycle.md", "kind": "asset" }
|
|
23
|
+
],
|
|
24
|
+
"dependencies": []
|
|
25
|
+
}
|