feat(KB-165): add responsive mobile layout for settings modal
- Add mobile CSS in @media query to stack settings sidebar horizontally above content - Style nav items as horizontal scrollable tabs with bottom-border active indicator - Adjust settings content, form groups, and auth provider rows for narrow viewports - Add SettingsModal tests verifying .settings-layout, .settings-sidebar, and .settings-content structure - Add test for active nav item class toggling and auth provider row DOM structure
This commit is contained in:
@@ -563,4 +563,80 @@ describe("SettingsModal", () => {
|
||||
expect(screen.getByText("Anthropic")).toBeTruthy();
|
||||
expect(screen.getByText("GitHub")).toBeTruthy();
|
||||
});
|
||||
|
||||
// --- Mobile structure tests (DOM classes that responsive CSS targets) ---
|
||||
|
||||
it("has .settings-layout wrapping sidebar and content", async () => {
|
||||
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
const layout = container.querySelector(".settings-layout");
|
||||
expect(layout).toBeTruthy();
|
||||
expect(layout!.querySelector(".settings-sidebar")).toBeTruthy();
|
||||
expect(layout!.querySelector(".settings-content")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has .settings-sidebar with 7 .settings-nav-item buttons for all sections", async () => {
|
||||
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
const sidebar = container.querySelector(".settings-sidebar");
|
||||
expect(sidebar).toBeTruthy();
|
||||
const navItems = sidebar!.querySelectorAll(".settings-nav-item");
|
||||
expect(navItems.length).toBe(7);
|
||||
|
||||
const labels = Array.from(navItems).map((el) => el.textContent);
|
||||
expect(labels).toEqual(["General", "Model", "Scheduling", "Worktrees", "Commands", "Merge", "Authentication"]);
|
||||
});
|
||||
|
||||
it("has .settings-content as sibling of .settings-sidebar", async () => {
|
||||
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
const layout = container.querySelector(".settings-layout");
|
||||
const children = Array.from(layout!.children);
|
||||
const sidebar = children.find((el) => el.classList.contains("settings-sidebar"));
|
||||
const content = children.find((el) => el.classList.contains("settings-content"));
|
||||
expect(sidebar).toBeTruthy();
|
||||
expect(content).toBeTruthy();
|
||||
});
|
||||
|
||||
it("marks the active nav item with .active class", async () => {
|
||||
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
// Default active section is General
|
||||
const activeItems = container.querySelectorAll(".settings-nav-item.active");
|
||||
expect(activeItems.length).toBe(1);
|
||||
expect(activeItems[0].textContent).toBe("General");
|
||||
|
||||
// Switch to Scheduling
|
||||
fireEvent.click(screen.getByText("Scheduling"));
|
||||
const newActive = container.querySelectorAll(".settings-nav-item.active");
|
||||
expect(newActive.length).toBe(1);
|
||||
expect(newActive[0].textContent).toBe("Scheduling");
|
||||
});
|
||||
|
||||
it("auth provider rows contain .auth-provider-info and action button", async () => {
|
||||
(fetchAuthStatus as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: true },
|
||||
{ id: "github", name: "GitHub", authenticated: false },
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Authentication"));
|
||||
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
|
||||
|
||||
const rows = container.querySelectorAll(".auth-provider-row");
|
||||
expect(rows.length).toBe(2);
|
||||
|
||||
for (const row of rows) {
|
||||
expect(row.querySelector(".auth-provider-info")).toBeTruthy();
|
||||
expect(row.querySelector("button")).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1343,4 +1343,61 @@ html, body {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Settings modal: stack sidebar above content for mobile */
|
||||
.settings-layout {
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
flex-direction: row;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 6px 8px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.settings-nav-item {
|
||||
border-left: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
padding: 6px 12px;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-nav-item:hover {
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: var(--border);
|
||||
}
|
||||
|
||||
.settings-nav-item.active {
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: var(--todo);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.settings-section-heading {
|
||||
padding: 12px 14px 10px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.auth-provider-row {
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user