d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
92 lines
3.2 KiB
TypeScript
92 lines
3.2 KiB
TypeScript
/**
|
|
* @vitest-environment jsdom
|
|
*/
|
|
import { act } from 'react'
|
|
import { createRoot, type Root } from 'react-dom/client'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useSmoothText } from '@/hooks/use-smooth-text'
|
|
|
|
interface ProbeProps {
|
|
content: string
|
|
isStreaming: boolean
|
|
snapOnNonAppend?: boolean
|
|
}
|
|
|
|
/**
|
|
* Minimal dependency-free hook harness (the repo has no `@testing-library/react`). Mounts the hook in
|
|
* a real React root under jsdom so effects and refs run exactly as in the app. Fake timers keep the
|
|
* paced reveal from advancing, so each assertion observes the synchronous reveal decision only.
|
|
*/
|
|
function renderSmoothText(initial: ProbeProps) {
|
|
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
const container = document.createElement('div')
|
|
const root: Root = createRoot(container)
|
|
const props = { ...initial }
|
|
let latest = ''
|
|
|
|
function Probe(p: ProbeProps) {
|
|
latest = useSmoothText(p.content, p.isStreaming, { snapOnNonAppend: p.snapOnNonAppend })
|
|
return null
|
|
}
|
|
|
|
const render = () =>
|
|
act(() => {
|
|
root.render(<Probe {...props} />)
|
|
})
|
|
render()
|
|
|
|
return {
|
|
value: () => latest,
|
|
rerender: (next: Partial<ProbeProps>) => {
|
|
Object.assign(props, next)
|
|
render()
|
|
},
|
|
unmount: () => act(() => root.unmount()),
|
|
}
|
|
}
|
|
|
|
const LONG = `# Existing Document\n\n${'Lorem ipsum dolor sit amet, '.repeat(8)}`
|
|
|
|
describe('useSmoothText — streaming that begins on an already-open document', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
})
|
|
afterEach(() => {
|
|
vi.clearAllTimers()
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('reveals a pre-existing document in full when an edit stream starts (no full-file replay)', () => {
|
|
// The editor mounts showing a static file (no stream yet).
|
|
const h = renderSmoothText({ content: '', isStreaming: false, snapOnNonAppend: true })
|
|
expect(h.value()).toBe('')
|
|
|
|
// The agent begins editing it: the first streamed value carries the whole existing document.
|
|
// It must appear instantly, not replay word-by-word from the first character.
|
|
h.rerender({ content: LONG, isStreaming: true })
|
|
expect(h.value()).toBe(LONG)
|
|
h.unmount()
|
|
})
|
|
|
|
it('still animates a brand-new file from the start (short content stays below the threshold)', () => {
|
|
// A create stream mounts already-streaming with a tiny first chunk → begins empty and paces in.
|
|
const h = renderSmoothText({ content: '# New file', isStreaming: true, snapOnNonAppend: true })
|
|
expect(h.value()).toBe('')
|
|
h.unmount()
|
|
})
|
|
|
|
it('shows content that is already large at mount in full (mount-time skip, unchanged)', () => {
|
|
const h = renderSmoothText({ content: LONG, isStreaming: true, snapOnNonAppend: true })
|
|
expect(h.value()).toBe(LONG)
|
|
h.unmount()
|
|
})
|
|
|
|
it('does not pre-reveal for chat (mounts already streaming with a small first chunk)', () => {
|
|
// Chat (no snapOnNonAppend) mounts streaming; the not-streaming→streaming edge never occurs, so
|
|
// the new transition skip cannot fire and ordinary paced reveal is preserved.
|
|
const h = renderSmoothText({ content: 'Hello', isStreaming: true })
|
|
expect(h.value()).toBe('')
|
|
h.unmount()
|
|
})
|
|
})
|