Skip to content

Commit 896ab13

Browse files
corvid-agentclaude
andauthored
test: add coverage for GitHubOAuthService and FrontmatterEditor (#73)
Add 26 new tests covering two previously untested modules: - GitHubOAuthService (14 tests): localStorage restore/corrupt JSON, cancelFlow, signOut, full device flow success, access_denied, expired_token, 3-strike consecutive failures (HTTP + network), unknown error with error_description, non-fatal profile fetch failure - FrontmatterEditorComponent (12 tests): availableModules computed filtering (excludes current module + existing deps), addFile/addTable/ addDependency with empty-input guard, removeFile/removeDependency by index, onFieldChange via DOM input, immutability of original frontmatter Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d67177b commit 896ab13

File tree

2 files changed

+555
-0
lines changed

2 files changed

+555
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { Component, signal } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
import { FrontmatterEditorComponent } from './frontmatter-editor';
4+
import { type SpecFrontmatter } from '../../models/spec.model';
5+
6+
@Component({
7+
standalone: true,
8+
imports: [FrontmatterEditorComponent],
9+
template: `<app-frontmatter-editor
10+
[frontmatter]="frontmatter()"
11+
[knownModules]="knownModules()"
12+
(frontmatterChange)="onFrontmatterChange($event)"
13+
/>`,
14+
})
15+
class TestHostComponent {
16+
frontmatter = signal<SpecFrontmatter>({
17+
module: 'auth-service',
18+
version: 1,
19+
status: 'draft',
20+
files: [],
21+
db_tables: [],
22+
depends_on: [],
23+
});
24+
knownModules = signal<string[]>([]);
25+
lastEmitted: SpecFrontmatter | null = null;
26+
onFrontmatterChange(fm: SpecFrontmatter): void {
27+
this.lastEmitted = fm;
28+
}
29+
}
30+
31+
describe('FrontmatterEditorComponent', () => {
32+
let host: TestHostComponent;
33+
let fixture: ReturnType<typeof TestBed.createComponent<TestHostComponent>>;
34+
35+
beforeEach(async () => {
36+
await TestBed.configureTestingModule({
37+
imports: [TestHostComponent],
38+
}).compileComponents();
39+
40+
fixture = TestBed.createComponent(TestHostComponent);
41+
host = fixture.componentInstance;
42+
fixture.detectChanges();
43+
});
44+
45+
it('should create', () => {
46+
expect(host).toBeTruthy();
47+
});
48+
49+
describe('availableModules computed', () => {
50+
it('should exclude current module from suggestions', () => {
51+
host.knownModules.set(['auth-service', 'user-service', 'db-service']);
52+
fixture.detectChanges();
53+
54+
const datalist = fixture.nativeElement.querySelector('#dep-suggestions');
55+
const options = datalist.querySelectorAll('option');
56+
const values = Array.from(options).map((o: any) => o.value);
57+
58+
expect(values).toEqual(['user-service', 'db-service']);
59+
expect(values).not.toContain('auth-service');
60+
});
61+
62+
it('should exclude already-added dependencies from suggestions', () => {
63+
host.knownModules.set(['auth-service', 'user-service', 'db-service', 'cache-service']);
64+
host.frontmatter.set({
65+
...host.frontmatter(),
66+
depends_on: ['user-service'],
67+
});
68+
fixture.detectChanges();
69+
70+
const datalist = fixture.nativeElement.querySelector('#dep-suggestions');
71+
const options = datalist.querySelectorAll('option');
72+
const values = Array.from(options).map((o: any) => o.value);
73+
74+
expect(values).toEqual(['db-service', 'cache-service']);
75+
expect(values).not.toContain('auth-service'); // current module
76+
expect(values).not.toContain('user-service'); // already added
77+
});
78+
});
79+
80+
describe('addFile', () => {
81+
it('should emit updated frontmatter with new file added', () => {
82+
const input = fixture.nativeElement.querySelector('#fm-new-file') as HTMLInputElement;
83+
const addBtn = fixture.nativeElement.querySelector(
84+
'fieldset:nth-of-type(1) .add-row button',
85+
) as HTMLButtonElement;
86+
87+
input.value = 'server/auth.ts';
88+
input.dispatchEvent(new Event('input'));
89+
fixture.detectChanges();
90+
91+
addBtn.click();
92+
fixture.detectChanges();
93+
94+
expect(host.lastEmitted).toBeTruthy();
95+
expect(host.lastEmitted!.files).toEqual(['server/auth.ts']);
96+
});
97+
98+
it('should not emit when input is empty or whitespace', () => {
99+
const input = fixture.nativeElement.querySelector('#fm-new-file') as HTMLInputElement;
100+
const addBtn = fixture.nativeElement.querySelector(
101+
'fieldset:nth-of-type(1) .add-row button',
102+
) as HTMLButtonElement;
103+
104+
input.value = ' ';
105+
input.dispatchEvent(new Event('input'));
106+
fixture.detectChanges();
107+
108+
addBtn.click();
109+
fixture.detectChanges();
110+
111+
expect(host.lastEmitted).toBeNull();
112+
});
113+
});
114+
115+
describe('removeFile', () => {
116+
it('should emit frontmatter without the removed file', () => {
117+
host.frontmatter.set({
118+
...host.frontmatter(),
119+
files: ['file-a.ts', 'file-b.ts', 'file-c.ts'],
120+
});
121+
fixture.detectChanges();
122+
123+
// Click the remove button for the second file (index 1)
124+
const removeButtons = fixture.nativeElement.querySelectorAll(
125+
'fieldset:nth-of-type(1) .list-item button',
126+
);
127+
expect(removeButtons.length).toBe(3);
128+
removeButtons[1].click();
129+
fixture.detectChanges();
130+
131+
expect(host.lastEmitted).toBeTruthy();
132+
expect(host.lastEmitted!.files).toEqual(['file-a.ts', 'file-c.ts']);
133+
});
134+
});
135+
136+
describe('addTable', () => {
137+
it('should emit updated frontmatter with new table added', () => {
138+
const input = fixture.nativeElement.querySelector('#fm-new-table') as HTMLInputElement;
139+
const addBtn = fixture.nativeElement.querySelector(
140+
'fieldset:nth-of-type(2) .add-row button',
141+
) as HTMLButtonElement;
142+
143+
input.value = 'sessions';
144+
input.dispatchEvent(new Event('input'));
145+
fixture.detectChanges();
146+
147+
addBtn.click();
148+
fixture.detectChanges();
149+
150+
expect(host.lastEmitted).toBeTruthy();
151+
expect(host.lastEmitted!.db_tables).toEqual(['sessions']);
152+
});
153+
});
154+
155+
describe('addDependency', () => {
156+
it('should emit updated frontmatter with new dependency added', () => {
157+
const input = fixture.nativeElement.querySelector('#fm-new-dep') as HTMLInputElement;
158+
const addBtn = fixture.nativeElement.querySelector(
159+
'fieldset:nth-of-type(3) .add-row button',
160+
) as HTMLButtonElement;
161+
162+
input.value = 'user-service';
163+
input.dispatchEvent(new Event('input'));
164+
fixture.detectChanges();
165+
166+
addBtn.click();
167+
fixture.detectChanges();
168+
169+
expect(host.lastEmitted).toBeTruthy();
170+
expect(host.lastEmitted!.depends_on).toEqual(['user-service']);
171+
});
172+
173+
it('should not emit when dependency input is empty', () => {
174+
const input = fixture.nativeElement.querySelector('#fm-new-dep') as HTMLInputElement;
175+
const addBtn = fixture.nativeElement.querySelector(
176+
'fieldset:nth-of-type(3) .add-row button',
177+
) as HTMLButtonElement;
178+
179+
input.value = '';
180+
input.dispatchEvent(new Event('input'));
181+
fixture.detectChanges();
182+
183+
addBtn.click();
184+
fixture.detectChanges();
185+
186+
expect(host.lastEmitted).toBeNull();
187+
});
188+
});
189+
190+
describe('removeDependency', () => {
191+
it('should emit frontmatter without the removed dependency', () => {
192+
host.frontmatter.set({
193+
...host.frontmatter(),
194+
depends_on: ['dep-a', 'dep-b'],
195+
});
196+
fixture.detectChanges();
197+
198+
const removeButtons = fixture.nativeElement.querySelectorAll(
199+
'fieldset:nth-of-type(3) .list-item button',
200+
);
201+
expect(removeButtons.length).toBe(2);
202+
removeButtons[0].click();
203+
fixture.detectChanges();
204+
205+
expect(host.lastEmitted).toBeTruthy();
206+
expect(host.lastEmitted!.depends_on).toEqual(['dep-b']);
207+
});
208+
});
209+
210+
describe('onFieldChange', () => {
211+
it('should emit when module name is changed via input', () => {
212+
const input = fixture.nativeElement.querySelector('#fm-module') as HTMLInputElement;
213+
input.value = 'new-module-name';
214+
input.dispatchEvent(new Event('input'));
215+
fixture.detectChanges();
216+
217+
expect(host.lastEmitted).toBeTruthy();
218+
expect(host.lastEmitted!.module).toBe('new-module-name');
219+
// Other fields should remain unchanged
220+
expect(host.lastEmitted!.version).toBe(1);
221+
expect(host.lastEmitted!.status).toBe('draft');
222+
});
223+
});
224+
225+
describe('immutability', () => {
226+
it('should not mutate the original frontmatter when adding a file', () => {
227+
const original = host.frontmatter();
228+
const originalFiles = [...original.files];
229+
230+
const input = fixture.nativeElement.querySelector('#fm-new-file') as HTMLInputElement;
231+
const addBtn = fixture.nativeElement.querySelector(
232+
'fieldset:nth-of-type(1) .add-row button',
233+
) as HTMLButtonElement;
234+
235+
input.value = 'new-file.ts';
236+
input.dispatchEvent(new Event('input'));
237+
fixture.detectChanges();
238+
addBtn.click();
239+
fixture.detectChanges();
240+
241+
// Original frontmatter should be untouched
242+
expect(host.frontmatter().files).toEqual(originalFiles);
243+
expect(host.lastEmitted!.files).toEqual(['new-file.ts']);
244+
});
245+
});
246+
});

0 commit comments

Comments
 (0)