diff --git a/packages/core/app/client-v2/index.html b/packages/core/app/client-v2/index.html
index 1e05b2cdd43..3e1c97e931c 100644
--- a/packages/core/app/client-v2/index.html
+++ b/packages/core/app/client-v2/index.html
@@ -1,5 +1,5 @@
-
+
diff --git a/packages/core/app/client/index.html b/packages/core/app/client/index.html
index 14696df901f..5d9a0acad2a 100644
--- a/packages/core/app/client/index.html
+++ b/packages/core/app/client/index.html
@@ -1,5 +1,5 @@
-
+
@@ -11,4 +11,4 @@
-
\ No newline at end of file
+
diff --git a/packages/core/client-v2/src/BaseApplication.tsx b/packages/core/client-v2/src/BaseApplication.tsx
index 3c732955eeb..600fc4c4a27 100644
--- a/packages/core/client-v2/src/BaseApplication.tsx
+++ b/packages/core/client-v2/src/BaseApplication.tsx
@@ -169,6 +169,18 @@ export abstract class BaseApplication<
return this.wsAuthorized;
}
+ public setDocumentLanguage(language?: string | null) {
+ if (typeof document === 'undefined') {
+ return;
+ }
+
+ if (language) {
+ document.documentElement.lang = language;
+ } else {
+ document.documentElement.removeAttribute('lang');
+ }
+ }
+
constructor(protected options: TOptions = {} as TOptions) {
this.initRequireJs();
this.defineObservableState();
@@ -205,6 +217,7 @@ export abstract class BaseApplication<
this.addRoutes();
this.i18n.on('languageChanged', (lng) => {
this.apiClient.auth.locale = lng;
+ this.setDocumentLanguage(lng);
});
this.initListeners();
this.afterManagersInitialized();
diff --git a/packages/core/client-v2/src/__tests__/app.test.tsx b/packages/core/client-v2/src/__tests__/app.test.tsx
index 286305dc5da..ab7d0a421fe 100644
--- a/packages/core/client-v2/src/__tests__/app.test.tsx
+++ b/packages/core/client-v2/src/__tests__/app.test.tsx
@@ -33,6 +33,7 @@ describe('app', () => {
afterEach(() => {
document.querySelectorAll('link[rel="shortcut icon"]').forEach((node) => node.remove());
+ document.documentElement.removeAttribute('lang');
vi.restoreAllMocks();
});
@@ -89,6 +90,14 @@ describe('app', () => {
expect(favicon.getAttribute('href')).toBe('/favicon/favicon.ico');
});
+ it('should sync document language when app language changes', async () => {
+ const app = new Application({ router });
+
+ await app.i18n.changeLanguage('ja-JP');
+
+ expect(document.documentElement.lang).toBe('ja-JP');
+ });
+
it('should escape app version html placeholder content', () => {
expect(getAppVersionHTML('&"')).toBe(
'v<script>alert(1)</script>&"',
diff --git a/packages/core/client-v2/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts b/packages/core/client-v2/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts
index dbf182ca119..24d0a5900aa 100644
--- a/packages/core/client-v2/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts
+++ b/packages/core/client-v2/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts
@@ -39,6 +39,7 @@ export class LocalePlugin extends Plugin {
if (data.lang) {
api.auth.setLocale(data.lang);
+ this.app.setDocumentLanguage(data.lang);
this.app.i18n.changeLanguage(data.lang);
}
diff --git a/packages/core/client/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts b/packages/core/client/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts
index 40d6adc7561..3b8263ff448 100644
--- a/packages/core/client/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts
+++ b/packages/core/client/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts
@@ -33,6 +33,7 @@ export class LocalePlugin extends Plugin {
this.app.use(App, { component: false });
if (data?.data?.lang) {
api.auth.setLocale(data?.data?.lang);
+ this.app.setDocumentLanguage(data?.data?.lang);
this.app.i18n.changeLanguage(data?.data?.lang);
}
Object.keys(data?.data?.resources || {}).forEach((key) => {
diff --git a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupSettings.test.tsx b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupSettings.test.tsx
index 8a3622016c8..16d737ff01b 100644
--- a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupSettings.test.tsx
+++ b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupSettings.test.tsx
@@ -1,24 +1,20 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
import { observer } from '@formily/react';
-import * as nocobaseClient from '@nocobase/client';
import { SchemaComponentProvider } from '@nocobase/client';
-import { sleep } from '@nocobase/test';
+import { sleep } from '@nocobase/test/web';
import { act, render, screen, userEvent } from '@nocobase/test/client';
import { App } from 'antd';
-import MockAdapter from 'axios-mock-adapter';
import React from 'react';
import { BackupSettings } from '../components/BackupSettings';
-
-vi.mock('@nocobase/client', async () => {
- const actual = await vi.importActual('@nocobase/client');
- return {
- ...actual,
- useApp: () => ({
- i18n: {
- t: vi.fn().mockImplementation((key) => key), // Mock translation function, return the key directly
- },
- }),
- };
-});
+import { createMockAppWrapper } from './testUtils';
const storagesData = {
data: [
@@ -85,20 +81,30 @@ const settingsData = {
};
describe('BackupSettings', () => {
- const mockedStorages = vi.fn().mockReturnValue(storagesData);
- const mockedGetSettings = vi.fn().mockReturnValue(settingsData);
- const mockedUpdateSettings = vi.fn().mockReturnValue({ data: { status: 'ok' } });
+ const { Wrapper, mockRequest } = createMockAppWrapper();
+ const mockedStorages = vi.fn();
+ const mockedGetSettings = vi.fn();
+ const mockedUpdateSettings = vi.fn();
+
+ beforeEach(() => {
+ mockRequest.reset();
+ mockedStorages.mockReset().mockReturnValue(storagesData);
+ mockedGetSettings.mockReset().mockReturnValue(settingsData);
+ mockedUpdateSettings.mockReset().mockReturnValue({ data: { status: 'ok' } });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
const MockedBackupSettings = observer(() => {
- const api = nocobaseClient.useAPIClient();
- const axioMocked = new MockAdapter(api.axios);
- axioMocked.onGet(`backupSettings:get/1`).reply(() => {
+ mockRequest.onGet(`backupSettings:get/1`).reply(() => {
return [200, mockedGetSettings()];
});
- axioMocked.onGet(`storages:list`).reply(() => {
+ mockRequest.onGet(`storages:list`).reply(() => {
return [200, mockedStorages()];
});
- axioMocked.onPost(`backupSettings:update/1`).reply((config) => {
+ mockRequest.onPost(`backupSettings:update/1`).reply((config) => {
return [200, mockedUpdateSettings(config.data)];
});
@@ -110,20 +116,19 @@ describe('BackupSettings', () => {
});
test('should render backup settings', async () => {
- render();
- expect(screen.getByText('Restore password')).toBeInTheDocument();
+ render(, { wrapper: Wrapper });
+ expect(await screen.findByText('Restore password')).toBeInTheDocument();
});
test('should list "sync backup to cloud storage" options', async () => {
const user = userEvent.setup();
- render();
+ render(, { wrapper: Wrapper });
await sleep(3000);
- const inputEle: HTMLElement = document.querySelector('div.ant-formily-item-control') as HTMLInputElement;
+ const storageCombobox = screen.getAllByRole('combobox').at(-1) as HTMLElement;
await act(async () => {
- await user.click(inputEle);
+ await user.click(storageCombobox);
});
- const optionItems = document.querySelectorAll('span.ant-select-selection-item');
- expect(optionItems.length).toBe(2);
+ expect(await screen.findByText('ali')).toBeInTheDocument();
});
test('should trigger update backup settings api', async () => {
@@ -134,14 +139,13 @@ describe('BackupSettings', () => {
},
});
const user = userEvent.setup();
- render();
+ render(, { wrapper: Wrapper });
await sleep(3000);
+ const passwordInput = document.querySelector('input[type=password]') as HTMLInputElement;
await act(async () => {
- await user.click(screen.getByText('Restore password').parentElement.firstElementChild);
- await user.click(document.querySelector('div.ant-formily-item-control'));
- await user.click(screen.getByText('ali'));
- await user.type(document.querySelector('input[type=password]'), '123456');
- await user.click(screen.getByText('Submit'));
+ await user.clear(passwordInput);
+ await user.type(passwordInput, '123456');
+ await user.click(screen.getByRole('button', { name: /Submit/ }));
});
expect(mockedUpdateSettings).toHaveBeenCalled();
expect(mockedUpdateSettings.mock.calls[0][0]).toEqual(
diff --git a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupsManagement.test.tsx b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupsManagement.test.tsx
index 47ce89abbf8..a10013be7be 100644
--- a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupsManagement.test.tsx
+++ b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupsManagement.test.tsx
@@ -1,32 +1,50 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
import * as nocobaseClient from '@nocobase/client';
import { render, waitFor } from '@nocobase/test/client';
import React from 'react';
import { vi } from 'vitest';
import { BackupsManagement } from '../components/BackupsManagement';
import { NAMESPACE } from '../constants';
+import { createMockAppWrapper } from './testUtils';
-vi.mock('@nocobase/client', async () => {
- const actual = await vi.importActual('@nocobase/client');
- return {
- ...actual,
- useApp: () => ({
- i18n: {
- t: vi.fn().mockImplementation((key) => key), // Mock translation function, return the key directly
- },
- }),
- };
-});
-
-vi.mock('../BackupsTable', async () => {
+vi.mock('../components/BackupsTable', async () => {
return {
BackupsTable: vi.fn().mockImplementation(() => Backup Table
),
};
});
+vi.mock('../components/NewBackup', async () => {
+ return {
+ NewBackup: vi.fn().mockImplementation(() => New Backup
),
+ };
+});
+
+vi.mock('../components/RefreshBackups', async () => {
+ return {
+ RefreshBackups: vi.fn().mockImplementation(() => Refresh Backups
),
+ };
+});
+
+vi.mock('../components/RestoreFromLocal', async () => {
+ return {
+ RestoreFromLocal: vi.fn().mockImplementation(() => Restore From Local
),
+ };
+});
+
describe('BackupsManagement', () => {
+ const { Wrapper } = createMockAppWrapper();
+
test('should query the backups list data on render', async () => {
const spy = vi.spyOn(nocobaseClient, 'useRequest').mockReturnValue({} as any);
- render();
+ render(, { wrapper: Wrapper });
await waitFor(() => {
expect(spy).toHaveBeenCalledWith({
url: `${NAMESPACE}:list`,
diff --git a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupsTable.test.tsx b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupsTable.test.tsx
index 0db74a943ce..28b5f62c5e7 100644
--- a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupsTable.test.tsx
+++ b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/BackupsTable.test.tsx
@@ -10,13 +10,13 @@
import * as nocobaseClient from '@nocobase/client';
import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
import { App } from 'antd';
-import MockAdapter from 'axios-mock-adapter';
import { saveAs } from 'file-saver';
import React from 'react';
import { describe, vi } from 'vitest';
import { BackupFile, BackupsTable } from '../components/BackupsTable';
import { NAMESPACE } from '../constants';
import { BackupsContext } from '../contexts';
+import { createMockAppWrapper } from './testUtils';
const mockedData = {
data: [
@@ -33,19 +33,6 @@ const mockedData = {
],
};
-vi.mock('@nocobase/client', async () => {
- const actual = await vi.importActual('@nocobase/client');
- return {
- ...actual,
- useApp: () => ({
- i18n: {
- t: vi.fn().mockImplementation((key) => key),
- },
- }),
- useCurrentAppInfo: () => undefined,
- };
-});
-
vi.mock('file-saver', async () => {
return {
saveAs: vi.fn(),
@@ -53,14 +40,28 @@ vi.mock('file-saver', async () => {
});
describe('BackupsTable', () => {
- let mocked: MockAdapter;
+ const { Wrapper, mockRequest } = createMockAppWrapper();
+
+ beforeEach(() => {
+ mockRequest.reset();
+ vi.spyOn(nocobaseClient, 'useCurrentAppInfo').mockReturnValue(undefined as any);
+ mockRequest.onGet(`${NAMESPACE}:appInfo`).reply(200, {
+ data: {
+ database: {
+ dialect: 'sqlite',
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
const MockedTable = () => {
- const api = nocobaseClient.useAPIClient();
- mocked = new MockAdapter(api.axios);
- mocked.onGet('backups:list').reply(200, mockedData);
- mocked.onGet('backups:download').reply(200, 'mocked-backup-content');
- mocked.onPost('backups:destroy').reply(200, {});
+ mockRequest.onGet('backups:list').reply(200, mockedData);
+ mockRequest.onGet('backups:download').reply(200, 'mocked-backup-content');
+ mockRequest.onPost('backups:destroy').reply(200, {});
const useRequestValues = nocobaseClient.useRequest<{ data: BackupFile[] }>({
url: `${NAMESPACE}:list`,
@@ -75,7 +76,7 @@ describe('BackupsTable', () => {
};
test('should render table with correct columns and data', async () => {
- render();
+ render(, { wrapper: Wrapper });
await waitFor(() => {
expect(screen.getByText('Backup list')).toBeInTheDocument();
expect(screen.getByText('backup_20240818_182301_9056.nbdata')).toBeInTheDocument();
@@ -84,7 +85,7 @@ describe('BackupsTable', () => {
test('should handle download action', async () => {
const user = userEvent.setup();
- render();
+ render(, { wrapper: Wrapper });
await waitFor(() => {
expect(screen.getAllByText('Download')[0]).toBeInTheDocument();
});
@@ -92,7 +93,7 @@ describe('BackupsTable', () => {
await user.click(screen.getAllByText('Download')[0]);
await waitFor(() => {
- expect(mocked.history.get).toEqual(
+ expect(mockRequest.history.get).toEqual(
expect.arrayContaining([
expect.objectContaining({
url: 'backups:download',
@@ -116,7 +117,7 @@ describe('BackupsTable', () => {
},
});
const user = userEvent.setup();
- render();
+ render(, { wrapper: Wrapper });
await waitFor(async () => {
await user.click(screen.getAllByText('Delete')[0]);
expect(modalConfirmSpy).toHaveBeenCalled();
@@ -124,7 +125,7 @@ describe('BackupsTable', () => {
});
test('should handle backing up data', async () => {
- render();
+ render(, { wrapper: Wrapper });
await waitFor(async () => {
expect(screen.getByText('Backup list')).toBeInTheDocument();
expect(screen.getByRole('cell', { name: /Backing up/i })).toBeInTheDocument();
diff --git a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/NewBackup.test.tsx b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/NewBackup.test.tsx
index 591d2f1953f..0719bed13d9 100644
--- a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/NewBackup.test.tsx
+++ b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/NewBackup.test.tsx
@@ -1,46 +1,52 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
import * as nocobaseClient from '@nocobase/client';
-import { useAPIClient, useRequest } from '@nocobase/client';
+import { useRequest } from '@nocobase/client';
import { act, render, screen, userEvent, waitFor } from '@nocobase/test/client';
import { App } from 'antd';
-import MockAdapter from 'axios-mock-adapter';
import React from 'react';
import { BackupFile } from '../components/BackupsTable';
import { NewBackup } from '../components/NewBackup';
import { NAMESPACE } from '../constants';
import { BackupsContext } from '../contexts';
-
-vi.mock('@nocobase/client', async () => {
- const actual = await vi.importActual('@nocobase/client');
- return {
- ...actual,
- useApp: () => ({
- i18n: {
- t: vi.fn().mockImplementation((key) => key), // Mock translation function, return the key directly
- },
- }),
- };
-});
+import { createMockAppWrapper } from './testUtils';
describe('NewBackup', () => {
- let axioMocked: MockAdapter;
- const mockedRefresh = vi.fn().mockReturnValue({ data: [] });
- const mockedCreate = vi
- .fn()
- .mockReturnValue({ data: { name: 'backup_20240818_182302_2122.nbdata', inProgress: true } });
- const mockedStatus = vi
- .fn()
- .mockReturnValue({ data: { 'backup_20240818_182302_2122.nbdata': { inProgress: false } } });
+ const { Wrapper, mockRequest } = createMockAppWrapper();
+ const mockedRefresh = vi.fn();
+ const mockedCreate = vi.fn();
+ const mockedStatus = vi.fn();
+
+ beforeEach(() => {
+ mockRequest.reset();
+ mockedRefresh.mockReset().mockReturnValue({ data: [] });
+ mockedCreate.mockReset().mockReturnValue({
+ data: { name: 'backup_20240818_182302_2122.nbdata', inProgress: true },
+ });
+ mockedStatus.mockReset().mockReturnValue({
+ data: { 'backup_20240818_182302_2122.nbdata': { inProgress: false } },
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
const MockedNewBackupBtn = () => {
- const api = useAPIClient();
- axioMocked = new MockAdapter(api.axios);
- axioMocked.onGet(`${NAMESPACE}:list`).reply(() => {
+ mockRequest.onGet(`${NAMESPACE}:list`).reply(() => {
return [200, mockedRefresh()];
});
- axioMocked.onPost(`${NAMESPACE}:create`).reply(() => {
+ mockRequest.onPost(`${NAMESPACE}:create`).reply(() => {
return [200, mockedCreate()];
});
- axioMocked.onGet(`${NAMESPACE}:taskStatus`).reply(() => {
+ mockRequest.onGet(`${NAMESPACE}:taskStatus`).reply(() => {
return [200, mockedStatus()];
});
@@ -57,7 +63,7 @@ describe('NewBackup', () => {
};
test('should render new backup button', async () => {
- render();
+ render(, { wrapper: Wrapper });
expect(screen.getByText('New backup')).toBeInTheDocument();
});
@@ -73,18 +79,20 @@ describe('NewBackup', () => {
},
});
const user = userEvent.setup();
- render();
+ render(, { wrapper: Wrapper });
const newBackupBtn = screen.getByText('New backup');
await act(async () => {
await user.click(newBackupBtn);
});
+ await waitFor(() => {
+ expect(mockedCreate).toHaveBeenCalled();
+ expect(mockedRefresh).toHaveBeenCalled();
+ });
await waitFor(
() => {
- expect(mockedCreate).toHaveBeenCalled();
expect(mockedStatus).toHaveBeenCalled();
- expect(mockedRefresh).toHaveBeenCalled();
},
- { interval: 1000 },
+ { timeout: 5000 },
);
});
});
diff --git a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RefreshBackups.test.tsx b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RefreshBackups.test.tsx
index 9132898b9e6..e77f3385033 100644
--- a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RefreshBackups.test.tsx
+++ b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RefreshBackups.test.tsx
@@ -1,32 +1,31 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
import * as nocobaseClient from '@nocobase/client';
-import MockAdapter from 'axios-mock-adapter';
import { NAMESPACE } from '../constants';
import { BackupFile } from '../components/BackupsTable';
import { BackupsContext } from '../contexts';
import React from 'react';
import { act, render, screen, userEvent, waitFor } from '@nocobase/test/client';
import { RefreshBackups } from '../components/RefreshBackups';
-
-vi.mock('@nocobase/client', async () => {
- const actual = await vi.importActual('@nocobase/client');
- return {
- ...actual,
- useApp: () => ({
- i18n: {
- t: vi.fn().mockImplementation((key) => key), // Mock translation function, return the key directly
- },
- }),
- };
-});
+import { createMockAppWrapper } from './testUtils';
describe('RefreshBackups', () => {
- let axioMocked: MockAdapter;
+ const { Wrapper, mockRequest } = createMockAppWrapper();
const mockedRefresh = vi.fn().mockReturnValue({ data: [] });
+ beforeEach(() => {
+ mockRequest.reset();
+ });
+
const MockedRefreshBackupsBtn = () => {
- const api = nocobaseClient.useAPIClient();
- axioMocked = new MockAdapter(api.axios);
- axioMocked.onGet(`${NAMESPACE}:list`).reply(() => {
+ mockRequest.onGet(`${NAMESPACE}:list`).reply(() => {
return [200, mockedRefresh()];
});
@@ -43,20 +42,23 @@ describe('RefreshBackups', () => {
};
test('should render refresh backups button', async () => {
- render();
+ render(, { wrapper: Wrapper });
expect(screen.getByText('Refresh')).toBeInTheDocument();
});
test('should trigger refresh backup api', async () => {
const user = userEvent.setup();
- render();
+ render(, { wrapper: Wrapper });
expect(mockedRefresh).toHaveBeenCalledTimes(1);
- const newBackupBtn = screen.getByText('Refresh');
+ const newBackupBtn = await screen.findByRole('button', { name: /Refresh/ });
+ await waitFor(() => {
+ expect(newBackupBtn).toBeEnabled();
+ });
await act(async () => {
- user.click(newBackupBtn);
+ await user.click(newBackupBtn);
});
await waitFor(() => {
- expect(mockedRefresh).toHaveBeenCalledTimes(2);
+ expect(mockedRefresh.mock.calls.length).toBeGreaterThan(1);
});
});
});
diff --git a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RestoreFromBackup.test.tsx b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RestoreFromBackup.test.tsx
index 685967ebba2..9b690085722 100644
--- a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RestoreFromBackup.test.tsx
+++ b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RestoreFromBackup.test.tsx
@@ -1,31 +1,37 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
import * as nocobaseClient from '@nocobase/client';
-import MockAdapter from 'axios-mock-adapter';
import { NAMESPACE } from '../constants';
import { BackupFile } from '../components/BackupsTable';
import React from 'react';
import { act, render, screen, userEvent, waitFor } from '@nocobase/test/client';
import { RestoreFromBackup } from '../components/RestoreFromBackup';
-
-vi.mock('@nocobase/client', async () => {
- const actual = await vi.importActual('@nocobase/client');
- return {
- ...actual,
- useApp: () => ({
- i18n: {
- t: vi.fn().mockImplementation((key) => key), // Mock translation function, return the key directly
- },
- }),
- };
-});
+import { createMockAppWrapper } from './testUtils';
describe('RestoreFromBackup', () => {
- let axioMocked: MockAdapter;
+ const { Wrapper, mockRequest } = createMockAppWrapper();
const mockedRestore = vi.fn().mockReturnValue({ data: { status: 'ok' } });
+ beforeEach(() => {
+ mockRequest.reset();
+ mockRequest.onGet(`${NAMESPACE}:appInfo`).reply(200, {
+ data: {
+ database: {
+ dialect: 'sqlite',
+ },
+ },
+ });
+ });
+
const MockedRestoreFromBackup = () => {
- const api = nocobaseClient.useAPIClient();
- axioMocked = new MockAdapter(api.axios);
- axioMocked.onPost(`${NAMESPACE}:restore`).reply(() => {
+ mockRequest.onPost(`${NAMESPACE}:restore`).reply(() => {
return [200, mockedRestore()];
});
@@ -40,21 +46,21 @@ describe('RestoreFromBackup', () => {
};
test('should render restore from backup button', async () => {
- render();
+ render(, { wrapper: Wrapper });
expect(screen.getByText('Restore')).toBeInTheDocument();
});
test('should trigger restore api', async () => {
const user = userEvent.setup();
- render();
+ render(, { wrapper: Wrapper });
await act(async () => {
- user.click(screen.getByText('Restore'));
+ await user.click(screen.getByText('Restore'));
});
await waitFor(() => {
expect(screen.getByText('Submit')).toBeInTheDocument();
});
await act(async () => {
- user.click(screen.getByText('Submit'));
+ await user.click(screen.getByText('Submit'));
});
await waitFor(() => {
expect(mockedRestore).toHaveBeenCalled();
diff --git a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RestoreFromLocal.test.tsx b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RestoreFromLocal.test.tsx
index 831bca4f52d..c3a7f324b72 100644
--- a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RestoreFromLocal.test.tsx
+++ b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/RestoreFromLocal.test.tsx
@@ -1,55 +1,59 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
import * as nocobaseClient from '@nocobase/client';
import { act, fireEvent, render, screen, userEvent, waitFor } from '@nocobase/test/client';
-import MockAdapter from 'axios-mock-adapter';
import React from 'react';
import { RestoreFromLocal } from '../components/RestoreFromLocal';
import { NAMESPACE } from '../constants';
-
-vi.mock('@nocobase/client', async () => {
- const actual = await vi.importActual('@nocobase/client');
- return {
- ...actual,
- useApp: () => ({
- i18n: {
- t: vi.fn().mockImplementation((key) => key), // Mock translation function, return the key directly
- },
- }),
- };
-});
+import { createMockAppWrapper } from './testUtils';
describe('RestoreFromLocal', () => {
- let axioMocked: MockAdapter;
+ const { Wrapper, mockRequest } = createMockAppWrapper();
const mockedRestore = vi.fn().mockReturnValue({ data: { status: 'ok' } });
+ beforeEach(() => {
+ mockRequest.reset();
+ mockRequest.onGet(`${NAMESPACE}:appInfo`).reply(200, {
+ data: {
+ database: {
+ dialect: 'sqlite',
+ },
+ },
+ });
+ });
+
const MockedRestoreFromLocal = () => {
- const api = nocobaseClient.useAPIClient();
- axioMocked = new MockAdapter(api.axios);
- axioMocked.onPost(`${NAMESPACE}:upload`).reply(() => {
+ mockRequest.onPost(`${NAMESPACE}:upload`).reply(() => {
return [200, mockedRestore()];
});
return ;
};
test('should render restore from backup button', async () => {
- render();
+ render(, { wrapper: Wrapper });
expect(screen.getByText('Restore backup from local')).toBeInTheDocument();
});
test('should trigger restore api', async () => {
const file = new File([''], 'backup_20240818_182302_2122.nbdata', { type: 'application/octet-stream' });
const user = userEvent.setup();
- render();
+ render(, { wrapper: Wrapper });
await act(async () => {
- user.click(screen.getByText('Restore backup from local'));
+ await user.click(screen.getByText('Restore backup from local'));
});
await waitFor(() => {
- const fileBtn = screen.getByText((content, element) => {
- return element.tagName.toLowerCase() === 'input' && element['type'] === 'file';
- });
+ const fileBtn = document.querySelector('input[type="file"]') as HTMLInputElement;
fireEvent.change(fileBtn, { target: { files: [file] } });
});
await act(async () => {
- user.click(screen.getByText('Submit'));
+ await user.click(screen.getByText('Submit'));
});
await waitFor(() => {
expect(mockedRestore).toHaveBeenCalled();
diff --git a/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/testUtils.tsx b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/testUtils.tsx
new file mode 100644
index 00000000000..32f477b3c6a
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-backups/src/client/__tests__/testUtils.tsx
@@ -0,0 +1,58 @@
+/**
+ * This file is part of the NocoBase (R) project.
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
+ * Authors: NocoBase Team.
+ *
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
+ * For more information, please refer to: https://www.nocobase.com/agreement.
+ */
+
+import { APIClientProvider, Application, ApplicationContext, mockAPIClient } from '@nocobase/client';
+import { App as AntdApp } from 'antd';
+import React from 'react';
+
+if (typeof window !== 'undefined') {
+ Object.defineProperty(window, 'matchMedia', {
+ configurable: true,
+ writable: true,
+ value: (query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: () => {},
+ removeListener: () => {},
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ dispatchEvent: () => false,
+ }),
+ });
+}
+
+export const createMockAppWrapper = () => {
+ const { apiClient, mockRequest } = mockAPIClient();
+ const app = new Application({
+ apiClient,
+ router: {
+ type: 'memory',
+ },
+ });
+
+ app.i18n.t = ((key: string) => key) as any;
+
+ const Wrapper = ({ children }: { children?: React.ReactNode }) => {
+ return (
+
+
+ {children}
+
+
+ );
+ };
+
+ return {
+ app,
+ apiClient: app.apiClient,
+ mockRequest,
+ Wrapper,
+ };
+};
diff --git a/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/backup.test.ts b/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/backup.test.ts
index b8281f3bd0a..0c15ec94e2c 100644
--- a/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/backup.test.ts
+++ b/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/backup.test.ts
@@ -17,6 +17,8 @@ import fs from 'fs';
import * as cp from 'child_process';
import PluginFileManagerServer from '@nocobase/plugin-file-manager';
import yauzl from 'yauzl';
+import { EventEmitter } from 'events';
+import { Readable, Writable } from 'stream';
vi.mock('child_process', async (importOriginal) => {
const actual = await importOriginal();
@@ -31,6 +33,24 @@ vi.mock('child_process', async (importOriginal) => {
}
callback(null, 'done');
}),
+ spawn: vi.fn().mockImplementation(() => {
+ const stdout = Readable.from(['mocked database backup']);
+ const stderr = Readable.from([]);
+ const stdin = new Writable({
+ write(_chunk, _encoding, callback) {
+ callback();
+ },
+ });
+ const childProcess = new EventEmitter() as any;
+ childProcess.stdout = stdout;
+ childProcess.stderr = stderr;
+ childProcess.stdin = stdin;
+ setImmediate(() => {
+ childProcess.emit('exit', 0);
+ childProcess.emit('close', 0);
+ });
+ return childProcess;
+ }),
};
});
@@ -69,6 +89,7 @@ describe('BackupManager', async () => {
beforeEach(async () => {
app = await getApp();
await fs.promises.mkdir(backupFilesFolder, { recursive: true });
+ await fs.promises.unlink(finalBackupFilePath).catch(() => {});
});
afterEach(async () => {
diff --git a/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/restore.test.ts b/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/restore.test.ts
index e18ef8bd869..4ce8af53500 100644
--- a/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/restore.test.ts
+++ b/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/restore.test.ts
@@ -19,11 +19,32 @@ import { BackupManager, BackupSettings } from '../../managers/backup';
import { RestoreManager } from '../../managers/restore';
import backupCliResource from '../../resourcers/backup-cli';
import backupsResource from '../../resourcers/backups';
+import { EventEmitter } from 'events';
+import { Readable, Writable } from 'stream';
let mockExecImplementation = (command, _options, callback) => {
callback(null, 'done');
};
+let mockSpawnImplementation = () => {
+ const stdout = Readable.from(['mocked database backup']);
+ const stderr = Readable.from([]);
+ const stdin = new Writable({
+ write(_chunk, _encoding, callback) {
+ callback();
+ },
+ });
+ const childProcess = new EventEmitter() as any;
+ childProcess.stdout = stdout;
+ childProcess.stderr = stderr;
+ childProcess.stdin = stdin;
+ setImmediate(() => {
+ childProcess.emit('exit', 0);
+ childProcess.emit('close', 0);
+ });
+ return childProcess;
+};
+
vi.mock('child_process', async (importOriginal) => {
const actual = await importOriginal();
return {
@@ -32,6 +53,7 @@ vi.mock('child_process', async (importOriginal) => {
exec: vi
.fn()
.mockImplementation((command, options, callback) => mockExecImplementation(command, options, callback)),
+ spawn: vi.fn().mockImplementation((command, args, options) => mockSpawnImplementation(command, args, options)),
};
});
@@ -71,6 +93,8 @@ describe('RestoreManager', () => {
beforeEach(async () => {
app = await getApp();
await fs.promises.mkdir(backupFilesFolder, { recursive: true });
+ await fs.promises.unlink(finalBackupFilePath).catch(() => {});
+ await fs.promises.unlink(schemaMismatchBackupFilePath).catch(() => {});
mockExecImplementation = (command, _options, callback) => {
if (command.includes('-f')) {
@@ -94,6 +118,25 @@ describe('RestoreManager', () => {
}
callback(null, 'done');
};
+
+ mockSpawnImplementation = () => {
+ const stdout = Readable.from(['mocked database backup']);
+ const stderr = Readable.from([]);
+ const stdin = new Writable({
+ write(_chunk, _encoding, callback) {
+ callback();
+ },
+ });
+ const childProcess = new EventEmitter() as any;
+ childProcess.stdout = stdout;
+ childProcess.stderr = stderr;
+ childProcess.stdin = stdin;
+ setImmediate(() => {
+ childProcess.emit('exit', 0);
+ childProcess.emit('close', 0);
+ });
+ return childProcess;
+ };
});
afterEach(async () => {