@@ -362,6 +362,28 @@ PAG_TEST(PAGXHTMLImporterTest, BackgroundShorthandColorPlusGradient) {
362362 EXPECT_TRUE (ColorNear (lg->colorStops .front ()->color , HexColor (0xFF0000 )));
363363}
364364
365+ // PAGX represents a URL background with one ImagePattern. When a shorthand contains multiple
366+ // image layers, retain the CSS-topmost layer and the fitting properties declared on that same
367+ // layer instead of pairing the image with a lower layer's size/repeat/position.
368+ PAG_TEST (PAGXHTMLImporterTest, BackgroundShorthandKeepsTopImageFittingTogether) {
369+ auto doc = ParseFromString (R"HTML(
370+ <html><body style="width:160px;height:120px">
371+ <div style="width:160px;height:120px;
372+ background:url(top.png) center / contain no-repeat,
373+ url(bottom.png) left top / cover repeat"></div>
374+ </body></html>
375+ )HTML" );
376+ ASSERT_NE (doc, nullptr );
377+ auto * layer = doc->layers .front ()->children .front ();
378+ auto * fill = FindElementOfType<pagx::Fill>(layer);
379+ ASSERT_NE (fill, nullptr );
380+ auto * pattern = As<pagx::ImagePattern>(fill->color );
381+ ASSERT_NE (pattern, nullptr );
382+ ASSERT_NE (pattern->image , nullptr );
383+ EXPECT_EQ (pattern->image ->filePath , " top.png" );
384+ EXPECT_EQ (pattern->scaleMode , pagx::ScaleMode::LetterBox);
385+ }
386+
365387// A higher-priority shorthand resets every background longhand supplied by a lower-priority rule.
366388// In particular, an inline colour-only background must clear a class-provided image.
367389PAG_TEST (PAGXHTMLImporterTest, BackgroundShorthandResetsLowerPriorityLonghands) {
@@ -1277,14 +1299,10 @@ PAG_TEST(PAGXHTMLImporterTest, WhiteSpacePreKeepsSpacesVerbatim) {
12771299 " </body></html>" );
12781300 ASSERT_NE (doc, nullptr );
12791301 auto * leaf = doc->layers .front ()->children .front ();
1302+ EXPECT_EQ (FindElementOfType<pagx::TextBox>(leaf), nullptr );
12801303 std::vector<pagx::Text*> texts;
12811304 std::vector<pagx::Fill*> fills;
1282- if (auto * tb = FindElementOfType<pagx::TextBox>(leaf)) {
1283- EXPECT_FALSE (tb->wordWrap );
1284- GatherTextRuns (tb->elements , &texts, &fills);
1285- } else {
1286- GatherTextRuns (leaf->contents , &texts, &fills);
1287- }
1305+ GatherTextRuns (leaf->contents , &texts, &fills);
12881306 ASSERT_FALSE (texts.empty ());
12891307 const std::string& t = texts.front ()->text ;
12901308 EXPECT_NE (t.find (" A B" ), std::string::npos);
@@ -3146,6 +3164,30 @@ PAG_TEST(PAGXHTMLImporterTest, DataStarAttributesPropagateOnFoldedRoundedImage)
31463164 EXPECT_EQ (wrapper->customData [" id" ], " avatar" );
31473165}
31483166
3167+ // The folded <img> is the more specific payload node, so its data value wins over the structural
3168+ // wrapper's value. The lossy collision is surfaced as a diagnostic rather than silently ignored.
3169+ PAG_TEST (PAGXHTMLImporterTest, FoldedImageDataConflictWarnsAndImageWins) {
3170+ auto doc = ParseFromString (R"HTML(
3171+ <html><body style="width:64px;height:64px">
3172+ <div data-id="wrapper" style="width:64px;height:64px;border-radius:9999px;overflow:hidden">
3173+ <img src="avatar.png" data-id="image" style="position:absolute;left:0;top:0;width:64px;height:64px"/>
3174+ </div>
3175+ </body></html>
3176+ )HTML" );
3177+ ASSERT_NE (doc, nullptr );
3178+ auto * wrapper = doc->layers .front ()->children .front ();
3179+ EXPECT_EQ (wrapper->customData [" id" ], " image" );
3180+ bool foundConflict = false ;
3181+ for (const auto & error : doc->errors ) {
3182+ if (error.find (" data-id" ) != std::string::npos &&
3183+ error.find (" overrides" ) != std::string::npos) {
3184+ foundConflict = true ;
3185+ break ;
3186+ }
3187+ }
3188+ EXPECT_TRUE (foundConflict);
3189+ }
3190+
31493191PAG_TEST (PAGXHTMLImporterTest, IdAttributePropagatesToLayer) {
31503192 auto doc = ParseFromString (R"HTML(
31513193 <html><body style="width:50px;height:50px">
@@ -3324,6 +3366,23 @@ PAG_TEST(PAGXHTMLImporterTest, BackgroundImageSizeContainMapsToLetterBox) {
33243366 EXPECT_EQ (pattern->scaleMode , pagx::ScaleMode::LetterBox);
33253367}
33263368
3369+ // html-snapshot escapes apostrophes inside its single-quoted CSS url(). The importer removes that
3370+ // simple CSS escape so local paths keep their actual filename.
3371+ PAG_TEST (PAGXHTMLImporterTest, BackgroundImageUnescapesQuotedURL) {
3372+ auto doc = ParseFromString (R"HTML(
3373+ <html><body style="width:160px;height:120px">
3374+ <div style="width:160px;height:120px;background-image:url('/tmp/b\'s.png')"></div>
3375+ </body></html>
3376+ )HTML" );
3377+ ASSERT_NE (doc, nullptr );
3378+ auto * fill = FindElementOfType<pagx::Fill>(doc->layers .front ()->children .front ());
3379+ ASSERT_NE (fill, nullptr );
3380+ auto * pattern = As<pagx::ImagePattern>(fill->color );
3381+ ASSERT_NE (pattern, nullptr );
3382+ ASSERT_NE (pattern->image , nullptr );
3383+ EXPECT_EQ (pattern->image ->filePath , " /tmp/b's.png" );
3384+ }
3385+
33273386// `background-size: cover` → Zoom.
33283387PAG_TEST (PAGXHTMLImporterTest, BackgroundImageSizeCoverMapsToZoom) {
33293388 auto doc = ParseFromString (R"HTML(
0 commit comments