mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
test: remove low-value navigation and recovery coverage
This commit is contained in:
@@ -20,7 +20,6 @@
|
||||
5 test-no-in-test-import-actual:src/main/ssh/config/ssh-config-loader-regression.test.ts
|
||||
2 test-no-in-test-import-actual:src/main/ssh/connection/ssh-connection-auth-fallback.test.ts
|
||||
1 test-no-in-test-import-actual:src/main/window/file-drop/dropped-path-preparation.test.ts
|
||||
1 test-no-in-test-import-actual:src/main/workspaces/removal/local-worktree-removal-recovery.test.ts
|
||||
3 test-no-in-test-import-actual:src/renderer/src/components/terminal-pane/snapshots/remote-snapshot-alt-screen-host-chain.test.ts
|
||||
1 test-no-polling-loop:src/main/browser/cookies/browser-cookie-import-concurrency.test.ts
|
||||
1 test-no-polling-loop:src/main/codex/hooks/reconciliation/codex-hook-toggle-off-wins.test.ts
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
killAllProcessesForWorktreeMock
|
||||
} from './worktrees-test-module-mocks'
|
||||
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
|
||||
import { mockKnownFeatureWorktree, mockSelectedWslProjectRuntime } from './worktrees-test-fixtures'
|
||||
import { mockKnownFeatureWorktree } from './worktrees-test-fixtures'
|
||||
|
||||
afterEach(() => {
|
||||
_resetWorktreeRemovalLifetimeForTests()
|
||||
@@ -402,72 +402,3 @@ it.each(['ipc', 'runtime'] as const)(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['ipc', 'runtime'] as const)(
|
||||
'%s synchronous WSL removal retains main shutdown behavior without a journal lease',
|
||||
async (entry) => {
|
||||
const runtime = setupWorktreeHandlers()
|
||||
mockSelectedWslProjectRuntime()
|
||||
const rows = mockKnownFeatureWorktree()
|
||||
const finish = vi.fn(async () => {})
|
||||
const gate = { finish: vi.fn(async () => {}) }
|
||||
const acquireWatcherRemoval = vi.fn(async (_path: string) => gate)
|
||||
runtime.acquireFileWatcherRemoval = acquireWatcherRemoval
|
||||
const signal = worktreeRemovalLifetime.signal
|
||||
removeWorktreeMock.mockImplementation(async (_repo, _path, _force, options) => {
|
||||
expect(options).not.toHaveProperty('checkoutDeleteSignal', expect.anything())
|
||||
expect(options).not.toHaveProperty('signal', expect.anything())
|
||||
await worktreeRemovalLifetime.close()
|
||||
expect(signal.aborted).toBe(true)
|
||||
return {}
|
||||
})
|
||||
const result =
|
||||
entry === 'ipc'
|
||||
? Promise.resolve(
|
||||
handlers['worktrees:remove'](null, {
|
||||
worktreeId: 'repo-1::/workspace/feature-wt',
|
||||
deleteBranch: false
|
||||
})
|
||||
)
|
||||
: removeRuntimeRegisteredLocalWorktree({
|
||||
repo: {
|
||||
id: 'repo-1',
|
||||
path: '/workspace/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: '',
|
||||
addedAt: 0
|
||||
},
|
||||
target: {
|
||||
id: 'repo-1::/workspace/feature-wt',
|
||||
repoId: 'repo-1',
|
||||
path: '/workspace/feature-wt'
|
||||
},
|
||||
registeredWorktree: rows[1],
|
||||
removedPushTarget: undefined,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the existing store fixture owns all metadata consumed by this real removal boundary.
|
||||
store: store as unknown as RuntimeStore,
|
||||
localOptions: { wslDistro: 'Ubuntu' },
|
||||
hasLocalOptions: true,
|
||||
force: false,
|
||||
runHooks: false,
|
||||
allowFailedArchiveHook: false,
|
||||
allowUnverifiedPtyStop: false,
|
||||
deleteBranch: false,
|
||||
acquireWatcherRemoval,
|
||||
stopPtys: async () => {},
|
||||
closeWatchers: async () => {},
|
||||
preserveBranchHead: (value) => value ?? {},
|
||||
finishRemoval: finish,
|
||||
onRemoved: () => {},
|
||||
publish: () => {}
|
||||
})
|
||||
await expect(result).resolves.toMatchObject({})
|
||||
if (entry === 'runtime') {
|
||||
expect(finish).toHaveBeenCalledOnce()
|
||||
} else {
|
||||
expect(store.removeWorktreeMeta).toHaveBeenCalled()
|
||||
}
|
||||
expect(gate.finish).toHaveBeenCalledWith(true)
|
||||
expect(removeWorktreeMock).toHaveBeenCalledOnce()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { externalChromiumCommandArguments } from './external-chromium-command-arguments'
|
||||
|
||||
describe('externalChromiumCommandArguments', () => {
|
||||
it('leaves cookie delete unmapped because agent-browser can only clear the whole jar', () => {
|
||||
expect(externalChromiumCommandArguments('browserCookieDelete', { name: 'sid' })).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
const { prove, missing, remove } = vi.hoisted(() => ({
|
||||
prove: vi.fn(),
|
||||
missing: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}))
|
||||
vi.mock('./local-worktree-filesystem', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
removeLocalWorktreePath: remove
|
||||
}))
|
||||
vi.mock('./worktree-removal-safety', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
canSafelyRemoveOrphanedWorktreeDirectory: prove,
|
||||
isWorktreePathMissing: missing
|
||||
}))
|
||||
import { cleanupLocalOrphanedWorktreeDirectory } from './local-orphaned-worktree-cleanup'
|
||||
beforeEach(() => {
|
||||
prove.mockReset().mockResolvedValue(true)
|
||||
missing.mockReset().mockResolvedValue(true)
|
||||
remove.mockReset().mockResolvedValue(undefined)
|
||||
})
|
||||
it.each(['proof', 'watcher', 'delete', 'success'] as const)(
|
||||
'does not convert cancelled orphan cleanup into absent-path success at %s',
|
||||
async (phase) => {
|
||||
const controller = new AbortController()
|
||||
if (phase === 'proof') {
|
||||
prove.mockImplementationOnce(async () => {
|
||||
controller.abort()
|
||||
return true
|
||||
})
|
||||
}
|
||||
const watcher = vi.fn(async () => {
|
||||
if (phase === 'watcher') {
|
||||
controller.abort()
|
||||
}
|
||||
})
|
||||
remove.mockImplementationOnce(async () => {
|
||||
controller.abort()
|
||||
if (phase === 'delete') {
|
||||
throw controller.signal.reason
|
||||
}
|
||||
})
|
||||
const pending = cleanupLocalOrphanedWorktreeDirectory(
|
||||
'/repo',
|
||||
'/repo/feature',
|
||||
{},
|
||||
watcher,
|
||||
controller.signal
|
||||
)
|
||||
if (phase === 'success') {
|
||||
await expect(pending).resolves.toBeUndefined()
|
||||
expect(missing).toHaveBeenCalledOnce()
|
||||
} else {
|
||||
const error = await pending.catch((error: unknown) => error)
|
||||
expect(error).toBe(controller.signal.reason)
|
||||
expect(missing).not.toHaveBeenCalled()
|
||||
}
|
||||
if (phase === 'proof' || phase === 'watcher') {
|
||||
expect(remove).not.toHaveBeenCalled()
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,268 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { NESTED_WORKTREE_REMOVAL_PREFIX } from '../../../shared/worktree/removal/nested-removal'
|
||||
import type * as LocalWorktreeFilesystem from './local-worktree-filesystem'
|
||||
|
||||
const { gitExecFileAsyncMock, listWorktreesStrictMock, removeLocalWorktreePathMock, hostRmMock } =
|
||||
vi.hoisted(() => ({
|
||||
gitExecFileAsyncMock: vi.fn(),
|
||||
listWorktreesStrictMock: vi.fn(),
|
||||
removeLocalWorktreePathMock: vi.fn(),
|
||||
hostRmMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../../git/command-runner/git-exec-file', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
gitExecFileAsync: gitExecFileAsyncMock
|
||||
}))
|
||||
|
||||
vi.mock('./local-worktree-filesystem', () => ({
|
||||
removeLocalWorktreePath: removeLocalWorktreePathMock
|
||||
}))
|
||||
|
||||
vi.mock('../../filesystem/asar-transparent-fs', () => ({ rm: hostRmMock }))
|
||||
|
||||
vi.mock('../../git/worktrees/catalog/worktree-listing', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
listWorktreesStrict: listWorktreesStrictMock
|
||||
}))
|
||||
|
||||
import {
|
||||
recoverPartialLocalWorktreeRemoval,
|
||||
removeStaleLocalWorktreeRegistration
|
||||
} from './local-worktree-removal-recovery'
|
||||
|
||||
async function withPlatform<T>(platform: NodeJS.Platform, fn: () => Promise<T>): Promise<T> {
|
||||
const original = process.platform
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: original })
|
||||
}
|
||||
}
|
||||
|
||||
describe('recoverPartialLocalWorktreeRemoval', () => {
|
||||
beforeEach(() => {
|
||||
gitExecFileAsyncMock.mockReset()
|
||||
listWorktreesStrictMock.mockReset()
|
||||
removeLocalWorktreePathMock.mockReset()
|
||||
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
|
||||
listWorktreesStrictMock.mockResolvedValue([])
|
||||
removeLocalWorktreePathMock.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('rejects native recovery without pruning or another pass when deletion is cancelled', async () => {
|
||||
const native = await vi.importActual<typeof LocalWorktreeFilesystem>(
|
||||
'./local-worktree-filesystem'
|
||||
)
|
||||
removeLocalWorktreePathMock.mockImplementationOnce(native.removeLocalWorktreePath)
|
||||
const controller = new AbortController()
|
||||
const closeWatcher = vi.fn().mockResolvedValue(undefined)
|
||||
hostRmMock.mockImplementationOnce(async () => {
|
||||
controller.abort()
|
||||
throw Object.assign(new Error('file held open'), { code: 'EBUSY' })
|
||||
})
|
||||
await withPlatform('win32', async () => {
|
||||
await expect(
|
||||
recoverPartialLocalWorktreeRemoval({
|
||||
error: new Error('Git deregistered but left the checkout'),
|
||||
force: true,
|
||||
canonicalWorktreePath: 'C:/repo/worktree/feature',
|
||||
repoPath: 'C:/repo',
|
||||
localWorktreeGitOptions: {},
|
||||
registeredWorktree: { branch: 'refs/heads/feature', head: 'abc123' },
|
||||
deleteBranch: false,
|
||||
closeWatcher,
|
||||
signal: controller.signal
|
||||
})
|
||||
).rejects.toThrow(/aborted/i)
|
||||
expect(removeLocalWorktreePathMock).toHaveBeenCalledWith(
|
||||
'C:/repo/worktree/feature',
|
||||
{},
|
||||
controller.signal
|
||||
)
|
||||
expect(closeWatcher).toHaveBeenCalledOnce()
|
||||
expect(hostRmMock).toHaveBeenCalledOnce()
|
||||
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['inventory', 'watcher', 'delete', 'success'] as const)(
|
||||
'keeps partial-removal cancellation distinct from success at %s',
|
||||
async (phase) => {
|
||||
const controller = new AbortController()
|
||||
if (phase === 'inventory') {
|
||||
listWorktreesStrictMock.mockImplementationOnce(async () => {
|
||||
controller.abort()
|
||||
return []
|
||||
})
|
||||
}
|
||||
const closeWatcher = vi.fn(async () => {
|
||||
if (phase === 'watcher') {
|
||||
controller.abort()
|
||||
}
|
||||
})
|
||||
removeLocalWorktreePathMock.mockImplementationOnce(async () => {
|
||||
controller.abort()
|
||||
if (phase === 'delete') {
|
||||
throw controller.signal.reason
|
||||
}
|
||||
})
|
||||
const pending = recoverPartialLocalWorktreeRemoval({
|
||||
error: new Error('Git deregistered but left files'),
|
||||
force: true,
|
||||
canonicalWorktreePath: '/repo/feature',
|
||||
repoPath: '/repo',
|
||||
localWorktreeGitOptions: {},
|
||||
registeredWorktree: { branch: 'feature', head: 'abc' },
|
||||
deleteBranch: true,
|
||||
closeWatcher,
|
||||
signal: controller.signal
|
||||
})
|
||||
if (phase === 'success') {
|
||||
await expect(pending).resolves.toBeDefined()
|
||||
} else {
|
||||
const error = await pending.catch((error: unknown) => error)
|
||||
expect(error).toBe(controller.signal.reason)
|
||||
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
|
||||
}
|
||||
if (phase === 'inventory' || phase === 'watcher') {
|
||||
expect(removeLocalWorktreePathMock).not.toHaveBeenCalled()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('does not recursively delete while watcher teardown remains unconfirmed', async () => {
|
||||
await withPlatform('win32', async () => {
|
||||
const watcherError = new Error('file watcher process did not exit after termination deadline')
|
||||
|
||||
await expect(
|
||||
recoverPartialLocalWorktreeRemoval({
|
||||
error: new Error('git worktree remove failed'),
|
||||
force: true,
|
||||
canonicalWorktreePath: 'C:/repo/worktree/feature',
|
||||
repoPath: 'C:/repo',
|
||||
localWorktreeGitOptions: {},
|
||||
registeredWorktree: { branch: 'refs/heads/feature', head: 'abc123' },
|
||||
deleteBranch: true,
|
||||
closeWatcher: vi.fn().mockRejectedValue(watcherError)
|
||||
})
|
||||
).rejects.toBe(watcherError)
|
||||
|
||||
expect(removeLocalWorktreePathMock).not.toHaveBeenCalled()
|
||||
expect(gitExecFileAsyncMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not delete a checkout that now contains a registered worktree', async () => {
|
||||
await withPlatform('darwin', async () => {
|
||||
listWorktreesStrictMock.mockResolvedValue([
|
||||
{
|
||||
path: '/Users/dev/orca/workspaces/feature/nested',
|
||||
head: 'def456',
|
||||
branch: 'refs/heads/nested',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
const closeWatcher = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
await expect(
|
||||
recoverPartialLocalWorktreeRemoval({
|
||||
error: Object.assign(new Error('git worktree remove failed'), {
|
||||
stderr:
|
||||
"error: failed to delete '/Users/dev/orca/workspaces/feature': Directory not empty"
|
||||
}),
|
||||
force: true,
|
||||
canonicalWorktreePath: '/Users/dev/orca/workspaces/feature',
|
||||
repoPath: '/Users/dev/repo',
|
||||
localWorktreeGitOptions: {},
|
||||
registeredWorktree: { branch: 'refs/heads/feature', head: 'abc123' },
|
||||
deleteBranch: true,
|
||||
closeWatcher
|
||||
})
|
||||
).rejects.toThrow(NESTED_WORKTREE_REMOVAL_PREFIX)
|
||||
expect(closeWatcher).not.toHaveBeenCalled()
|
||||
expect(removeLocalWorktreePathMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not recover a locked registration after a filesystem removal failure', async () => {
|
||||
await withPlatform('win32', async () => {
|
||||
const registeredWorktree = {
|
||||
branch: 'refs/heads/feature',
|
||||
head: 'abc123',
|
||||
locked: true,
|
||||
lockReason: 'active agent'
|
||||
}
|
||||
const closeWatcher = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
await expect(
|
||||
recoverPartialLocalWorktreeRemoval({
|
||||
error: Object.assign(new Error('git worktree remove failed'), {
|
||||
stderr: 'error: failed to delete deep/file.txt: Filename too long'
|
||||
}),
|
||||
force: true,
|
||||
canonicalWorktreePath: 'C:/workspaces/feature',
|
||||
repoPath: 'C:/repo',
|
||||
localWorktreeGitOptions: {},
|
||||
registeredWorktree,
|
||||
deleteBranch: true,
|
||||
closeWatcher
|
||||
})
|
||||
).rejects.toThrow('Worktree is locked by Git. Lock reason: active agent')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeStaleLocalWorktreeRegistration', () => {
|
||||
beforeEach(() => {
|
||||
gitExecFileAsyncMock.mockReset()
|
||||
listWorktreesStrictMock.mockReset()
|
||||
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
|
||||
listWorktreesStrictMock.mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('does not override a locked missing registration', async () => {
|
||||
await expect(
|
||||
removeStaleLocalWorktreeRegistration({
|
||||
canonicalWorktreePath: 'C:/workspaces/feature',
|
||||
repoPath: 'C:/repo',
|
||||
localWorktreeGitOptions: {},
|
||||
registeredWorktree: {
|
||||
branch: 'refs/heads/feature',
|
||||
head: 'abc123',
|
||||
locked: true,
|
||||
lockReason: 'active agent'
|
||||
},
|
||||
deleteBranch: true
|
||||
})
|
||||
).rejects.toThrow('Worktree is locked by Git')
|
||||
})
|
||||
|
||||
it('does not report success when prune leaves the registration behind', async () => {
|
||||
listWorktreesStrictMock.mockResolvedValue([
|
||||
{
|
||||
path: 'C:/workspaces/feature',
|
||||
head: 'abc123',
|
||||
branch: 'refs/heads/feature',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
|
||||
await expect(
|
||||
removeStaleLocalWorktreeRegistration({
|
||||
canonicalWorktreePath: 'C:/workspaces/feature',
|
||||
repoPath: 'C:/repo',
|
||||
localWorktreeGitOptions: {},
|
||||
registeredWorktree: { branch: 'refs/heads/feature', head: 'abc123' },
|
||||
deleteBranch: true
|
||||
})
|
||||
).rejects.toThrow('Git still has stale worktree registration')
|
||||
})
|
||||
})
|
||||
+2
-5
@@ -116,10 +116,7 @@ it('rejects a live terminal row on a different host before workspace activation'
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ label: 'live', activate: activateWorktreeAgentPane },
|
||||
{ label: 'retained', activate: activateRetainedAgentRow }
|
||||
])('opens a $label paired chat from its physical SSH workspace', ({ activate }) => {
|
||||
it('opens a paired chat from its physical SSH workspace', () => {
|
||||
const worktree = {
|
||||
...makeCreatedAgentWorktree(),
|
||||
hostId: 'ssh:server' as const,
|
||||
@@ -148,7 +145,7 @@ it.each([
|
||||
]
|
||||
}
|
||||
})
|
||||
activate(
|
||||
activateRetainedAgentRow(
|
||||
worktree.id,
|
||||
'native-B',
|
||||
structuredAgentSessionPaneKey('native-B', 'session-B'),
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { useNativeChatSideChat } from '../../messages/actions/use-native-chat-side-chat'
|
||||
import type { NativeChatPaneFork } from '../../messages/actions/use-native-chat-fork'
|
||||
import type { NativeChatSideChatForkPoint } from '../../messages/actions/native-chat-side-chat-fork-point'
|
||||
import { nativeChatForkCommand } from './use-native-chat-structured-composer-transport'
|
||||
|
||||
function paneFork(ids: string[]): NativeChatPaneFork {
|
||||
const rowAction = {
|
||||
support: { kind: 'known', fork: 'checkpoint' },
|
||||
pendingItemId: null,
|
||||
request: vi.fn()
|
||||
} as const
|
||||
return {
|
||||
...rowAction,
|
||||
contextFloorSequence: 0,
|
||||
forkableItemIds: new Set(ids),
|
||||
rowAction,
|
||||
forkLatest: vi.fn(() => 'started' as const)
|
||||
}
|
||||
}
|
||||
|
||||
function runForkWhileReplying(fork: NativeChatPaneFork, point: NativeChatSideChatForkPoint) {
|
||||
const { result } = renderHook(() => useNativeChatSideChat(fork))
|
||||
act(() => result.current?.reportForkPoint(point))
|
||||
return nativeChatForkCommand({ fork, sideChat: result.current }, true, 'codex')?.run()
|
||||
}
|
||||
|
||||
it('forks a replying chat through its newest finished response', () => {
|
||||
const fork = paneFork(['reply-1', 'reply-2-streaming'])
|
||||
|
||||
const notice = runForkWhileReplying(fork, { kind: 'ready', throughItemId: 'reply-1' })
|
||||
|
||||
expect(notice).toBeNull()
|
||||
expect(fork.request).toHaveBeenCalledWith('reply-1')
|
||||
expect(fork.forkLatest).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses without forking when the running reply is the only one', () => {
|
||||
const fork = paneFork(['reply-1-streaming'])
|
||||
|
||||
const notice = runForkWhileReplying(fork, { kind: 'reply-running' })
|
||||
|
||||
expect(notice).toEqual(expect.any(String))
|
||||
expect(fork.request).not.toHaveBeenCalled()
|
||||
expect(fork.forkLatest).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -127,78 +127,6 @@ describe('NativeChatQuestionCard', () => {
|
||||
pressKey({ key: 'Enter' }, rows[1]!)
|
||||
expect(onAnswer).toHaveBeenCalledWith([{ indices: [1], other: '' }])
|
||||
})
|
||||
it('opens with focus on option 1 without picking it, and Skip turns into Submit once picked', () => {
|
||||
vi.useFakeTimers()
|
||||
container.setAttribute('data-native-chat-root', 'true')
|
||||
act(() => {
|
||||
root.render(
|
||||
<NativeChatQuestionCard
|
||||
prompt={tabsOrSpaces}
|
||||
onAnswer={() => {}}
|
||||
onCancel={() => {}}
|
||||
shouldFocus
|
||||
/>
|
||||
)
|
||||
})
|
||||
const rows = container.querySelectorAll<HTMLButtonElement>('button[aria-pressed]')
|
||||
expect(document.activeElement).toBe(rows[0])
|
||||
expect([...rows].map((row) => row.getAttribute('aria-pressed'))).toEqual(['false', 'false'])
|
||||
expect(getByRole(container, 'button', { name: 'Skip' })).toBeTruthy()
|
||||
|
||||
act(() => rows[1]!.click())
|
||||
expect([...rows].map((row) => row.getAttribute('aria-pressed'))).toEqual(['false', 'true'])
|
||||
// The Enter hint is aria-hidden, so the button is still named "Submit".
|
||||
expect(getByRole(container, 'button', { name: 'Submit' })).toBeTruthy()
|
||||
expect(queryByRole(container, 'button', { name: 'Skip' })).toBeNull()
|
||||
})
|
||||
|
||||
it('confirms with Enter on the chosen single-select row instead of deselecting it', () => {
|
||||
vi.useFakeTimers()
|
||||
container.setAttribute('data-native-chat-root', 'true')
|
||||
const onAnswer = vi.fn()
|
||||
act(() => {
|
||||
root.render(
|
||||
<NativeChatQuestionCard
|
||||
prompt={{
|
||||
questions: [
|
||||
tabsOrSpaces.questions[0]!,
|
||||
{
|
||||
question: 'Two or four?',
|
||||
header: 'Width',
|
||||
multiSelect: false,
|
||||
options: [{ label: 'Two' }, { label: 'Four' }]
|
||||
}
|
||||
]
|
||||
}}
|
||||
onAnswer={onAnswer}
|
||||
onCancel={() => {}}
|
||||
shouldFocus
|
||||
/>
|
||||
)
|
||||
})
|
||||
const rows = (): HTMLButtonElement[] => [
|
||||
...container.querySelectorAll<HTMLButtonElement>('button[aria-pressed]')
|
||||
]
|
||||
act(() => rows()[0]!.click())
|
||||
act(() => vi.runAllTimers())
|
||||
expect(rows()[0]!.textContent).toContain('Two')
|
||||
|
||||
pressKey({ key: 'ArrowLeft' }, rows()[0]!)
|
||||
expect(rows()[0]!.textContent).toContain('Tabs')
|
||||
expect(rows()[0]!.getAttribute('aria-pressed')).toBe('true')
|
||||
expect(getByRole(container, 'button', { name: 'Next' })).toBeTruthy()
|
||||
act(() => rows()[0]!.focus())
|
||||
pressKey({ key: 'Enter' }, rows()[0]!)
|
||||
act(() => vi.runAllTimers())
|
||||
expect(rows()[0]!.textContent).toContain('Two')
|
||||
|
||||
pressKey({ key: 'ArrowLeft' }, rows()[0]!)
|
||||
expect(rows()[0]!.getAttribute('aria-pressed')).toBe('true')
|
||||
// Click still toggles the chosen row off.
|
||||
act(() => rows()[0]!.click())
|
||||
expect(rows()[0]!.getAttribute('aria-pressed')).toBe('false')
|
||||
expect(onAnswer).not.toHaveBeenCalled()
|
||||
})
|
||||
it('submits with a second Enter on the last step instead of deselecting the pick', () => {
|
||||
vi.useFakeTimers()
|
||||
container.setAttribute('data-native-chat-root', 'true')
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { loadNodeOniguruma } from '../syntax-highlighting/oniguruma-test-harness'
|
||||
import { createMarkdownTokensProvider } from './markdown-textmate-tokens'
|
||||
|
||||
vi.mock('../syntax-highlighting/oniguruma', () => ({ loadOniguruma: loadNodeOniguruma }))
|
||||
|
||||
describe('markdown tokenization', () => {
|
||||
it('colors frontmatter as YAML on first paint, then returns to markdown', async () => {
|
||||
const provider = await createMarkdownTokensProvider({
|
||||
editor: { defineTheme: vi.fn() },
|
||||
languages: { setTokensProvider: vi.fn() }
|
||||
})
|
||||
let state = provider.getInitialState()
|
||||
const tokens = ['---', 'name: "Orca"', 'enabled: true # flag', '---', '# Heading'].map(
|
||||
(line) => {
|
||||
const result = provider.tokenize(line, state)
|
||||
state = result.endState
|
||||
return result.tokens
|
||||
}
|
||||
)
|
||||
|
||||
expect(tokens[1]).toContainEqual({ startIndex: 0, scopes: 'md.entity.name.tag.yaml' })
|
||||
expect(tokens[1]).toContainEqual({ startIndex: 6, scopes: 'md.string.quoted.double.yaml' })
|
||||
expect(tokens[2]).toContainEqual({ startIndex: 9, scopes: 'md.constant.language.boolean.yaml' })
|
||||
expect(tokens[2]).toContainEqual({ startIndex: 14, scopes: 'md.comment.line.number-sign.yaml' })
|
||||
expect(tokens[4]).toContainEqual({ startIndex: 2, scopes: 'md.entity.name.section.markdown' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user