fix(web): ссылка в ветке «задачи нет в зеркале» в .issue, тесты класса и подписи фильтра, withCurrent выше комментария (#29/O-10, T5.1, T5.4, #29)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-15 01:25:10 +04:00
parent 6940da5a20
commit a002812f12
4 changed files with 27 additions and 2 deletions

View File

@ -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");

View File

@ -17,7 +17,7 @@ export function IssuePage() {
if (issue.isPending) return <p className="hint" data-testid="page-issue">загрузка {key}…</p>;
if (issue.error instanceof ApiError && issue.error.status === 404) {
return <p data-testid="page-issue">Задачи {key} нет в зеркале хаба. <Link to="/issues">к списку задач</Link></p>;
return <p className="issue" data-testid="page-issue">Задачи {key} нет в зеркале хаба. <Link to="/issues">к списку задач</Link></p>;
}
if (issue.isError && !issue.data) {
return (

View File

@ -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 = <T,>() => { let resolve!: (v: T) => void; const promise = new Promise<T>((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<Response>();
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(<IssuesPage />, { 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);

View File

@ -9,13 +9,13 @@ export const epicNames = (snap: BoardSnapshot | undefined): Record<string, strin
export const dueOf = (i: { planned_end: string | null; duedate: string | null }) => (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);