diff --git a/src/app/(rep)/rep/check-in-view.tsx b/src/app/(rep)/rep/check-in-view.tsx index fa90063..8b47b1f 100644 --- a/src/app/(rep)/rep/check-in-view.tsx +++ b/src/app/(rep)/rep/check-in-view.tsx @@ -577,7 +577,17 @@ export function CheckInView() { (open ? setPickerOpen(true) : closePicker())}> diff --git a/src/app/(rep)/rep/rep-picker-height.test.ts b/src/app/(rep)/rep/rep-picker-height.test.ts new file mode 100644 index 0000000..3b0fb86 --- /dev/null +++ b/src/app/(rep)/rep/rep-picker-height.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +/** + * Regression guard for the "list goes off the top of the screen" + * bug in the rep picker Sheet. + * + * The base-ui Sheet component applies `data-[side=bottom]:h-auto`, + * which sizes the bottom sheet to its content. With ~600 unpinned + * customers in the picker, that means the sheet grows past the + * viewport and the inner `overflow-y-auto` container has nothing + * to scroll within — the list renders above the visible area + * (verified on 2026-09-20, the list was 46,373px tall in an 809px + * viewport before the fix). + * + * The fix is a `max-h-[90dvh]` override at the call site. If a + * future refactor drops it, this test fails. + */ +const HERE = dirname(fileURLToPath(import.meta.url)); +const VIEW_PATH = resolve(HERE, "./check-in-view.tsx"); + +function viewSource(): string { + return readFileSync(VIEW_PATH, "utf8"); +} + +/** + * Pull out just the JSX block for the picker ``. + * We anchor on the picker-specific className substring that lives inside + * the call (not a generic helper), so this only matches the rep picker + * SheetContent even if other bottom sheets exist in the file. + */ +function pickerSheetContentBlock(source: string): string | null { + // Anchor on the unique `max-h-[90dvh]` cap (which is what we're + // testing for) and grab back to the opening `` of the opening tag — + // enough to read the full className. + const close = source.indexOf(">", start); + if (close === -1) return null; + return source.slice(start, close + 1); +} + +describe("rep picker SheetContent height contract", () => { + it("constrains the bottom sheet height with max-h-[90dvh]", () => { + const source = viewSource(); + const block = pickerSheetContentBlock(source); + expect(block, "picker SheetContent max-h-[90dvh] cap not found").not.toBeNull(); + expect(block!).toMatch(/max-h-\[90dvh\]/); + }); + + it("uses dvh (not just vh) so mobile browser chrome is accounted for", () => { + const source = viewSource(); + const block = pickerSheetContentBlock(source); + expect(block!).toMatch(/max-h-\[90dvh\]/); + // Negative: not bare `max-h-[90vh]` without the `d` suffix — + // plain `vh` ignores the mobile browser chrome and the sheet + // can still be pushed off the bottom of a small screen. + expect(block!).not.toMatch(/max-h-\[90vh\](?!d)/); + }); + + it("the inner list container has overflow-y-auto (so the cap actually scrolls)", () => { + const source = viewSource(); + // The inner div around the
    …
must have overflow-y-auto. + // `modal-shell__body flex-1 overflow-y-auto px-4 pb-4` carries it. + expect(source).toMatch(/modal-shell__body[^"]*\boverflow-y-auto\b/); + }); + + it("the picker list container has flex-1 so it fills remaining sheet height", () => { + const source = viewSource(); + // Without `flex-1`, the inner div sizes to its content and the + // outer `max-h-[90dvh]` cap doesn't trigger a scrollbar — same + // visual bug as dropping the cap. Pair this with the previous + // test to nail down both halves of the constraint. + expect(source).toMatch(/modal-shell__body[^"]*\bflex-1\b/); + }); +});