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:
@@ -2,10 +2,10 @@
|
|||||||
* @vitest-environment happy-dom
|
* @vitest-environment happy-dom
|
||||||
*/
|
*/
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
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 { getLocale, setLocale } from '../i18n/locale.ts';
|
||||||
import { t } from '../i18n/strings.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) => {
|
vi.mock('../net/api.ts', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('../net/api.ts')>();
|
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 () => {
|
it('does not swallow an unknown failure', async () => {
|
||||||
await expect(pinAndReadError(new Error('down'))).resolves.toBe(t('timetableErrorUnknown'));
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
* player reads and, in Management, pins.
|
||||||
*/
|
*/
|
||||||
export class TimetableGrid {
|
export class TimetableGrid {
|
||||||
|
|||||||
@@ -186,7 +186,30 @@ public class TimetableApiTests(AppHostFixture fixture)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Saturday_HasNoOccupancyOnTheMap()
|
public async Task GetTimetable_ClassAndPersonFilters_NarrowTheLessons()
|
||||||
|
{
|
||||||
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
|
await SchoolApiTests.ResetAsync(client);
|
||||||
|
var school = await SchoolApiTests.CreateAsync(client, "Расписание фильтр", TuesdayMorning);
|
||||||
|
var teacher = await HireMathAsync(client, school.Id);
|
||||||
|
|
||||||
|
var table = await GetTimetableAsync(client, school.Id);
|
||||||
|
var math = table.Lessons.First(lesson => lesson.Subject == "Mathematics");
|
||||||
|
var otherClass = table.Classes.First(klass => klass.Id != math.ClassId);
|
||||||
|
|
||||||
|
var byClass = await GetTimetableAsync(client, school.Id, classId: math.ClassId);
|
||||||
|
Assert.NotEmpty(byClass.Lessons);
|
||||||
|
Assert.All(byClass.Lessons, lesson => Assert.Equal(math.ClassId, lesson.ClassId));
|
||||||
|
Assert.Contains(byClass.Lessons, lesson => lesson.Subject == "Mathematics");
|
||||||
|
Assert.DoesNotContain(byClass.Lessons, lesson => lesson.ClassId == otherClass.Id);
|
||||||
|
|
||||||
|
var byTeacher = await GetTimetableAsync(client, school.Id, personId: teacher);
|
||||||
|
Assert.NotEmpty(byTeacher.Lessons);
|
||||||
|
Assert.All(byTeacher.Lessons, lesson => Assert.Equal(teacher, lesson.TeacherId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Saturday_StillStoresTheWeekTable()
|
||||||
{
|
{
|
||||||
using var client = fixture.App.CreateHttpClient("server");
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
await SchoolApiTests.ResetAsync(client);
|
await SchoolApiTests.ResetAsync(client);
|
||||||
@@ -206,10 +229,25 @@ public class TimetableApiTests(AppHostFixture fixture)
|
|||||||
return applicant.Id;
|
return applicant.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<TimetableResponse> GetTimetableAsync(HttpClient client, int schoolId)
|
private static async Task<TimetableResponse> GetTimetableAsync(
|
||||||
|
HttpClient client,
|
||||||
|
int schoolId,
|
||||||
|
string? classId = null,
|
||||||
|
string? personId = null)
|
||||||
{
|
{
|
||||||
|
var query = "lang=ru";
|
||||||
|
if (classId is not null)
|
||||||
|
{
|
||||||
|
query += $"&classId={Uri.EscapeDataString(classId)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (personId is not null)
|
||||||
|
{
|
||||||
|
query += $"&personId={Uri.EscapeDataString(personId)}";
|
||||||
|
}
|
||||||
|
|
||||||
var table = await client.GetFromJsonAsync<TimetableResponse>(
|
var table = await client.GetFromJsonAsync<TimetableResponse>(
|
||||||
$"/api/schools/{schoolId}/timetable?lang=ru",
|
$"/api/schools/{schoolId}/timetable?{query}",
|
||||||
TestContext.Current.CancellationToken);
|
TestContext.Current.CancellationToken);
|
||||||
Assert.NotNull(table);
|
Assert.NotNull(table);
|
||||||
return table;
|
return table;
|
||||||
|
|||||||
Reference in New Issue
Block a user