Skip to content

Commit baad47c

Browse files
committed
test: add comprehensive error and edge case tests for SvgPdfXObjectFactory
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 229195f commit baad47c

2 files changed

Lines changed: 162 additions & 0 deletions

File tree

tests/Unit/Pdf/Svg/SvgColorResolverTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ public static function provideExtractValueFromStyleAttributeScenarios(): iterabl
131131
'expected' => 'url(http://example.com/a:b.svg)',
132132
];
133133

134+
yield 'skip blank declarations before valid property' => [
135+
'style' => ' ; ; fill : red ; stroke:#000',
136+
'property' => 'fill',
137+
'expected' => 'red',
138+
'useColorExtractor' => true,
139+
];
140+
134141
yield 'empty style returns null' => [
135142
'style' => '',
136143
'property' => 'fill',
@@ -235,11 +242,29 @@ public static function provideResolveFillColorClassExtractionScenarios(): iterab
235242
'expected' => '#111111',
236243
];
237244

245+
yield 'blank class tokens do not win over real class' => [
246+
'attributes' => ['class' => ' primary '],
247+
'classColors' => [
248+
'' => '#999999',
249+
'primary' => '#111111',
250+
],
251+
'expected' => '#111111',
252+
];
253+
238254
yield 'empty class falls back to default fill' => [
239255
'attributes' => ['class' => ''],
240256
'classColors' => ['primary' => '#ff0000'],
241257
'expected' => '#000000',
242258
];
259+
260+
yield 'whitespace only class falls back to default fill' => [
261+
'attributes' => ['class' => ' '],
262+
'classColors' => [
263+
'' => '#999999',
264+
'primary' => '#ff0000',
265+
],
266+
'expected' => '#000000',
267+
];
243268
}
244269

245270
#[DataProvider('provideNormalizeColorRgbValidationScenarios')]
@@ -259,7 +284,10 @@ public static function provideNormalizeColorRgbValidationScenarios(): iterable
259284
yield 'accepts rgb channels with spaces' => ['input' => 'rgb( 255 , 0 , 0 )', 'expected' => '#ff0000'];
260285
yield 'rejects non numeric channels' => ['input' => 'rgb(25a, 0, 0)', 'expected' => null];
261286
yield 'rejects negative channels' => ['input' => 'rgb(255, -1, 0)', 'expected' => null];
287+
yield 'rejects explicit positive sign' => ['input' => 'rgb(+12, 0, 0)', 'expected' => null];
288+
yield 'rejects empty channel' => ['input' => 'rgb(, 0, 0)', 'expected' => null];
262289
yield 'clamps overflowing channels' => ['input' => 'rgb(256, 0, 0)', 'expected' => '#ff0000'];
290+
yield 'rejects integer overflow channel' => ['input' => 'rgb(999999999999999999999999, 0, 0)', 'expected' => null];
263291
yield 'rejects alphanumeric suffix' => ['input' => 'rgb(123abc, 0, 0)', 'expected' => null];
264292
yield 'rejects alphanumeric prefix' => ['input' => 'rgb(abc123, 0, 0)', 'expected' => null];
265293
yield 'rejects embedded spaces inside channel digits' => ['input' => 'rgb(12 3, 0, 0)', 'expected' => null];
@@ -284,6 +312,10 @@ public static function provideExtractColorFromStyleScenarios(): iterable
284312
'style' => 'fill: red; stroke: blue; opacity: 0.5;',
285313
'expected' => 'red',
286314
];
315+
yield 'extract from style with blank declarations' => [
316+
'style' => ' ; ; fill: red ; stroke: blue;',
317+
'expected' => 'red',
318+
];
287319
yield 'empty style returns null' => ['style' => '', 'expected' => null];
288320
yield 'whitespace style returns null' => ['style' => ' ', 'expected' => null];
289321
}

tests/Unit/Pdf/Svg/SvgPdfXObjectFactoryTest.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,136 @@ public function testCreateSkipsUnrecognizedShapeElementsGracefully(): void
400400
self::assertStringContainsString('1 0 0 rg', $xObject->stream);
401401
}
402402

