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:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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%,
|
||||
|
||||
Reference in New Issue
Block a user