import { readFileSync } from 'node:fs' import { runInContext } from 'node:vm' import { JSDOM } from 'jsdom' import { expect, it, vi } from 'vitest' import { resolveDesktopLocale } from '../src/locale.ts' it('keeps disabled packages visible and offers recovery without a running backend', async () => { const dom = new JSDOM(readFileSync(new URL('../renderer/plugin-manager.html', import.meta.url), 'utf8'), { runScripts: 'outside-only' }) let enabled = true let ready = false const disableAll = vi.fn(async () => { enabled = false; ready = true }) const toggle = vi.fn(async (_name: string, active: boolean) => { enabled = active }) const api = { locale: async () => resolveDesktopLocale('en'), backend: { status: async () => ready ? { phase: 'ready' } : { phase: 'error', message: 'plugin requires Cordis ^2.0.0' }, retry: vi.fn() }, plugins: { list: async () => [{ name: 'example-plugin', version: '1.0.0', enabled }], disableAll, toggle }, } Object.defineProperty(dom.window, 'dshDesktop', { value: api }) try { runInContext(readFileSync(new URL('../renderer/plugin-manager.js', import.meta.url), 'utf8'), dom.getInternalVMContext()) const document = dom.window.document await expect.poll(() => document.querySelector('#plugins li')?.textContent).toContain('example-plugin') expect(document.querySelector('#recovery')?.hidden).toBe(false) expect(document.querySelector('#startup-error')?.textContent).toBe('plugin requires Cordis ^2.0.0') document.querySelector('#disable-all')?.click() await expect.poll(() => document.querySelector('#recovery')?.hidden).toBe(true) expect(disableAll).toHaveBeenCalledOnce() expect(document.querySelector('#plugins li')?.textContent).toMatchInlineSnapshot('"example-plugin1.0.0 ยท DisabledEnableUpdateRemove"') document.querySelector('#plugins li button')?.click() await expect.poll(() => toggle.mock.calls).toEqual([['example-plugin', true]]) await expect.poll(() => document.querySelector('#plugins li')?.textContent).toBe('example-plugin1.0.0DisableUpdateRemove') } finally { dom.window.close() } }) it.each(['en', 'zh-CN'])('updates through the inline form, including the installed version (%s)', async (language) => { const dom = new JSDOM(readFileSync(new URL('../renderer/plugin-manager.html', import.meta.url), 'utf8'), { runScripts: 'outside-only' }) const locale = resolveDesktopLocale(language) const update = vi.fn(async () => {}) Object.defineProperty(dom.window, 'dshDesktop', { value: { locale: async () => locale, backend: { status: async () => ({ phase: 'ready' }) }, plugins: { list: async () => [{ name: 'example-plugin', version: '1.0.0', enabled: true }], update }, } }) try { runInContext(readFileSync(new URL('../renderer/plugin-manager.js', import.meta.url), 'utf8'), dom.getInternalVMContext()) const document = dom.window.document await expect.poll(() => document.querySelector('#plugins li')?.textContent).toContain('example-plugin') const updateButton = () => document.querySelectorAll('#plugins li button')[1]! const form = document.querySelector('#update-form')! const version = document.querySelector('#update-version')! updateButton().click() expect(form.hidden).toBe(false) expect(version.value).toBe('1.0.0') expect(document.activeElement).toBe(version) expect(document.querySelector('#update-label')?.textContent).toBe(locale.messages.targetVersion.replace('{name}', 'example-plugin')) expect(document.querySelector('#update-cancel')?.textContent).toBe(locale.messages.cancel) if (language === 'en') { expect({ label: document.querySelector('#update-label')?.textContent, actions: [...form.querySelectorAll('button')].map(button => button.textContent) }).toMatchInlineSnapshot(` { "actions": [ "Update", "Cancel", ], "label": "Enter the target version for example-plugin", } `) } document.querySelector('#update-cancel')!.click() expect(form.hidden).toBe(true) expect(document.activeElement).toBe(updateButton()) expect(update).not.toHaveBeenCalled() for (const target of ['1.0.0', '^2.0.0']) { updateButton().click() version.value = target form.requestSubmit() await expect.poll(() => document.querySelector('#status')?.textContent).toBe(locale.messages.operationComplete) expect(update).toHaveBeenLastCalledWith('example-plugin', target) expect(form.hidden).toBe(true) } expect(update).toHaveBeenCalledTimes(2) } finally { dom.window.close() } })