All checks were successful
Build, test and publish / verify (push) Successful in 7m31s
214 lines
8.1 KiB
TypeScript
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');
|
|
}
|
|
});
|
|
});
|