offeringId); self::assertSame('Your level?', $question->label); self::assertSame(Question::FIELD_TEXT, $question->fieldType); self::assertNull($question->options); self::assertFalse($question->isRequired); self::assertSame(0, $question->sortOrder); self::assertTrue($question->isActive); self::assertSame(Question::SCOPE_OFFERING, $question->scope); self::assertNull($question->id); } public function testAccountScopeQuestionHasNoOffering(): void { $question = new Question(null, 'Emergency contact', scope: Question::SCOPE_ACCOUNT); self::assertNull($question->offeringId); self::assertSame(Question::SCOPE_ACCOUNT, $question->scope); } public function testFromRowDecodesOptionsJson(): void { $row = (object) [ 'id' => '3', 'offering_id' => '7', 'scope' => Question::SCOPE_OFFERING, 'label' => 'Pick a level', 'field_type' => Question::FIELD_SELECT, 'options' => '["Beginner","Advanced"]', 'is_required' => '1', 'sort_order' => '2', 'is_active' => '1', ]; $question = Question::fromRow($row); self::assertSame(3, $question->id); self::assertSame(Question::FIELD_SELECT, $question->fieldType); self::assertSame(['Beginner', 'Advanced'], $question->options); self::assertTrue($question->isRequired); self::assertSame(2, $question->sortOrder); self::assertSame(Question::SCOPE_OFFERING, $question->scope); } public function testFromRowHandlesNullOptions(): void { $row = (object) [ 'id' => '4', 'offering_id' => '7', 'scope' => Question::SCOPE_OFFERING, 'label' => 'Notes', 'field_type' => Question::FIELD_TEXTAREA, 'options' => null, 'is_required' => '0', 'sort_order' => '0', 'is_active' => '0', ]; $question = Question::fromRow($row); self::assertNull($question->options); self::assertFalse($question->isActive); } public function testFromRowHandlesAccountScopeWithNullOffering(): void { $row = (object) [ 'id' => '5', 'offering_id' => null, 'scope' => Question::SCOPE_ACCOUNT, 'label' => 'How did you hear about us?', 'field_type' => Question::FIELD_TEXT, 'options' => null, 'is_required' => '1', 'sort_order' => '0', 'is_active' => '1', ]; $question = Question::fromRow($row); self::assertNull($question->offeringId); self::assertSame(Question::SCOPE_ACCOUNT, $question->scope); self::assertTrue($question->isRequired); } public function testToArrayContainsExpectedKeys(): void { $question = new Question(7, 'Label', Question::FIELD_TEXT, id: 9); $arr = $question->toArray(); foreach (['id', 'offering_id', 'scope', 'label', 'field_type', 'options', 'audience', 'is_required', 'is_required_child', 'sort_order', 'is_active'] as $key) { self::assertArrayHasKey($key, $arr); } } public function testDefaultsToBeingAskedOfEveryoneAndRequiredOfNobody(): void { $question = new Question(null, 'Instrument', scope: Question::SCOPE_ACCOUNT); self::assertSame(Question::AUDIENCE_ALL, $question->audience); self::assertTrue($question->askedOfSelf()); self::assertFalse($question->isRequiredForSelf()); self::assertFalse($question->isRequiredForChild()); } public function testAChildAudienceQuestionIsNeverAskedOfTheAccountHolder(): void { $question = new Question( null, 'School and grade', scope: Question::SCOPE_ACCOUNT, audience: Question::AUDIENCE_CHILD, isRequired: true, isRequiredChild: true ); self::assertFalse($question->askedOfSelf()); // Required-ness cannot outlive the audience: a question the account // holder is never shown must never be one they are held to. self::assertFalse($question->isRequiredForSelf()); self::assertTrue($question->isRequiredForChild()); } public function testAQuestionCanBeOptionalForYouAndRequiredForYourStudents(): void { $question = new Question( null, 'Previous experience', scope: Question::SCOPE_ACCOUNT, isRequired: false, isRequiredChild: true ); self::assertTrue($question->askedOfSelf()); self::assertFalse($question->isRequiredForSelf()); self::assertTrue($question->isRequiredForChild()); } public function testFromRowReadsAudienceAndTheStudentsRequiredFlag(): void { $row = (object) [ 'id' => '6', 'offering_id' => null, 'scope' => Question::SCOPE_ACCOUNT, 'label' => 'School and grade', 'field_type' => Question::FIELD_TEXT, 'options' => null, 'audience' => Question::AUDIENCE_CHILD, 'is_required' => '0', 'is_required_child' => '1', 'sort_order' => '0', 'is_active' => '1', ]; $question = Question::fromRow($row); self::assertSame(Question::AUDIENCE_CHILD, $question->audience); self::assertFalse($question->askedOfSelf()); self::assertTrue($question->isRequiredForChild()); } /** * A row read before dbDelta has added the columns — or one carrying a value * no longer recognised — falls back to the behaviour every question had * before the distinction existed: asked of everyone. */ public function testFromRowFallsBackToEveryoneWhenAudienceIsMissingOrUnknown(): void { $base = [ 'id' => '7', 'offering_id' => null, 'scope' => Question::SCOPE_ACCOUNT, 'label' => 'Instrument', 'field_type' => Question::FIELD_TEXT, 'options' => null, 'is_required' => '1', 'sort_order' => '0', 'is_active' => '1', ]; $missing = Question::fromRow((object) $base); $unknown = Question::fromRow((object) ($base + ['audience' => 'grown-ups'])); self::assertSame(Question::AUDIENCE_ALL, $missing->audience); self::assertTrue($missing->isRequiredForSelf()); self::assertFalse($missing->isRequiredForChild()); self::assertSame(Question::AUDIENCE_ALL, $unknown->audience); } public function testValidAudienceConstants(): void { self::assertContains(Question::AUDIENCE_ALL, Question::VALID_AUDIENCES); self::assertContains(Question::AUDIENCE_CHILD, Question::VALID_AUDIENCES); } public function testValidFieldTypeConstants(): void { self::assertContains(Question::FIELD_TEXT, Question::VALID_FIELD_TYPES); self::assertContains(Question::FIELD_TEXTAREA, Question::VALID_FIELD_TYPES); self::assertContains(Question::FIELD_SELECT, Question::VALID_FIELD_TYPES); self::assertContains(Question::FIELD_CHECKBOX, Question::VALID_FIELD_TYPES); } public function testValidScopeConstants(): void { self::assertContains(Question::SCOPE_OFFERING, Question::VALID_SCOPES); self::assertContains(Question::SCOPE_ACCOUNT, Question::VALID_SCOPES); } }