markTestSkipped("ext-{$ext} not loaded; PhpSpreadsheet cannot run on this host."); } } if (!is_file(self::FIXTURE)) { $this->markTestSkipped('Sample workbook not present at ' . self::FIXTURE); } } public function testParsesEverySheet(): void { $parser = new XlsxSprintImporter(); $sheets = $parser->parse(self::FIXTURE); $this->assertCount(3, $sheets); $this->assertSame(['Sprint 1', 'Sprint 2', 'Sprint 3'], array_map(fn(ParsedSheet $s) => $s->sheetName, $sheets)); } public function testSprint1ShapeAndCounts(): void { $parser = new XlsxSprintImporter(); $sheets = $parser->parse(self::FIXTURE); $s = $sheets[0]; $this->assertSame(5, count($s->weeks), '5 weeks'); $this->assertSame(15, count($s->workers), '15 workers'); $this->assertGreaterThan(20, count($s->tasks), 'more than 20 tasks'); $this->assertEqualsWithDelta(0.2, $s->reserveFraction, 1e-9, 'reserve fraction = 0.2'); $kws = array_map(fn($w) => $w->kw, $s->weeks); $this->assertSame([13, 14, 15, 16, 17], $kws, 'KWs 13..17 in order'); $maxDays = array_map(fn($w) => $w->maxWorkingDays, $s->weeks); $this->assertSame([2, 4, 4, 5, 2], $maxDays); } public function testSprint2ColourMappingMatchesSpreadsheet(): void { $parser = new XlsxSprintImporter(); $sheets = $parser->parse(self::FIXTURE); $s2 = $sheets[1]; $this->assertSame('Sprint 2', $s2->sheetName); $statusCounts = [ TaskAssignment::STATUS_ZUGEWIESEN => 0, TaskAssignment::STATUS_GESTARTET => 0, TaskAssignment::STATUS_ABGESCHLOSSEN => 0, TaskAssignment::STATUS_ABGEBROCHEN => 0, ]; foreach ($s2->tasks as $t) { foreach ($t->assignments as $a) { $statusCounts[$a->status]++; } } // From the openpyxl colour audit on the sample: // 17× FFFFFF00 + 6× FFFFEB9C + 4× FFFFC000 = 27 yellow/orange -> gestartet // 4× FF00B050 + 1× FFC6EFCE = 5 green -> abgeschlossen // the only red-coded cells in the workbook are zero. $this->assertSame(27, $statusCounts[TaskAssignment::STATUS_GESTARTET], 'yellow + orange cells = 27'); $this->assertSame(5, $statusCounts[TaskAssignment::STATUS_ABGESCHLOSSEN], 'green cells = 5'); $this->assertSame(0, $statusCounts[TaskAssignment::STATUS_ABGEBROCHEN], 'no red cells in sample'); } public function testSprint2SkipsArbeitstageGapAndDefinesSixteenWorkers(): void { // Sprint 2's Arbeitstage block has a blank row at C13 between Titus and // Suzan; the parser should resume past the gap and end up with 16 workers. $parser = new XlsxSprintImporter(); $sheets = $parser->parse(self::FIXTURE); $s2 = $sheets[1]; $this->assertCount(16, $s2->workers, 'Sprint 2 has 16 workers (gap row tolerated)'); $names = array_map(fn($w) => $w->name, $s2->workers); $this->assertContains('Suzan', $names); $this->assertContains('Nicole', $names); } public function testRoundTripsViaToArrayFromArray(): void { $parser = new XlsxSprintImporter(); $sheets = $parser->parse(self::FIXTURE); foreach ($sheets as $orig) { $clone = ParsedSheet::fromArray($orig->toArray()); $this->assertSame($orig->sheetName, $clone->sheetName); $this->assertSame(count($orig->weeks), count($clone->weeks)); $this->assertSame(count($orig->workers), count($clone->workers)); $this->assertSame(count($orig->tasks), count($clone->tasks)); $this->assertEqualsWithDelta($orig->reserveFraction, $clone->reserveFraction, 1e-9); } } }