Files
unsupervised-scheduler/tests/Unit/GroupClass/SessionScheduleTest.php
thatguygriffandClaude Opus 5 c9a1205fc0
CI / Tests (PHP 8.2) (pull_request) Successful in 58s
CI / PHPStan (pull_request) Successful in 2m53s
CI / Coding Standards (pull_request) Successful in 2m58s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m47s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 1m1s
CI / No Debug Code (pull_request) Successful in 3s
Never drop an enrolled class from upcoming lessons; delete a guardian's children with them; pin the panel's line spacing
Three fixes from testing the branch.

A group class was only listed when its schedule resolved to exact
datetimes, which needs a class time *and* a duration — both optional on
the offering form, and the schedule note exists precisely so a studio can
write "Tuesdays 4:00pm" instead. A class configured that way vanished
from the list, which is the one thing this feature must never do. So
Offering::sessionStarts() splits "when does it meet" from "how long does
it run" (sessionWindows() is that plus the duration, unchanged), and
SessionSchedule degrades instead of disappearing: dated rows with an open
end when there is no duration, and a single row carrying
Offering::scheduleLabel() when there is no time to derive dates from.
Only a class whose last day has passed drops out.

Deleting a guardian now deletes the children linked to them, releasing
each one's lessons and enrolments first. A child account is login-less
and exists only so the guardian has somebody to book for; without the
guardian nobody can reach it, book for it, or be billed for it, so it was
left stranded on the roster still holding slots. A `handled` set makes
the re-entrant delete_user each child deletion fires a no-op, and stops a
circular link recursing.

The upcoming panel never stated its own line-height, so a theme setting
line-height: 0 above it — the usual icon-font reset — was inherited
straight through. Below 1 that produces both reported symptoms at once:
stacked lines overlap, and the status pill's background is shorter than
the text in it. Pinned at the same id-level specificity as the rest.

Tests: composer test (863), composer lint, composer cs all pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-30 12:10:43 -03:00

