mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
test: remove low-value editor prop and helper tests (#38366)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -7023,7 +7023,6 @@
|
||||
"src/renderer/src/components/editor/rich-markdown/schema/drag-selection-guard-lifetime.test.ts": 626,
|
||||
"src/renderer/src/components/editor/autosave/editor-autosave-conflict-flow.test.ts": 1931,
|
||||
"src/renderer/src/components/editor/autosave/editor-autosave-controller.test.ts": 2324,
|
||||
"src/renderer/src/components/editor/autosave/editor-autosave.test.ts": 529,
|
||||
"src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.test.ts": 2812,
|
||||
"src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-sharing.test.ts": 5929,
|
||||
"src/renderer/src/components/editor/save/bytes/editor-bytes-documents.test.ts": 690,
|
||||
|
||||
+1
-95
@@ -9,7 +9,7 @@ import type { GitStatusEntry } from '../../../../shared/git/status/git-status-ty
|
||||
import { makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
|
||||
import { createEditorStore } from './autosave/editor-autosave-controller-test-fixture'
|
||||
import { createEditorPanelDraftSelector } from './editor-panel-draft-selector'
|
||||
import { editorTabModelUri } from './save/queue/editor-save-disk-precondition'
|
||||
|
||||
import { EditorConflictReviewSurface } from './EditorConflictReviewSurface'
|
||||
|
||||
type MonacoProps = {
|
||||
@@ -163,33 +163,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('conflict-review overview as a live read-only view of the record', () => {
|
||||
it("shows the record's unsaved text from another workspace's view, on that record's model", () => {
|
||||
const { store, view, review } = setup()
|
||||
store.getState().setEditorDraft(view.id, 'unsaved')
|
||||
store.getState().markFileDirty(view.id, true)
|
||||
|
||||
const overview = render(store, review, 'disk')
|
||||
expect(overview.content).toBe('unsaved')
|
||||
expect(overview.readOnly).toBe(true)
|
||||
expect(overview.fileId).toBe(view.id)
|
||||
const state = store.getState()
|
||||
const overviewView = state.openFiles.find((file) => file.id === overview.fileId)!
|
||||
expect(editorTabModelUri(state, { ...overviewView, filePath: overview.filePath })).toBe(
|
||||
editorTabModelUri(state, view)
|
||||
)
|
||||
})
|
||||
|
||||
it('follows typing in another view of the file', () => {
|
||||
const { store, view, review } = setup()
|
||||
expect(render(store, review, 'disk').content).toBe('disk')
|
||||
|
||||
store.getState().setEditorDraft(view.id, 'a')
|
||||
store.getState().markFileDirty(view.id, true)
|
||||
expect(render(store, review, 'disk').content).toBe('a')
|
||||
store.getState().setEditorDraft(view.id, 'ab')
|
||||
expect(render(store, review, 'disk').content).toBe('ab')
|
||||
})
|
||||
|
||||
it("can't edit or save the record", () => {
|
||||
const { store, view, review } = setup()
|
||||
store.getState().setEditorDraft(view.id, 'unsaved')
|
||||
@@ -222,71 +195,4 @@ describe('conflict-review overview as a live read-only view of the record', () =
|
||||
expect(state.openFiles[0].isDirty).toBe(true)
|
||||
expect(state.editorBufferKept).toEqual({})
|
||||
})
|
||||
|
||||
it('shows disk text when no view holds the file', () => {
|
||||
const { store, view, review } = setup()
|
||||
store.getState().closeFile(view.id)
|
||||
const overview = render(store, review, 'disk')
|
||||
expect(overview.fileId).toBe(PATH)
|
||||
expect(overview.content).toBe('disk')
|
||||
})
|
||||
|
||||
it("retries a failed read through the borrowed view's own workspace", () => {
|
||||
const { store, view, review } = setup()
|
||||
const reloadContent = vi.fn()
|
||||
render(store, review, 'disk', { loadError: 'denied', reloadContent })
|
||||
retries.current[0]()
|
||||
// Retry is a user act that may start a stopped WSL distro.
|
||||
expect(reloadContent).toHaveBeenCalledWith(view, { wakeWslDistro: true })
|
||||
})
|
||||
|
||||
it('applies a reveal explicitly owned by this overview and rejects another pane', () => {
|
||||
const { store, view, review } = setup()
|
||||
const reveal = { fileId: view.id, filePath: PATH, line: 9, column: 1, matchLength: 0 }
|
||||
const ownOverview = render(store, review, 'disk', {
|
||||
pendingEditorReveal: { ...reveal, viewStateId: 'panel-1:overview:file.ts' }
|
||||
})
|
||||
expect(ownOverview.revealLine).toBe(9)
|
||||
|
||||
const anotherPane = render(store, review, 'disk', {
|
||||
pendingEditorReveal: { ...reveal, viewStateId: 'another-pane' }
|
||||
})
|
||||
expect(anotherPane.revealLine).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps a selected review editor scoped to its review pane', () => {
|
||||
const { store, review } = setup()
|
||||
store.getState().openConflictReviewFile(review.id, 'wt-2', '/repo', ENTRY, 'typescript')
|
||||
const selectedReview = store.getState().openFiles.find((file) => file.id === review.id)
|
||||
if (!selectedReview?.conflictReview?.selectedFileId) {
|
||||
throw new Error('Expected selected conflict')
|
||||
}
|
||||
const reveal = {
|
||||
fileId: selectedReview.conflictReview.selectedFileId,
|
||||
filePath: PATH,
|
||||
line: 9,
|
||||
column: 1,
|
||||
matchLength: 0
|
||||
}
|
||||
expect(
|
||||
render(store, selectedReview, 'disk', {
|
||||
pendingEditorReveal: { ...reveal, viewStateId: 'another-pane' }
|
||||
}).revealLine
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
render(store, selectedReview, 'disk', {
|
||||
pendingEditorReveal: { ...reveal, viewStateId: 'panel-1' }
|
||||
}).revealLine
|
||||
).toBe(9)
|
||||
})
|
||||
|
||||
it("mounts as a passive mirror: the view's workspace, no cursor tracking, ignores untargeted reveals", () => {
|
||||
const { store, view, review } = setup()
|
||||
const overview = render(store, review, 'disk', {
|
||||
pendingEditorReveal: { fileId: view.id, filePath: PATH, line: 9, column: 1, matchLength: 0 }
|
||||
})
|
||||
expect(overview.worktreeId).toBe('wt-1')
|
||||
expect(overview.passiveMirror).toBe(true)
|
||||
expect(overview.revealLine).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { OpenFile } from '@/store/slices/editor'
|
||||
import { buildOwnedEditorFileId } from '@/store/slices/editor/file-ids/editor-file-ids'
|
||||
|
||||
const lifecycle = vi.hoisted(() => ({
|
||||
events: [] as string[],
|
||||
@@ -230,28 +229,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('EditorContent Monaco lifecycle boundary', () => {
|
||||
it('keeps default-pane view state separate for the same path on different runtime hosts', () => {
|
||||
const filePath = '/repo/main.ts'
|
||||
const worktreeId = 'repo::/repo'
|
||||
const first = file(filePath, {
|
||||
id: buildOwnedEditorFileId(filePath, worktreeId, 'host-a'),
|
||||
runtimeEnvironmentId: 'host-a'
|
||||
})
|
||||
const second = file(filePath, {
|
||||
id: buildOwnedEditorFileId(filePath, worktreeId, 'host-b'),
|
||||
runtimeEnvironmentId: 'host-b'
|
||||
})
|
||||
const view = render(
|
||||
<EditorContent {...props(first, 'first host')} viewStateScopeId={first.id} />
|
||||
)
|
||||
const firstKey = lifecycle.viewStateKeys.at(-1)
|
||||
expect(firstKey).toBeTypeOf('string')
|
||||
|
||||
view.rerender(<EditorContent {...props(second, 'second host')} viewStateScopeId={second.id} />)
|
||||
|
||||
expect(lifecycle.viewStateKeys.at(-1)).not.toBe(firstKey)
|
||||
})
|
||||
|
||||
it('unmounts the prior path before reconciling a stale retained target model', () => {
|
||||
const first = file('/repo/first.ts')
|
||||
const second = file('/repo/second.ts')
|
||||
@@ -302,70 +279,4 @@ describe('EditorContent Monaco lifecycle boundary', () => {
|
||||
'mount-diff:/repo/notes.ts'
|
||||
])
|
||||
})
|
||||
|
||||
it('passes live-tail ownership only for a read-only live log', () => {
|
||||
const liveLog = file('/repo/session.jsonl', { readOnly: true, liveTail: true })
|
||||
|
||||
render(<EditorContent {...props(liveLog, 'session content')} />)
|
||||
|
||||
expect(lifecycle.mountedProps).toEqual([
|
||||
{ filePath: liveLog.filePath, readOnly: true, liveTail: true, viewStateId: 'same-pane' }
|
||||
])
|
||||
})
|
||||
|
||||
it('tells Monaco which pane it is so it can retire an explicit focus handoff', () => {
|
||||
const source = file('/repo/main.ts')
|
||||
|
||||
render(<EditorContent {...props(source, 'source content')} />)
|
||||
|
||||
// Missing pane IDs leave reveals armed, allowing later rich-mode remounts to steal focus.
|
||||
expect(lifecycle.mountedProps.at(0)?.viewStateId).toBe('same-pane')
|
||||
})
|
||||
|
||||
it('preserves notebook save ownership and worktree routing across the extracted surface', () => {
|
||||
const notebook = file('/repo/analysis.ipynb', { language: 'notebook' })
|
||||
const notebookProps = props(notebook, '{"cells": []}')
|
||||
|
||||
render(
|
||||
<EditorContent {...notebookProps} resolvedLanguage="notebook" isNotebook mdViewMode="rich" />
|
||||
)
|
||||
|
||||
expect(lifecycle.notebookProps).toHaveLength(1)
|
||||
expect(lifecycle.notebookProps[0]).toMatchObject({
|
||||
fileId: notebook.id,
|
||||
filePath: notebook.filePath,
|
||||
worktreeId: notebook.worktreeId,
|
||||
scrollCacheKey: `${notebook.filePath}::same-pane:notebook`,
|
||||
onContentChange: notebookProps.handleContentChange,
|
||||
onSave: notebookProps.handleSave
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards SSH and runtime ownership to rich markdown after extraction', () => {
|
||||
const markdown = file('/repo/notes.md', {
|
||||
language: 'markdown',
|
||||
externalSshTargetId: 'ssh-target',
|
||||
runtimeEnvironmentId: 'runtime-environment'
|
||||
})
|
||||
|
||||
render(
|
||||
<EditorContent
|
||||
{...props(markdown, '# Notes')}
|
||||
resolvedLanguage="markdown"
|
||||
isMarkdown
|
||||
mdViewMode="rich"
|
||||
inlineMarkdownRenderState={{
|
||||
renderMode: 'rich-editor',
|
||||
richModeUnsupportedMessage: null
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(lifecycle.richMarkdownProps).toHaveLength(1)
|
||||
expect(lifecycle.richMarkdownProps[0]).toMatchObject({
|
||||
externalSshTargetId: 'ssh-target',
|
||||
runtimeEnvironmentId: 'runtime-environment',
|
||||
worktreeId: markdown.worktreeId
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -99,72 +99,6 @@ describe('EditorPanelHeaderPath inline rename', () => {
|
||||
vi.stubGlobal('cancelAnimationFrame', vi.fn())
|
||||
})
|
||||
|
||||
it('opens a field holding the whole name', () => {
|
||||
renderPath(baseFile())
|
||||
openRenameInput()
|
||||
|
||||
const input = getRenameInput('Rename file notes.md')
|
||||
expect(input.value).toBe('notes.md')
|
||||
})
|
||||
|
||||
it('lets the field claim the full header width', () => {
|
||||
renderPath(baseFile())
|
||||
openRenameInput()
|
||||
|
||||
const input = getRenameInput('Rename file notes.md')
|
||||
expect(input.className).toContain('w-full')
|
||||
expect(input.className).toContain('max-w-full')
|
||||
})
|
||||
|
||||
it('selects the basename so typing replaces just the name', () => {
|
||||
renderPath(baseFile())
|
||||
openRenameInput()
|
||||
|
||||
const input = getRenameInput('Rename file notes.md')
|
||||
expect(document.activeElement).toBe(input)
|
||||
expect(input.selectionStart).toBe(0)
|
||||
expect(input.selectionEnd).toBe('notes'.length)
|
||||
})
|
||||
|
||||
it('renames to the typed name verbatim', () => {
|
||||
renderPath(baseFile())
|
||||
openRenameInput()
|
||||
|
||||
const input = getRenameInput('Rename file notes.md')
|
||||
fireEvent.change(input, { target: { value: 'renamed.mdx' } })
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
|
||||
expect(renameFileOnDiskMock).toHaveBeenCalledWith({
|
||||
oldPath: '/repo/notes.md',
|
||||
newName: 'renamed.mdx',
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
documentScoped: false
|
||||
})
|
||||
})
|
||||
|
||||
it('commits on blur like the tab bar and file explorer', () => {
|
||||
renderPath(baseFile())
|
||||
openRenameInput()
|
||||
|
||||
const input = getRenameInput('Rename file notes.md')
|
||||
fireEvent.change(input, { target: { value: 'renamed.md' } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(renameFileOnDiskMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ newName: 'renamed.md' })
|
||||
)
|
||||
})
|
||||
|
||||
it('does not request a rename when the name was not edited', () => {
|
||||
renderPath(baseFile())
|
||||
openRenameInput()
|
||||
|
||||
fireEvent.keyDown(getRenameInput('Rename file notes.md'), { key: 'Enter' })
|
||||
|
||||
expect(renameFileOnDiskMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ignores IME confirmation and its redispatch before accepting deliberate Enter', () => {
|
||||
renderPath(baseFile())
|
||||
openRenameInput()
|
||||
@@ -236,56 +170,4 @@ describe('EditorPanelHeaderPath inline rename', () => {
|
||||
|
||||
expect(renameFileOnDiskMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('selects the whole name when there is no extension', () => {
|
||||
const file = baseFile({
|
||||
id: '/repo/Makefile',
|
||||
filePath: '/repo/Makefile',
|
||||
relativePath: 'Makefile'
|
||||
})
|
||||
renderPath(file)
|
||||
openRenameInput()
|
||||
|
||||
const input = getRenameInput('Rename file Makefile')
|
||||
expect(input.selectionStart).toBe(0)
|
||||
expect(input.selectionEnd).toBe('Makefile'.length)
|
||||
})
|
||||
})
|
||||
|
||||
describe('EditorPanelHeaderPath reveal in file manager', () => {
|
||||
const openInFileManager = vi.fn()
|
||||
|
||||
function openPathMenu(): HTMLElement {
|
||||
const pathRow = document.querySelector('.editor-header-path-row')
|
||||
if (!pathRow) {
|
||||
throw new Error('Missing editor header path row')
|
||||
}
|
||||
fireEvent.contextMenu(pathRow)
|
||||
return screen.getByRole('menuitem', { name: /Open Containing Folder/ })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
openInFileManager.mockReset().mockResolvedValue({ ok: true })
|
||||
Object.assign(window, { api: { shell: { openInFileManager } } })
|
||||
})
|
||||
|
||||
it('reveals the open file through the shared reveal action', () => {
|
||||
renderPath(baseFile())
|
||||
|
||||
fireEvent.click(openPathMenu())
|
||||
|
||||
expect(openInFileManager).toHaveBeenCalledWith('/repo/notes.md', 'local')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a remote runtime owns', { runtimeEnvironmentId: 'env-1' }],
|
||||
['opened from an SSH host outside the workspace', { externalSshTargetId: 'ssh-1' }]
|
||||
])('disables reveal as local-only for a file %s', (_owner, overrides) => {
|
||||
renderPath(baseFile(overrides))
|
||||
|
||||
const reveal = openPathMenu()
|
||||
|
||||
expect(reveal.getAttribute('aria-disabled')).toBe('true')
|
||||
expect(reveal.textContent).toContain('Local only')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -56,48 +56,6 @@ describe('editor autosave changed-on-disk conflict flow', () => {
|
||||
__clearSelfWriteRegistryForTests()
|
||||
})
|
||||
|
||||
it('preserves dirty drafts and marks the tab changed-on-disk on external file change', () => {
|
||||
stubEditorWindow()
|
||||
const store = createEditorStore()
|
||||
openDirtyFile(store)
|
||||
|
||||
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
|
||||
try {
|
||||
dispatchExternalChange()
|
||||
|
||||
const file = store.getState().openFiles[0]
|
||||
expect(file?.isDirty).toBe(true)
|
||||
expect(file?.externalMutation).toBe('changed')
|
||||
expect(store.getState().editorDrafts['/repo/file.ts']).toBe('unsaved edit')
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('clears drafts and a stale changed-on-disk mark for clean tabs on external file change', () => {
|
||||
stubEditorWindow()
|
||||
const store = createEditorStore()
|
||||
store.getState().openFile({
|
||||
filePath: '/repo/file.ts',
|
||||
relativePath: 'file.ts',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'typescript',
|
||||
mode: 'edit'
|
||||
})
|
||||
store.getState().setExternalMutation('/repo/file.ts', 'changed')
|
||||
|
||||
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
|
||||
try {
|
||||
dispatchExternalChange()
|
||||
|
||||
const file = store.getState().openFiles[0]
|
||||
expect(file?.isDirty).toBe(false)
|
||||
expect(file?.externalMutation).toBeUndefined()
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not backstop-mark a dirty tab for the echo of its own save', () => {
|
||||
stubEditorWindow()
|
||||
const store = createEditorStore()
|
||||
@@ -154,21 +112,6 @@ describe('editor autosave changed-on-disk conflict flow', () => {
|
||||
return { fakeDisk, store, cleanup: attachEditorAutosaveController(store) }
|
||||
}
|
||||
|
||||
it.each([
|
||||
['disk still holds the baseline', 'base'],
|
||||
['disk already holds the draft', 'restored draft']
|
||||
])('lands and goes clean without a banner when %s', async (_label, disk) => {
|
||||
const { fakeDisk, store, cleanup } = restore(disk)
|
||||
try {
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
expect(fakeDisk.files.get('/repo/file.ts')).toBe('restored draft')
|
||||
expect(store.getState().openFiles[0]).toMatchObject({ isDirty: false })
|
||||
expect(store.getState().openFiles[0]?.externalMutation).toBeUndefined()
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('is refused with the banner, keeps the draft and stops autosaving when disk changed', async () => {
|
||||
const { fakeDisk, store, cleanup } = restore('agent write')
|
||||
try {
|
||||
@@ -212,28 +155,4 @@ describe('editor autosave changed-on-disk conflict flow', () => {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it("clears the changed-on-disk mark after the banner's Overwrite", async () => {
|
||||
const writeFile = stubEditorWindow()
|
||||
const store = createEditorStore()
|
||||
openDirtyFile(store, 'user version')
|
||||
store.getState().setExternalMutation('/repo/file.ts', 'changed')
|
||||
|
||||
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
|
||||
try {
|
||||
await requestEditorFileSave({ fileId: '/repo/file.ts', overwrite: true })
|
||||
|
||||
expect(writeFile).toHaveBeenCalledWith({
|
||||
filePath: '/repo/file.ts',
|
||||
content: 'user version',
|
||||
connectionId: undefined,
|
||||
expectedExecutionHostId: 'local'
|
||||
})
|
||||
const file = store.getState().openFiles[0]
|
||||
expect(file?.isDirty).toBe(false)
|
||||
expect(file?.externalMutation).toBeUndefined()
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,13 +21,9 @@ import {
|
||||
import { clearRuntimeCompatibilityCacheForTests } from '@/runtime/runtime-rpc-client'
|
||||
import { getDefaultSettings } from '../../../../../shared/constants'
|
||||
import { makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
|
||||
import { folderWorkspaceKey } from '../../../../../shared/workspace-scope'
|
||||
|
||||
import { emptyTabLayoutState } from '../../../store/slices/editor-slice-test-harness'
|
||||
import { withLoadedBaselines } from './editor-autosave-controller-test-fixture'
|
||||
import {
|
||||
makeFolderWorkspace,
|
||||
makeProjectGroup
|
||||
} from '@/components/sidebar/workspace-creation/folder/folder-workspace-composer-submit-fixtures'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getConnectionIdForFile: vi.fn()
|
||||
@@ -166,220 +162,6 @@ describe('attachEditorAutosaveController', () => {
|
||||
__clearSelfWriteRegistryForTests()
|
||||
})
|
||||
|
||||
it('saves dirty files even when the visible EditorPanel is not mounted', async () => {
|
||||
const writeFile = vi.fn().mockResolvedValue(undefined)
|
||||
const eventTarget = new EventTarget()
|
||||
vi.stubGlobal('window', {
|
||||
addEventListener: eventTarget.addEventListener.bind(eventTarget),
|
||||
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
|
||||
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
|
||||
setTimeout: globalThis.setTimeout.bind(globalThis),
|
||||
clearTimeout: globalThis.clearTimeout.bind(globalThis),
|
||||
api: {
|
||||
fs: {
|
||||
writeFile
|
||||
}
|
||||
}
|
||||
} satisfies WindowStub)
|
||||
|
||||
const store = createEditorStore()
|
||||
store.getState().openFile({
|
||||
filePath: '/repo/file.ts',
|
||||
relativePath: 'file.ts',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'typescript',
|
||||
mode: 'edit'
|
||||
})
|
||||
store.getState().setEditorDraft('/repo/file.ts', 'edited')
|
||||
store.getState().markFileDirty('/repo/file.ts', true)
|
||||
|
||||
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
|
||||
try {
|
||||
await requestDirtyFileSave()
|
||||
|
||||
expect(writeFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
filePath: '/repo/file.ts',
|
||||
content: 'edited',
|
||||
connectionId: undefined,
|
||||
expectedExecutionHostId: 'local'
|
||||
})
|
||||
)
|
||||
expect(store.getState().openFiles[0]?.isDirty).toBe(false)
|
||||
expect(store.getState().editorDrafts).toEqual({})
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('saves folder workspace files through the path-specific SSH connection', async () => {
|
||||
const writeFile = vi.fn().mockResolvedValue(undefined)
|
||||
const eventTarget = new EventTarget()
|
||||
vi.stubGlobal('window', {
|
||||
addEventListener: eventTarget.addEventListener.bind(eventTarget),
|
||||
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
|
||||
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
|
||||
setTimeout: globalThis.setTimeout.bind(globalThis),
|
||||
clearTimeout: globalThis.clearTimeout.bind(globalThis),
|
||||
api: {
|
||||
fs: {
|
||||
writeFile
|
||||
}
|
||||
}
|
||||
} satisfies WindowStub)
|
||||
|
||||
const store = createEditorStore()
|
||||
const workspaceKey = folderWorkspaceKey('folder-workspace-1')
|
||||
store.setState({
|
||||
folderWorkspaces: [
|
||||
makeFolderWorkspace({
|
||||
folderPath: '/home/neil/platform',
|
||||
executionHostId: 'ssh:ssh-1',
|
||||
connectionId: 'ssh-1'
|
||||
})
|
||||
],
|
||||
projectGroups: [{ ...makeProjectGroup(), executionHostId: 'ssh:ssh-1' }],
|
||||
worktreesByRepo: {
|
||||
'folder-workspace-1': [
|
||||
makeWorktree({
|
||||
id: workspaceKey,
|
||||
repoId: 'folder-workspace-1',
|
||||
path: '/home/neil/platform',
|
||||
hostId: 'ssh:ssh-1'
|
||||
})
|
||||
]
|
||||
},
|
||||
sshConnectionStates: new Map([
|
||||
[
|
||||
'ssh-1',
|
||||
{
|
||||
targetId: 'ssh-1',
|
||||
status: 'connected',
|
||||
error: null,
|
||||
reconnectAttempt: 0,
|
||||
connectionGeneration: 4
|
||||
}
|
||||
]
|
||||
])
|
||||
})
|
||||
store.getState().openFile({
|
||||
filePath: '/home/neil/platform/api/src/file.ts',
|
||||
relativePath: 'api/src/file.ts',
|
||||
worktreeId: workspaceKey,
|
||||
language: 'typescript',
|
||||
mode: 'edit'
|
||||
})
|
||||
store.getState().setEditorDraft('/home/neil/platform/api/src/file.ts', 'edited')
|
||||
store.getState().markFileDirty('/home/neil/platform/api/src/file.ts', true)
|
||||
|
||||
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
|
||||
try {
|
||||
await requestDirtyFileSave()
|
||||
|
||||
expect(writeFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
filePath: '/home/neil/platform/api/src/file.ts',
|
||||
content: 'edited',
|
||||
connectionId: 'ssh-1',
|
||||
expectedExecutionHostId: 'ssh:ssh-1',
|
||||
expectedSshTargetId: 'ssh-1',
|
||||
expectedSshConnectionGeneration: 4
|
||||
})
|
||||
)
|
||||
expect(store.getState().openFiles[0]?.isDirty).toBe(false)
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it('saves runtime-owned folder workspace files through the folder root', async () => {
|
||||
clearRuntimeCompatibilityCacheForTests()
|
||||
const writeFile = vi.fn().mockResolvedValue(undefined)
|
||||
const runtimeCall = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
result: {},
|
||||
_meta: { runtimeId: 'runtime-env-1' }
|
||||
})
|
||||
const runtimeTransportCall = vi.fn((args: RuntimeEnvironmentCallRequest) => {
|
||||
return (
|
||||
createCompatibleRuntimeStatusResponseIfNeeded(args, 'runtime-env-1') ?? runtimeCall(args)
|
||||
)
|
||||
})
|
||||
const eventTarget = new EventTarget()
|
||||
vi.stubGlobal('window', {
|
||||
addEventListener: eventTarget.addEventListener.bind(eventTarget),
|
||||
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
|
||||
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
|
||||
setTimeout: globalThis.setTimeout.bind(globalThis),
|
||||
clearTimeout: globalThis.clearTimeout.bind(globalThis),
|
||||
api: {
|
||||
fs: { writeFile },
|
||||
runtimeEnvironments: { call: runtimeTransportCall }
|
||||
}
|
||||
} satisfies WindowStub)
|
||||
|
||||
const store = createEditorStore()
|
||||
const workspaceId = 'folder-workspace-runtime'
|
||||
const workspaceKey = folderWorkspaceKey(workspaceId)
|
||||
store.setState({
|
||||
settings: {
|
||||
editorAutoSave: true,
|
||||
editorAutoSaveDelayMs: 1000,
|
||||
activeRuntimeEnvironmentId: 'env-2'
|
||||
} as never,
|
||||
worktreesByRepo: {},
|
||||
folderWorkspaces: [
|
||||
{
|
||||
id: workspaceId,
|
||||
projectGroupId: 'group-runtime',
|
||||
folderPath: '/runtime/folder',
|
||||
connectionId: null
|
||||
} as never
|
||||
],
|
||||
projectGroups: [
|
||||
{
|
||||
id: 'group-runtime',
|
||||
connectionId: null,
|
||||
executionHostId: 'runtime:env-1'
|
||||
} as never
|
||||
]
|
||||
})
|
||||
const fileId = store.getState().openFile({
|
||||
filePath: '/runtime/folder/src/file.ts',
|
||||
relativePath: 'src/file.ts',
|
||||
worktreeId: workspaceKey,
|
||||
language: 'typescript',
|
||||
mode: 'edit'
|
||||
})
|
||||
store.getState().setEditorDraft(fileId, 'edited')
|
||||
store.getState().markFileDirty(fileId, true)
|
||||
|
||||
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
|
||||
try {
|
||||
await requestDirtyFileSave()
|
||||
|
||||
expect(runtimeCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
selector: 'env-1',
|
||||
method: 'files.writeIfUnchanged',
|
||||
expectedEnvironmentPairingRevision: undefined,
|
||||
params: {
|
||||
worktree: `id:${workspaceKey}`,
|
||||
relativePath: 'src/file.ts',
|
||||
content: 'edited',
|
||||
expectedExecutionHostId: 'local',
|
||||
diskPrecondition: expect.objectContaining({ allowMissing: true })
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
)
|
||||
expect(writeFile).not.toHaveBeenCalled()
|
||||
expect(store.getState().openFiles[0]?.isDirty).toBe(false)
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['unique', 'conflict-first', 'conflict-last'] as const)(
|
||||
'keeps legacy runtime saves on a unique published directory (%s)',
|
||||
async (variant) => {
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { OpenFile } from '@/store/slices/editor/types/open-file'
|
||||
import {
|
||||
canAutoSaveOpenFile,
|
||||
getOpenFilesForExternalFileChange,
|
||||
isExternalReloadableEditorTab,
|
||||
normalizeAutoSaveDelayMs,
|
||||
ORCA_EDITOR_REQUEST_FILE_CLOSE_EVENT,
|
||||
ORCA_EDITOR_QUIESCE_FILE_SAVES_EVENT,
|
||||
requestEditorFileClose,
|
||||
requestEditorFileSave,
|
||||
requestEditorSaveQuiesce
|
||||
} from './editor-autosave'
|
||||
|
||||
type WindowEventStub = Pick<
|
||||
Window,
|
||||
'addEventListener' | 'removeEventListener' | 'dispatchEvent' | 'setTimeout' | 'clearTimeout'
|
||||
>
|
||||
|
||||
function makeOpenFile(overrides: Partial<OpenFile> = {}): OpenFile {
|
||||
return {
|
||||
id: '/repo/file.ts',
|
||||
filePath: '/repo/file.ts',
|
||||
relativePath: 'file.ts',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'typescript',
|
||||
isDirty: false,
|
||||
mode: 'edit',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const eventTarget = new EventTarget()
|
||||
vi.stubGlobal('window', {
|
||||
addEventListener: eventTarget.addEventListener.bind(eventTarget),
|
||||
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
|
||||
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
|
||||
setTimeout: globalThis.setTimeout.bind(globalThis),
|
||||
clearTimeout: globalThis.clearTimeout.bind(globalThis)
|
||||
} satisfies WindowEventStub)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('canAutoSaveOpenFile', () => {
|
||||
it('allows normal edit tabs and unstaged single-file diffs', () => {
|
||||
expect(canAutoSaveOpenFile(makeOpenFile())).toBe(true)
|
||||
expect(
|
||||
canAutoSaveOpenFile(
|
||||
makeOpenFile({
|
||||
id: 'wt-1::diff::unstaged::file.ts',
|
||||
mode: 'diff',
|
||||
diffSource: 'unstaged'
|
||||
})
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects staged, combined, and conflict-review tabs', () => {
|
||||
expect(
|
||||
canAutoSaveOpenFile(
|
||||
makeOpenFile({
|
||||
id: 'wt-1::diff::staged::file.ts',
|
||||
mode: 'diff',
|
||||
diffSource: 'staged'
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
expect(
|
||||
canAutoSaveOpenFile(
|
||||
makeOpenFile({
|
||||
id: 'wt-1::all-diffs::uncommitted',
|
||||
filePath: '/repo',
|
||||
relativePath: 'All Changes',
|
||||
mode: 'diff',
|
||||
diffSource: 'combined-uncommitted'
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
expect(
|
||||
canAutoSaveOpenFile(
|
||||
makeOpenFile({
|
||||
id: 'wt-1::conflicts',
|
||||
filePath: '/repo',
|
||||
relativePath: 'Conflicts',
|
||||
mode: 'conflict-review'
|
||||
})
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects read-only edit tabs (AI Vault View Log)', () => {
|
||||
expect(canAutoSaveOpenFile(makeOpenFile({ readOnly: true }))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeAutoSaveDelayMs', () => {
|
||||
it('defaults and clamps invalid values', () => {
|
||||
expect(normalizeAutoSaveDelayMs(undefined)).toBe(1000)
|
||||
expect(normalizeAutoSaveDelayMs(Number.NaN)).toBe(1000)
|
||||
expect(normalizeAutoSaveDelayMs('750')).toBe(750)
|
||||
expect(normalizeAutoSaveDelayMs('oops')).toBe(1000)
|
||||
expect(normalizeAutoSaveDelayMs(10)).toBe(250)
|
||||
expect(normalizeAutoSaveDelayMs(25_000)).toBe(10_000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('requestEditorSaveQuiesce', () => {
|
||||
it('resolves immediately when no editor listener claims the request', async () => {
|
||||
const release = await requestEditorSaveQuiesce({ fileId: 'file-1' })
|
||||
expect(release).toBeTypeOf('function')
|
||||
expect(release()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('waits for a claiming listener to finish quiescing', async () => {
|
||||
let resolved = false
|
||||
const handler = (event: Event): void => {
|
||||
const detail = (event as CustomEvent).detail as {
|
||||
claim: () => void
|
||||
resolve: () => void
|
||||
}
|
||||
detail.claim()
|
||||
window.setTimeout(() => detail.resolve(), 0)
|
||||
}
|
||||
|
||||
window.addEventListener(ORCA_EDITOR_QUIESCE_FILE_SAVES_EVENT, handler as EventListener)
|
||||
try {
|
||||
const promise = requestEditorSaveQuiesce({ fileId: 'file-1' }).then(() => {
|
||||
resolved = true
|
||||
})
|
||||
|
||||
expect(resolved).toBe(false)
|
||||
await promise
|
||||
expect(resolved).toBe(true)
|
||||
} finally {
|
||||
window.removeEventListener(ORCA_EDITOR_QUIESCE_FILE_SAVES_EVENT, handler as EventListener)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('requestEditorFileSave', () => {
|
||||
it('rejects when no save controller claims the request', async () => {
|
||||
await expect(requestEditorFileSave({ fileId: 'file-1' })).rejects.toThrow(
|
||||
'Editor save controller is unavailable.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('requestEditorFileClose', () => {
|
||||
it('dispatches a close request event with the file id', () => {
|
||||
const listener = vi.fn()
|
||||
window.addEventListener(ORCA_EDITOR_REQUEST_FILE_CLOSE_EVENT, listener as EventListener)
|
||||
try {
|
||||
requestEditorFileClose('file-1')
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
const event = listener.mock.calls[0][0] as CustomEvent<{ fileId: string }>
|
||||
expect(event.detail).toEqual({ fileId: 'file-1' })
|
||||
} finally {
|
||||
window.removeEventListener(ORCA_EDITOR_REQUEST_FILE_CLOSE_EVENT, listener as EventListener)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('isExternalReloadableEditorTab', () => {
|
||||
it('includes edit, preview, and single-file staged/unstaged diff tabs', () => {
|
||||
expect(isExternalReloadableEditorTab(makeOpenFile())).toBe(true)
|
||||
expect(
|
||||
isExternalReloadableEditorTab(
|
||||
makeOpenFile({ mode: 'markdown-preview', language: 'markdown' })
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isExternalReloadableEditorTab(
|
||||
makeOpenFile({ mode: 'diff', diffSource: 'unstaged', id: 'diff-unstaged' })
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isExternalReloadableEditorTab(
|
||||
makeOpenFile({ mode: 'diff', diffSource: 'staged', id: 'diff-staged' })
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isExternalReloadableEditorTab(
|
||||
makeOpenFile({ mode: 'diff', diffSource: 'combined-uncommitted', id: 'combined' })
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOpenFilesForExternalFileChange', () => {
|
||||
it('matches edit tabs and unstaged diff tabs for the same worktree file', () => {
|
||||
const matchingEdit = makeOpenFile()
|
||||
const matchingPreview = makeOpenFile({
|
||||
id: 'markdown-preview::/repo/file.ts',
|
||||
mode: 'markdown-preview',
|
||||
language: 'markdown',
|
||||
markdownPreviewSourceFileId: '/repo/file.ts'
|
||||
})
|
||||
const matchingUnstagedDiff = makeOpenFile({
|
||||
id: 'wt-1::diff::unstaged::file.ts',
|
||||
mode: 'diff',
|
||||
diffSource: 'unstaged'
|
||||
})
|
||||
const stagedDiff = makeOpenFile({
|
||||
id: 'wt-1::diff::staged::file.ts',
|
||||
mode: 'diff',
|
||||
diffSource: 'staged'
|
||||
})
|
||||
const otherWorktree = makeOpenFile({
|
||||
id: '/other/file.ts',
|
||||
filePath: '/other/file.ts',
|
||||
worktreeId: 'wt-2'
|
||||
})
|
||||
|
||||
expect(
|
||||
getOpenFilesForExternalFileChange(
|
||||
[matchingEdit, matchingPreview, matchingUnstagedDiff, stagedDiff, otherWorktree],
|
||||
{
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
relativePath: 'file.ts'
|
||||
}
|
||||
).map((file) => file.id)
|
||||
).toEqual([
|
||||
'/repo/file.ts',
|
||||
'markdown-preview::/repo/file.ts',
|
||||
'wt-1::diff::unstaged::file.ts',
|
||||
'wt-1::diff::staged::file.ts'
|
||||
])
|
||||
})
|
||||
|
||||
it('filters same-path matches by runtime owner when the watcher supplies one', () => {
|
||||
const localEdit = makeOpenFile({ id: 'local-edit', runtimeEnvironmentId: null })
|
||||
const runtimeEdit = makeOpenFile({ id: 'runtime-edit', runtimeEnvironmentId: 'env-1' })
|
||||
const runtimeDiff = makeOpenFile({
|
||||
id: 'runtime-diff',
|
||||
mode: 'diff',
|
||||
diffSource: 'unstaged',
|
||||
runtimeEnvironmentId: 'env-1'
|
||||
})
|
||||
|
||||
expect(
|
||||
getOpenFilesForExternalFileChange([localEdit, runtimeEdit, runtimeDiff], {
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
relativePath: 'file.ts',
|
||||
runtimeEnvironmentId: null
|
||||
}).map((file) => file.id)
|
||||
).toEqual(['local-edit'])
|
||||
|
||||
expect(
|
||||
getOpenFilesForExternalFileChange([localEdit, runtimeEdit, runtimeDiff], {
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: '/repo',
|
||||
relativePath: 'file.ts',
|
||||
runtimeEnvironmentId: 'env-1'
|
||||
}).map((file) => file.id)
|
||||
).toEqual(['runtime-edit', 'runtime-diff'])
|
||||
})
|
||||
|
||||
it('matches Windows edit tabs across separator and drive spellings, but not tail case', () => {
|
||||
const tab = makeOpenFile({ id: 'tab', filePath: 'c:\\Repo\\File.ts' })
|
||||
const otherCase = makeOpenFile({ id: 'other-case', filePath: 'c:\\Repo\\file.ts' })
|
||||
|
||||
expect(
|
||||
getOpenFilesForExternalFileChange([tab, otherCase], {
|
||||
worktreeId: 'wt-1',
|
||||
worktreePath: 'C:/Repo',
|
||||
relativePath: 'File.ts'
|
||||
})
|
||||
).toEqual([tab])
|
||||
})
|
||||
|
||||
it('matches restored WSL aliases only for a proven local Windows watcher', () => {
|
||||
vi.stubGlobal('navigator', { userAgent: 'Windows' })
|
||||
const terminalLinkTab = makeOpenFile({
|
||||
id: '//wsl.localhost/Ubuntu/workspace/repo/file.ts',
|
||||
filePath: '//wsl.localhost/Ubuntu/workspace/repo/file.ts',
|
||||
worktreeId: 'wt-wsl'
|
||||
})
|
||||
|
||||
expect(
|
||||
getOpenFilesForExternalFileChange([terminalLinkTab], {
|
||||
worktreeId: 'wt-wsl',
|
||||
worktreePath: '\\\\wsl.localhost\\Ubuntu\\workspace\\repo',
|
||||
relativePath: 'file.ts',
|
||||
runtimeEnvironmentId: null,
|
||||
allowLocalWindowsWslAliases: true
|
||||
}).map((file) => file.id)
|
||||
).toEqual(['//wsl.localhost/Ubuntu/workspace/repo/file.ts'])
|
||||
|
||||
expect(
|
||||
getOpenFilesForExternalFileChange([terminalLinkTab], {
|
||||
worktreeId: 'wt-wsl',
|
||||
worktreePath: '\\\\wsl.localhost\\Ubuntu\\workspace\\repo',
|
||||
relativePath: 'file.ts',
|
||||
runtimeEnvironmentId: null
|
||||
})
|
||||
).toEqual([])
|
||||
|
||||
vi.stubGlobal('navigator', { userAgent: 'Linux' })
|
||||
expect(
|
||||
getOpenFilesForExternalFileChange([terminalLinkTab], {
|
||||
worktreeId: 'wt-wsl',
|
||||
worktreePath: '\\\\wsl.localhost\\Ubuntu\\workspace\\repo',
|
||||
relativePath: 'file.ts',
|
||||
runtimeEnvironmentId: null,
|
||||
allowLocalWindowsWslAliases: true
|
||||
})
|
||||
).toEqual([])
|
||||
|
||||
vi.stubGlobal('navigator', { userAgent: 'Windows' })
|
||||
vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
|
||||
expect(
|
||||
getOpenFilesForExternalFileChange([terminalLinkTab], {
|
||||
worktreeId: 'wt-wsl',
|
||||
worktreePath: '\\\\wsl.localhost\\Ubuntu\\workspace\\repo',
|
||||
relativePath: 'file.ts',
|
||||
runtimeEnvironmentId: null,
|
||||
allowLocalWindowsWslAliases: true
|
||||
})
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
-100
@@ -126,106 +126,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('synthetic outcome payloads through real sidebar and header actions', () => {
|
||||
it('keeps the provider action and truthful manual output without inventing failed steps', () => {
|
||||
const manualCheck = { ...check, conclusion: 'neutral' as const }
|
||||
const details = {
|
||||
...readingDetails(),
|
||||
conclusion: 'neutral',
|
||||
summary: 'Waiting for manual approval.',
|
||||
jobs: []
|
||||
}
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<CheckRunDetailsPanel
|
||||
check={manualCheck}
|
||||
details={details}
|
||||
loading={false}
|
||||
error={null}
|
||||
openUrl={check.url}
|
||||
worktreeId={null}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
expect(screen.getByText('Waiting for manual approval.')).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Open details' })).toBeTruthy()
|
||||
expect(screen.queryByText('Failed jobs')).toBeNull()
|
||||
expect(screen.queryByText('Log excerpt')).toBeNull()
|
||||
expect(document.body.textContent).not.toMatch(/expired|Couldn't load logs|steps failed/)
|
||||
})
|
||||
it.each(['available', 'empty', 'not_requested', undefined] as const)(
|
||||
'does not invent first-text navigation from null outcome %s',
|
||||
async (outcome) => {
|
||||
const first = readingDetails(null, outcome)
|
||||
const pending = gate()
|
||||
useAppStore.setState({ fetchPRCheckDetails: vi.fn(() => pending.promise) })
|
||||
open(first)
|
||||
const marker = vi.spyOn(diagnosticPosition, 'findInitialCheckLogDiagnostic')
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<StoredPanel />
|
||||
</TooltipProvider>
|
||||
)
|
||||
expect(scrollTopCache.size).toBe(1)
|
||||
expect([...scrollTopCache.keys()].some((key) => key.includes('::intent:'))).toBe(false)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
|
||||
await act(async () => pending.resolve(readingDetails(undefined, 'available')))
|
||||
expect(screen.getByTestId('outcome-pane').querySelector('pre')?.scrollTop).toBe(0)
|
||||
expect(marker).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('ends zero-layout first-text setup and cannot revive it after close or late frames', async () => {
|
||||
const frames = new Map<number, FrameRequestCallback>()
|
||||
let frameId = 0
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
||||
frames.set(++frameId, callback)
|
||||
return frameId
|
||||
})
|
||||
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation((key) => {
|
||||
frames.delete(key)
|
||||
})
|
||||
vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(0)
|
||||
const first = payload()
|
||||
const pending = gate()
|
||||
useAppStore.setState({ fetchPRCheckDetails: vi.fn(() => pending.promise) })
|
||||
open(first)
|
||||
const marker = vi.spyOn(diagnosticPosition, 'findInitialCheckLogDiagnostic')
|
||||
const { unmount } = render(
|
||||
<TooltipProvider>
|
||||
<StoredPanel />
|
||||
</TooltipProvider>
|
||||
)
|
||||
expect(marker).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
|
||||
await act(async () =>
|
||||
pending.resolve(payload('Error: synthetic delayed first text', 'available'))
|
||||
)
|
||||
const queued = [...frames.values()]
|
||||
act(() => {
|
||||
frames.clear()
|
||||
for (const callback of queued) {
|
||||
callback(0)
|
||||
}
|
||||
})
|
||||
expect(marker).not.toHaveBeenCalled()
|
||||
expect(frames.size).toBe(0)
|
||||
vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(200)
|
||||
fireEvent.resize(window)
|
||||
expect(marker).not.toHaveBeenCalled()
|
||||
act(() => {
|
||||
useAppStore.setState({ openFiles: [] })
|
||||
})
|
||||
scrollTopCache.clear()
|
||||
act(() => {
|
||||
for (const callback of queued) {
|
||||
callback(0)
|
||||
}
|
||||
})
|
||||
unmount()
|
||||
expect(scrollTopCache.size).toBe(0)
|
||||
expect(frames.size).toBe(0)
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'retries one combined request with unchanged identities, already open=%s',
|
||||
async (alreadyOpen) => {
|
||||
|
||||
+2
-221
@@ -2,20 +2,15 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { scrollTopCache, setWithLRU } from '@/lib/scroll-cache'
|
||||
import {
|
||||
checkDetailsReadingKey,
|
||||
checkLogDigest,
|
||||
checkReadingIntentKey,
|
||||
checkReadingLogKey,
|
||||
checkReadingOuterKey,
|
||||
checkReadingStepsKey,
|
||||
reconcileCheckDetailsReadingCache
|
||||
checkReadingOuterKey
|
||||
} from './check-details-reading-cache'
|
||||
import { createCheckDetailsReadingOwner } from './check-details-reading-owner'
|
||||
|
||||
import { attachCheckReadingViewport } from './check-details-reading-viewport'
|
||||
import {
|
||||
measuredReadingNode,
|
||||
readingDetails,
|
||||
readingFile,
|
||||
readingFixture
|
||||
} from './check-details-reading-fixture'
|
||||
import * as digest from '../../../../../../shared/sha256'
|
||||
@@ -52,49 +47,6 @@ function flushFrames(): void {
|
||||
}
|
||||
|
||||
describe('owned reading positions', () => {
|
||||
it.each([false, true])(
|
||||
'allows first payload navigation only while deliberate intent survives, cancelled=%s',
|
||||
(cancelled) => {
|
||||
const { owner, store, file, identity, text, logKey } = readingFixture()
|
||||
const pending = { ...file.checkRunDetails!, details: null }
|
||||
store.setState({ openFiles: [{ ...file, checkRunDetails: pending }] })
|
||||
reconcileCheckDetailsReadingCache(file.id, undefined, pending, () => true)
|
||||
const detachOwner = owner.attach()
|
||||
const outer = measuredReadingNode()
|
||||
const detachOuter = attachCheckReadingViewport({
|
||||
node: outer,
|
||||
owner,
|
||||
cacheKey: checkReadingOuterKey(owner.key)
|
||||
})
|
||||
if (cancelled) {
|
||||
fireReading(outer)
|
||||
}
|
||||
const next = file.checkRunDetails!
|
||||
store.setState({ openFiles: [file] })
|
||||
reconcileCheckDetailsReadingCache(file.id, pending, next, () => true)
|
||||
const node = measuredReadingNode(text)
|
||||
const initial = vi.fn(() => 600)
|
||||
const detach = attachCheckReadingViewport({
|
||||
node,
|
||||
owner,
|
||||
cacheKey: logKey,
|
||||
job: identity,
|
||||
text,
|
||||
initial,
|
||||
consumeIntent: () => {
|
||||
const eligible = owner.claim(identity)
|
||||
scrollTopCache.delete(checkReadingIntentKey(owner.key, identity))
|
||||
return eligible
|
||||
}
|
||||
})
|
||||
expect(initial).toHaveBeenCalledTimes(cancelled ? 0 : 1)
|
||||
expect(node.scrollTop).toBe(cancelled ? 0 : 600)
|
||||
detach()
|
||||
detachOuter()
|
||||
detachOwner()
|
||||
}
|
||||
)
|
||||
|
||||
it('retirement cancels a pending frame and rejects a captured callback after close and reopen', () => {
|
||||
const { owner, store, file, logKey, text, identity } = readingFixture()
|
||||
const node = measuredReadingNode(text, 0)
|
||||
@@ -119,40 +71,6 @@ describe('owned reading positions', () => {
|
||||
detachOwner()
|
||||
expect(scrollTopCache.has(logKey)).toBe(false)
|
||||
})
|
||||
|
||||
it('settled outer scrolling scans no cache keys and invokes no setup work', () => {
|
||||
const { owner, store, identity, text } = readingFixture()
|
||||
const node = measuredReadingNode()
|
||||
const detachOwner = owner.attach()
|
||||
const cancel = vi.fn()
|
||||
const unregister = owner.register({
|
||||
job: identity,
|
||||
text,
|
||||
start: () => {},
|
||||
retire: () => undefined,
|
||||
cancel
|
||||
})
|
||||
const initial = vi.fn(() => 400)
|
||||
const detach = attachCheckReadingViewport({
|
||||
node,
|
||||
owner,
|
||||
cacheKey: checkReadingOuterKey(owner.key),
|
||||
initial
|
||||
})
|
||||
fireReading(node)
|
||||
const keys = vi.spyOn(scrollTopCache, 'keys')
|
||||
for (let index = 0; index < 100; index++) {
|
||||
node.scrollTop = index
|
||||
node.dispatchEvent(new Event('scroll'))
|
||||
store.setState({ unrelated: index })
|
||||
}
|
||||
expect(keys).not.toHaveBeenCalled()
|
||||
expect(initial).not.toHaveBeenCalled()
|
||||
expect(cancel).toHaveBeenCalledTimes(1)
|
||||
detach()
|
||||
unregister()
|
||||
detachOwner()
|
||||
})
|
||||
it.each(['outer', 'inner'])(
|
||||
'preserves retained %s target through zero layout and a real remount',
|
||||
(kind) => {
|
||||
@@ -181,77 +99,6 @@ describe('owned reading positions', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('saves a valid zero in a measurable short document', () => {
|
||||
const { owner, logKey, text, identity } = readingFixture()
|
||||
const node = measuredReadingNode(text)
|
||||
Object.defineProperty(node, 'scrollHeight', { value: 100 })
|
||||
setWithLRU(scrollTopCache, logKey, 180)
|
||||
const detachOwner = owner.attach()
|
||||
const detach = attachCheckReadingViewport({
|
||||
node,
|
||||
owner,
|
||||
cacheKey: logKey,
|
||||
job: identity,
|
||||
text
|
||||
})
|
||||
expect(scrollTopCache.get(logKey)).toBe(0)
|
||||
detach()
|
||||
detachOwner()
|
||||
})
|
||||
|
||||
it('consumes an unmeasurable initial attempt once and cancels its sole frame on teardown', () => {
|
||||
const { owner, logKey, text, identity } = readingFixture()
|
||||
const node = measuredReadingNode(text, 0)
|
||||
const initial = vi.fn(() => 700)
|
||||
const detachOwner = owner.attach()
|
||||
const detach = attachCheckReadingViewport({
|
||||
node,
|
||||
owner,
|
||||
cacheKey: logKey,
|
||||
job: identity,
|
||||
text,
|
||||
initial,
|
||||
consumeIntent: () => true
|
||||
})
|
||||
expect(frames.size).toBe(1)
|
||||
flushFrames()
|
||||
expect(scrollTopCache.get(logKey)).toBe(0)
|
||||
Object.defineProperty(node, 'clientHeight', { configurable: true, value: 200 })
|
||||
node.dispatchEvent(new Event('resize'))
|
||||
node.dispatchEvent(new Event('scroll'))
|
||||
expect(initial).not.toHaveBeenCalled()
|
||||
detach()
|
||||
detachOwner()
|
||||
expect(frames.size).toBe(0)
|
||||
})
|
||||
|
||||
it('advances the watermark on another file dirty/save update and restores the later scroll', () => {
|
||||
const { owner, store, logKey, text, identity } = readingFixture()
|
||||
const detachOwner = owner.attach()
|
||||
const node = measuredReadingNode(text)
|
||||
const options = { node, owner, cacheKey: logKey, job: identity, text }
|
||||
const detach = attachCheckReadingViewport(options)
|
||||
store.setState({
|
||||
openFiles: [
|
||||
...store.getState().openFiles,
|
||||
{ ...readingFile(), id: 'other', mode: 'edit', isDirty: true }
|
||||
]
|
||||
})
|
||||
store.setState({
|
||||
openFiles: store
|
||||
.getState()
|
||||
.openFiles.map((file) => (file.id === 'other' ? { ...file, isDirty: false } : file))
|
||||
})
|
||||
node.scrollTop = 450
|
||||
node.dispatchEvent(new Event('scroll'))
|
||||
detach()
|
||||
node.scrollTop = 0
|
||||
const remount = attachCheckReadingViewport(options)
|
||||
expect(node.scrollTop).toBe(450)
|
||||
remount()
|
||||
detachOwner()
|
||||
})
|
||||
|
||||
it('fences callbacks published before the observer and never rewinds on a reentrant notification', () => {
|
||||
const { owner, store, file, logKey, text, identity } = readingFixture()
|
||||
const node = measuredReadingNode(text)
|
||||
@@ -315,30 +162,6 @@ describe('owned reading positions', () => {
|
||||
detachOwner()
|
||||
})
|
||||
|
||||
it('cancels first-payload and job intent with reading input, but ignores bubbled control clicks', () => {
|
||||
const { owner, file, text, identity } = readingFixture()
|
||||
reconcileCheckDetailsReadingCache(file.id, undefined, file.checkRunDetails!, () => true)
|
||||
const unclaimed = checkReadingIntentKey(
|
||||
checkDetailsReadingKey(file.id, null, 'context'),
|
||||
identity
|
||||
)
|
||||
const node = measuredReadingNode(text)
|
||||
const button = document.createElement('button')
|
||||
node.append(button)
|
||||
const detachOwner = owner.attach()
|
||||
const detach = attachCheckReadingViewport({
|
||||
node,
|
||||
owner,
|
||||
cacheKey: checkReadingOuterKey(owner.key)
|
||||
})
|
||||
button.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
expect(scrollTopCache.get(unclaimed)).toBe(-1)
|
||||
node.dispatchEvent(new WheelEvent('wheel'))
|
||||
expect(scrollTopCache.has(unclaimed)).toBe(false)
|
||||
detach()
|
||||
detachOwner()
|
||||
})
|
||||
|
||||
it('does no hashing, marker work, Range work or frame scheduling during 100 scroll/unrelated notifications', () => {
|
||||
const { owner, store, logKey, text, identity } = readingFixture()
|
||||
const node = measuredReadingNode(text)
|
||||
@@ -367,46 +190,4 @@ describe('owned reading positions', () => {
|
||||
detachOwner()
|
||||
expect(remove.mock.calls.map(([name]) => name)).toEqual(add.mock.calls.map(([name]) => name))
|
||||
})
|
||||
|
||||
it('shares the real 20-entry LRU across two five-job panes and ordinary editor keys', () => {
|
||||
const { store, file, identity, text } = readingFixture()
|
||||
const first = createCheckDetailsReadingOwner(store, file.id, 'left', 'context')
|
||||
const second = createCheckDetailsReadingOwner(store, file.id, 'right', 'context')
|
||||
for (let index = 0; index < 8; index++) {
|
||||
setWithLRU(scrollTopCache, `ordinary-${index}`, index)
|
||||
}
|
||||
for (const owner of [first, second]) {
|
||||
setWithLRU(scrollTopCache, checkReadingOuterKey(owner.key), 320)
|
||||
for (let index = 0; index < 5; index++) {
|
||||
setWithLRU(
|
||||
scrollTopCache,
|
||||
checkReadingLogKey(owner.key, `${identity}-${index}`, checkLogDigest(text)),
|
||||
180
|
||||
)
|
||||
setWithLRU(scrollTopCache, checkReadingStepsKey(owner.key, `${identity}-${index}`, true), 0)
|
||||
expect(scrollTopCache.size).toBeLessThanOrEqual(20)
|
||||
}
|
||||
}
|
||||
expect(scrollTopCache.size).toBe(20)
|
||||
expect(scrollTopCache.has(checkReadingOuterKey(first.key))).toBe(false)
|
||||
expect([...scrollTopCache.values()].every((value) => typeof value === 'number')).toBe(true)
|
||||
expect([...scrollTopCache.keys()].some((key) => key.includes(text))).toBe(false)
|
||||
const detachOwner = first.attach()
|
||||
const node = measuredReadingNode()
|
||||
const initial = vi.fn(() => 600)
|
||||
const detach = attachCheckReadingViewport({
|
||||
node,
|
||||
owner: first,
|
||||
cacheKey: checkReadingOuterKey(first.key),
|
||||
initial
|
||||
})
|
||||
expect(node.scrollTop).toBe(0)
|
||||
expect(initial).not.toHaveBeenCalled()
|
||||
detach()
|
||||
detachOwner()
|
||||
})
|
||||
})
|
||||
|
||||
function fireReading(node: HTMLElement): void {
|
||||
node.dispatchEvent(new WheelEvent('wheel'))
|
||||
}
|
||||
|
||||
-50
@@ -262,53 +262,3 @@ it('discards an in-flight read when the other area of its path changes', async (
|
||||
expect(hook.result.current.sections[0]?.modifiedContent).toBe('current')
|
||||
expect(hook.result.current.generation).toBe(generation)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'order',
|
||||
change: (review: Review): Review => ({ ...review, status: review.status.toReversed() })
|
||||
},
|
||||
{
|
||||
name: 'worktree owner',
|
||||
change: (review: Review): Review => ({
|
||||
...review,
|
||||
file: { ...review.file, worktreeId: 'folder:other' }
|
||||
})
|
||||
}
|
||||
])('uses the full reset when $name changes', async ({ name, change }) => {
|
||||
mocks.fetch.mockResolvedValue(textResult())
|
||||
const review: Review = { status: entries(3), file, viewStateKey: `fallback-${name}` }
|
||||
const hook = renderReview(review)
|
||||
await flush()
|
||||
const before = hook.result.current
|
||||
mocks.fetch.mockClear()
|
||||
const next = change(review)
|
||||
hook.rerender(next)
|
||||
await flush()
|
||||
expect(hook.result.current.generation).toBe(before.generation + 1)
|
||||
expect(hook.result.current.sections[0]).not.toBe(before.sections[0])
|
||||
expect(mocks.fetch.mock.calls[0]?.[0].file.worktreeId).toBe(next.file.worktreeId)
|
||||
})
|
||||
|
||||
it('reloads a row whose submodule admission changes under an unchanged cache signature', async () => {
|
||||
mocks.fetch.mockResolvedValue(textResult())
|
||||
const review: Review = {
|
||||
status: [{ ...entries(1)[0]!, added: undefined }],
|
||||
file,
|
||||
viewStateKey: 'submodule-policy'
|
||||
}
|
||||
const hook = renderReview(review)
|
||||
await flush()
|
||||
hook.rerender({
|
||||
...review,
|
||||
status: [
|
||||
{
|
||||
...review.status[0]!,
|
||||
submodule: { commitChanged: true, trackedChanges: false, untrackedChanges: false }
|
||||
}
|
||||
]
|
||||
})
|
||||
await flush()
|
||||
expect(hook.result.current.sections[0]?.loadOnDemand).toBe(false)
|
||||
expect(hook.result.current.sections[0]?.modifiedContent).toBe('after\n')
|
||||
})
|
||||
|
||||
-139
@@ -285,22 +285,6 @@ describe('combined diff discarded-result analysis budget', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps reloading a visible section while external writes never stop', async () => {
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
||||
mocks.fetch.mockResolvedValue(textResult('base', 'edited'))
|
||||
const hook = await startViewer()
|
||||
hook.result.current.registry.renderedIndicesRef.current.add(0)
|
||||
// Watch batches every 100 ms: a restarting 300 ms coalesce would never fire.
|
||||
for (let elapsed = 0; elapsed < 3_000; elapsed += 100) {
|
||||
hook.result.current.retry.requestSectionReload(0)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
})
|
||||
await flushLoads()
|
||||
}
|
||||
expect(mocks.fetch.mock.calls.length).toBeGreaterThanOrEqual(4)
|
||||
})
|
||||
|
||||
it('commits slow refetches while external writes never stop', async () => {
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
||||
let version = 0
|
||||
@@ -393,100 +377,12 @@ describe('combined diff discarded-result analysis budget', () => {
|
||||
expect(mocks.fetch).toHaveBeenCalledTimes(2)
|
||||
expect(reads.count()).toBe(0)
|
||||
})
|
||||
|
||||
it.each(['offscreen', 'collapsed'])(
|
||||
'does not force a %s stale section to reload',
|
||||
async (visibility) => {
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
||||
const body = 'obsolete hidden diff\n'.padEnd(4096, 'x')
|
||||
const pending = deferredResult()
|
||||
mocks.fetch.mockReturnValueOnce(pending.promise)
|
||||
const hook = await startViewer()
|
||||
if (visibility === 'collapsed') {
|
||||
act(() => {
|
||||
hook.result.current.setSections((sections) =>
|
||||
sections.map((section) => ({ ...section, collapsed: true }))
|
||||
)
|
||||
})
|
||||
hook.result.current.registry.renderedIndicesRef.current.add(0)
|
||||
}
|
||||
const sections = hook.result.current.sections
|
||||
hook.result.current.retry.requestSectionReload(0)
|
||||
const reads = countBodyReads(body)
|
||||
await settle(pending, textResult(body))
|
||||
reads.restore()
|
||||
expect(hook.result.current.sections).toBe(sections)
|
||||
expect(hook.result.current.registry.loadedIndicesRef.current.size).toBe(0)
|
||||
expect(hook.result.current.registry.loadingIndicesRef.current.size).toBe(0)
|
||||
expect(hook.result.current.registry.sectionLoadTokensRef.current.get(0)).toBe(2)
|
||||
expect(hook.result.current.registry.reloadTimersRef.current.size).toBe(0)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
})
|
||||
expect(mocks.fetch).toHaveBeenCalledOnce()
|
||||
expect(reads.count()).toBe(0)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('combined diff live result compatibility', () => {
|
||||
const body = 'original body\n'.padEnd(512, 'a')
|
||||
const modified = 'modified body\n'.padEnd(128, 'b')
|
||||
const legacy = textResult(body, modified)
|
||||
const metadata: GitDiffResult = {
|
||||
...legacy,
|
||||
largeDiffRenderLimit: getLargeDiffRenderLimit(legacy)
|
||||
}
|
||||
const hostLimitMetadata: GitDiffTextResult = {
|
||||
...textResult(''),
|
||||
largeDiffRenderLimit: getLargeDiffRenderLimit(textResult('\n'.repeat(120_001)))
|
||||
}
|
||||
const responseCases: [string, GitDiffResult, boolean][] = [
|
||||
['legacy', legacy, true],
|
||||
['native wire shape', structuredClone(legacy), true],
|
||||
['relay wire shape', structuredClone(legacy), true],
|
||||
['supplied metadata', metadata, false],
|
||||
[
|
||||
'binary',
|
||||
{
|
||||
kind: 'binary',
|
||||
originalContent: body,
|
||||
modifiedContent: modified,
|
||||
originalIsBinary: true,
|
||||
modifiedIsBinary: false,
|
||||
isImage: true,
|
||||
mimeType: 'application/pdf',
|
||||
modifiedDeleted: true
|
||||
},
|
||||
false
|
||||
],
|
||||
['empty', textResult(''), true],
|
||||
['line-limited legacy', textResult('\n'.repeat(120_001)), true],
|
||||
['character-limited legacy', textResult('x'.repeat(6_000_001)), false],
|
||||
['native limit metadata', structuredClone(hostLimitMetadata), false],
|
||||
['relay limit metadata', structuredClone(hostLimitMetadata), false]
|
||||
]
|
||||
|
||||
it.each(responseCases)('preserves the complete %s result', async (_name, response, scansBody) => {
|
||||
const pending = deferredResult()
|
||||
mocks.fetch.mockReturnValueOnce(pending.promise)
|
||||
const hook = await startViewer()
|
||||
const initial = hook.result.current.sections[0]!
|
||||
const reads = countBodyReads(body)
|
||||
await settle(pending, response)
|
||||
reads.restore()
|
||||
expect(hook.result.current.sections).toEqual([expectedSection(initial, response)])
|
||||
if (hook.result.current.sections[0]?.largeDiffRenderLimit?.limited) {
|
||||
expect(hook.result.current.sections[0]?.diffResult).not.toBe(response)
|
||||
} else {
|
||||
expect(hook.result.current.sections[0]?.diffResult).toBe(response)
|
||||
}
|
||||
expect(hook.result.current.registry.loadedIndicesRef.current.has(0)).toBe(true)
|
||||
expect(hook.result.current.registry.loadingIndicesRef.current.size).toBe(0)
|
||||
expect(hook.result.current.sectionHeights).toEqual({})
|
||||
expect(mocks.fetch).toHaveBeenCalledOnce()
|
||||
expect(reads.count()).toBe(scansBody && response.originalContent === body ? body.length : 0)
|
||||
})
|
||||
|
||||
it('preserves section identity on unchanged reload and remeasures changed content once', async () => {
|
||||
const first = deferredResult()
|
||||
@@ -523,27 +419,6 @@ describe('combined diff live result compatibility', () => {
|
||||
expect(mocks.fetch).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it.each([new Error(' original remote failure '), new Error(' '), 'rejected value'])(
|
||||
'keeps the existing rejection message for %s',
|
||||
async (error) => {
|
||||
const pending = deferredResult()
|
||||
mocks.fetch.mockReturnValueOnce(pending.promise)
|
||||
const hook = await startViewer()
|
||||
const initial = hook.result.current.sections[0]!
|
||||
await act(async () => {
|
||||
pending.reject(error)
|
||||
await Promise.resolve()
|
||||
})
|
||||
const message =
|
||||
error instanceof Error && error.message.trim() ? error.message : 'Unable to load diff.'
|
||||
expect(hook.result.current.sections).toEqual([
|
||||
expectedSection(initial, textResult(''), message)
|
||||
])
|
||||
expect(hook.result.current.registry.loadedIndicesRef.current.has(0)).toBe(true)
|
||||
expect(hook.result.current.registry.loadingIndicesRef.current.size).toBe(0)
|
||||
}
|
||||
)
|
||||
|
||||
it('preserves the actual 30 second deadline and clears its timer after failure', async () => {
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
||||
const pending = deferredResult()
|
||||
@@ -608,18 +483,4 @@ describe('combined diff live result compatibility', () => {
|
||||
expect(hook.result.current.sections).toEqual([expectedSection(sections[0]!, legacy)])
|
||||
expect(mocks.fetch).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('preserves a synchronous fetch failure', async () => {
|
||||
const failure = new Error('original synchronous failure')
|
||||
mocks.fetch.mockImplementationOnce(() => {
|
||||
throw failure
|
||||
})
|
||||
const hook = await startViewer()
|
||||
expect(hook.result.current.sections[0]?.error).toBe(failure.message)
|
||||
expect(hook.result.current.sections[0]?.diffResult).toEqual(textResult(''))
|
||||
expect(hook.result.current.sections[0]?.largeDiffRenderLimit).toBeNull()
|
||||
expect(hook.result.current.sections[0]?.loading).toBe(false)
|
||||
expect(hook.result.current.registry.loadedIndicesRef.current.has(0)).toBe(true)
|
||||
expect(hook.result.current.registry.loadingIndicesRef.current.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user