第一段
第二段
续行
';
$text = StudyTourDeclarationExporter::htmlToPlainText($html);
$this->assertStringContainsString("第一段", $text);
$this->assertStringContainsString("第二段", $text);
$this->assertStringContainsString("续行", $text);
$this->assertStringContainsString("要点A", $text);
$this->assertStringContainsString("要点B", $text);
}
public function test_sanitize_filename_strips_illegal_chars(): void
{
$this->assertSame('线路A_B', StudyTourDeclarationExporter::sanitizeFilename('线路A/B'));
$this->assertSame('研学线路', StudyTourDeclarationExporter::sanitizeFilename(' '));
}
public function test_docx_contains_declaration_sections_and_cover_image(): void
{
$png = sys_get_temp_dir().'/st-cover-test.png';
file_put_contents($png, base64_decode(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='
));
$uploadDir = storage_path('app/public/uploads');
if (! is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$implPng = $uploadDir.'/st-impl-test.png';
copy($png, $implPng);
$path = StudyTourDeclarationExporter::writeDocxFile([
'name' => '太湖蚕桑研学',
'org_name' => '苏州市科协',
'seasons' => ['spring', 'autumn'],
'venue_names' => ['苏州丝绸博物馆', '自定义营地'],
'suitable_count' => '30人',
'suitable_audience' => 'primary,junior',
'duration' => '2天1晚',
'contact_person' => '张老师',
'contact_phones' => '13800000000',
'cover_image_path' => $png,
'intro_html' => '走进蚕桑文化。
',
'route_plans' => [[
'date_label' => '第一天',
'items' => [[
'time' => '09:00',
'activity' => '开营仪式',
'location' => '活动中心',
]],
]],
'courses' => [[
'sort' => 1,
'name' => '缫丝体验',
'content' => '了解传统缫丝工艺',
]],
'fee_html' => '680元/人
',
'implementation_html' => '2026年7月1日第1场

',
'season_options' => [
['value' => 'spring', 'label' => '春季'],
['value' => 'summer', 'label' => '夏季'],
['value' => 'autumn', 'label' => '秋季'],
['value' => 'winter', 'label' => '冬季'],
],
'audience_options' => [
['value' => 'kindergarten', 'label' => '幼儿园'],
['value' => 'primary', 'label' => '小学'],
['value' => 'junior', 'label' => '初中'],
['value' => 'high', 'label' => '高中'],
['value' => 'all', 'label' => '全学段'],
],
]);
$this->assertFileExists($path);
$zip = new ZipArchive();
$this->assertTrue($zip->open($path) === true);
$xml = (string) $zip->getFromName('word/document.xml');
$mediaCount = 0;
for ($i = 0; $i < $zip->numFiles; $i++) {
$name = (string) $zip->getNameIndex($i);
if (str_starts_with($name, 'word/media/')) {
$mediaCount++;
}
}
$zip->close();
foreach ([
'一、线路基本情况',
'二、线路简介',
'三、线路规划',
'四、研学课程',
'五、线路收费标准',
'六、线路计划实施情况',
'太湖蚕桑研学',
'苏州市科协',
'苏州丝绸博物馆',
'走进蚕桑文化',
'开营仪式',
'缫丝体验',
'680元/人',
'封面图',
'2026年7月1日第1场',
] as $needle) {
$this->assertStringContainsString($needle, $xml, "missing {$needle}");
}
$this->assertGreaterThanOrEqual(2, $mediaCount, 'cover and implementation images should be embedded');
$phpWord = IOFactory::load($path);
$this->assertNotEmpty($phpWord->getSections());
@unlink($path);
@unlink($png);
@unlink($implPng);
}
}