306 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit\GroupClass;
use Brain\Monkey\Functions;
use Mockery;
use Unsupervised\Schedular\GroupClass\Enrollment;
use Unsupervised\Schedular\GroupClass\EnrollmentRepository;
use Unsupervised\Schedular\GroupClass\SessionSchedule;
use Unsupervised\Schedular\Offering\Offering;
use Unsupervised\Schedular\Offering\OfferingRepository;
use Unsupervised\Schedular\Tests\Unit\TestCase;
class SessionScheduleTest extends TestCase
{
private EnrollmentRepository&Mockery\MockInterface $enrollments;
private OfferingRepository&Mockery\MockInterface $offerings;
private SessionSchedule $schedule;
protected function setUp(): void
{
parent::setUp();
$this->enrollments = Mockery::mock(EnrollmentRepository::class);
$this->offerings = Mockery::mock(OfferingRepository::class);
$this->schedule = new SessionSchedule($this->enrollments, $this->offerings);
}
/** A three-week Tuesday class at 16:00, one hour long. */
private function choir(int $id = 8, string $title = 'Choir'): Offering
{
return new Offering(
instructorId: 3,
kind: Offering::KIND_GROUP_CLASS,
title: $title,
durationMinutes: 60,
termStart: '2026-09-08',
termEnd: '2026-09-22',
classTime: '16:00:00',
id: $id,
);
}
public function testAStudentsEnrolmentBecomesOneRowPerRemainingSession(): void
{
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, id: 40),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn($this->choir());
$rows = $this->schedule->upcomingForStudent(5, '2026-09-01 00:00:00');
self::assertCount(3, $rows);
self::assertSame(
['2026-09-08 16:00:00', '2026-09-15 16:00:00', '2026-09-22 16:00:00'],
array_column($rows, 'start_dt')
);
self::assertSame('2026-09-08 17:00:00', $rows[0]['end_dt']);
self::assertNull($rows[0]['schedule']);
self::assertSame('Choir', $rows[0]['offering_title']);
self::assertSame(40, $rows[0]['enrollment_id']);
self::assertSame(3, $rows[0]['instructor_id']);
self::assertSame(60, $rows[0]['duration_minutes']);
}
public function testSessionsThatHaveAlreadyStartedAreLeftOut(): void
{
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, id: 40),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn($this->choir());
// Mid-term: the first two dates are gone, the last is still to come.
$rows = $this->schedule->upcomingForStudent(5, '2026-09-16 09:00:00');
self::assertSame(['2026-09-22 16:00:00'], array_column($rows, 'start_dt'));
}
public function testAWithdrawnEnrolmentContributesNothing(): void
{
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(
offeringId: 8,
studentId: 5,
instructorId: 3,
status: Enrollment::STATUS_CANCELLED,
id: 40,
),
]);
$this->offerings->shouldNotReceive('findById');
self::assertSame([], $this->schedule->upcomingForStudent(5, '2026-09-01 00:00:00'));
}
/**
* "Completed" is a billing state, not a calendar one — the class may still
* have dates left to run, so its sessions stay on the list.
*/
public function testACompletedEnrolmentStillListsItsRemainingSessions(): void
{
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(
offeringId: 8,
studentId: 5,
instructorId: 3,
status: Enrollment::STATUS_COMPLETED,
id: 40,
),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn($this->choir());
$rows = $this->schedule->upcomingForStudent(5, '2026-09-01 00:00:00');
self::assertCount(3, $rows);
self::assertSame(Enrollment::STATUS_COMPLETED, $rows[0]['status']);
}
/**
* Both the class time and the duration are optional on the offering form, and
* the schedule note exists so a studio can say "Tuesdays 4:00pm" instead of
* pinning the class to a clock. A class configured that way used to vanish
* from the list entirely — the bug this covers. It now gets one row carrying
* the note in place of a date.
*/
public function testAClassWithNoClassTimeStillGetsARowDescribedInWords(): void
{
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, id: 40),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn(new Offering(
instructorId: 3,
kind: Offering::KIND_GROUP_CLASS,
title: 'Choir',
durationMinutes: 60,
termStart: '2026-09-08',
termEnd: '2026-12-08',
scheduleNote: 'Tuesdays 4:00pm',
id: 8,
));
$rows = $this->schedule->upcomingForStudent(5, '2026-09-01 00:00:00');
self::assertCount(1, $rows);
self::assertSame('Tuesdays 4:00pm', $rows[0]['schedule']);
self::assertSame('Choir', $rows[0]['offering_title']);
// A sort key, not a claim about the time: the class has not started yet,
// so it sorts to its first day.
self::assertSame('2026-09-08 00:00:00', $rows[0]['start_dt']);
self::assertSame('', $rows[0]['end_dt']);
}
/** Without a note, the term dates do the describing. */
public function testAnUndatedClassFallsBackToItsTermDates(): void
{
Functions\when('mysql2date')->alias(
static fn (string $format, string $date): string => date($format, (int) strtotime($date))
);
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, id: 40),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn(new Offering(
instructorId: 3,
kind: Offering::KIND_GROUP_CLASS,
title: 'Choir',
termStart: '2026-09-08',
termEnd: '2026-12-08',
id: 8,
));
$rows = $this->schedule->upcomingForStudent(5, '2026-09-01 00:00:00');
self::assertSame('Sep 8, 2026 Dec 8, 2026', $rows[0]['schedule']);
}
/**
* A term already under way sorts to "now" rather than to a start date in the
* past, so an ongoing class reads as current instead of dropping to the
* bottom of a list ordered by time.
*/
public function testAnUndatedClassAlreadyUnderWaySortsToNow(): void
{
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, id: 40),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn(new Offering(
instructorId: 3,
kind: Offering::KIND_GROUP_CLASS,
title: 'Choir',
termStart: '2026-09-08',
termEnd: '2026-12-08',
scheduleNote: 'Tuesdays 4:00pm',
id: 8,
));
$rows = $this->schedule->upcomingForStudent(5, '2026-10-01 09:00:00');
self::assertSame('2026-10-01 09:00:00', $rows[0]['start_dt']);
}
/** An undated class whose last day has passed is over, and drops out. */
public function testAnUndatedClassThatHasFinishedIsDropped(): void
{
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, id: 40),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn(new Offering(
instructorId: 3,
kind: Offering::KIND_GROUP_CLASS,
title: 'Choir',
termStart: '2026-09-08',
termEnd: '2026-12-08',
scheduleNote: 'Tuesdays 4:00pm',
id: 8,
));
self::assertSame([], $this->schedule->upcomingForStudent(5, '2026-12-09 00:00:00'));
}
/**
* A class time but no duration: the dates are still known, so they are still
* listed — the row just says when it starts and not when it ends, rather than
* inventing a finish.
*/
public function testAClassWithNoDurationKeepsItsDatesAndLeavesTheEndOpen(): void
{
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, id: 40),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn(new Offering(
instructorId: 3,
kind: Offering::KIND_GROUP_CLASS,
title: 'Choir',
termStart: '2026-09-08',
termEnd: '2026-09-15',
classTime: '16:00:00',
id: 8,
));
$rows = $this->schedule->upcomingForStudent(5, '2026-09-01 00:00:00');
self::assertSame(['2026-09-08 16:00:00', '2026-09-15 16:00:00'], array_column($rows, 'start_dt'));
self::assertSame(['', ''], array_column($rows, 'end_dt'));
self::assertNull($rows[0]['schedule']);
}
public function testADeletedOfferingIsSkippedRatherThanFatal(): void
{
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, id: 40),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn(null);
self::assertSame([], $this->schedule->upcomingForStudent(5, '2026-09-01 00:00:00'));
}
public function testTwoEnrolmentsAreInterleavedByDate(): void
{
$band = new Offering(
instructorId: 3,
kind: Offering::KIND_GROUP_CLASS,
title: 'Band',
durationMinutes: 45,
termStart: '2026-09-10',
termEnd: '2026-09-10',
classTime: '09:00:00',
id: 9,
);
$this->enrollments->shouldReceive('findByStudent')->with(5)->andReturn([
new Enrollment(offeringId: 8, studentId: 5, instructorId: 3, id: 40),
new Enrollment(offeringId: 9, studentId: 5, instructorId: 3, id: 41),
]);
$this->offerings->shouldReceive('findById')->with(8)->andReturn($this->choir());
$this->offerings->shouldReceive('findById')->with(9)->andReturn($band);
$rows = $this->schedule->upcomingForStudent(5, '2026-09-01 00:00:00');
self::assertSame(
['Choir', 'Band', 'Choir', 'Choir'],
array_column($rows, 'offering_title')
);
}
/**
* An instructor's list is built from the classes they teach, not from who
* has signed up: a class with no enrolments yet is still on their schedule.
*/
public function testAnInstructorSeesEachSessionOfTheirActiveClassesOnce(): void
{
$this->offerings->shouldReceive('findAll')
->once()
->with(3, Offering::KIND_GROUP_CLASS, true)
->andReturn([$this->choir()]);
$this->enrollments->shouldNotReceive('findByStudent');
$rows = $this->schedule->upcomingForInstructor(3, '2026-09-01 00:00:00');
self::assertCount(3, $rows);
self::assertSame(0, $rows[0]['enrollment_id']);
self::assertSame(3, $rows[0]['instructor_id']);
self::assertSame('Choir', $rows[0]['offering_title']);
}
}