feat(FN-4047): add responsive compact timestamps and mobile icon-only contr

Restored compact timestamp rendering in AgentDetailView and TaskDetailModal, added responsive timestamp layout for mobile and desktop, and added mobile icon-only stop/run controls to AgentDetailView, with new tests covering both components and a documentation note for the compact timestamp behavior.

Fusion-Task-Id: FN-4047
This commit is contained in:
Fusion
2026-05-11 18:50:27 -07:00
committed by gsxdsm
parent 1772572191
commit 2484591ae2
7 changed files with 149 additions and 13 deletions

View File

@@ -1221,6 +1221,22 @@ describe("AgentDetailView", () => {
});
});
it("keeps active header Stop and Run Now buttons accessible by name while using mobile icon-control class", async () => {
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>,
);
const stopButton = await screen.findByRole("button", { name: "Stop" });
const runNowButton = await screen.findByRole("button", { name: "Run now for Test Agent" });
expect(stopButton.className).toContain("agent-detail-mobile-icon-control");
expect(runNowButton.className).toContain("agent-detail-mobile-icon-control");
});
it("transitions running agent to paused when Stop is clicked", async () => {
mockFetchAgent.mockResolvedValue(createMockAgent({ state: "running" }));
@@ -1312,6 +1328,7 @@ describe("AgentDetailView", () => {
expect(stylesContent).toContain("grid-column: 1;");
expect(stylesContent).toContain(".agent-detail-header-actions {");
expect(stylesContent).toContain("grid-column: 2;");
expect(stylesContent).toContain(".agent-detail-controls .agent-detail-mobile-icon-control {");
expect(stylesContent).toContain(".agent-detail-mobile-icon-control .agent-detail-control-label {");
});

View File

@@ -1,4 +1,4 @@
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
@@ -162,6 +162,65 @@ describe("TaskDetailModal", () => {
expect(timestamps).toBeTruthy();
expect(provenance?.compareDocumentPosition(timestamps as Node) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
describe("compact timestamp metadata", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-11T12:00:00.000Z"));
});
afterEach(() => {
vi.useRealTimers();
});
it("renders compact relative timestamps for recent tasks", () => {
render(
<TaskDetailModal
task={makeTask({
sourceType: "dashboard_ui",
createdAt: "2026-05-09T12:00:00.000Z",
updatedAt: "2026-05-11T09:00:00.000Z",
})}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
const timestamps = screen.getByLabelText("Task timestamps");
expect(timestamps).toHaveTextContent("Created 2d ago");
expect(timestamps).toHaveTextContent("Updated 3h ago");
const times = timestamps.querySelectorAll("time");
expect(times[0]?.getAttribute("dateTime")).toBe("2026-05-09T12:00:00.000Z");
expect(times[1]?.getAttribute("dateTime")).toBe("2026-05-11T09:00:00.000Z");
});
it("renders short calendar date for older timestamps", () => {
render(
<TaskDetailModal
task={makeTask({
sourceType: "dashboard_ui",
createdAt: "2026-05-01T12:00:00.000Z",
updatedAt: "2026-05-02T12:00:00.000Z",
})}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
const timestamps = screen.getByLabelText("Task timestamps");
expect(timestamps).toHaveTextContent("Created May 1");
expect(timestamps).toHaveTextContent("Updated May 2");
});
});
});
it("renders modal wrapper structure and default close control", () => {

View File

@@ -27,6 +27,17 @@ describe("TaskDetailModal", () => {
expectBaseRule(css, ".detail-meta-inline-controls", "flex-wrap: nowrap;");
expect(css).not.toMatch(/@media \(max-width: 640px\)\s*\{[^}]*\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/);
});
it("uses grouped timestamp metadata on desktop with mobile stacked overrides", () => {
const css = readDashboardStylesSource();
expectBaseRule(css, ".detail-timestamps", "display: inline-flex;");
expectBaseRule(css, ".detail-timestamps", "flex-wrap: wrap;");
expectBaseRule(css, ".detail-timestamp-item", "display: inline-flex;");
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{\s*\.detail-provenance\s*\{[^}]*\}\s*\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/);
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.detail-timestamp-separator\s*\{[^}]*display:\s*none;/);
});
it("renders responsive structural classes (modal-lg, overlay, spacer, tabs, detail-body)", () => {
const { container } = render(
<TaskDetailModal
@@ -43,6 +54,8 @@ describe("TaskDetailModal", () => {
expect(container.querySelector(".modal-overlay.open")).toBeTruthy();
expect(container.querySelector(".modal-actions .modal-actions-spacer")).toBeTruthy();
expect(container.querySelector(".detail-body")).toBeTruthy();
expect(container.querySelector(".detail-timestamps")).toBeTruthy();
expect(container.querySelectorAll(".detail-timestamp-item").length).toBe(2);
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(10);
expect(tabs[0].classList.contains("detail-tab-active")).toBe(true);