fix(dashboard): repair lint+test regressions from CSS sweep and panel rearrange
- eslint: ignore .claude/** so agent worktree scripts don't pull thousands of spurious "console/process is not defined" errors into the workspace lint. - AgentsView: lift the controls popup max-height/overflow into a CSS modifier (.agent-controls-panel--scrollable) instead of an inline style so the inline-style discipline test stays at 4 (only runtime health colors). - board-mobile.test: scan every @media (max-width: 768px) block in the loaded CSS bundle, not just the legacy section in styles.css. After the cards/ inline-create extraction those rules now live in component CSS files. - AgentsView.test: update the layout assertion for the new "stats above the collection" placement and open the controls popup before asserting on the token-usage panel (which now lives there, not in the main view body). - agents-view-mobile.test: open the controls popup before asserting on the token-stats panel content. - TaskDetailModal.test: bump tab-count assertions from 7→8 (or 6→7 for triage/todo) to include the new Stats tab, and switch to the Stats tab before asserting on token-usage text in the optimistic-loading suite. - Column.test: add MoreVertical/ChevronDown/ChevronUp/Archive to the lucide-react vi.mock so the new column dropdown menu renders. After this: pnpm lint = 0 errors, pnpm typecheck = clean, pnpm test = all 9132 dashboard + plugin/engine/cli/desktop/mobile/core tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -114,6 +114,11 @@
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.agent-controls-panel--scrollable {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.agent-controls-panel .agent-controls {
|
||||
margin-bottom: 0;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -753,7 +753,6 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
role="dialog"
|
||||
aria-label="Agent controls"
|
||||
aria-modal="false"
|
||||
style={{ maxHeight: "70vh", overflowY: "auto" }}
|
||||
>
|
||||
<div className="agent-controls">
|
||||
<div className="agent-controls-filters">
|
||||
|
||||
@@ -170,24 +170,23 @@ describe("AgentsView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders secondary sections after the main collection", async () => {
|
||||
it("renders metrics above the main collection and active panel after it", async () => {
|
||||
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".agent-list")).toBeTruthy();
|
||||
expect(container.querySelector(".agent-metrics-bar")).toBeTruthy();
|
||||
expect(container.querySelector(".agent-token-stats-panel")).toBeTruthy();
|
||||
expect(container.querySelector(".active-agents-panel")).toBeTruthy();
|
||||
});
|
||||
|
||||
const list = container.querySelector(".agent-list");
|
||||
const metrics = container.querySelector(".agent-metrics-bar");
|
||||
const tokenPanel = container.querySelector(".agent-token-stats-panel");
|
||||
const activePanel = container.querySelector(".active-agents-panel");
|
||||
expect(list && metrics && tokenPanel && activePanel).toBeTruthy();
|
||||
expect(list!.compareDocumentPosition(metrics!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(metrics!.compareDocumentPosition(tokenPanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(tokenPanel!.compareDocumentPosition(activePanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(list && metrics && activePanel).toBeTruthy();
|
||||
// Metrics bar sits above the agent list (top-of-view stats placement).
|
||||
expect(metrics!.compareDocumentPosition(list!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
// Active agents panel comes after the main agent collection.
|
||||
expect(list!.compareDocumentPosition(activePanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
});
|
||||
|
||||
it("fetches agents only once on mount (regression: no duplicate initial load path)", async () => {
|
||||
@@ -205,6 +204,10 @@ describe("AgentsView", () => {
|
||||
it("renders token stats derived from the currently displayed agents", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
// Token-usage panel now lives inside the controls popup, not in the
|
||||
// main view body — open the controls panel before asserting.
|
||||
await openControlsPanel();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Token Usage by Agent")).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -34,6 +34,10 @@ vi.mock("../QuickEntryBox", () => ({
|
||||
vi.mock("lucide-react", () => ({
|
||||
Link: () => null,
|
||||
Clock: () => null,
|
||||
ChevronDown: () => null,
|
||||
ChevronUp: () => null,
|
||||
Archive: () => null,
|
||||
MoreVertical: () => null,
|
||||
}));
|
||||
|
||||
// Mock usePluginUiSlots hook
|
||||
|
||||
@@ -1532,13 +1532,14 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// For an in-progress task (no workflow steps, no merge commit),
|
||||
// the top-level tabs are: Definition, Logs, Changes, Comments, Documents, Model, Workflow
|
||||
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Documents", "Model", "Workflow"];
|
||||
// For an in-progress task (no workflow steps, no merge commit), the
|
||||
// top-level tabs are: Definition, Logs, Changes, Comments, Documents,
|
||||
// Model, Workflow, Stats.
|
||||
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Documents", "Model", "Workflow", "Stats"];
|
||||
const tabs = screen.getAllByRole("button").filter((b) =>
|
||||
tabTexts.includes(b.textContent || "")
|
||||
);
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
@@ -1546,9 +1547,10 @@ describe("TaskDetailModal", () => {
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
|
||||
// Activity and Agent Log are NOT top-level tabs (they are subviews inside Logs)
|
||||
expect(container.querySelectorAll(".detail-tab").length).toBe(7);
|
||||
expect(container.querySelectorAll(".detail-tab").length).toBe(8);
|
||||
// Workflow tab should always appear even when no workflow steps are configured
|
||||
expect(screen.getByText("Workflow")).toBeInTheDocument();
|
||||
// Commits tab should NOT appear for non-done tasks
|
||||
@@ -2415,7 +2417,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(container.querySelector(".modal-actions .modal-actions-spacer")).toBeTruthy();
|
||||
expect(container.querySelector(".detail-body")).toBeTruthy();
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs[0].classList.contains("detail-tab-active")).toBe(true);
|
||||
expect(Array.from(tabs).slice(1).every((t) => !t.classList.contains("detail-tab-active"))).toBe(true);
|
||||
// Responsive CSS controls sizing — no inline padding/fontSize/borderBottom leaks
|
||||
@@ -3109,10 +3111,10 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// In-progress tasks show exactly 7 tabs:
|
||||
// Definition, Logs, Changes, Comments, Documents, Model, Workflow
|
||||
// In-progress tasks show exactly 8 tabs:
|
||||
// Definition, Logs, Changes, Comments, Documents, Model, Workflow, Stats
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
@@ -3120,6 +3122,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
// Commits tab should NOT be present for non-done tasks
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -3137,9 +3140,9 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// In-progress task with workflow steps: 7 tabs (Workflow after Model)
|
||||
// In-progress task with workflow steps: 8 tabs (Workflow after Model, Stats last)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
@@ -3147,6 +3150,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
});
|
||||
|
||||
it("does NOT show Commits tab for done task with mergeDetails.commitSha (changes merged into Changes tab)", () => {
|
||||
@@ -3165,9 +3169,9 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Comments, Documents, Model, Workflow (7 tabs, no Commits)
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Comments, Documents, Model, Workflow, Stats (8 tabs, no Commits)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
@@ -3175,11 +3179,12 @@ describe("TaskDetailModal", () => {
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
// Commits tab should NOT be present
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows 6 tabs for done task with workflow steps and commit SHA (Commits merged into Changes)", () => {
|
||||
it("shows 8 tabs for done task with workflow steps and commit SHA (Commits merged into Changes)", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
@@ -3196,9 +3201,9 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Done task with workflow steps and commit SHA: 7 tabs (no Commits)
|
||||
// Done task with workflow steps and commit SHA: 8 tabs (no Commits)
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs.length).toBe(8);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
@@ -3206,6 +3211,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(tabs[4].textContent).toBe("Documents");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
expect(tabs[7].textContent).toBe("Stats");
|
||||
// Commits tab should NOT be present
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -3224,9 +3230,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
|
||||
expect(triageTabs.length).toBe(6); // Definition, Logs, Comments, Documents, Model, Workflow
|
||||
expect(triageTabs.length).toBe(7); // Definition, Logs, Comments, Documents, Model, Workflow, Stats
|
||||
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow",
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow", "Stats",
|
||||
]);
|
||||
|
||||
const { container: todoContainer } = render(
|
||||
@@ -3242,9 +3248,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
|
||||
expect(todoTabs.length).toBe(6); // Definition, Logs, Comments, Documents, Model, Workflow
|
||||
expect(todoTabs.length).toBe(7); // Definition, Logs, Comments, Documents, Model, Workflow, Stats
|
||||
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow",
|
||||
"Definition", "Logs", "Comments", "Documents", "Model", "Workflow", "Stats",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -5393,6 +5399,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText("Loading specification…")).toBeDefined();
|
||||
// Token stats now live in their own Stats tab — switch to it before
|
||||
// asserting on token-loading text.
|
||||
fireEvent.click(screen.getByRole("button", { name: "Stats" }));
|
||||
expect(screen.getByText("Loading token statistics…")).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -5451,6 +5460,9 @@ describe("TaskDetailModal", () => {
|
||||
|
||||
// Loading indicator should be gone
|
||||
expect(screen.queryByText("Loading specification…")).toBeNull();
|
||||
|
||||
// Token stats live behind the Stats tab now.
|
||||
fireEvent.click(screen.getByRole("button", { name: "Stats" }));
|
||||
expect(screen.queryByText("Loading token statistics…")).toBeNull();
|
||||
expect(screen.getByText((1200).toLocaleString())).toBeInTheDocument();
|
||||
expect(screen.getByText((450).toLocaleString())).toBeInTheDocument();
|
||||
@@ -5496,6 +5508,12 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Token stats live behind the Stats tab now — wait for the modal to
|
||||
// settle, then switch tabs and assert on the empty state.
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Loading specification…")).toBeNull();
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Stats" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No token usage recorded for this task yet.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -136,6 +136,12 @@ describe("AgentsView mobile adaptations", () => {
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".agent-list")).toBeTruthy();
|
||||
expect(container.querySelectorAll(".agent-card").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Token-stats panel now lives in the controls popup; open it before
|
||||
// asserting on the panel content.
|
||||
fireEvent.click(screen.getByRole("button", { name: "Controls" }));
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".agent-token-stats-panel")).toBeTruthy();
|
||||
expect(screen.getByText("Combined Tokens")).toBeTruthy();
|
||||
expect(screen.getByText("100")).toBeTruthy();
|
||||
|
||||
@@ -47,13 +47,28 @@ vi.mock("../../hooks/useTaskDiffStats", () => ({
|
||||
|
||||
|
||||
function getMainMobileSection(css: string): string {
|
||||
const sectionStart = css.indexOf("/* === Mobile Responsive Overrides ===");
|
||||
const sectionEnd = css.indexOf("/* === Tablet Responsive Tier", sectionStart);
|
||||
// Mobile rules now live across many co-located component CSS files (each
|
||||
// owns its own @media (max-width: 768px) block) instead of one monolith
|
||||
// section in styles.css. Concatenate every <=768px media block in the
|
||||
// bundle so these assertions remain location-agnostic.
|
||||
const re = /@media\s*\([^)]*max-width:\s*768px[^)]*\)\s*\{/g;
|
||||
const blocks: string[] = [];
|
||||
|
||||
expect(sectionStart).toBeGreaterThan(-1);
|
||||
expect(sectionEnd).toBeGreaterThan(sectionStart);
|
||||
for (const match of css.matchAll(re)) {
|
||||
const start = match.index! + match[0].length;
|
||||
let depth = 1;
|
||||
let i = start;
|
||||
while (i < css.length && depth > 0) {
|
||||
const ch = css[i];
|
||||
if (ch === "{") depth++;
|
||||
else if (ch === "}") depth--;
|
||||
i++;
|
||||
}
|
||||
blocks.push(css.slice(start, i - 1));
|
||||
}
|
||||
|
||||
return css.slice(sectionStart, sectionEnd);
|
||||
expect(blocks.length).toBeGreaterThan(0);
|
||||
return blocks.join("\n");
|
||||
}
|
||||
|
||||
function expectRuleToContain(section: string, selectorFragment: string, declaration: string): void {
|
||||
|
||||
Reference in New Issue
Block a user