🐛 fix(driver-manager): 修复驱动安装交互与 DuckDB Windows 发布链路

- 修复单驱动安装期间右侧目录操作被错误禁用的问题
- 调整 DuckDB Windows 优先下载专属 zip 并兼容带 query 的签名链接
- 补齐本地构建与 CI 发布的 duckdb-driver.zip 产物及回归测试
This commit is contained in:
Syngnat
2026-06-05 07:15:16 +08:00
parent a718c41d5d
commit 2438899ff5
8 changed files with 514 additions and 29 deletions

View File

@@ -0,0 +1,204 @@
import React from 'react';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import DriverManagerModal from './DriverManagerModal';
const storeState = vi.hoisted(() => ({
theme: 'light',
appearance: {
enabled: true,
opacity: 1,
blur: 0,
uiVersion: 'legacy',
},
}));
const backendApp = vi.hoisted(() => ({
CheckDriverNetworkStatus: vi.fn(),
DownloadDriverPackage: vi.fn(),
GetDriverVersionList: vi.fn(),
GetDriverVersionPackageSize: vi.fn(),
GetDriverStatusList: vi.fn(),
InstallLocalDriverPackage: vi.fn(),
OpenDriverDownloadDirectory: vi.fn(),
RemoveDriverPackage: vi.fn(),
SelectDriverPackageDirectory: vi.fn(),
SelectDriverPackageFile: vi.fn(),
}));
const runtimeApi = vi.hoisted(() => ({
EventsOn: vi.fn(() => vi.fn()),
}));
const messageApi = vi.hoisted(() => ({
error: vi.fn(),
success: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
}));
vi.mock('../store', () => ({
useStore: (selector: (state: typeof storeState) => any) => selector(storeState),
}));
vi.mock('../../wailsjs/go/app/App', () => backendApp);
vi.mock('../../wailsjs/runtime/runtime', () => runtimeApi);
vi.mock('@ant-design/icons', () => {
const Icon = () => <span />;
return {
DeleteOutlined: Icon,
DownloadOutlined: Icon,
FileSearchOutlined: Icon,
FolderOpenOutlined: Icon,
InfoCircleFilled: Icon,
ReloadOutlined: Icon,
};
});
vi.mock('antd', () => {
const Button: any = ({ children, disabled, loading, onClick, ...rest }: any) => (
<button type="button" disabled={disabled || loading} onClick={onClick} {...rest}>
{children}
</button>
);
const Input: any = ({ value, onChange, placeholder }: any) => (
<input value={value} onChange={onChange} placeholder={placeholder} />
);
Input.Search = ({ value, onChange, placeholder }: any) => (
<input value={value} onChange={onChange} placeholder={placeholder} />
);
const Select = () => null;
const Progress = () => <div data-progress="true" />;
const Tag = ({ children }: any) => <span>{children}</span>;
const Switch = ({ checked, onChange, disabled }: any) => (
<button type="button" disabled={disabled} data-switch-checked={String(checked)} onClick={() => onChange?.(!checked)}>
switch
</button>
);
const Space = ({ children }: any) => <div>{children}</div>;
const Text = ({ children }: any) => <span>{children}</span>;
const Paragraph = ({ children }: any) => <div>{children}</div>;
const Typography = { Paragraph, Text };
const Alert = ({ children, message, description }: any) => <div>{children}{message}{description}</div>;
const Empty: any = ({ description }: any) => <div>{description}</div>;
Empty.PRESENTED_IMAGE_SIMPLE = null;
const Collapse = ({ items }: any) => (
<div>{items?.map((item: any) => <div key={item.key}>{item.label}{item.children}</div>)}</div>
);
const Modal: any = ({ children, open, footer, title }: any) => (open ? (
<section data-modal-title={title}>
{children}
<div>{footer}</div>
</section>
) : null);
Modal.confirm = vi.fn();
return {
Alert,
Button,
Collapse,
Empty,
Input,
Modal,
Progress,
Select,
Space,
Switch,
Tag,
Typography,
message: messageApi,
};
});
const flushPromises = async () => {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
};
const textContent = (node: any): string =>
(node.children || [])
.map((item: any) => (typeof item === 'string' ? item : textContent(item)))
.join('');
const findButton = (renderer: ReactTestRenderer, text: string) =>
renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text))[0];
describe('DriverManagerModal toolbar actions', () => {
beforeEach(() => {
vi.clearAllMocks();
backendApp.GetDriverStatusList.mockResolvedValue({
success: true,
data: {
downloadDir: 'D:/drivers',
drivers: [
{
type: 'duckdb',
name: 'DuckDB',
builtIn: false,
pinnedVersion: '2.5.6',
runtimeAvailable: false,
packageInstalled: false,
connectable: false,
defaultDownloadUrl: 'builtin://activate/duckdb',
message: '未启用',
},
],
},
});
backendApp.CheckDriverNetworkStatus.mockResolvedValue({
success: true,
data: {
reachable: true,
summary: 'ok',
recommendedProxy: false,
proxyConfigured: false,
checks: [],
},
});
backendApp.GetDriverVersionList.mockResolvedValue({
success: true,
data: {
versions: [{ version: '2.5.6', downloadUrl: 'builtin://activate/duckdb', recommended: true }],
},
});
backendApp.DownloadDriverPackage.mockImplementation(() => new Promise(() => {}));
backendApp.OpenDriverDownloadDirectory.mockResolvedValue({ success: true });
backendApp.SelectDriverPackageDirectory.mockResolvedValue({ success: true, data: { path: 'D:/drivers/import' } });
});
it('keeps directory tools enabled while a single driver install is running', async () => {
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<DriverManagerModal open onClose={vi.fn()} />);
});
await flushPromises();
const installButton = findButton(renderer!, '安装启用');
const openDirButtonBefore = findButton(renderer!, '打开驱动目录');
const importDirButtonBefore = findButton(renderer!, '导入驱动目录');
const installAllButtonBefore = findButton(renderer!, '安装所有驱动');
expect(openDirButtonBefore.props.disabled).toBeFalsy();
expect(importDirButtonBefore.props.disabled).toBeFalsy();
expect(installAllButtonBefore.props.disabled).toBeFalsy();
await act(async () => {
installButton.props.onClick();
await Promise.resolve();
});
const openDirButtonAfter = findButton(renderer!, '打开驱动目录');
const importDirButtonAfter = findButton(renderer!, '导入驱动目录');
const installAllButtonAfter = findButton(renderer!, '安装所有驱动');
expect(openDirButtonAfter.props.disabled).toBeFalsy();
expect(importDirButtonAfter.props.disabled).toBeFalsy();
expect(installAllButtonAfter.props.disabled).toBe(true);
});
});

