From 829b93e431f0190310c0ed429a28f352cb466dfb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 01:53:30 +0000 Subject: [PATCH] refactor(components,plugin-list): the list load failure is drawn by DataErrorState, which now takes the icon props DataEmptyState had MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ListView` rendered its load FAILURE through `DataEmptyState` — the component named for the *empty* case — passing it a destructive icon, error copy and a retry control, while `DataErrorState` sat in the same file with the same layout, `role="alert"` already declared, and no consumer anywhere in the repo. It was not a drop-in replacement: it hardcoded its glyph, so the panel that must draw a network outage differently from a permission denial could only get an icon from the wrong component. `DataErrorState` gains three additive optional props — `icon`, `showIcon`, `iconWrapperClassName` — mirrored from `DataEmptyState` in the same file: same names, same types, same default semantics, including `iconWrapperClassName` REPLACING the wrapper's default class rather than merging with it. The only intended difference is the class the `??` falls back to, which stays this component's own destructive square. `illustration` and `action` are deliberately not mirrored. The migration moves no pixels. The call site passes the same glyph through the new `icon`, the same `iconWrapperClassName="mb-3"`, the same title, and the same copy through `message`; its retry ` )} - /> + ) : loading && data.length === 0 ? (
Promise) { + const ds = { + find: vi.fn().mockImplementation(find), + findOne: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }; + return render( + + + , + ); +} + +const failing = (error: unknown) => renderWith(() => Promise.reject(error)); +const forbidden = () => Object.assign(new Error('Forbidden'), { httpStatus: 403 }); + +async function panel(container: HTMLElement, testId: string): Promise { + await waitFor(() => { + expect(container.querySelector(`[data-testid="${testId}"]`)).not.toBeNull(); + }); + return container.querySelector(`[data-testid="${testId}"]`) as HTMLElement; +} + +describe('ListView — the load failure is a DataErrorState (#7143)', () => { + it('IDENTITY: the panel is data-slot="data-error-state"', async () => { + const { container } = failing(forbidden()); + const box = await panel(container, 'list-error-state'); + expect(box.getAttribute('data-slot')).toBe('data-error-state'); + // Named in both directions: the component it used to borrow must be gone + // from this node, not merely joined by a second slot value. + expect(box.getAttribute('data-slot')).not.toBe('data-empty-state'); + }); + + it('UNCHANGED: role, test id and error kind survive the swap', async () => { + const { container } = failing(forbidden()); + const box = await panel(container, 'list-error-state'); + expect(box.getAttribute('role')).toBe('alert'); + expect(box.getAttribute('data-error-kind')).toBe('forbidden'); + expect(box.textContent).toMatch(/permission|access/i); + }); + + it('UNCHANGED: the per-kind glyph still reaches the panel, in a stripped wrapper', async () => { + // The reason `DataErrorState` needed props at all: this panel draws an + // outage differently from a denial, and it removes the primitive's box. + const { container } = failing(new TypeError('Failed to fetch')); + const box = await panel(container, 'list-error-state'); + const icon = box.querySelector('[data-slot="data-error-state-icon"]'); + expect(icon).not.toBeNull(); + expect(icon!.querySelector('svg')).not.toBeNull(); + // `iconWrapperClassName` REPLACES rather than merges, so the destructive + // square the primitive draws by default must not have survived. + expect(icon!.className).toBe('mb-3'); + }); + + it('UNCHANGED: the retry button is still rendered inside the panel', async () => { + const { container } = failing(forbidden()); + const box = await panel(container, 'list-error-state'); + const retry = box.querySelector('[data-testid="list-error-retry"]'); + expect(retry).not.toBeNull(); + expect(retry!.textContent).toMatch(/retry/i); + }); + + it('UNCHANGED: an enable-block denial still offers no retry', async () => { + const err = Object.assign(new Error('Object API is disabled'), { + httpStatus: 404, + code: 'OBJECT_API_DISABLED', + }); + const { container } = failing(err); + const box = await panel(container, 'list-error-state'); + expect(box.querySelector('[data-testid="list-error-retry"]')).toBeNull(); + // The panel itself is still there — "no retry" must not be reached by + // rendering nothing at all. + expect(box.getAttribute('data-slot')).toBe('data-error-state'); + }); + + it('CONTROL: the genuine empty branch is still a DataEmptyState with role="status"', async () => { + const { container } = renderWith(() => Promise.resolve([])); + const box = await panel(container, 'empty-state'); + expect(box.getAttribute('data-slot')).toBe('data-empty-state'); + expect(box.getAttribute('role')).toBe('status'); + }); +});