import { Component, inject, signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Events } from '@ngrx/signals/events'; import { AuthFacade } from '@task-stream/web/identity/application'; import { ActivityGateway, CommentsGateway, IssuesGateway, Project, ProjectRole, ProjectsGateway, ReleasesGateway, TrackerRealtimeGateway, UsersGateway, } from '@task-stream/web/tracker/domain'; import { NEVER, of } from 'rxjs'; import { boardPageEvents } from '../events/board.events'; import { projectPageEvents } from '../events/project.events'; import { Notifications } from '../notifications'; import { BoardFacade, provideBoardFacade } from './board.facade'; import { HomeFacade } from './home.facade'; import { IssueFacade, provideIssueFacade } from './issue.facade'; import { ProjectFacade, provideProjectFacade } from './project.facade'; const ok = { id: 'x', key: null, aggregateVersion: 1 }; const project = { id: 'p1', key: 'WEB', members: [] } as unknown as Project; const issue = { id: 'i1', key: 'WEB-1', links: [] }; const analytics = { from: 'a', to: 'b', qa: { failureRate: 0.25 } }; /** Every command succeeds, every query answers once, nothing arrives in real time. */ function gateways() { return { projects: { project: vi.fn(() => of(project)), stats: vi.fn(() => NEVER), addMember: vi.fn(() => of(ok)), updateSettings: vi.fn(() => of(ok)), analytics: vi.fn(() => of(analytics)), }, issues: { board: vi.fn(() => of([])), issue: vi.fn(() => of(issue)), changeStatus: vi.fn(() => of(ok)), }, list: { list: vi.fn(() => of([])) }, realtime: { issueChanges: () => NEVER, commentChanges: () => NEVER, activity: () => NEVER, releaseChanges: () => NEVER, projectChanges: () => NEVER, }, }; } /** Facades are scoped to a route component, like in the app. */ @Component({ template: '', providers: [ provideProjectFacade(), provideBoardFacade(), provideIssueFacade(), ], }) class Host { readonly project = inject(ProjectFacade); readonly board = inject(BoardFacade); readonly issue = inject(IssueFacade); } function setup() { const fakes = gateways(); TestBed.configureTestingModule({ providers: [ { provide: ProjectsGateway, useValue: fakes.projects }, { provide: IssuesGateway, useValue: fakes.issues }, { provide: ReleasesGateway, useValue: fakes.list }, { provide: ActivityGateway, useValue: fakes.list }, { provide: CommentsGateway, useValue: fakes.list }, { provide: UsersGateway, useValue: { search: () => of([]) } }, { provide: TrackerRealtimeGateway, useValue: fakes.realtime }, { provide: Notifications, useValue: { success: vi.fn(), error: vi.fn() }, }, { provide: AuthFacade, useValue: { user: signal({ id: 'me' }) } }, ], }); const host = TestBed.createComponent(Host).componentInstance; return { ...host, fakes }; } describe('BoardFacade', () => { it('opens the board for a new project and refilters the same one', () => { const { board, fakes } = setup(); const seen: string[] = []; TestBed.inject(Events) .on(boardPageEvents.opened, boardPageEvents.filterChanged) .subscribe((event) => seen.push(event.type)); board.show({ projectId: 'p1' }); board.show({ projectId: 'p1', assigneeId: 'u2' }); board.show({ projectId: 'p2' }); expect(seen).toEqual([ boardPageEvents.opened.type, boardPageEvents.filterChanged.type, boardPageEvents.opened.type, ]); expect(fakes.issues.board).toHaveBeenLastCalledWith({ projectId: 'p2' }); expect(board.filter()).toEqual({ projectId: 'p2' }); }); }); describe('ProjectFacade', () => { it('ignores commands until a project is open, then sends its id', () => { const { project, fakes } = setup(); project.addMember('u2'); expect(fakes.projects.addMember).not.toHaveBeenCalled(); project.open('WEB'); expect(project.project()?.id).toBe('p1'); project.addMember('u2', ProjectRole.VIEWER); expect(fakes.projects.addMember).toHaveBeenCalledWith({ projectId: 'p1', userId: 'u2', role: ProjectRole.VIEWER, }); }); it('saves settings and loads statistics of the open project', () => { const { project, fakes } = setup(); project.open('WEB'); const settings = { statuses: [], transitions: [], issueTypes: [], areas: [], }; project.saveSettings(settings); expect(fakes.projects.updateSettings).toHaveBeenCalledWith('p1', settings); expect(project.savingSettings()).toBe(false); project.loadAnalytics(30); expect(fakes.projects.analytics).toHaveBeenCalledWith('p1', 30); expect(project.analyticsDays()).toBe(30); expect(project.analytics()).toBe(analytics); // Unknown ids still get a readable name. expect(project.workflow().status('IN_REVIEW').name).toBe('In review'); }); it('searches the directory from two characters', () => { const { project } = setup(); const seen: string[] = []; TestBed.inject(Events) .on(projectPageEvents.userSearchRequested) .subscribe(({ payload }) => seen.push(payload)); project.searchUsers(' a '); project.searchUsers(' an '); expect(seen).toEqual(['an']); }); }); describe('IssueFacade', () => { it('acts on the open ticket', () => { const { issue, fakes } = setup(); issue.changeStatus('IN_PROGRESS'); expect(fakes.issues.changeStatus).not.toHaveBeenCalled(); issue.open('WEB-1'); issue.changeStatus('QA_FAILED', 'Falla en Safari'); expect(fakes.issues.changeStatus).toHaveBeenCalledWith({ issueId: 'i1', status: 'QA_FAILED', reason: 'Falla en Safari', }); }); }); describe('HomeFacade', () => { const issue = (key: string, status: string, statusCategory: string) => ({ id: key, key, status, statusCategory, title: key }) as never; it('loads my work and groups it by category, QA failures first', () => { const mine = vi.fn((filter: { assignedToMe: boolean }) => of( filter.assignedToMe ? { items: [ issue('WEB-1', 'IN_PROGRESS', 'IN_PROGRESS'), issue('WEB-4', 'BACKLOG', 'TODO'), issue('WEB-2', 'QA_FAILED', 'IN_PROGRESS'), ], total: 3, } : { items: [issue('WEB-3', 'STAGING', 'QA')], total: 1 }, ), ); TestBed.configureTestingModule({ providers: [ { provide: IssuesGateway, useValue: { mine } }, { provide: ReleasesGateway, useValue: { upcoming: () => of([{ id: 'r1', name: 'v1.4.0' }]) }, }, ], }); const home = TestBed.inject(HomeFacade); home.load(); expect(mine).toHaveBeenCalledWith( expect.objectContaining({ assignedToMe: true }), ); expect(mine).toHaveBeenCalledWith( expect.objectContaining({ assignedToMe: false, categories: ['QA'], }), ); expect(home.loaded()).toBe(true); expect( home .mineByCategory() .map((g) => [g.category, g.issues.map((i) => i.key)]), ).toEqual([ ['IN_PROGRESS', ['WEB-2', 'WEB-1']], ['TODO', ['WEB-4']], ]); expect(home.qaFailed()).toBe(1); expect(home.awaitingQaTotal()).toBe(1); expect(home.releases().map((r) => r.name)).toEqual(['v1.4.0']); }); });