Files
orca/tests/e2e/native-chat-text-attachment-export.unit.test.ts
T
Brennan Benson 40820c4c5c Open pasted text in structured chats (#35636)
* Open large pasted text and attached text in structured chats

* Read pending pasted text before its host route resolves

* Pass structured attachment eligibility from the owning chat

* Preserve composer and Markdown state when inspecting text attachments

* fix: preserve attachment identity and cancel deferred reads

* test: consolidate phone attachment rendering contracts

* Scope attachment previews to visible chats and share route indexes

* test: consolidate duplicate attachment preview coverage

* perf: share remaining workspace catalog derivations

* feat(chat): open text attachments from structured goal objectives

* fix(chat): reuse preview owner and bind app hooks

* refactor(chat): trim duplicate tests and unused capability initialization

* test(chat): share routing scope mount fixture

* Fix routing performance attribution and folder row imports

* Share attachment capability through its domain module

* Align attachment fixtures and warm routing contracts with current gates

* Preserve relay connection timeout default for ordinary requests

* Share image classification ownership and repair integration fixtures

* Repair real attachment integration fixture boundaries

* Expose e2e compiler diagnostics and prune stale read allowance

* Type the recall fixture row ref as a div

* Keep text attachment capability in its existing capability owner

* Keep image MIME bindings in the media type owner

* Own private bridge response completion at the request boundary

* Complete bridge fixture return and retention types

* Localize native chat text attachment preview controls

* Complete attachment preview localization at presentation boundaries

* Preserve legacy preview locale identity and complete localization controls

* Fix typed mobile host witness lookup

* Commit attachment page edits and reset mobile page scroll

* test(chat): remove duplicate preview localization coverage

* fix(mobile): apply page input font and footer safe area

* fix(chat): commit edited page before admitted navigation

* fix(chat): retain field readiness on pending commit

* fix: preserve attachment field focus until control activation

* fix: restore attachment preview styles and canonical close

* Bind mobile input font role to the central theme

* Type font source fixture cases as tuples

* Attribute inherited owner filter work and qualify folder fixtures

* Keep owner filter fixture input types source-derived

* test: retain attachment capability assertion import

* test: focus attachment coverage on important regressions

* Replace text attachment paging with bounded scrolling and trim tests

* Preserve literal phone attachment whitespace and trim unused test meter

* Keep scroll view refs and measurements out of render

* Keep preview collection independent of native clipboard initialization

* Give the phone scroll fixture coherent measured DOM geometry

* Commit attachment scroll extent before restoring its anchor

* Preserve text viewport geometry across bounded reads

* Unify bounded text scrolling around the viewport

* Preserve the live text anchor across viewport resizing

* Measure committed text viewport bounds from the host

* Preserve live attachment position across native client extent changes

* Use the actual first source block in the resize regression fixture

* Reuse subscription handle type

* Keep attachment effects keyed to their existing triggers

* Keep attachment rows within the committed track

* Allow the full attachment renderer contract to settle

* Open text attachment previews in a large modal

* Preserve attachment modal header layout with primitive-owned styles
2026-11-02 18:36:29 -08:00

131 lines
5.3 KiB
TypeScript

// @vitest-environment happy-dom
import { createElement } from 'react'
import { EventEmitter } from 'node:events'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { NativeChatTextAttachmentPreview } from '@/components/native-chat/NativeChatTextAttachmentPreview'
import { AgentSessionAttachmentStore } from '../../src/main/native-chat/agent-session-attachments/agent-session-attachment-store'
import { createFilesystemHandlerContext } from '../../src/main/ipc/filesystem/filesystem-handler-context'
import { registerFilesystemDownloadHandlers } from '../../src/main/ipc/filesystem/filesystem-download-handlers'
import type { Store } from '../../src/main/persistence/loading-store/store'
const mocks = vi.hoisted(() => ({
handlers: new Map<string, (...args: unknown[]) => Promise<unknown>>(),
dialog: vi.fn(),
write: vi.fn(),
rpc: vi.fn()
}))
vi.mock('electron', () => ({
BrowserWindow: { fromWebContents: () => null },
dialog: { showSaveDialog: mocks.dialog },
ipcMain: {
handle: (name: string, handler: (...args: unknown[]) => Promise<unknown>) =>
mocks.handlers.set(name, handler)
}
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.rpc }))
vi.mock('sonner', () => ({ toast: { success: vi.fn() } }))
afterEach(cleanup)
it('saves zero-byte and invalid UTF8 committed files through the real native sink and copies empty text', async () => {
const folder = await mkdtemp(join(tmpdir(), 'export35636-'))
const sourceStore = new AgentSessionAttachmentStore(join(folder, 'agent-session-attachments'), {
hasSession: () => true
})
// SAFETY: only download handlers are registered; they do not access persistence.
const store: Store = Object.create(null)
const cancellations = { begin: () => null, finish: () => {}, cancel: () => {} }
const context = createFilesystemHandlerContext(store, undefined, cancellations, cancellations)
registerFilesystemDownloadHandlers(context)
const sender = Object.assign(new EventEmitter(), { id: 35636, isDestroyed: () => false })
const invoke = (name: string, args: unknown) => {
const handler = mocks.handlers.get(`fs:${name}`)
if (!handler) {
throw new Error(`Missing ${name}`)
}
return handler({ sender }, args)
}
const append = vi.fn((args: unknown) => invoke('appendDownloadedFileChunk', args))
const finish = vi.fn((args: unknown) => invoke('finishDownloadedFile', args))
const cancel = vi.fn((args: unknown) => invoke('cancelDownloadedFile', args))
Object.defineProperty(window, 'api', {
configurable: true,
value: {
ui: { writeClipboardText: mocks.write },
fs: {
startDownloadedFile: (args: unknown) => invoke('startDownloadedFile', args),
appendDownloadedFileChunk: append,
finishDownloadedFile: finish,
cancelDownloadedFile: cancel
}
}
})
mocks.rpc.mockImplementation(async (_target, method, request) =>
method === 'status.get'
? { runtimeId: '35636', capabilities: ['native-chat.text-attachment-read.v1'] }
: sourceStore.readTextChunk(request)
)
try {
for (const bytes of [Buffer.alloc(0), Buffer.from([0xff, 0, 0x80])]) {
const name = bytes.length ? 'invalid.txt' : 'empty.txt'
const { uploadId } = await sourceStore.startUpload({
callerKey: '35636',
sessionId: 's',
name,
byteLength: bytes.length
})
if (bytes.length) {
await sourceStore.appendChunk({
callerKey: '35636',
uploadId,
offset: 0,
contentBase64: bytes.toString('base64')
})
}
const { path } = await sourceStore.commitUpload({ callerKey: '35636', uploadId })
const destination = join(folder, name)
mocks.dialog.mockResolvedValue({ canceled: false, filePath: destination })
append.mockClear()
finish.mockClear()
cancel.mockClear()
const view = render(
createElement(NativeChatTextAttachmentPreview, {
path,
target: { kind: 'local' },
onClose: () => {}
})
)
await waitFor(() =>
expect(screen.getByRole('button', { name: 'Save file' }).hasAttribute('disabled')).toBe(
false
)
)
if (!bytes.length) {
fireEvent.click(screen.getByRole('button', { name: 'Copy contents' }))
await waitFor(() => expect(mocks.write).toHaveBeenCalledWith(''))
await waitFor(() =>
expect(screen.getByRole('button', { name: 'Save file' }).hasAttribute('disabled')).toBe(
false
)
)
}
fireEvent.click(screen.getByRole('button', { name: 'Save file' }))
await waitFor(() => expect(finish).toHaveBeenCalledOnce())
await waitFor(() => expect(context.downloadSessions.size).toBe(0))
expect(await readFile(destination)).toEqual(bytes)
expect(append).toHaveBeenCalledTimes(bytes.length ? 1 : 0)
expect(cancel).not.toHaveBeenCalled()
view.unmount()
}
} finally {
for (const id of context.downloadSessions.keys()) {
await context.closeDownloadSession(id, true)
}
sourceStore.clearInFlightForTests()
await rm(folder, { recursive: true, force: true })
}
})