refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// @vitest-environment jsdom
|
||||
import { Context, Service } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { apply, inject, NS } from '../src/client/index.ts'
|
||||
import { PluginInventorySettingsTab } from '../src/client/PluginInventorySettingsTab.tsx'
|
||||
import type { PluginInventorySettingsTabInjected } from '../src/client/PluginInventorySettingsTab.tsx'
|
||||
|
||||
usePinnedBrowserLanguages('zh-CN')
|
||||
afterEach(cleanup)
|
||||
|
||||
const EMPTY = { entries: [] }
|
||||
type ListResult =
|
||||
| { readonly ok: true; readonly value: typeof EMPTY }
|
||||
| { readonly ok: false; readonly error: { readonly code: string; readonly message: string } }
|
||||
|
||||
async function bench() {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SlotRegistry).await()
|
||||
const locale = new LocaleRuntime(ctx)
|
||||
ctx.provide('locale', locale)
|
||||
class RemoteService extends Service {
|
||||
constructor(serviceCtx: Context) {
|
||||
super(serviceCtx, 'remote')
|
||||
}
|
||||
}
|
||||
new RemoteService(ctx)
|
||||
const list = vi.fn<() => Promise<ListResult>>()
|
||||
.mockResolvedValue({ ok: true, value: EMPTY })
|
||||
ctx.provide('remote.pluginInventory', { list })
|
||||
return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, list }
|
||||
}
|
||||
|
||||
function declare(slots: SlotRegistry): () => void {
|
||||
return slots.register({
|
||||
name: 'root',
|
||||
children: { 'settings.plugins.tab': { kind: 'list', scope: 'root' } },
|
||||
} as never, () => null)
|
||||
}
|
||||
|
||||
describe('ui-settings-plugin-inventory browser plugin', () => {
|
||||
it('declares only the services used by the Settings Remote contribution', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.pluginInventory'])
|
||||
})
|
||||
|
||||
it('registers a localized tab without reading the Remote eagerly', async () => {
|
||||
const b = await bench()
|
||||
declare(b.slots)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
|
||||
const entry = b.slots.entries('settings.plugins.tab')[0]!
|
||||
expect(entry.component).toBe(PluginInventorySettingsTab)
|
||||
expect(entry.options).toMatchObject({ id: 'all', order: 10 })
|
||||
expect(entry.locale).toBe(NS)
|
||||
expect(resolveSlotLabel(entry.options.label)).toBe('插件列表')
|
||||
expect(b.list).not.toHaveBeenCalled()
|
||||
|
||||
const injected = (entry.inject as unknown as () => PluginInventorySettingsTabInjected)()
|
||||
await expect(injected.list()).resolves.toEqual(EMPTY)
|
||||
expect(b.list).toHaveBeenCalledOnce()
|
||||
b.list.mockResolvedValueOnce({ ok: false, error: { code: 'REMOTE_ERROR', message: 'unavailable' } })
|
||||
await expect(injected.list()).rejects.toThrow('pluginInventory.list failed: REMOTE_ERROR: unavailable')
|
||||
await b.ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('follows locale and recovers across late declaration and declarer reload', async () => {
|
||||
const b = await bench()
|
||||
const fiber = b.ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0)
|
||||
|
||||
const stop = declare(b.slots)
|
||||
await vi.waitFor(() => { expect(b.slots.entries('settings.plugins.tab')).toHaveLength(1) })
|
||||
b.locale.setLocale('en')
|
||||
expect(resolveSlotLabel(b.slots.entries('settings.plugins.tab')[0]!.options.label)).toBe('Plugin list')
|
||||
|
||||
stop()
|
||||
expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0)
|
||||
declare(b.slots)
|
||||
await vi.waitFor(() => {
|
||||
expect(b.slots.entries('settings.plugins.tab')[0]?.component).toBe(PluginInventorySettingsTab)
|
||||
})
|
||||
|
||||
await fiber.dispose()
|
||||
expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0)
|
||||
expect(() => b.locale.register(NS, 'zh', {})).not.toThrow()
|
||||
await b.ctx.fiber.dispose()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,127 @@
|
||||
// @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<ReturnType<PluginInventorySettingsTabInjected['list']>>
|
||||
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<Snapshot>()
|
||||
const list = vi.fn(() => deferred.promise)
|
||||
const view = render(<PluginInventorySettingsTab {...props(list)} />)
|
||||
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(<PluginInventorySettingsTab {...props(async () => 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<PluginInventorySettingsTabInjected['list']>()
|
||||
.mockRejectedValueOnce(new Error('private transport detail'))
|
||||
.mockResolvedValueOnce({ entries: [] })
|
||||
render(<PluginInventorySettingsTab {...props(list)} />)
|
||||
|
||||
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(<PluginInventorySettingsTab {...props(syncFailure)} />)
|
||||
expect((await screen.findByRole('alert')).textContent).toBe(en.error)
|
||||
failed.unmount()
|
||||
|
||||
const deferred = Promise.withResolvers<Snapshot>()
|
||||
const pending = render(<PluginInventorySettingsTab {...props(() => deferred.promise)} />)
|
||||
pending.unmount()
|
||||
await act(async () => { deferred.resolve(SNAPSHOT) })
|
||||
|
||||
const deferredFailure = Promise.withResolvers<Snapshot>()
|
||||
const pendingFailure = render(<PluginInventorySettingsTab {...props(() => deferredFailure.promise)} />)
|
||||
pendingFailure.unmount()
|
||||
await act(async () => { deferredFailure.reject(new Error('late failure')) })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
|
||||
import * as PluginsInvariant from '../src/invariant.ts'
|
||||
|
||||
describe('ui-settings-plugin-inventory invariant companion', () => {
|
||||
it('registers the empty installer and keeps the node half inert', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(InvariantRegistry, { enabled: true })
|
||||
await expect(ctx.plugin(PluginsInvariant).await()).resolves.toBeDefined()
|
||||
const { apply } = await import('../src/index.ts')
|
||||
apply()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user