From ba66b182af618d845b1c613ab5f46b8aa6a197b9 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sun, 6 Sep 2026 00:13:32 +0800 Subject: [PATCH] fix(frontend): unify extension installation and restore theme preview scrolling --- .../common/ExtensionInstallDialog.spec.ts | 40 +++++++++++ .../common/ExtensionInstallDialog.vue | 66 +++++++++++++++++++ frontend/src/features/plugins/PluginsView.vue | 11 ++-- frontend/src/features/skills/SkillsView.vue | 11 ++-- .../themes/CommunityThemePreview.spec.ts | 8 +++ .../features/themes/CommunityThemePreview.vue | 2 +- frontend/src/features/themes/ThemesView.vue | 2 +- 7 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/common/ExtensionInstallDialog.spec.ts create mode 100644 frontend/src/components/common/ExtensionInstallDialog.vue diff --git a/frontend/src/components/common/ExtensionInstallDialog.spec.ts b/frontend/src/components/common/ExtensionInstallDialog.spec.ts new file mode 100644 index 0000000..819858d --- /dev/null +++ b/frontend/src/components/common/ExtensionInstallDialog.spec.ts @@ -0,0 +1,40 @@ +// @vitest-environment happy-dom +import { afterEach, expect, it, vi } from 'vitest' +import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' +import ExtensionInstallDialog from './ExtensionInstallDialog.vue' + +let wrapper: VueWrapper +afterEach(() => { wrapper?.unmount() }) + +it.each(['Skill', 'Plugin'] as const)('installs %s from a trimmed directory and prevents duplicate submissions', async kind => { + let complete!: () => void + const install = vi.fn(() => new Promise(resolve => { complete = resolve })) + wrapper = mount(ExtensionInstallDialog, { props: { kind, install } }) + expect(wrapper.text()).toContain(`${kind.toLowerCase()}.yaml`) + expect(wrapper.get('button[type="submit"]').attributes('disabled')).toBeDefined() + await wrapper.get('input').setValue(' G:\\packages\\example ') + await wrapper.get('form').trigger('submit') + await wrapper.get('form').trigger('submit') + expect(install).toHaveBeenCalledExactlyOnceWith('G:\\packages\\example') + await wrapper.get('dialog').trigger('cancel') + expect(wrapper.emitted('close')).toBeUndefined() + expect(wrapper.get('input').attributes('disabled')).toBeDefined() + complete() + await flushPromises() + expect(wrapper.emitted('installed')).toHaveLength(1) +}) + +it('keeps the path and displays validation errors for retry', async () => { + const install = vi.fn().mockRejectedValueOnce(new Error('Manifest does not exist')).mockResolvedValueOnce(undefined) + wrapper = mount(ExtensionInstallDialog, { props: { kind: 'Plugin', install } }) + await wrapper.get('input').setValue('G:\\packages\\example') + await wrapper.get('form').trigger('submit') + await flushPromises() + expect(wrapper.get('[role="alert"]').text()).toBe('Manifest does not exist') + expect((wrapper.get('input').element as HTMLInputElement).value).toBe('G:\\packages\\example') + expect(wrapper.emitted('installed')).toBeUndefined() + await wrapper.get('form').trigger('submit') + await flushPromises() + expect(wrapper.find('[role="alert"]').exists()).toBe(false) + expect(wrapper.emitted('installed')).toHaveLength(1) +}) diff --git a/frontend/src/components/common/ExtensionInstallDialog.vue b/frontend/src/components/common/ExtensionInstallDialog.vue new file mode 100644 index 0000000..8252966 --- /dev/null +++ b/frontend/src/components/common/ExtensionInstallDialog.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/frontend/src/features/plugins/PluginsView.vue b/frontend/src/features/plugins/PluginsView.vue index 3ed8109..62128cd 100644 --- a/frontend/src/features/plugins/PluginsView.vue +++ b/frontend/src/features/plugins/PluginsView.vue @@ -1,6 +1,7 @@