План: пакет веб-багов web-bugfixes (#124), задачи 1–8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4441e77a58
commit
ba1db6568c
505
docs/superpowers/plans/2026-10-25-web-bugfixes.md
Normal file
505
docs/superpowers/plans/2026-10-25-web-bugfixes.md
Normal file
@ -0,0 +1,505 @@
|
||||
# Пакет веб-багов (web-bugfixes) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** убрать видимые пользователю ошибки веба хаба из пакета A сверки (sdlc-hub#124), каждую закрепив тестом, который до правки падает.
|
||||
|
||||
**Architecture:** только клиент (`web/src/`): точечные правки окна карточки, разборщика вики-разметки, трекера
|
||||
состояния модулей, вложений, доски и UI-кита. Одна новая чистая функция `wikiPlain` для текста ленты. API, схема, MCP и
|
||||
агенты не меняются.
|
||||
|
||||
**Tech Stack:** React 19 + TypeScript + Vite, React Router (data router), TanStack Query, Vitest + Testing Library, Playwright.
|
||||
|
||||
**Spec:** `openspec/changes/web-bugfixes/` — `design.md` (решения 1–21, ссылки на строки main 1405ccd),
|
||||
`specs/web-app/spec.md` (дельта требований и сценарии), `proposal.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Меняется только `web/src/` (+ `web/e2e/issues.spec.ts`). Сервер, API, MCP, агенты — без изменений.
|
||||
- Язык комментариев и сообщений коммитов — русский; идентификаторы — английские. Плотность комментариев — как в окружающем коде.
|
||||
- Разметка MUST NOT превращаться в HTML строкой (`innerHTML`, `dangerouslySetInnerHTML`).
|
||||
- Тесты — рядом с кодом (`*.test.ts(x)`), запросы к хабу мокаются `mockFetch`/`json` из `web/src/test/fetchMock.ts`.
|
||||
- Каждая задача начинается с теста, который падает до правки (RED подтверждается запуском и фиксируется в отчёте).
|
||||
- Тексты интерфейса — дословно из спеки: «Вид работы в Jira», «Назад», «Закрыть», «Остаться», «Уйти без сохранения
|
||||
заметки?», «Повторить», «Загрузка…», «хаб не отвечает», «нет в зеркале», «Задачи {key} нет в зеркале хаба.».
|
||||
- Коммит в конце каждой задачи; сообщение заканчивается строкой `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||||
- Зелёным считается только весь набор: финально `make check` из корня репозитория и `cd web && npm run e2e`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Ctrl/Cmd/средний щелчок по ключу в списке «Задачи» по-прежнему открывает карточку в новой вкладке (перехват только
|
||||
обычного щелчка) — тест в Task 4.
|
||||
2. Окно, открытое прямым адресом (`depth = 0`), и двойной ✕: адрес — список, дальше по истории не уходит — тест в Task 4.
|
||||
3. `wikiPlain` на незакрытой разметке, неизвестных макросах и пустом тексте не бросает и отдаёт текст как есть; пустой
|
||||
комментарий в ленте — по-прежнему «комментарий: —» — тест в Task 2.
|
||||
4. Настройки ещё не пришли или без `kind_labels`: все метки — в «Метках», строки «Вид работы в Jira» нет — тест в Task 3.
|
||||
5. Смена проекта A → B → A во время опроса: пакет A снова подхватывается при возврате (сброс не «запирает» модуль) — тест в Task 5.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Вики-разметка — краевые случаи (#56 M-5, M-6)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/modules/jira/issues/wiki/inline.ts` (`ESCAPABLE` :21, `bracket` :148-152)
|
||||
- Modify: `web/src/modules/jira/issues/wiki/parse.ts` (`MACRO` :17, `closeMacro` :103-116, `row` :118-139)
|
||||
- Test: `web/src/modules/jira/issues/wiki/inline.test.ts`, `web/src/modules/jira/issues/wiki/parse.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: —
|
||||
- Produces: прежние `parseWiki(text, opts): Block[]`, `parseInline(line, opts, label?): Inline[]` с исправленным разбором.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
В `inline.test.ts`:
|
||||
```ts
|
||||
it("\\# — символ решётки (M-5)", () => {
|
||||
expect(parseInline("\\# не пункт")).toEqual([{ t: "text", text: "# не пункт" }]);
|
||||
});
|
||||
it("[~логин|текст] — упоминание логина без подписи (M-6)", () => {
|
||||
expect(parseInline("[~ivanov|Иванов]")).toEqual([{ t: "mention", login: "ivanov" }]);
|
||||
});
|
||||
```
|
||||
В `parse.test.ts`:
|
||||
```ts
|
||||
it("\\# в начале строки — абзац, не список", () => {
|
||||
expect(parseWiki("\\# не пункт")).toEqual([{ t: "p", children: [{ t: "text", text: "# не пункт" }] }]);
|
||||
});
|
||||
it("quote и noformat с параметрами (M-5)", () => {
|
||||
expect(parseWiki("{quote:title=Итог}\nсогласовано\n{quote}")).toEqual([{ t: "quote", children: [{ t: "p", children: [{ t: "text", text: "согласовано" }] }] }]);
|
||||
expect(parseWiki("{noformat:borderStyle=solid}\n*x*\n{noformat}")).toEqual([{ t: "pre", text: "*x*" }]);
|
||||
});
|
||||
it("пробелы после открывающего {code} не дают пустой строки (M-6)", () => {
|
||||
expect(parseWiki("{code} \nx = 1\n{code}")).toEqual([{ t: "pre", text: "x = 1" }]);
|
||||
});
|
||||
it("непарная [ в ячейке не съедает разделители (M-6)", () => {
|
||||
const [table] = parseWiki("|[draft|итог|готово|");
|
||||
expect(table).toMatchObject({ t: "table" });
|
||||
expect((table as { rows: { children: { text: string }[] }[][] }).rows[0].map((c) => c.children[0].text)).toEqual(["[draft", "итог", "готово"]);
|
||||
});
|
||||
it("| внутри [текст|адрес] ячейку по-прежнему не делит", () => {
|
||||
const [table] = parseWiki("|[док|https://x.ru]|b|");
|
||||
expect((table as { rows: unknown[][] }).rows[0]).toHaveLength(2);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/wiki/`
|
||||
Expected: FAIL — новые тесты (кроме последнего, он фиксирует текущее поведение и должен быть зелёным).
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
- `ESCAPABLE` — добавить `#`.
|
||||
- `MACRO = /^\s*\{(code|noformat|quote)(?::[^}]*)?\}/`; имя тега в `closeMacro` — первая группа (без параметров), закрывающий тег — `{имя}`.
|
||||
- `closeMacro`: остаток строки открывающего тега, состоящий из одних пробелов, в `body` не попадает (`text.trim()` вместо `text` в условии для `k === start`).
|
||||
- `bracket`: при `body.startsWith("~")` логин — `body.slice(1).split("|")[0].trim()`; пустой или с пробелом — `null`, как сейчас.
|
||||
- `row`: `[` увеличивает `depth`, только если в `s` правее позиции есть `]` (`s.indexOf("]", j + 1) >= 0`).
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/wiki/`
|
||||
Expected: PASS (все тесты каталога, включая прежние).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/src/modules/jira/issues/wiki/
|
||||
git commit -m "Вики-разметка: \\#, макросы с параметрами, [~логин|текст], пробелы после {code}, непарная [ в таблице (#124, #56 M-5, M-6)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Текст комментария в ленте без разметки (#103 С6)
|
||||
|
||||
**Files:**
|
||||
- Create: `web/src/modules/jira/issues/wiki/plain.ts`
|
||||
- Modify: `web/src/modules/jira/feed/events.ts` (`eventText`, ветка `comment` :72-75)
|
||||
- Test: `web/src/modules/jira/issues/wiki/plain.test.ts` (новый), `web/src/modules/jira/feed/events.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `parseWiki`, `Block`, `Inline`, `WikiOptions` из Task 1 (`parse.ts`, `inline.ts`).
|
||||
- Produces: `export function wikiPlain(text: string, opts?: WikiOptions): string` — текст как читается отрендеренным,
|
||||
блоки через пробел (не одна строка — «одна строка» и обрезку делает `eventText`).
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
`plain.test.ts`:
|
||||
```ts
|
||||
import { wikiPlain } from "./plain.ts";
|
||||
it("выделение, ссылки, упоминание, заголовок", () => {
|
||||
expect(wikiPlain("h3. Итог\n*Готово*, см. [протокол|https://x.ru/p] и [~ivanov]").replace(/\s+/g, " ").trim())
|
||||
.toBe("Итог Готово, см. протокол и @ivanov");
|
||||
});
|
||||
it("[адрес] — сам адрес, {{моно}} и {code} — содержимое, ---- — пусто", () => {
|
||||
expect(wikiPlain("[https://x.ru] {{a_b}}\n----\n{code}\nx = 1\n{code}").replace(/\s+/g, " ").trim()).toBe("https://x.ru a_b x = 1");
|
||||
});
|
||||
it("ключ задачи и вложение — текстом", () => {
|
||||
const opts = { projectKey: "AURORA", attachments: [{ id: "1", filename: "скрин.png", created: null }] };
|
||||
expect(wikiPlain("см. AURORA-1 и !скрин.png!", opts).trim()).toBe("см. AURORA-1 и скрин.png");
|
||||
});
|
||||
it("незакрытая и неизвестная разметка — как есть, пустой текст — пусто (Review Focus 3)", () => {
|
||||
expect(wikiPlain("{color:red}важно *без конца").trim()).toBe("{color:red}важно *без конца");
|
||||
expect(wikiPlain("")).toBe("");
|
||||
});
|
||||
```
|
||||
В `events.test.ts`:
|
||||
```ts
|
||||
it("комментарий в ленте — без вики-разметки (С6)", () => {
|
||||
expect(eventText({ ...commentEvent, to: "h3. Итог\n*Готово*, см. [протокол|https://x.ru/p] и [~ivanov]" }))
|
||||
.toBe("комментарий: Итог Готово, см. протокол и @ivanov");
|
||||
});
|
||||
it("пустой комментарий — по-прежнему «—»", () => {
|
||||
expect(eventText({ ...commentEvent, to: " " })).toBe("комментарий: —");
|
||||
});
|
||||
```
|
||||
(`commentEvent` — событие `kind: "comment"` по образцу уже имеющихся в `events.test.ts`.)
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/wiki/plain.test.ts src/modules/jira/feed/events.test.ts`
|
||||
Expected: FAIL — нет модуля `plain.ts`; тест С6 видит сырую разметку.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
`wikiPlain` обходит дерево `parseWiki`: `text` — текст; метки выделения — дети; `code` — текст; `link` — дети
|
||||
(у `[адрес]` дети уже содержат адрес); `issue` — `key`, если детей нет, иначе дети; `attachment` — дети; `mention` —
|
||||
`"@" + login`; `br` — пробел; блоки (`heading`, `p`, `list` с пунктами и вложенными списками, `table` по ячейкам,
|
||||
`pre` — `text`, `quote` — вложенные блоки, `hr` — пусто) соединяются пробелом. В `eventText` ветка `comment`:
|
||||
`oneLine(wikiPlain(e.to ?? ""))`, остальное (обрезка 300 и «…», «—» для пустого) — как сейчас.
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/wiki/ src/modules/jira/feed/`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/src/modules/jira/issues/wiki/plain.ts web/src/modules/jira/issues/wiki/plain.test.ts web/src/modules/jira/feed/
|
||||
git commit -m "Лента: текст комментария без вики-разметки Jira (#124, #103 С6)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Карточка — связи «нет в зеркале», окно 404, метки видов, пустое описание (#42 M-5, #103, #56 M-4)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/modules/jira/issues/card.ts` (новая функция)
|
||||
- Modify: `web/src/modules/jira/issues/IssueCard.tsx` (404/ошибка :34-49, `Links` :110-134, описание :79, `OtherFields` :225-237)
|
||||
- Modify: `web/src/modules/jira/issues/overlay.tsx` (передать в карточку `onBack`, если `card.depth >= 1` … см. Step 3)
|
||||
- Test: `web/src/modules/jira/issues/card.test.ts` (если нет — создать), `web/src/modules/jira/issues/IssueCard.test.tsx`, `web/src/modules/jira/issues/IssueModal.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `JIRA_URL` (`../board/constants.ts`), `settings.data.kind_labels` (вид → метка).
|
||||
- Produces:
|
||||
- `export function splitKindLabels(labels: string[], kindLabels: Record<string, string> | undefined): { kinds: string[]; rest: string[] }` в `card.ts` — `kinds` — названия видов в порядке меток задачи, `rest` — прочие метки;
|
||||
- `IssueCard` получает необязательный проп `onBack?: () => void`; `IssueModal` прокидывает его из `useIssueOverlay`.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
`card.test.ts`:
|
||||
```ts
|
||||
it("метки видов работ отдельно от прочих", () => {
|
||||
expect(splitKindLabels(["AWPCA", "kind-dev", "kind-ui"], { разработка: "kind-dev", UI: "kind-ui" }))
|
||||
.toEqual({ kinds: ["разработка", "UI"], rest: ["AWPCA"] });
|
||||
});
|
||||
it("без kind_labels — все метки прочие (Review Focus 4)", () => {
|
||||
expect(splitKindLabels(["AWPCA", "kind-dev"], undefined)).toEqual({ kinds: [], rest: ["AWPCA", "kind-dev"] });
|
||||
});
|
||||
```
|
||||
`IssueCard.test.tsx`:
|
||||
- «Прочие поля»: задача с метками `["AWPCA", "kind-dev"]`, настройки фикстуры `settings.json` → видны пары «Вид работы в Jira» / «разработка» и «Метки» / «AWPCA», текста `kind-dev` в карточке нет.
|
||||
- описание `" \n\n"` → нет заголовка «Описание».
|
||||
- связь на ключ вне зеркала (`summary: null, status: null`) → в «Связях» `getByRole("link", { name: /AURORA-1999/ })` имеет `href` = `${JIRA_URL}/browse/AURORA-1999` и `target="_blank"`; рядом текст «нет в зеркале».
|
||||
|
||||
`IssueModal.test.tsx` (сценарий спеки «Задача не в зеркале после перехода»): из списка открыта AURORA-9002, в окне
|
||||
переход по ссылке на ключ, для которого хаб отвечает 404 (`json(404, {})` в `hub()` для этого ключа) → в окне «Задачи
|
||||
AURORA-1999 нет в зеркале хаба.» и кнопки «Назад» и «Закрыть»; «Назад» — заголовок окна снова AURORA-9002; при новом
|
||||
переходе и «Закрыть» — окна нет, адрес — список. Прямой адрес `/app/jira/issues/AURORA-1999` (404) → только «Закрыть».
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/card.test.ts src/modules/jira/issues/IssueCard.test.tsx src/modules/jira/issues/IssueModal.test.tsx`
|
||||
Expected: FAIL — нет `splitKindLabels`, метка `kind-dev` видна в «Метках», рамка описания есть, ссылка ведёт на `/jira/issues/…`, кнопки «Назад» нет.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
- `OtherFields`: строки «Вид работы в Jira» (`kinds.join(", ")`) и «Метки» (`rest.join(", ")`) вместо прежней «Метки»;
|
||||
`kind_labels` — из `useSettings()` (передать пропом из `IssueCard`). Положение «Вид работы в Jira» — сразу перед «Метками».
|
||||
- Описание: условие `i.description?.trim()`.
|
||||
- `Links`: при `l.summary === null && l.status === null` ключ — `<a href={`${JIRA_URL}/browse/${l.key}`} target="_blank" rel="noopener">` со значком `IconExternal` (как «Открыть в Jira»), иначе — `IssueLink`, как сейчас.
|
||||
- `useIssueOverlay`: «Назад» — только если окно открыто переходом из другой карточки. По `openCardState`
|
||||
(`modal.ts:25-28`): из списка `depth = 1, direct = false`; прямой адрес — `depth = 0, direct = true`; каждый переход
|
||||
внутри окна — `depth + 1`. Отсюда `onBack = card && (card.depth >= 2 || (card.direct && card.depth >= 1)) ? () => void
|
||||
navigate(-1) : undefined`; прокинуть через `IssueModal` в `IssueCard`. Тесты: «из списка — без Назад», «после перехода —
|
||||
с Назад», «прямой адрес — без Назад», «переход из прямого адреса — с Назад».
|
||||
- 404 и ошибка загрузки: `action` — «Назад» (если `onBack`) и «Закрыть» / «Повторить», «Назад» (если есть), «Закрыть».
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/src/modules/jira/issues/
|
||||
git commit -m "Карточка: связь вне зеркала ведёт в Jira, «Назад» у окна 404, метки видов работ отдельно, пустое описание (#124, #42 M-5, #103, #56 M-4)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Окно карточки — поиск, двойное закрытие, подтверждение ухода (#42 M-1, M-2, M-4)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/modules/jira/issues/IssuesPage.tsx` (:14-38, `open` :50)
|
||||
- Modify: `web/src/modules/jira/issues/IssueTable.tsx` (ключ :38 — обычный щелчок через `onOpen`)
|
||||
- Modify: `web/src/modules/jira/issues/overlay.tsx` (`close` :37-41)
|
||||
- Modify: `web/src/modules/jira/issues/NoteForm.tsx` (подтверждение ухода :82-89, эффект :42)
|
||||
- Test: `web/src/modules/jira/issues/IssueModal.test.tsx`, `web/src/modules/jira/issues/IssuesPage.test.tsx`, `web/e2e/issues.spec.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `openCardState`, `cardPath` (`modal.ts`, `IssueLink.tsx`).
|
||||
- Produces: `IssueTable` сохраняет проп `onOpen(key: string): void` — теперь его зовут и строка, и обычный щелчок по ключу.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
`IssueModal.test.tsx`:
|
||||
- «поиск перед открытием не закрывает окно» (сценарий спеки): список, в поле поиска (`getByRole("searchbox")`)
|
||||
набрано «свод», сразу щёлкнут ключ AURORA-9002 (без ожидания 300 мс) → окно открыто; после `await new Promise(r => setTimeout(r, 400))`
|
||||
окно всё ещё открыто (`screen.getByRole("dialog")`), `router.state.location.pathname` — `/app/jira/issues/AURORA-9002`;
|
||||
после ✕ адрес — `/app/jira/issues` с `q=свод`. То же для щелчка по ячейке темы строки.
|
||||
- «двойной щелчок по ✕ не уводит дальше назад»: история `["/app/jira/links", "/app/jira/issues"]`, открыта AURORA-9002,
|
||||
переход по связи на AURORA-9001, `await userEvent.dblClick(✕)` → адрес — `/app/jira/issues`, не `/app/jira/links`.
|
||||
- то же для прямого адреса (Review Focus 2): история `["/app/jira/issues/AURORA-9002"]`, двойной ✕ → `/app/jira/issues`.
|
||||
- подтверждение ухода: в форме заметки правка, нажата Esc → `getByRole("alertdialog", { name: "Уйти без сохранения заметки?" })`,
|
||||
`document.activeElement` — кнопка «Остаться».
|
||||
|
||||
`IssuesPage.test.tsx` (Review Focus 1): у ключа строки `href` прежний (`/jira/issues/AURORA-9001`), щелчок с `ctrlKey`
|
||||
(`fireEvent.click(link, { ctrlKey: true })`) не вызывает навигацию внутри приложения (адрес страницы не меняется).
|
||||
|
||||
`web/e2e/issues.spec.ts`: сценарий «поиск перед открытием карточки» — ввести текст в поиск и сразу щёлкнуть первую
|
||||
строку; через 600 мс окно открыто; Escape — адрес списка содержит `q=`.
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/IssueModal.test.tsx src/modules/jira/issues/IssuesPage.test.tsx`
|
||||
Expected: FAIL — окно закрывается через 300 мс; двойной ✕ уходит на `/app/jira/links`; у подтверждения нет имени и фокуса.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
- `IssuesPage`: функция `openCard(key)` — снять таймер поиска; если `q` отличается от `filters.q`, записать `q` в адрес
|
||||
(`replace`) и открыть карточку с фоном, у которого `search` уже содержит `q` (`navigate(cardPath(key), { state:
|
||||
openCardState({ ...loc, search: <поиск с q> }) })`, `written.current = q`). Её получает `IssueTable` как `onOpen`.
|
||||
Таймер не перезаводится, пока страница под окном (адрес `filters.q` совпадает с `q` после записи — эффект ничего не делает).
|
||||
- `IssueTable`: ключ — та же ссылка `IssueLink` (адрес и состояние для новой вкладки), но обычный щелчок (левая кнопка
|
||||
без `ctrl`/`meta`/`shift`/`alt`) — `preventDefault()` и `onOpen(key)`.
|
||||
- `overlay.tsx`: `closingKey = useRef<string | null>(null)`; `close` выходит, если `closingKey.current === location.key`,
|
||||
иначе запоминает `location.key` и закрывает как сейчас. Сброс — эффект по смене `location.key`.
|
||||
- `NoteForm`: у блока подтверждения ухода `aria-label="Уйти без сохранения заметки?"`; ref на кнопку «Остаться»; при
|
||||
`blocker.state === "blocked"` — `scrollIntoView` (как сейчас) и `focus()` этой кнопки.
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/ && npm run e2e -- issues.spec.ts`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/src/modules/jira/issues/ web/e2e/issues.spec.ts
|
||||
git commit -m "Окно карточки: поиск не закрывает окно, повторное закрытие игнорируется, фокус подтверждения ухода (#124, #42 M-1, M-2, M-4)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Строка состояния — ответ опроса прежнего проекта (#116)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/shell/activity/ActivityProvider.tsx` (эффект опроса :126-154)
|
||||
- Test: `web/src/shell/activity/activity.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: —
|
||||
- Produces: поведение `ModuleActivity` без изменений интерфейса.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
В `activity.test.tsx` — детерминированный тест «ответ опроса после смены проекта не возвращает событие»: ответ
|
||||
`GET /batches/9` (пакет `wiki_sync` проекта AWPCA) задерживается промисом-заглушкой (`deferred` из `web/src/test/batchHub.ts`);
|
||||
когда первый ответ показан (`event.kind === "batch"`), следующий ответ держится; в одном `act` вызвать
|
||||
`m.setProject("STOCKS_DRRK")` и разрешить задержанный ответ → после `await waitFor(...)` и `sleep(50)` `m.now().wiki.event`
|
||||
равно `null`. Исполнитель подбирает порядок так, чтобы тест падал на текущем коде (RED обязателен, см. Risks в design.md);
|
||||
если воспроизвести в одном такте не удаётся — описать в отчёте, как получен RED (например, `pollMs` 0 и разрешение
|
||||
ответа синхронно перед `setProject`).
|
||||
|
||||
Второй тест (Review Focus 5): A → B → A — после возврата на AWPCA незавершённый пакет 9 снова подхватывается
|
||||
(`event` `{ kind: "batch", id: 9 }`).
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd web && npx vitest run src/shell/activity/activity.test.tsx`
|
||||
Expected: FAIL — первый тест: событие `batch` id 9 после смены проекта.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
В начале эффекта опроса: `if (!active || activeRef.current !== active) return;` — ответ обрабатывается, только если
|
||||
отслеживаемый пакет тот же объект, что в `activeRef` (смена проекта синхронно обнуляет `activeRef` в `setActiveNow`).
|
||||
Проверить, что `setActiveNow({ ...active, own: true })` (:138) и `track`/`start` по-прежнему проходят: они сами
|
||||
обновляют `activeRef`.
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd web && npx vitest run src/shell/activity/activity.test.tsx` и прогон в цикле:
|
||||
`for i in $(seq 20); do npx vitest run src/shell/activity/activity.test.tsx --silent || break; done`
|
||||
Expected: PASS все 20 прогонов.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/src/shell/activity/
|
||||
git commit -m "Состояние модулей: ответ опроса прежнего проекта не возвращает событие (#124, #116)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Список задач ждёт настройки (#60 M-3)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/modules/jira/issues/IssuesPage.tsx` (запрос :45-48, «Загрузка…» :60-61)
|
||||
- Modify: `web/src/modules/jira/api/queries.ts` (`useIssues` — необязательный параметр `enabled`)
|
||||
- Test: `web/src/modules/jira/issues/IssuesPage.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useSettings()` (`isPending`).
|
||||
- Produces: `useIssues(filters: IssueFilters, opts?: { enabled?: boolean })`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
«закрытый статус в адресе до настроек» (сценарий спеки): ответ `/settings` задержан; маршрут `/issues?status=Готово`;
|
||||
пока настройки не пришли — `issuesCalls(fetch)` пуст, видно «Загрузка…», нет «По этим фильтрам задач нет»; после
|
||||
ответа `/settings` (с «Готово» в `closed_statuses`) — ровно один вызов `/issues` с `status=Готово` и без `hide_closed`.
|
||||
Второй тест (R-11): `/settings` отвечает 500 → один вызов `/issues` с `hide_closed=1`.
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/IssuesPage.test.tsx`
|
||||
Expected: FAIL — первый вызов `/issues` уходит до настроек с `hide_closed=1`.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
`useIssues(query, { enabled: !settings.isPending })`; пока настройки грузятся, «Загрузка…» (условие подсказки — по
|
||||
`list.isPending || settings.isPending`), сообщение «По этим фильтрам задач нет» — только после ответа списка.
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/src/modules/jira/
|
||||
git commit -m "Список задач: запрос после настроек, без мелькания «задач нет» (#124, #60 M-3)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Вложения — прежняя ошибка, 401 при опросе, порядок хаба (#86 T6.2, T6.3, M-4)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/modules/jira/issues/attachments/useAttachmentDownload.ts`
|
||||
- Modify: `web/src/modules/jira/issues/attachments/AttachmentList.tsx` (`newestFirst` :14-17, ошибка :36)
|
||||
- Modify: `web/src/modules/jira/issues/wiki/inline.ts` (`findAttachment` :36-45)
|
||||
- Test: `web/src/modules/jira/issues/attachments/useAttachmentDownload.test.tsx`, `web/src/modules/jira/issues/IssueCard.test.tsx`, `web/src/modules/jira/issues/wiki/inline.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `dropTokenOn`, `AuthError`, `HubDownError` (`web/src/api/client.ts`).
|
||||
- Produces: `DownloadStatus` дополняется полем `ok: boolean` (успех в этой карточке); `IDLE.ok = false`.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
- `useAttachmentDownload.test.tsx`: опрос `GET /attachments/{id}` отвечает 401 → вызван сброс токена (токен в хранилище
|
||||
пуст / экран входа по образцу других тестов `AuthError`), текста ошибки нет; ответ — сеть недоступна (`HubDownError`)
|
||||
→ `status(id).error === "хаб не отвечает"`.
|
||||
- `IssueCard.test.tsx` (сценарий «Повтор после ошибки»): вложение с `state: "failed", error: "сбой"`, «Повторить», хаб
|
||||
отдаёт `ready` и файл, перечитанная карточка всё ещё отдаёт `failed` (задержать) → текста «сбой» нет.
|
||||
- `IssueCard.test.tsx`: вложения в ответе хаба в порядке `[b (created null), a (created 2026-10-20)]` → в разделе в том же порядке.
|
||||
- `inline.test.ts`: два вложения «скрин.png» — `[{id: "5", created: null}, {id: "9", created: "2026-10-20T10:00:00.000+0300"}]` →
|
||||
`!скрин.png!` даёт `attachment` с `id: "5"` (первое в порядке хаба).
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/attachments/ src/modules/jira/issues/IssueCard.test.tsx src/modules/jira/issues/wiki/inline.test.ts`
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
- В `useAttachmentDownload`: общая функция разбора ошибки — `AuthError` → `dropTokenOn(e)` без текста; `HubDownError` →
|
||||
«хаб не отвечает»; прочие — `errorText`. Успех — `put(id, { busy: false, error: null, ok: true })`; `start` сбрасывает `ok`.
|
||||
- `AttachmentList`: порядок — как в `items`; серверный `failed` показывается, только если `!ok`. Убрать `newestFirst`
|
||||
(и его экспорт, если других потребителей нет).
|
||||
- `findAttachment`: первое вложение с этим именем в порядке `opts.attachments`.
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/issues/`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/src/modules/jira/issues/
|
||||
git commit -m "Вложения: без прежней ошибки после повтора, 401 — экран входа, порядок хаба (#124, #86 T6.2, T6.3, M-4)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Доска, проекты, поиск UI-кита (#35 T4.1/M-15, #64 T9.1, M-6, #68 T3.3) и итоговая проверка
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/modules/jira/board/useBoardPrefs.ts` (`toggle`, `toggleHidden` :39-54)
|
||||
- Modify: `web/src/modules/jira/board/model/fromHub.ts` (`hasSprint` :30)
|
||||
- Modify: `web/src/projects/ProjectGate.tsx` (:11)
|
||||
- Modify: `web/src/ui/inputs.tsx` (`SearchInput` :15-24)
|
||||
- Test: `web/src/modules/jira/board/BoardPage.test.tsx` или новый `useBoardPrefs.test.tsx`, тест модели доски рядом с `fromHub.ts`, `web/src/projects/projects.test.tsx`, `web/src/ui/forms.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useProjects()` (`refetch`) из `web/src/api/queries.ts`.
|
||||
- Produces: —
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
- `useBoardPrefs`: `renderHook`; в одном `act` два вызова `toggle(<флаг A>)` и `toggle(<флаг B>)` → оба флага включены
|
||||
и в состоянии, и в `localStorage`; в одном `act` `toggleHidden("kind", "x")` и `toggleHidden("kind", "y")` → скрыты оба.
|
||||
- `fromHub`: снимок, где спринт только у эпика (тип «Эпик», `neighbor: false`), у остальных своих задач спринта нет →
|
||||
`hasSprint === false`.
|
||||
- `projects.test.tsx`: `GET /projects` → 500, затем 200 → видно «Проекты не загрузились: …» и кнопка «Повторить»;
|
||||
после нажатия — содержимое страницы проекта.
|
||||
- `forms.test.tsx`: `SearchInput` со значением, щелчок «Очистить» → `onChange("")` и фокус в поле поиска
|
||||
(`document.activeElement === getByRole("searchbox")`) — с обёрткой-состоянием, чтобы кнопка исчезла.
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cd web && npx vitest run src/modules/jira/board/ src/projects/ src/ui/`
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
- `useBoardPrefs`: `prefsRef` с последним значением; `toggle`/`toggleHidden` считают новое значение из `prefsRef.current`,
|
||||
синхронно присваивают `prefsRef.current = next`, пишут `st.writeFlag`/`st.writeHidden` и `setPrefs(next)`.
|
||||
- `fromHub`: `hasSprint = snap.issues.some((it) => !isEpic(it) && !it.neighbor && !!it.sprint)`.
|
||||
- `ProjectGate`: `action={<Button onClick={retry}>Повторить</Button>}`, где `retry` перезапрашивает `GET /projects`.
|
||||
- `SearchInput`: `ref` на `input`; «Очистить» — `onChange("")`, затем `ref.current?.focus()`.
|
||||
|
||||
- [ ] **Step 4: Run the whole suite**
|
||||
|
||||
Run (из корня репозитория): `make check`, затем `cd web && npm run e2e`
|
||||
Expected: всё зелёное (pytest, lint, vitest, e2e).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web/src/
|
||||
git commit -m "Доска, проекты, поиск: два переключения не теряются, спринт эпика, «Повторить», фокус после «Очистить» (#124, #35 T4.1, #64 T9.1, M-6, #68 T3.3)"
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user