403+
public function testCreateWithEmptySvgStringThrows(): void
404+
{
405+
$factory = new SvgPdfXObjectFactory();
406+
407+
$this->expectException(InvalidArgumentException::class);
408+
$this->expectExceptionMessage('Unable to parse SVG source "/tmp/empty.svg".');
409+
410+
$factory->create('', '/tmp/empty.svg');
411+
}
412+
413+
public function testCreateWithNonSvgRootElementThrows(): void
414+
{
415+
$factory = new SvgPdfXObjectFactory();
416+
417+
$this->expectException(InvalidArgumentException::class);
418+
$this->expectExceptionMessage('Unable to parse SVG source "/tmp/wrong-root.svg".');
419+
420+
$factory->create('<?xml version="1.0"?><root></root>', '/tmp/wrong-root.svg');
421+
}
422+
423+
public function testCreateWithMissingDimensionsOrViewBoxThrows(): void
424+
{
425+
$factory = new SvgPdfXObjectFactory();
426+
427+
$this->expectException(InvalidArgumentException::class);
428+
$this->expectExceptionMessage(
429+
'SVG source "/tmp/no-viewport.svg" must define either a valid viewBox or positive width/height.',
430+
);
431+
432+
$factory->create(
433+
'<svg xmlns="http://www.w3.org/2000/svg"><path d="M0,0"/></svg>',
434+
'/tmp/no-viewport.svg',
435+
);
436+
}
437+
438+
public function testCreateWithZeroDimensionsThrows(): void
439+
{
440+
$factory = new SvgPdfXObjectFactory();
441+
442+
$this->expectException(InvalidArgumentException::class);
443+
$this->expectExceptionMessage(
444+
'SVG source "/tmp/zero-dims.svg" must define a positive viewBox.',
445+
);
446+
447+
$factory->create(
448+
'<svg viewBox="0 0 0 0" xmlns="http://www.w3.org/2000/svg"><path d="M0,0"/></svg>',
449+
'/tmp/zero-dims.svg',
450+
);
451+
}
452+
453+
public function testCreateWithViewBoxButNegativeHeightThrows(): void
454+
{
455+
$factory = new SvgPdfXObjectFactory();
456+
457+
$this->expectException(InvalidArgumentException::class);
458+
$this->expectExceptionMessage(
459+
'SVG source "/tmp/negative-height.svg" must define a positive viewBox.',
460+
);
461+
462+
$factory->create(
463+
'<svg viewBox="0 0 10 -5" xmlns="http://www.w3.org/2000/svg"><path d="M0,0"/></svg>',
464+
'/tmp/negative-height.svg',
465+
);
466+
}
467+
468+
public function testCreateWithLeadingSignInNumericDimension(): void
469+
{
470+
$factory = new SvgPdfXObjectFactory();
471+
472+
$xObject = $factory->create(
473+
<<<'SVG'
474+
<svg width="+100" height="-50" viewBox="+0 +0 +100 +50" xmlns="http://www.w3.org/2000/svg">
475+
<rect x="0" y="0" width="100" height="50" fill="#000000"/>
476+
</svg>
477+
SVG,
478+
'/tmp/signed-dims.svg',
479+
);
480+
481+
self::assertSame([0.0, 0.0, 100.0, 50.0], $xObject->dictionary['BBox']);
482+
}
483+
484+
public function testCreateWithFractionalDimensions(): void
485+
{
486+
$factory = new SvgPdfXObjectFactory();
487+
488+
$xObject = $factory->create(
489+
<<<'SVG'
490+
<svg width="12.5" height="7.25" xmlns="http://www.w3.org/2000/svg">
491+
<rect x="0" y="0" width="12.5" height="7.25" fill="#000000"/>
492+
</svg>
493+
SVG,
494+
'/tmp/fractional-dims.svg',
495+
);
496+
497+
self::assertSame([0.0, 0.0, 12.5, 7.25], $xObject->dictionary['BBox']);
498+
}
499+
500+
public function testCreateWithStrokeWidthFromStyleAttribute(): void
501+
{
502+
$factory = new SvgPdfXObjectFactory();
503+
504+
$xObject = $factory->create(
505+
<<<'SVG'
506+
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg">
507+
<path d="M0,0 L10,10" stroke="#000000" style="stroke-width:2.5"/>
508+
</svg>
509+
SVG,
510+
'/tmp/style-stroke.svg',
511+
);
512+
513+
self::assertStringContainsString('2.500000 w', $xObject->stream);
514+
}
515+
516+
public function testCreateWithNegativeStrokeWidthClamped(): void
517+
{
518+
$factory = new SvgPdfXObjectFactory();
519+
520+
$xObject = $factory->create(
521+
<<<'SVG'
522+
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg">
523+
<path d="M0,0 L10,10" stroke="#000000" stroke-width="-5"/>
524+
</svg>
525+
SVG,
526+
'/tmp/negative-stroke.svg',
527+
);
528+
529+
// Negative stroke width should be clamped to 0.0
530+
self::assertStringContainsString('0.000000 w', $xObject->stream);
531+
}
532+
403533
public static function providePaintModeScenarios(): iterable
404534
{
405535
yield 'stroke only path without fill' => [

0 commit comments

Comments
 (0)