mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-05 15:10:16 +02:00
Fix quizToXlsx to support unlimited answers and add header count tests
- Replace hardcoded 6-column arrays with dynamic Coordinate arithmetic - Write data rows first to determine max answer count, write headers last - Replace try/catch ErrorException in fillQuizFromArray with array_key_exists - Add data-provider test covering 2, 6, 7, and 10 answers - Add cross-question max-header and 7-answer round-trip tests
This commit is contained in:
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Tvdt\Service;
|
namespace Tvdt\Service;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||||
use PhpOffice\PhpSpreadsheet\Reader;
|
use PhpOffice\PhpSpreadsheet\Reader;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use PhpOffice\PhpSpreadsheet\Writer;
|
use PhpOffice\PhpSpreadsheet\Writer;
|
||||||
@@ -94,24 +95,16 @@ class QuizSpreadsheetService
|
|||||||
$answerCounter = 1;
|
$answerCounter = 1;
|
||||||
$arrCounter = 1;
|
$arrCounter = 1;
|
||||||
|
|
||||||
while (true) {
|
while (\array_key_exists($arrCounter, $questionArr) && null !== $questionArr[$arrCounter]) {
|
||||||
try {
|
|
||||||
if (null === $questionArr[$arrCounter]) {
|
|
||||||
if (1 === $answerCounter) {
|
|
||||||
$errors[] = \sprintf('Question %d has no answers', $answerCounter);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (\ErrorException) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$answer = new Answer((string) $questionArr[$arrCounter++], (bool) $questionArr[$arrCounter++]);
|
$answer = new Answer((string) $questionArr[$arrCounter++], (bool) $questionArr[$arrCounter++]);
|
||||||
$answer->ordering = $answerCounter++;
|
$answer->ordering = $answerCounter++;
|
||||||
$question->addAnswer($answer);
|
$question->addAnswer($answer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (1 === $answerCounter) {
|
||||||
|
$errors[] = \sprintf('Question %d has no answers', $answerCounter);
|
||||||
|
}
|
||||||
|
|
||||||
$quiz->addQuestion($question);
|
$quiz->addQuestion($question);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,41 +118,41 @@ class QuizSpreadsheetService
|
|||||||
$spreadsheet = new Spreadsheet();
|
$spreadsheet = new Spreadsheet();
|
||||||
$sheet = $spreadsheet->getActiveSheet();
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
|
||||||
$sheet->getStyle('1:1')->getFont()->setBold(true);
|
// Write data rows first so we know the maximum answer count.
|
||||||
|
$maxAnswers = 0;
|
||||||
$sheet->setCellValue('A1', 'Question');
|
|
||||||
$sheet->getColumnDimension('A')->setWidth(30);
|
|
||||||
$sheet->getStyle('A:A')->getAlignment()->setWrapText(true);
|
|
||||||
|
|
||||||
$counter = 1;
|
|
||||||
foreach (range('B', 'L', 2) as $column) {
|
|
||||||
$sheet->setCellValue($column.'1', 'Answer '.$counter++);
|
|
||||||
$sheet->getColumnDimension($column)->setWidth(30);
|
|
||||||
$sheet->getStyle($column.':'.$column)->getAlignment()->setWrapText(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (range('C', 'M', 2) as $column) {
|
|
||||||
$sheet->setCellValue($column.'1', 'Correct');
|
|
||||||
$sheet->getColumnDimension($column)->setAutoSize(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$answerColumns = range('B', 'L', 2);
|
|
||||||
$correctColumns = range('C', 'M', 2);
|
|
||||||
|
|
||||||
$row = 2;
|
$row = 2;
|
||||||
foreach ($quiz->questions as $question) {
|
foreach ($quiz->questions as $question) {
|
||||||
$sheet->setCellValue('A'.$row, $question->question);
|
$sheet->setCellValue('A'.$row, $question->question);
|
||||||
|
|
||||||
$col = 0;
|
$col = 0;
|
||||||
foreach ($question->answers as $answer) {
|
foreach ($question->answers as $answer) {
|
||||||
$sheet->setCellValue($answerColumns[$col].$row, $answer->text);
|
$sheet->setCellValue(Coordinate::stringFromColumnIndex(2 + 2 * $col).$row, $answer->text);
|
||||||
$sheet->setCellValue($correctColumns[$col].$row, $answer->isRightAnswer);
|
$sheet->setCellValue(Coordinate::stringFromColumnIndex(3 + 2 * $col).$row, $answer->isRightAnswer);
|
||||||
++$col;
|
++$col;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$maxAnswers = max($maxAnswers, $col);
|
||||||
++$row;
|
++$row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write headers last, sized to the widest question.
|
||||||
|
$sheet->getStyle('1:1')->getFont()->setBold(true);
|
||||||
|
$sheet->setCellValue('A1', 'Question');
|
||||||
|
$sheet->getColumnDimension('A')->setWidth(30);
|
||||||
|
$sheet->getStyle('A:A')->getAlignment()->setWrapText(true);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $maxAnswers; ++$i) {
|
||||||
|
$answerCol = Coordinate::stringFromColumnIndex(2 + 2 * $i);
|
||||||
|
$correctCol = Coordinate::stringFromColumnIndex(3 + 2 * $i);
|
||||||
|
|
||||||
|
$sheet->setCellValue($answerCol.'1', 'Answer '.($i + 1));
|
||||||
|
$sheet->getColumnDimension($answerCol)->setWidth(30);
|
||||||
|
$sheet->getStyle($answerCol.':'.$answerCol)->getAlignment()->setWrapText(true);
|
||||||
|
|
||||||
|
$sheet->setCellValue($correctCol.'1', 'Correct');
|
||||||
|
$sheet->getColumnDimension($correctCol)->setAutoSize(true);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->toXlsx($spreadsheet);
|
return $this->toXlsx($spreadsheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Tvdt\Tests\Service;
|
namespace Tvdt\Tests\Service;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Reader;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\HttpFoundation\File\File;
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
use Tvdt\Entity\Answer;
|
use Tvdt\Entity\Answer;
|
||||||
@@ -140,6 +142,65 @@ final class QuizSpreadsheetServiceTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{int, string, int, string, int, int}> */
|
||||||
|
public static function answerCountHeaderProvider(): array
|
||||||
|
{
|
||||||
|
// Columns (0-based): Question=0, Answer1=1, Correct=2, Answer2=3, Correct=4, …
|
||||||
|
// Answer N is at index 1+2*(N-1) = 2N-1, Correct N at 2+2*(N-1) = 2N.
|
||||||
|
return [
|
||||||
|
'2 answers → 2 header pairs' => [2, 'Answer 2', 3, 'Correct', 4, 5],
|
||||||
|
'6 answers → 6 header pairs' => [6, 'Answer 6', 11, 'Correct', 12, 13],
|
||||||
|
'7 answers → 7 header pairs' => [7, 'Answer 7', 13, 'Correct', 14, 15],
|
||||||
|
'10 answers → 10 header pairs' => [10, 'Answer 10', 19, 'Correct', 20, 21],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('answerCountHeaderProvider')]
|
||||||
|
public function testQuizToXlsxHeaderCountMatchesAnswerCount(
|
||||||
|
int $answerCount,
|
||||||
|
string $lastAnswerHeader,
|
||||||
|
int $lastAnswerIndex,
|
||||||
|
string $lastCorrectHeader,
|
||||||
|
int $lastCorrectIndex,
|
||||||
|
int $absentIndex,
|
||||||
|
): void {
|
||||||
|
$path = $this->captureXlsx($this->subject->quizToXlsx($this->makeQuizWithAnswerCounts($answerCount)));
|
||||||
|
$headers = $this->readFirstRow($path);
|
||||||
|
|
||||||
|
$this->assertSame($lastAnswerHeader, $headers[$lastAnswerIndex]);
|
||||||
|
$this->assertSame($lastCorrectHeader, $headers[$lastCorrectIndex]);
|
||||||
|
$this->assertArrayNotHasKey($absentIndex, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testQuizToXlsxHeadersMatchMaxAnswersAcrossQuestions(): void
|
||||||
|
{
|
||||||
|
$quiz = new Quiz();
|
||||||
|
$quiz->addQuestion($this->makeQuestion('Short', 3));
|
||||||
|
$quiz->addQuestion($this->makeQuestion('Long', 7));
|
||||||
|
$quiz->addQuestion($this->makeQuestion('Medium', 5));
|
||||||
|
|
||||||
|
$path = $this->captureXlsx($this->subject->quizToXlsx($quiz));
|
||||||
|
$headers = $this->readFirstRow($path);
|
||||||
|
|
||||||
|
$this->assertSame('Answer 7', $headers[13]);
|
||||||
|
$this->assertSame('Correct', $headers[14]);
|
||||||
|
$this->assertArrayNotHasKey(15, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testQuizToXlsxRoundTripWithSevenAnswers(): void
|
||||||
|
{
|
||||||
|
$original = $this->makeQuizWithAnswerCounts(7);
|
||||||
|
$path = $this->captureXlsx($this->subject->quizToXlsx($original));
|
||||||
|
|
||||||
|
$imported = new Quiz();
|
||||||
|
$this->subject->xlsxToQuiz($imported, new File($path));
|
||||||
|
|
||||||
|
$this->assertCount(1, $imported->questions);
|
||||||
|
/** @var Question $question */
|
||||||
|
$question = $imported->questions->first();
|
||||||
|
$this->assertCount(7, $question->answers);
|
||||||
|
}
|
||||||
|
|
||||||
private function makeQuiz(): Quiz
|
private function makeQuiz(): Quiz
|
||||||
{
|
{
|
||||||
$quiz = new Quiz();
|
$quiz = new Quiz();
|
||||||
@@ -163,6 +224,36 @@ final class QuizSpreadsheetServiceTest extends TestCase
|
|||||||
return $quiz;
|
return $quiz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function makeQuizWithAnswerCounts(int ...$counts): Quiz
|
||||||
|
{
|
||||||
|
$quiz = new Quiz();
|
||||||
|
foreach ($counts as $i => $count) {
|
||||||
|
$quiz->addQuestion($this->makeQuestion("Question $i", $count));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $quiz;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeQuestion(string $text, int $answerCount): Question
|
||||||
|
{
|
||||||
|
$question = new Question();
|
||||||
|
$question->question = $text;
|
||||||
|
$question->ordering = 1;
|
||||||
|
for ($i = 1; $i <= $answerCount; ++$i) {
|
||||||
|
$question->addAnswer(new Answer("Answer $i", isRightAnswer: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $question;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, string|null> */
|
||||||
|
private function readFirstRow(string $path): array
|
||||||
|
{
|
||||||
|
$rows = (new Reader\Xlsx())->setReadDataOnly(true)->load($path)->getActiveSheet()->toArray(formatData: false);
|
||||||
|
|
||||||
|
return $rows[0] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
private function captureXlsx(\Closure $closure): string
|
private function captureXlsx(\Closure $closure): string
|
||||||
{
|
{
|
||||||
$path = $this->createTempPath('.xlsx');
|
$path = $this->createTempPath('.xlsx');
|
||||||
|
|||||||
Reference in New Issue
Block a user