Files
deepseek-harness/packages/client/ui-settings-general/tests/components.client.spec.tsx
T
Pine e87a3084ea feat(desktop): manual-download update check and OSS release manifest
Adds an unsigned, manual-download update flow. The main process fetches the OSS
updates/releases.json manifest (DSH_UPDATE_URL overrides), compares the latest
version, and on startup and hourly prompts to open the per-platform installer
URL; a preload bridge exposes the same check to the SPA's About "check for
updates" button, which renders the version, notes, and a download link.
scripts/generate-release-json.mjs builds the manifest from the packaged
.dmg/.exe. Windows nsis packaging is configured.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 20:45:21 +08:00

193 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import type { GeneralSectionComponentProps } from '../src/client/GeneralSection.tsx'
import { GeneralSection } from '../src/client/GeneralSection.tsx'
import { AboutSection } from '../src/client/AboutSection.tsx'
import type { AboutSectionComponentProps } from '../src/client/AboutSection.tsx'
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
import type { TriggerContentProps } from '../src/client/chrome.tsx'
import { SettingsDocumentAction } from '../src/client/SettingsDocumentAction.tsx'
import { SettingsDocumentStore } from '../src/client/settings-document-store.ts'
import { en } from '../src/client/locales.ts'
afterEach(cleanup)
// The seat's key domain is settings ∪ common; the stub answers from the
// package dictionary and falls back to the key like the real chain.
const t: TriggerContentProps['t'] = key => (en as Record<string, string>)[key] ?? key
// Global standard kit stubs: none of these components consume the hooks.
const unusedHook = (() => { throw new Error('unused by settings-general components') }) as never
const kit = { useSessions: unusedHook, useWorkspaces: unusedHook }
describe('chrome content', () => {
it('TriggerContent renders the icon with the label in the wide column', () => {
const { container } = render(<TriggerContent {...kit} wide t={t} />)
expect(container.querySelector('svg')).toBeTruthy()
expect(screen.getByText('Settings')).toBeTruthy()
})
it('TriggerContent drops the label in the rail state', () => {
const { container } = render(<TriggerContent {...kit} wide={false} t={t} />)
expect(container.querySelector('svg')).toBeTruthy()
expect(screen.queryByText('Settings')).toBeNull()
})
it('HeaderContent and CloseLabel render their translated text', () => {
render(<HeaderContent {...kit} t={t} />)
render(<CloseLabel {...kit} t={t} />)
expect(screen.getByText('Settings')).toBeTruthy()
expect(screen.getByText('Close')).toBeTruthy()
})
})
describe('GeneralSection', () => {
function mount() {
const renderSlot = vi.fn(
((key: string) => <div data-testid={`slot-${key}`} />) as GeneralSectionComponentProps['renderSlot'],
)
const props: GeneralSectionComponentProps = { ...kit, renderSlot, close: vi.fn() }
const view = render(<GeneralSection {...props} />)
return { view, renderSlot }
}
it('renders the item slot as the section body', () => {
const { renderSlot } = mount()
expect(renderSlot).toHaveBeenCalledWith('settings.general.item', {})
expect(screen.getByTestId('slot-settings.general.item')).toBeTruthy()
})
})
describe('AboutSection', () => {
const aboutProps: AboutSectionComponentProps = { close: vi.fn(), t } as never
it('renders the company copy and the current version', () => {
render(<AboutSection {...aboutProps} />)
expect(screen.getByText('PineSound')).toBeTruthy()
expect(screen.getByText('AI audio creation platform')).toBeTruthy()
expect(screen.getByText(en['about.currentVersion'])).toBeTruthy()
expect(screen.getByText('0.1.0-rc.5')).toBeTruthy()
expect(screen.getByRole('button', { name: en['about.checkUpdates'] })).toBeTruthy()
})
it('reports up-to-date on a web surface without the desktop bridge', async () => {
render(<AboutSection {...aboutProps} />)
fireEvent.click(screen.getByRole('button', { name: en['about.checkUpdates'] }))
await waitFor(() => {
expect(screen.getByRole('status').textContent).toContain(en['about.upToDate'])
})
})
it('shows a download link when the bridge reports a newer version', async () => {
const checkUpdate = vi.fn(async () => ({
status: 'update-available' as const, current: '0.1.0-rc.5', latest: '0.1.0-rc.6',
notes: 'New release', url: 'https://example.com/app.dmg',
}))
;(window as { dshApp?: unknown }).dshApp = { checkUpdate }
try {
render(<AboutSection {...aboutProps} />)
fireEvent.click(screen.getByRole('button', { name: en['about.checkUpdates'] }))
const status = await screen.findByRole('status')
expect(status.textContent).toContain(en['about.updateAvailable'])
expect(status.textContent).toContain('0.1.0-rc.6')
expect(screen.getByRole('link', { name: en['about.download'] }).getAttribute('href')).toBe('https://example.com/app.dmg')
} finally {
delete (window as { dshApp?: unknown }).dshApp
}
})
})
describe('SettingsDocumentAction', () => {
it('appears only for a file-backed provider and requests its Host-owned document', async () => {
const openDocument = vi.fn(() => Promise.resolve({
rpcId: 'document-open' as never,
result: { ok: true as const, value: { opened: true as const } },
}))
const controller = new SettingsDocumentStore({
settings: {
describe: vi.fn(() => Promise.resolve({
rpcId: 'document-action' as never,
result: {
ok: true as const,
value: { writable: true, hasDocument: true, namespaces: [] },
},
})),
openDocument,
},
} as never)
render(<SettingsDocumentAction
{...kit}
t={t}
controller={controller}
useSnapshot={bindSnapshotSelector(controller.store)}
/>)
const action = await screen.findByRole('button', { name: 'Open configuration file' })
fireEvent.click(action)
await waitFor(() => { expect(openDocument).toHaveBeenCalledWith({}) })
})
it('stays absent without a document and retries availability after remount', async () => {
const describe = vi.fn()
.mockResolvedValueOnce({
rpcId: 'document-action-absent' as never,
result: { ok: true as const, value: { writable: true, hasDocument: false, namespaces: [] } },
})
.mockResolvedValueOnce({
rpcId: 'document-action-ready' as never,
result: { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] } },
})
const controller = new SettingsDocumentStore({
settings: {
describe,
openDocument: vi.fn(),
},
} as never)
const first = render(<SettingsDocumentAction
{...kit}
t={t}
controller={controller}
useSnapshot={bindSnapshotSelector(controller.store)}
/>)
await waitFor(() => { expect(controller.store.getSnapshot().status).toBe('unavailable') })
expect(screen.queryByRole('button', { name: 'Open configuration file' })).toBeNull()
first.unmount()
render(<SettingsDocumentAction
{...kit}
t={t}
controller={controller}
useSnapshot={bindSnapshotSelector(controller.store)}
/>)
expect(await screen.findByRole('button', { name: 'Open configuration file' })).toBeTruthy()
expect(describe).toHaveBeenCalledTimes(2)
})
it('keeps the action available and reports a native-open failure', async () => {
const controller = new SettingsDocumentStore({
settings: {
describe: vi.fn(() => Promise.resolve({
rpcId: 'document-action' as never,
result: {
ok: true as const,
value: { writable: true, hasDocument: true, namespaces: [] },
},
})),
openDocument: vi.fn(() => Promise.resolve({
rpcId: 'document-open-failed' as never,
result: { ok: false as const, error: { code: 'internal' as const, message: 'xdg-open missing', details: {} } },
})),
},
} as never)
render(<SettingsDocumentAction
{...kit}
t={t}
controller={controller}
useSnapshot={bindSnapshotSelector(controller.store)}
/>)
fireEvent.click(await screen.findByRole('button', { name: 'Open configuration file' }))
expect((await screen.findByRole('alert')).textContent).toBe('Could not open configuration file')
expect(screen.getByRole('button', { name: 'Open configuration file' })).toBeTruthy()
})
})