mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(e2e): select the measured wrapper by data-slot, not by birth order
The navigation-geometry spec failed on a merge commit whose tree was byte-identical to the branch head that had passed minutes earlier, so nothing had regressed. The spec resolved the element it measures with `main.firstElementChild`, independently in each of its two `page.evaluate` calls. Any node mounting ahead of the wrapper between the two reads made it measure two different elements, which is why the failure was 64 against 131.92 rather than a two-pixel drift. A timing problem gives a small difference or a null; this was a substitution. The shell's content wrapper now carries a stable `data-slot`, both reads select it by that, and the wait gates on the element rather than on scroll state alone. The same positional pattern in the overscroll spec is fixed with it, so the class is gone from the measurement paths rather than from one spec. Re-running the job made it green, which is a workaround. This is the fix.
This commit is contained in:
1 parent
8d86416072
commit
a4bb2adcb2
3 files changed
+51
-18
No files matched your search
@@ -68,12 +68,22 @@ test.describe("settings + admin vertical over-scroll guard", () => {
|
||||
// long when the page is ready and too short on a loaded runner, and
|
||||
// it never says what it is waiting for. Gate on the element the
|
||||
// measurement reads instead.
|
||||
await settleBeforeMeasure(page, page.locator("#main-content"));
|
||||
const wrapperLocator = page.locator(
|
||||
'#main-content [data-slot="main-content-wrapper"]',
|
||||
);
|
||||
await settleBeforeMeasure(page, wrapperLocator);
|
||||
|
||||
const dims = await page.evaluate(() => {
|
||||
const main = document.getElementById("main-content");
|
||||
if (!main) return null;
|
||||
const wrapper = main.firstElementChild as HTMLElement | null;
|
||||
// Selected by the stable `data-slot` (auth-shell.tsx), not by
|
||||
// birth order: `firstElementChild` would resolve to whatever
|
||||
// lazily-mounted sibling (portal, toast, hydrating region) landed
|
||||
// first, silently measuring the wrong subtree instead of the
|
||||
// real content wrapper.
|
||||
const wrapper = main.querySelector<HTMLElement>(
|
||||
'[data-slot="main-content-wrapper"]',
|
||||
);
|
||||
if (!wrapper) return null;
|
||||
// Lowest content edge: max CONTENT-box bottom over the wrapper's
|
||||
// visible, non-fixed descendants, in the scroll container's
|
||||
|
||||
@@ -21,25 +21,38 @@ test.describe("v1.34.1 navigation geometry", () => {
|
||||
|
||||
const heading = page.getByRole("heading", { level: 1 });
|
||||
const sidebar = page.locator("aside").first();
|
||||
// The wrapper carries a stable `data-slot` (auth-shell.tsx) so both
|
||||
// reads below measure the same DOM node instead of resolving
|
||||
// `firstElementChild` twice — a lazily-mounted sibling (portal,
|
||||
// toast, hydrating region) landing ahead of it between the two
|
||||
// reads used to substitute a different element into the second
|
||||
// measurement, producing an unrelated number rather than a
|
||||
// drifted one. Resolving the handle once removes the second
|
||||
// resolution step entirely.
|
||||
const wrapper = page.locator('[data-slot="main-content-wrapper"]');
|
||||
await settleBeforeMeasure(page, heading);
|
||||
await expect(sidebar).toBeVisible();
|
||||
await expect(wrapper).toBeVisible();
|
||||
const wrapperHandle = await wrapper.elementHandle();
|
||||
if (!wrapperHandle) {
|
||||
throw new Error("main content wrapper not found");
|
||||
}
|
||||
|
||||
const before = await page.evaluate(() => {
|
||||
const before = await page.evaluate((wrapperEl) => {
|
||||
const main = document.getElementById("main-content");
|
||||
const heading = main?.querySelector("h1");
|
||||
const aside = main?.querySelector("aside");
|
||||
const wrapper = main?.firstElementChild;
|
||||
if (!main || !heading || !aside || !wrapper) return null;
|
||||
if (!main || !heading || !aside || !wrapperEl) return null;
|
||||
return {
|
||||
headingTop: heading.getBoundingClientRect().top,
|
||||
asideTop: aside.getBoundingClientRect().top,
|
||||
wrapperLeft: wrapper.getBoundingClientRect().left,
|
||||
wrapperLeft: wrapperEl.getBoundingClientRect().left,
|
||||
mainWidth: main.clientWidth,
|
||||
documentScrollable:
|
||||
document.documentElement.scrollHeight >
|
||||
document.documentElement.clientHeight + 1,
|
||||
};
|
||||
});
|
||||
}, wrapperHandle);
|
||||
|
||||
expect(before).not.toBeNull();
|
||||
if (!before) return;
|
||||
@@ -51,26 +64,33 @@ test.describe("v1.34.1 navigation geometry", () => {
|
||||
await page.locator("#main-content").evaluate((main) => {
|
||||
main.scrollTop = Math.min(320, main.scrollHeight - main.clientHeight);
|
||||
});
|
||||
await page.waitForFunction(() => {
|
||||
// Gate on the wrapper being present and laid out (not just on
|
||||
// scroll state) — the measurement below reads it, so the wait
|
||||
// must cover everything that measurement depends on.
|
||||
await page.waitForFunction((wrapperEl) => {
|
||||
const main = document.getElementById("main-content");
|
||||
if (!main) return true;
|
||||
const wrapperReady =
|
||||
!!wrapperEl &&
|
||||
wrapperEl.isConnected &&
|
||||
wrapperEl.getBoundingClientRect().width > 0;
|
||||
return (
|
||||
!main ||
|
||||
main.scrollTop > 0 ||
|
||||
main.scrollHeight <= main.clientHeight
|
||||
wrapperReady &&
|
||||
(main.scrollTop > 0 || main.scrollHeight <= main.clientHeight)
|
||||
);
|
||||
});
|
||||
}, wrapperHandle);
|
||||
|
||||
const after = await page.evaluate(() => {
|
||||
const after = await page.evaluate((wrapperEl) => {
|
||||
const main = document.getElementById("main-content");
|
||||
const aside = main?.querySelector("aside");
|
||||
const wrapper = main?.firstElementChild;
|
||||
if (!main || !aside || !wrapper) return null;
|
||||
if (!main || !aside || !wrapperEl) return null;
|
||||
return {
|
||||
asideTop: aside.getBoundingClientRect().top,
|
||||
wrapperLeft: wrapper.getBoundingClientRect().left,
|
||||
wrapperLeft: wrapperEl.getBoundingClientRect().left,
|
||||
mainWidth: main.clientWidth,
|
||||
};
|
||||
});
|
||||
}, wrapperHandle);
|
||||
await wrapperHandle.dispose();
|
||||
|
||||
expect(after).not.toBeNull();
|
||||
if (!after) return;
|
||||
|
||||
@@ -335,7 +335,10 @@ export function AuthShell({
|
||||
the desktop bottom-6 anchor), so the last line of content
|
||||
can always scroll out from under the floating button.
|
||||
*/}
|
||||
<div className="mx-auto max-w-screen-xl px-4 pt-6 pb-20 md:px-6">
|
||||
<div
|
||||
data-slot="main-content-wrapper"
|
||||
className="mx-auto max-w-screen-xl px-4 pt-6 pb-20 md:px-6"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in new issue
Block a user