mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 17:52:49 +00:00
* feat(plugins): install reviewed artifacts from open registries * fix(website): build protocol before deployment checks * fix(website): make plugin pages discoverable to search engines Resolve missing registry entries in route loaders to return HTTP 404. Serve a registry-backed sitemap so newly listed plugins and authors are discoverable without rebuilding the website. * feat(website): keep plugin directory unlinked for coordinated launch Preserve direct routes and SEO while retaining the partner directory link. Cover nested Git artifacts through registry installation and pinned website commands. * fix(website): link plugin cards by registry owner and stub the clipboard in e2e Plugin and author URLs come from the owner segment of the plugin ID, not the author's GitHub login, so a listed plugin always resolves. The Playwright spec records clipboard writes instead of granting a permission WebKit does not have.
217 lines
9.4 KiB
TypeScript
217 lines
9.4 KiB
TypeScript
import { expect, test, type Page } from "playwright/test";
|
||
|
||
async function openPlugins(page: Page) {
|
||
// Wait for hydration so typing reaches React rather than the server-rendered input.
|
||
await page.goto("/plugins", { waitUntil: "networkidle" });
|
||
await expect(page.getByRole("heading", { level: 1, name: "Plugins" })).toBeVisible();
|
||
}
|
||
|
||
test("browses from the directory into a category, a plugin, and its author", async ({
|
||
page,
|
||
context,
|
||
}) => {
|
||
// WebKit has no clipboard permission to grant; record what the page writes instead.
|
||
await context.addInitScript(() => {
|
||
Object.defineProperty(navigator, "clipboard", {
|
||
configurable: true,
|
||
value: {
|
||
writeText: (text: string) => {
|
||
(window as unknown as { __copied?: string }).__copied = text;
|
||
return Promise.resolve();
|
||
},
|
||
},
|
||
});
|
||
});
|
||
await openPlugins(page);
|
||
|
||
const themes = page.getByRole("region", { name: "Themes" });
|
||
await expect(themes.getByRole("link", { name: /Dracula/ })).toBeVisible();
|
||
|
||
await browseCategory(page, "Git & code review");
|
||
await expect(page).toHaveURL(/category=/);
|
||
await expect(page.getByRole("heading", { level: 2 })).toHaveCount(1);
|
||
await expect(page.getByRole("region", { name: "Themes" })).toHaveCount(0);
|
||
|
||
await openPlugin(page, /Fresh Worktrees/);
|
||
await expect(page).toHaveURL(/\/plugins\/omercnet\/fresh-worktrees$/);
|
||
await expect(
|
||
page.getByRole("heading", { level: 1, name: "Fresh Worktrees", exact: true }),
|
||
).toBeVisible();
|
||
await expect(page.getByText("paseo plugin install omercnet/fresh-worktrees")).toBeVisible();
|
||
await expect(
|
||
page.getByRole("heading", { level: 2, name: "Link to this section Behavior", exact: true }),
|
||
).toBeVisible();
|
||
await copyInstallCommand(page);
|
||
await expect
|
||
.poll(() => page.evaluate(() => (window as unknown as { __copied?: string }).__copied))
|
||
.toBe("paseo plugin install omercnet/fresh-worktrees");
|
||
|
||
const breadcrumbs = page.getByRole("navigation", { name: "Breadcrumb" });
|
||
await expect(breadcrumbs).toContainText("Plugins");
|
||
await expect(breadcrumbs).toContainText("Git & code review");
|
||
await expect(breadcrumbs).toContainText("Fresh Worktrees");
|
||
|
||
await openAuthor(page, "Omer Cohen");
|
||
await expect(page).toHaveURL(/\/plugins\/omercnet$/);
|
||
await expect(page.getByRole("heading", { level: 1, name: "Omer Cohen" })).toBeVisible();
|
||
await expect(page.getByRole("link", { name: /Agent Monitor/ })).toBeVisible();
|
||
await expect(page.getByRole("link", { name: /Defer/ })).toHaveCount(0);
|
||
});
|
||
|
||
test("filters by search and clears to the full directory", async ({ page }) => {
|
||
await openPlugins(page);
|
||
|
||
await searchPlugins(page, "graphite");
|
||
await expect(page).toHaveURL(/q=graphite/);
|
||
await expect(page.getByRole("heading", { level: 2, name: /Results for/ })).toBeVisible();
|
||
await expect(page.getByRole("link", { name: /Graphite/ })).toBeVisible();
|
||
await expect(page.getByRole("link", { name: /Dracula/ })).toHaveCount(0);
|
||
|
||
await searchPlugins(page, "zzzz-nothing");
|
||
await expect(page.getByText("No plugins match.")).toBeVisible();
|
||
await page.getByRole("link", { name: "Clear filters" }).click();
|
||
await expect(page).toHaveURL(/\/plugins\/?$/);
|
||
await expect(page.getByRole("region", { name: "Themes" })).toBeVisible();
|
||
});
|
||
|
||
test("explains a plugin that is not listed", async ({ page }) => {
|
||
await page.goto("/plugins/acme/does-not-exist");
|
||
await expect(page.getByRole("heading", { level: 1, name: "Plugin not found" })).toBeVisible();
|
||
await page.getByRole("link", { name: "Browse all plugins" }).click();
|
||
await expect(page.getByRole("heading", { level: 1, name: "Plugins" })).toBeVisible();
|
||
});
|
||
|
||
async function copyInstallCommand(page: Page) {
|
||
await page.getByRole("button", { name: "Copy to clipboard" }).first().click();
|
||
}
|
||
|
||
async function browseCategory(page: Page, category: string) {
|
||
await page
|
||
.getByRole("region", { name: category })
|
||
.getByRole("link", { name: /^View all/ })
|
||
.click();
|
||
}
|
||
async function openPlugin(page: Page, name: RegExp) {
|
||
await page.getByRole("link", { name }).first().click();
|
||
}
|
||
async function openAuthor(page: Page, name: string) {
|
||
await page.getByRole("link", { name }).first().click();
|
||
}
|
||
async function searchPlugins(page: Page, query: string) {
|
||
await page.getByRole("searchbox", { name: "Search plugins" }).fill(query);
|
||
}
|
||
|
||
test.describe("search engine visits without JavaScript", () => {
|
||
test.use({ javaScriptEnabled: false });
|
||
|
||
test("reads directory, author, and complete plugin HTML with page metadata", async ({ page }) => {
|
||
await openPlugins(page);
|
||
await expect(page.getByRole("link", { name: /Fresh Worktrees/ }).first()).toBeVisible();
|
||
await expectPageMetadata(page, "Plugins – Extend Paseo with community plugins", "/plugins");
|
||
|
||
await page.goto("/plugins/omercnet");
|
||
await expect(page.getByRole("heading", { level: 1, name: "Omer Cohen" })).toBeVisible();
|
||
await expect(page.getByRole("link", { name: /Fresh Worktrees/ }).first()).toBeVisible();
|
||
await expectPageMetadata(page, "Omer Cohen – Paseo plugins", "/plugins/omercnet");
|
||
|
||
const response = await page.goto("/plugins/omercnet/fresh-worktrees");
|
||
expect(response?.status()).toBe(200);
|
||
expect(response?.headers()["cache-control"]).toBe("private, no-store");
|
||
expect(response?.headers()["x-robots-tag"]).toBeUndefined();
|
||
await expect(page.getByText("paseo plugin install omercnet/fresh-worktrees")).toBeVisible();
|
||
await expect(
|
||
page.getByRole("heading", { level: 2, name: "Link to this section Behavior", exact: true }),
|
||
).toBeVisible();
|
||
await expectPageMetadata(
|
||
page,
|
||
"Fresh Worktrees – Paseo plugin",
|
||
"/plugins/omercnet/fresh-worktrees",
|
||
);
|
||
await expect(page.locator('meta[property="og:image"]')).toHaveAttribute(
|
||
"content",
|
||
"https://raw.githubusercontent.com/omercnet/paseo-plugins/main/fresh-worktrees/docs/images/fresh-worktrees-behind.png",
|
||
);
|
||
await expect(page.locator('meta[name="description"]')).toHaveAttribute(
|
||
"content",
|
||
"Fast-forwards clean local base branches before Paseo creates branch-off worktrees",
|
||
);
|
||
await expect(page.locator('meta[name="robots"]')).toHaveCount(0);
|
||
|
||
await page.goto("/plugins/tomgrin10/graphite");
|
||
await expectPageMetadata(page, "Graphite – Paseo plugin", "/plugins/tomgrin10/graphite");
|
||
await expect(page.locator('meta[property="og:image"]')).toHaveAttribute(
|
||
"content",
|
||
"https://paseo.sh/og-image.png",
|
||
);
|
||
});
|
||
|
||
test("returns real 404 pages for unknown plugins and authors", async ({ page }) => {
|
||
const plugin = await page.goto("/plugins/acme/does-not-exist");
|
||
expect(plugin?.status()).toBe(404);
|
||
await expect(page.getByRole("heading", { name: "Plugin not found" })).toBeVisible();
|
||
await expect(page).toHaveTitle("Plugin not found – Paseo");
|
||
const author = await page.goto("/plugins/does-not-exist");
|
||
expect(author?.status()).toBe(404);
|
||
await expect(page.getByRole("heading", { name: "Author not found" })).toBeVisible();
|
||
await expect(page).toHaveTitle("Author not found – Paseo");
|
||
});
|
||
|
||
test("discovers plugin and author URLs through robots and the sitemap index", async ({
|
||
request,
|
||
}) => {
|
||
const robots = await request.get("/robots.txt");
|
||
expect(await robots.text()).toContain("Sitemap: https://paseo.sh/sitemap-index.xml");
|
||
const index = await request.get("/sitemap-index.xml");
|
||
expect(await index.text()).toContain("https://paseo.sh/sitemap-plugins.xml");
|
||
const plugins = await request.get("/sitemap-plugins.xml");
|
||
expect(plugins.status()).toBe(200);
|
||
expect(plugins.headers()["content-type"]).toContain("application/xml");
|
||
expect(await plugins.text()).toContain("<loc>https://paseo.sh/plugins/omercnet</loc>");
|
||
expect(await plugins.text()).toContain(
|
||
"<loc>https://paseo.sh/plugins/omercnet/fresh-worktrees</loc>",
|
||
);
|
||
});
|
||
});
|
||
|
||
async function expectPageMetadata(page: Page, title: string, path: string) {
|
||
await expect(page).toHaveTitle(title);
|
||
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
|
||
"href",
|
||
`https://paseo.sh${path}`,
|
||
);
|
||
await expect(page.locator('meta[property="og:title"]')).toHaveAttribute("content", title);
|
||
await expect(page.locator('meta[property="og:url"]')).toHaveAttribute(
|
||
"content",
|
||
`https://paseo.sh${path}`,
|
||
);
|
||
await expect(page.locator('meta[property="og:type"]')).toHaveAttribute("content", "website");
|
||
await expect(page.locator('meta[name="twitter:card"]')).toHaveAttribute(
|
||
"content",
|
||
"summary_large_image",
|
||
);
|
||
const description = await page.locator('meta[name="description"]').getAttribute("content");
|
||
expect(description).toBeTruthy();
|
||
await expect(page.locator('meta[property="og:description"]')).toHaveAttribute(
|
||
"content",
|
||
description!,
|
||
);
|
||
}
|
||
|
||
test("keeps the directory unlinked until the coordinated announcement", async ({ page }) => {
|
||
await page.goto("/");
|
||
await expect(
|
||
page.getByRole("banner").getByRole("link", { name: "Plugins", exact: true }),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
page.getByRole("contentinfo").getByRole("link", { name: "Plugins", exact: true }),
|
||
).toHaveCount(0);
|
||
await expect(page.getByRole("link", { name: "Community plugins" })).toHaveAttribute(
|
||
"href",
|
||
"https://paseo.cafe",
|
||
);
|
||
await expect(page.locator('a[href="/plugins"]')).toHaveCount(0);
|
||
const response = await page.goto("/plugins");
|
||
expect(response?.status()).toBe(200);
|
||
await expect(page.getByRole("heading", { level: 1, name: "Plugins" })).toBeVisible();
|
||
});
|