diff --git a/components/Book/expanding-side-img.scss b/components/Book/expanding-side-img.scss index 2e8d423..2702548 100644 --- a/components/Book/expanding-side-img.scss +++ b/components/Book/expanding-side-img.scss @@ -217,20 +217,27 @@ div.expanding-side-img-container { display: none !important; } - .expanding-side-img { - img { - width: 100% - } - &:has(.caption) { - text-align: center; + div.expanding-side-img { + position: static; + width: 100%; + max-width: 100%; + transform: none; - &::after { - display: none; - } + img { + width: 100%; + height: auto; + } - img { - margin-bottom: 0; - } + &:has(.caption) { + text-align: center; + + &::after { + display: none; + } + + img { + margin-bottom: 0; + } .caption, .children { font-weight: 400 !important; diff --git a/e2e/book-page.spec.ts b/e2e/book-page.spec.ts index 4bf623b..8265e0a 100644 --- a/e2e/book-page.spec.ts +++ b/e2e/book-page.spec.ts @@ -40,6 +40,14 @@ async function paintedLineCount(locator: Locator) { }); } +const SIDE_IMAGE_ALT = "Synthetic side image regression fixture"; +const SIDE_IMAGE_CAPTION = "Side image test fixture."; +const SIDE_IMAGE_CHILDREN = + "This repository-owned image exists only to verify side-image rendering."; +const FOLLOWING_CONTENT = + "The side image should remain visible alongside this ordinary chapter content."; +const GEOMETRY_TOLERANCE = 1; + test("renders the representative book page", async ({ page }, testInfo) => { await page.goto(BOOK_PATH); @@ -174,3 +182,186 @@ test("renders the representative book page", async ({ page }, testInfo) => { ); expect(horizontalOverflow).toBe(false); }); + +test("renders side images on narrow screens", async ({ page }, testInfo) => { + await page.goto(BOOK_PATH); + + const sideImage = page + .locator(".expanding-side-img") + .getByRole("img", { name: SIDE_IMAGE_ALT }); + const wrapper = sideImage.locator(".."); + const container = wrapper.locator(".."); + const caption = wrapper.getByText(SIDE_IMAGE_CAPTION, { exact: true }); + const children = wrapper.getByText(SIDE_IMAGE_CHILDREN, { exact: true }); + const followingContent = page.getByText(FOLLOWING_CONTENT, { exact: true }); + + await expect(container).toBeAttached(); + await expect(wrapper).toBeAttached(); + await expect(sideImage).toBeAttached(); + await expect(sideImage).toHaveJSProperty("complete", true); + await container.scrollIntoViewIfNeeded(); + + const [containerBox, wrapperBox, imageBox, followingContentBox] = + await Promise.all([ + container.boundingBox(), + wrapper.boundingBox(), + sideImage.boundingBox(), + followingContent.boundingBox(), + ]); + const withEdges = ( + box: { x: number; y: number; width: number; height: number } | null + ) => + box && { + ...box, + top: box.y, + right: box.x + box.width, + bottom: box.y + box.height, + left: box.x, + }; + const containerBounds = withEdges(containerBox); + const wrapperBounds = withEdges(wrapperBox); + const imageBounds = withEdges(imageBox); + const followingContentBounds = withEdges(followingContentBox); + const elementDetails = await sideImage.evaluate((img: HTMLImageElement) => { + const wrapperStyle = getComputedStyle(img.parentElement!); + const imageStyle = getComputedStyle(img); + const documentElement = document.documentElement; + const body = document.body; + + return { + wrapper: { + position: wrapperStyle.position, + transform: wrapperStyle.transform, + }, + image: { + position: imageStyle.position, + transform: imageStyle.transform, + complete: img.complete, + naturalWidth: img.naturalWidth, + naturalHeight: img.naturalHeight, + src: img.currentSrc || img.src, + }, + horizontalOverflow: + documentElement.scrollWidth > documentElement.clientWidth || + body.scrollWidth > body.clientWidth, + }; + }); + const viewport = page.viewportSize()!; + const visibility = { + image: await sideImage.isVisible(), + caption: await caption.isVisible(), + children: await children.isVisible(), + followingContent: await followingContent.isVisible(), + }; + const intersectsViewport = Boolean( + imageBounds && + imageBounds.width > 0 && + imageBounds.height > 0 && + imageBounds.x + imageBounds.width > 0 && + imageBounds.y + imageBounds.height > 0 && + imageBounds.x < viewport.width && + imageBounds.y < viewport.height + ); + const diagnostics = { + viewport, + containerBounds, + wrapper: { bounds: wrapperBounds, ...elementDetails.wrapper }, + img: { bounds: imageBounds, ...elementDetails.image }, + followingContentBounds, + intersectsViewport, + horizontalOverflow: elementDetails.horizontalOverflow, + visibility, + }; + + const artifactPrefix = + testInfo.project.name === MOBILE_PROJECT ? "mobile" : "desktop"; + const diagnosticsPath = testInfo.outputPath( + `${artifactPrefix}-side-image-diagnostics.json` + ); + await fs.writeFile( + diagnosticsPath, + JSON.stringify(diagnostics, null, 2) + ); + await testInfo.attach(`${artifactPrefix}-side-image-diagnostics`, { + path: diagnosticsPath, + contentType: "application/json", + }); + + const fullPagePath = testInfo.outputPath( + `${artifactPrefix}-side-image-full-page.png` + ); + await page.screenshot({ path: fullPagePath, fullPage: true }); + await testInfo.attach(`${artifactPrefix}-side-image-full-page`, { + path: fullPagePath, + contentType: "image/png", + }); + + await page + .getByRole("heading", { name: "Side image fixture" }) + .scrollIntoViewIfNeeded(); + const regionPath = testInfo.outputPath( + `${artifactPrefix}-side-image-region.png` + ); + await page.screenshot({ path: regionPath }); + await testInfo.attach(`${artifactPrefix}-side-image-region`, { + path: regionPath, + contentType: "image/png", + }); + + expect( + diagnostics.img.naturalWidth, + "Expected the side-image asset to load" + ).toBeGreaterThan(0); + expect(diagnostics.img.naturalHeight).toBeGreaterThan(0); + expect(diagnostics.wrapper.bounds?.width ?? 0).toBeGreaterThan(0); + expect(diagnostics.wrapper.bounds?.height ?? 0).toBeGreaterThan(0); + expect(diagnostics.img.bounds.width).toBeGreaterThan(0); + expect(diagnostics.img.bounds.height).toBeGreaterThan(0); + await expect( + sideImage, + "Expected the side image to be visible on mobile and desktop" + ).toBeVisible(); + + if (testInfo.project.name !== MOBILE_PROJECT) { + expect( + diagnostics.wrapper.bounds!.right, + "Expected the desktop image to remain in the side column" + ).toBeLessThanOrEqual( + diagnostics.containerBounds!.left + GEOMETRY_TOLERANCE + ); + expect(diagnostics.wrapper.bounds!.width).toBeLessThan( + diagnostics.containerBounds!.width + ); + return; + } + + await expect(caption).toBeVisible(); + await expect(children).toBeVisible(); + await expect(followingContent).toBeVisible(); + expect(diagnostics.intersectsViewport).toBe(true); + expect(diagnostics.wrapper.bounds!.left).toBeGreaterThanOrEqual( + -GEOMETRY_TOLERANCE + ); + expect(diagnostics.wrapper.bounds!.right).toBeLessThanOrEqual( + diagnostics.viewport.width + GEOMETRY_TOLERANCE + ); + expect(diagnostics.wrapper.bounds!.left).toBeGreaterThanOrEqual( + diagnostics.containerBounds!.left - GEOMETRY_TOLERANCE + ); + expect(diagnostics.wrapper.bounds!.right).toBeLessThanOrEqual( + diagnostics.containerBounds!.right + GEOMETRY_TOLERANCE + ); + expect(diagnostics.img.bounds.left).toBeGreaterThanOrEqual( + diagnostics.wrapper.bounds!.left - GEOMETRY_TOLERANCE + ); + expect(diagnostics.img.bounds.right).toBeLessThanOrEqual( + diagnostics.wrapper.bounds!.right + GEOMETRY_TOLERANCE + ); + expect( + diagnostics.followingContentBounds!.top, + "Expected following content to start below the inline side image" + ).toBeGreaterThanOrEqual( + diagnostics.wrapper.bounds!.bottom - GEOMETRY_TOLERANCE + ); + expect(diagnostics.horizontalOverflow).toBe(false); +}); diff --git a/e2e/fixtures/notes/mobile-baseline/chapter-one/images/side-image-test.svg b/e2e/fixtures/notes/mobile-baseline/chapter-one/images/side-image-test.svg new file mode 100644 index 0000000..c8876e0 --- /dev/null +++ b/e2e/fixtures/notes/mobile-baseline/chapter-one/images/side-image-test.svg @@ -0,0 +1,7 @@ + + + + + SIDE IMAGE TEST + + diff --git a/e2e/fixtures/notes/mobile-baseline/chapter-one/index.md b/e2e/fixtures/notes/mobile-baseline/chapter-one/index.md index 37c35ca..171a5f8 100644 --- a/e2e/fixtures/notes/mobile-baseline/chapter-one/index.md +++ b/e2e/fixtures/notes/mobile-baseline/chapter-one/index.md @@ -8,3 +8,17 @@ It includes a short list: - desktop Chromium renders this public book page; - mobile WebKit renders the same content through Playwright's iPhone device emulation. + +## Side image fixture + +This synthetic example exercises the same expandable side-image component used by Notes content. + + +This repository-owned image exists only to verify side-image rendering. + + +The side image should remain visible alongside this ordinary chapter content.