update test
This commit is contained in:
@@ -1,29 +1,92 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { IssueComments } from './issue-comments';
|
||||
import { IssuesStore } from '../issues.store';
|
||||
import { IssueEntity, IssuesStore } from '../issues.store';
|
||||
|
||||
const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
id: 1,
|
||||
type: 'Story',
|
||||
assignee: '',
|
||||
epic: '',
|
||||
name: 'Test Issue',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
estimatedTime: null,
|
||||
dependsOnIds: [],
|
||||
comments: [],
|
||||
priority: 'Moyenne',
|
||||
status: 'draft',
|
||||
progress: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
class FakeIssuesStore {
|
||||
private _data = signal<IssueEntity[]>([makeIssue({ id: 1 })]);
|
||||
|
||||
readonly issues = this._data.asReadonly();
|
||||
readonly loading = signal(false);
|
||||
readonly loaded = signal(true);
|
||||
|
||||
getById(id: number): IssueEntity | undefined {
|
||||
return this._data().find((i) => i.id === id);
|
||||
}
|
||||
|
||||
getNextId(): number {
|
||||
const ids = this._data().map((i) => i.id);
|
||||
return ids.length === 0 ? 1 : Math.max(...ids) + 1;
|
||||
}
|
||||
|
||||
load(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
upsert(issue: any): Promise<IssueEntity> {
|
||||
const { comments: c, estimatedTime: et, dependsOnIds: deps, ...rest } = issue;
|
||||
const normalized: IssueEntity = {
|
||||
...makeIssue(),
|
||||
...rest,
|
||||
dependsOnIds: Array.isArray(deps) ? deps.filter((v: unknown) => typeof v === 'number') : [],
|
||||
comments: Array.isArray(c) ? c : [],
|
||||
estimatedTime: et ?? null,
|
||||
};
|
||||
this._data.update((issues) => {
|
||||
const idx = issues.findIndex((i) => i.id === normalized.id);
|
||||
if (idx === -1) return [...issues, normalized];
|
||||
const copy = [...issues];
|
||||
copy[idx] = normalized;
|
||||
return copy;
|
||||
});
|
||||
return Promise.resolve(normalized);
|
||||
}
|
||||
|
||||
deleteById(id: number): Promise<void> {
|
||||
this._data.update((issues) =>
|
||||
issues
|
||||
.filter((i) => i.id !== id)
|
||||
.map((i) => ({ ...i, dependsOnIds: i.dependsOnIds.filter((d) => d !== id) })),
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
describe('IssueComments', () => {
|
||||
let component: IssueComments;
|
||||
let fixture: ComponentFixture<IssueComments>;
|
||||
let store: IssuesStore;
|
||||
let store: FakeIssuesStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
store = new FakeIssuesStore();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IssueComments],
|
||||
providers: [{ provide: IssuesStore, useValue: store }],
|
||||
}).compileComponents();
|
||||
|
||||
store = TestBed.inject(IssuesStore);
|
||||
fixture = TestBed.createComponent(IssueComments);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput('issueId', 1);
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
@@ -68,9 +131,9 @@ describe('IssueComments', () => {
|
||||
expect(store.getById(1)?.comments[0].text).toBe('Test comment');
|
||||
});
|
||||
|
||||
it('clears newCommentText after adding', () => {
|
||||
it('clears newCommentText after adding', async () => {
|
||||
(component as any).newCommentText = 'Some text';
|
||||
(component as any).addComment();
|
||||
await (component as any).addComment();
|
||||
expect((component as any).newCommentText).toBe('');
|
||||
});
|
||||
|
||||
@@ -147,9 +210,9 @@ describe('IssueComments', () => {
|
||||
expect(store.getById(1)?.comments[0].updatedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
it('resets editing state after saving', () => {
|
||||
it('resets editing state after saving', async () => {
|
||||
(component as any).editingCommentText = 'Done';
|
||||
(component as any).saveEditComment();
|
||||
await (component as any).saveEditComment();
|
||||
expect((component as any).editingCommentId).toBeNull();
|
||||
expect((component as any).editingCommentText).toBe('');
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
|
||||
import { provideRouter } from '@angular/router';
|
||||
@@ -23,6 +24,63 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
class FakeIssuesStore {
|
||||
private _data = signal<IssueEntity[]>([
|
||||
makeIssue({ id: 1, name: 'Issue 1' }),
|
||||
makeIssue({ id: 2, name: 'Issue 2' }),
|
||||
makeIssue({ id: 3, name: 'Issue 3' }),
|
||||
]);
|
||||
|
||||
readonly issues = this._data.asReadonly();
|
||||
readonly loading = signal(false);
|
||||
readonly loaded = signal(true);
|
||||
|
||||
getById(id: number): IssueEntity | undefined {
|
||||
return this._data().find((i) => i.id === id);
|
||||
}
|
||||
|
||||
getNextId(): number {
|
||||
const ids = this._data().map((i) => i.id);
|
||||
return ids.length === 0 ? 1 : Math.max(...ids) + 1;
|
||||
}
|
||||
|
||||
load(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
upsert(issue: any): Promise<IssueEntity> {
|
||||
const { comments: c, estimatedTime: et, dependsOnIds: deps, dependsOnId: legacy, ...rest } = issue;
|
||||
const normalized: IssueEntity = {
|
||||
...makeIssue(),
|
||||
...rest,
|
||||
dependsOnIds: Array.isArray(deps)
|
||||
? deps.filter((v: unknown) => typeof v === 'number')
|
||||
: typeof legacy === 'number'
|
||||
? [legacy]
|
||||
: [],
|
||||
comments: Array.isArray(c) ? c : [],
|
||||
estimatedTime: et ?? null,
|
||||
};
|
||||
this._data.update((issues) => {
|
||||
const idx = issues.findIndex((i) => i.id === normalized.id);
|
||||
if (idx === -1) return [...issues, normalized];
|
||||
const copy = [...issues];
|
||||
copy[idx] = normalized;
|
||||
return copy;
|
||||
});
|
||||
return Promise.resolve(normalized);
|
||||
}
|
||||
|
||||
deleteById(id: number): Promise<void> {
|
||||
this._data.update((issues) =>
|
||||
issues
|
||||
.filter((i) => i.id !== id)
|
||||
.map((i) => ({ ...i, dependsOnIds: i.dependsOnIds.filter((d) => d !== id) })),
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function makeRoute(id = '1', path = 'issues/:id') {
|
||||
return {
|
||||
snapshot: {
|
||||
@@ -37,30 +95,26 @@ function makeRoute(id = '1', path = 'issues/:id') {
|
||||
describe('IssueDetail — existing issue', () => {
|
||||
let component: IssueDetail;
|
||||
let fixture: ComponentFixture<IssueDetail>;
|
||||
let store: IssuesStore;
|
||||
let store: FakeIssuesStore;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
store = new FakeIssuesStore();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IssueDetail],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: ActivatedRoute, useValue: makeRoute('1') },
|
||||
{ provide: IssuesStore, useValue: store },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
store = TestBed.inject(IssuesStore);
|
||||
router = TestBed.inject(Router);
|
||||
fixture = TestBed.createComponent(IssueDetail);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
@@ -115,7 +169,7 @@ describe('IssueDetail — existing issue', () => {
|
||||
describe('deleteIssue', () => {
|
||||
it('removes the issue and navigates to /issues', async () => {
|
||||
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
(component as any).deleteIssue();
|
||||
await (component as any).deleteIssue();
|
||||
expect(store.getById(1)).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith(['/issues']);
|
||||
});
|
||||
@@ -169,9 +223,9 @@ describe('IssueDetail — existing issue', () => {
|
||||
expect((component as any).selectedCandidateId).toBeNull();
|
||||
});
|
||||
|
||||
it('confirmAddDependency adds the selected id and saves', () => {
|
||||
it('confirmAddDependency adds the selected id and saves', async () => {
|
||||
(component as any).selectedCandidateId = 2;
|
||||
(component as any).confirmAddDependency();
|
||||
await (component as any).confirmAddDependency();
|
||||
expect((component as any).issue.dependsOnIds).toContain(2);
|
||||
expect(store.getById(1)?.dependsOnIds).toContain(2);
|
||||
expect((component as any).showAddDependency).toBe(false);
|
||||
@@ -370,9 +424,9 @@ describe('IssueDetail — existing issue', () => {
|
||||
expect(created?.type).toBe('Story');
|
||||
});
|
||||
|
||||
it('confirmCreateInEpic resets the form', () => {
|
||||
it('confirmCreateInEpic resets the form', async () => {
|
||||
(component as any).newIssueName = 'Child Issue';
|
||||
(component as any).confirmCreateInEpic();
|
||||
await (component as any).confirmCreateInEpic();
|
||||
expect((component as any).showCreateInEpic).toBe(false);
|
||||
expect((component as any).newIssueName).toBe('');
|
||||
});
|
||||
@@ -405,9 +459,9 @@ describe('IssueDetail — existing issue', () => {
|
||||
expect((component as any).selectedEpicCandidateId).toBeNull();
|
||||
});
|
||||
|
||||
it('confirmAddToEpic assigns the epic name to the selected issue', () => {
|
||||
it('confirmAddToEpic assigns the epic name to the selected issue', async () => {
|
||||
(component as any).selectedEpicCandidateId = 2;
|
||||
(component as any).confirmAddToEpic();
|
||||
await (component as any).confirmAddToEpic();
|
||||
expect(store.getById(2)?.epic).toBe('My Epic');
|
||||
expect((component as any).showAddToEpic).toBe(false);
|
||||
});
|
||||
@@ -436,11 +490,11 @@ describe('IssueDetail — existing issue', () => {
|
||||
describe('IssueDetail — new issue route', () => {
|
||||
let component: IssueDetail;
|
||||
let fixture: ComponentFixture<IssueDetail>;
|
||||
let store: IssuesStore;
|
||||
let store: FakeIssuesStore;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
store = new FakeIssuesStore();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IssueDetail],
|
||||
providers: [
|
||||
@@ -456,20 +510,16 @@ describe('IssueDetail — new issue route', () => {
|
||||
paramMap: of(convertToParamMap({})),
|
||||
},
|
||||
},
|
||||
{ provide: IssuesStore, useValue: store },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
store = TestBed.inject(IssuesStore);
|
||||
router = TestBed.inject(Router);
|
||||
fixture = TestBed.createComponent(IssueDetail);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
@@ -486,14 +536,14 @@ describe('IssueDetail — new issue route', () => {
|
||||
it('saveIssue without explicit flag does nothing for new route', () => {
|
||||
(component as any).issue.name = 'Draft Name';
|
||||
const countBefore = store.issues().length;
|
||||
(component as any).saveIssue(); // explicit = false
|
||||
(component as any).saveIssue();
|
||||
expect(store.issues().length).toBe(countBefore);
|
||||
});
|
||||
|
||||
it('saveIssue with explicit=true creates the issue and navigates', async () => {
|
||||
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
(component as any).issue.name = 'Brand New Issue';
|
||||
(component as any).saveIssue(true);
|
||||
await (component as any).saveIssue(true);
|
||||
expect(store.issues().some((i) => i.name === 'Brand New Issue')).toBe(true);
|
||||
expect(spy).toHaveBeenCalledWith(['/issues', 10]);
|
||||
});
|
||||
|
||||
@@ -278,8 +278,9 @@ export class IssueDetail {
|
||||
const isNewIssueRoute = this.route.snapshot.routeConfig?.path === 'issues/new';
|
||||
|
||||
if (isNewIssueRoute) {
|
||||
const draftId = Number(this.route.snapshot.queryParamMap.get('draftId') ?? 0);
|
||||
return {
|
||||
id: 0,
|
||||
id: draftId,
|
||||
type: 'Story',
|
||||
assignee: '',
|
||||
epic: '',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { provideRouter } from '@angular/router';
|
||||
@@ -22,30 +23,93 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
class FakeIssuesStore {
|
||||
private _data = signal<IssueEntity[]>([
|
||||
makeIssue({ id: 1, name: 'Issue 1', progress: 0 }),
|
||||
makeIssue({ id: 2, name: 'Issue 2', progress: 0 }),
|
||||
makeIssue({ id: 3, name: 'Issue 3', progress: 0 }),
|
||||
]);
|
||||
|
||||
readonly issues = this._data.asReadonly();
|
||||
readonly loading = signal(false);
|
||||
readonly loaded = signal(true);
|
||||
|
||||
getById(id: number): IssueEntity | undefined {
|
||||
return this._data().find((i) => i.id === id);
|
||||
}
|
||||
|
||||
getNextId(): number {
|
||||
const ids = this._data().map((i) => i.id);
|
||||
return ids.length === 0 ? 1 : Math.max(...ids) + 1;
|
||||
}
|
||||
|
||||
load(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
upsert(issue: any): Promise<IssueEntity> {
|
||||
const { comments: c, estimatedTime: et, dependsOnIds: deps, dependsOnId: legacy, ...rest } = issue;
|
||||
const normalized: IssueEntity = {
|
||||
type: 'Story',
|
||||
assignee: '',
|
||||
epic: '',
|
||||
name: '',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
estimatedTime: et ?? null,
|
||||
comments: Array.isArray(c) ? c : [],
|
||||
priority: 'Moyenne',
|
||||
status: 'draft',
|
||||
progress: 0,
|
||||
...rest,
|
||||
dependsOnIds: Array.isArray(deps)
|
||||
? deps.filter((v: unknown) => typeof v === 'number')
|
||||
: typeof legacy === 'number'
|
||||
? [legacy]
|
||||
: [],
|
||||
};
|
||||
this._data.update((issues) => {
|
||||
const idx = issues.findIndex((i) => i.id === normalized.id);
|
||||
if (idx === -1) return [...issues, normalized];
|
||||
const copy = [...issues];
|
||||
copy[idx] = normalized;
|
||||
return copy;
|
||||
});
|
||||
return Promise.resolve(normalized);
|
||||
}
|
||||
|
||||
deleteById(id: number): Promise<void> {
|
||||
this._data.update((issues) =>
|
||||
issues
|
||||
.filter((i) => i.id !== id)
|
||||
.map((i) => ({ ...i, dependsOnIds: i.dependsOnIds.filter((d) => d !== id) })),
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
describe('Issues', () => {
|
||||
let component: Issues;
|
||||
let fixture: ComponentFixture<Issues>;
|
||||
let store: IssuesStore;
|
||||
let store: FakeIssuesStore;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
store = new FakeIssuesStore();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Issues],
|
||||
providers: [provideRouter([])],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: IssuesStore, useValue: store },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
store = TestBed.inject(IssuesStore);
|
||||
router = TestBed.inject(Router);
|
||||
fixture = TestBed.createComponent(Issues);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
@@ -65,7 +129,6 @@ describe('Issues', () => {
|
||||
it('returns empty array when no issues match the selected type', () => {
|
||||
(component as any).selectedType = 'Epic';
|
||||
const filtered: IssueEntity[] = (component as any).filteredIssues;
|
||||
// Default store has no Epics, so this should be empty
|
||||
expect(filtered.every((i) => i.type === 'Epic')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -97,13 +160,10 @@ describe('Issues', () => {
|
||||
});
|
||||
|
||||
describe('createIssue', () => {
|
||||
it('navigates to /issues/new with a draftId query param', async () => {
|
||||
it('navigates to /issues/new', () => {
|
||||
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
(component as any).createIssue();
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
['/issues/new'],
|
||||
expect.objectContaining({ queryParams: expect.objectContaining({ mode: 'edit' }) }),
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(['/issues/new']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,11 @@ export class IssuesStore {
|
||||
return this.data().find((i) => i.id === id);
|
||||
}
|
||||
|
||||
getNextId(): number {
|
||||
const ids = this.data().map((i) => i.id);
|
||||
return ids.length === 0 ? 1 : Math.max(...ids) + 1;
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
if (this.loaded()) return;
|
||||
this.loading.set(true);
|
||||
|
||||
Reference in New Issue
Block a user