FN-5947: fix mobile task review header spacing

Tighten the Task Review tab layout on mobile and lock in the responsive header contract.

- override the mobile .task-review-tab__summary-wrap flex rule so the summary block stops reserving excessive blank space
- extend TaskReviewTab coverage to assert the mobile summary-wrap override and prevent regressions between empty and populated review states across reviewer-agent and pull-request sources
- keep the review header structure stable across both review sources and empty/populated states

Files changed:
 .../dashboard/app/components/TaskReviewTab.css     |  4 +
 .../components/__tests__/TaskReviewTab.test.tsx    | 98 ++++++++++++++++++++++
 2 files changed, 102 insertions(+)

Fusion-Task-Id: FN-5947
Fusion-Task-Lineage: 813e1142-bf28-4687-8510-2f93ec40e582
This commit is contained in:
gsxdsm
2026-06-03 13:24:21 -07:00
parent f1572a3b62
commit da7c82ebea
2 changed files with 102 additions and 0 deletions

View File

@@ -274,6 +274,10 @@
width: 100%;
}
.task-review-tab__summary-wrap {
flex: 0 0 auto;
}
.task-review-tab__actions {
justify-content: flex-start;
gap: var(--space-sm);

View File

@@ -504,8 +504,13 @@ describe("TaskReviewTab", () => {
const mobileMediaStart = css.indexOf("@media (max-width: 768px)");
expect(mobileMediaStart).toBeGreaterThanOrEqual(0);
const mobileCss = css.slice(mobileMediaStart);
const baseSummaryWrapRule = css.match(/\.task-review-tab__summary-wrap\s*\{[^}]*\}/)?.[0] ?? "";
expect(baseSummaryWrapRule).toMatch(/flex\s*:\s*1\s+1\s+20rem\s*;/);
expect(baseSummaryWrapRule).not.toMatch(/flex\s*:\s*0\s+0\s+auto\s*;/);
expect(mobileCss).toMatch(/\.task-review-tab__header\s*\{[^}]*flex-direction\s*:\s*column\s*;[^}]*\}/);
expect(mobileCss).toMatch(/\.task-review-tab__summary-wrap\s*\{[^}]*flex\s*:\s*0\s+0\s+auto\s*;[^}]*\}/);
expect(mobileCss).not.toMatch(/\.task-review-tab__summary-wrap\s*\{[^}]*flex\s*:\s*1\s+1\s+20rem\s*;[^}]*\}/);
expect(mobileCss).toMatch(/\.task-review-tab__actions\s*\{[^}]*justify-content\s*:\s*flex-start\s*;[^}]*\}/);
expect(mobileCss).toMatch(/\.task-review-tab__actions\s+\.btn\s*\{[^}]*width\s*:\s*100%\s*;[^}]*\}/);
expect(mobileCss).toMatch(/\.task-review-tab__body\s*\{[^}]*padding\s*:\s*var\(--space-sm\)\s*;[^}]*\}/);
@@ -515,6 +520,99 @@ describe("TaskReviewTab", () => {
expect(css).toMatch(/\.task-review-tab__item\s*\{[^}]*padding\s*:\s*var\(--card-padding\)\s*;[^}]*\}/);
});
it("preserves review header structure across sources and empty or populated states", async () => {
const cases = [
{
task: makeTask({ id: "FN-100" }),
response: {
reviewState: {
source: "reviewer-agent" as const,
summary: { summary: "reviewer-agent", verdict: "REVISE", reviewType: "code" },
items: [],
addressing: [],
},
automationStatus: null,
emptyMessage: "No reviewer feedback yet — this task has not produced reviewer-agent feedback in direct mode.",
},
summaryText: "reviewer-agent · 0 review item(s)",
emptyText: "No reviewer feedback yet — this task has not produced reviewer-agent feedback in direct mode.",
},
{
task: makeTask({ id: "FN-101" }),
response: {
reviewState: {
source: "reviewer-agent" as const,
summary: { summary: "Needs fixes", verdict: "REVISE", reviewType: "code" },
items: [{ id: "reviewer-item-1", body: "Fix failing test", author: { login: "reviewer-agent" }, createdAt: new Date().toISOString(), summary: "Fix failing test" }],
addressing: [],
},
automationStatus: null,
emptyMessage: null,
},
summaryText: "Needs fixes · 1 review item(s)",
itemText: "Fix failing test",
},
{
task: makeTask({ id: "FN-102" }),
response: {
reviewState: {
source: "pull-request" as const,
summary: { reviewDecision: "REVIEW_REQUIRED", reviewers: [], blockingReasons: [], checks: [] },
items: [],
addressing: [],
},
automationStatus: null,
emptyMessage: null,
},
summaryText: "REVIEW_REQUIRED · 0 review item(s)",
emptyText: "No review items yet.",
},
{
task: makeTask({ id: "FN-103" }),
response: {
reviewState: {
source: "pull-request" as const,
summary: { reviewDecision: "APPROVED", reviewers: [], blockingReasons: [], checks: [] },
items: [{ id: "pr-item-1", body: "Looks good", author: { login: "octocat" }, createdAt: new Date().toISOString(), summary: "Looks good" }],
addressing: [],
},
automationStatus: null,
emptyMessage: null,
},
summaryText: "APPROVED · 1 review item(s)",
itemText: "Looks good",
},
];
apiMocks.fetchTaskReview
.mockResolvedValueOnce(cases[0].response)
.mockResolvedValueOnce(cases[1].response)
.mockResolvedValueOnce(cases[2].response)
.mockResolvedValueOnce(cases[3].response);
const { container, rerender } = render(<TaskReviewTab task={cases[0].task} addToast={vi.fn()} />);
for (const [index, testCase] of cases.entries()) {
if (index > 0) {
rerender(<TaskReviewTab task={testCase.task} addToast={vi.fn()} />);
}
expect(await screen.findByText(testCase.summaryText)).toBeInTheDocument();
expect(container.querySelector(".task-review-tab__header")).not.toBeNull();
expect(container.querySelector(".task-review-tab__summary-wrap")).not.toBeNull();
expect(container.querySelector(".task-review-tab__summary-group")).not.toBeNull();
expect(container.querySelector(".task-review-tab__actions")).not.toBeNull();
if (testCase.emptyText) {
expect(screen.getByText(testCase.emptyText)).toBeInTheDocument();
}
if (testCase.itemText) {
expect(screen.getByText(testCase.itemText)).toBeInTheDocument();
}
}
});
it("shows create PR action when in-review without prInfo and auth is available", async () => {
const onRequestCreatePr = vi.fn();
const task = makeTask({ column: "in-review", prInfo: undefined });