From c3d3d95777a987631957f76ef0fc26633ff0d32e Mon Sep 17 00:00:00 2001 From: lxbme <18108274905@163.com> Date: Tue, 4 Aug 2026 14:23:16 +0800 Subject: [PATCH 1/5] test(css): assert body text keeps its padding and the mobile nav precedes the article --- next/tests/e2e/docs-mobile-layout.spec.mjs | 99 ++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 next/tests/e2e/docs-mobile-layout.spec.mjs diff --git a/next/tests/e2e/docs-mobile-layout.spec.mjs b/next/tests/e2e/docs-mobile-layout.spec.mjs new file mode 100644 index 0000000000000..8c521bb66a5e4 --- /dev/null +++ b/next/tests/e2e/docs-mobile-layout.spec.mjs @@ -0,0 +1,99 @@ +import { expect, test } from '@playwright/test'; + +/** Computed horizontal padding of the first element matching `selector`. */ +async function inlinePadding(page, selector) { + return page.locator(selector).first().evaluate((el) => { + const cs = getComputedStyle(el); + return { left: parseFloat(cs.paddingLeft), right: parseFloat(cs.paddingRight) }; + }); +} + +/** + * Discover a post from the blog index rather than naming one. A hardcoded URL + * turns an unrelated content change into a broken test; the index is the same + * thing a reader would follow. `/blog/20…` matches dated post URLs only — + * `/blog/page/`, `/blog/archive/` and `/blog/tags/` do not start that way. + */ +async function firstBlogPost(page) { + await page.goto('/blog/'); + const href = await page.locator('a[href^="/blog/20"]').first().getAttribute('href'); + expect(href, 'the blog index must list at least one post').toBeTruthy(); + return href; +} + +// `/edit/` is the one .article-wrap page present in every build, including the +// content-less local one, so this check needs no gate and runs everywhere. +test('the edit page keeps its horizontal padding', async ({ page }) => { + await page.goto('/edit/'); + const pad = await inlinePadding(page, '.article-wrap'); + expect(pad.left, '.article-wrap must not zero out .container padding').toBeGreaterThan(0); + expect(pad.right).toBeGreaterThan(0); +}); + +// Docs and blog article pages only exist in the assembled tree, so the rest is +// gated the same way main-pages.spec.mjs:141 gates its overlay checks. +test('docs pages keep horizontal padding and put the nav above the article', async ({ page }) => { + test.skip( + process.env.EXPECT_DOCUSARUS_ROUTES !== 'true', + 'Docs pages only exist in the final overlaid tree', + ); + + await page.goto('/docs/apisix/getting-started/README/'); + + const pad = await inlinePadding(page, '.docs-layout'); + expect(pad.left, '.docs-layout must not zero out .container padding').toBeGreaterThan(0); + expect(pad.right).toBeGreaterThan(0); + + // The header was always correct, so it is the reference the article should + // match. Only meaningful below the 1140px container cap, where both are full + // width; above it the centred container makes the comparison meaningless. + const geom = await page.evaluate(() => ({ + viewport: window.innerWidth, + h1Left: document.querySelector('.docs-content h1').getBoundingClientRect().left, + brandLeft: document.querySelector('.site-header .brand').getBoundingClientRect().left, + navTop: document.querySelector('.docs-sidebar').offsetTop, + articleTop: document.querySelector('.docs-content').offsetTop, + })); + + expect(geom.h1Left, 'article text must not touch the viewport edge').toBeGreaterThan(0); + if (geom.viewport <= 1140) { + expect(Math.abs(geom.h1Left - geom.brandLeft), + 'article should line up with the header brand').toBeLessThanOrEqual(1); + } + expect(geom.navTop, 'the docs nav must come before the article').toBeLessThan(geom.articleTop); +}); + +test('blog posts keep their horizontal padding', async ({ page }) => { + test.skip( + process.env.EXPECT_DOCUSARUS_ROUTES !== 'true', + 'Blog posts only exist in the final overlaid tree', + ); + + await page.goto(await firstBlogPost(page)); + const pad = await inlinePadding(page, '.article-wrap'); + expect(pad.left, 'blog posts share the .article-wrap defect').toBeGreaterThan(0); + expect(pad.right).toBeGreaterThan(0); +}); + +test('desktop keeps the three-column article rails', async ({ page }) => { + test.skip( + process.env.EXPECT_DOCUSARUS_ROUTES !== 'true', + 'Blog posts only exist in the final overlaid tree', + ); + test.skip(test.info().project.name !== 'desktop-chrome', 'Rails only exist at >=1240px'); + + await page.goto(await firstBlogPost(page)); + + // Assert the rails exist before measuring them, so a post that legitimately + // has none fails loudly here instead of silently passing a vacuous check. + const rails = page.locator('.article-wrap.with-rails'); + await expect(rails, 'the discovered post should render the rails layout').toHaveCount(1); + + // Restoring the inline padding narrows the content box by 2.5rem. The rails + // grid needs 1276px of a 1340px cap, so this is the assertion that catches a + // miscalculation squeezing it from three columns down to fewer. + const columns = await rails.evaluate( + (el) => getComputedStyle(el).gridTemplateColumns.split(/\s+/).filter(Boolean).length, + ); + expect(columns, 'the rails grid must stay three columns').toBe(3); +}); From c604d403d7b35833b7ab6e085e900fa598fe9bfc Mon Sep 17 00:00:00 2001 From: lxbme <18108274905@163.com> Date: Tue, 4 Aug 2026 14:31:40 +0800 Subject: [PATCH 2/5] test(css): cover docs layout in PR CI and drop the untestable edit case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /edit/ is not in deploy.yml's overlay allowlist, so production serves the Docusaurus build of it and there is no .article-wrap to measure — that test timed out rather than asserting anything. docs/general/** ships from this repo, so it is present once sync-content.mjs has run, which is what lint.yml does. Asserting there needs no gate and moves verification of this fix into PR CI. The apisix tree still needs .sync/ checkouts only the deploy pipeline has, so that case keeps its gate and now shares one assertDocsLayout helper with the general case. --- next/tests/e2e/docs-mobile-layout.spec.mjs | 49 ++++++++++------------ 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/next/tests/e2e/docs-mobile-layout.spec.mjs b/next/tests/e2e/docs-mobile-layout.spec.mjs index 8c521bb66a5e4..a7f19f158d9ee 100644 --- a/next/tests/e2e/docs-mobile-layout.spec.mjs +++ b/next/tests/e2e/docs-mobile-layout.spec.mjs @@ -21,27 +21,12 @@ async function firstBlogPost(page) { return href; } -// `/edit/` is the one .article-wrap page present in every build, including the -// content-less local one, so this check needs no gate and runs everywhere. -test('the edit page keeps its horizontal padding', async ({ page }) => { - await page.goto('/edit/'); - const pad = await inlinePadding(page, '.article-wrap'); - expect(pad.left, '.article-wrap must not zero out .container padding').toBeGreaterThan(0); - expect(pad.right).toBeGreaterThan(0); -}); - -// Docs and blog article pages only exist in the assembled tree, so the rest is -// gated the same way main-pages.spec.mjs:141 gates its overlay checks. -test('docs pages keep horizontal padding and put the nav above the article', async ({ page }) => { - test.skip( - process.env.EXPECT_DOCUSARUS_ROUTES !== 'true', - 'Docs pages only exist in the final overlaid tree', - ); - - await page.goto('/docs/apisix/getting-started/README/'); +/** Padding restored and nav ahead of the article, for any docs page. */ +async function assertDocsLayout(page, url) { + await page.goto(url); const pad = await inlinePadding(page, '.docs-layout'); - expect(pad.left, '.docs-layout must not zero out .container padding').toBeGreaterThan(0); + expect(pad.left, `${url}: .docs-layout must not zero out .container padding`).toBeGreaterThan(0); expect(pad.right).toBeGreaterThan(0); // The header was always correct, so it is the reference the article should @@ -55,20 +40,32 @@ test('docs pages keep horizontal padding and put the nav above the article', asy articleTop: document.querySelector('.docs-content').offsetTop, })); - expect(geom.h1Left, 'article text must not touch the viewport edge').toBeGreaterThan(0); + expect(geom.h1Left, `${url}: article text must not touch the viewport edge`).toBeGreaterThan(0); if (geom.viewport <= 1140) { expect(Math.abs(geom.h1Left - geom.brandLeft), - 'article should line up with the header brand').toBeLessThanOrEqual(1); + `${url}: article should line up with the header brand`).toBeLessThanOrEqual(1); } - expect(geom.navTop, 'the docs nav must come before the article').toBeLessThan(geom.articleTop); + expect(geom.navTop, `${url}: the docs nav must come before the article`) + .toBeLessThan(geom.articleTop); +} + +// docs/general/** ships from this repo, so it exists in the PR CI build too — +// no gate, and the fix is verified before anything is deployed. +test('general docs keep padding and put the nav above the article', async ({ page }) => { + await assertDocsLayout(page, '/docs/general/contributor-guide/'); }); -test('blog posts keep their horizontal padding', async ({ page }) => { +// Same assertions over the 200-link apisix tree that motivated the report. +// Gated: apisix docs need .sync/ checkouts only the deploy pipeline has. +test('apisix docs keep padding and put the nav above the article', async ({ page }) => { test.skip( process.env.EXPECT_DOCUSARUS_ROUTES !== 'true', - 'Blog posts only exist in the final overlaid tree', + 'apisix docs only exist in the final overlaid tree', ); + await assertDocsLayout(page, '/docs/apisix/getting-started/README/'); +}); +test('blog posts keep their horizontal padding', async ({ page }) => { await page.goto(await firstBlogPost(page)); const pad = await inlinePadding(page, '.article-wrap'); expect(pad.left, 'blog posts share the .article-wrap defect').toBeGreaterThan(0); @@ -76,10 +73,6 @@ test('blog posts keep their horizontal padding', async ({ page }) => { }); test('desktop keeps the three-column article rails', async ({ page }) => { - test.skip( - process.env.EXPECT_DOCUSARUS_ROUTES !== 'true', - 'Blog posts only exist in the final overlaid tree', - ); test.skip(test.info().project.name !== 'desktop-chrome', 'Rails only exist at >=1240px'); await page.goto(await firstBlogPost(page)); From f32f8111b9cd3f0cb2e9bce4b2e79bce205acf9c Mon Sep 17 00:00:00 2001 From: lxbme <18108274905@163.com> Date: Tue, 4 Aug 2026 14:53:24 +0800 Subject: [PATCH 3/5] test(css): scope layout assertions to the breakpoint they describe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Above 960px the nav and the article are grid items on the same row, so their offsetTop is equal and `toBeLessThan` can never hold — the suite would have stayed red on desktop even after a correct fix. The order assertion is now scoped to the stacked layout, and the desktop side asserts equality instead, which catches `order` escaping the media query. The alignment check used 1140px, the container cap, rather than 960px, where the layout actually collapses. The rails check counted grid tracks, which an explicit template always reports as three however narrow the container gets — a wrapper forced to 600px still reports three, as "190px 44px 230px". It now asserts the reading column keeps a usable width, which is what restoring the inline padding could threaten. --- next/tests/e2e/docs-mobile-layout.spec.mjs | 43 +++++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/next/tests/e2e/docs-mobile-layout.spec.mjs b/next/tests/e2e/docs-mobile-layout.spec.mjs index a7f19f158d9ee..bf443825e508e 100644 --- a/next/tests/e2e/docs-mobile-layout.spec.mjs +++ b/next/tests/e2e/docs-mobile-layout.spec.mjs @@ -41,12 +41,27 @@ async function assertDocsLayout(page, url) { })); expect(geom.h1Left, `${url}: article text must not touch the viewport edge`).toBeGreaterThan(0); - if (geom.viewport <= 1140) { + + // Both remaining checks are breakpoint-dependent, and 960px is the line + // (global.css:340) where .docs-layout collapses to one column. + if (geom.viewport <= 960) { + // Stacked: the article shares the container's inline padding with the + // header, so their left edges line up. expect(Math.abs(geom.h1Left - geom.brandLeft), `${url}: article should line up with the header brand`).toBeLessThanOrEqual(1); + // Stacked: the nav must precede the article. This is the defect — `order: 2` + // used to push it below. + expect(geom.navTop, `${url}: the docs nav must come before the article`) + .toBeLessThan(geom.articleTop); + } else { + // Side by side: nav and article are grid items on the same row, so their + // offsetTop is EQUAL. Measured on production at 1440px: both 132. + // Asserting `toBeLessThan` here would be unsatisfiable — and asserting + // equality is the guard that catches `order` leaking out of the media + // query and stacking the desktop layout. + expect(geom.navTop, `${url}: nav and article should share a grid row`) + .toBe(geom.articleTop); } - expect(geom.navTop, `${url}: the docs nav must come before the article`) - .toBeLessThan(geom.articleTop); } // docs/general/** ships from this repo, so it exists in the PR CI build too — @@ -82,11 +97,19 @@ test('desktop keeps the three-column article rails', async ({ page }) => { const rails = page.locator('.article-wrap.with-rails'); await expect(rails, 'the discovered post should render the rails layout').toHaveCount(1); - // Restoring the inline padding narrows the content box by 2.5rem. The rails - // grid needs 1276px of a 1340px cap, so this is the assertion that catches a - // miscalculation squeezing it from three columns down to fewer. - const columns = await rails.evaluate( - (el) => getComputedStyle(el).gridTemplateColumns.split(/\s+/).filter(Boolean).length, - ); - expect(columns, 'the rails grid must stay three columns').toBe(3); + const tracks = await rails.evaluate((el) => + getComputedStyle(el).gridTemplateColumns.split(/\s+/).filter(Boolean).map(parseFloat)); + + expect(tracks.length, 'the rails grid must stay three columns').toBe(3); + + // The count alone is VACUOUS and must not be the only assertion here. + // `.with-rails` uses an explicit template (190px minmax(0,760px) 230px), so + // computed gridTemplateColumns always reports three tracks no matter how + // narrow the container gets. Measured on production: forcing the wrapper to + // 600px still reports 3 tracks — as "190px 44px 230px", with the reading + // column crushed. Restoring the inline padding shrinks the middle track, it + // never removes one, so track WIDTH is the only thing worth guarding. + // Design target is 760px; the rule's own comment allows ~680px at 1240. + expect(tracks[1], 'the reading column must not be squeezed by the padding fix') + .toBeGreaterThan(700); }); From a0eae59c45411e040dd771f9dca205c79cf1eca2 Mon Sep 17 00:00:00 2001 From: lxbme <18108274905@163.com> Date: Tue, 4 Aug 2026 14:59:37 +0800 Subject: [PATCH 4/5] fix(css): stop layout wrappers zeroing the container's inline padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .container sets padding: 0 1.25rem, but .article-wrap and .docs-layout are applied to the same elements and re-declared the padding shorthand later in the sheet, resetting the inline axis to 0. Body text therefore ran edge to edge on any viewport narrower than the 1140px container cap — on docs pages, blog posts, articles and the Learning Center alike. Both now declare only padding-block and leave the inline axis to .container. The mobile docs nav also loses its order: 2. Document order already places the nav before the article; the override pushed it about seven screens down, taking the version picker with it, and only ~4% of the link tree was visible once reached. The box is capped at 30vh so the article heading still lands on the first screen. --- next/src/styles/global.css | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/next/src/styles/global.css b/next/src/styles/global.css index e5fbdf11c0ede..320b015d8c6c1 100644 --- a/next/src/styles/global.css +++ b/next/src/styles/global.css @@ -206,7 +206,7 @@ a.tag:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2 .pagination a:hover { border-color: var(--color-primary); text-decoration: none; } /* ---------- article / prose ---------- */ -.article-wrap { display: grid; grid-template-columns: minmax(0, 720px); justify-content: center; padding: 2.5rem 0; } +.article-wrap { display: grid; grid-template-columns: minmax(0, 720px); justify-content: center; padding-block: 2.5rem; } /* ---------- article side rails (wide screens) ---------- */ /* Left: tags + back. Right: on-this-page TOC. Both sticky, both hidden below @@ -321,7 +321,7 @@ a.tag:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2 .admonition-important { border-color: #a25ddc; background: #f4eefb; } /* ---------- docs layout ---------- */ -.docs-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 2.5rem; align-items: start; padding: 2rem 0; } +.docs-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 2.5rem; align-items: start; padding-block: 2rem; } .docs-sidebar { position: sticky; top: calc(var(--header-height) + 1rem); max-height: calc(100vh - var(--header-height) - 2rem); @@ -338,11 +338,13 @@ a.tag:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2 .docs-content { min-width: 0; padding-bottom: 3rem; } .docs-meta { border-top: 1px solid var(--color-border); margin-top: 2.5rem; padding-top: 1rem; font-size: .85rem; color: var(--color-text-soft); display: flex; gap: 1rem; flex-wrap: wrap; } @media (max-width: 960px) { - /* Content first on phones: the full link tree is ~7 screens tall, so it - moves below the article (grid order) and scrolls within a capped box. - minmax(0,1fr) keeps long code lines from inflating the column. */ + /* The nav stays in document order, above the article. It used to be pushed + below it, which put the link tree — and the version picker inside it — + roughly seven screens down, with only ~4% of the tree visible once you got + there. Capping the box at 30vh keeps the article's own heading on the + first screen. minmax(0,1fr) keeps long code lines from inflating the column. */ .docs-layout { grid-template-columns: minmax(0, 1fr); } - .docs-sidebar { order: 2; position: static; max-height: 45vh; max-height: 45dvh; overflow-y: auto; border: 1px solid var(--color-border); border-radius: 8px; padding: .75rem; } + .docs-sidebar { position: static; max-height: 30vh; max-height: 30dvh; overflow-y: auto; border: 1px solid var(--color-border); border-radius: 8px; padding: .75rem; } } /* ---------- homepage extras ---------- */ From d6db43974c3452c7564cf586d85fe6f0a6c06a0b Mon Sep 17 00:00:00 2001 From: lxbme <18108274905@163.com> Date: Tue, 4 Aug 2026 15:07:15 +0800 Subject: [PATCH 5/5] test(css): drop the contradictory breakpoint comment The header-alignment note still cited the 1140px container cap while the code gates at 960px and a second comment ten lines below said so correctly. The stale note also mis-stated the reason: between 961 and 1140 the comparison fails because the two-column grid offsets the article, not because the container is centred. The surviving note also stops citing a line number, which would go stale the first time anything above it moves. --- next/tests/e2e/docs-mobile-layout.spec.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/next/tests/e2e/docs-mobile-layout.spec.mjs b/next/tests/e2e/docs-mobile-layout.spec.mjs index bf443825e508e..5f44e2040208a 100644 --- a/next/tests/e2e/docs-mobile-layout.spec.mjs +++ b/next/tests/e2e/docs-mobile-layout.spec.mjs @@ -30,8 +30,7 @@ async function assertDocsLayout(page, url) { expect(pad.right).toBeGreaterThan(0); // The header was always correct, so it is the reference the article should - // match. Only meaningful below the 1140px container cap, where both are full - // width; above it the centred container makes the comparison meaningless. + // match — but only once the layout stacks; see the breakpoint note below. const geom = await page.evaluate(() => ({ viewport: window.innerWidth, h1Left: document.querySelector('.docs-content h1').getBoundingClientRect().left, @@ -43,7 +42,8 @@ async function assertDocsLayout(page, url) { expect(geom.h1Left, `${url}: article text must not touch the viewport edge`).toBeGreaterThan(0); // Both remaining checks are breakpoint-dependent, and 960px is the line - // (global.css:340) where .docs-layout collapses to one column. + // where .docs-layout collapses to one column (the max-width: 960px media + // query in global.css). if (geom.viewport <= 960) { // Stacked: the article shares the container's inline padding with the // header, so their left edges line up.