import { expect, test } from "@playwright/test"; import { login } from "./hub.ts"; // экраны доски: «Бэклог», «Спринт», «Kanban» (spec web-app «Спринты и бэклог»); данные — сид тестового хаба (R-27) type Page = import("@playwright/test").Page; const side = (page: Page) => page.getByRole("complementary", { name: "Панель приложения" }); // строка списка / карточка колонки по ключу задачи (ссылка с точным именем), без привязки к порядку в DOM const itemOf = (page: Page, key: string) => page.getByRole("listitem").filter({ has: page.getByRole("link", { name: key, exact: true }) }); const colOf = (page: Page, name: string) => page.getByRole("region", { name, exact: true }); test("«Бэклог» AWPCA: секции, «Прошлые спринты (1)», щелчок открывает карточку", async ({ page }) => { await login(page, "/app/jira/backlog?project=AWPCA"); await expect(page.getByRole("heading", { name: "AWPCA_Спринт 2" })).toBeVisible(); await expect(page.getByRole("heading", { name: "AWPCA_Спринт 3" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Бэклог", exact: true, level: 2 })).toBeVisible(); const past = page.getByText("Прошлые спринты (1)", { exact: true }); await expect(past).toBeVisible(); await past.click(); const pastSprint = page.locator("details.agile-past__sprint").filter({ has: page.locator("summary", { hasText: "AWPCA_Спринт 1" }) }); await pastSprint.locator("summary").click(); const row = pastSprint.getByRole("listitem").filter({ has: page.getByRole("link", { name: "AURORA-9001", exact: true }) }); await expect(row).toBeVisible(); await row.click(); await expect(page).toHaveURL(/\/app\/jira\/issues\/AURORA-9001/); await expect(page.getByRole("dialog")).toBeVisible(); await page.keyboard.press("Escape"); await expect(page).toHaveURL(/\/app\/jira\/backlog/); await expect(page.getByRole("dialog")).toHaveCount(0); }); test("«Спринт» AWPCA: колонки доски, «осталось 7 дн.», карточка окном", async ({ page }) => { await login(page, "/app/jira/sprint?project=AWPCA"); await expect(page.getByRole("heading", { name: "AWPCA_Спринт 2" })).toBeVisible(); await expect(page.getByText(/осталось \d+ дн\./)).toBeVisible(); for (const name of ["К выполнению", "В работе", "Готово"]) { await expect(page.getByRole("heading", { name, exact: true, level: 3 })).toBeVisible(); } await itemOf(page, "AURORA-9001").click(); await expect(page).toHaveURL(/\/app\/jira\/issues\/AURORA-9001/); await expect(page.getByRole("dialog")).toBeVisible(); await page.keyboard.press("Escape"); await expect(page).toHaveURL(/\/app\/jira\/sprint/); }); test("«Kanban»: «Бэклог (1)», колонки «В работе» и «Готово», подпись «за 14 дней»", async ({ page }) => { await login(page, "/app/jira/kanban?project=KANBAN_E2E"); await expect(page.getByText("Бэклог (1)", { exact: true })).toBeVisible(); await expect(colOf(page, "В работе")).toBeVisible(); await expect(colOf(page, "Готово")).toBeVisible(); await expect(colOf(page, "Бэклог")).toHaveCount(0); await expect(page.getByText("за 14 дней", { exact: true })).toBeVisible(); await expect(colOf(page, "В работе").getByRole("link", { name: "AURORA-9102", exact: true })).toBeVisible(); await expect(colOf(page, "Готово").getByRole("link", { name: "AURORA-9103", exact: true })).toBeVisible(); }); test("без scrum-доски: /app/jira/backlog?project=KANBAN_E2E — «У проекта нет scrum-доски», ссылка «Связи»", async ({ page }) => { await login(page, "/app/jira/backlog?project=KANBAN_E2E"); await expect(page.getByText("У проекта нет scrum-доски")).toBeVisible(); await page.getByRole("main").getByRole("link", { name: "Связи", exact: true }).click(); await expect(page).toHaveURL(/\/app\/jira\/links\?project=KANBAN_E2E/); }); test("меню по проекту: у AWPCA «Бэклог» и «Спринт», у KANBAN_E2E — «Kanban»", async ({ page }) => { await login(page, "/app/jira/links?project=AWPCA"); await expect(side(page).getByRole("link", { name: "Бэклог" })).toBeVisible(); await expect(side(page).getByRole("link", { name: "Спринт" })).toBeVisible(); await expect(side(page).getByRole("link", { name: "Kanban" })).toHaveCount(0); await page.goto("/app/jira/links?project=KANBAN_E2E"); await expect(side(page).getByRole("link", { name: "Kanban" })).toBeVisible(); await expect(side(page).getByRole("link", { name: "Бэклог" })).toHaveCount(0); await expect(side(page).getByRole("link", { name: "Спринт" })).toHaveCount(0); }); test("900 px: колонки «Спринта» прокручиваются внутри доски, страница без горизонтальной прокрутки", async ({ page }) => { await page.setViewportSize({ width: 900, height: 900 }); await login(page, "/app/jira/sprint?project=AWPCA"); await expect(colOf(page, "К выполнению")).toBeVisible(); const m = await page.evaluate(() => { const d = document.documentElement, c = document.querySelector(".agile-columns")!; return { doc: [d.scrollWidth, d.clientWidth], cols: [c.scrollWidth, c.clientWidth], bottom: c.getBoundingClientRect().bottom, gutter: c.getBoundingClientRect().left - document.querySelector(".agile")!.getBoundingClientRect().left }; }); expect(m.gutter, "колонки не получают второй отступ внутри страницы (T10.1)").toBe(0); expect(m.doc[0]).toBeLessThanOrEqual(m.doc[1]); expect(m.cols[0]).toBeGreaterThan(m.cols[1]); expect(m.bottom, "нижний край доски в окне").toBeLessThanOrEqual(900); }); test("«Широкая доска» 2560×1300: колонки одной высоты, доска в окне, горизонтальная прокрутка внутри, «Без колонки» достижима", async ({ page }) => { await page.setViewportSize({ width: 2560, height: 1300 }); await login(page, "/app/jira/sprint?project=WIDE_E2E"); await expect(colOf(page, "Колонка 1")).toBeVisible(); await expect(colOf(page, "Без колонки")).toHaveCount(1); const heights = () => page.evaluate(() => [...document.querySelectorAll(".agile-col")].map((c) => c.getBoundingClientRect().height)); await expect.poll(async () => { const h = await heights(); return Math.max(...h) - Math.min(...h); }, { message: "высоты колонок равны" }).toBeLessThanOrEqual(1); const m = await page.evaluate(() => { const board = document.querySelector(".agile-columns")!; const heights = [...document.querySelectorAll(".agile-col")].map((c) => c.getBoundingClientRect().height); return { heights, bottom: board.getBoundingClientRect().bottom, inner: window.innerHeight, width: board.getBoundingClientRect().width, sw: board.scrollWidth, cw: board.clientWidth, count: heights.length }; }); expect(m.count, "11 колонок и «Без колонки»").toBe(12); expect(m.bottom, "нижний край доски в окне").toBeLessThanOrEqual(m.inner); expect(m.sw, "доска прокручивается по горизонтали").toBeGreaterThan(m.cw); expect(m.width, "доска шире 1440 px (нет max-width)").toBeGreaterThan(1440); await page.evaluate(() => { const b = document.querySelector(".agile-columns")!; b.scrollLeft = b.scrollWidth; }); const extra = colOf(page, "Без колонки"); await expect(extra).toBeInViewport({ ratio: 1 }); const doc = await page.evaluate(() => [document.documentElement.scrollWidth, document.documentElement.clientWidth]); expect(doc[0], "страница без горизонтальной прокрутки").toBeLessThanOrEqual(doc[1]); }); test("узкий контейнер: на 900 px вместо панели «Эпики» — выбор «Эпик», на 1600 px — панель и кнопка", async ({ page }) => { await page.setViewportSize({ width: 900, height: 900 }); await login(page, "/app/jira/backlog?project=AWPCA"); await expect(page.getByRole("heading", { name: "AWPCA_Спринт 2" })).toBeVisible(); await expect(page.getByRole("complementary", { name: "Эпики" })).toBeHidden(); await expect(page.getByRole("button", { name: "Эпики", exact: true })).toBeHidden(); await expect(page.getByRole("combobox", { name: "Эпик" })).toBeVisible(); await page.setViewportSize({ width: 1600, height: 900 }); await expect(page.getByRole("complementary", { name: "Эпики" })).toBeVisible(); await expect(page.getByRole("button", { name: "Эпики", exact: true })).toBeVisible(); await expect(page.getByRole("combobox", { name: "Эпик" })).toBeHidden(); }); test("«Бэклог» AWPCA: панель «Эпики», выбор эпика оставляет только его задачи, свёртка секции переживает перезагрузку", async ({ page }) => { await login(page, "/app/jira/backlog?project=AWPCA"); const panel = page.getByRole("complementary", { name: "Эпики" }); await expect(panel).toBeVisible(); await expect(itemOf(page, "AURORA-9004")).toBeVisible(); await panel.getByRole("button", { name: /AURORA-9100/ }).click(); await expect(itemOf(page, "AURORA-9001")).toBeVisible(); await expect(itemOf(page, "AURORA-9004")).toHaveCount(0); await expect(itemOf(page, "AURORA-9008")).toHaveCount(0); await panel.getByRole("button", { name: "Все задачи" }).click(); await expect(itemOf(page, "AURORA-9004")).toBeVisible(); const toggle = page.getByRole("button", { name: "AWPCA_Спринт 3", exact: true }); await expect(toggle).toHaveAttribute("aria-expanded", "true"); await toggle.click(); await expect(toggle).toHaveAttribute("aria-expanded", "false"); await expect(itemOf(page, "AURORA-9004")).toHaveCount(0); await page.reload(); await expect(page.getByRole("button", { name: "AWPCA_Спринт 3", exact: true })).toHaveAttribute("aria-expanded", "false"); await expect(itemOf(page, "AURORA-9004")).toHaveCount(0); }); test("«Спринт» AWPCA: выбор двух спринтов пишет sprint=102,103 в адрес, скрытая колонка «Готово» переживает перезагрузку", async ({ page }) => { await login(page, "/app/jira/sprint?project=AWPCA"); await expect(colOf(page, "Готово")).toBeVisible(); await page.getByRole("button", { name: "AWPCA_Спринт 2" }).click(); await page.getByRole("checkbox", { name: /AWPCA_Спринт 3/ }).click(); await expect(page).toHaveURL(/sprint=102(,|%2C)103/); await expect(page.getByRole("heading", { name: "AWPCA_Спринт 3" })).toBeVisible(); await page.keyboard.press("Escape"); await page.getByRole("button", { name: "Колонки" }).click(); await page.getByRole("checkbox", { name: "Готово", exact: true }).click(); await expect(colOf(page, "Готово")).toHaveCount(0); await page.reload(); await expect(colOf(page, "В работе")).toBeVisible(); await expect(colOf(page, "Готово")).toHaveCount(0); }); test("390×800: документ без горизонтальной прокрутки, нижний край доски в окне (T8.1)", async ({ page }) => { await page.setViewportSize({ width: 390, height: 800 }); await login(page, "/app/jira/sprint?project=WIDE_E2E"); await expect(colOf(page, "Колонка 1")).toBeVisible(); const m = await page.evaluate(() => { const d = document.documentElement, b = document.querySelector(".agile-columns")!; return { doc: [d.scrollWidth, d.clientWidth], bottom: b.getBoundingClientRect().bottom, inner: window.innerHeight, sw: b.scrollWidth, cw: b.clientWidth }; }); expect(m.doc[0], JSON.stringify(m)).toBeLessThanOrEqual(m.doc[1]); expect(m.bottom, JSON.stringify(m)).toBeLessThanOrEqual(m.inner); expect(m.sw).toBeGreaterThan(m.cw); }); test("панель фильтров на 1600 px: «Исполнитель» и поиск на одной строке, подпись видна (I-2, M-2)", async ({ page }) => { await page.setViewportSize({ width: 1600, height: 900 }); await login(page, "/app/jira/sprint?project=AWPCA"); const search = page.getByRole("searchbox", { name: "Поиск по ключу или теме" }); const select = page.getByRole("combobox", { name: "Исполнитель" }); await expect(select).toBeVisible(); await expect(page.locator(".agile-toolbar__field").getByText("Исполнитель", { exact: true })).toBeVisible(); const mid = async (l: typeof search) => { const b = (await l.boundingBox())!; return b.y + b.height / 2; }; expect(Math.abs((await mid(search)) - (await mid(select))), "центры по вертикали").toBeLessThanOrEqual(2); const bar = (await page.locator(".agile-toolbar").boundingBox())!; const sel = (await select.boundingBox())!; expect(sel.width, "выбор не растянут на строку").toBeLessThan(bar.width / 2); });