Merge branch 'review/slice-4-recheck'
This commit is contained in:
@@ -2,6 +2,56 @@
|
||||
|
||||
Сводка: [`../reviewed.md`](../reviewed.md). Индекс среза: [`README.md`](README.md).
|
||||
|
||||
## Срез 4. Расписание — перепроверка
|
||||
|
||||
- **Фазы:** 14–17
|
||||
- **Проверен на:** `5a049d4`, 2026-08-20
|
||||
- **Пути:** `src/HSchool.Schedule`, `src/HSchool.Content` (каркас дня, каникулы, `SubjectDef.room`),
|
||||
`src/HSchool.Simulation/SchoolTimetables.cs`, `src/HSchool.Server/Api/TimetableEndpoints.cs`,
|
||||
`src/HSchool.Server/Api/TimetableModels.cs`,
|
||||
`src/HSchool.Server/Game/SchoolWorker.Mailbox.cs` (пин),
|
||||
`src/HSchool.Server/Game/SchoolWorker.Persist.cs` (сейв и перестроение),
|
||||
`src/HSchool.Server/Game/SchoolWorker.Tick.cs` (`PeopleChanged` + `TimetableDirty`),
|
||||
`src/HSchool.Client/src/ui/timetableGrid.ts`, `src/HSchool.Client/src/ui/gameScreen.ts`,
|
||||
`tests/HSchool.Schedule.Tests`, `tests/HSchool.Content.Tests/CalendarTests.cs`,
|
||||
`tests/HSchool.Content.Tests/LunchBreakTests.cs`,
|
||||
`tests/HSchool.AppHost.Tests/TimetableApiTests.cs`,
|
||||
`src/HSchool.Client/src/ui/timetableGrid.test.ts`
|
||||
- **Итог:** перепроверка после дрейфа с `bdb1253`; обещания фаз 14–17 целы; дописаны тесты
|
||||
фильтров `GET` и `personTimetableQuery`
|
||||
|
||||
Что подтверждено:
|
||||
|
||||
- Календарь: суббота, каникулы и ночь — «вне учебного дня»; 10:20 вторника — третий урок;
|
||||
шестидневка; зима через 1 января. Физкультура в спортзале. Обеденные смены в каркасе дня
|
||||
(младшие после 3-го, старшие после 4-го).
|
||||
- Планировщик: все шесть тестов фазы 15 на месте; запреты Arch/ASP.NET/`DateTime.Now` живут
|
||||
в `LayerBoundaryTests` (срез 10), не в `Schedule.Tests`.
|
||||
- Перестроение только при `PeopleChanged` + `TimetableDirty`, не по тику — после раскола
|
||||
воркера это `SchoolWorker.Tick.cs`. `GET /timetable` читает снимок; пин требует владельца
|
||||
(срез 8) и идёт через `TaskCompletionSource`. `Reload_RestoresLockedLessons`.
|
||||
- Сетка, закрепление, тексты отказа, скобки в дереве из присутствия (ещё и с числом людей),
|
||||
личное расписание: учитель `?personId=`, ученик `?classId=`, родитель без сетки. Всё через `t(...)`.
|
||||
- Занятость узла в протоколе v7 живёт в кадре `Presence`, не в снимке карты — фаза 19.
|
||||
|
||||
Дописано:
|
||||
|
||||
- `TimetableApiTests.GetTimetable_ClassAndPersonFilters_NarrowTheLessons` — `?classId=` и
|
||||
`?personId=` сужают уроки; это обещание фазы 16 без отдельного теста.
|
||||
- `personTimetableQuery`: staff → `personId`, ученик → `classId`, родитель не staff → `null`.
|
||||
- Закреплённая ячейка несёт класс `timetable__cell--locked` и `title` из `t('timetableLocked')`.
|
||||
|
||||
Исправлено:
|
||||
|
||||
- Комментарий у `TimetableGrid` всё ещё писал, что занятость живёт в снимке карты.
|
||||
- `Saturday_HasNoOccupancyOnTheMap` не проверял занятость — переименован в
|
||||
`Saturday_StillStoresTheWeekTable` (пустой стол в выходной уже в `TimetableClockTests`).
|
||||
|
||||
Открыто:
|
||||
|
||||
- `GET /api/catalog` по-прежнему отдаёт каркас дня без `lunchBreaks` (только `longBreakAfter`).
|
||||
Сетке смены не нужны; молча расширять HTTP ревью не стало.
|
||||
|
||||
## Срез 4. Расписание
|
||||
|
||||
- **Фазы:** 14–17
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
| Срез 1. Оболочка и карта | `d087486` | [01-shell](01-shell/reviewed.md) |
|
||||
| Срез 2. Люди | `8108508` | [02-people](02-people/reviewed.md) |
|
||||
| Срез 3. Штат | `bdb1253` | [03-staffing](03-staffing/reviewed.md) |
|
||||
| Срез 4. Расписание | `bdb1253` | [04-schedule](04-schedule/reviewed.md) |
|
||||
| Срез 4. Расписание | `5a049d4` | [04-schedule](04-schedule/reviewed.md) |
|
||||
| Срез 5. Жизнь школы | `cbe739a` | [05-ai](05-ai/reviewed.md) |
|
||||
| Срез 6. Фундамент | `cbe739a` | [06-foundation](06-foundation/reviewed.md) |
|
||||
| Срез 7. Этап A — данные и гардероб | `d5547b7` | [07-inventory](07-inventory/reviewed.md) |
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -186,7 +186,30 @@ public class TimetableApiTests(AppHostFixture fixture)
|
||||
}
|
||||
|
||||
[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");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
@@ -206,10 +229,25 @@ public class TimetableApiTests(AppHostFixture fixture)
|
||||
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>(
|
||||
$"/api/schools/{schoolId}/timetable?lang=ru",
|
||||
$"/api/schools/{schoolId}/timetable?{query}",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(table);
|
||||
return table;
|
||||
|
||||
Reference in New Issue
Block a user