fix(comments): review fixes for the edit-card redesign (#561)
- F1: restore the "Applied" badge on an applied suggested-edit card (green light
Badge + aria-label, reusing the pre-redesign t("Applied") key). Renders only
when suggestionAppliedAt is set (a reply-kept, resolved suggestion — a
childless applied one is hard-deleted per #329), never for a pending edit.
- F2: add the four new t() keys introduced by the redesign to both locales —
"Critical", "Major", "{{count}} edits", "{{count}} major" (en value==key; ru
translated, ICU plural for the count keys, placeholders intact).
- F3: test RunHeader — edit count, major = major+critical only (not
minor/unknown, non-vacuous), provenance line, no "Accept all" control. Uses an
isolated initialised i18n instance so the interpolated count keys render.
- F4: restore the card aria-label (t("Jump to comment selection")) on the
clickable edit card for screen readers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@
|
||||
"Create page": "Create page",
|
||||
"Create space": "Create space",
|
||||
"Create workspace": "Create workspace",
|
||||
"Critical": "Critical",
|
||||
"Current password": "Current password",
|
||||
"Dark": "Dark",
|
||||
"Date": "Date",
|
||||
@@ -125,6 +126,7 @@
|
||||
"Link copied": "Link copied",
|
||||
"Login": "Login",
|
||||
"Logout": "Logout",
|
||||
"Major": "Major",
|
||||
"Manage Group": "Manage Group",
|
||||
"Manage members": "Manage members",
|
||||
"member": "member",
|
||||
@@ -453,6 +455,8 @@
|
||||
"{{count}} Columns": "{{count}} Columns",
|
||||
"{{count}} command available_one": "1 command available",
|
||||
"{{count}} command available_other": "{{count}} commands available",
|
||||
"{{count}} edits": "{{count}} edits",
|
||||
"{{count}} major": "{{count}} major",
|
||||
"{{count}} result available_one": "1 result available",
|
||||
"{{count}} result available_other": "{{count}} results available",
|
||||
"{{count}} result found_one": "{{count}} result found",
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"Create page": "Создать страницу",
|
||||
"Create space": "Создать пространство",
|
||||
"Create workspace": "Создать рабочую область",
|
||||
"Critical": "Критично",
|
||||
"Current password": "Текущий пароль",
|
||||
"Dark": "Темная",
|
||||
"Date": "Дата",
|
||||
@@ -127,6 +128,7 @@
|
||||
"Link copied": "Ссылка скопирована",
|
||||
"Login": "Войти",
|
||||
"Logout": "Выйти",
|
||||
"Major": "Существенно",
|
||||
"Manage Group": "Управление группой",
|
||||
"Manage members": "Управление участниками",
|
||||
"member": "участник",
|
||||
@@ -461,6 +463,8 @@
|
||||
"{{count}} Columns": "{count, plural, one{# столбец} few{# столбца} many{# столбцов} other{# столбца}}",
|
||||
"{{count}} command available_one": "Доступна 1 команда",
|
||||
"{{count}} command available_other": "Доступно {{count}} команд",
|
||||
"{{count}} edits": "{count, plural, one{# правка} few{# правки} many{# правок} other{# правки}}",
|
||||
"{{count}} major": "{count, plural, one{# существенная} few{# существенных} many{# существенных} other{# существенных}}",
|
||||
"{{count}} result available_one": "Доступен 1 результат",
|
||||
"{{count}} result available_other": "Доступно {{count}} результатов",
|
||||
"{{count}} result found_one": "Найден {{count}} результат",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { createInstance } from "i18next";
|
||||
import { initReactI18next, I18nextProvider } from "react-i18next";
|
||||
import { IComment } from "@/features/comment/types/comment.types";
|
||||
|
||||
// matchMedia (read by MantineProvider) is stubbed globally in vitest.setup.ts.
|
||||
@@ -37,7 +39,7 @@ vi.mock("@/features/space/queries/space-query.ts", () => ({
|
||||
useGetSpaceBySlugQuery: () => ({ data: undefined }),
|
||||
}));
|
||||
|
||||
import AgentEditCard from "./agent-edit-card";
|
||||
import AgentEditCard, { RunHeader } from "./agent-edit-card";
|
||||
|
||||
const body = (text: string) =>
|
||||
JSON.stringify({
|
||||
@@ -109,6 +111,18 @@ describe("AgentEditCard — suggested edit diff + Apply (#315)", () => {
|
||||
expect(screen.queryByRole("button", { name: "Apply" })).toBeNull();
|
||||
});
|
||||
|
||||
it("shows the Applied badge for an applied suggestion, not a pending one (F1)", () => {
|
||||
// Pending: no Applied badge.
|
||||
const { unmount } = renderCard(edit(), true);
|
||||
expect(screen.queryByText("Applied")).toBeNull();
|
||||
unmount();
|
||||
// Applied (kept alive by replies -> resolved, #329): the badge is restored.
|
||||
renderCard(edit({ suggestionAppliedAt: new Date() }), true);
|
||||
expect(screen.getByText("Applied")).toBeDefined();
|
||||
expect(screen.queryByRole("button", { name: "Apply" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Dismiss" })).toBeNull();
|
||||
});
|
||||
|
||||
it("calls the apply mutation with {commentId, pageId} on click", () => {
|
||||
applyMutateAsync.mockClear();
|
||||
renderCard(edit(), true);
|
||||
@@ -185,3 +199,86 @@ describe("AgentEditCard — provenance", () => {
|
||||
beforeEach(() => localStorage.clear());
|
||||
afterEach(() => localStorage.clear());
|
||||
});
|
||||
|
||||
// The RunHeader's "N edits · M major" is built with interpolated t() keys; the
|
||||
// default (uninitialized) react-i18next t returns the key verbatim WITHOUT
|
||||
// interpolating, so a numeric assertion needs a real, initialised i18n instance.
|
||||
// This isolated instance carries just the two count keys with escapeValue off so
|
||||
// "{{count}}" is substituted and the rendered numbers are assertable.
|
||||
const headerI18n = createInstance();
|
||||
headerI18n.use(initReactI18next).init({
|
||||
lng: "en",
|
||||
fallbackLng: "en",
|
||||
resources: {
|
||||
en: {
|
||||
translation: {
|
||||
"{{count}} edits": "{{count}} edits",
|
||||
"{{count}} major": "{{count}} major",
|
||||
},
|
||||
},
|
||||
},
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
|
||||
const runComment = (id: string, tag: string): IComment =>
|
||||
edit({
|
||||
id,
|
||||
content: body(`${tag} some rationale`),
|
||||
});
|
||||
|
||||
function renderRunHeader(comments: IComment[]) {
|
||||
return render(
|
||||
<I18nextProvider i18n={headerI18n}>
|
||||
<MantineProvider>
|
||||
<RunHeader comments={comments} />
|
||||
</MantineProvider>
|
||||
</I18nextProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("RunHeader — agent-run series header (F3)", () => {
|
||||
// 5 edits: 2 critical + 1 major + 1 minor + 1 unknown(verdict) => 3 "major".
|
||||
const series = () => [
|
||||
runComment("e1", "[Критично]"),
|
||||
runComment("e2", "[Критично]"),
|
||||
runComment("e3", "[Существенно]"),
|
||||
runComment("e4", "[Незначительно]"),
|
||||
runComment("e5", "[Неверно]"),
|
||||
];
|
||||
|
||||
it("shows the total edit count", () => {
|
||||
renderRunHeader(series());
|
||||
expect(screen.getByText(/5 edits/)).toBeDefined();
|
||||
});
|
||||
|
||||
it("counts ONLY major+critical as major (not minor/unknown)", () => {
|
||||
renderRunHeader(series());
|
||||
// 2 critical + 1 major = 3; minor and the [Неверно] verdict are excluded.
|
||||
expect(screen.getByText(/3 major/)).toBeDefined();
|
||||
// Non-vacuous: the wrong tally (counting all 5, or 4) must NOT appear.
|
||||
expect(screen.queryByText(/5 major/)).toBeNull();
|
||||
expect(screen.queryByText(/4 major/)).toBeNull();
|
||||
});
|
||||
|
||||
it("omits the major segment entirely when there are no significant edits", () => {
|
||||
renderRunHeader([
|
||||
runComment("e1", "[Незначительно]"),
|
||||
runComment("e2", "[Неверно]"),
|
||||
]);
|
||||
expect(screen.getByText(/2 edits/)).toBeDefined();
|
||||
expect(screen.queryByText(/major/)).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the provenance line (agent role name)", () => {
|
||||
renderRunHeader(series());
|
||||
expect(screen.getAllByText("Corrector").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders NO 'Accept all' control (rejected by product)", () => {
|
||||
renderRunHeader(series());
|
||||
expect(screen.queryByText(/accept all/i)).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /accept all/i }),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useMemo } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
@@ -222,6 +223,7 @@ function AgentEditCard({
|
||||
const isOwnerOrAdmin =
|
||||
currentUser?.user?.id === comment.creatorId || userSpaceRole === "admin";
|
||||
|
||||
const isApplied = comment.suggestionAppliedAt != null;
|
||||
const showApply = canShowApply(comment, canEdit);
|
||||
const showDismiss = canShowDismiss(comment, canComment, isOwnerOrAdmin);
|
||||
const pending =
|
||||
@@ -263,6 +265,7 @@ function AgentEditCard({
|
||||
p="10px 12px"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t("Jump to comment selection")}
|
||||
onClick={() => scrollToCommentMark(comment.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
@@ -313,6 +316,21 @@ function AgentEditCard({
|
||||
</Text>
|
||||
)}
|
||||
<Box style={{ flex: 1 }} />
|
||||
{/* Applied state (#315): a suggestion that was applied but kept alive by
|
||||
its replies (so #329 resolved instead of hard-deleting it) still
|
||||
shows it was applied — the badge the pre-redesign card carried. A
|
||||
childless applied suggestion is gone from the list entirely, so this
|
||||
only renders in the Resolved tab. */}
|
||||
{isApplied && (
|
||||
<Badge
|
||||
size="sm"
|
||||
color="green"
|
||||
variant="light"
|
||||
aria-label={t("Applied")}
|
||||
>
|
||||
{t("Applied")}
|
||||
</Badge>
|
||||
)}
|
||||
{showDismiss && (
|
||||
<Button
|
||||
size="compact-sm"
|
||||
|
||||
Reference in New Issue
Block a user