132 lines
8.2 KiB
TypeScript
132 lines
8.2 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
test("player can open an active spot", async ({ page }) => {
|
|
await page.goto("/");
|
|
await expect(page.getByRole("heading", { name: "Выбирай место, пока клюёт." })).toBeVisible();
|
|
const activeSpots = page.locator(".spot-card[data-testid]");
|
|
await expect(activeSpots).not.toHaveCount(0);
|
|
const activeSpot = activeSpots.first();
|
|
await expect(activeSpot.locator(".source-chip")).not.toHaveCount(0);
|
|
const level = await activeSpot.locator("[data-activity-level]").getAttribute("data-activity-level");
|
|
await expect(page.locator(".detail-score [data-activity-level]")).toHaveAttribute("data-activity-level", level ?? "");
|
|
await activeSpot.click();
|
|
await expect(page.getByRole("heading", { name: /^Точка / })).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "Последние уловы" })).toBeVisible();
|
|
await expect(page.locator(".catch-list .source-chip")).not.toHaveCount(0);
|
|
await expect(page.locator("[data-activity-level]")).toHaveAttribute("data-activity-level", level ?? "");
|
|
});
|
|
|
|
test("submitted catch appears publicly only after moderation", async ({ page }) => {
|
|
const player = `E2E Player ${Date.now()}`;
|
|
const x = 1000 + Date.now() % 8000;
|
|
const y = x + 1;
|
|
const spotTestId = `spot-${x}-${y}`;
|
|
|
|
await page.goto("/report");
|
|
await page.getByLabel("Координата X *").fill(String(x));
|
|
await page.getByLabel("Координата Y *").fill(String(y));
|
|
await page.getByLabel("Вес, граммы *").fill("6789");
|
|
await page.getByText("Дополнительные сведения").click();
|
|
await page.getByLabel("Ник игрока").fill(player);
|
|
await page.getByLabel(/Я отправляю собственное наблюдение/).check();
|
|
await page.getByRole("button", { name: "Отправить на проверку" }).click();
|
|
await expect(page.getByText("Улов отправлен на модерацию. Спасибо!")).toBeVisible();
|
|
|
|
await page.goto("/");
|
|
await expect(page.getByTestId(spotTestId)).toHaveCount(0);
|
|
|
|
await page.goto("/admin/moderation");
|
|
await page.getByLabel("Административный токен").fill("change-me-in-production");
|
|
await page.getByRole("button", { name: "Открыть очередь" }).click();
|
|
const report = page.locator(".moderation-card").filter({ hasText: player });
|
|
await expect(report).toBeVisible();
|
|
await report.getByRole("button", { name: "Одобрить" }).click();
|
|
await expect(report).toHaveCount(0);
|
|
|
|
await page.goto("/");
|
|
await expect(page.getByTestId(spotTestId)).toBeVisible();
|
|
});
|
|
|
|
for (const viewport of [{ name: "desktop", width: 1280, height: 900 }, { name: "mobile", width: 390, height: 844 }]) {
|
|
test(`home filters work on ${viewport.name}`, async ({ page }) => {
|
|
await page.setViewportSize(viewport);
|
|
await page.goto("/");
|
|
await page.getByLabel("Водоём").selectOption("kuori");
|
|
await page.getByLabel("Рыба").selectOption("lake-trout");
|
|
if (viewport.name === "mobile") await page.getByText("Период и порядок").click();
|
|
await page.getByLabel("Период").selectOption("72");
|
|
await page.getByLabel("Сначала").selectOption("freshness");
|
|
await page.getByRole("button", { name: "⌕ Найти клёв" }).click();
|
|
await expect(page).toHaveURL(/waterbody=kuori.*fish=lake-trout.*hours=72.*sort=freshness/);
|
|
await expect(page.getByRole("heading", { name: "Горячие точки" })).toBeVisible();
|
|
await expect(page.getByLabel("Применённые фильтры")).toContainText("Куори");
|
|
await expect(page.getByLabel("Применённые фильтры")).toContainText("Озёрная форель");
|
|
await expect(page.getByRole("link", { name: "Сбросить" })).toBeVisible();
|
|
await expect(page.locator(".spot-list, .state")).toBeVisible();
|
|
const dimensions = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }));
|
|
expect(dimensions.scroll).toBeLessThanOrEqual(dimensions.width);
|
|
});
|
|
|
|
test(`external staging queue is safe on ${viewport.name}`, async ({ page }) => {
|
|
await page.setViewportSize(viewport);
|
|
let mutations = 0;
|
|
await page.route("**/api/v1/**", async route => {
|
|
const request = route.request();
|
|
if (request.method() !== "GET") mutations += 1;
|
|
const path = new URL(request.url()).pathname;
|
|
if (path.endsWith("/fishes")) return route.fulfill({ json: [{ slug: "pike", name_ru: "Щука" }] });
|
|
if (path.endsWith("/waterbodies")) return route.fulfill({ json: [{ slug: "kuori", name_ru: "Куори" }] });
|
|
if (path.endsWith("/source-status")) return route.fulfill({ json: [{ source_system: "rf4db", name: "RF4DB", status: "healthy", last_started_at: null, last_success_at: null, observations: 1 }] });
|
|
if (path.endsWith("/admin/external-observations")) return route.fulfill({ json: [{ id: "10000000-0000-0000-0000-000000000001", source_system: "rf4db", source_external_id: "fixture-1", source_url: "https://rf4db.com/fixture-1", fish_name: "Pike", waterbody_name: "Kuori", x: 72, y: 84, weight_g: null, status: "staged", fish_slug: null, waterbody_slug: null, review_note: null }] });
|
|
return route.fulfill({ status: 404 });
|
|
});
|
|
await page.goto("/admin/external-sources");
|
|
await page.getByLabel("Административный токен").fill("test-token-not-sent");
|
|
await page.getByRole("button", { name: "Открыть очередь" }).click();
|
|
const card = page.locator(".moderation-card");
|
|
await expect(card).toContainText("rf4db");
|
|
await expect(card.getByLabel("Каноническая рыба")).toContainText("Щука");
|
|
await expect(card.getByRole("button", { name: "Опубликовать" })).toBeDisabled();
|
|
expect(mutations).toBe(0);
|
|
const dimensions = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }));
|
|
expect(dimensions.scroll).toBeLessThanOrEqual(dimensions.width);
|
|
});
|
|
|
|
test(`admin media review is read-only on ${viewport.name}`, async ({ page }) => {
|
|
await page.setViewportSize(viewport);
|
|
let mutations = 0;
|
|
await page.route("**/api/v1/**", async route => {
|
|
const request = route.request();
|
|
if (request.method() !== "GET") mutations += 1;
|
|
const path = new URL(request.url()).pathname;
|
|
if (path.endsWith("/admin/media/catalog")) return route.fulfill({ json: [{ id: "a".repeat(64), status: "upgrade_stored", entity_type: "fish", entity_key: "fish:pike", label: "Щука", width: 1024, height: 1024, content_type: "image/webp", image_url: "/api/v1/admin/media/assets/" + "a".repeat(64), source_system: "rf4db", source_url: "https://rf4db.com/fish/pike", derivatives: [{ role: "card", format: "webp", width: 256, height: 256 }] }] });
|
|
return route.fulfill({ status: 404 });
|
|
});
|
|
await page.goto("/admin/media");
|
|
await page.getByLabel("Административный токен").fill("test-token-not-sent");
|
|
await page.getByRole("button", { name: "Открыть медиатеку" }).click();
|
|
await expect(page.locator(".media-library__card")).toContainText("Щука");
|
|
await expect(page.locator(".media-library__card")).toContainText("upgrade_stored");
|
|
expect(mutations).toBe(0);
|
|
const dimensions = await page.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }));
|
|
expect(dimensions.scroll).toBeLessThanOrEqual(dimensions.width);
|
|
});
|
|
}
|
|
|
|
test("public pages fit the narrow viewport and expose the skip link", async ({ page }) => {
|
|
await page.setViewportSize({ width: 320, height: 800 });
|
|
for (const path of ["/", "/records", "/report", "/waterbodies", "/status"]) {
|
|
await page.goto(path);
|
|
const dimensions = await page.evaluate(() => ({
|
|
clientWidth: document.documentElement.clientWidth,
|
|
scrollWidth: document.documentElement.scrollWidth,
|
|
}));
|
|
expect(dimensions.scrollWidth, `${path} must not scroll horizontally`).toBeLessThanOrEqual(dimensions.clientWidth);
|
|
}
|
|
|
|
await page.goto("/report");
|
|
await page.keyboard.press("Tab");
|
|
await expect(page.locator(".skip-link")).toBeFocused();
|
|
await expect(page.locator(".skip-link")).toBeVisible();
|
|
});
|