update test

This commit is contained in:
2026-05-24 11:09:12 +02:00
parent c9f2863815
commit 264d9f1402
9 changed files with 575 additions and 138 deletions
+107 -88
View File
@@ -1,5 +1,8 @@
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { IssueEntity, IssuesStore } from './issues.store';
import { API_BASE_URL } from './issues-api.service';
const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
id: 99,
@@ -20,29 +23,59 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
describe('IssuesStore', () => {
let store: IssuesStore;
let httpMock: HttpTestingController;
const loadWith = async (issues: IssueEntity[]) => {
const p = store.load();
httpMock.expectOne(`${API_BASE_URL}/issues`).flush(issues);
await p;
};
beforeEach(() => {
localStorage.clear();
TestBed.configureTestingModule({});
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
store = TestBed.inject(IssuesStore);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
localStorage.clear();
httpMock.verify();
});
it('should be created', () => {
expect(store).toBeTruthy();
});
it('should load default issues when localStorage is empty', () => {
expect(store.issues().length).toBeGreaterThan(0);
describe('load', () => {
it('populates issues from the API', async () => {
await loadWith([makeIssue({ id: 1 }), makeIssue({ id: 2 })]);
expect(store.issues().length).toBe(2);
});
it('sets loading to true during load and false after', async () => {
const p = store.load();
expect(store.loading()).toBe(true);
httpMock.expectOne(`${API_BASE_URL}/issues`).flush([]);
await p;
expect(store.loading()).toBe(false);
expect(store.loaded()).toBe(true);
});
it('does not reload if already loaded', async () => {
await loadWith([]);
await store.load();
httpMock.expectNone(`${API_BASE_URL}/issues`);
});
});
describe('getById', () => {
beforeEach(async () => {
await loadWith([makeIssue({ id: 1 }), makeIssue({ id: 2 })]);
});
it('returns the issue with the given id', () => {
const issue = store.getById(1);
expect(issue?.id).toBe(1);
expect(store.getById(1)?.id).toBe(1);
});
it('returns undefined for an unknown id', () => {
@@ -51,122 +84,108 @@ describe('IssuesStore', () => {
});
describe('getNextId', () => {
it('returns max id + 1', () => {
const ids = store.issues().map((i) => i.id);
const expectedNext = Math.max(...ids) + 1;
expect(store.getNextId()).toBe(expectedNext);
it('returns max id + 1', async () => {
await loadWith([makeIssue({ id: 1 }), makeIssue({ id: 5 }), makeIssue({ id: 3 })]);
expect(store.getNextId()).toBe(6);
});
it('returns 1 when there are no issues', () => {
store.deleteById(1);
store.deleteById(2);
store.deleteById(3);
it('returns 1 when there are no issues', async () => {
await loadWith([]);
expect(store.getNextId()).toBe(1);
});
});
describe('upsert', () => {
it('adds a new issue when the id does not exist', () => {
const before = store.issues().length;
store.upsert(makeIssue({ id: 999 }));
expect(store.issues().length).toBe(before + 1);
expect(store.getById(999)?.name).toBe('Test Issue');
beforeEach(async () => {
await loadWith([makeIssue({ id: 1, name: 'Existing' }), makeIssue({ id: 2 }), makeIssue({ id: 3 })]);
});
it('updates an existing issue', () => {
store.upsert(makeIssue({ id: 1, name: 'Updated Name' }));
it('creates a new issue via POST when id is 0', async () => {
const before = store.issues().length;
const p = store.upsert(makeIssue({ id: 0, name: 'New Issue' }));
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 999, name: 'New Issue' }));
await p;
expect(store.issues().length).toBe(before + 1);
expect(store.getById(999)?.name).toBe('New Issue');
});
it('updates an existing issue via PUT', async () => {
const p = store.upsert(makeIssue({ id: 1, name: 'Updated Name' }));
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, name: 'Updated Name' }));
await p;
expect(store.getById(1)?.name).toBe('Updated Name');
expect(store.issues().filter((i) => i.id === 1).length).toBe(1);
});
it('persists the issue list to localStorage', () => {
store.upsert(makeIssue({ id: 999 }));
const raw = localStorage.getItem('bonsai.issues');
expect(raw).not.toBeNull();
const parsed = JSON.parse(raw!);
expect(parsed.some((i: IssueEntity) => i.id === 999)).toBe(true);
});
it('normalizes legacy dependsOnId (single number) to dependsOnIds array when dependsOnIds is absent', () => {
// dependsOnIds must be omitted (not an array) for the legacy field to take effect
store.upsert({ id: 998, type: 'Story', name: 'Legacy', dependsOnId: 1 } as any);
it('normalizes legacy dependsOnId to dependsOnIds array', async () => {
const issue = { id: 0, type: 'Story', name: 'Legacy', dependsOnId: 1 } as any;
const p = store.upsert(issue);
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 998, dependsOnIds: [1] }));
await p;
expect(store.getById(998)?.dependsOnIds).toEqual([1]);
});
it('filters non-number values from dependsOnIds', () => {
store.upsert({ ...makeIssue({ id: 997 }), dependsOnIds: [1, 'two', null] } as any);
it('filters non-number values from dependsOnIds', async () => {
const issue = { ...makeIssue({ id: 0 }), dependsOnIds: [1, 'two', null] } as any;
const p = store.upsert(issue);
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 997, dependsOnIds: [1] }));
await p;
expect(store.getById(997)?.dependsOnIds).toEqual([1]);
});
it('ensures comments is always an array when missing', () => {
store.upsert({ ...makeIssue({ id: 996 }), comments: undefined } as any);
it('ensures comments is always an array when missing', async () => {
const issue = { ...makeIssue({ id: 0 }), comments: undefined } as any;
const p = store.upsert(issue);
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 996, comments: [] }));
await p;
expect(store.getById(996)?.comments).toEqual([]);
});
it('sets default type to Story when type is missing', () => {
store.upsert({ ...makeIssue({ id: 995 }), type: undefined } as any);
it('sets default type to Story when type is missing', async () => {
const issue = { ...makeIssue({ id: 0 }), type: undefined } as any;
const p = store.upsert(issue);
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 995, type: 'Story' }));
await p;
expect(store.getById(995)?.type).toBe('Story');
});
it('sets estimatedTime to null when missing', () => {
store.upsert({ ...makeIssue({ id: 994 }), estimatedTime: undefined } as any);
it('sets estimatedTime to null when missing in API response', async () => {
const issue = { ...makeIssue({ id: 0 }), estimatedTime: undefined } as any;
const p = store.upsert(issue);
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush({ ...makeIssue({ id: 994 }), estimatedTime: undefined });
await p;
expect(store.getById(994)?.estimatedTime).toBeNull();
});
});
describe('deleteById', () => {
it('removes the issue from the store', () => {
store.upsert(makeIssue({ id: 999 }));
store.deleteById(999);
expect(store.getById(999)).toBeUndefined();
beforeEach(async () => {
await loadWith([
makeIssue({ id: 1 }),
makeIssue({ id: 100 }),
makeIssue({ id: 101, dependsOnIds: [100] }),
]);
});
it('removes the deleted id from dependsOnIds of other issues', () => {
store.upsert(makeIssue({ id: 100 }));
store.upsert(makeIssue({ id: 101, dependsOnIds: [100] }));
store.deleteById(100);
it('removes the issue from the store', async () => {
const p = store.deleteById(1);
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/1` }).flush(null);
await p;
expect(store.getById(1)).toBeUndefined();
});
it('removes the deleted id from dependsOnIds of other issues', async () => {
const p = store.deleteById(100);
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/100` }).flush(null);
await p;
expect(store.getById(101)?.dependsOnIds).toEqual([]);
});
it('persists the updated list to localStorage', () => {
store.upsert(makeIssue({ id: 999 }));
store.deleteById(999);
const raw = localStorage.getItem('bonsai.issues');
const parsed = JSON.parse(raw!);
expect(parsed.some((i: IssueEntity) => i.id === 999)).toBe(false);
});
});
describe('localStorage persistence', () => {
it('loads issues from localStorage on construction', () => {
const saved: IssueEntity[] = [makeIssue({ id: 42, name: 'From storage' })];
localStorage.setItem('bonsai.issues', JSON.stringify(saved));
TestBed.resetTestingModule();
TestBed.configureTestingModule({});
const freshStore = TestBed.inject(IssuesStore);
expect(freshStore.getById(42)?.name).toBe('From storage');
});
it('falls back to defaults when localStorage contains invalid JSON', () => {
localStorage.setItem('bonsai.issues', 'not-valid-json');
TestBed.resetTestingModule();
TestBed.configureTestingModule({});
const freshStore = TestBed.inject(IssuesStore);
expect(freshStore.issues().length).toBeGreaterThan(0);
});
it('falls back to defaults when localStorage contains a non-array', () => {
localStorage.setItem('bonsai.issues', '{"key":"value"}');
TestBed.resetTestingModule();
TestBed.configureTestingModule({});
const freshStore = TestBed.inject(IssuesStore);
expect(freshStore.issues().length).toBeGreaterThan(0);
it('does not affect issues with unrelated dependsOnIds', async () => {
const p = store.deleteById(1);
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/1` }).flush(null);
await p;
expect(store.getById(101)?.dependsOnIds).toEqual([100]);
});
});
});