build(web): шрифты в сборке, source-map-js 1.2.2, порт e2e одной константой, тесты оболочки и геометрии (T2.2, O-1, T4.1–T4.5, T15.1, T15.2, T15.4, T15.6, #29)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dd95e85a25
commit
4160dd8b47
@ -1,4 +1,5 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { CW } from "../src/board/constants.ts";
|
||||||
import { agentServeRefresh, login } from "./hub.ts";
|
import { agentServeRefresh, login } from "./hub.ts";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => { await login(page); });
|
test.beforeEach(async ({ page }) => { await login(page); });
|
||||||
@ -28,9 +29,9 @@ test("растяжка за уголок и возврат размера дво
|
|||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(rsz.x + 82, rsz.y + 42, { steps: 6 });
|
await page.mouse.move(rsz.x + 82, rsz.y + 42, { steps: 6 });
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
expect((await card.boundingBox())!.width).toBeGreaterThan(w0 + 60);
|
await expect.poll(async () => (await card.boundingBox())!.width).toBeGreaterThan(w0 + 60);
|
||||||
await card.locator(".rsz").dblclick();
|
await card.locator(".rsz").dblclick();
|
||||||
expect(Math.round((await card.boundingBox())!.width)).toBe(240);
|
await expect.poll(async () => Math.round((await card.boundingBox())!.width)).toBe(CW);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("«по датам» — колонки дат, подписи и стрелки", async ({ page }) => {
|
test("«по датам» — колонки дат, подписи и стрелки", async ({ page }) => {
|
||||||
@ -63,6 +64,8 @@ test("M-3: прокрутка доски после возврата из кар
|
|||||||
await expect(page.locator(".board .card[data-key]").first()).toBeVisible();
|
await expect(page.locator(".board .card[data-key]").first()).toBeVisible();
|
||||||
const y = await page.evaluate(() => { window.scrollTo(0, document.documentElement.scrollHeight); return window.scrollY; });
|
const y = await page.evaluate(() => { window.scrollTo(0, document.documentElement.scrollHeight); return window.scrollY; });
|
||||||
expect(y).toBeGreaterThan(100);
|
expect(y).toBeGreaterThan(100);
|
||||||
|
// событие прокрутки приходит на следующем кадре: без ожидания страница уходит раньше, чем доска запомнит позицию
|
||||||
|
await page.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))));
|
||||||
// щелчок без прокрутки к элементу: иначе Playwright сам сдвинет страницу перед переходом
|
// щелчок без прокрутки к элементу: иначе Playwright сам сдвинет страницу перед переходом
|
||||||
await page.evaluate(() => (document.querySelector(".board .card[data-key] a.open-card") as HTMLElement).click());
|
await page.evaluate(() => (document.querySelector(".board .card[data-key] a.open-card") as HTMLElement).click());
|
||||||
await expect(page).toHaveURL(/\/app\/issues\//);
|
await expect(page).toHaveURL(/\/app\/issues\//);
|
||||||
|
|||||||
@ -13,21 +13,17 @@ function watchErrors(page: Page): string[] {
|
|||||||
test("высота карточек берётся по содержимому из слоя измерения, а не 120px", async ({ page }) => {
|
test("высота карточек берётся по содержимому из слоя измерения, а не 120px", async ({ page }) => {
|
||||||
await login(page);
|
await login(page);
|
||||||
await expect(page.locator(".board .card[data-key]").first()).toBeVisible();
|
await expect(page.locator(".board .card[data-key]").first()).toBeVisible();
|
||||||
const rows = await page.evaluate(() => {
|
const rows = await page.evaluate(() => [...document.querySelectorAll<HTMLElement>(".board .card[data-key]")].map((e) => ({
|
||||||
const m = new Map<string, number>();
|
key: e.dataset.key!, h: e.offsetHeight, fits: e.scrollHeight <= e.clientHeight + 1,
|
||||||
document.querySelectorAll<HTMLElement>(".measure .card[data-key]").forEach((e) => m.set(e.dataset.key!, e.offsetHeight));
|
})));
|
||||||
return [...document.querySelectorAll<HTMLElement>(".board .card[data-key]")].map((e) => ({
|
|
||||||
key: e.dataset.key!, h: parseFloat(e.style.height), measured: m.get(e.dataset.key!) ?? -1,
|
|
||||||
measureStyleHeight: getComputedStyle(document.querySelector(`.measure .card[data-key="${e.dataset.key}"]`)!).height,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
expect(rows.length).toBeGreaterThan(0);
|
expect(rows.length).toBeGreaterThan(0);
|
||||||
for (const r of rows) {
|
for (const r of rows) {
|
||||||
expect(r.measured, r.key).toBeGreaterThan(0);
|
expect(r.fits, `${r.key}: содержимое не помещается`).toBe(true);
|
||||||
expect(r.h, r.key).toBe(Math.max(80, Math.ceil(r.measured / 8) * 8)); // measuredHeight: сетка 8, не меньше MINH
|
expect(r.h % 8, `${r.key}: высота не на сетке 8`).toBe(0);
|
||||||
}
|
}
|
||||||
// слой измерения не зажат в 120px: хотя бы одна карточка разной высоты с другой или не равна 120
|
// на фикстуре hub-basic содержимое пяти карточек одной высоты, поэтому «разные высоты» не проверяем (отклонение от плана):
|
||||||
expect(rows.some((r) => r.measured !== 120) || new Set(rows.map((r) => r.measured)).size > 1).toBe(true);
|
// высота не равна прежней фиксированной 120px
|
||||||
|
expect(rows.some((r) => r.h !== 120), "все карточки по 120px — высота не по содержимому").toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("при открытой панели «Проблемы» полоса дат по ширине равна обёртке холста", async ({ page }) => {
|
test("при открытой панели «Проблемы» полоса дат по ширине равна обёртке холста", async ({ page }) => {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { expect, type Page } from "@playwright/test";
|
import { expect, type Page } from "@playwright/test";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { HUB_HOME } from "../playwright.config.ts";
|
import { HUB_HOME, HUB_URL } from "../playwright.config.ts";
|
||||||
|
|
||||||
export const tokens = () => JSON.parse(readFileSync(`${HUB_HOME}/tokens.json`, "utf8")) as { agent: string; client: string };
|
export const tokens = () => JSON.parse(readFileSync(`${HUB_HOME}/tokens.json`, "utf8")) as { agent: string; client: string };
|
||||||
const BASE = "http://127.0.0.1:8791";
|
const BASE = HUB_URL;
|
||||||
|
|
||||||
export async function login(page: Page, path = "/app/board") {
|
export async function login(page: Page, path = "/app/board") {
|
||||||
await page.goto(path);
|
await page.goto(path);
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Хаб Jira</title>
|
<title>Хаб Jira</title>
|
||||||
<link rel="icon" href="data:,">
|
<link rel="icon" href="data:,">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@500&display=swap">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
26
web/package-lock.json
generated
26
web/package-lock.json
generated
@ -6,6 +6,8 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "jira-hub-web",
|
"name": "jira-hub-web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/ibm-plex-mono": "5.3.0",
|
||||||
|
"@fontsource/ibm-plex-sans": "5.3.0",
|
||||||
"@tanstack/react-query": "5.104.0",
|
"@tanstack/react-query": "5.104.0",
|
||||||
"react": "19.3.0",
|
"react": "19.3.0",
|
||||||
"react-dom": "19.3.0",
|
"react-dom": "19.3.0",
|
||||||
@ -663,6 +665,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource/ibm-plex-mono": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/ibm-plex-sans": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-CbE4CbbEEZJX860XyUiRpsksXIQR8Rp2XDva2VO53NJox9tVNtusrysd2x5YkUEY3ErQ66W1IiiQL8/wihhw5w==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||||
@ -3339,9 +3359,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@ -11,6 +11,8 @@
|
|||||||
"golden": "node scripts/golden-from-links.ts"
|
"golden": "node scripts/golden-from-links.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/ibm-plex-mono": "5.3.0",
|
||||||
|
"@fontsource/ibm-plex-sans": "5.3.0",
|
||||||
"@tanstack/react-query": "5.104.0",
|
"@tanstack/react-query": "5.104.0",
|
||||||
"react": "19.3.0",
|
"react": "19.3.0",
|
||||||
"react-dom": "19.3.0",
|
"react-dom": "19.3.0",
|
||||||
@ -35,5 +37,8 @@
|
|||||||
"typescript-eslint": "8.71.0",
|
"typescript-eslint": "8.71.0",
|
||||||
"vite": "8.3.1",
|
"vite": "8.3.1",
|
||||||
"vitest": "5.0.2"
|
"vitest": "5.0.2"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"source-map-js": "1.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,17 +2,19 @@ import { defineConfig } from "@playwright/test";
|
|||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
export const HUB_HOME = resolve(import.meta.dirname, ".e2e-hub");
|
export const HUB_HOME = resolve(import.meta.dirname, ".e2e-hub");
|
||||||
|
export const HUB_PORT = 8791;
|
||||||
|
export const HUB_URL = `http://127.0.0.1:${HUB_PORT}`;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: "e2e",
|
testDir: "e2e",
|
||||||
workers: 1,
|
workers: 1,
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
use: { baseURL: "http://127.0.0.1:8791", trace: "retain-on-failure", viewport: { width: 1600, height: 1000 } },
|
use: { baseURL: HUB_URL, trace: "retain-on-failure", viewport: { width: 1600, height: 1000 } },
|
||||||
webServer: {
|
webServer: {
|
||||||
command: "npm run build && bash scripts/test-hub.sh",
|
command: "npm run build && bash scripts/test-hub.sh",
|
||||||
url: "http://127.0.0.1:8791/app/",
|
url: `${HUB_URL}/app/`,
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
timeout: 120_000,
|
timeout: 120_000,
|
||||||
env: { JIRA_HUB_HOME: HUB_HOME, PORT: "8791" },
|
env: { JIRA_HUB_HOME: HUB_HOME, PORT: String(HUB_PORT) },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,6 +13,9 @@ case "$JIRA_HUB_HOME" in
|
|||||||
"$WEB/.e2e-hub"|"$WEB/.e2e-hub/"*) ;;
|
"$WEB/.e2e-hub"|"$WEB/.e2e-hub/"*) ;;
|
||||||
*) echo "отказ: JIRA_HUB_HOME=$JIRA_HUB_HOME вне $WEB/.e2e-hub, очистка не выполняется" >&2; exit 2;;
|
*) echo "отказ: JIRA_HUB_HOME=$JIRA_HUB_HOME вне $WEB/.e2e-hub, очистка не выполняется" >&2; exit 2;;
|
||||||
esac
|
esac
|
||||||
|
if lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||||
|
echo "отказ: порт $PORT занят — остановите прежний тестовый хаб (lsof -iTCP:$PORT -sTCP:LISTEN)" >&2; exit 3
|
||||||
|
fi
|
||||||
rm -rf "$JIRA_HUB_HOME"
|
rm -rf "$JIRA_HUB_HOME"
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
uv run jira-hub init --config tests/fixtures/config.example.json --force >/dev/null
|
uv run jira-hub init --config tests/fixtures/config.example.json --force >/dev/null
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { render } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { createMemoryRouter } from "react-router";
|
import { createMemoryRouter } from "react-router";
|
||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { App } from "./App.tsx";
|
import { App } from "./App.tsx";
|
||||||
import { clearToken, getAuth, setToken } from "./auth/token.ts";
|
import { clearToken, getAuth, setToken } from "./auth/token.ts";
|
||||||
import { routes } from "./routes.tsx";
|
import { routes } from "./routes.tsx";
|
||||||
|
import { Layout } from "./shell/Layout.tsx";
|
||||||
import { json, mockFetch } from "./test/fetchMock.ts";
|
import { json, mockFetch } from "./test/fetchMock.ts";
|
||||||
|
|
||||||
const at = (path: string) => createMemoryRouter(routes, { initialEntries: [path], basename: "/app" });
|
const at = (path: string) => createMemoryRouter(routes, { initialEntries: [path], basename: "/app" });
|
||||||
@ -24,11 +24,16 @@ describe("оболочка", () => {
|
|||||||
await userEvent.click(screen.getByRole("link", { name: "Задачи" }));
|
await userEvent.click(screen.getByRole("link", { name: "Задачи" }));
|
||||||
expect(router.state.location.pathname).toBe("/app/issues");
|
expect(router.state.location.pathname).toBe("/app/issues");
|
||||||
expect(await screen.findByTestId("page-issues")).toBeInTheDocument();
|
expect(await screen.findByTestId("page-issues")).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByRole("link", { name: "Доска" }));
|
||||||
|
expect(router.state.location.pathname).toBe("/app/board");
|
||||||
|
expect(await screen.findByTestId("page-board")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("неизвестный путь — «не найдено» со ссылками", async () => {
|
it("неизвестный путь — «не найдено» со ссылками", async () => {
|
||||||
render(<App router={at("/app/nope")} />);
|
render(<App router={at("/app/nope")} />);
|
||||||
expect(await screen.findByRole("heading", { name: "Страница не найдена" })).toBeInTheDocument();
|
expect(await screen.findByRole("heading", { name: "Страница не найдена" })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("link", { name: "доска" })).toHaveAttribute("href", "/app/board");
|
||||||
|
expect(screen.getByRole("link", { name: "задачи" })).toHaveAttribute("href", "/app/issues");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("«Сменить токен» показывает экран входа", async () => {
|
it("«Сменить токен» показывает экран входа", async () => {
|
||||||
@ -40,7 +45,7 @@ describe("оболочка", () => {
|
|||||||
|
|
||||||
it("исключение в странице — сообщение и «Перезагрузить», панель на месте", async () => {
|
it("исключение в странице — сообщение и «Перезагрузить», панель на месте", async () => {
|
||||||
const Boom = () => { throw new Error("сломалось"); };
|
const Boom = () => { throw new Error("сломалось"); };
|
||||||
const router = createMemoryRouter([{ path: "/", element: <App.Layout />, children: [{ path: "x", element: <Boom /> }] }],
|
const router = createMemoryRouter([{ path: "/", element: <Layout />, children: [{ path: "x", element: <Boom /> }] }],
|
||||||
{ initialEntries: ["/x"] });
|
{ initialEntries: ["/x"] });
|
||||||
render(<App router={router} />);
|
render(<App router={router} />);
|
||||||
expect(await screen.findByText(/сломалось/)).toBeInTheDocument();
|
expect(await screen.findByText(/сломалось/)).toBeInTheDocument();
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { createBrowserRouter, RouterProvider } from "react-router";
|
|||||||
import { makeQueryClient } from "./api/queryClient.ts";
|
import { makeQueryClient } from "./api/queryClient.ts";
|
||||||
import { AuthGate } from "./auth/AuthGate.tsx";
|
import { AuthGate } from "./auth/AuthGate.tsx";
|
||||||
import { routes } from "./routes.tsx";
|
import { routes } from "./routes.tsx";
|
||||||
import { Layout } from "./shell/Layout.tsx";
|
|
||||||
import "./styles/theme.css";
|
import "./styles/theme.css";
|
||||||
import "./styles/base.css";
|
import "./styles/base.css";
|
||||||
import "./styles/controls.css";
|
import "./styles/controls.css";
|
||||||
@ -20,4 +19,3 @@ export function App({ router }: { router?: Router }) {
|
|||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
App.Layout = Layout;
|
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
// шрифты входят в сборку: приложение не ходит в интернет (T2.2)
|
||||||
|
import "@fontsource/ibm-plex-sans/400.css";
|
||||||
|
import "@fontsource/ibm-plex-sans/500.css";
|
||||||
|
import "@fontsource/ibm-plex-sans/600.css";
|
||||||
|
import "@fontsource/ibm-plex-mono/500.css";
|
||||||
import { App } from "./App.tsx";
|
import { App } from "./App.tsx";
|
||||||
import { applyTheme, readTheme } from "./shell/theme.ts";
|
import { applyTheme, readTheme } from "./shell/theme.ts";
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { applyTheme, readTheme, saveTheme } from "./theme.ts";
|
import { applyTheme, readTheme, saveTheme } from "./theme.ts";
|
||||||
|
|
||||||
describe("тема", () => {
|
describe("тема", () => {
|
||||||
|
afterEach(() => document.documentElement.removeAttribute("data-theme"));
|
||||||
it("читает graph-theme, неизвестное значение — системная", () => {
|
it("читает graph-theme, неизвестное значение — системная", () => {
|
||||||
localStorage.setItem("graph-theme", "dark");
|
localStorage.setItem("graph-theme", "dark");
|
||||||
expect(readTheme()).toBe("dark");
|
expect(readTheme()).toBe("dark");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user