Веб: токен эпика, проверка определённости var(--…), выделение в строке (#108)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-24 20:02:12 +04:00
parent a6de46b3f8
commit 9212c19dcf
5 changed files with 44 additions and 5 deletions

View File

@ -144,6 +144,27 @@ describe("«Бэклог»", () => {
await waitFor(() => expect(router.state.location.pathname).toBe("/app/jira/backlog"));
});
it("выделение вне строки не мешает открыть карточку", async () => {
hub();
const router = open("/app/jira/backlog?project=AWPCA");
const p = await loaded();
window.getSelection()!.selectAllChildren(within(p).getByRole("heading", { name: "AWPCA_Sprint_4" }));
await userEvent.click(within(p).getByText("Тема AURORA-1"));
await screen.findByRole("dialog");
expect(router.state.location.pathname).toBe("/app/jira/issues/AURORA-1");
window.getSelection()!.removeAllRanges();
});
it("щелчок по ссылке ключа открывает карточку один раз", async () => {
hub();
const router = open("/app/jira/backlog?project=AWPCA");
const p = await loaded();
await userEvent.click(within(p).getByRole("link", { name: "AURORA-1" }));
await screen.findByRole("dialog");
await userEvent.click(within(screen.getByRole("dialog")).getByRole("button", { name: "Закрыть" }));
await waitFor(() => expect(router.state.location.pathname).toBe("/app/jira/backlog")); // одна запись истории, а не две
});
it("нет данных доски — «Нужен агент v15 и обновление из Jira» и «Обновить из Jira»", async () => {
hub({ data: { board: null, sprints: [], backlog: null, board_issues: null } });
open("/app/jira/backlog?project=AWPCA");

View File

@ -9,7 +9,8 @@ export function AgileIssueRow({ issue, mark }: { issue: AgileIssue; mark?: strin
const open = useOpenIssue();
const click = (e: MouseEvent) => {
if ((e.target as HTMLElement).closest("a")) return;
if (window.getSelection()?.toString()) return;
const sel = window.getSelection();
if (sel && !sel.isCollapsed && sel.containsNode(e.currentTarget, true)) return; // выделен текст этой строки
open(issue.key);
};
return (

View File

@ -1,8 +1,7 @@
/* экраны доски (бэклог, спринт, Kanban): только токены дизайн-системы */
.agile{display:flex;flex-direction:column;gap:var(--space-4);padding-top:var(--space-6)}
.agile .ui-page-header{margin-bottom:0}
.agile__gate{padding-top:var(--space-6)}
.agile .agile__gate{padding-top:0}
.agile__gate{min-width:0}
.agile__link{color:var(--color-action-primary);font-weight:600;text-decoration:none}
.agile__link:hover{text-decoration:underline}
.agile__empty{margin:0;color:var(--color-text-secondary)}
@ -25,7 +24,7 @@
.agile-row__sp{flex:0 0 32px;text-align:right;color:var(--color-text-secondary)}
.agile-row__mark{flex:0 0 auto;font-size:var(--fs-caption);color:var(--color-status-warning)}
.agile-type{display:inline-flex;flex:0 0 auto;color:var(--color-text-secondary)}
.agile-type--epic{color:var(--color-cat-1)}
.agile-type--epic{color:var(--k-ui)}
.agile-type--bug{color:var(--color-status-error)}
.agile-type--task{color:var(--color-status-success)}
.agile-past{display:flex;flex-direction:column;gap:var(--space-2)}

View File

@ -71,7 +71,6 @@ export interface IssueDetail extends IssueRow {
changes?: ChangeEvent[]; // хаб до журнала изменений (#70) поля не шлёт
}
// ответ GET /agile (доска проекта, спринты, бэклог); поля — как отдаёт хаб
export interface AgileIssue {
key: string; summary: string | null; issue_type: string | null; status: string | null; priority: string | null;

View File

@ -64,6 +64,25 @@ describe("токены AURA", () => {
});
});
describe("использованные токены определены", () => {
const defs = (css: string) => new Set([...stripComments(css).matchAll(/(--[\w-]+)\s*:/g)].map((m) => m[1]));
// переменные, которые выставляет код страницы (setProperty), а не css
const SET_BY_JS = new Set(["--head-h"]);
const tokenDefs = defs(readFileSync(TOKENS, "utf8"));
it("каждый var(--…) в css определён в tokens.css или в том же файле", () => {
const missing: string[] = [];
for (const f of cssFiles(SRC)) {
if (f === TOKENS) continue;
const css = readFileSync(f, "utf8");
const local = defs(css);
for (const m of stripComments(css).matchAll(/var\(\s*(--[\w-]+)/g)) {
if (!tokenDefs.has(m[1]) && !local.has(m[1]) && !SET_BY_JS.has(m[1])) missing.push(`${relative(SRC, f)}: ${m[1]}`);
}
}
expect([...new Set(missing)]).toEqual([]);
});
});
describe("доска без чередования дорожек (#69, design 9)", () => {
it("токена --lane-alt нет, правила чётной дорожки нет", () => {
expect(readFileSync(TOKENS, "utf8")).not.toContain("--lane-alt");