FN-5903: fix SkillsView mobile row wrapping

Keep mobile discovered-skill rows aligned after toggles and skill enablement.

- keep .skills-view-item rows from wrapping at the mobile breakpoint
- let .skills-view-item-info flex instead of forcing full-width stacking
- add CSS regression coverage for the mobile row layout and toggle geometry
- add component coverage that preserves row structure across enabled and disabled toggle states

Files changed:
 packages/dashboard/app/components/SkillsView.css   |  5 ++-
 .../components/__tests__/SkillsView.css.test.ts    | 49 ++++++++++++++++++++++
 .../app/components/__tests__/SkillsView.test.tsx   | 47 +++++++++++++++++++++
 .../__tests__/skills-view-mobile.test.tsx          |  9 ++--
 4 files changed, 104 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-5903

Fusion-Task-Lineage: 1004a702-28e7-4e7e-af22-591cc122be01
This commit is contained in:
gsxdsm
2026-06-02 16:11:42 -07:00
parent 8666c9fdc2
commit 32f8cac284
4 changed files with 104 additions and 6 deletions

View File

@@ -413,11 +413,12 @@
.skills-view-item {
padding: var(--space-md);
min-height: calc(var(--space-lg) + var(--space-md) + var(--space-xs));
flex-wrap: wrap;
flex-wrap: nowrap;
}
.skills-view-item-info {
width: 100%;
flex: 1 1 auto;
width: auto;
}
.skills-view-item-toggle {

View File

@@ -2,6 +2,36 @@ import { describe, expect, it } from "vitest";
import { loadAllAppCss } from "../../test/cssFixture";
function extractRuleBlock(css: string, selector: string): string {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const matches = [...css.matchAll(new RegExp(`${escapedSelector}\\s*\\{([^}]*)\\}`, "g"))];
return matches.at(-1)?.[1] ?? "";
}
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media[^{]*\(max-width: 768px\)[^{]*\{/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {
const startIdx = match.index + match[0].length;
let braceCount = 1;
let endIdx = startIdx;
while (braceCount > 0 && endIdx < content.length) {
if (content[endIdx] === "{") braceCount += 1;
if (content[endIdx] === "}") braceCount -= 1;
endIdx += 1;
}
if (braceCount === 0) {
blocks.push(content.slice(startIdx, endIdx - 1));
}
}
return blocks.join("\n");
}
describe("SkillsView/runtime-card token guardrails", () => {
it("does not use forbidden runtime fallback literals/tokens", async () => {
const css = await loadAllAppCss();
@@ -13,4 +43,23 @@ describe("SkillsView/runtime-card token guardrails", () => {
expect(css).not.toContain("var(--accent-red, #ef4444)");
expect(css).not.toContain("var(--accent, #4f46e5)");
});
it("keeps discovered-skill rows on one line at the mobile breakpoint", async () => {
const css = await loadAllAppCss();
const mobileMediaBlock = extractMobileMediaBlocks(css);
const itemBlock = extractRuleBlock(mobileMediaBlock, ".skills-view-item");
const infoBlock = extractRuleBlock(mobileMediaBlock, ".skills-view-item-info");
expect(itemBlock).toContain("flex-wrap: nowrap");
expect(infoBlock).toContain("flex: 1 1 auto");
expect(infoBlock).toContain("width: auto");
});
it("keeps checked and unchecked toggle geometry token-aligned", async () => {
const css = await loadAllAppCss();
expect(css).toContain("width: calc(var(--space-xl) + var(--space-lg))");
expect(css).toContain("transform: translateX(calc(var(--space-lg) + (var(--space-xs) / 2)))");
expect(css).toContain("background: var(--color-success)");
});
});

View File

@@ -308,6 +308,53 @@ describe("SkillsView", () => {
// Should revert to original state
expect((enabledToggle as HTMLInputElement).checked).toBe(true);
});
it("keeps the discovered-skill row structure stable for enabled and disabled states", async () => {
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
await waitFor(() => {
expect(screen.getByText("test-skill")).toBeTruthy();
expect(screen.getByText("another-skill")).toBeTruthy();
});
const enabledRow = screen.getByText("test-skill").closest(".skills-view-item");
const disabledRow = screen.getByText("another-skill").closest(".skills-view-item");
const disabledToggle = screen.getByLabelText("Enable another-skill") as HTMLInputElement;
const enabledToggle = screen.getByLabelText("Disable test-skill") as HTMLInputElement;
expect(enabledRow?.querySelector(".skills-view-item-info")).toBeTruthy();
expect(enabledRow?.querySelector(".skills-view-item-toggle")).toBeTruthy();
expect(enabledRow?.querySelector(".skills-view-toggle-slider")).toBeTruthy();
expect(disabledRow?.querySelector(".skills-view-item-info")).toBeTruthy();
expect(disabledRow?.querySelector(".skills-view-item-toggle")).toBeTruthy();
expect(disabledRow?.querySelector(".skills-view-toggle-slider")).toBeTruthy();
await act(async () => {
fireEvent.click(disabledToggle);
});
await waitFor(() => {
expect(screen.getByLabelText("Disable another-skill")).toBeTruthy();
});
await act(async () => {
fireEvent.click(enabledToggle);
});
await waitFor(() => {
expect(screen.getByLabelText("Enable test-skill")).toBeTruthy();
});
const enabledRowAfterToggle = screen.getByText("test-skill").closest(".skills-view-item");
const disabledRowAfterToggle = screen.getByText("another-skill").closest(".skills-view-item");
expect(enabledRowAfterToggle?.querySelector(".skills-view-item-info")).toBeTruthy();
expect(enabledRowAfterToggle?.querySelector(".skills-view-item-toggle")).toBeTruthy();
expect(enabledRowAfterToggle?.querySelector(".skills-view-toggle-slider")).toBeTruthy();
expect(disabledRowAfterToggle?.querySelector(".skills-view-item-info")).toBeTruthy();
expect(disabledRowAfterToggle?.querySelector(".skills-view-item-toggle")).toBeTruthy();
expect(disabledRowAfterToggle?.querySelector(".skills-view-toggle-slider")).toBeTruthy();
});
});
describe("catalog search", () => {

View File

@@ -82,10 +82,10 @@ describe("skills-view mobile css", () => {
expect(mobileMediaBlock).toMatch(/\.skills-view-grid\s*\{[^}]*grid-template-columns:\s*1fr/);
});
it("defines .skills-view-item wrapping on mobile", () => {
it("keeps .skills-view-item on one line on mobile", () => {
expect(mobileMediaBlock).toContain(".skills-view-item");
const block = extractRuleBlock(mobileMediaBlock, ".skills-view-item");
expect(block).toContain("flex-wrap: wrap");
expect(block).toContain("flex-wrap: nowrap");
});
it("defines .skills-view-toggle-slider with minimum dimensions on mobile", () => {
@@ -143,10 +143,11 @@ describe("skills-view mobile css", () => {
expect(block).toContain("padding: var(--space-md)");
});
it("defines .skills-view-item-info with full width on mobile", () => {
it("keeps .skills-view-item-info flexible instead of forcing full width on mobile", () => {
expect(mobileMediaBlock).toContain(".skills-view-item-info");
const block = extractRuleBlock(mobileMediaBlock, ".skills-view-item-info");
expect(block).toContain("width: 100%");
expect(block).toContain("flex: 1 1 auto");
expect(block).toContain("width: auto");
});
it("defines .skills-view-item-toggle with padding on mobile", () => {