diff --git a/apps/web/src/pages/AdminSavedViewsPage.stories.js b/apps/web/src/pages/AdminSavedViewsPage.stories.js index 93929f9..4247131 100644 --- a/apps/web/src/pages/AdminSavedViewsPage.stories.js +++ b/apps/web/src/pages/AdminSavedViewsPage.stories.js @@ -1,5 +1,158 @@ +import { expect, userEvent, within, waitFor } from '@storybook/test'; import AdminSavedViewsPage from './AdminSavedViewsPage.vue'; -export default { title: 'Pages/AdminSavedViewsPage', component: AdminSavedViewsPage }; +export default { + title: 'Pages/AdminSavedViewsPage', component: AdminSavedViewsPage, + beforeEach: (context) => { + const originalFetch = globalThis.fetch; + const scenario = context.parameters.savedViewsScenario; + let reads = 0; + let creates = 0; + let deletes = 0; + let views = scenario === 'empty' ? [] : [{ id: 'view-1', name: 'Needs images', filters: { status: 'ACTIVE' } }]; + const reply = (data) => new Response(JSON.stringify({ data }), { headers: { 'content-type': 'application/json' } }); + const fail = (message) => new Response(JSON.stringify({ error: { message } }), { status: 503 }); + globalThis.fetch = async (url, options = {}) => { + if (!String(url).includes('/admin/saved-views')) return originalFetch(url, options); + if (options.method === 'POST') { + creates += 1; + if (scenario === 'saving') return new Promise(() => {}); + if (scenario === 'create-error' && creates === 1) return fail('Could not create view. Try again.'); + const view = { id: 'view-2', ...JSON.parse(options.body) }; + views = [...views, view]; + return reply(view); + } + if (options.method === 'DELETE') { + deletes += 1; + if (scenario === 'deleting') return new Promise(() => {}); + if (scenario === 'delete-error' && deletes === 1) return fail('Could not delete view. Try again.'); + views = views.filter((view) => !String(url).endsWith(`/${view.id}`)); + return reply(null); + } + reads += 1; + if (scenario === 'loading') return new Promise(() => {}); + if (scenario === 'load-error' || (scenario === 'retry-load' && reads === 1)) return fail('Could not load saved views.'); + return reply(views); + }; + return () => { globalThis.fetch = originalFetch; }; + }, +}; export const SavedViews = {}; +export const Empty = { + parameters: { savedViewsScenario: 'empty' }, + play: async ({ canvasElement }) => { + await expect(await within(canvasElement).findByText('No saved views yet.')).toBeVisible(); + }, +}; +export const Loading = { + parameters: { savedViewsScenario: 'loading' }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole('status')).toHaveTextContent('Loading saved views...'); + await expect(canvas.queryByText('No saved views yet.')).toBeNull(); + await expect(canvas.queryByRole('button', { name: 'Save view' })).toBeNull(); + }, +}; +export const LoadError = { + parameters: { savedViewsScenario: 'load-error' }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findByRole('alert'); + await expect(canvas.queryByText('No saved views yet.')).toBeNull(); + await expect(canvas.queryByRole('button', { name: 'Save view' })).toBeNull(); + const retry = canvas.getByRole('button', { name: 'Retry loading saved views' }); + retry.focus(); + await userEvent.keyboard('{Enter}'); + await waitFor(() => expect(canvas.getByRole('button', { name: 'Retry loading saved views' })).toHaveFocus()); + }, +}; +export const RetryLoad = { + parameters: { savedViewsScenario: 'retry-load' }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const retry = await canvas.findByRole('button', { name: 'Retry loading saved views' }); + retry.focus(); + await userEvent.keyboard('{Enter}'); + await expect(await canvas.findByLabelText('Name')).toHaveFocus(); + await expect(canvas.getByText('Needs images')).toBeVisible(); + await expect(canvas.queryByRole('alert')).toBeNull(); + }, +}; +export const CreateFailureAndRetry = { + parameters: { savedViewsScenario: 'create-error' }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const name = await canvas.findByLabelText('Name'); + await userEvent.type(name, 'Available inventory'); + await userEvent.click(canvas.getByRole('button', { name: 'Save view' })); + await expect(await canvas.findByRole('alert')).toHaveTextContent('Could not create view. Try again.'); + await expect(name).toHaveValue('Available inventory'); + await expect(canvas.getByLabelText('Filters (JSON)')).toHaveValue('{}'); + await userEvent.click(canvas.getByRole('button', { name: 'Save view' })); + await waitFor(() => expect(canvas.getByRole('status')).toHaveTextContent('Saved view created.')); + await expect(name).toHaveValue(''); + await expect(canvas.getByText('Available inventory')).toBeVisible(); + await expect(canvas.queryByRole('alert')).toBeNull(); + }, +}; +export const DeleteFailureAndRetry = { + parameters: { savedViewsScenario: 'delete-error' }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const remove = await canvas.findByRole('button', { name: 'Delete' }); + remove.focus(); + await userEvent.keyboard('{Enter}'); + await expect(await canvas.findByRole('alert')).toHaveTextContent('Could not delete view. Try again.'); + await expect(canvas.getByText('Needs images')).toBeVisible(); + await expect(remove).toHaveFocus(); + await userEvent.keyboard('{Enter}'); + await waitFor(() => expect(canvas.getByRole('status')).toHaveTextContent('Saved view deleted.')); + await expect(canvas.queryByRole('alert')).toBeNull(); + await expect(canvas.getByText('No saved views yet.')).toBeVisible(); + await expect(canvas.getByRole('heading', { name: 'Your saved views' })).toHaveFocus(); + }, +}; +export const Deleting = { + parameters: { savedViewsScenario: 'deleting' }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(await canvas.findByRole('button', { name: 'Delete' })); + await expect(canvas.getByRole('status')).toHaveTextContent('Deleting view...'); + await expect(canvas.getByRole('button', { name: 'Delete' })).toBeDisabled(); + await expect(canvas.getByRole('button', { name: 'Save view' })).toBeDisabled(); + await expect(canvas.getByLabelText('Name')).toBeDisabled(); + }, +}; +export const Saving = { + parameters: { savedViewsScenario: 'saving' }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(await canvas.findByLabelText('Name'), 'Inventory'); + await userEvent.click(canvas.getByRole('button', { name: 'Save view' })); + await expect(canvas.getByRole('status')).toHaveTextContent('Saving view...'); + await expect(canvas.getByRole('button', { name: 'Save view' })).toBeDisabled(); + await expect(canvas.getByLabelText('Filters (JSON)')).toBeDisabled(); + }, +}; +export const MobileRetryLoad = { ...RetryLoad }; +export const DarkDeleteRecovery = { + ...DeleteFailureAndRetry, + globals: { theme: 'dark' }, + decorators: [() => ({ template: '
' })], +}; + +export const InvalidFilters = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(await canvas.findByLabelText('Name'), 'Draft view'); + const filters = canvas.getByLabelText('Filters (JSON)'); + await userEvent.clear(filters); + await userEvent.type(filters, 'not JSON'); + await userEvent.click(canvas.getByRole('button', { name: 'Save view' })); + await expect(await canvas.findByRole('alert')).toHaveTextContent('Filters must be valid JSON.'); + await expect(filters).toHaveValue('not JSON'); + await expect(filters).toBeEnabled(); + await expect(canvas.getByLabelText('Name')).toHaveValue('Draft view'); + }, +}; diff --git a/apps/web/src/pages/AdminSavedViewsPage.vue b/apps/web/src/pages/AdminSavedViewsPage.vue index 2b3f480..1c0779d 100644 --- a/apps/web/src/pages/AdminSavedViewsPage.vue +++ b/apps/web/src/pages/AdminSavedViewsPage.vue @@ -1,5 +1,5 @@