Веб: правки вики в общей ленте (#140)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
558fe7812b
commit
93eff7eac0
@ -17,4 +17,9 @@ describe("сводка вики", () => {
|
||||
expect(screen.getByText("ещё не синхронизирована")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/0 страниц/)).toBeNull();
|
||||
});
|
||||
|
||||
it("у проекта с корнем — ссылка «Правки» на ленту с источником вики", () => {
|
||||
renderWithProviders(<Summary project={P} />);
|
||||
expect(screen.getByRole("link", { name: "Правки" })).toHaveAttribute("href", "/feed?project=AWPCA&sources=wiki");
|
||||
});
|
||||
});
|
||||
|
||||
@ -26,6 +26,7 @@ export function Summary({ project }: { project: Project }) {
|
||||
<p className="wiki-summary__text">{pages === null ? "ещё не синхронизирована" : `${pages} страниц`}</p>
|
||||
<form onSubmit={submit} role="search" className="wiki-summary__search"><SearchBox value={q} onChange={setQ} /></form>
|
||||
<Link to={`/wiki${base}`} className="wiki-summary__link">Вики</Link>
|
||||
<Link to={`/feed${base}&sources=wiki`} className="wiki-summary__link">Правки</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { api } from "../../../api/client.ts";
|
||||
|
||||
export interface WikiBatch { id: number; status: string }
|
||||
export interface WikiStatus {
|
||||
root: string; root_title: string | null; synced_at: string | null; pages: number; error: string | null; batch: WikiBatch | null;
|
||||
root: string; root_title: string | null; synced_at: string | null; history_at?: string | null; pages: number; error: string | null; batch: WikiBatch | null;
|
||||
}
|
||||
export interface WikiNode { code: string; title: string | null; depth: number; parent_code: string | null; size: number | null }
|
||||
export interface WikiAttachment { id: number; name: string | null; state: string; size: number | null; error?: string | null; image?: boolean }
|
||||
@ -14,6 +14,14 @@ export interface WikiPageData {
|
||||
export interface WikiHit { code: string; title: string | null; path: string; snippet: string }
|
||||
export interface WikiAttachmentState { id: number; name: string | null; state: string; error: string | null; size: number | null }
|
||||
|
||||
export interface WikiChange {
|
||||
at: string; page_code: string; title: string | null; path: string | null; version: string;
|
||||
author: { login: string | null; name: string | null }; added: number; removed: number;
|
||||
}
|
||||
export interface WikiChanges {
|
||||
since: string; until: string; synced_at: string | null; history_at: string | null; truncated: boolean; changes: WikiChange[];
|
||||
}
|
||||
|
||||
export const POLL_MS = 3000; // опрос хода синхронизации, пока пакет не завершён
|
||||
export const SEARCH_LIMIT = 20;
|
||||
|
||||
@ -24,6 +32,7 @@ export const wikiQk = {
|
||||
tree: (p: string) => ["wiki", p, "tree"] as const,
|
||||
page: (p: string, code: string) => ["wiki", p, "page", code] as const,
|
||||
search: (p: string, q: string) => ["wiki", p, "search", q] as const,
|
||||
changes: (p: string, period: string) => ["wiki", p, "changes", period] as const,
|
||||
};
|
||||
|
||||
const terminal = (b: WikiBatch | null) => b === null || b.status === "done" || b.status === "cancelled";
|
||||
@ -62,3 +71,14 @@ export const useWikiSearch = (project: string, q: string) =>
|
||||
queryKey: wikiQk.search(project, q), enabled: q !== "",
|
||||
queryFn: () => api.get<{ results: WikiHit[] }>("/wiki/search", { project, q, limit: SEARCH_LIMIT }).then((r) => r.results),
|
||||
});
|
||||
|
||||
// правки за период для ленты; границы считает запрос в момент выполнения (перечитывание берёт «до сейчас»)
|
||||
export const WIKI_CHANGES_LIMIT = 2000;
|
||||
export const useWikiChanges = (project: string, period: string, bounds: () => { since: Date; until: Date }, enabled: boolean) =>
|
||||
useQuery({
|
||||
queryKey: wikiQk.changes(project, period), enabled, refetchOnWindowFocus: true,
|
||||
queryFn: () => {
|
||||
const { since, until } = bounds();
|
||||
return api.get<WikiChanges>("/wiki/changes", { project, since: since.toISOString(), until: until.toISOString(), limit: WIKI_CHANGES_LIMIT });
|
||||
},
|
||||
});
|
||||
|
||||
31
web/src/modules/wiki/feed/WikiFeedGroup.tsx
Normal file
31
web/src/modules/wiki/feed/WikiFeedGroup.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { Panel } from "../../../ui/index.ts";
|
||||
import { eventTime } from "../../../ui/format.ts";
|
||||
import { versionHref, wikiPageHref } from "../paths.ts";
|
||||
import { authorName, deltaText, type PageGroup } from "./group.ts";
|
||||
import "./feed.css";
|
||||
|
||||
// группа ленты — одна страница вики: название ссылкой на страницу, путь и правки; правка открывает окно версии поверх ленты
|
||||
export function WikiFeedGroup({ project, group, now }: { project: string; group: PageGroup; now: Date }) {
|
||||
const loc = useLocation();
|
||||
const state = { background: { pathname: loc.pathname, search: loc.search }, depth: 1 };
|
||||
return (
|
||||
<Panel className="wiki-feed-group" data-testid="feed-group">
|
||||
<header className="wiki-feed-group__head">
|
||||
<Link className="wiki-feed-group__title" to={wikiPageHref(project, group.code)}>{group.title || group.code}</Link>
|
||||
{group.path && <span className="wiki-feed-group__path">{group.path}</span>}
|
||||
</header>
|
||||
<ul className="wiki-feed-edits">
|
||||
{group.changes.map((c) => (
|
||||
<li key={c.version}>
|
||||
<Link className="wiki-feed-edit" to={versionHref(project, c.page_code, c.version)} state={state}>
|
||||
<span className="wiki-feed-edit__time">{eventTime(c.at, now)}</span>
|
||||
<span className="wiki-feed-edit__author">{authorName(c.author)}</span>
|
||||
<span className="wiki-feed-edit__delta">{deltaText(c)}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
12
web/src/modules/wiki/feed/feed.css
Normal file
12
web/src/modules/wiki/feed/feed.css
Normal file
@ -0,0 +1,12 @@
|
||||
/* источник «Вики» в ленте (spec web-app «Правки вики в ленте»): только токены дизайн-системы */
|
||||
.wiki-feed-group{display:flex;flex-direction:column;gap:var(--space-3);min-width:0}
|
||||
.wiki-feed-group__head{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--space-1) var(--space-3)}
|
||||
.wiki-feed-group__title{font-weight:600;min-width:0;overflow-wrap:anywhere;color:var(--color-action-primary);text-decoration:none}
|
||||
.wiki-feed-group__title:hover{text-decoration:underline}
|
||||
.wiki-feed-group__path{min-width:0;overflow-wrap:anywhere;font-size:13px;color:var(--color-text-secondary)}
|
||||
.wiki-feed-edits{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--space-2)}
|
||||
.wiki-feed-edit{display:flex;flex-wrap:wrap;gap:var(--space-1) var(--space-3);font-size:var(--fs-body);line-height:var(--lh-body);min-width:0;color:var(--color-text-primary);text-decoration:none}
|
||||
.wiki-feed-edit:hover .wiki-feed-edit__author{text-decoration:underline}
|
||||
.wiki-feed-edit__time{flex:0 0 auto;font-family:var(--font-mono);font-size:13px;color:var(--color-text-secondary);white-space:nowrap}
|
||||
.wiki-feed-edit__author{font-weight:600;min-width:0;overflow-wrap:anywhere}
|
||||
.wiki-feed-edit__delta{font-family:var(--font-mono);font-size:13px;color:var(--color-text-secondary);white-space:nowrap}
|
||||
24
web/src/modules/wiki/feed/group.test.ts
Normal file
24
web/src/modules/wiki/feed/group.test.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { WikiChange } from "../api/queries.ts";
|
||||
import { authorName, deltaText, groupByPage } from "./group.ts";
|
||||
|
||||
const c = (page_code: string, version: string): WikiChange => ({
|
||||
at: "2026-10-28T09:00:00+00:00", page_code, title: page_code, path: null, version,
|
||||
author: { login: null, name: null }, added: 1, removed: 0,
|
||||
});
|
||||
|
||||
describe("группировка правок вики", () => {
|
||||
it("по странице, по первому появлению; правки — в порядке ответа", () => {
|
||||
const g = groupByPage([c("DOC-3", "V3"), c("DOC-2", "V2"), c("DOC-3", "V1")]);
|
||||
expect(g.map((x) => x.code)).toEqual(["DOC-3", "DOC-2"]);
|
||||
expect(g[0].changes.map((x) => x.version)).toEqual(["V3", "V1"]);
|
||||
});
|
||||
it("deltaText: плюс, косая черта, минус U+2212", () => {
|
||||
expect(deltaText({ added: 40, removed: 12 })).toBe("+40 / −12");
|
||||
});
|
||||
it("authorName: имя, иначе логин, иначе «—»", () => {
|
||||
expect(authorName({ login: "x", name: null })).toBe("x");
|
||||
expect(authorName({ login: "x", name: "Икс" })).toBe("Икс");
|
||||
expect(authorName({ login: null, name: null })).toBe("—");
|
||||
});
|
||||
});
|
||||
18
web/src/modules/wiki/feed/group.ts
Normal file
18
web/src/modules/wiki/feed/group.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { WikiChange } from "../api/queries.ts";
|
||||
|
||||
export type PageGroup = { code: string; title: string | null; path: string | null; changes: WikiChange[] };
|
||||
|
||||
// правки одной страницы вместе; группы — по первому появлению (ответ хаба идёт от новых к старым), правки — в порядке ответа
|
||||
export function groupByPage(changes: WikiChange[]): PageGroup[] {
|
||||
const by = new Map<string, PageGroup>();
|
||||
for (const c of changes) {
|
||||
let g = by.get(c.page_code);
|
||||
if (!g) { g = { code: c.page_code, title: c.title, path: c.path, changes: [] }; by.set(c.page_code, g); }
|
||||
g.changes.push(c);
|
||||
}
|
||||
return [...by.values()];
|
||||
}
|
||||
|
||||
export const deltaText = (c: { added: number; removed: number }): string => `+${c.added} / −${c.removed}`;
|
||||
|
||||
export const authorName = (a: { login: string | null; name: string | null }): string => a.name || a.login || "—";
|
||||
29
web/src/modules/wiki/feed/source.tsx
Normal file
29
web/src/modules/wiki/feed/source.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useMemo } from "react";
|
||||
import type { FeedResult, FeedSource, FeedSourceInput } from "../../types.ts";
|
||||
import { useWikiChanges } from "../api/queries.ts";
|
||||
import { groupByPage } from "./group.ts";
|
||||
import { WikiFeedGroup } from "./WikiFeedGroup.tsx";
|
||||
|
||||
const EMPTY: FeedResult = { groups: [], count: 0, window: null, pending: false, error: null, truncated: false, notice: null };
|
||||
|
||||
function useWikiFeed(input: FeedSourceInput): FeedResult {
|
||||
const project = input.project.label;
|
||||
const q = useWikiChanges(project, input.period, input.bounds, input.enabled);
|
||||
const data = input.enabled ? q.data : undefined;
|
||||
const updatedAt = q.dataUpdatedAt;
|
||||
const groups = useMemo(() => {
|
||||
const now = new Date(updatedAt || Date.now()); // «текущие сутки» для времени правок — на момент ответа
|
||||
return groupByPage(data?.changes ?? []).map((g) => ({
|
||||
key: g.code, at: g.changes[0]?.at ?? "", node: <WikiFeedGroup project={project} group={g} now={now} />,
|
||||
}));
|
||||
}, [data, updatedAt, project]);
|
||||
if (!input.enabled) return EMPTY;
|
||||
return {
|
||||
groups, count: data?.changes.length ?? 0, window: data ? { since: data.since, until: data.until } : null,
|
||||
pending: q.isPending, truncated: !!data?.truncated,
|
||||
error: q.isError ? { message: (q.error as Error).message, retry: () => void q.refetch() } : null,
|
||||
notice: data && data.history_at === null ? "история вики ещё не загружена — нужен eva-agent v2" : null,
|
||||
};
|
||||
}
|
||||
|
||||
export const wikiFeed: FeedSource = { available: (p) => !!p.wiki_root, useFeed: useWikiFeed };
|
||||
@ -1,6 +1,7 @@
|
||||
import { ProjectGate } from "../../projects/ProjectGate.tsx";
|
||||
import { IconBook } from "../../ui/index.ts";
|
||||
import type { HubModule } from "../types.ts";
|
||||
import { wikiFeed } from "./feed/source.tsx";
|
||||
import { wikiStatus } from "./status.ts";
|
||||
import { Summary } from "./Summary.tsx";
|
||||
import { WikiPage } from "./WikiPage.tsx";
|
||||
@ -12,5 +13,6 @@ export const wikiModule: HubModule = {
|
||||
nav: [{ to: "/wiki", label: "Вики", icon: <IconBook />, padded: true }],
|
||||
status: wikiStatus,
|
||||
Summary,
|
||||
feed: wikiFeed,
|
||||
projectFields: [{ name: "wiki_root", label: "Корень вики", hint: "DOC-… или ссылка на страницу Eva", kind: "text" }],
|
||||
};
|
||||
|
||||
10
web/src/modules/wiki/paths.ts
Normal file
10
web/src/modules/wiki/paths.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// адреса вики: страница с выбранным кодом и окно версии (окно — поверх текущей страницы)
|
||||
export const VERSION_PATH = "/wiki/pages/:code/versions/:version";
|
||||
|
||||
export function wikiPageHref(project: string, code: string): string {
|
||||
return `/wiki?${new URLSearchParams({ project, page: code })}`;
|
||||
}
|
||||
|
||||
export function versionHref(project: string, code: string, version: string): string {
|
||||
return `/wiki/pages/${encodeURIComponent(code)}/versions/${encodeURIComponent(version)}?${new URLSearchParams({ project })}`;
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { act } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { App } from "../../App.tsx";
|
||||
import type { ChangeEvent } from "../../modules/jira/api/types.ts";
|
||||
import type { WikiChange, WikiChanges } from "../../modules/wiki/api/queries.ts";
|
||||
import { setToken } from "../../auth/token.ts";
|
||||
import settings from "../../modules/jira/board/__fixtures__/settings.json";
|
||||
import { routes } from "../../routes.tsx";
|
||||
@ -25,7 +26,12 @@ const ev = (over: Partial<ChangeEvent>): ChangeEvent => ({
|
||||
});
|
||||
|
||||
type Changes = { since?: string; until?: string; truncated?: boolean; changes: ChangeEvent[] } | Response;
|
||||
function hub(changes: () => Changes = () => ({ changes: [] })) {
|
||||
const wc = (over: Partial<WikiChange> = {}): WikiChange => ({
|
||||
at: minutesAgo(20), page_code: "DOC-101252", title: "Методика расчёта", path: "АРМ / Методика", version: "DOCH-2",
|
||||
author: { login: "d.sosnov", name: "Дмитрий Соснов" }, added: 40, removed: 12, ...over,
|
||||
});
|
||||
type WikiBody = Partial<WikiChanges> | Response;
|
||||
function hub(changes: () => Changes = () => ({ changes: [] }), wiki: () => WikiBody = () => ({ changes: [] })) {
|
||||
const calls: URL[] = [];
|
||||
const wikiCalls: URL[] = [];
|
||||
mockFetch((method, url) => {
|
||||
@ -37,7 +43,8 @@ function hub(changes: () => Changes = () => ({ changes: [] })) {
|
||||
}
|
||||
if (k === "GET /wiki/changes") {
|
||||
wikiCalls.push(url);
|
||||
return json(200, { since: "", until: "", synced_at: null, history_at: null, truncated: false, changes: [] });
|
||||
const r = wiki();
|
||||
return r instanceof Response ? r : json(200, { since: "", until: "", synced_at: null, history_at: "2026-10-28T09:00:00+00:00", truncated: false, changes: [], ...r });
|
||||
}
|
||||
if (k === "GET /projects") return json(200, { projects: [A, B, NOWIKI] });
|
||||
if (k === "GET /settings") return json(200, settings);
|
||||
@ -69,7 +76,14 @@ describe("лента изменений", () => {
|
||||
expect(document.querySelector(".ui-btn--primary")).toBeNull();
|
||||
});
|
||||
|
||||
it.todo("шапка у проекта с вики: и «Обновить вики» (П-10) — источник вики появляется в задаче 6");
|
||||
it("шапка у проекта с вики: и «Обновить из Jira», и «Обновить вики» (П-10), обе вторичные", async () => {
|
||||
hub();
|
||||
open("/app/feed?project=AWPCA");
|
||||
await page();
|
||||
expect(screen.getByRole("button", { name: "Обновить из Jira" })).toHaveClass("ui-btn--secondary");
|
||||
expect(screen.getByRole("button", { name: "Обновить вики" })).toHaveClass("ui-btn--secondary");
|
||||
expect(document.querySelector(".ui-btn--primary")).toBeNull();
|
||||
});
|
||||
|
||||
it("«Проект без вики»: флажков источников нет, GET /wiki/changes нет", async () => {
|
||||
const calls = hub(() => ({ changes: [ev({})] }));
|
||||
@ -80,9 +94,101 @@ describe("лента изменений", () => {
|
||||
expect(calls.wiki).toHaveLength(0);
|
||||
});
|
||||
|
||||
it.todo("сняты все источники — «выберите хотя бы один источник», запросов нет, адрес sources=none — флажки источников появляются в задаче 6");
|
||||
it.todo("«Что закрылось за два часа» с источником «Вики»: sources=jira в адресе, GET /wiki/changes после снятия не было — задача 6");
|
||||
it.todo("«Ошибка одного источника»: /wiki/changes 500 — события Jira показаны, на месте вики — ошибка с «Повторить» — задача 6");
|
||||
it("сняты все источники — «выберите хотя бы один источник», запросов нет, адрес sources=none", async () => {
|
||||
const calls = hub();
|
||||
const router = open("/app/feed?project=AWPCA");
|
||||
await page();
|
||||
await waitFor(() => { expect(calls.length).toBe(1); expect(calls.wiki.length).toBe(1); });
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: "Jira" }));
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: "Вики" }));
|
||||
expect(await screen.findByText("выберите хотя бы один источник")).toBeInTheDocument();
|
||||
expect(router.state.location.search).toBe("?project=AWPCA&sources=none");
|
||||
expect(calls.length).toBe(1);
|
||||
expect(calls.wiki.length).toBe(1);
|
||||
});
|
||||
|
||||
it("«Что закрылось за два часа»: период 2h, только Статусы и только Jira — sources=jira, к вики запросов после снятия нет", async () => {
|
||||
const calls = hub();
|
||||
const router = open("/app/feed?project=AWPCA");
|
||||
await page();
|
||||
await waitFor(() => expect(calls.wiki.length).toBe(1));
|
||||
await userEvent.click(screen.getByRole("radio", { name: "2 часа" }));
|
||||
await waitFor(() => expect(calls.wiki.length).toBe(2));
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: "Вики" }));
|
||||
for (const name of ["Описание", "Сроки", "Комментарии"]) await userEvent.click(screen.getByRole("checkbox", { name }));
|
||||
await waitFor(() => expect(router.state.location.search).toBe("?project=AWPCA&period=2h&sources=jira&kinds=status"));
|
||||
expect(router.state.location.pathname).toBe("/app/feed");
|
||||
await waitFor(() => expect(calls.at(-1)!.searchParams.get("kinds")).toBe("status"));
|
||||
const since = new Date(calls.at(-1)!.searchParams.get("since")!).getTime();
|
||||
expect(Math.abs(Date.now() - 2 * 3600_000 - since)).toBeLessThan(60_000);
|
||||
expect(calls.wiki.length).toBe(2); // после снятия «Вики» новых запросов нет
|
||||
});
|
||||
|
||||
it("«Правка из ленты»: группа — название и путь, правка — время, автор, «+40 / −12»; запрос с project, since, limit", async () => {
|
||||
const calls = hub(undefined, () => ({ changes: [wc({ at: new Date(new Date().setHours(12, 40, 0, 0)).toISOString() })] }));
|
||||
open("/app/feed?project=AWPCA");
|
||||
const group = await screen.findByTestId("feed-group");
|
||||
const head = within(group).getByRole("link", { name: "Методика расчёта" });
|
||||
expect(head).toHaveAttribute("href", "/app/wiki?project=AWPCA&page=DOC-101252");
|
||||
expect(within(group).getByText("АРМ / Методика")).toBeInTheDocument();
|
||||
const edit = within(group).getByRole("link", { name: /Дмитрий Соснов/ });
|
||||
expect(edit).toHaveAttribute("href", "/app/wiki/pages/DOC-101252/versions/DOCH-2?project=AWPCA");
|
||||
expect(edit.textContent).toContain("+40 / −12");
|
||||
const q = calls.wiki[0].searchParams;
|
||||
expect(q.get("project")).toBe("AWPCA");
|
||||
const midnight = new Date(); midnight.setHours(0, 0, 0, 0);
|
||||
expect(new Date(q.get("since")!).getTime()).toBe(midnight.getTime());
|
||||
expect(q.get("limit")).toBe("2000");
|
||||
expect(screen.getByText(/1 событие/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("«Jira и Вики вперемешку»: группы обоих источников по времени последнего события, новые сверху", async () => {
|
||||
const today = (h: number) => new Date(new Date().setHours(h, 0, 0, 0)).toISOString();
|
||||
hub(() => ({ changes: [ev({ at: today(12), key: "AURORA-2", summary: "Вторая" }), ev({ at: today(10), key: "AURORA-1", summary: "Первая" })] }),
|
||||
() => ({ changes: [wc({ at: today(11), page_code: "DOC-3", title: "Третья" })] }));
|
||||
open("/app/feed?project=AWPCA");
|
||||
await waitFor(() => expect(screen.getAllByTestId("feed-group")).toHaveLength(3));
|
||||
const names = screen.getAllByTestId("feed-group").map((g) => within(g).getAllByRole("link")[0].textContent);
|
||||
expect(names).toEqual(["AURORA-2", "Третья", "AURORA-1"]);
|
||||
});
|
||||
|
||||
it("«История не загружена»: history_at null — подсказка про eva-agent v2, события Jira на месте", async () => {
|
||||
hub(() => ({ changes: [ev({})] }), () => ({ history_at: null }));
|
||||
open("/app/feed?project=AWPCA");
|
||||
expect(await screen.findByText("история вики ещё не загружена — нужен eva-agent v2")).toBeInTheDocument();
|
||||
expect(await screen.findAllByTestId("feed-group")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("«Ошибка одного источника»: /wiki/changes 500 — события Jira показаны, «Вики: …» и «Повторить» перезапрашивает только вики", async () => {
|
||||
let fail = true;
|
||||
const calls = hub(() => ({ changes: [ev({})] }), () => fail ? json(500, { error: "boom", detail: "сбой" }) : { changes: [wc()] });
|
||||
open("/app/feed?project=AWPCA");
|
||||
expect(await screen.findAllByTestId("feed-group")).toHaveLength(1);
|
||||
expect(await screen.findByText("Вики: хаб ответил 500")).toBeInTheDocument();
|
||||
const jiraCalls = calls.length;
|
||||
fail = false;
|
||||
await userEvent.click(screen.getByRole("button", { name: "Повторить" }));
|
||||
expect(await screen.findByText("Методика расчёта")).toBeInTheDocument();
|
||||
expect(calls.length).toBe(jiraCalls);
|
||||
expect(screen.queryByText("Вики: хаб ответил 500")).toBeNull();
|
||||
});
|
||||
|
||||
it("«Пустой период»: обе ленты пусты — «за период изменений нет»", async () => {
|
||||
hub();
|
||||
open("/app/feed?project=AWPCA");
|
||||
expect(await screen.findByText("за период изменений нет")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("смена проекта на sfm_kedr при sources=wiki — источник Jira, флажков нет, к вики запросов нет", async () => {
|
||||
const calls = hub(() => ({ changes: [ev({})] }));
|
||||
open("/app/feed?project=AWPCA&sources=wiki");
|
||||
await waitFor(() => expect(calls.wiki.length).toBe(1));
|
||||
expect(calls.length).toBe(0);
|
||||
await userEvent.selectOptions(screen.getByRole("combobox", { name: "Проект" }), "sfm_kedr");
|
||||
expect(await screen.findAllByTestId("feed-group")).toHaveLength(1);
|
||||
expect(screen.queryByRole("group", { name: "Источники" })).toBeNull();
|
||||
expect(calls.wiki.length).toBe(1);
|
||||
});
|
||||
|
||||
it("прежний /app/jira/feed → /app/feed с тем же search, replace (прежние закладки)", async () => {
|
||||
hub();
|
||||
@ -130,20 +236,6 @@ describe("лента изменений", () => {
|
||||
expect(screen.getByText(/3 события/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("«Что закрылось за два часа»: период и виды в адресе и в запросе", async () => {
|
||||
const calls = hub();
|
||||
const router = open("/app/feed?project=AWPCA");
|
||||
await page();
|
||||
await userEvent.click(screen.getByRole("radio", { name: "2 часа" }));
|
||||
for (const name of ["Описание", "Сроки", "Комментарии"]) await userEvent.click(screen.getByRole("checkbox", { name }));
|
||||
await waitFor(() => expect(router.state.location.search).toBe("?project=AWPCA&period=2h&kinds=status")); // источник один (вики — в задаче 6): sources в адрес не пишется
|
||||
expect(router.state.location.pathname).toBe("/app/feed");
|
||||
expect(calls.wiki).toHaveLength(0);
|
||||
await waitFor(() => expect(calls.at(-1)!.searchParams.get("kinds")).toBe("status"));
|
||||
const since = new Date(calls.at(-1)!.searchParams.get("since")!).getTime();
|
||||
expect(Math.abs(Date.now() - 2 * 3600_000 - since)).toBeLessThan(60_000);
|
||||
});
|
||||
|
||||
it("карточка из ленты — окно поверх, закрытие возвращает ленту с тем же периодом", async () => {
|
||||
hub(() => ({ changes: [ev({})] }));
|
||||
const router = open("/app/feed?project=AWPCA&period=7d");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user