mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
test: remove low-value mobile and host policy suites (#41436)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -2175,7 +2175,6 @@
|
||||
"src/main/ipc/wsl/wsl-host-automation-wake-launch.test.ts": 939,
|
||||
"src/main/ipc/wsl/wsl-host-handlers-user-actions.test.ts": 772,
|
||||
"src/main/ipc/wsl/wsl-host-visible-work-windows.test.ts": 39,
|
||||
"src/main/ipc/wsl/wsl-move-driver-handlers.test.ts": 432,
|
||||
"src/main/ipc/wsl/wsl-move-handlers-setting-off.test.ts": 724,
|
||||
"src/main/ipc/wsl/wsl-move-host-connect.test.ts": 984,
|
||||
"src/main/ipc/wsl/wsl-move-partial-undo.test.ts": 939,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { readPushNotificationsPreference } from '../../storage/preferences'
|
||||
import { shouldPresentNotificationOptIn } from './notification-opt-in-gate'
|
||||
|
||||
vi.mock('../../storage/preferences', () => ({
|
||||
readPushNotificationsPreference: vi.fn()
|
||||
}))
|
||||
|
||||
describe('notification opt-in gate', () => {
|
||||
it('asks for push-service consent when no choice is saved', async () => {
|
||||
vi.mocked(readPushNotificationsPreference).mockResolvedValue({ value: null, loaded: true })
|
||||
await expect(shouldPresentNotificationOptIn()).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it.each([true, false])('does not ask again after choosing %s', async (value) => {
|
||||
vi.mocked(readPushNotificationsPreference).mockResolvedValue({ value, loaded: true })
|
||||
await expect(shouldPresentNotificationOptIn()).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('does not prompt when the saved choice cannot be read', async () => {
|
||||
vi.mocked(readPushNotificationsPreference).mockResolvedValue({ value: null, loaded: false })
|
||||
await expect(shouldPresentNotificationOptIn()).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,58 +0,0 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
const state = vi.hoisted(
|
||||
(): { taskName: string; task: null | ((input: unknown) => Promise<void>) } => ({
|
||||
taskName: '',
|
||||
task: null
|
||||
})
|
||||
)
|
||||
vi.mock('expo-task-manager', () => ({
|
||||
defineTask: (name: string, task: typeof state.task) => {
|
||||
state.taskName = name
|
||||
state.task = task
|
||||
},
|
||||
isAvailableAsync: async () => true
|
||||
}))
|
||||
vi.mock('expo-notifications', () => ({
|
||||
registerTaskAsync: vi.fn(),
|
||||
getPresentedNotificationsAsync: vi.fn(async () => []),
|
||||
dismissNotificationAsync: vi.fn()
|
||||
}))
|
||||
vi.mock('./push-tray-dismissal', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('./push-tray-dismissal')>()
|
||||
return {
|
||||
...actual,
|
||||
dismissPresentedPushNotification: vi.fn(actual.dismissPresentedPushNotification)
|
||||
}
|
||||
})
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { dismissPresentedPushNotification } from './push-tray-dismissal'
|
||||
import './push-background-dismissal'
|
||||
|
||||
it('does not turn ordinary alerts into dismissals', async () => {
|
||||
vi.mocked(dismissPresentedPushNotification).mockClear()
|
||||
await state.task!({
|
||||
data: { data: { orca: { hostFingerprint: 'host-a', notificationId: 'same-id' } } }
|
||||
})
|
||||
expect(dismissPresentedPushNotification).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('an ID-only background dismissal preserves versioned tray alerts', async () => {
|
||||
const base = { hostFingerprint: 'host-a', notificationId: 'same-id' }
|
||||
vi.mocked(Notifications.getPresentedNotificationsAsync).mockResolvedValue([
|
||||
{ request: { identifier: 'legacy', content: { data: base } } },
|
||||
{
|
||||
request: {
|
||||
identifier: 'versioned',
|
||||
content: {
|
||||
data: {
|
||||
...base,
|
||||
notificationEpoch: 'epoch',
|
||||
notificationSeq: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
] as never)
|
||||
await state.task!({ data: { data: { ...base, kind: 'dismiss' } } })
|
||||
expect(Notifications.dismissNotificationAsync).toHaveBeenCalledExactlyOnceWith('legacy')
|
||||
})
|
||||
@@ -54,24 +54,3 @@ it('merges batched and concurrent fences without lowering duplicates or acceptin
|
||||
expect(await wasPushDismissed({ ...payload, notificationId: 'other' })).toBe(true)
|
||||
expect(await wasPushDismissed({ ...payload, notificationId: 'invalid' })).toBe(false)
|
||||
})
|
||||
|
||||
it.each([1, 3])('retains live dismissals beyond 512 entries across %i hosts', async (hosts) => {
|
||||
for (let index = 0; index < 520; index++) {
|
||||
await rememberPushDismissal({
|
||||
...payload,
|
||||
hostFingerprint: `host-${index % hosts}`,
|
||||
notificationId: `note-${index}`
|
||||
})
|
||||
}
|
||||
vi.resetModules()
|
||||
const restarted = await import('./push-dismissal-watermarks')
|
||||
for (const index of [0, 1, 519]) {
|
||||
const alert = {
|
||||
...payload,
|
||||
hostFingerprint: `host-${index % hosts}`,
|
||||
notificationId: `note-${index}`
|
||||
}
|
||||
expect(await restarted.wasPushDismissed(alert)).toBe(true)
|
||||
expect(await restarted.wasPushDismissed({ ...alert, notificationSeq: 3 })).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import type {
|
||||
FirebaseRemoteMessageNotification,
|
||||
Notification,
|
||||
NotificationTrigger
|
||||
} from 'expo-notifications'
|
||||
import { startAndroidForegroundPushPresentation } from './android-foreground-push'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
platform: { OS: 'android' },
|
||||
receive: (_notification: Notification) => {},
|
||||
remove: vi.fn(),
|
||||
eligible: vi.fn().mockResolvedValue(true),
|
||||
schedule: vi.fn().mockResolvedValue('message-1')
|
||||
}))
|
||||
vi.mock('./push-receive', () => ({ canPresentForegroundPush: mocks.eligible }))
|
||||
vi.mock('react-native', () => ({ Platform: mocks.platform }))
|
||||
vi.mock('expo-notifications', () => ({
|
||||
addNotificationReceivedListener: (listener: typeof mocks.receive) => {
|
||||
mocks.receive = listener
|
||||
return { remove: mocks.remove }
|
||||
},
|
||||
scheduleNotificationAsync: mocks.schedule
|
||||
}))
|
||||
|
||||
// Build complete Expo message types once so fixtures need no unchecked assertions.
|
||||
const REMOTE_NOTIFICATION: FirebaseRemoteMessageNotification = {
|
||||
body: null,
|
||||
bodyLocalizationArgs: null,
|
||||
bodyLocalizationKey: null,
|
||||
channelId: null,
|
||||
clickAction: null,
|
||||
color: null,
|
||||
eventTime: null,
|
||||
icon: null,
|
||||
imageUrl: null,
|
||||
lightSettings: null,
|
||||
link: null,
|
||||
localOnly: false,
|
||||
notificationCount: null,
|
||||
notificationPriority: null,
|
||||
sound: null,
|
||||
sticky: false,
|
||||
tag: null,
|
||||
ticker: null,
|
||||
title: null,
|
||||
titleLocalizationArgs: null,
|
||||
titleLocalizationKey: null,
|
||||
usesDefaultLightSettings: false,
|
||||
usesDefaultSound: false,
|
||||
usesDefaultVibrateSettings: false,
|
||||
vibrateTimings: null,
|
||||
visibility: null
|
||||
}
|
||||
|
||||
function pushTrigger(remote: FirebaseRemoteMessageNotification | null): NotificationTrigger {
|
||||
return {
|
||||
type: 'push',
|
||||
remoteMessage: {
|
||||
collapseKey: null,
|
||||
data: {},
|
||||
from: null,
|
||||
messageId: 'message-1',
|
||||
messageType: null,
|
||||
notification: remote,
|
||||
originalPriority: 1,
|
||||
priority: 1,
|
||||
sentTime: 0,
|
||||
to: null,
|
||||
ttl: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ORCA_PUSH_DATA: Record<string, unknown> = {
|
||||
hostFingerprint: 'host',
|
||||
notificationId: 'event',
|
||||
notificationEpoch: 'epoch',
|
||||
notificationSeq: '3',
|
||||
paneKey: 'pane',
|
||||
channelId: 'orca-desktop'
|
||||
}
|
||||
|
||||
function notification(
|
||||
trigger: NotificationTrigger = pushTrigger(null),
|
||||
data: Record<string, unknown> = ORCA_PUSH_DATA
|
||||
): Notification {
|
||||
return {
|
||||
date: 0,
|
||||
request: {
|
||||
identifier: 'message-1',
|
||||
trigger,
|
||||
content: {
|
||||
title: 'Test notification',
|
||||
subtitle: null,
|
||||
body: '',
|
||||
categoryIdentifier: null,
|
||||
threadIdentifier: null,
|
||||
sound: 'default',
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.eligible.mockReset().mockResolvedValue(true)
|
||||
mocks.platform.OS = 'android'
|
||||
})
|
||||
|
||||
it('does not reschedule its own local notification or normal provider notifications', () => {
|
||||
startAndroidForegroundPushPresentation()
|
||||
mocks.receive(notification(null))
|
||||
mocks.receive(notification({ channelId: 'orca-desktop' }))
|
||||
mocks.receive(notification(pushTrigger({ ...REMOTE_NOTIFICATION, title: 'Test' })))
|
||||
expect(mocks.schedule).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves silent dismissals and unrelated messages alone', () => {
|
||||
startAndroidForegroundPushPresentation()
|
||||
mocks.receive(notification(undefined, { ...ORCA_PUSH_DATA, kind: 'dismiss' }))
|
||||
mocks.receive(notification(undefined, {}))
|
||||
expect(mocks.schedule).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { Platform } from 'react-native'
|
||||
import { ensureDesktopNotificationChannel } from './desktop-notification-channel'
|
||||
|
||||
vi.mock('expo-notifications', () => ({
|
||||
AndroidImportance: { HIGH: 'high' },
|
||||
setNotificationChannelAsync: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('react-native', () => ({
|
||||
AppState: { currentState: 'background' },
|
||||
Platform: { OS: 'android' }
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
Object.assign(Platform, { OS: 'android' })
|
||||
vi.mocked(Notifications.setNotificationChannelAsync).mockResolvedValue(null as never)
|
||||
})
|
||||
|
||||
describe('ensureDesktopNotificationChannel', () => {
|
||||
it('does nothing on iOS, which has no notification channels', () => {
|
||||
Object.assign(Platform, { OS: 'ios' })
|
||||
|
||||
ensureDesktopNotificationChannel()
|
||||
|
||||
expect(Notifications.setNotificationChannelAsync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports channel failure so registration can retry', async () => {
|
||||
vi.mocked(Notifications.setNotificationChannelAsync).mockRejectedValue(new Error('no channels'))
|
||||
|
||||
await expect(ensureDesktopNotificationChannel()).rejects.toThrow('no channels')
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getNotificationNavigationTarget } from './notification-routing'
|
||||
|
||||
describe('notification routing', () => {
|
||||
// Identities stay raw: the target is dispatched as navigator params, not a URL.
|
||||
it('routes notification taps to the worktree terminal screen', () => {
|
||||
expect(
|
||||
getNotificationNavigationTarget({
|
||||
hostId: 'host-1',
|
||||
worktreeId: 'repo::/Users/me/orca/workspaces/feature'
|
||||
})
|
||||
).toEqual({
|
||||
hostId: 'host-1',
|
||||
sessionTarget: {
|
||||
name: '[hostId]/session/[worktreeId]',
|
||||
params: { hostId: 'host-1', worktreeId: 'repo::/Users/me/orca/workspaces/feature' }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to the host screen when the payload has no worktree id', () => {
|
||||
expect(getNotificationNavigationTarget({ hostId: 'host-1' })).toEqual({
|
||||
hostId: 'host-1',
|
||||
sessionTarget: null
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores payloads that cannot identify the paired host', () => {
|
||||
expect(getNotificationNavigationTarget({ worktreeId: 'repo::/tmp/worktree' })).toBeNull()
|
||||
})
|
||||
|
||||
it('ignores payloads for hosts that are no longer paired', () => {
|
||||
expect(
|
||||
getNotificationNavigationTarget(
|
||||
{ hostId: 'removed-host', worktreeId: 'repo::/tmp/worktree' },
|
||||
{ knownHostIds: new Set(['host-1']) }
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves the originating pane in the workspace route', () => {
|
||||
const paneKey = 'tab-b:11111111-1111-4111-8111-111111111111'
|
||||
expect(
|
||||
getNotificationNavigationTarget({ hostId: 'host', worktreeId: 'folder:/work', paneKey })
|
||||
?.sessionTarget?.params
|
||||
).toEqual({ hostId: 'host', worktreeId: 'folder:/work', paneKey })
|
||||
})
|
||||
|
||||
it('scopes a tap to a server workspace, and keeps anything else on the desktop', () => {
|
||||
const paramsFor = (executionHost: unknown) =>
|
||||
getNotificationNavigationTarget({ hostId: 'host', worktreeId: 'repo::/srv', executionHost })
|
||||
?.sessionTarget?.params
|
||||
expect(paramsFor('runtime:env-1')).toEqual({
|
||||
hostId: 'host',
|
||||
worktreeId: 'repo::/srv',
|
||||
executionHost: 'runtime:env-1'
|
||||
})
|
||||
for (const notAServer of [undefined, '', 'local', 'ssh:box', 'runtime:', 'env-1', 42]) {
|
||||
expect(paramsFor(notAServer)).toEqual({ hostId: 'host', worktreeId: 'repo::/srv' })
|
||||
}
|
||||
})
|
||||
@@ -1,8 +0,0 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { readNativeNotificationData } from './native-notification-data'
|
||||
|
||||
it('keeps Android push and local notification data', () => {
|
||||
const data = { hostId: 'host', notificationId: 'done' }
|
||||
expect(readNativeNotificationData({ content: { data }, trigger: { type: 'push' } })).toBe(data)
|
||||
expect(readNativeNotificationData({ content: { data }, trigger: null })).toBe(data)
|
||||
})
|
||||
@@ -1,19 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { deriveHostFingerprint, resolveHostIdForFingerprint } from './push-host-fingerprint'
|
||||
import { resolveHostIdForFingerprint } from './push-host-fingerprint'
|
||||
|
||||
const publicKey = Uint8Array.from({ length: 32 }, (_, index) => index)
|
||||
const publicKeyB64 = Buffer.from(publicKey).toString('base64')
|
||||
|
||||
describe('deriveHostFingerprint', () => {
|
||||
it.each([
|
||||
['a key of the wrong length', Buffer.from(new Uint8Array(16)).toString('base64')],
|
||||
['text that is not base64 at all', '!!!not base64!!!'],
|
||||
['an empty key', '']
|
||||
])('returns null for %s', (_label, value) => {
|
||||
expect(deriveHostFingerprint(value)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveHostIdForFingerprint', () => {
|
||||
const other = Uint8Array.from({ length: 32 }, (_, index) => index + 1)
|
||||
const hosts = [
|
||||
@@ -25,12 +15,4 @@ describe('resolveHostIdForFingerprint', () => {
|
||||
it('maps a push fingerprint back to the paired host id', () => {
|
||||
expect(resolveHostIdForFingerprint('Yw3NKWbEM2aRElRI', hosts)).toBe('host-1')
|
||||
})
|
||||
|
||||
it('returns null for a fingerprint no paired host derives', () => {
|
||||
expect(resolveHostIdForFingerprint('0123456789abcdef', hosts)).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects a fingerprint of the wrong length', () => {
|
||||
expect(resolveHostIdForFingerprint('Yw3NKWbEM2aRElRI'.slice(0, 8), hosts)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { addPushTokenListener, getDevicePushToken } from './push-token'
|
||||
|
||||
vi.mock('expo-notifications', () => ({
|
||||
getDevicePushTokenAsync: vi.fn(),
|
||||
addPushTokenListener: vi.fn()
|
||||
}))
|
||||
|
||||
const dev = globalThis as { __DEV__?: boolean }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete dev.__DEV__
|
||||
})
|
||||
|
||||
describe('getDevicePushToken', () => {
|
||||
it.each([
|
||||
[true, 'sandbox'],
|
||||
[false, 'production']
|
||||
])('reports apnsEnvironment for a __DEV__=%s iOS build as %s', async (isDev, environment) => {
|
||||
dev.__DEV__ = isDev
|
||||
vi.mocked(Notifications.getDevicePushTokenAsync).mockResolvedValue({
|
||||
type: 'ios',
|
||||
data: 'a'.repeat(64)
|
||||
} as never)
|
||||
|
||||
await expect(getDevicePushToken()).resolves.toEqual({
|
||||
platform: 'ios',
|
||||
token: 'a'.repeat(64),
|
||||
apnsEnvironment: environment
|
||||
})
|
||||
})
|
||||
|
||||
it('omits apnsEnvironment for Android, where FCM has no environment split', async () => {
|
||||
vi.mocked(Notifications.getDevicePushTokenAsync).mockResolvedValue({
|
||||
type: 'android',
|
||||
data: 'fcm-registration-token'
|
||||
} as never)
|
||||
|
||||
await expect(getDevicePushToken()).resolves.toEqual({
|
||||
platform: 'android',
|
||||
token: 'fcm-registration-token'
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a web push subscription', { type: 'web', data: { endpoint: 'https://example.test' } }],
|
||||
['an empty token', { type: 'ios', data: '' }]
|
||||
])('returns null for %s', async (_label, raw) => {
|
||||
vi.mocked(Notifications.getDevicePushTokenAsync).mockResolvedValue(raw as never)
|
||||
|
||||
await expect(getDevicePushToken()).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when the shell cannot mint a token at all', async () => {
|
||||
vi.mocked(Notifications.getDevicePushTokenAsync).mockRejectedValue(new Error('no entitlement'))
|
||||
|
||||
await expect(getDevicePushToken()).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('addPushTokenListener', () => {
|
||||
it('forwards a rolled native token and removes the subscription on teardown', () => {
|
||||
const remove = vi.fn()
|
||||
const native: { emit?: Parameters<typeof Notifications.addPushTokenListener>[0] } = {}
|
||||
vi.mocked(Notifications.addPushTokenListener).mockImplementation((listener) => {
|
||||
native.emit = listener
|
||||
return { remove }
|
||||
})
|
||||
const seen: unknown[] = []
|
||||
|
||||
const stop = addPushTokenListener((token) => seen.push(token))
|
||||
native.emit?.({ type: 'android', data: 'rolled' })
|
||||
native.emit?.({ type: 'web', data: {} })
|
||||
stop()
|
||||
|
||||
expect(seen).toEqual([{ platform: 'android', token: 'rolled' }])
|
||||
expect(remove).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -69,17 +69,6 @@ function createWriteQueue(): WriteQueueHarness {
|
||||
}
|
||||
}
|
||||
|
||||
function drain(queue: WriteQueueHarness): void {
|
||||
queue.pump()
|
||||
let guard = 0
|
||||
while (queue.pendingWrites.length > 0) {
|
||||
queue.flushWrite()
|
||||
if (++guard > 10_000) {
|
||||
throw new Error('write queue did not drain')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('terminal WebView write queue', () => {
|
||||
// Count queue-reachable string units, not heap bytes; xterm may still retain submitted chunks.
|
||||
|
||||
@@ -127,14 +116,4 @@ describe('terminal WebView write queue', () => {
|
||||
|
||||
expect(queue.writes).toEqual(['first'])
|
||||
})
|
||||
|
||||
it('drains reentrant enqueues in order', () => {
|
||||
const queue = createWriteQueue()
|
||||
queue.enqueue('first')
|
||||
queue.enqueueBoundary(() => queue.enqueue('reentrant'))
|
||||
queue.enqueue('second')
|
||||
drain(queue)
|
||||
|
||||
expect(queue.writes).toEqual(['first', 'secondreentrant'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,14 +47,4 @@ describe('the terminal waits for its bundled glyphs', () => {
|
||||
expect(clearTextureAtlas).not.toHaveBeenCalled()
|
||||
expect(fonts.postToHost).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('withholds readiness until the font can paint the first terminal snapshot', async () => {
|
||||
const fonts = pendingFonts()
|
||||
startMessageBridge(fonts.scope)
|
||||
expect(fonts.postToHost).not.toHaveBeenCalled()
|
||||
fonts.finish()
|
||||
await fonts.loaded
|
||||
expect(fonts.postToHost).toHaveBeenCalledWith({ type: 'web-ready', cellBox: null })
|
||||
stopMessageBridge(fonts.scope)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ const terminals: Terminal[] = []
|
||||
|
||||
afterEach(() => {
|
||||
terminals.splice(0).forEach((term) => term.dispose())
|
||||
vi.unstubAllGlobals()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
@@ -30,29 +29,6 @@ async function fixture(text: string) {
|
||||
return { term, scope, select, selected }
|
||||
}
|
||||
|
||||
describe('word selection uses the rendered xterm cell range', () => {
|
||||
it('keeps cell mapping when a legacy WebView has no grapheme segmenter', async () => {
|
||||
const f = await fixture('합성 🙂 cafe\u0301')
|
||||
vi.stubGlobal('Intl', Object.assign(Object.create(Intl), { Segmenter: undefined }))
|
||||
seedWordSelection(f.scope, 3, 0)
|
||||
expect(f.select).toHaveBeenLastCalledWith(0, 0, 4)
|
||||
expect(f.selected()).toBe('합성')
|
||||
seedWordSelection(f.scope, 6, 0)
|
||||
expect(f.select).toHaveBeenLastCalledWith(5, 0, 2)
|
||||
expect(f.selected()).toBe('🙂')
|
||||
})
|
||||
|
||||
it('keeps an absolute scrollback row and tolerates an evicted line', async () => {
|
||||
const f = await fixture('한글\r\nsecond\r\nthird\r\nfourth')
|
||||
expect(f.term.buffer.active.viewportY).toBeGreaterThan(0)
|
||||
seedWordSelection(f.scope, 2, 0)
|
||||
expect(f.select).toHaveBeenLastCalledWith(0, 0, 4)
|
||||
expect(f.selected()).toBe('한글')
|
||||
seedWordSelection(f.scope, 2, 99)
|
||||
expect(f.select).toHaveBeenLastCalledWith(2, 99, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Unicode selection boundaries across every rendered column', () => {
|
||||
it.each([
|
||||
['한글', 4],
|
||||
@@ -73,16 +49,4 @@ describe('Unicode selection boundaries across every rendered column', () => {
|
||||
seedWordSelection(f.scope, 3 + width + 1, 0)
|
||||
expect(f.selected()).toBe('next')
|
||||
})
|
||||
|
||||
it('keeps the last wide word on the row and the next wrapped row independent', async () => {
|
||||
const f = await fixture(' '.repeat(56) + '한글next')
|
||||
for (const col of [56, 57, 58, 59]) {
|
||||
seedWordSelection(f.scope, col, 0)
|
||||
expect(f.select).toHaveBeenLastCalledWith(56, 0, 4)
|
||||
expect(f.selected()).toBe('한글')
|
||||
}
|
||||
seedWordSelection(f.scope, 0, 1)
|
||||
expect(f.select).toHaveBeenLastCalledWith(0, 1, 4)
|
||||
expect(f.selected()).toBe('next')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,13 +5,11 @@ import { join } from 'node:path'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import type { AgentJournalMessageItem } from '../../../shared/agent-session/journal/agent-session-journal-types'
|
||||
import type { NativeChatBlock } from '../../../shared/native-chat/native-chat-types'
|
||||
import { AcpPromptContentError, acpDispatchPrompt, acpPromptBlocks } from './acp-prompt-content'
|
||||
import { AcpPromptContentError, acpPromptBlocks } from './acp-prompt-content'
|
||||
|
||||
const PNG = Buffer.from('89504e470d0a1a0a', 'hex')
|
||||
const dir = mkdtempSync(join(tmpdir(), 'acp-prompt-content-'))
|
||||
afterAll(() => rmSync(dir, { recursive: true, force: true }))
|
||||
const pngPath = join(dir, 'shot.png')
|
||||
writeFileSync(pngPath, PNG)
|
||||
|
||||
function message(...blocks: NativeChatBlock[]): AgentJournalMessageItem {
|
||||
return { kind: 'message', role: 'user', blocks }
|
||||
@@ -26,14 +24,6 @@ async function refusal(body: AgentJournalMessageItem, images = true) {
|
||||
}
|
||||
|
||||
describe('ACP prompt content', () => {
|
||||
it('refuses a withheld command typed with arguments', async () => {
|
||||
const result = await acpDispatchPrompt(message({ type: 'text', text: '/allow-all on' }), {
|
||||
spec: { agent: 'copilot', dialect: { withheldCommands: ['allow-all'] } },
|
||||
connection: { initialize: async () => ({ protocolVersion: 1 }) }
|
||||
})
|
||||
expect(Array.isArray(result)).toBe(false)
|
||||
})
|
||||
|
||||
it('sends a host image up to 20 MiB inline and keeps a larger one as a path', async () => {
|
||||
const inlinePath = join(dir, 'photo.png')
|
||||
writeFileSync(inlinePath, Buffer.alloc(20 * 1024 * 1024))
|
||||
|
||||
@@ -87,22 +87,6 @@ describe('WSL host IPC: the user Add and Remove (D3a)', () => {
|
||||
service.add.mockClear()
|
||||
})
|
||||
|
||||
it('refuses an Add in serve-only and says so in its capability, keeping saved hosts usable', async () => {
|
||||
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '0')
|
||||
register()
|
||||
|
||||
expect(await handlers.get('wslHosts:add')?.({}, { distro: 'Ubuntu' })).toMatchObject({
|
||||
ok: false,
|
||||
kind: 'disabled'
|
||||
})
|
||||
expect(service.add).not.toHaveBeenCalled()
|
||||
expect(await handlers.get('wslHosts:getCapability')?.({})).toMatchObject({
|
||||
acceptsNewWork: false
|
||||
})
|
||||
await handlers.get('wslHosts:remove')?.({}, { environmentId: 'wsl:Ubuntu' })
|
||||
expect(service.remove).toHaveBeenCalledWith('wsl:Ubuntu')
|
||||
})
|
||||
|
||||
// A block published while Orca runs must stop new hosts now, not at the next launch.
|
||||
it('a live policy block refuses an Add at once, tells the window, and keeps the saved host served', async () => {
|
||||
vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1')
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import type * as WslMoveDriverPreviewModule from '../../persistence/moving-wsl-projects/execution/wsl-move-driver-preview'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { WSL_HOSTS_INTERNAL_GATE_ENV } from '../../../shared/wsl-host-internal-gate'
|
||||
|
||||
type Handler = (event: unknown, request?: unknown) => unknown
|
||||
|
||||
const handlers = vi.hoisted(() => new Map<string, Handler>())
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
removeHandler: (channel: string) => handlers.delete(channel),
|
||||
handle: (channel: string, handler: Handler) => handlers.set(channel, handler)
|
||||
}
|
||||
}))
|
||||
const driver = vi.hoisted(() => ({
|
||||
previewWslMove: vi.fn(async () => ({ outcome: 'ready' })),
|
||||
moveProjectsToWslHost: vi.fn(async () => ({ outcome: 'moved' })),
|
||||
cancelWslMove: vi.fn(() => true)
|
||||
}))
|
||||
vi.mock('../../persistence/moving-wsl-projects/wsl-move-driver', () => ({
|
||||
moveProjectsToWslHost: driver.moveProjectsToWslHost
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'../../persistence/moving-wsl-projects/execution/wsl-move-driver-preview',
|
||||
async (importOriginal) => ({
|
||||
...(await importOriginal<typeof WslMoveDriverPreviewModule>()),
|
||||
previewWslMove: driver.previewWslMove
|
||||
})
|
||||
)
|
||||
const running = vi.hoisted(() => ({ any: false }))
|
||||
vi.mock('../../persistence/moving-wsl-projects/orchestration/wsl-move-running-moves', () => ({
|
||||
isAnyWslMoveRunning: () => running.any,
|
||||
cancelWslMove: driver.cancelWslMove
|
||||
}))
|
||||
const track = vi.hoisted(() => vi.fn())
|
||||
vi.mock('../../telemetry/client', () => ({ track }))
|
||||
|
||||
import { registerWslMoveHandlers } from './wsl-move-handlers'
|
||||
|
||||
const MAIN_WINDOW_ID = 7
|
||||
const progress = vi.fn()
|
||||
const deps = vi.fn(() => ({ now: () => new Date(0), progress }))
|
||||
const invoke = (channel: string, request?: unknown, senderId = MAIN_WINDOW_ID) =>
|
||||
handlers.get(channel)?.({ sender: { id: senderId } }, request)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
|
||||
deps.mockClear()
|
||||
track.mockClear()
|
||||
for (const fn of Object.values(driver)) {
|
||||
fn.mockClear()
|
||||
}
|
||||
registerWslMoveHandlers(
|
||||
() => '/unused',
|
||||
(senderId) => senderId === MAIN_WINDOW_ID,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The driver is mocked; the handler only passes deps through.
|
||||
deps as never
|
||||
)
|
||||
})
|
||||
afterEach(() => vi.unstubAllEnvs())
|
||||
|
||||
describe('WSL move driver IPC', () => {
|
||||
it('is unreachable with the internal WSL hosts gate closed: nothing is built or run', async () => {
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '0')
|
||||
const refused = { outcome: 'refused', code: 'wsl_move_unavailable' }
|
||||
expect(await invoke('wslMove:preview', { distro: 'Ubuntu' })).toEqual(refused)
|
||||
expect(await invoke('wslMove:start', { distro: 'Ubuntu' })).toEqual(refused)
|
||||
expect(invoke('wslMove:cancel', 'Ubuntu')).toBe(false)
|
||||
expect(deps).not.toHaveBeenCalled()
|
||||
expect(driver.previewWslMove).not.toHaveBeenCalled()
|
||||
expect(driver.moveProjectsToWslHost).not.toHaveBeenCalled()
|
||||
expect(driver.cancelWslMove).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses another window, and a malformed request', async () => {
|
||||
expect(await invoke('wslMove:start', { distro: 'Ubuntu' }, 99)).toMatchObject({
|
||||
code: 'wsl_move_unavailable',
|
||||
detail: 'not-main-window'
|
||||
})
|
||||
for (const bad of [
|
||||
undefined,
|
||||
'Ubuntu',
|
||||
{ distro: '' },
|
||||
{ distro: '..\\x' },
|
||||
{ distro: 'U', repoIds: [1] }
|
||||
]) {
|
||||
expect(await invoke('wslMove:start', bad)).toMatchObject({ detail: 'invalid-request' })
|
||||
}
|
||||
expect(driver.moveProjectsToWslHost).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -297,36 +297,6 @@ describe('orchestration federated worker output', () => {
|
||||
return dispatchId
|
||||
}
|
||||
|
||||
it('routes show and read by Dispatch without repeating the worker server', async () => {
|
||||
const dispatchId = await startRemoteWorker()
|
||||
|
||||
const shown = await homeCall('rpc_remote_show', 'orchestration.workerShow', {
|
||||
dispatch: dispatchId
|
||||
})
|
||||
const read = await homeCall('rpc_remote_read', 'orchestration.workerRead', {
|
||||
dispatch: dispatchId,
|
||||
limit: 20
|
||||
})
|
||||
|
||||
expect(shown).toMatchObject({
|
||||
ok: true,
|
||||
result: {
|
||||
server: { environmentId: 'environment_windows', name: 'windows' },
|
||||
observation: { status: 'live', exactWorker: true },
|
||||
terminal: { handle: 'term_windows_worker' }
|
||||
}
|
||||
})
|
||||
expect(read).toMatchObject({
|
||||
ok: true,
|
||||
result: {
|
||||
source: 'terminal',
|
||||
fallbackReason: 'session_not_reported',
|
||||
server: { environmentId: 'environment_windows', name: 'windows' },
|
||||
terminal: { tail: ['remote output'] }
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('reads the exact transcript on the worker server without leaking its path home', async () => {
|
||||
const dispatchId = await startRemoteWorker()
|
||||
const directory = await mkdtemp(join(tmpdir(), 'orca-federated-worker-output-'))
|
||||
@@ -455,84 +425,4 @@ describe('orchestration federated worker output', () => {
|
||||
homeOmitsAutoRelease = omitted
|
||||
}
|
||||
})
|
||||
|
||||
it('does not report a captured remote archive or close when persistence fails', async () => {
|
||||
const dispatchId = await startRemoteWorker()
|
||||
workerDb.db.exec('DROP TABLE worker_terminal_archives')
|
||||
|
||||
const released = await homeDispatcher.dispatch({
|
||||
id: 'rpc_remote_release_archive_failure',
|
||||
authToken: 'coordinator-token',
|
||||
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
|
||||
orchestrationRequestId: 'remote_release_archive_failure',
|
||||
method: 'orchestration.workerRelease',
|
||||
params: { dispatch: dispatchId }
|
||||
})
|
||||
|
||||
expect(released).toMatchObject({
|
||||
ok: true,
|
||||
result: { state: 'retained', processAction: 'none', archive: null }
|
||||
})
|
||||
expect(workerRuntime.closeTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps reads, fleet snapshots, and release on legacy fallbacks for an old peer', async () => {
|
||||
workerAdvertisesNewCapabilities = false
|
||||
// Ask shipped hosts directly for structured output; only method_not_found permits terminal fallback.
|
||||
workerSupportsStructuredRead = false
|
||||
const dispatchId = await startRemoteWorker()
|
||||
remoteCalls = []
|
||||
const read = await homeCall('rpc_old_peer_read', 'orchestration.workerRead', {
|
||||
dispatch: dispatchId
|
||||
})
|
||||
const fleet = await homeCall('rpc_old_peer_fleet', 'orchestration.workerList', {
|
||||
includeRemote: true
|
||||
})
|
||||
const release = await homeDispatcher.dispatch({
|
||||
id: 'rpc_old_peer_release',
|
||||
authToken: 'coordinator-token',
|
||||
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
|
||||
orchestrationRequestId: 'old_peer_release',
|
||||
method: 'orchestration.workerRelease',
|
||||
params: { dispatch: dispatchId }
|
||||
})
|
||||
|
||||
expect(read).toMatchObject({
|
||||
ok: true,
|
||||
result: { fallbackReason: 'remote_capability_unavailable' }
|
||||
})
|
||||
expect(fleet).toMatchObject({
|
||||
ok: true,
|
||||
result: { partialHostErrors: [{ code: 'capability_unsupported' }] }
|
||||
})
|
||||
expect(release).toMatchObject({
|
||||
ok: true,
|
||||
result: { state: 'retained', reason: 'federation_unsupported' }
|
||||
})
|
||||
expect(remoteCalls).toContain('orchestration.federationReadOutput')
|
||||
expect(remoteCalls).toContain('orchestration.federationFleetSnapshot')
|
||||
expect(remoteCalls).not.toContain('orchestration.federationRelease')
|
||||
})
|
||||
|
||||
it('retains a mixed-version worker when its host cannot guarantee a durable archive', async () => {
|
||||
workerAdvertisesDurableRelease = false
|
||||
const dispatchId = await startRemoteWorker()
|
||||
remoteCalls = []
|
||||
|
||||
const release = await homeDispatcher.dispatch({
|
||||
id: 'rpc_nondurable_peer_release',
|
||||
authToken: 'coordinator-token',
|
||||
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
|
||||
orchestrationRequestId: 'nondurable_peer_release',
|
||||
method: 'orchestration.workerRelease',
|
||||
params: { dispatch: dispatchId }
|
||||
})
|
||||
|
||||
expect(release).toMatchObject({
|
||||
ok: true,
|
||||
result: { state: 'retained', reason: 'federation_unsupported', archive: null }
|
||||
})
|
||||
expect(remoteCalls).not.toContain('orchestration.federationRelease')
|
||||
expect(workerRuntime.closeTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcRenderer: { invoke: vi.fn(), on: vi.fn(), removeListener: vi.fn() }
|
||||
}))
|
||||
|
||||
import {
|
||||
formatWslHostsPolicyBlockedArgument,
|
||||
formatWslHostsServeOnlyArgument,
|
||||
WSL_HOSTS_INTERNAL_GATE_ENV
|
||||
} from '../../shared/wsl-host-internal-gate'
|
||||
import { getWslHostsApi } from './wsl-hosts-api'
|
||||
|
||||
describe('getWslHostsApi', () => {
|
||||
it('is present on Windows with no gate variable set', () => {
|
||||
expect(getWslHostsApi({}, 'win32', [])).toBeDefined()
|
||||
})
|
||||
|
||||
it.each<NodeJS.Platform>(['darwin', 'linux'])('is absent on %s', (platform) => {
|
||||
expect(getWslHostsApi({}, platform, [])).toBeUndefined()
|
||||
})
|
||||
|
||||
it('is absent on Windows when the launch turns the gate off', () => {
|
||||
expect(getWslHostsApi({ [WSL_HOSTS_INTERNAL_GATE_ENV]: '0' }, 'win32', [])).toBeUndefined()
|
||||
})
|
||||
|
||||
it('is absent on Windows when the remote policy blocked the gate', () => {
|
||||
expect(getWslHostsApi({}, 'win32', [formatWslHostsPolicyBlockedArgument()])).toBeUndefined()
|
||||
})
|
||||
|
||||
it('stays present behind a closed gate when main stamped serve-only', () => {
|
||||
const env = { [WSL_HOSTS_INTERNAL_GATE_ENV]: '0' }
|
||||
expect(getWslHostsApi(env, 'win32', [formatWslHostsServeOnlyArgument()])).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
parsePluginGrammarArtifact,
|
||||
parsePluginLanguageConfiguration,
|
||||
parsePluginSyntaxPayload,
|
||||
validatePluginGrammarIncludes
|
||||
} from './plugin-syntax-artifact'
|
||||
|
||||
@@ -11,12 +10,6 @@ function grammar(scopeName: string, body: Record<string, unknown> = {}): string
|
||||
}
|
||||
|
||||
describe('plugin syntax artifacts', () => {
|
||||
it('refuses a grammar whose scope differs from its declaration', () => {
|
||||
expect(() => parsePluginGrammarArtifact(grammar('source.other'), 'source.demo')).toThrow(
|
||||
'does not match'
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses oversized and deeply nested grammars', () => {
|
||||
const huge = grammar('source.demo', { comment: 'x'.repeat(1024 * 1024) })
|
||||
expect(() => parsePluginGrammarArtifact(huge, 'source.demo')).toThrow('exceeds')
|
||||
@@ -82,24 +75,4 @@ describe('plugin syntax artifacts', () => {
|
||||
surroundingPairs: []
|
||||
})
|
||||
})
|
||||
|
||||
it('bounds language configuration and validates payloads at renderer ingress', () => {
|
||||
expect(
|
||||
parsePluginLanguageConfiguration(
|
||||
JSON.stringify({ comments: { lineComment: '//' }, brackets: [['{', '}']] })
|
||||
)
|
||||
).toEqual({ comments: { lineComment: '//' }, brackets: [['{', '}']] })
|
||||
expect(() =>
|
||||
parsePluginLanguageConfiguration(JSON.stringify({ brackets: [['', '}']] }), 'strict')
|
||||
).toThrow()
|
||||
const payload = {
|
||||
id: 'plugin:samples.demo/demo',
|
||||
revision: 'abc',
|
||||
scopeName: 'source.demo',
|
||||
grammars: [{ scopeName: 'source.demo', patterns: [], repository: {} }],
|
||||
configuration: {}
|
||||
}
|
||||
expect(parsePluginSyntaxPayload(payload)).toEqual(payload)
|
||||
expect(parsePluginSyntaxPayload({ ...payload, grammars: [] })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isWslHostsInternalGateOpen, WSL_HOSTS_INTERNAL_GATE_ENV } from './wsl-host-internal-gate'
|
||||
|
||||
describe('WSL hosts internal gate', () => {
|
||||
it('is open when the variable is unset', () => {
|
||||
expect(isWslHostsInternalGateOpen({})).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['0', 'false', 'off', 'no', 'FALSE', ' Off ', 'No\n', '"0"', "'off'", ' "False" '])(
|
||||
'is closed for %j',
|
||||
(value) => {
|
||||
expect(isWslHostsInternalGateOpen({ [WSL_HOSTS_INTERNAL_GATE_ENV]: value })).toBe(false)
|
||||
}
|
||||
)
|
||||
|
||||
// Empty reads like unset; anything else that isn't an off word keeps today's internal "on".
|
||||
it.each(['1', 'true', 'on', 'yes', '', ' ', 'disabled', '""', '"0'])(
|
||||
'is open for %j',
|
||||
(value) => {
|
||||
expect(isWslHostsInternalGateOpen({ [WSL_HOSTS_INTERNAL_GATE_ENV]: value })).toBe(true)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* The WSL move dialog against a fake move transport: no wsl.exe or distro in CI. Main's
|
||||
* `wslMove:*` handlers are swapped for a scripted host, so the real dialog runs open preview →
|
||||
* blockers → skip → start → progress → refusal → start again → moved → reload. A second test
|
||||
* proves that with the internal gate closed none of it shows and main refuses.
|
||||
* blockers → skip → start → progress → refusal → start again → moved → reload.
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
@@ -409,30 +408,3 @@ test('moves projects through preview, blockers, progress and result', async ({},
|
||||
rmSync(path.dirname(webPath), { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
// oxlint-disable-next-line no-empty-pattern -- Playwright passes fixtures before testInfo.
|
||||
test('with the internal gate closed, no move entry shows and main refuses', async ({}, testInfo) => {
|
||||
// The gated-off launch still boots a full app before main is asked to refuse.
|
||||
test.setTimeout(180_000)
|
||||
const desktop = await launch(testInfo, false)
|
||||
const { page } = desktop
|
||||
try {
|
||||
expect(
|
||||
await page.evaluate((distro) => window.api.wslMove.preview({ distro }), DISTRO)
|
||||
).toMatchObject({ outcome: 'refused', code: 'wsl_move_unavailable' })
|
||||
expect(await page.evaluate(() => window.api.wslMove.listUnfinished())).toEqual([])
|
||||
// Serve-only (#34683): the saved host still lists, but nothing new is added or moved.
|
||||
await openServersPane(page)
|
||||
const hosts = page.locator('[data-settings-section="wsl-hosts"]')
|
||||
await expect(hosts.getByRole('button', { name: 'Add WSL distro' })).toHaveCount(0)
|
||||
await hosts.getByRole('button', { name: `More actions for ${DISTRO} (WSL)` }).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Remove…' })).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Move projects from this distro…' })
|
||||
).toHaveCount(0)
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(page.getByTestId('wsl-move-dialog')).toHaveCount(0)
|
||||
} finally {
|
||||
await desktop.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user