View File

@@ -234,7 +234,8 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
const [versionSizeLoadingMap, setVersionSizeLoadingMap] = useState<Record<string, boolean>>({});
const downloadDirRef = useRef(downloadDir);
const progressMapRef = useRef<Record<string, DriverProgressState>>({});
const batchBusy = batchDirectoryImporting || batchAction !== '' || actionState.kind !== '';
const batchBusy = batchDirectoryImporting || batchAction !== '';
const installMutatingBusy = batchBusy || actionState.kind !== '';
useEffect(() => {
downloadDirRef.current = downloadDir;
@@ -1436,7 +1437,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
</Button>
<Button key="close" type="primary" onClick={onClose}>
{batchBusy ? '后台运行' : '关闭'}
{installMutatingBusy ? '后台运行' : '关闭'}
</Button>
</Space>
)}
@@ -1584,12 +1585,12 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
<Switch
checked={forceOverwriteInstalled}
onChange={(checked) => setForceOverwriteInstalled(checked)}
disabled={batchBusy}
disabled={batchDirectoryImporting}
/>
<Button
type="primary"
icon={<DownloadOutlined />}
disabled={batchBusy || installableRows.length === 0}
disabled={installMutatingBusy || installableRows.length === 0}
loading={batchAction === 'install-all'}
onClick={() => void installAllDrivers()}
>
@@ -1598,7 +1599,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
<Button
type="primary"
icon={<DownloadOutlined />}
disabled={batchBusy || reinstallableRows.length === 0}
disabled={installMutatingBusy || reinstallableRows.length === 0}
loading={batchAction === 'reinstall-updates'}
onClick={() => void reinstallNeededDrivers()}
>
@@ -1607,7 +1608,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
<Button
danger
icon={<DeleteOutlined />}
disabled={batchBusy || removableRows.length === 0}
disabled={installMutatingBusy || removableRows.length === 0}
loading={batchAction === 'remove-all'}
onClick={() => void removeAllDrivers()}
>
@@ -1615,7 +1616,6 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
</Button>
<Button
icon={<FolderOpenOutlined />}
disabled={batchBusy}
onClick={() => void openDriverDirectory()}
>
@@ -1623,7 +1623,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
<Button
icon={<FolderOpenOutlined />}
loading={batchDirectoryImporting}
disabled={batchBusy && !batchDirectoryImporting}
disabled={batchDirectoryImporting}
onClick={() => void installDriversFromDirectory()}
>