Loading panel/e2e/mobile.spec.ts +21 −0 Changes for panel/e2e/mobile.spec.ts: 21 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ for (const [account, path] of [ ["linked", "/account"], ["owner", "/servers"], ["owner", "/admin/users"], ["owner", "/servers/survival/luckperms"], ] as const) { test(`${path} fits a 375px screen for ${account}`, async ({ page, signIn }) => { await signIn(account); Loading @@ -29,3 +30,23 @@ for (const [account, path] of [ await expectFitsScreen(page); }); } test("the LuckPerms player list leaves the rest of the page on the first screen", async ({ page, signIn }) => { await signIn("owner"); await page.goto("/servers/survival/luckperms"); const list = page.getByRole("button", { name: "mock_player", exact: true }).locator(".."); await expect(list).toBeVisible(); // Twelve players online: the list scrolls inside its card, so the prompt // under it shows without scrolling the page. await expect(page.getByText(t("servers:luckperms_select_player_prompt"))).toBeInViewport({ ratio: 1 }); const { scrollHeight, clientHeight } = await list.evaluate((el) => ({ scrollHeight: el.scrollHeight, clientHeight: el.clientHeight, })); expect(scrollHeight).toBeGreaterThan(clientHeight); // The last one is still a tap away. await page.getByRole("button", { name: "Herobrine", exact: true }).click(); await expect(page.getByRole("textbox", { name: t("servers:luckperms_player_lookup") })).toHaveValue("Herobrine"); }); panel/e2e/smoke.spec.ts +16 −0 Changes for panel/e2e/smoke.spec.ts: 16 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -73,3 +73,19 @@ test("a player's pages load on demand and never pull in the admin pages", async await expect.poll(() => fetched.some((p) => ACCOUNT_PAGE.test(p))).toBe(true); expect(fetched.filter((p) => ADMIN_PAGE.test(p))).toEqual([]); }); test("on a wide screen the LuckPerms player list runs down its column", async ({ page, signIn }) => { await signIn("owner"); await page.goto("/servers/survival/luckperms"); const list = page.getByRole("button", { name: "mock_player", exact: true }).locator(".."); await expect(list).toBeVisible(); const listBox = (await list.boundingBox())!; const cardBox = (await list.locator("..").boundingBox())!; expect(cardBox.height).toBeGreaterThanOrEqual(500); expect(Math.abs(listBox.y + listBox.height - (cardBox.y + cardBox.height))).toBeLessThanOrEqual(2); // A taller screen grows the card to its cap, whatever the roster holds. await page.setViewportSize({ width: 1280, height: 1000 }); await expect.poll(async () => (await list.locator("..").boundingBox())!.height).toBe(700); }); panel/src/pages/ServerLuckPerms.tsx +6 −4 Changes for panel/src/pages/ServerLuckPerms.tsx: 6 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -397,8 +397,10 @@ export function ServerLuckPerms() { /> ) : ( <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 items-start"> {/* Left Directory Sidebar: Single card for Search + Online Players */} <Card className="lg:col-span-1 flex flex-col h-[calc(100vh-14rem)] lg:min-h-[500px] max-h-[700px] border-border bg-card/60 backdrop-blur-sm overflow-hidden"> {/* Left Directory Sidebar: Single card for Search + Online Players. On a wide screen it fills the column; on a phone it fits its content and the list scrolls inside it, so the player's panel below is on the first screen. */} <Card className="lg:col-span-1 flex flex-col lg:h-[calc(100vh-14rem)] lg:min-h-[500px] lg:max-h-[700px] border-border bg-card/60 backdrop-blur-sm overflow-hidden"> <CardHeader className="p-4 pb-2 border-b border-border/50 shrink-0"> <CardTitle className="text-xs font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-2"> <Users className="h-4 w-4 text-primary" /> Loading @@ -424,7 +426,7 @@ export function ServerLuckPerms() { {t("access_online_title")} </span> </div> <CardContent className="p-2 overflow-y-auto flex-1 space-y-0.5 min-h-0"> <CardContent className="p-2 overflow-y-auto max-h-52 lg:max-h-none flex-1 space-y-0.5 min-h-0"> {playersLoading && onlinePlayers.length === 0 ? ( <div className="flex justify-center py-6"> <Loader2 className="h-5 w-5 animate-spin text-primary" /> Loading Loading @@ -463,7 +465,7 @@ export function ServerLuckPerms() { {/* Right Main Panel */} <div className="lg:col-span-3 space-y-6"> {!selectedPlayer ? ( <Card className="flex flex-col items-center justify-center py-24 text-center border-dashed border-border bg-card/20 min-h-[500px]"> <Card className="flex flex-col items-center justify-center py-8 lg:py-24 text-center border-dashed border-border bg-card/20 lg:min-h-[500px]"> <Shield className="h-14 w-14 text-muted-foreground/15 mb-4 animate-pulse" /> <p className="text-sm text-muted-foreground font-medium max-w-xs leading-normal"> {t("luckperms_select_player_prompt")} Loading Loading
panel/e2e/mobile.spec.ts +21 −0 Changes for panel/e2e/mobile.spec.ts: 21 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ for (const [account, path] of [ ["linked", "/account"], ["owner", "/servers"], ["owner", "/admin/users"], ["owner", "/servers/survival/luckperms"], ] as const) { test(`${path} fits a 375px screen for ${account}`, async ({ page, signIn }) => { await signIn(account); Loading @@ -29,3 +30,23 @@ for (const [account, path] of [ await expectFitsScreen(page); }); } test("the LuckPerms player list leaves the rest of the page on the first screen", async ({ page, signIn }) => { await signIn("owner"); await page.goto("/servers/survival/luckperms"); const list = page.getByRole("button", { name: "mock_player", exact: true }).locator(".."); await expect(list).toBeVisible(); // Twelve players online: the list scrolls inside its card, so the prompt // under it shows without scrolling the page. await expect(page.getByText(t("servers:luckperms_select_player_prompt"))).toBeInViewport({ ratio: 1 }); const { scrollHeight, clientHeight } = await list.evaluate((el) => ({ scrollHeight: el.scrollHeight, clientHeight: el.clientHeight, })); expect(scrollHeight).toBeGreaterThan(clientHeight); // The last one is still a tap away. await page.getByRole("button", { name: "Herobrine", exact: true }).click(); await expect(page.getByRole("textbox", { name: t("servers:luckperms_player_lookup") })).toHaveValue("Herobrine"); });
panel/e2e/smoke.spec.ts +16 −0 Changes for panel/e2e/smoke.spec.ts: 16 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -73,3 +73,19 @@ test("a player's pages load on demand and never pull in the admin pages", async await expect.poll(() => fetched.some((p) => ACCOUNT_PAGE.test(p))).toBe(true); expect(fetched.filter((p) => ADMIN_PAGE.test(p))).toEqual([]); }); test("on a wide screen the LuckPerms player list runs down its column", async ({ page, signIn }) => { await signIn("owner"); await page.goto("/servers/survival/luckperms"); const list = page.getByRole("button", { name: "mock_player", exact: true }).locator(".."); await expect(list).toBeVisible(); const listBox = (await list.boundingBox())!; const cardBox = (await list.locator("..").boundingBox())!; expect(cardBox.height).toBeGreaterThanOrEqual(500); expect(Math.abs(listBox.y + listBox.height - (cardBox.y + cardBox.height))).toBeLessThanOrEqual(2); // A taller screen grows the card to its cap, whatever the roster holds. await page.setViewportSize({ width: 1280, height: 1000 }); await expect.poll(async () => (await list.locator("..").boundingBox())!.height).toBe(700); });
panel/src/pages/ServerLuckPerms.tsx +6 −4 Changes for panel/src/pages/ServerLuckPerms.tsx: 6 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -397,8 +397,10 @@ export function ServerLuckPerms() { /> ) : ( <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 items-start"> {/* Left Directory Sidebar: Single card for Search + Online Players */} <Card className="lg:col-span-1 flex flex-col h-[calc(100vh-14rem)] lg:min-h-[500px] max-h-[700px] border-border bg-card/60 backdrop-blur-sm overflow-hidden"> {/* Left Directory Sidebar: Single card for Search + Online Players. On a wide screen it fills the column; on a phone it fits its content and the list scrolls inside it, so the player's panel below is on the first screen. */} <Card className="lg:col-span-1 flex flex-col lg:h-[calc(100vh-14rem)] lg:min-h-[500px] lg:max-h-[700px] border-border bg-card/60 backdrop-blur-sm overflow-hidden"> <CardHeader className="p-4 pb-2 border-b border-border/50 shrink-0"> <CardTitle className="text-xs font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-2"> <Users className="h-4 w-4 text-primary" /> Loading @@ -424,7 +426,7 @@ export function ServerLuckPerms() { {t("access_online_title")} </span> </div> <CardContent className="p-2 overflow-y-auto flex-1 space-y-0.5 min-h-0"> <CardContent className="p-2 overflow-y-auto max-h-52 lg:max-h-none flex-1 space-y-0.5 min-h-0"> {playersLoading && onlinePlayers.length === 0 ? ( <div className="flex justify-center py-6"> <Loader2 className="h-5 w-5 animate-spin text-primary" /> Loading Loading @@ -463,7 +465,7 @@ export function ServerLuckPerms() { {/* Right Main Panel */} <div className="lg:col-span-3 space-y-6"> {!selectedPlayer ? ( <Card className="flex flex-col items-center justify-center py-24 text-center border-dashed border-border bg-card/20 min-h-[500px]"> <Card className="flex flex-col items-center justify-center py-8 lg:py-24 text-center border-dashed border-border bg-card/20 lg:min-h-[500px]"> <Shield className="h-14 w-14 text-muted-foreground/15 mb-4 animate-pulse" /> <p className="text-sm text-muted-foreground font-medium max-w-xs leading-normal"> {t("luckperms_select_player_prompt")} Loading