mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-08 23:53:30 +08:00
Merge remote-tracking branch 'origin/dev' into feature/20260602_connection_driver_i18n
# Conflicts: # frontend/src/components/QueryEditor.results-and-drop.test.tsx
This commit is contained in:
@@ -4114,7 +4114,7 @@ function App() {
|
||||
) : null,
|
||||
isLatestUpdateDownloaded ? (
|
||||
<Button key="install-direct" type="primary" icon={<DownloadOutlined />} onClick={handleInstallFromProgress}>
|
||||
{isMacRuntime ? t('app.about.action.open_install_directory') : t('app.about.action.install_update')}
|
||||
{t('app.about.action.install_update')}
|
||||
</Button>
|
||||
) : null,
|
||||
].filter(Boolean)}
|
||||
@@ -5111,7 +5111,7 @@ function App() {
|
||||
] : (updateDownloadProgress.status === 'done' ? [
|
||||
<Button key="close" onClick={hideUpdateDownloadProgress}>{t('common.close')}</Button>,
|
||||
<Button key="install" type="primary" onClick={handleInstallFromProgress}>
|
||||
{isMacRuntime ? t('app.about.action.open_install_directory') : t('app.about.action.install_update')}
|
||||
{t('app.about.action.install_update')}
|
||||
</Button>
|
||||
] : (updateDownloadProgress.status === 'error' ? [
|
||||
<Button key="close" onClick={hideUpdateDownloadProgress}>{t('common.close')}</Button>
|
||||
|
||||
@@ -5,10 +5,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { readV2ThemeCss } from '../test/readV2ThemeCss';
|
||||
|
||||
import { setCurrentLanguage } from '../i18n';
|
||||
import { catalogs } from '../i18n/catalog';
|
||||
import type { SavedQuery, TabData } from '../types';
|
||||
import { formatSqlExecutionError } from '../utils/sqlErrorSemantics';
|
||||
import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator';
|
||||
import { formatSqlExecutionError } from '../utils/sqlErrorSemantics';
|
||||
import { clearQueryTabDraft, clearSQLFileTabDraft, getQueryTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
|
||||
import { normalizeQueryResultMessages } from './queryEditor/QueryEditorHelpers';
|
||||
import QueryEditor, {
|
||||
@@ -360,10 +359,9 @@ vi.mock('./DataGrid', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('./LogPanel', () => ({
|
||||
default: ({ variant, executionError }: { variant?: string; executionError?: string }) => (
|
||||
<div data-log-panel={variant}>
|
||||
SQL 执行日志
|
||||
{executionError ? ` ${executionError}` : ''}
|
||||
default: ({ executionError }: any) => (
|
||||
<div data-log-panel="true">
|
||||
{executionError || 'log-panel'}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -474,28 +472,12 @@ vi.mock('antd', () => {
|
||||
});
|
||||
|
||||
const textContent = (node: any): string => {
|
||||
if (node === null || node === undefined) return '';
|
||||
if (typeof node === 'string' || typeof node === 'number') return String(node);
|
||||
if (node == null) return '';
|
||||
if (typeof node === 'string') return node;
|
||||
if (Array.isArray(node)) return node.map((item) => textContent(item)).join('');
|
||||
return textContent(node.children || []);
|
||||
};
|
||||
|
||||
const queryResultMessageText = (renderer: ReactTestRenderer): string => {
|
||||
const values: string[] = [];
|
||||
const walk = (node: any) => {
|
||||
if (!node) return;
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach(walk);
|
||||
return;
|
||||
}
|
||||
if (typeof node !== 'object') return;
|
||||
if (typeof node.props?.['data-query-result-message-textarea'] === 'string') {
|
||||
values.push(String(node.props.value || ''));
|
||||
}
|
||||
walk(node.children || []);
|
||||
};
|
||||
walk(renderer.toJSON());
|
||||
return values.join('\n');
|
||||
return (node.children || [])
|
||||
.map((item: any) => (typeof item === 'string' ? item : textContent(item)))
|
||||
.join('');
|
||||
};
|
||||
|
||||
const findButton = (renderer: ReactTestRenderer, text: string) =>
|
||||
@@ -507,6 +489,11 @@ const findButtons = (renderer: ReactTestRenderer, text: string) =>
|
||||
const findExactButton = (renderer: ReactTestRenderer, text: string) =>
|
||||
renderer.root.findAll((node) => node.type === 'button' && textContent(node) === text)[0];
|
||||
|
||||
const findResultMessageTextarea = (renderer: ReactTestRenderer, mode: 'compact' | 'full' = 'full') =>
|
||||
renderer.root.find((node) =>
|
||||
node.type === 'textarea' && node.props['data-query-result-message-textarea'] === mode,
|
||||
);
|
||||
|
||||
const findEditorAction = (id: string) =>
|
||||
editorState.editor.addAction.mock.calls
|
||||
.map((call: any[]) => call[0])
|
||||
@@ -808,21 +795,22 @@ describe('QueryEditor external SQL save', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const rendered = `${textContent(renderer!.toJSON())}\n${queryResultMessageText(renderer!)}`;
|
||||
expect(rendered).toContain('消息 1');
|
||||
expect(rendered).toContain("Table 'users'. Scan count 1, logical reads 3.");
|
||||
expect(dataGridState.latestProps?.columnNames).not.toEqual([]);
|
||||
expect(textContent(renderer!.toJSON())).toContain('消息 1');
|
||||
expect(findResultMessageTextarea(renderer!).props.value).toBe("Table 'users'. Scan count 1, logical reads 3.");
|
||||
expect(dataGridState.latestProps).toBeNull();
|
||||
});
|
||||
|
||||
it('normalizes sqlserver mssql-prefixed message lines line-by-line', () => {
|
||||
it('preserves sqlserver message indentation and blank lines after stripping mssql prefixes', () => {
|
||||
expect(normalizeQueryResultMessages([
|
||||
"mssql: select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
|
||||
"mssql: 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
|
||||
'',
|
||||
" where funcno = @funcno and tabname = '$vendorclass'",
|
||||
])).toEqual([
|
||||
"select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
|
||||
"'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
|
||||
"where funcno = @funcno and tabname = '$vendorclass'",
|
||||
" select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
|
||||
" 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
|
||||
'',
|
||||
" where funcno = @funcno and tabname = '$vendorclass'",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -965,14 +953,16 @@ describe('QueryEditor external SQL save', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const rendered = `${textContent(renderer!.toJSON())}\n${queryResultMessageText(renderer!)}`;
|
||||
expect(rendered).toContain('消息 2');
|
||||
expect(rendered).toContain("insert into c_dyscript(projectid,name) values (1,'demo')");
|
||||
expect(rendered).not.toContain('影响行数:0');
|
||||
expect(textContent(renderer!.toJSON())).toContain('消息 2');
|
||||
expect(findResultMessageTextarea(renderer!).props.value).toBe([
|
||||
"insert into c_dyscript(projectid,name) values (1,'demo')",
|
||||
"insert into c_dyscript(projectid,name) values (2,'next')",
|
||||
].join('\n'));
|
||||
expect(textContent(renderer!.toJSON())).not.toContain('影响行数:0');
|
||||
expect(dataGridState.latestProps).toBeNull();
|
||||
});
|
||||
|
||||
it('strips mssql prefixes before rendering sqlserver message-only results', async () => {
|
||||
it('preserves sqlserver message indentation in the rendered result message textarea', async () => {
|
||||
storeState.connections[0].config.type = 'sqlserver';
|
||||
storeState.connections[0].config.database = 'hydee';
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
@@ -985,6 +975,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
messages: [
|
||||
"mssql: select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
|
||||
"mssql: 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
|
||||
'',
|
||||
" where funcno = @funcno and tabname = '$vendorclass'",
|
||||
],
|
||||
},
|
||||
@@ -1004,12 +995,16 @@ describe('QueryEditor external SQL save', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const rendered = `${textContent(renderer!.toJSON())}\n${queryResultMessageText(renderer!)}`;
|
||||
const rendered = textContent(renderer!.toJSON());
|
||||
const messageTextarea = findResultMessageTextarea(renderer!);
|
||||
expect(rendered).toContain('消息 1');
|
||||
expect(rendered).toContain("select c.queryno,'' ,left(dbo.f_vendor_class");
|
||||
expect(rendered).toContain("'char','',''),'自动生成'");
|
||||
expect(rendered).toContain("where funcno = @funcno and tabname = '$vendorclass'");
|
||||
expect(rendered).not.toContain('mssql:');
|
||||
expect(messageTextarea.props.value).toBe([
|
||||
" select c.queryno,'' ,left(dbo.f_vendor_class(''' + b.groupid + ''',' + colname + '),",
|
||||
" 'char','',''),'自动生成',0,isdefault,defaultoperator,defaultvalue,defaultvalue2,ishaving",
|
||||
'',
|
||||
" where funcno = @funcno and tabname = '$vendorclass'",
|
||||
].join('\n'));
|
||||
expect(messageTextarea.props.value).not.toContain('mssql:');
|
||||
});
|
||||
|
||||
it('renders top-level sqlserver print messages when result sets contain only status rows', async () => {
|
||||
@@ -1038,10 +1033,9 @@ describe('QueryEditor external SQL save', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const rendered = `${textContent(renderer!.toJSON())}\n${queryResultMessageText(renderer!)}`;
|
||||
expect(rendered).toContain('消息 2');
|
||||
expect(rendered).toContain("insert into c_dyscript(projectid,name) values (1,'demo')");
|
||||
expect(rendered).not.toContain('影响行数:0');
|
||||
expect(textContent(renderer!.toJSON())).toContain('消息 2');
|
||||
expect(findResultMessageTextarea(renderer!).props.value).toBe("insert into c_dyscript(projectid,name) values (1,'demo')");
|
||||
expect(textContent(renderer!.toJSON())).not.toContain('影响行数:0');
|
||||
expect(dataGridState.latestProps).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1388,16 +1382,12 @@ describe('QueryEditor external SQL save', () => {
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab()} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
findButton(renderer!, 'Show results panel').props.onClick();
|
||||
renderer = create(<QueryEditor tab={createTab({ resultPanelVisible: true })} />);
|
||||
});
|
||||
|
||||
const rendered = textContent(renderer!.toJSON());
|
||||
expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.title']);
|
||||
expect(rendered).toContain(catalogs['en-US']['query_editor.empty_state.description']);
|
||||
expect(rendered).toContain('Awaiting SQL execution');
|
||||
expect(rendered).toContain('Run a query to display results below in the new data grid.');
|
||||
expect(rendered).not.toContain('等待执行 SQL');
|
||||
expect(rendered).not.toContain('运行查询后,结果会在下方以新版数据网格展示。');
|
||||
});
|
||||
@@ -1733,14 +1723,15 @@ describe('QueryEditor external SQL save', () => {
|
||||
await findButton(renderer, 'Run').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const rendered = textContent(renderer.toJSON());
|
||||
expect(rendered).toContain('Statement 2 failed:');
|
||||
expect(rendered).toContain('Raw error: driver exploded');
|
||||
expect(rendered).not.toContain('第 2 条语句执行失败:driver exploded');
|
||||
const rendered = textContent(renderer.toJSON());
|
||||
expect(rendered).toContain(formatSqlExecutionError('driver exploded', {
|
||||
prefix: 'Statement 2 failed:',
|
||||
}));
|
||||
expect(rendered).not.toContain('第 2 条语句执行失败:driver exploded');
|
||||
});
|
||||
|
||||
it('shows the Mongo zero-result success toast in English', async () => {
|
||||
@@ -1811,16 +1802,11 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(messageApi.success).not.toHaveBeenCalledWith('已执行完成,生成 2 个结果集。');
|
||||
});
|
||||
|
||||
it('renders the non-Mongo zero-row transactional result in English', async () => {
|
||||
storeState.languagePreference = 'en-US';
|
||||
setCurrentLanguage('en-US');
|
||||
const query = 'update users set active = 1 where 1 = 0;';
|
||||
backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({
|
||||
success: true,
|
||||
transactionId: 'tx-zero-rows',
|
||||
transactionPending: true,
|
||||
data: [{ columns: ['affectedRows'], rows: [{ affectedRows: 0 }], statementIndex: 1 }],
|
||||
});
|
||||
it('shows the non-Mongo zero-result success toast in English', async () => {
|
||||
storeState.languagePreference = 'en-US';
|
||||
setCurrentLanguage('en-US');
|
||||
const query = 'update users set active = 1 where 1 = 0;';
|
||||
backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({ success: true, data: [] });
|
||||
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
@@ -1845,17 +1831,13 @@ describe('QueryEditor external SQL save', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const rendered = textContent(renderer.toJSON());
|
||||
expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled();
|
||||
expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
|
||||
expect(backendApp.DBQueryMultiTransactional).toHaveBeenCalledTimes(1);
|
||||
expect(String(backendApp.DBQueryMultiTransactional.mock.calls[0][2])).toContain('update users set active = 1 where 1 = 0');
|
||||
expect(rendered).toContain(catalogs['en-US']['query_editor.result.execution_success']);
|
||||
expect(rendered).toContain(catalogs['en-US']['query_editor.result.affected_rows'].replace('{{count}}', '0'));
|
||||
expect(rendered).not.toContain('执行成功');
|
||||
expect(rendered).not.toContain('影响行数:0');
|
||||
expect(messageApi.success).not.toHaveBeenCalledWith('Execution succeeded.');
|
||||
});
|
||||
expect(backendApp.DBQueryWithCancel).not.toHaveBeenCalled();
|
||||
expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
|
||||
expect(backendApp.DBQueryMultiTransactional).toHaveBeenCalledTimes(1);
|
||||
expect(String(backendApp.DBQueryMultiTransactional.mock.calls[0][2])).toContain('update users set active = 1 where 1 = 0');
|
||||
expect(messageApi.success).toHaveBeenCalledWith('Execution succeeded.');
|
||||
expect(messageApi.success).not.toHaveBeenCalledWith('执行成功。');
|
||||
});
|
||||
|
||||
it('shows the wrapped execution failure toast in English while preserving raw error detail', async () => {
|
||||
storeState.languagePreference = 'en-US';
|
||||
|
||||
@@ -25,7 +25,19 @@ export type CompletionTriggerMeta = {dbName: string, triggerName: string, tableN
|
||||
export type CompletionRoutineMeta = {dbName: string, routineName: string, routineType: string, schemaName?: string};
|
||||
|
||||
export const QUERY_LOCATOR_ALIAS_PREFIX = '__gonavi_locator_';
|
||||
const SQLSERVER_MESSAGE_PREFIX_RE = /^\s*mssql:\s*/i;
|
||||
const SQLSERVER_MESSAGE_PREFIX_RE = /^\s*mssql:/i;
|
||||
|
||||
const trimBoundaryBlankEntries = (entries: string[]): string[] => {
|
||||
let start = 0;
|
||||
let end = entries.length;
|
||||
while (start < end && !String(entries[start] || '').trim()) start++;
|
||||
while (end > start && !String(entries[end - 1] || '').trim()) end--;
|
||||
return entries.slice(start, end);
|
||||
};
|
||||
|
||||
const stripSqlServerMessagePrefix = (line: string): string => (
|
||||
line.replace(SQLSERVER_MESSAGE_PREFIX_RE, '').replace(/^[ \t]/, '')
|
||||
);
|
||||
|
||||
export const buildQueryReadOnlyLocator = (reason: string): EditRowLocator => ({
|
||||
strategy: 'none',
|
||||
@@ -121,33 +133,33 @@ export const stripQueryIdentifierQuotes = (part: string): string => {
|
||||
return text;
|
||||
};
|
||||
|
||||
export const normalizeQueryResultMessageText = (message: unknown): string => {
|
||||
export const normalizeQueryResultMessageText = (
|
||||
message: unknown,
|
||||
options?: { preserveIndentation?: boolean },
|
||||
): string => {
|
||||
const text = String(message ?? '').replace(/\r\n?/g, '\n');
|
||||
if (!text.trim()) return '';
|
||||
if (!text) return '';
|
||||
|
||||
let prefixRemoved = false;
|
||||
const normalizedLines = text
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (!line.trim()) return '';
|
||||
const preserveIndentation = options?.preserveIndentation === true;
|
||||
const normalizedLines = trimBoundaryBlankEntries(
|
||||
text.split('\n').map((line) => {
|
||||
if (SQLSERVER_MESSAGE_PREFIX_RE.test(line)) {
|
||||
prefixRemoved = true;
|
||||
return line.replace(SQLSERVER_MESSAGE_PREFIX_RE, '').trimStart();
|
||||
return stripSqlServerMessagePrefix(line);
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
const normalized = (prefixRemoved
|
||||
? normalizedLines.map((line) => line.trim() ? line.trimStart() : '').join('\n')
|
||||
: normalizedLines.join('\n'))
|
||||
.trim();
|
||||
|
||||
return prefixRemoved ? normalized : text.trim();
|
||||
return preserveIndentation ? line : (line.trim() ? line : '');
|
||||
}),
|
||||
);
|
||||
if (normalizedLines.length === 0) return '';
|
||||
return preserveIndentation ? normalizedLines.join('\n') : normalizedLines.join('\n').trim();
|
||||
};
|
||||
|
||||
export const normalizeQueryResultMessages = (messages: unknown): string[] => (
|
||||
Array.isArray(messages)
|
||||
? messages.map((item) => normalizeQueryResultMessageText(item)).filter(Boolean)
|
||||
? (() => {
|
||||
const preserveIndentation = messages.some((item) => SQLSERVER_MESSAGE_PREFIX_RE.test(String(item ?? '')));
|
||||
const normalized = messages.map((item) => normalizeQueryResultMessageText(item, { preserveIndentation }));
|
||||
return preserveIndentation ? trimBoundaryBlankEntries(normalized) : normalized.filter(Boolean);
|
||||
})()
|
||||
: []
|
||||
);
|
||||
|
||||
|
||||
159
frontend/src/hooks/useAppUpdateManager.test.tsx
Normal file
159
frontend/src/hooks/useAppUpdateManager.test.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useAppUpdateManager } from './useAppUpdateManager';
|
||||
|
||||
const runtimeApi = vi.hoisted(() => ({
|
||||
EventsOn: vi.fn(() => vi.fn()),
|
||||
}));
|
||||
|
||||
const messageApi = vi.hoisted(() => ({
|
||||
info: vi.fn(),
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../wailsjs/runtime', () => runtimeApi);
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
message: messageApi,
|
||||
}));
|
||||
|
||||
type BackendAppMock = {
|
||||
CheckForUpdates: ReturnType<typeof vi.fn>;
|
||||
CheckForUpdatesSilently: ReturnType<typeof vi.fn>;
|
||||
DownloadUpdate: ReturnType<typeof vi.fn>;
|
||||
InstallUpdateAndRestart: ReturnType<typeof vi.fn>;
|
||||
OpenDownloadedUpdateDirectory: ReturnType<typeof vi.fn>;
|
||||
GetAppInfo: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
const createBackendAppMock = (): BackendAppMock => ({
|
||||
CheckForUpdates: vi.fn(),
|
||||
CheckForUpdatesSilently: vi.fn(),
|
||||
DownloadUpdate: vi.fn(),
|
||||
InstallUpdateAndRestart: vi.fn(),
|
||||
OpenDownloadedUpdateDirectory: vi.fn(),
|
||||
GetAppInfo: vi.fn(async () => ({ success: true, data: { version: '0.8.1', author: 'Syngnat' } })),
|
||||
});
|
||||
|
||||
describe('useAppUpdateManager', () => {
|
||||
let backendApp: BackendAppMock;
|
||||
let hook: ReturnType<typeof useAppUpdateManager> | null = null;
|
||||
let renderer: ReactTestRenderer | null = null;
|
||||
|
||||
const t = (key: string, params?: Record<string, any>) => {
|
||||
if (params?.version) return `${key}:${params.version}`;
|
||||
if (params?.path) return `${key}:${params.path}`;
|
||||
if (params?.error) return `${key}:${params.error}`;
|
||||
return key;
|
||||
};
|
||||
|
||||
const renderHook = (isMacRuntime: boolean) => {
|
||||
const Harness = () => {
|
||||
hook = useAppUpdateManager({
|
||||
isMacRuntime,
|
||||
runtimeBuildType: 'release',
|
||||
t,
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
act(() => {
|
||||
renderer = create(<Harness />);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
backendApp = createBackendAppMock();
|
||||
hook = null;
|
||||
renderer = null;
|
||||
runtimeApi.EventsOn.mockClear();
|
||||
messageApi.info.mockReset();
|
||||
messageApi.success.mockReset();
|
||||
messageApi.error.mockReset();
|
||||
vi.useFakeTimers();
|
||||
vi.stubGlobal('window', {
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
go: {
|
||||
app: {
|
||||
App: backendApp,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
renderer?.unmount();
|
||||
});
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('uses InstallUpdateAndRestart for downloaded macOS updates', async () => {
|
||||
backendApp.CheckForUpdates.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
hasUpdate: true,
|
||||
currentVersion: '0.8.1',
|
||||
latestVersion: '0.8.2',
|
||||
downloaded: true,
|
||||
assetSize: 1024,
|
||||
},
|
||||
});
|
||||
backendApp.InstallUpdateAndRestart.mockResolvedValue({ success: true });
|
||||
backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true });
|
||||
|
||||
renderHook(true);
|
||||
|
||||
await act(async () => {
|
||||
await hook?.checkForUpdates(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await hook?.handleInstallFromProgress();
|
||||
});
|
||||
|
||||
expect(backendApp.InstallUpdateAndRestart).toHaveBeenCalledTimes(1);
|
||||
expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not auto-open the downloaded macOS package directory after download succeeds', async () => {
|
||||
backendApp.CheckForUpdates.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
hasUpdate: true,
|
||||
currentVersion: '0.8.1',
|
||||
latestVersion: '0.8.2',
|
||||
downloaded: false,
|
||||
assetSize: 2048,
|
||||
},
|
||||
});
|
||||
backendApp.DownloadUpdate.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
downloadPath: '/Users/test/Desktop/GoNavi-0.8.2-MacOS-Arm64.dmg',
|
||||
},
|
||||
});
|
||||
backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true });
|
||||
|
||||
renderHook(true);
|
||||
|
||||
await act(async () => {
|
||||
await hook?.checkForUpdates(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await hook?.downloadUpdate(hook?.lastUpdateInfo!, false);
|
||||
});
|
||||
|
||||
expect(backendApp.DownloadUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled();
|
||||
expect(hook?.lastUpdateInfo?.downloaded).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -167,16 +167,6 @@ export const useAppUpdateManager = ({
|
||||
void message.success({ content: t('app.about.message.download_completed'), duration: 2 });
|
||||
}
|
||||
setAboutUpdateStatus(formatAboutUpdateStatus({ ...info, downloaded: true }));
|
||||
if (isMacRuntime && !updateUserDismissedRef.current) {
|
||||
try {
|
||||
const openRes = await (window as any).go.app.App.OpenDownloadedUpdateDirectory();
|
||||
if (openRes?.success) {
|
||||
void message.success(openRes?.message || t('app.about.message.install_directory_opened_manual_replace'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('自动打开下载目录失败', e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
@@ -218,28 +208,6 @@ export const useAppUpdateManager = ({
|
||||
if (!canInstall) {
|
||||
return;
|
||||
}
|
||||
if (isMacRuntime) {
|
||||
const res = await (window as any).go.app.App.OpenDownloadedUpdateDirectory();
|
||||
if (!res?.success) {
|
||||
void message.error(t('app.about.message.open_install_directory_failed_with_error', { error: res?.message || t('common.unknown') }));
|
||||
updateDownloadedVersionRef.current = null;
|
||||
updateDownloadMetaRef.current = null;
|
||||
setUpdateDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
status: 'idle',
|
||||
percent: 0,
|
||||
downloaded: 0,
|
||||
open: false,
|
||||
}));
|
||||
setLastUpdateInfo((prev) => prev ? { ...prev, downloaded: false, downloadPath: undefined } : prev);
|
||||
setAboutUpdateStatus((prev) => lastUpdateInfo ? formatAboutUpdateStatus({ ...lastUpdateInfo, downloaded: false, downloadPath: undefined }) : prev);
|
||||
return;
|
||||
}
|
||||
updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null;
|
||||
hideUpdateDownloadProgress();
|
||||
void message.success(res?.message || t('app.about.message.install_directory_opened_manual_replace'));
|
||||
return;
|
||||
}
|
||||
const res = await (window as any).go.app.App.InstallUpdateAndRestart();
|
||||
if (!res?.success) {
|
||||
void message.error(t('app.about.message.install_failed_with_error', { error: res?.message || t('common.unknown') }));
|
||||
@@ -247,7 +215,7 @@ export const useAppUpdateManager = ({
|
||||
}
|
||||
updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null;
|
||||
hideUpdateDownloadProgress();
|
||||
}, [formatAboutUpdateStatus, hideUpdateDownloadProgress, isMacRuntime, lastUpdateInfo, updateDownloadProgress.status, updateDownloadProgress.version, t]);
|
||||
}, [hideUpdateDownloadProgress, lastUpdateInfo, updateDownloadProgress.status, updateDownloadProgress.version, t]);
|
||||
|
||||
const checkForUpdates = useCallback(async (silent: boolean) => {
|
||||
if (updateCheckInFlightRef.current) return;
|
||||
|
||||
Reference in New Issue
Block a user