Cover class and person timetable filters that phases 16-17 already promised.

GET ?classId=/personId= and personTimetableQuery had no tests; occupancy on the grid comment still pointed at the old map snapshot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 14:58:39 +03:00
co-authored by Cursor
parent 67b7f7ec08
commit 5a049d43c8
3 changed files with 112 additions and 6 deletions
@@ -2,10 +2,10 @@
* @vitest-environment happy-dom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiError, pinLesson, type Timetable, type TimetableLesson } from '../net/api.ts';
import { ApiError, pinLesson, type PersonCard, type Timetable, type TimetableLesson } from '../net/api.ts';
import { getLocale, setLocale } from '../i18n/locale.ts';
import { t } from '../i18n/strings.ts';
import { TimetableGrid } from './timetableGrid.ts';
import { personTimetableQuery, TimetableGrid } from './timetableGrid.ts';
vi.mock('../net/api.ts', async (importOriginal) => {
const actual = await importOriginal<typeof import('../net/api.ts')>();
@@ -100,4 +100,72 @@ describe('TimetableGrid planner errors', () => {
it('does not swallow an unknown failure', async () => {
await expect(pinAndReadError(new Error('down'))).resolves.toBe(t('timetableErrorUnknown'));
});
it('marks a pinned lesson with the locked cell class', () => {
const grid = new TimetableGrid({ editable: true, showClass: false });
document.body.append(grid.element);
grid.setTable({ ...table(), lessons: [{ ...lesson, locked: true }] }, 'c1');
const cell = grid.element.querySelector('.timetable__cell--locked');
expect(cell).not.toBeNull();
expect(cell?.getAttribute('title')).toBe(t('timetableLocked'));
});
});
describe('personTimetableQuery', () => {
it('asks for a staff member by personId', () => {
expect(personTimetableQuery(card({ id: 't1', roles: ['staff'], classId: null }))).toEqual({
personId: 't1',
});
});
it('asks for a pupil by classId', () => {
expect(personTimetableQuery(card({ id: 'p1', roles: ['student'], classId: 'c1' }))).toEqual({
classId: 'c1',
});
});
it('gives a parent who is not staff no personal grid', () => {
expect(personTimetableQuery(card({ id: 'a1', roles: ['parent'], classId: null }))).toBeNull();
});
});
function card(overrides: Partial<PersonCard>): PersonCard {
return {
id: 'p1',
fullName: 'Ivanov Ivan',
surname: 'Ivanov',
given: 'Ivan',
patronymic: '',
female: false,
age: 12,
birthDate: '2000-01-01',
roles: ['student'],
classYear: 5,
classLetter: 'A',
classId: 'c1',
position: null,
positionLabel: null,
body: [],
skills: [],
traits: [],
needs: [],
activity: null,
activityLabel: null,
talkCircleMemberIds: [],
talkTopicId: null,
family: { parents: [], children: [], siblings: [], partners: [] },
worn: [],
carried: [],
carryMass: 0,
carryCapacity: 0,
hasLocker: false,
homeCount: 0,
hasAvatar: false,
hasCustom: false,
hasFullBody: false,
customPortraitPrompt: null,
connections: null,
...overrides,
};
}
+1 -1
View File
@@ -19,7 +19,7 @@ export interface TimetableGridOptions {
}
/**
* Day × period grid. Occupancy itself lives on the map snapshot; this is the week table the
* Day × period grid. Live occupancy lives on the presence frame; this is the week table the
* player reads and, in Management, pins.
*/
export class TimetableGrid {