1, 'field' => 'first', 'name' => '前置', 'edit_input' => 'radio', 'select_item' => [['key' => 'yes', 'value' => '是']]]; $child = ['id' => 2, 'field' => 'second', 'name' => '后续', 'edit_input' => 'radio', 'select_item' => [['key' => 'yes', 'value' => '是']], 'config' => ['visible_when' => ['field' => 'first', 'value' => 'yes']]]; $last = ['id' => 3, 'field' => 'third', 'name' => '末题', 'edit_input' => 'text', 'config' => ['visible_when' => ['field' => 'second', 'value' => 'yes']]]; SurveySchema::validateConditions([$parent, $child, $last]); $this->assertFalse(SurveySchema::visible($last, [$parent, $child, $last], ['1' => ['value' => 'no'], '2' => ['value' => 'yes']])); $this->assertTrue(SurveySchema::visible($last, [$parent, $child, $last], ['1' => ['value' => 'yes'], '2' => ['value' => 'yes']])); foreach ([[$child, $parent], [$child], [array_merge($parent, ['select_item' => []]), $child]] as $invalid) { try { SurveySchema::validateConditions($invalid); $this->fail('Invalid reference accepted'); } catch (InvalidArgumentException $e) { $this->assertStringContainsString('显示条件', $e->getMessage()); } } } private function questions(): array { return [ ['id' => 1, 'field' => 'section', 'name' => '资源对接', 'edit_input' => 'section'], ['id' => 2, 'field' => 'resource', 'name' => '融资', 'edit_input' => 'multi_dimension', 'need_fill' => true, 'dimensions' => [ ['field' => 'intent', 'name' => '意向', 'need_fill' => true, 'select_item' => [['key' => 'yes', 'value' => '是'], ['key' => 'no', 'value' => '否']]], ['field' => 'quantity', 'name' => '数量', 'edit_input' => 'number', 'need_fill' => true, 'config' => ['min' => 0, 'max' => 100, 'integer' => true]], ]], ['id' => 3, 'field' => 'rating', 'name' => '满意度', 'edit_input' => 'radio', 'config' => ['is_score' => true], 'select_item' => [['key' => 'a', 'value' => '满意', 'score' => 5], ['key' => 'b', 'value' => '一般', 'score' => 3]]], ['id' => 4, 'field' => 'needs', 'name' => '需求', 'edit_input' => 'checkbox', 'config' => ['other_key' => 'other', 'other_required' => true], 'select_item' => [['key' => 'bank', 'value' => '银行'], ['key' => 'other', 'value' => '其他']]], ]; } public function testServerSchemaAndZeroArePreserved(): void { $q = $this->questions(); $answers = SurveySchema::normalize($q, [ ['id' => 2, 'name' => '篡改标题', 'value' => ['intent' => 'no', 'quantity' => '0', 'unknown' => 8]], ['id' => 3, 'value' => 'a', 'select_item' => [['key' => 'a', 'score' => 999]]], ], true); $this->assertSame('融资', $answers[1]['name']); $this->assertSame(['intent' => 'no', 'quantity' => 0.0], $answers[1]['value']); $this->assertSame(5, $answers[2]['select_item'][0]['score']); $this->assertSame([], $answers[3]['value']); $reply = SurveySchema::replyQuestions([], $answers, ['version' => 2, 'questions' => $q]); $this->assertSame('0', $reply[1]['dimensions'][1]['display_value']); $this->assertSame('否', $reply[1]['dimensions'][0]['display_value']); } public function testReplySelectionsSupportSnapshotsLegacyAndZeroKeys(): void { $q = $this->questions(); $q[2]['select_item'][] = ['key' => 0, 'value' => '零分']; $reply = SurveySchema::replyQuestions([], [ ['id' => 2, 'value' => ['intent' => 'no']], ['id' => 3, 'value' => 0], ['id' => 4, 'value' => ['bank', 'other']], ], ['version' => 2, 'questions' => $q]); $this->assertSame(['no'], $reply[1]['dimensions'][0]['selected_keys']); $this->assertSame([], $reply[1]['dimensions'][1]['selected_keys']); $this->assertSame(['0'], $reply[2]['selected_keys']); $this->assertSame(['bank', 'other'], $reply[3]['selected_keys']); $legacy = SurveySchema::replyQuestions($q, [ ['id' => 2, 'value' => ['intent' => '否']], ['id' => 3, 'value' => '满意'], ['id' => 4, 'value' => '银行,其他'], ], null); $this->assertSame(['no'], $legacy[1]['dimensions'][0]['selected_keys']); $this->assertSame(['a'], $legacy[2]['selected_keys']); $this->assertSame(['bank', 'other'], $legacy[3]['selected_keys']); $empty = SurveySchema::replyQuestions($q, [], null); $this->assertSame([], $empty[2]['selected_keys']); } public function testConditionalIdentityAndPerOptionNotes(): void { $questions = [ ['id' => 1, 'field' => 'insurance', 'name' => '保险', 'edit_input' => 'radio', 'select_item' => [['key' => 'yes', 'value' => '需要'], ['key' => 'no', 'value' => '不需要']]], ['id' => 2, 'field' => 'identity', 'name' => '身份证', 'edit_input' => 'text', 'need_fill' => true, 'config' => ['visible_when' => ['field' => 'insurance', 'value' => 'yes']]], ['id' => 3, 'field' => 'venue', 'name' => '会场', 'edit_input' => 'checkbox', 'config' => ['max_choices' => 2, 'option_notes' => ['tax' => []]], 'select_item' => [['key' => 'tax', 'value' => '财税'], ['key' => 'legal', 'value' => '法律'], ['key' => 'ip', 'value' => '知识产权']]], ]; $raw = [['id' => 1, 'value' => 'no'], ['id' => 2, 'value' => 'must discard'], ['id' => 3, 'value' => ['tax'], 'option_texts' => ['tax' => '咨询问题', 'legal' => '不应存储']]]; $answers = SurveySchema::normalize($questions, $raw, true); $this->assertNull($answers[1]['value']); $this->assertTrue($answers[1]['hidden']); $this->assertSame(['tax' => '咨询问题'], $answers[2]['option_texts']); $reply = SurveySchema::replyQuestions([], $answers, ['version' => 2, 'questions' => $questions]); $this->assertTrue($reply[1]['hidden']); $this->assertSame(['tax' => '咨询问题'], $reply[2]['option_texts']); $raw[0]['value'] = 'yes'; $raw[1]['value'] = ''; try { SurveySchema::normalize($questions, $raw, true); $this->fail('Missing conditional answer accepted'); } catch (InvalidArgumentException $e) { $this->assertStringContainsString('身份证', $e->getMessage()); } $raw[1]['value'] = 'valid'; $raw[2]['value'] = ['tax', 'legal', 'ip']; $this->expectException(InvalidArgumentException::class); SurveySchema::normalize($questions, $raw, true); } public function testCustomScoresAreNotLimitedToFivePointScale(): void { $q = $this->questions()[2]; $q['select_item'] = [['key' => 'custom', 'value' => '自定义', 'score' => -2.5], ['key' => 'ten', 'value' => '10分', 'score' => 10]]; $q = SurveySchema::question($q); $this->assertSame(-2.5, $q['select_item'][0]['score']); $this->assertSame(10.0, $q['select_item'][1]['score']); } public function testCombinationRequiredIsControlledByParent(): void { $q = $this->questions()[1]; $q['need_fill'] = false; $q['rule'] = ''; $normalized = SurveySchema::normalize([$q], [], true); $this->assertNull($normalized[0]['value']['intent']); $q['need_fill'] = true; $this->expectException(InvalidArgumentException::class); SurveySchema::normalize([$q], [['id' => 2, 'value' => ['intent' => 'yes']]], true); } /** @dataProvider invalidNumberProvider */ public function testInvalidNumbersAreRejected($value): void { $this->expectException(InvalidArgumentException::class); SurveySchema::normalize($this->questions(), [['id' => 2, 'value' => ['intent' => 'yes', 'quantity' => $value]]], true); } public function invalidNumberProvider(): array { return [[-1], [1.5], [101], ['abc'], [null], ['1e999']]; } public function testOtherRequiresTextAndUnselectedTextIsDiscarded(): void { $q = [$this->questions()[3]]; $answer = SurveySchema::normalize($q, [['id' => 4, 'value' => ['bank'], 'other_text' => '旧内容']], true); $this->assertSame('', $answer[0]['other_text']); $this->expectException(InvalidArgumentException::class); SurveySchema::normalize($q, [['id' => 4, 'value' => ['other']]], true); } public function testForgedOptionsCannotBeSubmitted(): void { $this->expectException(InvalidArgumentException::class); SurveySchema::normalize([$this->questions()[2]], [['id' => 3, 'value' => 'fake']], true); } public function testLegacyTextOptionsAndSerializedDimensions(): void { $q = $this->questions(); $answers = [['id' => 2, 'value' => json_encode(['intent' => '否', 'quantity' => 0])], ['id' => 3, 'value' => '满意']]; $normalized = SurveySchema::normalize($q, $answers); $this->assertSame('no', $normalized[1]['value']['intent']); $this->assertSame('a', $normalized[2]['value']); $reply = SurveySchema::replyQuestions($q, $answers, null); $this->assertSame('否', $reply[1]['dimensions'][0]['display_value']); $this->assertSame('0', $reply[1]['dimensions'][1]['display_value']); } public function testStatisticsUseValidAnswersAndCountSelectionsOnce(): void { $q = $this->questions(); $stats = SurveyStatistics::summarize($q, [ ['snapshot' => ['version' => 2], 'data' => [['id' => 2, 'value' => ['intent' => 'no', 'quantity' => 0]], ['id' => 3, 'value' => 'a'], ['id' => 4, 'value' => ['bank', 'other', 'bank'], 'other_text' => '融资']]], ['data' => [['id' => 2, 'value' => '{"intent":"是","quantity":3}'], ['id' => 3, 'value' => '一般']]], ['data' => []], ]); $this->assertSame(3, $stats['responses']); $this->assertSame(3, $stats['questions_count']); $this->assertEquals(4, $stats['average']); $this->assertSame(2, $stats['questions'][1]['dimensions'][1]['stats']['valid_count']); $this->assertEquals(3, $stats['questions'][1]['dimensions'][1]['stats']['sum']); $this->assertEquals(100, $stats['questions'][3]['stats']['options'][0]['percentage']); $this->assertSame(1, $stats['questions'][3]['stats']['options'][0]['count']); $this->assertSame(1, $stats['questions'][3]['stats']['other_count']); } public function testSnapshotSurvivesChangedCurrentDefinitions(): void { $original = $this->questions()[2]; $current = array_replace($original, ['name' => '新问题']); $reply = SurveySchema::replyQuestions([$current], [['id' => 3, 'value' => 'a']], ['version' => 2, 'questions' => [$original]]); $this->assertSame('满意度', $reply[0]['name']); $this->assertSame('满意', $reply[0]['display_value']); } public function testQuestionValidationRejectsDuplicateDimensionIdentifiers(): void { $q = $this->questions()[1]; $q['dimensions'][1]['field'] = 'intent'; $this->expectException(InvalidArgumentException::class); SurveySchema::question($q); } public function testDeletedLegacyQuestionRemainsVisible(): void { $q = $this->questions()[2]; $q['value'] = '满意'; $reply = SurveySchema::replyQuestions([], [$q], null); $this->assertCount(1, $reply); $this->assertSame('满意', $reply[0]['display_value']); } }