feat(FN-1316): add mobile responsive styles and tests for ProjectOverview

- Add mobile responsive CSS rules for ProjectOverview component
- Add mobile breakpoint styles for card layouts and navigation elements
- Add ProjectOverview mobile structure tests for responsive behavior
- Add ProjectCard mobile visibility tests
This commit is contained in:
gsxdsm
2026-04-09 09:33:31 -07:00
parent 914e80add3
commit 33b80d1d35
3 changed files with 211 additions and 0 deletions

View File

@@ -459,4 +459,28 @@ describe("ProjectCard", () => {
// The formatted date should show (1/15/2026 format in US locale)
expect(screen.getByText(/1\//)).toBeDefined();
});
describe("mobile responsive structure", () => {
it("renders card with correct class structure for mobile CSS targets", () => {
const { container } = render(
<ProjectCard
project={makeProject()}
health={makeHealth()}
onSelect={noop}
onPause={noop}
onResume={noop}
onRemove={noop}
/>
);
// Verify all mobile-targeted class names exist
expect(container.querySelector(".project-card")).not.toBeNull();
expect(container.querySelector(".project-card-header")).not.toBeNull();
expect(container.querySelector(".project-card-health")).not.toBeNull();
expect(container.querySelector(".project-card-footer")).not.toBeNull();
expect(container.querySelector(".project-card-actions")).not.toBeNull();
// Verify action buttons have mobile-targeted classes
expect(container.querySelector(".project-card-action")).not.toBeNull();
});
});
});

View File

@@ -339,4 +339,88 @@ describe("ProjectOverview", () => {
const erroredTab = screen.getAllByText("Errored").find(el => el.tagName === "BUTTON");
expect(erroredTab?.className).toContain("has-errors");
});
describe("mobile responsive structure", () => {
it("renders overview with correct class structure for mobile CSS targets", () => {
const { container } = render(
<ProjectOverview
projects={[makeProject()]}
onSelectProject={noop}
onAddProject={noop}
onPauseProject={noop}
onResumeProject={noop}
onRemoveProject={noop}
/>
);
// Verify container class exists for mobile override targeting
expect(container.querySelector(".project-overview")).not.toBeNull();
// Verify header section
expect(container.querySelector(".project-overview__header")).not.toBeNull();
// Verify stats section
expect(container.querySelector(".project-overview__stats")).not.toBeNull();
// Verify filters section
expect(container.querySelector(".project-overview__filters")).not.toBeNull();
// Verify grid container
expect(container.querySelector(".project-grid")).not.toBeNull();
});
it("renders stat elements with value/label structure for mobile sizing", () => {
const { container } = render(
<ProjectOverview
projects={[makeProject()]}
onSelectProject={noop}
onAddProject={noop}
onPauseProject={noop}
onResumeProject={noop}
onRemoveProject={noop}
/>
);
// Stats should have the nested structure mobile CSS targets
const stats = container.querySelectorAll(".project-stat");
expect(stats.length).toBeGreaterThanOrEqual(3); // Total, Active Tasks, Completed
stats.forEach((stat) => {
expect(stat.querySelector(".project-stat__value")).not.toBeNull();
expect(stat.querySelector(".project-stat__label")).not.toBeNull();
});
});
it("renders filter tabs with correct classes for mobile stacking", () => {
const { container } = render(
<ProjectOverview
projects={[makeProject()]}
onSelectProject={noop}
onAddProject={noop}
onPauseProject={noop}
onResumeProject={noop}
onRemoveProject={noop}
/>
);
expect(container.querySelector(".project-filter-tabs")).not.toBeNull();
const tabs = container.querySelectorAll(".project-filter-tab");
expect(tabs.length).toBe(4); // All, Active, Paused, Errored
tabs.forEach((tab) => {
expect(tab.querySelector(".project-filter-count")).not.toBeNull();
});
});
it("renders sort select with aria-label for mobile full-width targeting", () => {
const { container } = render(
<ProjectOverview
projects={[makeProject()]}
onSelectProject={noop}
onAddProject={noop}
onPauseProject={noop}
onResumeProject={noop}
onRemoveProject={noop}
/>
);
expect(container.querySelector(".project-sort")).not.toBeNull();
expect(container.querySelector(".project-sort-select")).not.toBeNull();
expect(screen.getByLabelText("Sort projects")).toBeDefined();
});
});
});

View File

@@ -2241,6 +2241,109 @@ body {
.project-card--skeleton:nth-child(5) .project-skeleton--title { animation-delay: 0.4s; }
.project-card--skeleton:nth-child(6) .project-skeleton--title { animation-delay: 0.5s; }
/* === ProjectOverview Mobile Responsive (FN-1316) === */
@media (max-width: 768px) {
/* Overview container */
.project-overview {
padding: var(--space-md);
}
/* Header */
.project-overview__header {
flex-direction: column;
align-items: stretch;
gap: var(--space-md);
}
.project-overview__title {
font-size: 18px;
}
.project-overview__stats {
flex-direction: row;
flex-wrap: wrap;
flex: none;
}
.project-stat {
padding: var(--space-xs) var(--space-sm);
}
.project-stat__value {
font-size: 14px;
}
.project-stat__label {
font-size: 10px;
}
.project-overview__add-btn {
width: 100%;
justify-content: center;
}
/* Filters */
.project-overview__filters {
flex-direction: column;
align-items: stretch;
}
.project-filter-tabs {
width: 100%;
justify-content: space-between;
}
.project-filter-tab {
flex: 1;
justify-content: center;
font-size: 12px;
padding: 6px 8px;
}
.project-sort {
width: 100%;
justify-content: space-between;
}
.project-sort-select {
flex: 1;
margin-left: var(--space-sm);
}
/* Grid */
.project-grid {
grid-template-columns: 1fr;
gap: var(--space-md);
}
/* Cards */
.project-card {
min-width: 0;
}
.project-card-health {
flex-wrap: wrap;
gap: var(--space-md);
}
.project-card-footer {
flex-direction: column;
align-items: stretch;
gap: var(--space-sm);
}
.project-card-actions {
justify-content: flex-end;
flex-wrap: wrap;
}
/* Empty state */
.project-empty-state {
padding: var(--space-lg);
max-width: 100%;
}
}
/* --- Dashboard Loader --- */
@keyframes dashboard-loader-pulse {
0%,