2026-04-27 21:56:45 +08:00
|
|
|
import { expect, test } from "@playwright/test";
|
2026-07-18 11:22:38 +08:00
|
|
|
import { openDocsSearch, waitForHydration } from "./helpers";
|
2026-04-27 21:56:45 +08:00
|
|
|
|
2026-06-19 18:46:59 +08:00
|
|
|
test.describe("docs search (Pagefind)", () => {
|
2026-04-27 21:56:45 +08:00
|
|
|
test("search button is visible", async ({ page }) => {
|
2026-06-19 18:46:59 +08:00
|
|
|
await page.goto("/");
|
|
|
|
|
await expect(page.locator(".blog-search").first()).toBeVisible();
|
2026-04-27 21:56:45 +08:00
|
|
|
});
|
|
|
|
|
|
2026-06-19 18:46:59 +08:00
|
|
|
test("clicking search opens the dialog with an input", async ({ page }) => {
|
|
|
|
|
await page.goto("/");
|
2026-07-18 11:22:38 +08:00
|
|
|
const input = await openDocsSearch(page);
|
|
|
|
|
// It's the real search combobox with a (config-driven) placeholder. Don't
|
|
|
|
|
// assert the exact placeholder copy, just that one is present.
|
|
|
|
|
await expect(input).toHaveAttribute("role", "combobox");
|
|
|
|
|
await expect(input).toHaveAttribute("placeholder", /\S/);
|
2026-04-27 21:56:45 +08:00
|
|
|
});
|
|
|
|
|
|
2026-06-19 18:46:59 +08:00
|
|
|
test("typing a query shows results", async ({ page }) => {
|
|
|
|
|
await page.goto("/");
|
2026-07-18 11:22:38 +08:00
|
|
|
const input = await openDocsSearch(page);
|
2026-06-19 18:46:59 +08:00
|
|
|
await input.fill("docker");
|
2026-07-18 11:22:38 +08:00
|
|
|
await expect(page.locator('[role="option"]').first()).toBeVisible({ timeout: 10_000 });
|
2026-04-27 21:56:45 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe("Theme Toggle", () => {
|
2026-04-27 22:34:11 +08:00
|
|
|
test("theme toggle exists in DOM", async ({ page }) => {
|
2026-04-27 21:56:45 +08:00
|
|
|
await page.goto("/guide/getting-started");
|
|
|
|
|
const toggle = page.locator(
|
2026-04-27 22:34:11 +08:00
|
|
|
'button[role="switch"][title*="dark"], button[role="switch"][title*="light"]',
|
2026-04-27 21:56:45 +08:00
|
|
|
);
|
2026-04-27 22:34:11 +08:00
|
|
|
await expect(toggle.first()).toBeAttached();
|
2026-04-27 21:56:45 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("clicking theme toggle changes appearance", async ({ page }) => {
|
|
|
|
|
await page.goto("/guide/getting-started");
|
2026-07-18 11:22:38 +08:00
|
|
|
// The appearance switch is a Vue handler, so wait for hydration before the
|
|
|
|
|
// single click (otherwise the click is swallowed and the class never flips).
|
|
|
|
|
await waitForHydration(page);
|
|
|
|
|
|
2026-06-19 18:46:59 +08:00
|
|
|
const html = page.locator("html");
|
2026-07-18 11:22:38 +08:00
|
|
|
const wasDark = ((await html.getAttribute("class")) ?? "").includes("dark");
|
2026-04-27 21:56:45 +08:00
|
|
|
|
2026-07-18 12:27:00 +08:00
|
|
|
// Click whichever theme toggle is actually visible at this viewport: below
|
|
|
|
|
// 1440px the nav uses VitePress's native inline toggle, at >=1440px our
|
|
|
|
|
// custom cluster toggle takes over. Filtering to :visible skips the hidden
|
|
|
|
|
// duplicates that sit earlier in DOM order.
|
|
|
|
|
await page.locator('button[role="switch"]:visible').first().click();
|
2026-04-27 21:56:45 +08:00
|
|
|
|
2026-06-19 18:46:59 +08:00
|
|
|
// Assert the dark class actually toggled
|
|
|
|
|
if (wasDark) {
|
|
|
|
|
await expect(html).not.toHaveClass(/dark/);
|
|
|
|
|
} else {
|
|
|
|
|
await expect(html).toHaveClass(/dark/);
|
|
|
|
|
}
|
2026-04-27 21:56:45 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe("GitHub Stars Component", () => {
|
2026-07-18 12:27:00 +08:00
|
|
|
// The custom nav cluster (toggle + Fund + GitHub Star) only renders at
|
|
|
|
|
// >=1440px, where it fits without overflowing the nav. Below that the nav
|
|
|
|
|
// defers to VitePress's native responsive layout, so pin a wide desktop here.
|
|
|
|
|
test.use({ viewport: { width: 1500, height: 900 } });
|
|
|
|
|
|
2026-04-27 21:56:45 +08:00
|
|
|
test("GitHub star button is visible in navbar", async ({ page }) => {
|
|
|
|
|
await page.goto("/");
|
|
|
|
|
const githubBtn = page.locator(".github-btn, .github-btn-wrapper").first();
|
|
|
|
|
await expect(githubBtn).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("GitHub star button links to correct repo", async ({ page }) => {
|
|
|
|
|
await page.goto("/");
|
|
|
|
|
const starLink = page.locator('a[title="Star on GitHub"]').first();
|
2026-04-27 22:34:11 +08:00
|
|
|
await expect(starLink).toHaveAttribute("href", "https://github.com/snapotter-hq/snapotter");
|
2026-04-27 21:56:45 +08:00
|
|
|
await expect(starLink).toHaveAttribute("target", "_blank");
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-07-18 12:27:00 +08:00
|
|
|
|
|
|
|
|
// Regression guard for the nav overflow reported in #556. The custom nav cluster
|
|
|
|
|
// used to render inline at every width, pushing the nav past narrow viewports
|
|
|
|
|
// (a horizontal scrollbar at 768-959px, off-screen clipping at 1280-1366px).
|
|
|
|
|
test.describe("Nav has no horizontal overflow", () => {
|
|
|
|
|
for (const width of [768, 834, 900, 1024, 1280, 1366]) {
|
|
|
|
|
test(`no horizontal scroll at ${width}px`, async ({ page }) => {
|
|
|
|
|
await page.setViewportSize({ width, height: 900 });
|
|
|
|
|
await page.goto("/guide/getting-started");
|
|
|
|
|
await waitForHydration(page);
|
|
|
|
|
const overflow = await page.evaluate(() => {
|
|
|
|
|
const de = document.documentElement;
|
|
|
|
|
return de.scrollWidth - de.clientWidth;
|
|
|
|
|
});
|
|
|
|
|
// Allow a 1px sub-pixel rounding margin; the bug produced 300px+ scroll.
|
|
|
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|