Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions extensions/dsh-browser/tests/content-selection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { MAX_SELECTION_CHARS } from '../src/selection.ts'
import { MAX_SELECTION_CHARS, type SelectionCapture } from '../src/selection.ts'
import { SelectionWatcher, readSelectionCapture } from '../src/content/selection.ts'

function selectText(value: string, anchor: Node | null = document.body): void {
Expand All @@ -23,7 +23,21 @@ function setUserGesture(active: boolean): void {
})
}

/** Watchers kept alive across an assertion so afterEach can disarm debounce timers. */
const liveWatchers = new Set<SelectionWatcher>()

function watch(emit: (capture: SelectionCapture) => void, settleMs = 10): SelectionWatcher {
const watcher = new SelectionWatcher(emit, settleMs)
liveWatchers.add(watcher)
return watcher
}

afterEach(() => {
// Disarm before restoring real timers: pending debounce callbacks otherwise
// fire after jsdom is gone and throw `document is not defined`.
for (const watcher of liveWatchers) watcher.dispose()
liveWatchers.clear()
vi.clearAllTimers()
document.body.innerHTML = ''
document.title = ''
Reflect.deleteProperty(navigator, 'userActivation')
Expand Down Expand Up @@ -99,7 +113,7 @@ describe('selection watcher', () => {
it('ignores a selection the page moved without a user gesture', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)
watcher.setEnabled(true)

// An iframe calling getSelection().selectAllChildren() looks like this.
Expand All @@ -119,7 +133,7 @@ describe('selection watcher', () => {
it('reports the same passage again after the panel dropped it', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)
watcher.setEnabled(true)

selectText('quoted text')
Expand All @@ -137,7 +151,7 @@ describe('selection watcher', () => {
it('ignores an arm command older than the one already applied', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)

expect(watcher.setEnabled(true, 5, 'worker-a')).toBe(true)
// A slow DSH_CONTENT_READY reply computed before the panel opened.
Expand All @@ -153,7 +167,7 @@ describe('selection watcher', () => {
it('accepts a fresh worker even when its revision counter restarted', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)

expect(watcher.setEnabled(true, 5, 'worker-a')).toBe(true)
expect(watcher.setEnabled(false, 0, 'worker-b')).toBe(true)
Expand All @@ -167,7 +181,7 @@ describe('selection watcher', () => {
it('stays silent until a panel arms it', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)

selectText('quoted text')
document.dispatchEvent(new Event('selectionchange'))
Expand All @@ -184,7 +198,7 @@ describe('selection watcher', () => {
it('reports a highlight made before the panel opened', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)

selectText('highlighted before opening')
watcher.setEnabled(true)
Expand All @@ -197,7 +211,7 @@ describe('selection watcher', () => {
it('emits once for a drag that fires many selection changes', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)
watcher.setEnabled(true)

for (const partial of ['q', 'qu', 'quoted text']) {
Expand All @@ -214,7 +228,7 @@ describe('selection watcher', () => {
it('keeps the captured quote when the user clears the highlight', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)
watcher.setEnabled(true)

selectText('quoted text')
Expand All @@ -230,7 +244,7 @@ describe('selection watcher', () => {
it('reports the same passage after the highlight was cleared', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)
watcher.setEnabled(true)

selectText('quoted text')
Expand All @@ -249,7 +263,7 @@ describe('selection watcher', () => {
it('reports when the same captured prefix becomes truncated', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)
watcher.setEnabled(true)

selectText('x'.repeat(MAX_SELECTION_CHARS))
Expand All @@ -266,7 +280,7 @@ describe('selection watcher', () => {
it('does not resend an unchanged highlight', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)
watcher.setEnabled(true)

selectText('quoted text')
Expand All @@ -281,7 +295,7 @@ describe('selection watcher', () => {
it('releases the page listener when it is disarmed', () => {
vi.useFakeTimers()
const emit = vi.fn()
const watcher = new SelectionWatcher(emit, 10)
const watcher = watch(emit, 10)
watcher.setEnabled(true)
watcher.dispose()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,14 @@ describe('panel session transitions', () => {
open: async () => { throw new Error('unexpected stream') },
failure: (error) => ({ code: 'internal', message: String(error), details: {} }),
},
}, { createSharedFetchHandler: () => ({ fetch: async () => { throw new Error('unexpected fetch') } }) })
}, {
createSharedFetchHandler: () => ({ fetch: async () => { throw new Error('unexpected fetch') } }),
createPeer: () => ({
signal: new AbortController().signal,
principal: { kind: 'owner' as const },
interactions: 'answer',
}),
})
const original = rpc.getMockImplementation()!
rpc.mockImplementation(async (method, payload) => {
if (method !== 'session.models' && method !== 'session.selectModel') return original(method, payload)
Expand Down
Loading
Loading