*/ 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([], $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> $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); } }