test(browser): complete sidebar retention and navigation coverage
This commit is contained in:
1 parent
599f556deb
commit
8e441f7da2
10 files changed
+255
-18
No files matched your search
@@ -46,23 +46,24 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
||||
const host = useRef<HTMLElement | null>(null)
|
||||
const body = useRef<HTMLDivElement | null>(null)
|
||||
useLayoutEffect(() => {
|
||||
if (host.current !== null) host.current.inert = !visible
|
||||
// Both refs target unconditional descendants attached before these effects.
|
||||
const section = host.current as HTMLElement
|
||||
section.inert = !visible
|
||||
const focused = document.activeElement
|
||||
if (!visible && focused instanceof HTMLElement && host.current?.contains(focused)) focused.blur()
|
||||
if (!visible && focused instanceof HTMLElement && section.contains(focused)) focused.blur()
|
||||
const request = focusRequest.current
|
||||
if (!visible || request?.tabId !== tab.id) return
|
||||
focusRequest.current = undefined
|
||||
// Preserve deliberate focus taken by another control or the newly shown body.
|
||||
if (focused !== null && focused !== document.body && focused !== document.documentElement && focused !== request.origin) return
|
||||
const strip = host.current?.querySelector('[data-dockkit-strip]')
|
||||
const strip = section.querySelector('[data-dockkit-strip]')
|
||||
const chip = [...strip?.querySelectorAll<HTMLElement>('[data-dockkit-tab]') ?? []]
|
||||
.find(element => element.dataset.dockkitTab === tab.id)
|
||||
chip?.focus({ preventScroll: true })
|
||||
}, [visible, tab.id, focusRequest])
|
||||
// Electron emits a non-bubbling focus event on the webview element.
|
||||
useEffect(() => {
|
||||
const element = body.current
|
||||
if (element === null) return
|
||||
const element = body.current as HTMLDivElement
|
||||
const focus = (): void => {
|
||||
if (!visible) return
|
||||
if (floating) floats.raise(pane.id)
|
||||
|
||||
@@ -84,9 +84,17 @@ it('keeps body ancestors through pane moves, floating, raising and docking', ()
|
||||
fireEvent.pointerDown(h.node('[data-dockkit-divider]'), { button: 0, pointerId: 1 })
|
||||
expect(h.callbacks.onDividerPressed).toHaveBeenCalledOnce()
|
||||
h.redraw({ preview: undefined })
|
||||
const secondPane = getPane(state, split.children[1]!).id
|
||||
fireEvent.click(h.node(`[data-dockkit-content="${tab}"] [data-dockkit-tab="${tab}"]`))
|
||||
expect(controller.getSnapshot().state.activePaneId).toBe(split.children[0])
|
||||
act(() => { controller.focusPane(secondPane); h.redraw() })
|
||||
fireEvent.focus(body)
|
||||
expect(controller.getSnapshot().state.activePaneId).toBe(split.children[0])
|
||||
act(() => { controller.focusPane(secondPane); h.redraw() })
|
||||
fireEvent.pointerDown(h.node(`[data-dockkit-content="${tab}"]`), { button: 0, pointerId: 1 })
|
||||
fireEvent.click(body)
|
||||
fireEvent.focus(body)
|
||||
expect(controller.getSnapshot().state.activePaneId).toBe(getPane(state, split.children[0]!).id)
|
||||
const secondPane = getPane(state, split.children[1]!).id
|
||||
act(() => { controller.placeTab(tab, secondPane, 0); h.redraw() })
|
||||
expect(h.body(tab)).toBe(body)
|
||||
expect(body.closest('[data-dockkit-host]')).toBe(host)
|
||||
@@ -97,6 +105,7 @@ it('keeps body ancestors through pane moves, floating, raising and docking', ()
|
||||
act(() => { controller.setExpanded(false); h.redraw() })
|
||||
expect(body.closest('[data-dockkit-content]')?.getAttribute('aria-hidden')).toBeNull()
|
||||
fireEvent.pointerDown(h.node(`[data-dockkit-float="${floating}"]`), { pointerId: 1, button: 0 })
|
||||
fireEvent.click(h.node(`[data-dockkit-float="${floating}"]`))
|
||||
fireEvent.focus(body)
|
||||
const resize = h.node('[data-dockkit-float-resize]')
|
||||
fireEvent.pointerDown(resize, { pointerId: 2, button: 0, clientX: 500, clientY: 400 })
|
||||
@@ -125,6 +134,47 @@ it('unmounts unretained bodies and restores focus to their replacement strip', (
|
||||
expect(focus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.for(['new-control', 'origin-control', 'document', 'no-focus'] as const)('transfers tab focus without overriding a new owner: %s', (kind, test) => {
|
||||
const controller = seededController()
|
||||
controller.setExpanded(true)
|
||||
const first = getPane(controller.getSnapshot().state, controller.getSnapshot().state.rootId).tabs[0]!
|
||||
const second = controller.openContent({ kind: 'file', contentId: 'file:b', title: 'B' })
|
||||
controller.focusTab(first)
|
||||
const h = mounted(controller)
|
||||
const control = document.createElement('button')
|
||||
document.body.append(control)
|
||||
test.onTestFinished(() => { control.remove() })
|
||||
if (kind === 'origin-control') control.focus()
|
||||
vi.spyOn(h.callbacks, 'onFocusTab').mockImplementation((id) => {
|
||||
if (kind === 'new-control') control.focus()
|
||||
if (kind === 'document' || kind === 'no-focus') {
|
||||
vi.spyOn(document, 'activeElement', 'get').mockReturnValue(kind === 'document' ? document.documentElement : null)
|
||||
}
|
||||
controller.focusTab(id)
|
||||
h.redraw()
|
||||
})
|
||||
const focus = vi.spyOn(HTMLElement.prototype, 'focus')
|
||||
fireEvent.click(h.node(`[data-dockkit-tab="${second}"]`))
|
||||
if (kind === 'new-control') expect(document.activeElement).toBe(control)
|
||||
else expect(focus).toHaveBeenCalledWith({ preventScroll: true })
|
||||
})
|
||||
|
||||
it('does not require a docked strip when a selected tab becomes floating in the same update', () => {
|
||||
const controller = seededController()
|
||||
controller.setExpanded(true)
|
||||
const first = getPane(controller.getSnapshot().state, controller.getSnapshot().state.rootId).tabs[0]!
|
||||
const second = controller.openContent({ kind: 'file', contentId: 'file:b', title: 'B' })
|
||||
controller.focusTab(first)
|
||||
const h = mounted(controller)
|
||||
vi.spyOn(h.callbacks, 'onFocusTab').mockImplementation((id) => {
|
||||
controller.focusTab(id)
|
||||
controller.floatTab(id)
|
||||
h.redraw()
|
||||
})
|
||||
fireEvent.click(h.node(`[data-dockkit-tab="${second}"]`))
|
||||
expect(h.node(`[data-dockkit-content="${second}"]`).hasAttribute('data-dockkit-float')).toBe(true)
|
||||
})
|
||||
|
||||
it('renders an empty pane and rejects unsupported docked trees', () => {
|
||||
const controller = new DockController()
|
||||
controller.setExpanded(true)
|
||||
|
||||
@@ -233,7 +233,6 @@ export function createBrowserControllers(actions: BoundActions<BrowserStore>, cr
|
||||
}
|
||||
const created = new BrowserController({ ...request, actions: currentActions, createPage })
|
||||
const forget = (): void => {
|
||||
if (controllers.get(tabId)?.controller !== created) return
|
||||
controllers.delete(tabId)
|
||||
// Plugin unload also aborts occurrences; only layout removal deletes saved navigation.
|
||||
if (!isTabOpen(tabId)) currentActions.forget(tabId)
|
||||
|
||||
@@ -78,9 +78,7 @@ export class IframeImpl implements BrowserFrame {
|
||||
this.store.set({ ...this.store.getSnapshot(), sandboxEnabled: enabled })
|
||||
return
|
||||
}
|
||||
const request = this.navigation.reload()
|
||||
if (request === undefined) this.publish()
|
||||
else this.load(request)
|
||||
this.load(this.navigation.reload())
|
||||
}
|
||||
|
||||
private load(request: ReturnType<BrowserNavigation['navigate']> | undefined): void {
|
||||
|
||||
@@ -94,7 +94,7 @@ export function BrowserBody(props: BrowserBodyProps): ReactNode {
|
||||
<button type="submit" className={[css.tool, css.addressGo].join(' ')} aria-label={t('go')} title={t('go')}><IconLinkOutlineRegular /></button>
|
||||
</div>
|
||||
<button type="button" className={css.tool} aria-label={t('external')} title={t('external')} disabled={externalUrl === undefined}
|
||||
onClick={() => { if (externalUrl !== undefined) window.open(externalUrl, '_blank', 'noopener,noreferrer') }}
|
||||
onClick={externalUrl === undefined ? undefined : () => { window.open(externalUrl, '_blank', 'noopener,noreferrer') }}
|
||||
><IconRightUpOutlineRegular size={14} /></button>
|
||||
{sandboxed !== undefined && <button
|
||||
type="button"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @vitest-environment jsdom
|
||||
/** Browser type, Slot, locale, and HMR disposal through the real registries. */
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
@@ -9,11 +10,15 @@ import type { BrowserInjected } from '../src/client/browser/BrowserController.ts
|
||||
import { BROWSER_ID, BROWSER_KIND } from '../src/client/definition.tsx'
|
||||
import { apply, inject } from '../src/client/index.ts'
|
||||
import { en, zh } from '../src/client/locales.ts'
|
||||
import { createBrowserStore } from '../src/client/browser/store.ts'
|
||||
import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit'
|
||||
import type { DesktopBrowserBridge, DesktopBrowserLeaseId } from '../src/types.ts'
|
||||
|
||||
const contexts: Context[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
interface Recorded {
|
||||
@@ -47,15 +52,65 @@ async function boot() {
|
||||
}),
|
||||
}
|
||||
ctx.provide('sidebarRightTabs', tabs as never)
|
||||
ctx.provide('sidebarRight', { openTabs: createSnapshotStore([]) } as never)
|
||||
const openTabs = createSnapshotStore<readonly { sessionId: string; tabId: TabId }[]>([])
|
||||
ctx.provide('sidebarRight', { openTabs } as never)
|
||||
ctx.provide('workspaces', { list: createSnapshotStore({ phase: 'ready', items: [] }) } as never)
|
||||
ctx.provide('slots', slots as never)
|
||||
ctx.provide('locale', locale as never)
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
return { tabs, registered, dictionaries, fiber }
|
||||
return { tabs, registered, dictionaries, fiber, openTabs }
|
||||
}
|
||||
|
||||
describe('ui-sidebar-browser apply', () => {
|
||||
it.each([0, 1])('binds and rebinds session controllers under desktop protocol %s', async (protocolVersion) => {
|
||||
const acquire = vi.fn(async () => ({ lease: 'test-lease' as DesktopBrowserLeaseId, partition: 'test-partition' }))
|
||||
const bridge: DesktopBrowserBridge = {
|
||||
acquire,
|
||||
release: vi.fn(async () => {}),
|
||||
onOpenRequested: vi.fn(() => () => {}),
|
||||
}
|
||||
vi.stubGlobal('dshDesktop', { protocolVersion, browser: bridge })
|
||||
const h = await boot()
|
||||
expect(h.tabs.get(BROWSER_KIND)?.keepMounted).toBe(protocolVersion === 1)
|
||||
const injectFace = h.registered.find(entry => entry.name === 'sidebar.right.pane.tab')!.inject as
|
||||
(sessionId: string, actions: Parameters<BrowserInjected['rebind']>[0]) => BrowserInjected
|
||||
const firstStore = createBrowserStore().create(`apply-first-${protocolVersion}`)
|
||||
const replacementStore = createBrowserStore().create(`apply-replacement-${protocolVersion}`)
|
||||
const controller = injectFace('session', firstStore.actions)
|
||||
expect(injectFace('session', replacementStore.actions)).toBe(controller)
|
||||
const host = document.createElement('div')
|
||||
host.id = `browser-apply-${protocolVersion}`
|
||||
document.body.append(host)
|
||||
const signal = new AbortController()
|
||||
const tabId = 'apply-tab' as TabId
|
||||
try {
|
||||
controller.mount({ tabId, signal: signal.signal, viewportId: host.id, applicationOrigin: 'https://dsh.example',
|
||||
initial: undefined, initialUrl: 'https://example.test/', openTab: vi.fn() })
|
||||
expect(replacementStore.getSnapshot().byTab[tabId]).toBeDefined()
|
||||
expect(firstStore.getSnapshot().byTab[tabId]).toBeUndefined()
|
||||
if (protocolVersion === 1) await vi.waitFor(() => { expect(acquire).toHaveBeenCalledWith('session:session') })
|
||||
else expect(host.querySelector('iframe')).not.toBeNull()
|
||||
h.openTabs.set([{ sessionId: 'other', tabId }, { sessionId: 'session', tabId: 'other-tab' as TabId }])
|
||||
signal.abort()
|
||||
expect(replacementStore.getSnapshot().byTab[tabId]).toBeUndefined()
|
||||
const reopened = new AbortController()
|
||||
controller.mount({ tabId, signal: reopened.signal, viewportId: host.id, applicationOrigin: 'https://dsh.example',
|
||||
initial: undefined, initialUrl: 'https://retained.example/', openTab: vi.fn() })
|
||||
h.openTabs.set([{ sessionId: 'session', tabId }])
|
||||
reopened.abort()
|
||||
expect(replacementStore.getSnapshot().byTab[tabId]).toBeDefined()
|
||||
const active = new AbortController()
|
||||
controller.mount({ tabId, signal: active.signal, viewportId: host.id, applicationOrigin: 'https://dsh.example',
|
||||
initial: undefined, initialUrl: undefined, openTab: vi.fn() })
|
||||
await h.fiber.dispose()
|
||||
expect(controller.keyedHooks.browserState(tabId)).toBeUndefined()
|
||||
} finally {
|
||||
await h.fiber.dispose()
|
||||
host.remove()
|
||||
}
|
||||
})
|
||||
|
||||
it('registers a multi-instance builtin and its body and title', async () => {
|
||||
const { tabs, registered, dictionaries } = await boot()
|
||||
const definition = tabs.get(BROWSER_KIND)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { PaneId, TabId } from '@deepseek-ai/dsh-client-ui-dockkit'
|
||||
@@ -11,6 +11,10 @@ import { BrowserBody } from '../src/client/view/BrowserBody.tsx'
|
||||
import { WEB_BROWSER_SANDBOX } from '../src/client/view/IframePresentation.ts'
|
||||
import { createIframePage } from '../src/client/pages.ts'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
import { browserAddressCheckpoint, type BrowserTabState } from '../src/client/browser/BrowserPersistence.ts'
|
||||
import type { BrowserPageFactory, BrowserPageOptions } from '../src/client/browser/BrowserPage.ts'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { emptyBrowserFrame, type BrowserFrameState } from '../src/client/browser/BrowserFrame.ts'
|
||||
|
||||
const SESSION = 'session' as SessionId
|
||||
const TAB = 'tab' as TabId
|
||||
@@ -33,11 +37,13 @@ const absentState = {
|
||||
getSnapshot: (): BrowserControllerState | undefined => undefined,
|
||||
}
|
||||
|
||||
function mountBrowser(navigation?: { readonly url?: string }) {
|
||||
function mountBrowser(navigation?: { readonly url?: string },
|
||||
options: { initial?: BrowserTabState; createPage?: BrowserPageFactory } = {}) {
|
||||
const store = createBrowserStore().create(`browser-body-test-${String(++mountSequence)}`)
|
||||
if (options.initial !== undefined) store.actions.replace(TAB, options.initial)
|
||||
const lifetime = new AbortController()
|
||||
lifetimes.add(lifetime)
|
||||
const injected = createBrowserControllers(store.actions, createIframePage, () => true)
|
||||
const injected = createBrowserControllers(store.actions, options.createPage ?? createIframePage, () => true)
|
||||
controllers.push(injected)
|
||||
const { keyedHooks, ...commands } = injected
|
||||
const tabActions = { openResource: vi.fn(), openTab: vi.fn(), close: vi.fn() }
|
||||
@@ -67,7 +73,7 @@ function mountBrowser(navigation?: { readonly url?: string }) {
|
||||
}
|
||||
const renderBody = () => render(<BrowserBody {...props as BrowserBodyProps} />)
|
||||
return {
|
||||
view: renderBody(), remount: renderBody, store, lifetime, injected,
|
||||
view: renderBody(), remount: renderBody, store, lifetime, injected, tabActions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +87,39 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('BrowserBody', () => {
|
||||
it('shows the saved title and URL without loading until Restore is clicked', async () => {
|
||||
const target = { kind: 'https' as const, url: 'https://saved.example/page', title: 'Saved title' }
|
||||
const mounted = mountBrowser(undefined, { initial: browserAddressCheckpoint(target, 1) })
|
||||
expect(mounted.view.getByText(target.title)).toBeDefined()
|
||||
expect(mounted.view.getByText(target.url)).toBeDefined()
|
||||
expect(mounted.view.container.querySelector('iframe')).toBeNull()
|
||||
expect(mounted.view.getByRole('button', { name: zh.reload })).toHaveProperty('disabled', false)
|
||||
fireEvent.click(mounted.view.getByRole('button', { name: zh['restore.action'] }))
|
||||
await waitFor(() => { expect(mounted.view.container.querySelector('iframe')?.src).toBe(target.url) })
|
||||
expect(mounted.view.queryByText(target.title)).toBeNull()
|
||||
})
|
||||
|
||||
it('renders native error details and routes provider open requests through the source tab', () => {
|
||||
const state = createSnapshotStore<BrowserFrameState>({ ...emptyBrowserFrame(), error: { code: -105, description: 'DNS failure' } })
|
||||
const providers: BrowserPageOptions[] = []
|
||||
const mounted = mountBrowser(undefined, { createPage: (options) => {
|
||||
providers.push(options)
|
||||
return {
|
||||
presentation: { mount: () => () => {} },
|
||||
frame: { getSnapshot: () => state.getSnapshot(), subscribe: listener => state.subscribe(listener),
|
||||
loadUrl: vi.fn(), goBack: vi.fn(), goForward: vi.fn(), reload: vi.fn(), dispose: async () => {} },
|
||||
}
|
||||
} })
|
||||
expect(mounted.view.getByRole('status').textContent).toContain('DNS failure')
|
||||
expect(mounted.view.getByRole('status').textContent).toContain('-105')
|
||||
act(() => { state.set({ ...state.getSnapshot(), error: { code: -105, description: undefined } }) })
|
||||
expect(mounted.view.getByRole('status').textContent).toBe(zh['load.failed'])
|
||||
act(() => { providers[0]!.openRequested('https://new.example/') })
|
||||
expect(mounted.tabActions.openTab).toHaveBeenCalledExactlyOnceWith('browser', {
|
||||
params: { url: 'https://new.example/' }, revealIfOpened: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('routes address input to the controller and renders parser failures', async () => {
|
||||
const mounted = mountBrowser()
|
||||
const input = mounted.view.getByRole('textbox')
|
||||
|
||||
@@ -4,6 +4,10 @@ import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit'
|
||||
import { BrowserController, createBrowserControllers } from '../src/client/browser/BrowserController.ts'
|
||||
import { createBrowserStore } from '../src/client/browser/store.ts'
|
||||
import { createIframePage } from '../src/client/pages.ts'
|
||||
import type { BrowserPageFactory, BrowserPageOptions } from '../src/client/browser/BrowserPage.ts'
|
||||
import { browserAddressCheckpoint } from '../src/client/browser/BrowserPersistence.ts'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { emptyBrowserFrame, type BrowserFrameState } from '../src/client/browser/BrowserFrame.ts'
|
||||
|
||||
const TAB = 'tab' as TabId
|
||||
const APP = 'https://dsh.example'
|
||||
@@ -50,6 +54,91 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('BrowserController', () => {
|
||||
it('restores only on request and redirects saved checkpoints to a replacement binding', () => {
|
||||
const h = harness()
|
||||
const saved = browserAddressCheckpoint({ kind: 'https', url: 'https://saved.example/', title: 'Saved page' }, 2)
|
||||
h.store.actions.replace(TAB, saved)
|
||||
const tabLifetime = lifetime()
|
||||
h.mount(tabLifetime.signal)
|
||||
const state = h.face.keyedHooks.browserState(TAB)!
|
||||
expect(state.getSnapshot().restoreTarget?.title).toBe('Saved page')
|
||||
expect(state.getSnapshot().frame.target).toBeUndefined()
|
||||
const rebound = createBrowserStore().create('rebound-browser-controller')
|
||||
h.face.rebind(rebound.actions)
|
||||
h.mount(tabLifetime.signal)
|
||||
expect(rebound.getSnapshot().byTab[TAB]).toEqual(saved)
|
||||
h.face.restore(TAB)
|
||||
expect(h.iframe().src).toBe('https://saved.example/')
|
||||
h.face.restore(TAB)
|
||||
h.face.loadUrl(TAB, 'https://next.example/')
|
||||
expect(rebound.getSnapshot().byTab[TAB]?.entries.at(-1)?.url).toBe('https://next.example/')
|
||||
expect(h.store.getSnapshot().byTab[TAB]).toEqual(saved)
|
||||
tabLifetime.abort()
|
||||
expect(rebound.getSnapshot().byTab[TAB]).toBeDefined()
|
||||
h.face.restore(TAB)
|
||||
h.face.setSandbox(TAB, false)
|
||||
h.mount(tabLifetime.signal)()
|
||||
expect(h.face.keyedHooks.browserState(TAB)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('refresh restores a saved page and ignores cancellation from a retired physical mount', () => {
|
||||
const h = harness()
|
||||
h.store.actions.replace(TAB, browserAddressCheckpoint({ kind: 'https', url: 'https://saved.example/', title: 'Saved' }, 1))
|
||||
const tabLifetime = lifetime()
|
||||
const oldMount = h.mount(tabLifetime.signal)
|
||||
h.face.reload(TAB)
|
||||
const secondHost = document.createElement('div')
|
||||
secondHost.id = 'replacement-browser-host'
|
||||
document.body.append(secondHost)
|
||||
hosts.push(secondHost)
|
||||
h.face.mount({ tabId: TAB, signal: tabLifetime.signal, viewportId: secondHost.id,
|
||||
applicationOrigin: APP, initial: undefined, initialUrl: undefined, openTab: vi.fn() })
|
||||
oldMount()
|
||||
expect(secondHost.querySelector('iframe')?.src).toBe('https://saved.example/')
|
||||
expect(() => h.face.mount({ tabId: TAB, signal: tabLifetime.signal, viewportId: 'missing-browser-host',
|
||||
applicationOrigin: APP, initial: undefined, initialUrl: undefined, openTab: vi.fn() })).toThrow('not mounted')
|
||||
})
|
||||
|
||||
it('validates provider open requests and ignores late provider callbacks after disposal', async () => {
|
||||
const saved = browserAddressCheckpoint({ kind: 'https', url: 'https://saved.example/', title: 'Saved' }, 1)
|
||||
const frames = createSnapshotStore<BrowserFrameState>(emptyBrowserFrame())
|
||||
const callbacks: BrowserPageOptions[] = []
|
||||
const pendingNotifications: (() => void)[] = []
|
||||
const pageFactory: BrowserPageFactory = (options) => {
|
||||
callbacks.push(options)
|
||||
return {
|
||||
frame: { getSnapshot: () => frames.getSnapshot(), subscribe: (listener) => {
|
||||
pendingNotifications.push(listener)
|
||||
return frames.subscribe(listener)
|
||||
}, loadUrl: vi.fn(), goBack: vi.fn(), goForward: vi.fn(), reload: vi.fn(), dispose: async () => {} },
|
||||
presentation: { mount: () => () => {}, dispose: () => {} },
|
||||
}
|
||||
}
|
||||
const store = createBrowserStore().create('provider-callbacks')
|
||||
const openTab = vi.fn()
|
||||
const controller = new BrowserController({ tabId: TAB, signal: lifetime().signal, applicationOrigin: APP,
|
||||
actions: store.actions, initial: saved, createPage: pageFactory, openTab })
|
||||
disposables.push(controller)
|
||||
const provider = callbacks[0]!
|
||||
provider.openRequested('file:/secret')
|
||||
expect(controller.getSnapshot().addressFailure).toBe('protocol')
|
||||
provider.openRequested('https://new.example/path')
|
||||
expect(openTab).toHaveBeenCalledExactlyOnceWith('https://new.example/path')
|
||||
controller.setSandbox(false)
|
||||
frames.set({ ...emptyBrowserFrame(), loading: true })
|
||||
expect(controller.getSnapshot().restoreTarget?.title).toBe('Saved')
|
||||
expect(controller.getSnapshot().addressFailure).toBe('protocol')
|
||||
const before = controller.getSnapshot()
|
||||
await controller.dispose()
|
||||
controller.start('https://ignored.example/')
|
||||
provider.persist(saved)
|
||||
provider.openRequested('https://ignored.example/')
|
||||
for (const notify of pendingNotifications) notify()
|
||||
expect(controller.getSnapshot()).toBe(before)
|
||||
expect(store.getSnapshot().byTab[TAB]).toBeUndefined()
|
||||
expect(openTab).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('ignores navigation commands after its tab occurrence ends', async () => {
|
||||
const store = createBrowserStore().create('browser-controller-ended-test')
|
||||
const tabLifetime = lifetime()
|
||||
|
||||
@@ -17,6 +17,9 @@ describe('IframeImpl', () => {
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = frame.subscribe(listener)
|
||||
try {
|
||||
frame.handleLoaded(0)
|
||||
expect(frame.getSnapshot().address).toBe('empty')
|
||||
frame.sandbox.setEnabled(false)
|
||||
frame.sandbox.setEnabled(false)
|
||||
expect(frame.getSnapshot().sandboxEnabled).toBe(false)
|
||||
expect(frame.getSnapshot().target).toBeUndefined()
|
||||
@@ -43,6 +46,10 @@ describe('IframeImpl', () => {
|
||||
frame.handleLoadFailed(2)
|
||||
frame.handleLoaded(2)
|
||||
frame.reload()
|
||||
frame.loadUrl(target)
|
||||
frame.goBack()
|
||||
frame.goForward()
|
||||
frame.sandbox.setEnabled(true)
|
||||
expect(frame.getSnapshot()).toBe(stopped)
|
||||
} finally {
|
||||
unsubscribe()
|
||||
|
||||
@@ -73,7 +73,6 @@ export class SidebarSessionViews {
|
||||
}
|
||||
|
||||
private publish(): void {
|
||||
if (this.closed) return
|
||||
this.source.set([...this.views.values()].sort((a, b) => a.sessionId.localeCompare(b.sessionId)).map(view => ({
|
||||
sessionId: view.sessionId, reference: view.reference, selected: view.sessionId === this.selected, retainTab: view.retainTab,
|
||||
})))
|
||||
|
||||
Reference in new issue
Block a user