diff --git a/web/src/issues/IssuePage.test.tsx b/web/src/issues/IssuePage.test.tsx index 2f9fa35..801f37e 100644 --- a/web/src/issues/IssuePage.test.tsx +++ b/web/src/issues/IssuePage.test.tsx @@ -180,6 +180,13 @@ describe("карточка задачи", () => { await waitFor(() => expect(screen.queryByRole("alert")).toBeNull()); }); + it("O-10: ссылка в ветке «задачи нет в зеркале» стоит внутри .issue (цвет темы)", async () => { + mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(404, { error: "not_found", detail: "issue" })); + open("/issues/AURORA-1"); + const link = await screen.findByRole("link", { name: "к списку задач" }); + expect(link.closest(".issue")).not.toBeNull(); + }); + it("O-11: задача без команд — текст вместо пустой таблицы", async () => { mockFetch((_m, url) => url.pathname === "/settings" ? json(200, settings) : json(200, issue("AURORA-9001", { commands: [] }))); open("/issues/AURORA-9001"); diff --git a/web/src/issues/IssuePage.tsx b/web/src/issues/IssuePage.tsx index 4e2f707..9a8b93c 100644 --- a/web/src/issues/IssuePage.tsx +++ b/web/src/issues/IssuePage.tsx @@ -17,7 +17,7 @@ export function IssuePage() { if (issue.isPending) return

загрузка {key}…

; if (issue.error instanceof ApiError && issue.error.status === 404) { - return

Задачи {key} нет в зеркале хаба. к списку задач

; + return

Задачи {key} нет в зеркале хаба. к списку задач

; } if (issue.isError && !issue.data) { return ( diff --git a/web/src/issues/IssuesPage.test.tsx b/web/src/issues/IssuesPage.test.tsx index fefda5b..f29233f 100644 --- a/web/src/issues/IssuesPage.test.tsx +++ b/web/src/issues/IssuesPage.test.tsx @@ -10,6 +10,7 @@ import { renderWithProviders } from "../test/render.tsx"; import { IssuesPage } from "./IssuesPage.tsx"; const SNAP = JSON.parse(readFileSync(join(import.meta.dirname, "..", "board", "__fixtures__", "hub-basic.board.json"), "utf8")); +const deferred = () => { let resolve!: (v: T) => void; const promise = new Promise((r) => { resolve = r; }); return { promise, resolve }; }; const row = (n: number) => ({ ...SNAP.issues[1], key: `AURORA-${9000 + n}`, summary: `Задача ${n}`, fetched_at: "" }); function hub(pages: (params: URLSearchParams) => unknown[]) { @@ -117,6 +118,23 @@ describe("список задач", () => { expect(sel).toHaveValue("AWPCA_Спринт 0"); }); + it("T5.1: значение есть на доске, снимок грузится — подпись без пометки и после загрузки", async () => { + const board = deferred(); + mockFetch((_m, url) => { + if (url.pathname === "/settings") return json(200, settings); + if (url.pathname === "/board") return board.promise; + return json(200, { issues: [row(1)] }); + }); + renderWithProviders(, { route: "/issues?sprint=AWPCA_Спринт 2", path: "/issues" }); + await screen.findByRole("link", { name: "AURORA-9001" }); + const sel = await screen.findByLabelText("Спринт"); + expect(within(sel).getByRole("option", { name: "AWPCA_Спринт 2" })).toBeInTheDocument(); + board.resolve(json(200, SNAP)); + await waitFor(() => expect(within(sel).getByRole("option", { name: "AWPCA_Спринт 2" })).toBeInTheDocument()); + expect(within(sel).queryByRole("option", { name: /нет на доске/ })).toBeNull(); + expect(sel).toHaveValue("AWPCA_Спринт 2"); + }); + it("T13.1: пока снимок доски грузится, пометки «(нет на доске)» нет", async () => { mockFetch((_m, url) => { if (url.pathname === "/settings") return json(200, settings); diff --git a/web/src/issues/options.ts b/web/src/issues/options.ts index df31ca6..aa151d7 100644 --- a/web/src/issues/options.ts +++ b/web/src/issues/options.ts @@ -9,13 +9,13 @@ export const epicNames = (snap: BoardSnapshot | undefined): Record (i.planned_end || i.duedate || "").slice(0, 10); -// варианты выпадающих списков фильтров: из снимка /board и словарей настроек, без новых маршрутов API // T13.1: значение фильтра из адреса, которого нет среди вариантов (закрытый спринт, ушедший исполнитель), видно в списке; // пока снимок доски не загружен, пометку не ставим (R-4) export const withCurrent = (opts: Opt[], value: string | undefined, loaded: boolean): Opt[] => !value || opts.some((o) => o.value === value) ? opts : [{ value, label: loaded ? `${value} (нет на доске)` : value }, ...opts]; +// варианты выпадающих списков фильтров: из снимка /board и словарей настроек, без новых маршрутов API export function filterOptions(snap: BoardSnapshot | undefined, s: HubSettings | undefined) { const issues = snap?.issues ?? []; const names = epicNames(snap);