Gestion issue epic dans milestone
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { of } from 'rxjs';
|
||||
import { afterEach, vi } from 'vitest';
|
||||
import { MilestoneDetail } from './milestone-detail';
|
||||
import { IssueEntity, IssuesStore } from '../../issues/issues.store';
|
||||
import { MilestoneEntity, MilestonesStore } from '../milestones.store';
|
||||
|
||||
const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||
id: 99,
|
||||
type: 'Story',
|
||||
assignee: '',
|
||||
epic: '',
|
||||
name: 'Test Issue',
|
||||
dueDate: '',
|
||||
description: '',
|
||||
estimatedTime: null,
|
||||
dependsOnIds: [],
|
||||
comments: [],
|
||||
priority: 'MOYENNE',
|
||||
status: 'draft',
|
||||
progress: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeMilestone = (overrides: Partial<MilestoneEntity> = {}): MilestoneEntity => ({
|
||||
id: 1,
|
||||
name: 'Sprint 1',
|
||||
description: '',
|
||||
dueDate: '',
|
||||
issueIds: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
class FakeIssuesStore {
|
||||
private _data = signal<IssueEntity[]>([]);
|
||||
|
||||
readonly issues = this._data.asReadonly();
|
||||
readonly loading = signal(false);
|
||||
readonly loaded = signal(true);
|
||||
|
||||
seed(issues: IssueEntity[]): void {
|
||||
this._data.set(issues);
|
||||
}
|
||||
|
||||
getById(id: number): IssueEntity | undefined {
|
||||
return this._data().find((i) => i.id === id);
|
||||
}
|
||||
|
||||
load(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
upsert(issue: IssueEntity): Promise<IssueEntity> {
|
||||
const normalized = { ...makeIssue(), ...issue };
|
||||
this._data.update((list) => {
|
||||
const idx = list.findIndex((i) => i.id === normalized.id);
|
||||
if (idx === -1) return [...list, { ...normalized, id: normalized.id || list.length + 1 }];
|
||||
const copy = [...list];
|
||||
copy[idx] = normalized;
|
||||
return copy;
|
||||
});
|
||||
return Promise.resolve(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeMilestonesStore {
|
||||
private _data = signal<MilestoneEntity[]>([makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [] })]);
|
||||
|
||||
readonly milestones = this._data.asReadonly();
|
||||
readonly loading = signal(false);
|
||||
readonly loaded = signal(true);
|
||||
|
||||
seed(milestones: MilestoneEntity[]): void {
|
||||
this._data.set(milestones);
|
||||
}
|
||||
|
||||
getById(id: number): MilestoneEntity | undefined {
|
||||
return this._data().find((m) => m.id === id);
|
||||
}
|
||||
|
||||
load(): Promise<MilestoneEntity[]> {
|
||||
return Promise.resolve(this._data());
|
||||
}
|
||||
|
||||
upsert(milestone: MilestoneEntity): Promise<MilestoneEntity> {
|
||||
this._data.update((list) => {
|
||||
const idx = list.findIndex((m) => m.id === milestone.id);
|
||||
if (idx === -1) return [...list, milestone];
|
||||
const copy = [...list];
|
||||
copy[idx] = milestone;
|
||||
return copy;
|
||||
});
|
||||
return Promise.resolve(milestone);
|
||||
}
|
||||
|
||||
deleteById(id: number): Promise<void> {
|
||||
this._data.update((list) => list.filter((m) => m.id !== id));
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function makeRoute(id = '1', path = 'milestones/:id') {
|
||||
return {
|
||||
snapshot: {
|
||||
routeConfig: { path },
|
||||
paramMap: convertToParamMap(id ? { id } : {}),
|
||||
},
|
||||
paramMap: of(convertToParamMap(id ? { id } : {})),
|
||||
};
|
||||
}
|
||||
|
||||
describe('MilestoneDetail', () => {
|
||||
let component: MilestoneDetail;
|
||||
let fixture: ComponentFixture<MilestoneDetail>;
|
||||
let issuesStore: FakeIssuesStore;
|
||||
let milestonesStore: FakeMilestonesStore;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
issuesStore = new FakeIssuesStore();
|
||||
milestonesStore = new FakeMilestonesStore();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MilestoneDetail],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: ActivatedRoute, useValue: makeRoute('1') },
|
||||
{ provide: IssuesStore, useValue: issuesStore },
|
||||
{ provide: MilestonesStore, useValue: milestonesStore },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
fixture = TestBed.createComponent(MilestoneDetail);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('linkedIssues', () => {
|
||||
it('returns issues whose id is in milestone.issueIds', () => {
|
||||
issuesStore.seed([makeIssue({ id: 1 }), makeIssue({ id: 2 })]);
|
||||
(component as any).milestone.issueIds = [1];
|
||||
const linked: IssueEntity[] = (component as any).linkedIssues;
|
||||
expect(linked.map((i) => i.id)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('returns empty array when milestone has no issues', () => {
|
||||
issuesStore.seed([makeIssue({ id: 1 })]);
|
||||
(component as any).milestone.issueIds = [];
|
||||
expect((component as any).linkedIssues).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('availableIssues', () => {
|
||||
beforeEach(() => {
|
||||
issuesStore.seed([
|
||||
makeIssue({ id: 1, name: 'Issue 1', epic: '' }),
|
||||
makeIssue({ id: 2, name: 'Issue 2', epic: '' }),
|
||||
makeIssue({ id: 3, name: 'Issue 3', epic: '' }),
|
||||
]);
|
||||
milestonesStore.seed([makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [] })]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [] });
|
||||
});
|
||||
|
||||
it('returns all issues when milestone is empty', () => {
|
||||
expect((component as any).availableIssues).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('excludes issues already in this milestone', () => {
|
||||
(component as any).milestone.issueIds = [1];
|
||||
const available: IssueEntity[] = (component as any).availableIssues;
|
||||
expect(available.some((i) => i.id === 1)).toBe(false);
|
||||
expect(available).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('excludes issues assigned to another milestone', () => {
|
||||
milestonesStore.seed([
|
||||
makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [] }),
|
||||
makeMilestone({ id: 2, name: 'Sprint 2', issueIds: [2] }),
|
||||
]);
|
||||
const available: IssueEntity[] = (component as any).availableIssues;
|
||||
expect(available.some((i) => i.id === 2)).toBe(false);
|
||||
expect(available).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('excludes children of an epic already in this milestone', () => {
|
||||
issuesStore.seed([
|
||||
makeIssue({ id: 1, type: 'Epic', name: 'My Epic', epic: '' }),
|
||||
makeIssue({ id: 2, name: 'Child 1', epic: 'My Epic' }),
|
||||
makeIssue({ id: 3, name: 'Independent', epic: '' }),
|
||||
]);
|
||||
(component as any).milestone.issueIds = [1];
|
||||
const available: IssueEntity[] = (component as any).availableIssues;
|
||||
expect(available.some((i) => i.id === 2)).toBe(false);
|
||||
expect(available.some((i) => i.id === 3)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not exclude issues whose epic is not in the milestone', () => {
|
||||
issuesStore.seed([
|
||||
makeIssue({ id: 1, type: 'Epic', name: 'Other Epic', epic: '' }),
|
||||
makeIssue({ id: 2, name: 'Child', epic: 'Other Epic' }),
|
||||
makeIssue({ id: 3, name: 'Independent', epic: '' }),
|
||||
]);
|
||||
(component as any).milestone.issueIds = [];
|
||||
const available: IssueEntity[] = (component as any).availableIssues;
|
||||
expect(available.some((i) => i.id === 2)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayedIssues', () => {
|
||||
it('shows all linked issues when no epic is in the milestone', () => {
|
||||
issuesStore.seed([
|
||||
makeIssue({ id: 1, name: 'Story A', epic: '' }),
|
||||
makeIssue({ id: 2, name: 'Story B', epic: '' }),
|
||||
]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, issueIds: [1, 2] });
|
||||
expect((component as any).displayedIssues).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('hides children of an epic already in the milestone', () => {
|
||||
issuesStore.seed([
|
||||
makeIssue({ id: 1, type: 'Epic', name: 'My Epic', epic: '' }),
|
||||
makeIssue({ id: 2, name: 'Child', epic: 'My Epic' }),
|
||||
makeIssue({ id: 3, name: 'Independent', epic: '' }),
|
||||
]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, issueIds: [1, 2, 3] });
|
||||
const displayed: IssueEntity[] = (component as any).displayedIssues;
|
||||
expect(displayed.some((i) => i.id === 1)).toBe(true);
|
||||
expect(displayed.some((i) => i.id === 2)).toBe(false);
|
||||
expect(displayed.some((i) => i.id === 3)).toBe(true);
|
||||
});
|
||||
|
||||
it('shows children when their epic is not in the milestone', () => {
|
||||
issuesStore.seed([
|
||||
makeIssue({ id: 1, type: 'Epic', name: 'Other Epic', epic: '' }),
|
||||
makeIssue({ id: 2, name: 'Child', epic: 'Other Epic' }),
|
||||
]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, issueIds: [2] });
|
||||
const displayed: IssueEntity[] = (component as any).displayedIssues;
|
||||
expect(displayed.some((i) => i.id === 2)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('progress', () => {
|
||||
it('returns 0 when milestone has no issues', () => {
|
||||
issuesStore.seed([]);
|
||||
(component as any).milestone.issueIds = [];
|
||||
expect((component as any).progress).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 100 when all linked issues are done', () => {
|
||||
issuesStore.seed([
|
||||
makeIssue({ id: 1, status: 'done' }),
|
||||
makeIssue({ id: 2, status: 'done' }),
|
||||
]);
|
||||
(component as any).milestone.issueIds = [1, 2];
|
||||
expect((component as any).progress).toBe(100);
|
||||
});
|
||||
|
||||
it('returns 50 when half the linked issues are done', () => {
|
||||
issuesStore.seed([
|
||||
makeIssue({ id: 1, status: 'done' }),
|
||||
makeIssue({ id: 2, status: 'todo' }),
|
||||
]);
|
||||
(component as any).milestone.issueIds = [1, 2];
|
||||
expect((component as any).progress).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('issueSuggestions', () => {
|
||||
beforeEach(() => {
|
||||
issuesStore.seed([
|
||||
makeIssue({ id: 1, name: 'Fix login bug' }),
|
||||
makeIssue({ id: 2, name: 'Add dashboard' }),
|
||||
makeIssue({ id: 3, name: 'Fix signup bug' }),
|
||||
]);
|
||||
milestonesStore.seed([makeMilestone({ id: 1, issueIds: [] })]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, issueIds: [] });
|
||||
});
|
||||
|
||||
it('returns all available issues when query is empty', () => {
|
||||
(component as any).issueSearchQuery = '';
|
||||
expect((component as any).issueSuggestions).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('filters by name match', () => {
|
||||
(component as any).issueSearchQuery = 'Fix';
|
||||
const suggestions: IssueEntity[] = (component as any).issueSuggestions;
|
||||
expect(suggestions).toHaveLength(2);
|
||||
expect(suggestions.every((i) => i.name.toLowerCase().includes('fix'))).toBe(true);
|
||||
});
|
||||
|
||||
it('filters by id match', () => {
|
||||
(component as any).issueSearchQuery = '2';
|
||||
const suggestions: IssueEntity[] = (component as any).issueSuggestions;
|
||||
expect(suggestions.some((i) => i.id === 2)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openAddIssue / cancelAddIssue', () => {
|
||||
it('openAddIssue shows the add form', () => {
|
||||
(component as any).openAddIssue();
|
||||
expect((component as any).showAddIssue).toBe(true);
|
||||
expect((component as any).showCreateIssue).toBe(false);
|
||||
});
|
||||
|
||||
it('cancelAddIssue hides the form', () => {
|
||||
(component as any).showAddIssue = true;
|
||||
(component as any).cancelAddIssue();
|
||||
expect((component as any).showAddIssue).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openCreateIssue / cancelCreateIssue', () => {
|
||||
it('openCreateIssue shows the create form', () => {
|
||||
(component as any).openCreateIssue();
|
||||
expect((component as any).showCreateIssue).toBe(true);
|
||||
expect((component as any).showAddIssue).toBe(false);
|
||||
});
|
||||
|
||||
it('cancelCreateIssue hides the form and clears name', () => {
|
||||
(component as any).showCreateIssue = true;
|
||||
(component as any).newIssueName = 'Draft';
|
||||
(component as any).cancelCreateIssue();
|
||||
expect((component as any).showCreateIssue).toBe(false);
|
||||
expect((component as any).newIssueName).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigateToIssue', () => {
|
||||
it('navigates to the issue detail page', () => {
|
||||
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
(component as any).navigateToIssue(42);
|
||||
expect(spy).toHaveBeenCalledWith(['/issues', 42]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleMoreMenu / closeMoreMenu', () => {
|
||||
it('toggleMoreMenu switches moreMenuOpen', () => {
|
||||
expect((component as any).moreMenuOpen).toBe(false);
|
||||
(component as any).toggleMoreMenu();
|
||||
expect((component as any).moreMenuOpen).toBe(true);
|
||||
(component as any).toggleMoreMenu();
|
||||
expect((component as any).moreMenuOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('closeMoreMenu sets moreMenuOpen to false', () => {
|
||||
(component as any).moreMenuOpen = true;
|
||||
(component as any).closeMoreMenu();
|
||||
expect((component as any).moreMenuOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addIssueFromSearch', () => {
|
||||
it('adds the issue to milestone.issueIds and saves', async () => {
|
||||
milestonesStore.seed([makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [] })]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [] });
|
||||
await (component as any).addIssueFromSearch(5);
|
||||
expect((component as any).milestone.issueIds).toContain(5);
|
||||
expect((component as any).showAddIssue).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeIssue', () => {
|
||||
it('removes the issue from milestone.issueIds and saves', async () => {
|
||||
milestonesStore.seed([makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [1, 2] })]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [1, 2] });
|
||||
await (component as any).removeIssue(1);
|
||||
expect((component as any).milestone.issueIds).not.toContain(1);
|
||||
expect((component as any).milestone.issueIds).toContain(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('confirmCreateIssue', () => {
|
||||
beforeEach(() => {
|
||||
milestonesStore.seed([makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [] })]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [] });
|
||||
});
|
||||
|
||||
it('creates an issue and adds it to the milestone', async () => {
|
||||
(component as any).newIssueName = 'New Task';
|
||||
await (component as any).confirmCreateIssue();
|
||||
expect((component as any).milestone.issueIds).toHaveLength(1);
|
||||
expect((component as any).showCreateIssue).toBe(false);
|
||||
expect((component as any).newIssueName).toBe('');
|
||||
});
|
||||
|
||||
it('does nothing when name is blank', async () => {
|
||||
(component as any).newIssueName = ' ';
|
||||
await (component as any).confirmCreateIssue();
|
||||
expect((component as any).milestone.issueIds).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveMilestone', () => {
|
||||
it('persists the milestone to the store', async () => {
|
||||
milestonesStore.seed([makeMilestone({ id: 1, name: 'Sprint 1', issueIds: [] })]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, name: 'Updated', issueIds: [] });
|
||||
await (component as any).saveMilestone();
|
||||
expect(milestonesStore.getById(1)?.name).toBe('Updated');
|
||||
});
|
||||
|
||||
it('does nothing when name is blank', async () => {
|
||||
(component as any).milestone.name = ' ';
|
||||
await (component as any).saveMilestone();
|
||||
expect(milestonesStore.getById(1)?.name).toBe('Sprint 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('statusBadge', () => {
|
||||
it('returns correct label and colors for draft', () => {
|
||||
const badge = (component as any).statusBadge('draft');
|
||||
expect(badge.label).toBe('BROUILLON');
|
||||
});
|
||||
|
||||
it('returns correct label for done', () => {
|
||||
expect((component as any).statusBadge('done').label).toBe('TERMINÉ');
|
||||
});
|
||||
|
||||
it('returns correct label for in-progress', () => {
|
||||
expect((component as any).statusBadge('in-progress').label).toBe('EN COURS');
|
||||
});
|
||||
|
||||
it('returns correct label for todo', () => {
|
||||
expect((component as any).statusBadge('todo').label).toBe('À FAIRE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('priorityDisplay', () => {
|
||||
it('returns correct symbol for TRES_HAUTE', () => {
|
||||
expect((component as any).priorityDisplay('TRES_HAUTE').symbol).toBe('↑↑');
|
||||
});
|
||||
|
||||
it('returns correct symbol for BASSE', () => {
|
||||
expect((component as any).priorityDisplay('BASSE').symbol).toBe('↓');
|
||||
});
|
||||
|
||||
it('returns correct symbol for MOYENNE', () => {
|
||||
expect((component as any).priorityDisplay('MOYENNE').symbol).toBe('–');
|
||||
});
|
||||
});
|
||||
|
||||
describe('typeIcon', () => {
|
||||
it('returns correct letter for Epic', () => {
|
||||
expect((component as any).typeIcon('Epic').letter).toBe('E');
|
||||
});
|
||||
|
||||
it('returns correct letter for Bug', () => {
|
||||
expect((component as any).typeIcon('Bug').letter).toBe('B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMilestone', () => {
|
||||
it('removes the milestone and navigates to /milestones', async () => {
|
||||
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
await (component as any).deleteMilestone();
|
||||
expect(spy).toHaveBeenCalledWith(['/milestones']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelCreation', () => {
|
||||
it('navigates to /milestones', () => {
|
||||
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
(component as any).cancelCreation();
|
||||
expect(spy).toHaveBeenCalledWith(['/milestones']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('descriptionHtml', () => {
|
||||
it('returns sanitized HTML from markdown description', () => {
|
||||
(component as any).milestone.description = '**bold**';
|
||||
const html = (component as any).descriptionHtml;
|
||||
expect(html).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns sanitized HTML for empty description', () => {
|
||||
(component as any).milestone.description = '';
|
||||
const html = (component as any).descriptionHtml;
|
||||
expect(html).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hideIssueSuggestions', () => {
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it('hides suggestions after 150ms', () => {
|
||||
vi.useFakeTimers();
|
||||
(component as any).showIssueSuggestions = true;
|
||||
(component as any).hideIssueSuggestions();
|
||||
expect((component as any).showIssueSuggestions).toBe(true);
|
||||
vi.advanceTimersByTime(150);
|
||||
expect((component as any).showIssueSuggestions).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDescriptionPaste', () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it('does not change description when clipboard has no image', () => {
|
||||
const ta = document.createElement('textarea');
|
||||
const event = {
|
||||
clipboardData: { items: [] },
|
||||
preventDefault: vi.fn(),
|
||||
target: ta,
|
||||
} as unknown as ClipboardEvent;
|
||||
const before = (component as any).milestone.description;
|
||||
(component as any).onDescriptionPaste(event);
|
||||
expect((component as any).milestone.description).toBe(before);
|
||||
});
|
||||
|
||||
it('updates description with markdown when an image is pasted', async () => {
|
||||
vi.stubGlobal('FileReader', class {
|
||||
readonly result = 'data:image/png;base64,abc';
|
||||
onload: ((e: any) => void) | null = null;
|
||||
readAsDataURL(_file: File) {
|
||||
Promise.resolve().then(() => this.onload?.({ target: { result: this.result } }));
|
||||
}
|
||||
});
|
||||
const file = new File([''], 'img.png', { type: 'image/png' });
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = '';
|
||||
const event = {
|
||||
clipboardData: { items: [{ type: 'image/png', getAsFile: () => file }] },
|
||||
preventDefault: vi.fn(),
|
||||
target: ta,
|
||||
} as unknown as ClipboardEvent;
|
||||
(component as any).milestone.description = '';
|
||||
(component as any).onDescriptionPaste(event);
|
||||
await Promise.resolve();
|
||||
expect((component as any).milestone.description).toContain('![image]');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MilestoneDetail — new route', () => {
|
||||
let component: MilestoneDetail;
|
||||
let fixture: ComponentFixture<MilestoneDetail>;
|
||||
let milestonesStore: FakeMilestonesStore;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
milestonesStore = new FakeMilestonesStore();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MilestoneDetail],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
snapshot: {
|
||||
routeConfig: { path: 'milestones/new' },
|
||||
paramMap: convertToParamMap({}),
|
||||
},
|
||||
paramMap: of(convertToParamMap({})),
|
||||
},
|
||||
},
|
||||
{ provide: IssuesStore, useValue: new FakeIssuesStore() },
|
||||
{ provide: MilestonesStore, useValue: milestonesStore },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
fixture = TestBed.createComponent(MilestoneDetail);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('isNewRoute is true', () => {
|
||||
expect((component as any).isNewRoute).toBe(true);
|
||||
});
|
||||
|
||||
it('buildMilestone creates an empty milestone', () => {
|
||||
expect((component as any).milestone.id).toBe(0);
|
||||
expect((component as any).milestone.name).toBe('');
|
||||
});
|
||||
|
||||
it('saveMilestone without explicit flag does nothing', async () => {
|
||||
(component as any).milestone.name = 'New Sprint';
|
||||
await (component as any).saveMilestone();
|
||||
expect(milestonesStore.getById(0)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('saveMilestone with explicit=true saves and navigates to the new milestone', async () => {
|
||||
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
milestonesStore.seed([makeMilestone({ id: 1, name: 'New Sprint', issueIds: [] })]);
|
||||
(component as any).milestone = makeMilestone({ id: 1, name: 'New Sprint', issueIds: [] });
|
||||
await (component as any).saveMilestone(true);
|
||||
expect(spy).toHaveBeenCalledWith(['/milestones', 1]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user