Files
agiledancers/agiledancers-fe/src/app/shared/entity-editor.spec.ts
Wanja Krah 694bf3fa86
All checks were successful
Build, test and publish / verify (push) Successful in 7m31s
S16 postgresql opslag added
2026-11-04 17:10:25 +01:00

214 lines
8.1 KiB
TypeScript

import { TestBed } from '@angular/core/testing';
import { HttpErrorResponse } from '@angular/common/http';
import { of, throwError } from 'rxjs';
import { SchoolApi } from '../core/school-api';
import { EntityEditor } from './entity-editor';
import { EditorTarget, Show } from '../core/api-models';
describe('EntityEditor', () => {
const api = { saveSchool: vi.fn(), saveGroup: vi.fn(), saveDancer: vi.fn(), saveShow: vi.fn() };
beforeEach(() => {
vi.resetAllMocks();
TestBed.configureTestingModule({
imports: [EntityEditor],
providers: [{ provide: SchoolApi, useValue: api }],
});
});
function editor(target: EditorTarget, shows: Show[] = []) {
const fixture = TestBed.createComponent(EntityEditor);
fixture.componentRef.setInput('target', target);
fixture.componentRef.setInput('schoolId', 7);
fixture.componentRef.setInput('shows', shows);
fixture.componentInstance.ngOnInit();
return fixture.componentInstance;
}
it('rejects blank names and invalid age ranges before calling the API', async () => {
const component = editor({ kind: 'group' });
component.name = ' ';
expect(component.validDetails()).toBe(false);
component.name = 'Teens';
component.startAge = 18;
component.endAge = 12;
await component.save();
expect(api.saveGroup).not.toHaveBeenCalled();
component.endAge = 18.5;
expect(component.validDetails()).toBe(false);
});
it('does not accept a future birth date', () => {
const component = editor({ kind: 'dancer' });
component.name = 'Sam';
component.date = '2999-01-01';
expect(component.validDetails()).toBe(false);
});
it('trims the school name and emits saved only after success', async () => {
api.saveSchool.mockReturnValue(of({ id: 0 }));
const component = editor({ kind: 'school' });
component.name = ' Studio ';
const saved = vi.fn();
component.saved.subscribe(saved);
await component.save();
expect(api.saveSchool).toHaveBeenCalledWith({ name: 'Studio' }, undefined, undefined);
expect(saved).toHaveBeenCalledOnce();
expect(component.busy()).toBe(false);
});
it('retains the revision from opening the editor across background refreshes', async () => {
api.saveDancer.mockReturnValue(of({ id: 3 }));
const fixture = TestBed.createComponent(EntityEditor);
fixture.componentRef.setInput('target', {
kind: 'dancer',
value: { id: 3, name: 'Sam', birthDate: '2010-01-01' },
});
fixture.componentRef.setInput('schoolId', 7);
fixture.componentRef.setInput('revision', 4);
const component = fixture.componentInstance;
component.ngOnInit();
fixture.componentRef.setInput('revision', 5);
component.name = 'Sam edited';
await component.save();
expect(api.saveDancer).toHaveBeenCalledWith(
7,
{ name: 'Sam edited', birthDate: '2010-01-01' },
3,
4,
);
});
it('keeps the editor open and preserves input when saving fails', async () => {
api.saveSchool.mockReturnValue(throwError(() => new HttpErrorResponse({ status: 0 })));
const component = editor({ kind: 'school' });
component.name = 'Studio';
const saved = vi.fn();
component.saved.subscribe(saved);
await component.save();
expect(saved).not.toHaveBeenCalled();
expect(component.name).toBe('Studio');
expect(component.error()).toContain('niet bereikbaar');
expect(component.busy()).toBe(false);
});
const show: Show = {
id: 3,
name: 'Summer',
preferredDayParts: 2,
finalized: false,
violations: [],
dayParts: [
{ date: '2027-06-01', type: 'AFTERNOON', danceGroupIds: [4] },
{ date: '2027-06-02', type: 'AFTERNOON', danceGroupIds: [4] },
],
appearances: { REGULAR: 2, SELECTION: 2, PRE_SCHOOLER: 1, SHOW_CASE: 2, PRE_EDUCATION: 2 },
};
it('defaults preferredDayParts to two and rejects duplicate dated dayparts and fractional preferredDayParts', () => {
const component = editor({ kind: 'show' });
component.name = 'Show';
expect(component.validDetails()).toBe(true);
component.addPart();
component.dayParts[0].date = '2027-06-01';
component.addPart();
component.dayParts[1].date = '2027-06-02';
component.addPart();
component.dayParts[2].date = '2027-06-03';
expect(component.preferredDayParts).toBe(2);
expect(component.validDetails()).toBe(true);
component.dayParts[2].date = '2027-06-02';
expect(component.validDetails()).toBe(false);
component.dayParts[2].date = '2027-06-03';
component.preferredDayParts = 1.5;
expect(component.validDetails()).toBe(false);
});
it('preserves an explicitly chosen preferredDayParts when adding dayparts to a new show', async () => {
api.saveShow.mockReturnValue(of(show));
const component = editor({ kind: 'show' });
component.name = 'Show';
component.addPart();
component.dayParts[0].date = '2027-06-01';
component.addPart();
component.dayParts[1].date = '2027-06-02';
component.preferredDayParts = 1;
component.addPart();
component.dayParts[2].date = '2027-06-03';
expect(component.preferredDayParts).toBe(1);
await component.save();
expect(api.saveShow).toHaveBeenCalledWith(
7,
expect.objectContaining({ preferredDayParts: 1 }),
undefined,
undefined,
);
});
it('retains preferredDayParts when dayparts are removed so it can be explicitly corrected', () => {
const component = editor({ kind: 'show', value: show });
component.addPart();
component.dayParts[2].date = '2027-06-03';
expect(component.preferredDayParts).toBe(2);
component.removePart(2);
component.removePart(1);
expect(component.preferredDayParts).toBe(2);
expect(component.validDetails()).toBe(true);
component.preferredDayParts = 1;
expect(component.validDetails()).toBe(true);
});
it('allows cancelling an invalidating edit and skips warnings for name-only edits', async () => {
api.saveShow.mockReturnValue(of(show));
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false);
try {
const component = editor({ kind: 'show', value: show });
component.preferredDayParts = 1;
await component.save();
expect(confirm).toHaveBeenCalledWith(expect.stringContaining('Summer'));
expect(api.saveShow).not.toHaveBeenCalled();
component.preferredDayParts = 2;
confirm.mockClear();
component.name = 'Renamed';
await component.save();
expect(confirm).not.toHaveBeenCalled();
expect(api.saveShow).toHaveBeenCalledOnce();
} finally {
confirm.mockRestore();
}
});
it('names affected shows before adding a group and allows cancellation', async () => {
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false);
try {
const component = editor({ kind: 'group' }, [show]);
component.name = 'New group';
await component.save();
expect(confirm).toHaveBeenCalledWith(expect.stringContaining('Summer'));
expect(api.saveGroup).not.toHaveBeenCalled();
} finally {
confirm.mockRestore();
}
});
it('displays one editable preferredDayParts field with a default of one for a one-daypart show', async () => {
// jsdom does not implement native dialog opening.
const descriptor = Object.getOwnPropertyDescriptor(HTMLDialogElement.prototype, 'showModal');
Object.defineProperty(HTMLDialogElement.prototype, 'showModal', {
configurable: true,
value: () => undefined,
});
try {
const fixture = TestBed.createComponent(EntityEditor);
fixture.componentRef.setInput('target', { kind: 'show' });
fixture.componentInstance.addPart();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
const inputs = Array.from(element.querySelectorAll<HTMLInputElement>('input[type="number"]'));
expect(inputs).toHaveLength(1);
expect(inputs.every((input) => !input.disabled && input.value === '1')).toBe(true);
} finally {
if (descriptor) Object.defineProperty(HTMLDialogElement.prototype, 'showModal', descriptor);
else Reflect.deleteProperty(HTMLDialogElement.prototype, 'showModal');
}
});
});