// @vitest-environment jsdom import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { PluginInventorySettingsTab } from '../src/client/PluginInventorySettingsTab.tsx' import type { PluginInventorySettingsTabInjected, PluginInventorySettingsTabProps, } from '../src/client/PluginInventorySettingsTab.tsx' import { en, type PluginInventoryLocaleKey } from '../src/client/locales.ts' afterEach(cleanup) type Snapshot = Awaited> const t = ((key: PluginInventoryLocaleKey): string => en[key]) as PluginInventorySettingsTabProps['t'] function props(list: PluginInventorySettingsTabInjected['list']): PluginInventorySettingsTabProps { return { t, list, } as PluginInventorySettingsTabProps } const SNAPSHOT = { entries: [ { entryId: '8a1b2c3d', moduleName: '@deepseek-ai/cordis-plugin-hmr', enabled: true, fiberPhase: 'active' }, { entryId: 'pending', moduleName: 'cordis:pending-name', enabled: true, fiberPhase: 'pending' }, { entryId: 'loading', moduleName: '@fixture/loading-name', enabled: true, fiberPhase: 'loading' }, { entryId: 'failed', moduleName: '@fixture/failed-name', enabled: true, fiberPhase: 'failed' }, { entryId: 'unloading', moduleName: '@fixture/unloading-name', enabled: true, fiberPhase: 'unloading' }, { entryId: 'unobserved', moduleName: '@fixture/unobserved-name', enabled: true, fiberPhase: null }, { entryId: 'disabled-entry', moduleName: '@deepseek-ai/dsh-host-directory-picker-native', enabled: false, fiberPhase: null }, ], } as unknown as Snapshot describe('PluginInventorySettingsTab', () => { it('renders runtime status only for enabled plugins', async () => { const deferred = Promise.withResolvers() const list = vi.fn(() => deferred.promise) const view = render() expect(screen.getByText(en.loading)).toBeTruthy() await act(async () => { deferred.resolve(SNAPSHOT) }) expect(list).toHaveBeenCalledOnce() expect(screen.getByRole('searchbox', { name: en.search })).toBeTruthy() expect(screen.getByRole('heading', { name: en.catalog })).toBeTruthy() expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('7') expect(screen.getAllByRole('listitem')).toHaveLength(7) expect(screen.getAllByText(en.enabledTag)).toHaveLength(6) expect(screen.getByText(en.disabledTag)).toBeTruthy() for (const value of [ 'Mounted', 'Waiting for dependencies', 'Loading', 'Mount failed', 'Unloading', 'Not mounted', ]) { expect(screen.getByRole('img', { name: value })).toBeTruthy() } const active = screen.getByRole('button', { name: 'hmr, Mounted, Enabled' }) expect(active.getAttribute('aria-expanded')).toBe('false') fireEvent.click(active) expect(active.getAttribute('aria-expanded')).toBe('true') expect(view.container.querySelector('[data-loader-entry]')?.textContent).toBe('8a1b2c3d') expect(screen.getByText(en.configuration)).toBeTruthy() expect(screen.getByText(en.cordis)).toBeTruthy() fireEvent.click(active) expect(view.container.querySelector('[data-loader-entry]')).toBeNull() fireEvent.click(active) fireEvent.change(screen.getByRole('searchbox', { name: en.search }), { target: { value: 'disabled-entry' }, }) expect(view.container.querySelector('[data-loader-entry]')).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'directory-picker-native, Disabled' })) expect(screen.getAllByText(en.disabledTag)).toHaveLength(2) expect(screen.queryByText(en.cordis)).toBeNull() expect(screen.queryByText(en.unobserved)).toBeNull() }) it('filters by module name or Loader entry id', async () => { render( SNAPSHOT)} />) const search = await screen.findByRole('searchbox', { name: en.search }) fireEvent.change(search, { target: { value: 'disabled-entry' } }) expect(screen.getAllByRole('listitem')).toHaveLength(1) expect(screen.getByText('directory-picker-native')).toBeTruthy() fireEvent.change(search, { target: { value: 'cordis-plugin-hmr' } }) expect(screen.getAllByRole('listitem')).toHaveLength(1) expect(screen.getByText('hmr')).toBeTruthy() fireEvent.change(search, { target: { value: 'not-a-plugin' } }) expect(screen.queryAllByRole('listitem')).toHaveLength(0) expect(screen.getByText(en.emptySearch)).toBeTruthy() }) it('shows a generic failure and retries into the empty state', async () => { const list = vi.fn() .mockRejectedValueOnce(new Error('private transport detail')) .mockResolvedValueOnce({ entries: [] }) render() expect((await screen.findByRole('alert')).textContent).toBe(en.error) expect(screen.queryByText('private transport detail')).toBeNull() fireEvent.click(screen.getByRole('button', { name: en.retry })) await waitFor(() => { expect(list).toHaveBeenCalledTimes(2) }) expect(await screen.findByText(en.empty)).toBeTruthy() }) it('contains a synchronous Remote failure and ignores a result after unmount', async () => { const syncFailure = vi.fn(() => { throw new Error('namespace unavailable') }) as PluginInventorySettingsTabInjected['list'] const failed = render() expect((await screen.findByRole('alert')).textContent).toBe(en.error) failed.unmount() const deferred = Promise.withResolvers() const pending = render( deferred.promise)} />) pending.unmount() await act(async () => { deferred.resolve(SNAPSHOT) }) const deferredFailure = Promise.withResolvers() const pendingFailure = render( deferredFailure.promise)} />) pendingFailure.unmount() await act(async () => { deferredFailure.reject(new Error('late failure')) }) }) })