aaa434afbd
The About section hardcoded its version; the main process now exposes get-app-info so it reads app.getVersion() dynamically, and the update result carries the manifest release date. The manifest generator orders versions numerically (0.10.0 outranks 0.9.0). A new stage-release script copies releases.json plus installers into the deepseek-harness-web site root's updates/, which deploy.py publishes to OSS alongside the download page, so the app and web share one update channel. The About section also gains links to the release site and official site. Co-Authored-By: Claude <noreply@anthropic.com>
215 lines
9.0 KiB
TypeScript
215 lines
9.0 KiB
TypeScript
// @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 a current-version row (fallback on plain web)', () => {
|
||
render(<AboutSection {...aboutProps} />)
|
||
expect(screen.getByText('PineSound')).toBeTruthy()
|
||
expect(screen.getByText('AI audio creation platform')).toBeTruthy()
|
||
expect(screen.getByText(en['about.currentVersion'])).toBeTruthy()
|
||
// No desktop bridge on a plain web surface: the version row falls back.
|
||
expect(screen.getByText('—')).toBeTruthy()
|
||
expect(screen.getByRole('button', { name: en['about.checkUpdates'] })).toBeTruthy()
|
||
})
|
||
|
||
it('shows the packaged version from the desktop bridge', async () => {
|
||
;(window as { dshApp?: unknown }).dshApp = { getAppInfo: vi.fn(async () => ({ version: '0.1.0-rc.5' })) }
|
||
try {
|
||
render(<AboutSection {...aboutProps} />)
|
||
await screen.findByText('0.1.0-rc.5')
|
||
} finally {
|
||
delete (window as { dshApp?: unknown }).dshApp
|
||
}
|
||
})
|
||
|
||
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',
|
||
date: '2026-08-01', notes: 'New release', url: 'https://example.com/app.dmg',
|
||
}))
|
||
;(window as { dshApp?: unknown }).dshApp = {
|
||
checkUpdate,
|
||
getAppInfo: vi.fn(async () => ({ version: '0.1.0-rc.5' })),
|
||
}
|
||
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(status.textContent).toContain('2026-08-01')
|
||
expect(status.textContent).toContain('New release')
|
||
expect(screen.getByRole('link', { name: en['about.download'] }).getAttribute('href')).toBe('https://example.com/app.dmg')
|
||
} finally {
|
||
delete (window as { dshApp?: unknown }).dshApp
|
||
}
|
||
})
|
||
|
||
it('links to the release site and the official site', () => {
|
||
render(<AboutSection {...aboutProps} />)
|
||
expect(screen.getByRole('link', { name: en['about.releaseSite'] }).getAttribute('href')).toBe('https://deepseek.pinesound.cn/')
|
||
expect(screen.getByRole('link', { name: en['about.officialSite'] }).getAttribute('href')).toBe('https://www.deepseek.com/')
|
||
})
|
||
})
|
||
|
||
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()
|
||
})
|
||
})
|