CI / Tests (PHP 8.2) (pull_request) Successful in 42s
CI / Tests (PHP 8.1) (pull_request) Successful in 52s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m54s
CI / PHPStan (pull_request) Successful in 2m55s
[us_account], or the Account block: the signed-in visitor's name, their email, a Sign out link, and — only when the account books for someone besides itself — the students it books for. A parent's first question on seeing "signed in as Grace" is whether this is the account their children's lessons are on. Two decisions worth naming. Signed out with no login page chosen, the block renders nothing. Its whole subject is the person signed in, which a stranger is not, and a bare "you are not signed in" in a site header is noise with no way to act on it. With a login page chosen it offers a Sign in link instead. The editor preview is populated regardless, so the block is never an invisible box to the person placing it. Signing out returns to the chosen login page, or to the current page when there is none. A block meant for a header should not also navigate someone somewhere when they use it; the login page wins when configured, because the page they were on may well be members-only. The name comes from UserName::format(), so the block never exposes a username the way display_name can. Also brings docs/features/editor-blocks.md back in step: it still described "four shortcodes" and had never listed the family block. Closes #142 Co-Authored-By: Claude Opus 5 <[email protected]>
567 lines
20 KiB
PHP
567 lines
20 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\AccountPage;
|
|
use Unsupervised\Schedular\Auth\LoginPage;
|
|
use Unsupervised\Schedular\Auth\RegistrationPage;
|
|
use Unsupervised\Schedular\BlockRegistrar;
|
|
use Unsupervised\Schedular\Booking\BookingPage;
|
|
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
|
use Unsupervised\Schedular\Guardian\FamilyPage;
|
|
|
|
/**
|
|
* 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 FamilyPage&Mockery\MockInterface $familyPage;
|
|
private AccountPage&Mockery\MockInterface $accountPage;
|
|
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->familyPage = Mockery::mock(FamilyPage::class);
|
|
$this->accountPage = Mockery::mock(AccountPage::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,
|
|
$this->familyPage,
|
|
$this->accountPage,
|
|
);
|
|
}
|
|
|
|
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 testRegisterBlocksRegistersAllBlocksWithAssets(): 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',
|
|
'us-scheduler/family',
|
|
'us-scheduler/account',
|
|
],
|
|
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->familyPage,
|
|
$this->accountPage,
|
|
);
|
|
|
|
$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);
|
|
}
|
|
}
|