Files
unsupervised-scheduler/tests/Unit/BlockRegistrarTest.php
T
thatguygriffandClaude Opus 5 264d9cba01
CI / Tests (PHP 8.1) (pull_request) Successful in 47s
CI / Tests (PHP 8.2) (pull_request) Successful in 54s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m51s
CI / Coding Standards (pull_request) Successful in 2m56s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped
Add per-embed lesson-type and section options to the booking block
Three sidebar options on the Lesson Booking block, all mirrored as shortcode
attributes and carried to the front end as data attributes on
#us-booking-app (or as omitted containers):

- Lesson type (lessonTypeId / lesson_type) pins the calendar to a single
  private-lesson type: only the times bookable as it are listed, and it is
  the only type bookable there, auto-selected on the registration form. A
  pinned type that is no longer offered says so instead of showing an empty
  calendar.
- Show the lesson-type filter (showTypeFilter / show_filter) drops the
  "Show Only" control for studios that do not want it.
- Sections (displayMode / show) embeds one half of the page — the booking
  calendar or the student's upcoming lessons — so the two can live on
  different pages. The script skips the work belonging to a missing half:
  no availability or catalog request for an upcoming-only embed, no
  bookings request for a booking-only one. An unrecognised value renders
  the whole page. The editor preview follows the same setting.

Also fixes the expanded filter's first lesson type sharing a line with the
"Lesson type" heading — the choices now sit in their own row beneath it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 13:16:18 -03:00

555 lines
19 KiB
PHP

