From 6b528350c003431657db6a7612a0bdf62710a74e Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Thu, 10 Sep 2026 07:04:40 -0400 Subject: [PATCH] fix: correct catalog error contrast and add catalog Storybook coverage --- apps/web/.storybook/preview.js | 7 ++++ apps/web/.storybook/router.js | 24 ++++++++++++++ apps/web/.storybook/test-runner.js | 10 ++++++ apps/web/src/assets/styles/global.scss | 2 +- apps/web/src/pages/CatalogPage.stories.js | 39 +++++++++++++++++++++++ docs/IMPLEMENTATION-STATUS.md | 2 ++ 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 apps/web/.storybook/router.js create mode 100644 apps/web/.storybook/test-runner.js create mode 100644 apps/web/src/pages/CatalogPage.stories.js diff --git a/apps/web/.storybook/preview.js b/apps/web/.storybook/preview.js index 8247ecf..359cb7a 100644 --- a/apps/web/.storybook/preview.js +++ b/apps/web/.storybook/preview.js @@ -1,4 +1,11 @@ +import { setup } from '@storybook/vue3'; + import '../src/assets/styles/global.scss'; +import { storybookRouter } from './router.js'; + +setup((app) => { + app.use(storybookRouter); +}); export const globalTypes = { theme: { diff --git a/apps/web/.storybook/router.js b/apps/web/.storybook/router.js new file mode 100644 index 0000000..7ba94f2 --- /dev/null +++ b/apps/web/.storybook/router.js @@ -0,0 +1,24 @@ +import { createMemoryHistory, createRouter } from 'vue-router'; + +const stub = { render: () => null }; + +// Mirrors the named routes in src/router/index.js so RouterLink resolution +// works in isolated stories without importing the real app router/guards. +export const storybookRouter = createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/', name: 'catalog', component: stub }, + { path: '/listings/:id', name: 'listing-detail', component: stub }, + { path: '/products/:slug', name: 'product-detail', component: stub }, + { path: '/login', name: 'login', component: stub }, + { path: '/admin', name: 'admin-home', component: stub }, + { path: '/admin/inventory/:id/images', name: 'admin-image-manager', component: stub }, + { path: '/admin/inventory', name: 'admin-inventory', component: stub }, + { path: '/admin/listings', name: 'admin-listings', component: stub }, + { path: '/admin/sales', name: 'admin-sales', component: stub }, + { path: '/admin/acquisitions', name: 'admin-acquisitions', component: stub }, + { path: '/admin/settings', name: 'admin-settings', component: stub }, + { path: '/admin/saved-views', name: 'admin-saved-views', component: stub }, + { path: '/:pathMatch(.*)*', name: 'storybook-catch-all', component: stub }, + ], +}); diff --git a/apps/web/.storybook/test-runner.js b/apps/web/.storybook/test-runner.js new file mode 100644 index 0000000..7572f85 --- /dev/null +++ b/apps/web/.storybook/test-runner.js @@ -0,0 +1,10 @@ +// Forces a narrow viewport for stories that specifically exercise mobile-only +// CSS breakpoints, since @storybook/addon-viewport does not affect the +// headless iframe size that @storybook/test-runner navigates to directly. +export default { + async preVisit(page, context) { + if (context.id?.startsWith('pages-catalogpage--mobile')) { + await page.setViewportSize({ width: 375, height: 900 }); + } + }, +}; diff --git a/apps/web/src/assets/styles/global.scss b/apps/web/src/assets/styles/global.scss index a4da689..e1bcf37 100644 --- a/apps/web/src/assets/styles/global.scss +++ b/apps/web/src/assets/styles/global.scss @@ -34,7 +34,7 @@ h1 { max-width: 760px; margin: var(--space-3) 0 var(--space-4); font-size: var(- .product-facts dt { color: var(--color-text-muted); font-size: var(--text-xs); font-weight: var(--weight-bold); letter-spacing: 0.1em; text-transform: uppercase; } .product-facts dd { margin: 0; } .catalog-state { padding: var(--space-12) 0; color: var(--color-text-muted); } -.catalog-state--error { color: var(--color-danger-500); } +.catalog-state--error { color: var(--color-danger-text); } .pagination { display: flex; align-items: center; justify-content: center; gap: var(--space-6); margin-top: var(--space-8); color: var(--color-text-muted); font-size: var(--text-sm); } .pagination button { min-width: 44px; min-height: 44px; border: var(--border-thin) solid var(--color-border-strong); border-radius: var(--radius-md); padding: var(--space-2) var(--space-4); background: var(--color-surface); color: var(--color-interactive); } .pagination button:disabled { cursor: not-allowed; opacity: 0.45; } diff --git a/apps/web/src/pages/CatalogPage.stories.js b/apps/web/src/pages/CatalogPage.stories.js new file mode 100644 index 0000000..591706e --- /dev/null +++ b/apps/web/src/pages/CatalogPage.stories.js @@ -0,0 +1,39 @@ +import { expect, userEvent, within } from '@storybook/test'; + +import CatalogPage from './CatalogPage.vue'; + +export default { + title: 'Pages/CatalogPage', + component: CatalogPage, +}; + +export const Overview = {}; + +export const MobileFilterDisclosure = { + parameters: { + docs: { + description: { + story: + 'On narrow viewports, catalog filters collapse behind a keyboard-operable disclosure that hides filter controls from the accessibility tree until expanded.', + }, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const toggle = canvas.getByRole('button', { name: /Filters/ }); + const setFilter = canvas.getByPlaceholderText('SV151'); + + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + await expect(setFilter).not.toBeVisible(); + + await userEvent.click(toggle); + + await expect(toggle).toHaveAttribute('aria-expanded', 'true'); + await expect(setFilter).toBeVisible(); + + await userEvent.click(toggle); + + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + await expect(setFilter).not.toBeVisible(); + }, +}; diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index 34820fd..7476321 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -184,6 +184,8 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro - The guided wizard now manages focus and announces step changes: selecting a stage (via the stepper or Back/Next) moves keyboard/assistive-technology focus to that stage's labeled region and updates a live-region status announcement, with Storybook interaction coverage. - Seller-scoped latest market valuations are now readable through `GET /api/v1/admin/valuations`. - Physical inventory management (bulk archive, bulk pricing refresh, bulk tag updates, market valuations, and pricing job retry) is now a dedicated `/admin/inventory` screen with its own navigation entry; the guided workflow overview links to it and only tracks a lightweight inventory count for wizard completion. +- Storybook now installs a lightweight router mirroring the app's named routes plus a mobile-viewport-forcing test-runner hook, enabling isolated page stories (including the public catalog) that use `useRoute`/`useRouter` or narrow-viewport CSS breakpoints. +- The public catalog now has its own Storybook coverage, including an interaction test proving the mobile filter disclosure is keyboard-operable and correctly hides/reveals filter controls via `aria-expanded`. This review also found and fixed a real color-contrast failure in the catalog error-state text. ## Local Commands