Files

237 lines
13 KiB
TypeScript

import { expect, test } from "@playwright/test";
test("GitHub stars refresh automatically and survive a failed refresh", async ({ page }) => {
await page.clock.install();
let requests = 0;
await page.route("https://api.github.com/repos/hao-ai-lab/FastVideo", async (route) => {
requests++;
if (requests === 2) return route.fulfill({ status: 503, body: "Unavailable" });
await route.fulfill({ json: { stargazers_count: requests === 1 ? 4612 : 5234 } });
});
await page.goto("/FastVideo");
const stars = page.locator(".home-github-stars");
await expect(stars).toHaveText("4.6k");
await expect(stars).toHaveAccessibleName("FastVideo on GitHub, 4,612 stars");
await page.clock.fastForward(5 * 60 * 1000);
await expect.poll(() => requests).toBe(2);
await expect(stars).toHaveText("4.6k");
await page.clock.fastForward(5 * 60 * 1000);
await expect(stars).toHaveText("5.2k");
});
test("GitHub rate limits leave a usable Star link without a fabricated count", async ({ page }) => {
await page.route("https://api.github.com/repos/hao-ai-lab/FastVideo", (route) =>
route.fulfill({ status: 403, json: { message: "API rate limit exceeded" } }),
);
await page.goto("/FastVideo");
const stars = page.getByRole("link", { name: "Star FastVideo on GitHub", exact: true });
await expect(stars).toHaveText("Star");
await expect(stars).toHaveAttribute("href", "https://github.com/hao-ai-lab/FastVideo");
});
test("documentation navigation opens transferred guides", async ({ page }) => {
await page.goto("/FastVideo");
await expect(page.getByRole("heading", { name: "Welcome to FastVideo", exact: true })).toBeVisible();
await page.locator("#blume-nav-drawer").getByRole("link", { name: "Installation", exact: true }).click();
await expect(page).toHaveURL(/getting_started\/installation/, { timeout: 30000 });
await expect(page.getByRole("heading", { level: 1 })).toBeVisible({ timeout: 30000 });
});
test("the welcome page introduces the project and opens the cookbook", async ({ page }) => {
await page.goto("/FastVideo");
const logo = page.getByRole("heading", { level: 1, name: "Welcome to FastVideo" }).getByRole("img");
await expect(logo).toBeVisible();
expect(await logo.evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true);
const column = await page.locator(".home-page").boundingBox();
const image = await logo.boundingBox();
expect(Math.abs(image!.x + image!.width / 2 - (column!.x + column!.width / 2))).toBeLessThan(2);
await expect(page.getByRole("link", { name: "Hao AI Lab", exact: true })).toHaveAttribute(
"href",
"https://haoailab.com/",
);
const lab = page.locator(".home-lab-link img");
expect(await lab.evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true);
const trending = page.locator(".home-trendshift a");
await expect(trending).toHaveAttribute(
"href",
"https://trendshift.io/repositories/14509?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-14509",
);
await expect(trending.locator("img")).toHaveAttribute("src", "https://trendshift.io/api/badge/repositories/14509");
await expect(page.locator(".home-news time")).toHaveCount(2);
await expect(page.locator(".home-news")).toContainText("Kandinsky 6");
await expect(page.getByRole("heading", { name: "Projects and research", exact: false })).toBeVisible();
await expect(page.getByRole("link", { name: "DanceGRPO", exact: true })).toBeVisible();
await expect(page.getByText("Use the navigation menu on the left to explore different sections:")).toHaveCount(0);
await expect(page.getByText("About FastVideo and performance overview", { exact: true })).toHaveCount(0);
await expect(page.locator(".home-page > p").first()).toContainText(
"FastVideo is an inference and post-training framework",
);
await page.getByRole("link", { name: /^Cookbook Choose a model recipe/ }).click();
await expect(page).toHaveURL(/FastVideo\/cookbook/);
await expect(page.getByRole("heading", { name: "Cookbook", exact: true })).toBeVisible();
});
test("Fast Models connects the homepage, family collections, and checkpoints", async ({ page }) => {
await page.goto("/FastVideo");
await page.locator(".home-project-links").getByRole("link", { name: "Fast Models", exact: true }).click();
await expect(page).toHaveURL(/FastVideo\/fast-models$/);
await expect(page.getByRole("heading", { level: 1, name: "Fast Models", exact: true })).toBeVisible();
for (const family of ["FastH3", "FastWan", "FastMetal"]) {
await expect(page.getByRole("heading", { level: 2, name: family, exact: false })).toBeVisible();
}
await expect(page.getByRole("link", { name: "FastMetal collection", exact: true })).toHaveAttribute(
"href",
"https://huggingface.co/collections/FastVideo/fastmetal",
);
await expect(page.locator('a[href="https://huggingface.co/FastVideo/FastWan-QAD-FP8-1.3B"]').first()).toBeVisible();
const checkpoints = await page
.locator('a[href^="https://huggingface.co/FastVideo/"]')
.evaluateAll((links) => [...new Set(links.map((link) => link.getAttribute("href")))]);
expect(checkpoints.length).toBeGreaterThanOrEqual(18);
await expect(
page.locator("#blume-nav-drawer").getByRole("link", { name: "Fast Models", exact: true }),
).toHaveAttribute("aria-current", "page");
await page.setViewportSize({ width: 390, height: 844 });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
});
test("V1 API renders Python signatures within the Blume shell", async ({ page }) => {
await page.goto("/FastVideo/getting_started/v1_api");
await expect(page.getByRole("heading", { name: "V1 API", exact: true })).toBeVisible();
await expect(page.locator('[id="fastvideo.entrypoints.video_generator.VideoGenerator"]')).toBeVisible();
await expect(page.locator(".python-reference")).toContainText("from_pretrained");
for (const description of [
"The inference arguments",
"The executor class to use for inference",
"Whether to log statistics",
"Optional multiprocessing.Queue to forward worker logs to",
]) {
await expect(page.locator(".python-reference")).toContainText(description);
}
await expect(page.getByRole("button", { name: "Search", exact: true })).toBeVisible();
});
test("the API overview resolves Python references and guide links", async ({ page }) => {
await page.goto("/FastVideo/api");
const reference = page.locator(".python-reference");
await expect(reference.getByRole("link", { name: "API contracts and sampling", exact: true })).toHaveAttribute(
"href",
"/FastVideo/api/fastvideo/api#fastvideo.api",
);
await reference.getByRole("link", { name: "fastvideo.VideoGenerator", exact: true }).click();
await expect(page.locator('[id="fastvideo.VideoGenerator"]')).toBeVisible();
await page.goto("/FastVideo/api");
await page.locator(".python-reference").getByRole("link", { name: "V1 API", exact: true }).click();
await expect(page).toHaveURL(/\/FastVideo\/getting_started\/v1_api$/);
await expect(page.getByRole("heading", { name: "V1 API", exact: true })).toBeVisible();
});
test("generated module links stay inside the transferred Python reference", async ({ page }) => {
await page.goto("/FastVideo/api/fastvideo");
await page.getByRole("link", { name: "configs", exact: true }).click();
await expect(page).toHaveURL(/FastVideo\/api\/fastvideo\/configs/);
});
test("search finds a transferred guide and opens it by keyboard", async ({ page }) => {
test.setTimeout(180000);
await page.goto("/FastVideo");
await page.getByRole("button", { name: "Search", exact: true }).click();
const query = page.getByRole("combobox", { name: "Search docs", exact: true });
await query.fill("offloading");
await expect(page.getByRole("option", { name: /^Offloading/ }).first()).toBeVisible({ timeout: 120000 });
await query.press("Enter");
await expect(page).toHaveURL(/FastVideo\/inference\/offloading/, { timeout: 30000 });
});
test("the welcome cards reflow on mobile without horizontal scrolling", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/FastVideo");
await expect(page.getByRole("heading", { name: "Welcome to FastVideo", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: /Quick start Generate/ })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
});
test("the model catalog filters by family and checkpoint name", async ({ page }) => {
await page.goto("/FastVideo/fast-models");
const catalog = page.locator(".model-catalog");
await expect(catalog.getByRole("status")).toHaveText("21 checkpoints");
await page.getByRole("button", { name: "FastMetal", exact: true }).click();
await expect(catalog.getByRole("status")).toHaveText("3 checkpoints");
await page.getByRole("searchbox", { name: "Search checkpoints", exact: true }).fill("14B");
await expect(catalog.getByRole("status")).toHaveText("1 checkpoint");
await page.getByRole("searchbox").fill("no-such-model");
await expect(catalog.getByRole("status")).toHaveText("0 checkpoints");
await page.getByRole("button", { name: "Clear filters", exact: true }).click();
await expect(catalog.getByRole("status")).toHaveText("21 checkpoints");
});
test("hardware guides are grouped below general inference tasks", async ({ page }) => {
await page.goto("/FastVideo");
const nav = page.locator("#blume-nav-drawer");
await expect(nav.getByRole("link", { name: "Two DGX Sparks", exact: true })).not.toBeVisible();
await nav.getByText("Hardware", { exact: true }).click();
await nav.getByRole("link", { name: "Two DGX Sparks", exact: true }).click();
await expect(page).toHaveURL(/getting_started\/installation\/spark_pair/);
});
test("family runbooks use React model cards without the old cookbook runtime", async ({ page }) => {
await page.goto("/FastVideo/cookbook/wan/");
await expect(page.locator(".recipe-card").first()).toBeVisible();
await expect(page.getByRole("button", { name: "Search", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "← All models", exact: true })).toHaveAttribute(
"href",
/\/FastVideo\/cookbook$/,
);
await expect(page.locator('script[src*="/assets/cookbook.js"],link[href*="cookbook-runbooks.css"]')).toHaveCount(0);
});
test("generated example titles render once and retain their original fragment", async ({ page }) => {
await page.goto("/FastVideo/inference/examples/basic");
await expect(page.getByRole("heading", { name: "Basic Video Generation Tutorial", exact: true })).toHaveCount(1);
await expect(page.locator('[id="basic-video-generation-tutorial"]')).toHaveCount(1);
await expect(page.locator("article")).toContainText("examples/inference/basic");
});
test("hardware overview distinguishes runtime support and AMD documentation limits", async ({ page }) => {
await page.goto("/FastVideo/getting_started/hardware");
const table = page.getByRole("table");
for (const platform of ["RTX", "data center", "DGX Spark", "Apple Silicon", "AMD"]) {
await expect(table).toContainText(platform);
}
await page.getByRole("link", { name: "ROCm support and current limits", exact: true }).click();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("AMD and ROCm");
await expect(page.locator("article")).toContainText("not currently documented");
});
test("homepage project buttons have visible borders in dark mode", async ({ page }) => {
await page.emulateMedia({ colorScheme: "dark" });
await page.goto("/FastVideo");
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
const colors = await page.locator(".home-project-links a").evaluateAll((links) =>
links.map((link) => {
const style = getComputedStyle(link);
return {
border: style.borderTopColor,
width: style.borderTopWidth,
background: getComputedStyle(document.body).backgroundColor,
};
}),
);
expect(colors).toHaveLength(3);
for (const color of colors) {
expect(color.width).toBe("1px");
const luminance = (value: string) => {
const channels = value
.match(/[\d.]+/g)!
.slice(0, 3)
.map(Number)
.map((channel) => (value.startsWith("color(srgb") ? channel : channel / 255))
.map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4));
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
};
const border = luminance(color.border);
const background = luminance(color.background);
expect((Math.max(border, background) + 0.05) / (Math.min(border, background) + 0.05)).toBeGreaterThanOrEqual(3);
}
});