test(desktop): late-registered plugin route wins over the chat catch-all
Regression test from #111541: mounts ChatRoutesSurface at a not-yet-registered path through the real useContributions + registry under the compiler-enabled vitest ui project, registers the route after mount, and asserts the page replaces the `:sessionId` chat fallback. Red on origin/main, green with the snapshot-fed contributedRoutes(). Replaces the equivalent surface test from #109418 so the workspace surface carries one invariant test.
This commit is contained in:
+32
-47
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* A plugin page route registered AFTER the workspace surface mounts must
|
||||
* become navigable. Regression for late-loaded desktop plugins (disk plugins
|
||||
* load async): the route table was compiled into a memo slot keyed on
|
||||
* unrelated props, so a late `routes`-area registration never entered the
|
||||
* table — the sidebar row rendered but navigating to the path fell through
|
||||
* to the `:sessionId` chat route. Uses the REAL useContributions + registry
|
||||
* (unlike surfaces.test.tsx) because the reactive flow is the subject.
|
||||
*/
|
||||
import { act, cleanup, render, screen } from '@testing-library/react'
|
||||
import { atom } from 'nanostores'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
@@ -8,13 +17,6 @@ import { registry } from '@/contrib/registry'
|
||||
import { ChatRoutesSurface } from './surfaces'
|
||||
import type { WiringActions } from './types'
|
||||
|
||||
// #109063: the workspace surface subscribes to ROUTES_AREA but used to derive
|
||||
// routes via an independently-called `contributedRoutes()`, which React
|
||||
// Compiler can keep memoized across the delivered subscription update. The
|
||||
// registry, `useContributions` and the routes module all stay REAL here — only
|
||||
// unrelated stores and heavy sibling views are stubbed, because mocking the
|
||||
// route list is exactly what hid the defect.
|
||||
|
||||
vi.mock('@/store/connections', () => ({ $activeConnectionId: atom('local') }))
|
||||
vi.mock('@/store/gateway', () => ({ $gateway: atom<unknown>(null) }))
|
||||
vi.mock('@/store/profile', () => ({ $activeGatewayProfile: atom('default') }))
|
||||
@@ -30,61 +32,44 @@ vi.mock('../shell/hooks/use-statusbar-items', () => ({
|
||||
useStatusbarItems: () => ({ leftStatusbarItems: [], statusbarItems: [] })
|
||||
}))
|
||||
vi.mock('../shell/statusbar-controls', () => ({ StatusbarControls: () => null }))
|
||||
vi.mock('../shell/model-menu-panel', () => ({ ModelMenuPanel: () => null }))
|
||||
vi.mock('./latest-actions', () => ({ latestChatActions: () => ({}), latestSidebarActions: () => ({}) }))
|
||||
vi.mock('./panes', () => ({ setStatusbarItemGroup: vi.fn(), useStatusbarContributions: () => [] }))
|
||||
vi.mock('../shell/model-menu-panel', () => ({ ModelMenuPanel: () => null }))
|
||||
vi.mock('../shell/reasoning-menu-panel', () => ({ ReasoningMenuPanel: () => null }))
|
||||
|
||||
afterEach(cleanup)
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
/** Register (or hot-replace) the plugin route page mounted at `/resetwatch`. */
|
||||
function registerResetwatch(label: string) {
|
||||
return registry.register({
|
||||
id: 'resetwatch',
|
||||
area: 'routes',
|
||||
title: `Resetwatch ${label}`,
|
||||
data: { path: '/resetwatch' },
|
||||
render: () => <div>{`PLUGIN-PAGE-${label}`}</div>
|
||||
})
|
||||
}
|
||||
|
||||
describe('ChatRoutesSurface route-contribution reactivity (#109063)', () => {
|
||||
it('mounts a plugin page registered after the surface, tracks replacement, and unmounts on dispose', () => {
|
||||
const actions = { getGateway: () => null } as unknown as WiringActions
|
||||
describe('ChatRoutesSurface late-registered plugin routes', () => {
|
||||
it('renders a page whose route registers after mount', () => {
|
||||
const actions = {} as unknown as WiringActions
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/resetwatch']}>
|
||||
<MemoryRouter initialEntries={['/late-plugin']}>
|
||||
<ChatRoutesSurface actions={actions} />
|
||||
</MemoryRouter>
|
||||
)
|
||||
// Unregistered path falls through the catch-all redirect to the chat view.
|
||||
|
||||
// Before registration the path falls through to the chat catch-all.
|
||||
expect(screen.queryByTestId('late-page')).toBeNull()
|
||||
expect(screen.getByTestId('chat-view')).toBeTruthy()
|
||||
|
||||
let dispose = () => {}
|
||||
act(() => {
|
||||
// Late registration after the surface has mounted and settled.
|
||||
dispose = registerResetwatch('A')
|
||||
dispose = registry.register({
|
||||
area: 'routes',
|
||||
id: 'late-plugin:page',
|
||||
data: { path: '/late-plugin' },
|
||||
render: () => <div data-testid="late-page" />
|
||||
})
|
||||
})
|
||||
expect(screen.getByText('PLUGIN-PAGE-A')).toBeTruthy()
|
||||
|
||||
// The late registration must reach the route table and win over the
|
||||
// `:sessionId` dynamic route.
|
||||
expect(screen.getByTestId('late-page')).toBeTruthy()
|
||||
expect(screen.queryByTestId('chat-view')).toBeNull()
|
||||
|
||||
act(() => {
|
||||
dispose()
|
||||
dispose = registerResetwatch('B')
|
||||
})
|
||||
expect(screen.getByText('PLUGIN-PAGE-B')).toBeTruthy()
|
||||
|
||||
act(() => {
|
||||
dispose()
|
||||
})
|
||||
expect(screen.getByTestId('chat-view')).toBeTruthy()
|
||||
|
||||
act(() => {
|
||||
dispose = registerResetwatch('A')
|
||||
})
|
||||
expect(screen.getByText('PLUGIN-PAGE-A')).toBeTruthy()
|
||||
|
||||
act(() => {
|
||||
dispose()
|
||||
})
|
||||
act(() => dispose())
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user