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 @@
+
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.