Ask who the signup is for as a three-way choice
CI / Tests (PHP 8.2) (pull_request) Successful in 50s
CI / PHPStan (pull_request) Successful in 2m53s
CI / Coding Standards (pull_request) Successful in 2m57s
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m40s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.2) (pull_request) Successful in 50s
CI / PHPStan (pull_request) Successful in 2m53s
CI / Coding Standards (pull_request) Successful in 2m57s
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m40s
CI / Build Plugin Zip (pull_request) Skipped
Replaces the single "I'm registering as a parent or guardian" tick with "Just myself" / "On behalf of one or more students" / "Both". Radios, not checkboxes as the feedback put it: the three answers are mutually exclusive, and "both" only means anything as a third choice alongside the other two. The tick could only ever say whether there were children to add. It could not say whether the account holder was a student, so bookableStudents() always offered them their own name and any guardian could book themselves a lesson nobody meant to sell. "On behalf of" now records us_guardian_only and leaves them out of the picker. That flag is stored as the negative on purpose. Every account predating this choice is a bookable student, and absence has to keep meaning exactly that, or the picker would quietly stop offering people themselves on upgrade. setGuardianOnly() clears the key rather than writing 0, so "not set" stays the single spelling of "yes, a student". A guardian-only account with nobody linked to it is still offered itself — an empty picker is no way to book at all, and they can put the account right from the profile page. An unrecognised or absent value reads as "just myself": the choice that collects the least and grants the least. A missing radio must never be taken as "register these children". Bumps to 1.4.0. The account holder's own questions stay out of play whenever students are being added, "both" included — asking them there is #146. Verified the form in a headless browser across all three choices: which blocks show, which fields carry `required`, whether the account holder's question panel is disabled, which submit is offered, and that switching back to "just myself" leaves no hidden required field blocking submit. Closes #145 Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -35,6 +35,8 @@ class RegistrationPageTest extends TestCase
|
||||
|
||||
Functions\when('wp_unslash')->alias(static fn ($v) => $v);
|
||||
Functions\when('sanitize_text_field')->alias(static fn ($v) => $v);
|
||||
// Every submit reads the "who are you registering?" radio through it.
|
||||
Functions\when('sanitize_key')->alias(static fn ($v) => strtolower((string) $v));
|
||||
Functions\when('sanitize_textarea_field')->alias(static fn ($v) => $v);
|
||||
Functions\when('sanitize_email')->alias(static fn ($v) => $v);
|
||||
// Reached on every submit now that the email is validated before the
|
||||
@@ -74,6 +76,8 @@ class RegistrationPageTest extends TestCase
|
||||
$this->ctx['versions'] = Mockery::mock(PolicyVersionRepository::class);
|
||||
$this->ctx['acceptances'] = Mockery::mock(AcceptanceRepository::class);
|
||||
$this->ctx['guardians'] = Mockery::mock(GuardianService::class);
|
||||
// Recorded on every successful signup; the tests that care assert on it.
|
||||
$this->ctx['guardians']->shouldReceive('setGuardianOnly')->byDefault();
|
||||
|
||||
$this->ctx['page'] = new RegistrationPage(
|
||||
$invites,
|
||||
@@ -109,6 +113,17 @@ class RegistrationPageTest extends TestCase
|
||||
$this->ctx['settings']->shouldReceive('openRegistrationEnabled')->andReturn(true);
|
||||
}
|
||||
|
||||
/** Everything the invite success branch touches once the account is created. */
|
||||
private function stubInviteSuccess(): void
|
||||
{
|
||||
Functions\when('email_exists')->justReturn(false);
|
||||
Functions\when('wp_insert_user')->justReturn(42);
|
||||
Functions\when('is_wp_error')->alias(static fn ($thing): bool => $thing instanceof \WP_Error);
|
||||
Functions\when('wp_set_current_user')->justReturn(null);
|
||||
Functions\when('wp_set_auth_cookie')->justReturn(null);
|
||||
$this->ctx['invites']->shouldReceive('markAccepted')->once();
|
||||
}
|
||||
|
||||
private function submit(?Invite $invite, bool $open): string
|
||||
{
|
||||
$method = new \ReflectionMethod(RegistrationPage::class, 'handleSubmit');
|
||||
@@ -624,7 +639,7 @@ class RegistrationPageTest extends TestCase
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_is_guardian' => '1',
|
||||
'us_registering_for' => RegistrationPage::FOR_STUDENTS,
|
||||
'children' => [
|
||||
['name' => 'Ada', 'birth_year' => '2015', 'answers' => [7 => 'Piano']],
|
||||
['name' => 'Alan', 'birth_year' => '2017', 'answers' => [7 => 'Violin']],
|
||||
@@ -711,12 +726,111 @@ class RegistrationPageTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* "On behalf of students" is the one choice that says the account holder is
|
||||
* not a student, so it is the one that sets the flag.
|
||||
*/
|
||||
public function testRegisteringOnlyForStudentsMarksTheAccountGuardianOnly(): void
|
||||
{
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_registering_for' => RegistrationPage::FOR_STUDENTS,
|
||||
'children' => [['name' => 'Ada', 'birth_year' => '2015', 'answers' => []]],
|
||||
];
|
||||
|
||||
$this->ctx['questions']->shouldReceive('findByScope')->andReturn([]);
|
||||
$this->ctx['guardians']->shouldReceive('createChild')->once()->andReturn(101);
|
||||
$this->stubInviteSuccess();
|
||||
|
||||
$this->ctx['guardians']->shouldReceive('setGuardianOnly')->once()->with(42, true);
|
||||
|
||||
self::assertSame('invite', $this->submit(new Invite(email: '[email protected]', token: 'hash'), false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider modesThatKeepTheAccountHolderAStudent
|
||||
*/
|
||||
public function testTheAccountHolderStaysAStudentForTheOtherTwoChoices(string $mode, bool $withChildren): void
|
||||
{
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_registering_for' => $mode,
|
||||
];
|
||||
|
||||
if ($withChildren) {
|
||||
$_POST['children'] = [['name' => 'Ada', 'birth_year' => '2015', 'answers' => []]];
|
||||
$this->ctx['guardians']->shouldReceive('createChild')->once()->andReturn(101);
|
||||
}
|
||||
|
||||
$this->ctx['questions']->shouldReceive('findByScope')->andReturn([]);
|
||||
$this->stubInviteSuccess();
|
||||
|
||||
$this->ctx['guardians']->shouldReceive('setGuardianOnly')->once()->with(42, false);
|
||||
|
||||
self::assertSame('invite', $this->submit(new Invite(email: '[email protected]', token: 'hash'), false));
|
||||
}
|
||||
|
||||
/** @return array<string, array{string, bool}> */
|
||||
public static function modesThatKeepTheAccountHolderAStudent(): array
|
||||
{
|
||||
return [
|
||||
'just myself' => [RegistrationPage::FOR_SELF, false],
|
||||
'myself and students' => [RegistrationPage::FOR_BOTH, true],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* "Both" collects students exactly as "on behalf of" does — the only
|
||||
* difference is whether the account holder is one of them.
|
||||
*/
|
||||
public function testBothStillRequiresAtLeastOneStudent(): void
|
||||
{
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_registering_for' => RegistrationPage::FOR_BOTH,
|
||||
'children' => [],
|
||||
];
|
||||
|
||||
$this->ctx['questions']->shouldReceive('findByScope')->andReturn([]);
|
||||
Functions\when('email_exists')->justReturn(false);
|
||||
Functions\expect('wp_insert_user')->never();
|
||||
|
||||
self::assertStringContainsString('at least one student', $this->submit(new Invite(email: '[email protected]', token: 'hash'), false));
|
||||
}
|
||||
|
||||
/**
|
||||
* A form posted without the radio — an old cached page, or a crafted
|
||||
* request — must fall to the choice that collects and grants the least,
|
||||
* never be read as "register these children".
|
||||
*/
|
||||
public function testAMissingOrUnknownChoiceFallsBackToJustMyself(): void
|
||||
{
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_registering_for' => 'something-else',
|
||||
'children' => [['name' => 'Ada', 'birth_year' => '2015', 'answers' => []]],
|
||||
];
|
||||
|
||||
$this->ctx['questions']->shouldReceive('findByScope')->andReturn([]);
|
||||
$this->stubInviteSuccess();
|
||||
|
||||
// No student is created from children[] the caller never asked to register.
|
||||
$this->ctx['guardians']->shouldNotReceive('createChild');
|
||||
$this->ctx['guardians']->shouldReceive('setGuardianOnly')->once()->with(42, false);
|
||||
|
||||
self::assertSame('invite', $this->submit(new Invite(email: '[email protected]', token: 'hash'), false));
|
||||
}
|
||||
|
||||
public function testGuardianSignupWithNoChildrenIsRejected(): void
|
||||
{
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_is_guardian' => '1',
|
||||
'us_registering_for' => RegistrationPage::FOR_STUDENTS,
|
||||
'children' => [['name' => '', 'birth_year' => '', 'answers' => []]],
|
||||
];
|
||||
|
||||
@@ -740,7 +854,7 @@ class RegistrationPageTest extends TestCase
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_is_guardian' => '1',
|
||||
'us_registering_for' => RegistrationPage::FOR_STUDENTS,
|
||||
'children' => [
|
||||
['name' => 'Ada', 'birth_year' => '2015', 'answers' => []],
|
||||
['name' => '', 'birth_year' => '2017', 'answers' => []],
|
||||
@@ -766,7 +880,7 @@ class RegistrationPageTest extends TestCase
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_is_guardian' => '1',
|
||||
'us_registering_for' => RegistrationPage::FOR_STUDENTS,
|
||||
'children' => [['name' => 'Ada', 'birth_year' => $submitted, 'answers' => []]],
|
||||
];
|
||||
|
||||
@@ -800,7 +914,7 @@ class RegistrationPageTest extends TestCase
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_is_guardian' => '1',
|
||||
'us_registering_for' => RegistrationPage::FOR_STUDENTS,
|
||||
'children' => [
|
||||
['name' => 'Ada', 'birth_year' => '2015', 'answers' => [7 => 'Piano']],
|
||||
['name' => 'Alan', 'birth_year' => '2017', 'answers' => [7 => ' ']],
|
||||
@@ -827,7 +941,7 @@ class RegistrationPageTest extends TestCase
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_is_guardian' => '1',
|
||||
'us_registering_for' => RegistrationPage::FOR_STUDENTS,
|
||||
'children' => [
|
||||
['name' => 'Ada', 'birth_year' => '2015', 'answers' => []],
|
||||
['name' => 'Alan', 'birth_year' => '2017', 'answers' => []],
|
||||
@@ -864,7 +978,7 @@ class RegistrationPageTest extends TestCase
|
||||
$_POST = [
|
||||
'password' => 'thistle-marrow-42',
|
||||
'display_name' => 'Grace',
|
||||
'us_is_guardian' => '1',
|
||||
'us_registering_for' => RegistrationPage::FOR_STUDENTS,
|
||||
'accept' => [3],
|
||||
'children' => [['name' => 'Ada', 'birth_year' => '2015', 'answers' => []]],
|
||||
];
|
||||
|
||||
@@ -17,6 +17,32 @@ abstract class TestCase extends BaseTestCase
|
||||
Monkey\setUp();
|
||||
Monkey\Functions\stubTranslationFunctions();
|
||||
Monkey\Functions\stubEscapeFunctions();
|
||||
|
||||
// WordPress's form-state helpers. Stubbed here rather than per test
|
||||
// because they are pure output helpers with no behaviour worth faking
|
||||
// differently anywhere — a template either emits the attribute or not.
|
||||
Monkey\Functions\when('checked')->alias(
|
||||
static fn (mixed $checked, mixed $current = true, bool $echo = true): string
|
||||
=> self::formAttribute('checked', $checked, $current, $echo)
|
||||
);
|
||||
Monkey\Functions\when('selected')->alias(
|
||||
static fn (mixed $selected, mixed $current = true, bool $echo = true): string
|
||||
=> self::formAttribute('selected', $selected, $current, $echo)
|
||||
);
|
||||
}
|
||||
|
||||
/** Mirrors WordPress's `__checked_selected_helper()`, echo included. */
|
||||
private static function formAttribute(string $type, mixed $helper, mixed $current, bool $echo): string
|
||||
{
|
||||
// WordPress compares loosely, and templates rely on that: `checked($a, $b)`
|
||||
// is routinely called with an int against a numeric string.
|
||||
$result = $helper == $current ? " {$type}='{$type}'" : ''; // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
|
||||
if ($echo) {
|
||||
echo $result; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fixed literal.
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
|
||||
Reference in New Issue
Block a user