Files
unsupervised-scheduler/tests/Unit/BlockRegistrarTest.php
T
thatguygriffandClaude Fable 5 cde8704267
CI / Tests (PHP 8.2) (pull_request) Successful in 45s
CI / Tests (PHP 8.1) (pull_request) Successful in 48s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 1m14s
CI / PHPStan (pull_request) Successful in 1m16s
CI / Tests (PHP 8.3) (pull_request) Successful in 37s
CI / Build Plugin Zip (pull_request) Has been skipped
Group class term dates, single-class embed mode, and offering editing
Group class offerings now carry real dates: the add/edit form takes a
start date plus a sessions control (one-off, or weekly for N sessions;
the end date is computed as start + (N-1) weeks via
Offering::weeklyTermEnd). Dates are validated strictly (Y-m-d) and shown
in the offerings list and on the student-facing class card, including
the weekly session count.

[us_group_classes offering="<id>"] (block attribute offeringId, chosen
from a dropdown of active classes fetched from the public offerings
endpoint) restricts the page to a single class so the enrolment flow can
be embedded on a page dedicated to that class; a pinned class that is no
longer offered reports itself closed instead of falling back to the
catalog.

Offerings are now editable from the admin screen: an Edit button
prefills the shared add/edit form and saving posts usc_action=update.
Updates always preserve the original owner and currency, and non-admin
instructors can only load and update their own offerings. The form also
gains the previously missing description field and an Active toggle (the
admin-UI counterpart of the REST is_active flag) so an edit cannot wipe
data the form never collected.

Closes #59

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-05 23:18:59 -03:00

394 lines
13 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);
$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'],
array_keys($registered['us-scheduler/booking']['attributes'])
);
self::assertSame(
['bookingPageId', 'autoRedirect'],
array_keys($registered['us-scheduler/student-login']['attributes'])
);
self::assertSame([], $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 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 testNoRedirectOutsideSingularFrontEndRequests(): void
{
Functions\when('is_admin')->justReturn(false);
Functions\when('is_singular')->justReturn(false);
$this->registrar->maybeAutoRedirect();
self::assertSame([], $this->registrar->redirects);
}
}