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]>
174 lines
6.5 KiB
PHP
174 lines
6.5 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\Booking\BookingPage;
|
|
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
|
use Unsupervised\Schedular\Guardian\FamilyPage;
|
|
use Unsupervised\Schedular\ShortcodeRegistrar;
|
|
|
|
class ShortcodeRegistrarTest 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 ShortcodeRegistrar $registrar;
|
|
|
|
/** @var array<string, callable> */
|
|
private array $shortcodes = [];
|
|
|
|
/** @var array<string, mixed> */
|
|
private array $localized = [];
|
|
|
|
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);
|
|
|
|
$this->registrar = new ShortcodeRegistrar(
|
|
$this->bookingPage,
|
|
$this->loginPage,
|
|
$this->registrationPage,
|
|
$this->groupClassPage,
|
|
$this->familyPage,
|
|
$this->accountPage,
|
|
);
|
|
|
|
$shortcodes = &$this->shortcodes;
|
|
Functions\when('add_shortcode')->alias(
|
|
static function (string $tag, callable $callback) use (&$shortcodes): void {
|
|
$shortcodes[$tag] = $callback;
|
|
}
|
|
);
|
|
}
|
|
|
|
public function testRegisterAddsAllShortcodesAndHooks(): void
|
|
{
|
|
Actions\expectAdded('template_redirect')
|
|
->once()
|
|
->with([$this->registrationPage, 'maybeRedirectToRegistrationPage']);
|
|
Actions\expectAdded('wp_enqueue_scripts')
|
|
->once()
|
|
->with([$this->registrar, 'enqueueAssets']);
|
|
|
|
$this->registrar->register();
|
|
|
|
self::assertSame(
|
|
['us_booking', 'us_student_login', 'us_student_register', 'us_group_classes', 'us_family', 'us_account'],
|
|
array_keys($this->shortcodes)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* WordPress passes an empty string (not an array) to a shortcode callback
|
|
* when the shortcode is used without attributes, e.g. `[us_booking]` —
|
|
* the wrapper must normalize it before the typed render methods.
|
|
*/
|
|
public function testBareShortcodeUsageIsNormalizedToAnEmptyAttributeArray(): void
|
|
{
|
|
$this->registrar->register();
|
|
|
|
$this->bookingPage->shouldReceive('render')->once()->with([])->andReturn('booking');
|
|
$this->loginPage->shouldReceive('render')->once()->with([])->andReturn('login');
|
|
$this->registrationPage->shouldReceive('render')->once()->with([])->andReturn('register');
|
|
$this->groupClassPage->shouldReceive('render')->once()->with([])->andReturn('group');
|
|
|
|
self::assertSame('booking', $this->shortcodes['us_booking'](''));
|
|
self::assertSame('login', $this->shortcodes['us_student_login'](''));
|
|
self::assertSame('register', $this->shortcodes['us_student_register'](''));
|
|
self::assertSame('group', $this->shortcodes['us_group_classes'](''));
|
|
}
|
|
|
|
/**
|
|
* The booking and group-class scripts both read prices through the shared
|
|
* pricing helper, so it must be registered ahead of them (and behind the
|
|
* payment helper, which carries the localized config it reads).
|
|
*/
|
|
public function testPricingHelperIsRegisteredAheadOfTheBookingAndGroupScripts(): void
|
|
{
|
|
$scripts = $this->captureEnqueuedAssets();
|
|
|
|
self::assertSame(['us-scheduler-payment'], $scripts['us-scheduler-pricing']);
|
|
self::assertSame(['us-scheduler-pricing', 'us-scheduler-guardian'], $scripts['us-scheduler']);
|
|
self::assertSame(['us-scheduler-pricing', 'us-scheduler-guardian'], $scripts['us-scheduler-group']);
|
|
}
|
|
|
|
/**
|
|
* The studio HST rate reaches the front end so a price quoted on a booking
|
|
* form matches the total the student is actually billed.
|
|
*/
|
|
public function testStudioTaxRateIsLocalizedToTheFrontEnd(): void
|
|
{
|
|
$this->captureEnqueuedAssets();
|
|
|
|
self::assertSame(13.0, $this->localized['taxRate']);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array<int, string>> Registered script handle => dependencies.
|
|
*/
|
|
private function captureEnqueuedAssets(): array
|
|
{
|
|
$scripts = [];
|
|
$localized = &$this->localized;
|
|
|
|
Functions\when('wp_register_style')->justReturn(true);
|
|
Functions\when('rest_url')->justReturn('https://example.test/wp-json/us-scheduler/v1/');
|
|
Functions\when('wp_create_nonce')->justReturn('nonce');
|
|
Functions\when('get_option')->alias(
|
|
static fn (string $name, mixed $default = false): mixed => match ($name) {
|
|
'us_hst_rate' => '13',
|
|
'start_of_week' => 1,
|
|
default => $default,
|
|
}
|
|
);
|
|
Functions\when('wp_register_script')->alias(
|
|
static function (string $handle, string $src, array $deps = []) use (&$scripts): bool {
|
|
$scripts[$handle] = $deps;
|
|
|
|
return true;
|
|
}
|
|
);
|
|
Functions\when('wp_localize_script')->alias(
|
|
static function (string $handle, string $object, array $data) use (&$localized): bool {
|
|
$localized = $data;
|
|
|
|
return true;
|
|
}
|
|
);
|
|
|
|
$this->registrar->enqueueAssets();
|
|
|
|
return $scripts;
|
|
}
|
|
|
|
public function testShortcodeAttributesArePassedThroughUnchanged(): void
|
|
{
|
|
$this->registrar->register();
|
|
|
|
$this->bookingPage->shouldReceive('render')
|
|
->once()->with(['login_page_id' => '5'])->andReturn('booking');
|
|
$this->loginPage->shouldReceive('render')
|
|
->once()->with(['booking_page_id' => '9'])->andReturn('login');
|
|
|
|
self::assertSame('booking', $this->shortcodes['us_booking'](['login_page_id' => '5']));
|
|
self::assertSame('login', $this->shortcodes['us_student_login'](['booking_page_id' => '9']));
|
|
}
|
|
}
|