74 lines
5.6 KiB
TypeScript
74 lines
5.6 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { afterEach, expect, it, vi } from 'vitest'
|
|
import { hostInvoke } from '@/services/platform/desktop'
|
|
import SyncSettings from './SyncSettings.vue'
|
|
vi.mock('@/services/platform/preferenceSync', () => ({ preferenceSyncIssues: [], resolvePreferenceDraft: vi.fn(), seedCurrentPreferences: vi.fn() }))
|
|
vi.mock('@/services/platform/desktop', () => ({ hostInvoke: vi.fn() }))
|
|
const confirm = vi.hoisted(() => vi.fn())
|
|
vi.mock('@/composables/useActionDialog', () => ({ useActionDialog: () => ({ actionDialog: null, resolveAction: vi.fn(), askConfirm: confirm }) }))
|
|
afterEach(() => { vi.useRealTimers(); vi.resetAllMocks() })
|
|
const empty = () => ({ vault_id: 'local', binding: null, paused: false, pending: 0, conflicts: [], credential_state: 'unbound', running: false, error: null, retry_in: null })
|
|
it('clears login password, explicitly opts into HTTP and stops polling on unmount', async () => {
|
|
vi.useFakeTimers()
|
|
vi.mocked(hostInvoke).mockImplementation(async command => command === 'sync_status' ? empty() : command === 'sync_login' ? { endpoint: 'http://test.example:18080/', account: 'test' } : { items: [] })
|
|
const wrapper = mount(SyncSettings); await flushPromises()
|
|
await wrapper.get('input[type=url]').setValue('http://test.example:18080')
|
|
await wrapper.get('input[autocomplete=username]').setValue('test')
|
|
await wrapper.get('input[type=password]').setValue('private-fixture')
|
|
await wrapper.get('input[type=checkbox]').setValue(true)
|
|
await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
expect(hostInvoke).toHaveBeenCalledWith('sync_login', { request: { endpoint: 'http://test.example:18080', account: 'test', password: 'private-fixture', device_name: 'OpenNexus Desktop', allow_test_http: true } })
|
|
expect((wrapper.get('input[type=password]').element as HTMLInputElement).value).toBe('')
|
|
expect(wrapper.text()).not.toContain('private-fixture')
|
|
wrapper.unmount(); const count = vi.mocked(hostInvoke).mock.calls.length
|
|
await vi.advanceTimersByTimeAsync(6000); expect(hostInvoke).toHaveBeenCalledTimes(count)
|
|
})
|
|
it('cancels destructive choices and binds accepted conflict decisions to their snapshot', async () => {
|
|
const state = { ...empty(), binding: { id: 'binding', endpoint: 'https://test.example/', account: 'test', remote_vault: 'remote', cursor: 7 }, credential_state: 'ready', conflicts: [{ sequence: 7, local_path: 'note.md', local_hash: 'original-hash', remote: { path: 'note.md', operation: 'put' } }] }
|
|
vi.mocked(hostInvoke).mockResolvedValue(state)
|
|
const wrapper = mount(SyncSettings); await flushPromises()
|
|
const button = wrapper.findAll('button').find(button => button.text() === '采用远端')!
|
|
confirm.mockResolvedValue(false); await button.trigger('click'); await flushPromises()
|
|
expect(vi.mocked(hostInvoke).mock.calls.some(([command]) => command === 'sync_resolve')).toBe(false)
|
|
confirm.mockResolvedValue(true); await button.trigger('click'); await flushPromises()
|
|
expect(hostInvoke).toHaveBeenCalledWith('sync_resolve', { bindingId: 'binding', sequence: 7, choice: 'remote', destination: '', expected: 'original-hash' })
|
|
expect(wrapper.get('input[type=url]').attributes('disabled')).toBeDefined()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('requires a reviewed merge fingerprint and invalidates preview when the remote changes', async () => {
|
|
vi.mocked(hostInvoke).mockImplementation(async command => {
|
|
if (command === 'sync_status') return empty()
|
|
if (command === 'sync_login') return { endpoint: 'https://test.example/', account: 'test' }
|
|
if (command === 'sync_vaults') return { items: [{ id: 'one', name: 'One', sequence: 2 }, { id: 'two', name: 'Two', sequence: 3 }] }
|
|
if (command === 'sync_preview') return { fingerprint: 'reviewed-snapshot', boundary: 2, items: [{ path: 'same.md', action: 'conflict' }] }
|
|
return null
|
|
})
|
|
const wrapper = mount(SyncSettings); await flushPromises()
|
|
await wrapper.get('input[type=url]').setValue('https://test.example/')
|
|
await wrapper.get('input[autocomplete=username]').setValue('test')
|
|
await wrapper.get('input[type=password]').setValue('fixture')
|
|
await wrapper.get('form').trigger('submit'); await flushPromises()
|
|
await wrapper.get('select').setValue('one')
|
|
await wrapper.findAll('button').find(button => button.text() === '预览合并')!.trigger('click'); await flushPromises()
|
|
expect(wrapper.text()).toContain('same.md')
|
|
await wrapper.get('select').setValue('two')
|
|
expect(wrapper.text()).not.toContain('same.md')
|
|
await wrapper.get('select').setValue('one')
|
|
await wrapper.findAll('button').find(button => button.text() === '预览合并')!.trigger('click'); await flushPromises()
|
|
confirm.mockResolvedValue(true)
|
|
await wrapper.findAll('button').find(button => button.text() === '确认合并并绑定')!.trigger('click'); await flushPromises()
|
|
expect(hostInvoke).toHaveBeenCalledWith('sync_bind', { request: { vault_id: 'local', endpoint: 'https://test.example/', account: 'test', remote_vault: 'one', mode: 'merge', fingerprint: 'reviewed-snapshot' } })
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('shows a persisted halt without suggesting an automatic retry countdown', async () => {
|
|
vi.mocked(hostInvoke).mockResolvedValue({ ...empty(), binding: { id: 'binding', endpoint: 'https://test.example/', account: 'test', remote_vault: 'remote', cursor: 7 }, halted: true, failures: 3, error: 'UNAUTHORIZED', retry_in: 120 })
|
|
const wrapper = mount(SyncSettings); await flushPromises()
|
|
expect(wrapper.text()).toContain('自动同步已停止')
|
|
expect(wrapper.text()).toContain('UNAUTHORIZED')
|
|
expect(wrapper.text()).not.toContain('120s')
|
|
wrapper.unmount()
|
|
})
|