<?php
declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit;
use Brain\Monkey\Actions;
use Brain\Monkey\Functions;
use Mockery;
use Unsupervised\Schedular\Auth\LoginPage;
use Unsupervised\Schedular\Auth\RegistrationPage;
use Unsupervised\Schedular\BlockRegistrar;
use Unsupervised\Schedular\Booking\BookingPage;
use Unsupervised\Schedular\GroupClass\GroupClassPage;
/**
* Test double exposing editor-preview mode as a switch (the real detection
* relies on the REST_REQUEST constant, which cannot be toggled within a
* single PHP process) and recording redirects instead of exiting.
*/
class TestableBlockRegistrar extends BlockRegistrar
{
public bool $preview = false;
/** @var list<string> */
public array $redirects = [];
protected function isEditorPreview(): bool
{
return $this->preview;
}
protected function redirect(string $url): void
{
$this->redirects[] = $url;
}
}
class BlockRegistrarTest extends TestCase
{
private BookingPage&Mockery\MockInterface $bookingPage;
private LoginPage&Mockery\MockInterface $loginPage;
private RegistrationPage&Mockery\MockInterface $registrationPage;
private GroupClassPage&Mockery\MockInterface $groupClassPage;
private TestableBlockRegistrar $registrar;
protected function setUp(): void
{
parent::setUp();
$this->bookingPage = Mockery::mock(BookingPage::class);
$this->loginPage = Mockery::mock(LoginPage::class);
$this->registrationPage = Mockery::mock(RegistrationPage::class);
$this->groupClassPage = Mockery::mock(GroupClassPage::class);
// Most requests are not a just-finished registration; the tests that
// exercise that path override this.
$this->registrationPage->shouldReceive('isRegistrationComplete')
->andReturn(false)
->byDefault();
$this->registrar = new TestableBlockRegistrar(
$this->bookingPage,
$this->loginPage,
$this->registrationPage,
$this->groupClassPage,
);
}
public function testRegisterHooksBlockRegistrationAndAutoRedirect(): void
{
Actions\expectAdded('init')->once()->with([$this->registrar, 'registerBlocks']);
Actions\expectAdded('template_redirect')->once()->with([$this->registrar, 'maybeAutoRedirect']);
$this->registrar->register();
}
public function testRegisterBlocksRegistersAllFourBlocksWithAssets(): void
{
Functions\expect('wp_register_script')
->once()
->with(
BlockRegistrar::SCRIPT_HANDLE,
Mockery::pattern('~assets/js/blocks\.js$~'),
Mockery::type('array'),
USC_VERSION,
true
);
Functions\when('wp_style_is')->justReturn(false);
Functions\expect('wp_register_style')
->once()
->with(
BlockRegistrar::STYLE_HANDLE,
Mockery::pattern('~assets/css/frontend\.css$~'),
[],
USC_VERSION
);
$registered = [];
Functions\when('register_block_type')->alias(
static function (string $name, array $args) use (&$registered): bool {
$registered[$name] = $args;
return true;
}
);
$this->registrar->registerBlocks();
self::assertSame(
[
'us-scheduler/booking',
'us-scheduler/student-login',
'us-scheduler/student-register',
'us-scheduler/group-classes',
],
array_keys($registered)
);
foreach ($registered as $args) {
self::assertSame(BlockRegistrar::SCRIPT_HANDLE, $args['editor_script']);
self::assertSame(BlockRegistrar::STYLE_HANDLE, $args['style']);
self::assertIsCallable($args['render_callback']);
}
// The link-target and auto-redirect options must be declared
// server-side or the block-renderer preview rejects them.
self::assertSame(
['loginPageId', 'autoRedirect', 'lessonTypeId', 'showTypeFilter', 'displayMode'],
array_keys($registered['us-scheduler/booking']['attributes'])
);
self::assertSame(
['bookingPageId', 'autoRedirect'],
array_keys($registered['us-scheduler/student-login']['attributes'])
);
self::assertSame(
['loginPageId', 'autoRedirect', 'inviteOnlyMessage'],
array_keys($registered['us-scheduler/student-register']['attributes'])
);
self::assertSame(
['offeringId'],
array_keys($registered['us-scheduler/group-classes']['attributes'])
);
}
public function testRegisterBlocksDoesNotReRegisterAnAlreadyRegisteredStyle(): void
{
Functions\when('wp_register_script')->justReturn(true);
Functions\when('wp_style_is')->justReturn(true);
Functions\expect('wp_register_style')->never();
Functions\when('register_block_type')->justReturn(true);
$this->registrar->registerBlocks();
}
public function testFrontEndRenderDelegatesToThePageObjectsPassingAttributes(): void
{
$this->registrar->preview = false;
$this->bookingPage->shouldReceive('render')
->once()->with(['loginPageId' => 5])->andReturn('booking-html');
$this->loginPage->shouldReceive('render')
->once()->with(['bookingPageId' => 9])->andReturn('login-html');
$this->registrationPage->shouldReceive('render')->once()->with([])->andReturn('register-html');
$this->groupClassPage->shouldReceive('render')->once()->with([])->andReturn('group-html');
self::assertSame('booking-html', $this->registrar->renderBooking(['loginPageId' => 5]));
self::assertSame('login-html', $this->registrar->renderLogin(['bookingPageId' => 9]));
self::assertSame('register-html', $this->registrar->renderRegistration());
self::assertSame('group-html', $this->registrar->renderGroupClasses());
}
public function testEditorPreviewRendersStaticMarkupWithoutTouchingThePages(): void
{
$this->registrar->preview = true;
Functions\when('wp_nonce_field')->justReturn('');
$this->bookingPage->shouldNotReceive('render');
$this->loginPage->shouldNotReceive('render');
$this->registrationPage->shouldNotReceive('render');
$this->groupClassPage->shouldNotReceive('render');
self::assertStringContainsString('us-booking-app', $this->registrar->renderBooking());
self::assertStringContainsString('us-login-form', $this->registrar->renderLogin());
self::assertStringContainsString('us-register-form', $this->registrar->renderRegistration());
self::assertStringContainsString('us-group-app', $this->registrar->renderGroupClasses());
}
public function testEditorPreviewOfAPinnedGroupClassOmitsTheDescription(): void
{
$this->registrar->preview = true;
$this->groupClassPage->shouldNotReceive('render');
$all = $this->registrar->renderGroupClasses();
$single = $this->registrar->renderGroupClasses(['offeringId' => 12]);
self::assertStringContainsString('A sample class shown so the page can be styled.', $all);
self::assertStringNotContainsString('A sample class shown so the page can be styled.', $single);
self::assertStringContainsString('us-enrol-deadline', $single);
}
public function testIsEditorPreviewIsFalseOutsideRestRequests(): void
{
// REST_REQUEST is undefined in the test process, so the real
// registrar must take the front-end path.
$registrar = new BlockRegistrar(
$this->bookingPage,
$this->loginPage,
$this->registrationPage,
$this->groupClassPage,
);
$this->bookingPage->shouldReceive('render')->once()->with([])->andReturn('live');
self::assertSame('live', $registrar->renderBooking());
}
/**
* Stubs the front-end request context for maybeAutoRedirect: a singular
* page whose content parses to the given blocks.
*
* @param list<array<string, mixed>> $parsedBlocks
*/
private function stubSingularRequest(int $postId, array $parsedBlocks, bool $loggedIn): void
{
$post = new \WP_Post();
$post->ID = $postId;
$post->post_content = 'serialized-block-content';
Functions\when('is_admin')->justReturn(false);
Functions\when('is_singular')->justReturn(true);
Functions\when('get_post')->justReturn($post);
Functions\when('is_user_logged_in')->justReturn($loggedIn);
Functions\when('has_block')->alias(
static function (string $blockName) use ($parsedBlocks): bool {
foreach ($parsedBlocks as $block) {
if (($block['blockName'] ?? null) === $blockName) {
return true;
}
foreach ((array) ($block['innerBlocks'] ?? []) as $inner) {
if (is_array($inner) && ($inner['blockName'] ?? null) === $blockName) {
return true;
}
}
}
return false;
}
);
Functions\when('parse_blocks')->justReturn($parsedBlocks);
}
public function testAutoRedirectSendsLoggedOutVisitorToTheLoginPage(): void
{
$this->stubSingularRequest(
10,
[
[
'blockName' => 'us-scheduler/booking',
'attrs' => ['autoRedirect' => true, 'loginPageId' => 7],
'innerBlocks' => [],
],
],
false
);
$this->bookingPage->shouldReceive('loginUrl')->once()->with(7)->andReturn('https://example.com/login/');
$this->registrar->maybeAutoRedirect();
self::assertSame(['https://example.com/login/'], $this->registrar->redirects);
}
public function testAutoRedirectFindsTheBookingBlockNestedInsideAnotherBlock(): void
{
$this->stubSingularRequest(
10,
[
[
'blockName' => 'core/group',
'attrs' => [],
'innerBlocks' => [
[
'blockName' => 'us-scheduler/booking',
'attrs' => ['autoRedirect' => true],
'innerBlocks' => [],
],
],
],
],
false
);
// loginPageId omitted from the serialized block (defaults are not
// stored) — falls back to the WordPress login screen.
$this->bookingPage->shouldReceive('loginUrl')->once()->with(0)->andReturn('https://example.com/wp-login.php');
$this->registrar->maybeAutoRedirect();
self::assertSame(['https://example.com/wp-login.php'], $this->registrar->redirects);
}
public function testNoRedirectWhenTheBookingBlockDoesNotOptIn(): void
{
$this->stubSingularRequest(
10,
[
[
'blockName' => 'us-scheduler/booking',
'attrs' => ['loginPageId' => 7],
'innerBlocks' => [],
],
],
false
);
$this->bookingPage->shouldNotReceive('loginUrl');
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
public function testNoRedirectWhenTheBookingBlockPointsAtItsOwnPage(): void
{
$this->stubSingularRequest(
7,
[
[
'blockName' => 'us-scheduler/booking',
'attrs' => ['autoRedirect' => true, 'loginPageId' => 7],
'innerBlocks' => [],
],
],
false
);
$this->bookingPage->shouldNotReceive('loginUrl');
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
public function testAutoRedirectSendsLoggedInVisitorToTheBookingPage(): void
{
$this->stubSingularRequest(
20,
[
[
'blockName' => 'us-scheduler/student-login',
'attrs' => ['autoRedirect' => true, 'bookingPageId' => 9],
'innerBlocks' => [],
],
],
true
);
$this->loginPage->shouldReceive('bookingUrl')->once()->with(9)->andReturn('https://example.com/book/');
$this->registrar->maybeAutoRedirect();
self::assertSame(['https://example.com/book/'], $this->registrar->redirects);
}
public function testNoRedirectWhenTheLoginBlockHasNoBookingPageChosen(): void
{
$this->stubSingularRequest(
20,
[
[
'blockName' => 'us-scheduler/student-login',
'attrs' => ['autoRedirect' => true],
'innerBlocks' => [],
],
],
true
);
$this->loginPage->shouldReceive('bookingUrl')->once()->with(0)->andReturnNull();
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
public function testNoRedirectForLoggedInVisitorOnTheBookingPage(): void
{
$this->stubSingularRequest(
10,
[
[
'blockName' => 'us-scheduler/booking',
'attrs' => ['autoRedirect' => true, 'loginPageId' => 7],
'innerBlocks' => [],
],
],
true
);
$this->bookingPage->shouldNotReceive('loginUrl');
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
public function testAutoRedirectSendsAFinishedRegistrationToTheChosenPage(): void
{
$this->stubSingularRequest(
30,
[
[
'blockName' => 'us-scheduler/student-register',
'attrs' => ['autoRedirect' => true, 'loginPageId' => 4],
'innerBlocks' => [],
],
],
false
);
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
$this->registrationPage->shouldReceive('continueUrl')
->once()->with(4)->andReturn('https://example.com/welcome/');
$this->registrar->maybeAutoRedirect();
self::assertSame(['https://example.com/welcome/'], $this->registrar->redirects);
}
public function testAutoRedirectSendsAJustLoggedInInvitedStudentToTheChosenPage(): void
{
// The invited-student branch completes logged in, so the logged-in
// student-login branch must not get first claim on the request.
$this->stubSingularRequest(
30,
[
[
'blockName' => 'us-scheduler/student-register',
'attrs' => ['autoRedirect' => true, 'loginPageId' => 4],
'innerBlocks' => [],
],
],
true
);
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
$this->registrationPage->shouldReceive('continueUrl')
->once()->with(4)->andReturn('https://example.com/welcome/');
$this->registrar->maybeAutoRedirect();
self::assertSame(['https://example.com/welcome/'], $this->registrar->redirects);
}
public function testNoRedirectWhenTheRegistrationIsNotFinished(): void
{
// e.g. the "check your email" step, or a validation error — the
// message has to be read, so the block never redirects past it.
$this->stubSingularRequest(
30,
[
[
'blockName' => 'us-scheduler/student-register',
'attrs' => ['autoRedirect' => true, 'loginPageId' => 4],
'innerBlocks' => [],
],
],
false
);
$this->registrationPage->shouldReceive('continueUrl')->never();
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
public function testNoRedirectWhenTheRegisterBlockDoesNotOptIn(): void
{
$this->stubSingularRequest(
30,
[
[
'blockName' => 'us-scheduler/student-register',
'attrs' => ['loginPageId' => 4],
'innerBlocks' => [],
],
],
false
);
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
$this->registrationPage->shouldReceive('continueUrl')->never();
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
public function testNoRedirectWhenTheRegisterBlockHasNoPageChosen(): void
{
$this->stubSingularRequest(
30,
[
[
'blockName' => 'us-scheduler/student-register',
'attrs' => ['autoRedirect' => true],
'innerBlocks' => [],
],
],
false
);
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
// No page chosen: there is no login-screen fallback to redirect to,
// so the student keeps the on-page confirmation instead.
$this->registrationPage->shouldReceive('continueUrl')->once()->with(0)->andReturnNull();
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
public function testNoRedirectWhenTheRegisterBlockPointsAtItsOwnPage(): void
{
$this->stubSingularRequest(
4,
[
[
'blockName' => 'us-scheduler/student-register',
'attrs' => ['autoRedirect' => true, 'loginPageId' => 4],
'innerBlocks' => [],
],
],
false
);
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
$this->registrationPage->shouldReceive('continueUrl')->never();
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
public function testNoRedirectOutsideSingularFrontEndRequests(): void
{
Functions\when('is_admin')->justReturn(false);
Functions\when('is_singular')->justReturn(false);
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
}