mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
6bab6fbe97
Documents the agent writes open in the workspace panel instead of another application, and local images the agent mentions show up in the conversation. Workspace preview - PDF (pdf.js with its own layout and text layer), Word (docx-preview inside a scripts-disabled sandboxed iframe) and Excel (SheetJS; .xlsx, .xlsm, .xls) open in the side panel with zoom and fit, per-file scroll/zoom/sheet memory, and a refresh when the agent rewrites the file. The engines load lazily. - Bytes come from a new GET /api/sessions/:id/workspace/raw route, with an extension allowlist, size caps, the workspace boundary and canonical-path checks. The file endpoint returns metadata and a version for documents. The client fetches with the bearer credential, so it works in Electron, LAN H5 and remote access alike. - Chat links, output cards and the change card open pdf/docx/xlsx in the workspace; documents outside the workdir still go to the system application. - Image viewer with fit, zoom and pan, and "open in system app". Chat images - Markdown images outside the workdir, at ~/, C:\ and file:// paths render, open in the viewer, and offer "open original" (pictures only). - Images returned by tools such as Read appear as thumbnails under the call. Hardening found in review - previewFsUrl escapes each path segment; a double-escaped %2e%2e used to leave /preview-fs/<session>/. - The CORS, API timing and remote-access header decorators set headers in place. Rebuilding the response buffered whole files in memory and dropped Content-Length. - The engine owns the pdf.js worker, so closing one document no longer fails the next open. - Office archives are inflated in steps to check their real sizes, not the sizes they declare. - A viewer that fails to load stays in its panel instead of taking the window down. Adds pdfjs-dist, docx-preview, xlsx (SheetJS 0.20.3 tarball) and fflate as renderer dev dependencies; Vite bundles them. Refs #1397
116 lines
3.4 KiB
TypeScript
116 lines
3.4 KiB
TypeScript
import { act, render, screen } from '@testing-library/react'
|
|
import { StrictMode } from 'react'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import '@testing-library/jest-dom'
|
|
import { useElementSize } from './useElementSize'
|
|
|
|
const originalClientWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientWidth')
|
|
const originalClientHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientHeight')
|
|
|
|
function Probe() {
|
|
const [measuredRef, size] = useElementSize<HTMLDivElement>()
|
|
return (
|
|
<div ref={measuredRef}>
|
|
<span data-testid="size">{size === null ? 'unmeasured' : `${size.width}x${size.height}`}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
describe('useElementSize', () => {
|
|
const observers = new Set<() => void>()
|
|
let box = { width: 0, height: 0 }
|
|
|
|
function stubLayout(width: number, height: number) {
|
|
box = { width, height }
|
|
Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, get: () => box.width })
|
|
Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, get: () => box.height })
|
|
}
|
|
|
|
function stubResizeObserver() {
|
|
class StubResizeObserver {
|
|
constructor(private readonly callback: () => void) {}
|
|
observe() { observers.add(this.callback) }
|
|
unobserve() { observers.delete(this.callback) }
|
|
disconnect() { observers.delete(this.callback) }
|
|
}
|
|
vi.stubGlobal('ResizeObserver', StubResizeObserver)
|
|
}
|
|
|
|
function resizeTo(width: number, height: number) {
|
|
box = { width, height }
|
|
act(() => {
|
|
observers.forEach((notify) => notify())
|
|
})
|
|
}
|
|
|
|
afterEach(() => {
|
|
observers.clear()
|
|
box = { width: 0, height: 0 }
|
|
vi.unstubAllGlobals()
|
|
for (const [property, original] of [
|
|
['clientWidth', originalClientWidth],
|
|
['clientHeight', originalClientHeight],
|
|
] as const) {
|
|
if (original) Object.defineProperty(HTMLElement.prototype, property, original)
|
|
else Reflect.deleteProperty(HTMLElement.prototype, property)
|
|
}
|
|
})
|
|
|
|
it('measures the node as soon as it mounts', () => {
|
|
stubLayout(640, 480)
|
|
stubResizeObserver()
|
|
|
|
render(<Probe />)
|
|
|
|
expect(screen.getByTestId('size')).toHaveTextContent('640x480')
|
|
})
|
|
|
|
it('reports null rather than a zero size before layout, so callers keep their fallback', () => {
|
|
// jsdom lays nothing out: everything measures 0.
|
|
render(<Probe />)
|
|
|
|
expect(screen.getByTestId('size')).toHaveTextContent('unmeasured')
|
|
})
|
|
|
|
it('follows the element as it resizes', () => {
|
|
stubLayout(640, 480)
|
|
stubResizeObserver()
|
|
render(<Probe />)
|
|
|
|
resizeTo(320, 200)
|
|
|
|
expect(screen.getByTestId('size')).toHaveTextContent('320x200')
|
|
})
|
|
|
|
it('goes back to null when the element collapses to nothing', () => {
|
|
stubLayout(640, 480)
|
|
stubResizeObserver()
|
|
render(<Probe />)
|
|
|
|
resizeTo(0, 0)
|
|
|
|
expect(screen.getByTestId('size')).toHaveTextContent('unmeasured')
|
|
})
|
|
|
|
it('keeps observing under StrictMode, which replays effects without reattaching the ref', () => {
|
|
stubLayout(640, 480)
|
|
stubResizeObserver()
|
|
render(<StrictMode><Probe /></StrictMode>)
|
|
|
|
resizeTo(100, 50)
|
|
|
|
expect(screen.getByTestId('size')).toHaveTextContent('100x50')
|
|
})
|
|
|
|
it('stops observing on unmount', () => {
|
|
stubLayout(640, 480)
|
|
stubResizeObserver()
|
|
const { unmount } = render(<Probe />)
|
|
expect(observers.size).toBe(1)
|
|
|
|
unmount()
|
|
|
|
expect(observers.size).toBe(0)
|
|
})
|
|
})
|