|
|
@@ -1,9 +1,10 @@
|
|
|
// @vitest-environment jsdom
|
|
|
/**
|
|
|
- * ui-deliverables browser half: the derivation contract of
|
|
|
- * `producedForClosing` over engine-published Turn data, the row's rendering
|
|
|
- * and opener wiring, and the plugin registrations' fiber-teardown removal
|
|
|
- * (HMR safety) against the real SlotRegistry.
|
|
|
+ * ui-deliverables browser half: the derivation contract of produced paths,
|
|
|
+ * recorded changes, and deliveries over engine-published Turn data, the
|
|
|
+ * changed-files card's rendering and opener wiring, and the plugin
|
|
|
+ * registrations' fiber-teardown removal (HMR safety) against the real
|
|
|
+ * SlotRegistry.
|
|
|
*/
|
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
|
import { cleanup, fireEvent, render, within } from '@testing-library/react'
|
|
|
@@ -23,10 +24,9 @@ import type { ChatFileMentions, TurnTailOwnerProps } from '@deepseek-ai/dsh-clie
|
|
|
import { makeTranslate, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
|
|
|
import { Deliverables, selectDeliverables, type DeliverablesInjected } from '../src/client/Deliverables.tsx'
|
|
|
import { PresentedOpenController } from '../src/client/present-open.ts'
|
|
|
-import { ProducedFiles } from '../src/client/ProducedFiles.tsx'
|
|
|
import {
|
|
|
- basename, deliverablesDefinition, presentedForClosing, producedFileMentions, producedForClosing, selectProducedFiles,
|
|
|
- type DeliverablesTurnData,
|
|
|
+ basename, changesForClosing, deliverablesDefinition, presentedForClosing, producedFileMentions, producedForClosing,
|
|
|
+ selectProducedFiles, type ChangesTurnData, type DeliverablesTurnData,
|
|
|
} from '../src/client/turn-deliverables.ts'
|
|
|
import { apply, inject } from '../src/client/index.ts'
|
|
|
import { en, zh } from '../src/client/locales.ts'
|
|
|
@@ -42,6 +42,7 @@ function openProps(controller = new PresentedOpenController()) {
|
|
|
usePresentedHost: <T,>(select: (state: ReturnType<typeof controller.host.getSnapshot>) => T): T =>
|
|
|
select(controller.host.getSnapshot()),
|
|
|
openPresented: vi.fn((...args: Parameters<PresentedOpenController['open']>) => controller.open(...args)),
|
|
|
+ openChanged: vi.fn((...args: Parameters<PresentedOpenController['openChanged']>) => controller.openChanged(...args)),
|
|
|
usePresentedOpen: <T,>(select: (state: ReturnType<typeof controller.state.getSnapshot>) => T): T =>
|
|
|
select(controller.state.getSnapshot()),
|
|
|
}
|
|
|
@@ -424,58 +425,135 @@ describe('produced-file Turn data', () => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
-describe('ProducedFiles row', () => {
|
|
|
- const t = makeTranslate(zh)
|
|
|
+const changedFile = (display: string, added = 1, deleted = 0, extra: { binary?: true; path?: string } = {}) =>
|
|
|
+ ({ path: extra.path ?? display, display, added, deleted, ...extra.binary === true ? { binary: true as const } : {} })
|
|
|
|
|
|
- it('renders the bounded chips and opens the file it was clicked for', () => {
|
|
|
- const paths = ['deep/a.html', 'b.css', 'c.ts', 'd.ts', 'e.ts', 'f.ts', 'g.ts', 'h.ts']
|
|
|
- const openFile = vi.fn<(path: string) => void>()
|
|
|
+const changesEvent = (seq: number, files: ReturnType<typeof changedFile>[], turn = 1, total = files.length) =>
|
|
|
+ at(seq, 'workspace/changes', { turn, files, total, snapshot: { before: 'a'.repeat(40), after: 'b'.repeat(40) } })
|
|
|
|
|
|
- const view = render(<ProducedFiles matched={paths} openFile={openFile} t={t} />)
|
|
|
- expect(view.getByText('本轮文件改动')).toBeTruthy()
|
|
|
- const row = view.container.querySelector('[data-produced-files-row]')
|
|
|
- if (!(row instanceof HTMLElement)) throw new Error('produced row missing')
|
|
|
- expect(within(row).getAllByRole('button')).toHaveLength(6)
|
|
|
- expect(within(row).getByText('+ 2 个文件')).toBeTruthy()
|
|
|
- const chip = view.getByRole('button', { name: '打开 deep/a.html' })
|
|
|
- expect(chip.textContent).toBe('a.html')
|
|
|
- expect(chip.getAttribute('title')).toBe('deep/a.html')
|
|
|
- expect(view.queryByRole('button', { name: '打开 g.ts' })).toBeNull()
|
|
|
- fireEvent.click(chip)
|
|
|
- // The row hands over the path it was given; where it opens is the
|
|
|
- // Sidebar's decision, not this row's.
|
|
|
- expect(openFile).toHaveBeenCalledWith('deep/a.html')
|
|
|
+describe('recorded changes Turn data', () => {
|
|
|
+ it('keeps the latest valid summary of the turn and ignores malformed or empty ones for the card', () => {
|
|
|
+ const value = assembler([
|
|
|
+ at(1, 'turn/start', { turn: 1 }),
|
|
|
+ changesEvent(2, [changedFile('old.ts')]),
|
|
|
+ at(3, 'workspace/changes', { turn: 1, files: [{ path: 'x' }], total: 1 }),
|
|
|
+ at(4, 'turn/end', { turn: 1, reason: { kind: 'completed' } }),
|
|
|
+ changesEvent(5, [changedFile('src/a.ts', 3, 1), changedFile('b.bin', 0, 0, { binary: true })], 1, 7),
|
|
|
+ at(6, 'turn/start', { turn: 2 }),
|
|
|
+ changesEvent(7, [], 2),
|
|
|
+ ])
|
|
|
+ const owner = tailOwner(deliverablesOf(value), 4)
|
|
|
+ expect(changesForClosing(owner)).toEqual({ seq: 5, total: 7, files: [changedFile('src/a.ts', 3, 1), changedFile('b.bin', 0, 0, { binary: true })] })
|
|
|
+ expect(selectDeliverables(owner)).toEqual({ changes: changesForClosing(owner), presented: [] })
|
|
|
+ expect(changesForClosing(tailOwner(deliverablesOf(value, 2), 8))).toBeNull()
|
|
|
+ expect(selectDeliverables(tailOwner(deliverablesOf(value, 2), 8))).toBeNull()
|
|
|
+ expect(changesForClosing(tailOwner(undefined, 8))).toBeNull()
|
|
|
})
|
|
|
|
|
|
- it('renders a remainder counter after every chip but the last when every file fits', () => {
|
|
|
- const view = render(<ProducedFiles matched={['a.md', 'b.md', 'c.md']} openFile={() => {}} t={t} />)
|
|
|
- const row = view.container.querySelector('[data-produced-files-row]')
|
|
|
- if (!(row instanceof HTMLElement)) throw new Error('produced row missing')
|
|
|
- expect(within(row).getAllByRole('button')).toHaveLength(3)
|
|
|
- // One counter per chip that could be the last visible one; the final chip hides nothing.
|
|
|
- expect([...row.querySelectorAll('[data-shown]')].map(node => node.getAttribute('data-shown'))).toEqual(['1', '2'])
|
|
|
+ it('preserves Turn data identity across unrelated appends', () => {
|
|
|
+ const value = assembler([at(1, 'turn/start', { turn: 1 }), changesEvent(2, [changedFile('a.ts')])])
|
|
|
+ const first = deliverablesOf(value)
|
|
|
+ value.append(call(3, 'later', 'read', { file_path: 'a.ts' }))
|
|
|
+ value.flush()
|
|
|
+ expect(deliverablesOf(value)).toBe(first)
|
|
|
})
|
|
|
+})
|
|
|
|
|
|
- it('offers no folder action, because a directory has no preview to open', () => {
|
|
|
+describe('ChangedFiles card', () => {
|
|
|
+ const files = [
|
|
|
+ changedFile('config/design-token', 42, 11), changedFile('config/feature-flags.json', 143, 32),
|
|
|
+ changedFile('config/launch-plan.yaml', 654, 9), changedFile('src/index.ts', 393, 274),
|
|
|
+ changedFile('~/.zshrc', 0, 0, { binary: true, path: '/home/u/.zshrc' }),
|
|
|
+ ]
|
|
|
+ const changes: ChangesTurnData = { seq: 5, files, total: 11 }
|
|
|
+
|
|
|
+ function renderCard(controller = new PresentedOpenController(), locale = en, matched = { changes, presented: [] as never[] }) {
|
|
|
+ const props = openProps(controller)
|
|
|
+ props.openChanged.mockResolvedValue(undefined)
|
|
|
const openFile = vi.fn<(path: string) => void>()
|
|
|
- const overflowing = ['a.md', 'b.md', 'c.md', 'd.md', 'e.md', 'f.md', 'g.md']
|
|
|
- const view = render(<ProducedFiles matched={overflowing} openFile={openFile} t={t} />)
|
|
|
- expect(view.queryByRole('button', { name: '在文件夹中显示' })).toBeNull()
|
|
|
- // Nothing in the row reaches the local machine any more.
|
|
|
+ const view = render(<Deliverables {...props} matched={matched} openFile={openFile} sessionId={SessionId('child-session')} t={makeTranslate(locale)} />)
|
|
|
+ return { props, openFile, view }
|
|
|
+ }
|
|
|
+
|
|
|
+ it('summarizes the turn, folds after three rows, and opens rows and the folder natively', () => {
|
|
|
+ const { props, openFile, view } = renderCard()
|
|
|
+ const card = view.container.querySelector('[data-changed-files]')
|
|
|
+ if (!(card instanceof HTMLElement)) throw new Error('changed-files card missing')
|
|
|
+ expect(within(card).getByText('Edited 11 files')).toBeTruthy()
|
|
|
+ expect(within(card).getByText('+1,232')).toBeTruthy()
|
|
|
+ expect(within(card).getByText('-326')).toBeTruthy()
|
|
|
+ expect(within(card).getAllByRole('listitem')).toHaveLength(3)
|
|
|
+ expect(within(card).getByText('config/design-token')).toBeTruthy()
|
|
|
+ expect(within(card).getByText('+42')).toBeTruthy()
|
|
|
+ expect(within(card).queryByText('src/index.ts')).toBeNull()
|
|
|
+ fireEvent.click(within(card).getByRole('button', { name: 'Open config/feature-flags.json in default app' }))
|
|
|
+ expect(props.openChanged).toHaveBeenLastCalledWith('child-session', 5, 1)
|
|
|
+ fireEvent.click(within(card).getByRole('button', { name: 'Open the folder containing the changed files' }))
|
|
|
+ expect(props.openChanged).toHaveBeenLastCalledWith('child-session', 5, null)
|
|
|
expect(openFile).not.toHaveBeenCalled()
|
|
|
+ const expand = within(card).getByRole('button', { name: 'Show all 5 changed files' })
|
|
|
+ expect(expand.getAttribute('aria-expanded')).toBe('false')
|
|
|
+ expect(expand.textContent).toContain('All 5 files')
|
|
|
+ fireEvent.click(expand)
|
|
|
+ expect(within(card).getAllByRole('listitem')).toHaveLength(5)
|
|
|
+ expect(within(card).getByText('binary')).toBeTruthy()
|
|
|
+ expect(within(card).getByRole('button', { name: 'Open ~/.zshrc in default app' }).getAttribute('title')).toBe('/home/u/.zshrc')
|
|
|
+ const collapse = within(card).getByRole('button', { name: 'Collapse changed files' })
|
|
|
+ expect(collapse.getAttribute('aria-expanded')).toBe('true')
|
|
|
+ expect(card.lastElementChild).toBe(collapse)
|
|
|
+ fireEvent.click(collapse)
|
|
|
+ expect(within(card).getAllByRole('listitem')).toHaveLength(3)
|
|
|
})
|
|
|
|
|
|
- it('uses singular English copy when exactly one file is hidden', () => {
|
|
|
- const view = render(
|
|
|
- <ProducedFiles
|
|
|
- matched={['a.md', 'b.md', 'c.md', 'd.md', 'e.md', 'f.md', 'g.md']}
|
|
|
- openFile={() => {}}
|
|
|
- t={makeTranslate(en)}
|
|
|
- />,
|
|
|
- )
|
|
|
- const row = view.container.querySelector('[data-produced-files-row]')
|
|
|
- if (!(row instanceof HTMLElement)) throw new Error('produced row missing')
|
|
|
- expect(within(row).getByText('+ 1 file')).toBeTruthy()
|
|
|
+ it('previews rows in the Sidebar and offers no folder action without a desktop', () => {
|
|
|
+ const controller = new PresentedOpenController()
|
|
|
+ const { openFile, props, view } = renderCard(controller, zh)
|
|
|
+ controller.host.set('error')
|
|
|
+ view.rerender(<Deliverables {...props} matched={{ changes, presented: [] }} openFile={openFile} sessionId={SessionId('child-session')} t={makeTranslate(zh)} />)
|
|
|
+ expect(view.queryByRole('button', { name: '打开改动文件所在的文件夹' })).toBeNull()
|
|
|
+ controller.host.set({ name: 'server', available: false, fileManager: null })
|
|
|
+ view.rerender(<Deliverables {...props} matched={{ changes, presented: [] }} openFile={openFile} sessionId={SessionId('child-session')} t={makeTranslate(zh)} />)
|
|
|
+ expect(view.getByText('已编辑 11 个文件')).toBeTruthy()
|
|
|
+ expect(view.queryByRole('button', { name: '打开改动文件所在的文件夹' })).toBeNull()
|
|
|
+ fireEvent.click(view.getByRole('button', { name: '在侧边栏打开 config/design-token' }))
|
|
|
+ expect(openFile).toHaveBeenCalledWith('config/design-token')
|
|
|
+ expect(props.openChanged).not.toHaveBeenCalled()
|
|
|
+ expect(view.getByRole('button', { name: '展开全部 5 个改动文件' }).textContent).toContain('全部 5 个文件')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('shows row and folder gesture states in place of the counts', () => {
|
|
|
+ const controller = new PresentedOpenController()
|
|
|
+ controller.state.set({
|
|
|
+ '/api/changes.open?sessionId=child-session&seq=5&index=0': 'opening',
|
|
|
+ '/api/changes.open?sessionId=child-session&seq=5&index=1': 'error',
|
|
|
+ '/api/changes.open?sessionId=child-session&seq=5&index=2': 'nativeUnavailable',
|
|
|
+ '/api/changes.open?sessionId=child-session&seq=5': 'opened',
|
|
|
+ })
|
|
|
+ const { view } = renderCard(controller)
|
|
|
+ expect(view.getByText(en['presented.opening'])).toBeTruthy()
|
|
|
+ expect((view.getByRole('button', { name: 'Open config/design-token in default app' }) as HTMLButtonElement).disabled).toBe(true)
|
|
|
+ expect(view.getByText(en['presented.error']).closest('[data-error]')).toBeTruthy()
|
|
|
+ expect(view.getByText(en['presented.nativeUnavailable'])).toBeTruthy()
|
|
|
+ expect(view.getByText(en['changes.folderOpened'])).toBeTruthy()
|
|
|
+ view.unmount()
|
|
|
+ controller.state.set({ '/api/changes.open?sessionId=child-session&seq=5': 'opening', '/api/changes.open?sessionId=child-session&seq=5&index=0': 'opened' })
|
|
|
+ const { view: pending } = renderCard(controller)
|
|
|
+ expect(pending.getByText(en['changes.folderOpening'])).toBeTruthy()
|
|
|
+ expect((pending.getByRole('button', { name: 'Open the folder containing the changed files' }) as HTMLButtonElement).disabled).toBe(true)
|
|
|
+ expect(pending.getByText(en['presented.opened'])).toBeTruthy()
|
|
|
+ pending.unmount()
|
|
|
+ controller.state.set({ '/api/changes.open?sessionId=child-session&seq=5': 'error' })
|
|
|
+ const { view: failed } = renderCard(controller)
|
|
|
+ expect(failed.getByText(en['changes.folderError']).closest('[data-error]')).toBeTruthy()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('renders without a fold for three files or fewer and beside delivery cards', () => {
|
|
|
+ const short: ChangesTurnData = { seq: 5, files: files.slice(0, 2), total: 2 }
|
|
|
+ const { view } = renderCard(new PresentedOpenController(), en, { changes: short, presented: [{ path: 'report.pdf', seq: 6, index: 0 }] as never[] })
|
|
|
+ expect(view.getByText('Edited 2 files')).toBeTruthy()
|
|
|
+ expect(view.queryByRole('button', { name: /Show all|Collapse changed/ })).toBeNull()
|
|
|
+ expect(view.container.querySelectorAll('[data-presented-file]')).toHaveLength(1)
|
|
|
+ expect(view.container.querySelector('[data-presented-files-row]')?.parentElement?.getAttribute('data-after-changes')).toBe('true')
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -572,6 +650,8 @@ describe('plugin registration', () => {
|
|
|
expect(face.hooks.presentedHost.getSnapshot()).toBeNull()
|
|
|
await face.openPresented(SessionId('child-session'), 2, 0)
|
|
|
expect(face.hooks.presentedOpen.getSnapshot()['/api/present.open?sessionId=child-session&seq=2&index=0']).toBe('opened')
|
|
|
+ await face.openChanged(SessionId('child-session'), 5, null)
|
|
|
+ expect(face.hooks.presentedOpen.getSnapshot()['/api/changes.open?sessionId=child-session&seq=5']).toBe('opened')
|
|
|
// A turn that produced nothing yields no vocabulary at all.
|
|
|
expect(service?.forClosing(tailOwner(undefined, 2), SessionId('viewed-session'))).toBeUndefined()
|
|
|
|
|
|
@@ -636,7 +716,7 @@ describe('presented files', () => {
|
|
|
expect(props.openPresented).toHaveBeenCalledWith('child-session', 2, 0, 'open')
|
|
|
fireEvent.click(view.getByRole('button', { name: 'Collapse delivered files' }))
|
|
|
expect(view.container.querySelectorAll('[data-presented-file]')).toHaveLength(4)
|
|
|
- expect(view.queryByText('Files changed')).toBeNull()
|
|
|
+ expect(view.container.querySelector('[data-changed-files]')).toBeNull()
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -648,22 +728,23 @@ it.each([null, [], 'invalid'])('declines non-object delivery data: %j', (data) =
|
|
|
it.each([{}, { turn: '1', callId: 'bad', files: [] },
|
|
|
{ turn: 1.5, callId: 'bad', files: [] }, { turn: 0, callId: 'bad', files: [] },
|
|
|
{ turn: 1, files: [] }, { turn: 1, callId: '', files: [] }, { turn: 1, callId: 'bad', files: null },
|
|
|
-])('ignores malformed delivery data and keeps the existing produced row: %j', (data) => {
|
|
|
+])('ignores malformed delivery data and keeps the recorded changes card: %j', (data) => {
|
|
|
const value = assembler([
|
|
|
at(1, 'turn/start', { turn: 1 }),
|
|
|
call(2, 'write-a', 'write', { file_path: 'a.txt', content: 'a' }),
|
|
|
result(3, 'write-a'),
|
|
|
at(4, 'deliverables/presented', data),
|
|
|
+ at(5, 'workspace/changes', { turn: 1, files: [{ path: 'a.txt', display: 'a.txt', added: 1, deleted: 0 }], total: 1 }),
|
|
|
])
|
|
|
- const owner = tailOwner(deliverablesOf(value), 5)
|
|
|
+ const owner = tailOwner(deliverablesOf(value), 6)
|
|
|
const matched = selectDeliverables(owner)!
|
|
|
const view = render(<Deliverables {...openProps()} matched={matched} openFile={owner.openFile} sessionId={SessionId('session')} t={makeTranslate(en)} />)
|
|
|
- expect(view.getByText('Files changed')).toBeTruthy()
|
|
|
+ expect(view.getByText('Edited 1 files')).toBeTruthy()
|
|
|
expect(view.queryByText('Deliverables')).toBeNull()
|
|
|
})
|
|
|
|
|
|
it('shows descriptions and falls back to file metadata without hiding extensionless deliveries', () => {
|
|
|
- const view = render(<Deliverables {...openProps()} matched={{ produced: [], presented: [
|
|
|
+ const view = render(<Deliverables {...openProps()} matched={{ changes: null, presented: [
|
|
|
{ path: 'out/report.txt', description: 'Quarterly summary', seq: 2, index: 0 },
|
|
|
{ path: 'LICENSE', seq: 2, index: 1 },
|
|
|
] }} openFile={() => {}} sessionId={SessionId('session')} t={makeTranslate(en)} />)
|
|
|
@@ -673,18 +754,9 @@ it('shows descriptions and falls back to file metadata without hiding extensionl
|
|
|
expect(view.getByText('report.txt')).toBeTruthy()
|
|
|
})
|
|
|
|
|
|
-it('marks delivery cards that directly follow the produced-files row', () => {
|
|
|
- const shared = { ...openProps(), openFile: () => {}, sessionId: SessionId('session'), t: makeTranslate(en) }
|
|
|
- const presented = [{ path: 'report.txt', seq: 2, index: 0 }]
|
|
|
- const view = render(<Deliverables {...shared} matched={{ produced: ['source.ts'], presented }} />)
|
|
|
- expect(view.getByText('Files changed')).toBeTruthy()
|
|
|
- expect(view.container.querySelector('[data-presented-files-row]')?.parentElement
|
|
|
- ?.getAttribute('data-after-produced-files')).toBe('true')
|
|
|
-})
|
|
|
-
|
|
|
it('distinguishes PDF, Word, Markdown, and code files with compact decorative card icons', () => {
|
|
|
const paths = ['report.pdf', 'report.docx', 'README.md', 'index.tsx']
|
|
|
- const view = render(<Deliverables {...openProps()} matched={{ produced: [], presented:
|
|
|
+ const view = render(<Deliverables {...openProps()} matched={{ changes: null, presented:
|
|
|
paths.map((path, index) => ({ path, seq: 2, index })),
|
|
|
}} openFile={() => {}} sessionId={SessionId('session')} t={makeTranslate(en)} />)
|
|
|
const icons = [...view.container.querySelectorAll('[data-presented-file]')].map((card) => {
|
|
|
@@ -697,7 +769,7 @@ it('distinguishes PDF, Word, Markdown, and code files with compact decorative ca
|
|
|
})
|
|
|
|
|
|
it('lets one delivered file span the complete row without an expansion control', () => {
|
|
|
- const view = render(<Deliverables {...openProps()} matched={{ produced: [], presented: [
|
|
|
+ const view = render(<Deliverables {...openProps()} matched={{ changes: null, presented: [
|
|
|
{ path: 'report.pdf', seq: 2, index: 0 },
|
|
|
] }} openFile={() => {}} sessionId={SessionId('session')} t={makeTranslate(en)} />)
|
|
|
expect(view.container.querySelector('[data-presented-files-row]')?.getAttribute('data-single')).toBe('true')
|
|
|
@@ -709,7 +781,7 @@ it.each(['opening', 'opened', 'error'] as const)('shows the %s state and permits
|
|
|
const controller = new PresentedOpenController()
|
|
|
controller.state.set({ '/api/present.open?sessionId=session&seq=2&index=0': phase })
|
|
|
const props = openProps(controller)
|
|
|
- const view = render(<Deliverables {...props} matched={{ produced: [], presented: [
|
|
|
+ const view = render(<Deliverables {...props} matched={{ changes: null, presented: [
|
|
|
{ path: 'report.txt', seq: 2, index: 0 },
|
|
|
] }} openFile={() => {}} sessionId={SessionId('session')} t={makeTranslate(en)} />)
|
|
|
expect(view.getByText(en[`presented.${phase}`])).toBeTruthy()
|
|
|
@@ -720,7 +792,7 @@ it.each(['opening', 'opened', 'error'] as const)('shows the %s state and permits
|
|
|
it('explains a missing desktop and retries failed Host metadata', () => {
|
|
|
const controller = new PresentedOpenController()
|
|
|
const props = openProps(controller)
|
|
|
- const matched = { produced: [], presented: [{ path: 'file.txt', seq: 2, index: 0 }] }
|
|
|
+ const matched = { changes: null, presented: [{ path: 'file.txt', seq: 2, index: 0 }] }
|
|
|
controller.host.set('error')
|
|
|
const view = render(<Deliverables {...props} matched={matched} openFile={() => {}} sessionId={SessionId('session')} t={makeTranslate(en)} />)
|
|
|
props.reloadPresentedHost.mockResolvedValue(undefined)
|
|
|
@@ -732,14 +804,15 @@ it('explains a missing desktop and retries failed Host metadata', () => {
|
|
|
})
|
|
|
|
|
|
|
|
|
-it('loads desktop information only when delivery cards appear', () => {
|
|
|
+it('loads desktop information once the tail renders and not again while it is known', () => {
|
|
|
const controller = new PresentedOpenController()
|
|
|
const props = openProps(controller)
|
|
|
controller.host.set(null)
|
|
|
props.reloadPresentedHost.mockResolvedValue(undefined)
|
|
|
const shared = { ...props, openFile: () => {}, sessionId: SessionId('session'), t: makeTranslate(en) }
|
|
|
- const view = render(<Deliverables {...shared} matched={{ produced: ['source.ts'], presented: [] }} />)
|
|
|
- expect(props.reloadPresentedHost).not.toHaveBeenCalled()
|
|
|
- view.rerender(<Deliverables {...shared} matched={{ produced: [], presented: [{ path: 'report.txt', seq: 2, index: 0 }] }} />)
|
|
|
+ const view = render(<Deliverables {...shared} matched={{ changes: { seq: 2, total: 1, files: [{ path: 'a.ts', display: 'a.ts', added: 1, deleted: 0 }] }, presented: [] }} />)
|
|
|
+ expect(props.reloadPresentedHost).toHaveBeenCalledOnce()
|
|
|
+ controller.host.set({ name: 'desktop', available: true, fileManager: 'finder' })
|
|
|
+ view.rerender(<Deliverables {...shared} matched={{ changes: null, presented: [{ path: 'report.txt', seq: 2, index: 0 }] }} />)
|
|
|
expect(props.reloadPresentedHost).toHaveBeenCalledOnce()
|
|
|
})
|