@nocobase/client-v2 2.1.0-beta.34 → 2.1.0-beta.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/es/BaseApplication.d.ts +7 -1
  2. package/es/PluginManager.d.ts +2 -0
  3. package/es/components/PoweredBy.d.ts +18 -0
  4. package/es/components/SwitchLanguage.d.ts +11 -0
  5. package/es/components/form/DialogFormLayout.d.ts +75 -0
  6. package/es/components/form/DrawerFormLayout.d.ts +11 -11
  7. package/es/components/form/PasswordInput.d.ts +40 -0
  8. package/es/components/form/RemoteSelect.d.ts +79 -0
  9. package/es/components/form/index.d.ts +3 -0
  10. package/es/components/form/table/styles.d.ts +10 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/models/base/ActionModelCore.d.ts +6 -0
  13. package/es/flow/models/base/GridModel.d.ts +2 -0
  14. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
  15. package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
  16. package/es/flow-compat/passwordUtils.d.ts +1 -1
  17. package/es/hooks/index.d.ts +2 -0
  18. package/es/hooks/useCurrentAppInfo.d.ts +9 -0
  19. package/es/index.mjs +117 -105
  20. package/es/json-logic/globalOperators.d.ts +11 -0
  21. package/es/nocobase-buildin-plugin/index.d.ts +25 -0
  22. package/es/utils/appVersionHTML.d.ts +10 -0
  23. package/es/utils/globalDeps.d.ts +7 -0
  24. package/es/utils/index.d.ts +1 -0
  25. package/es/utils/remotePlugins.d.ts +4 -1
  26. package/lib/index.js +120 -108
  27. package/package.json +7 -6
  28. package/src/BaseApplication.tsx +11 -3
  29. package/src/PluginManager.ts +2 -0
  30. package/src/PluginSettingsManager.ts +2 -1
  31. package/src/__tests__/PluginSettingsManager.test.ts +19 -0
  32. package/src/__tests__/PoweredBy.test.tsx +130 -0
  33. package/src/__tests__/app.test.tsx +39 -0
  34. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
  35. package/src/__tests__/remotePlugins.test.ts +203 -0
  36. package/src/__tests__/useCurrentRoles.test.tsx +100 -0
  37. package/src/components/PoweredBy.tsx +71 -0
  38. package/src/components/README.md +314 -0
  39. package/src/components/README.zh-CN.md +312 -0
  40. package/src/components/SwitchLanguage.tsx +48 -0
  41. package/src/components/form/DialogFormLayout.tsx +111 -0
  42. package/src/components/form/DrawerFormLayout.tsx +13 -32
  43. package/src/components/form/PasswordInput.tsx +211 -0
  44. package/src/components/form/RemoteSelect.tsx +137 -0
  45. package/src/components/form/index.tsx +3 -0
  46. package/src/components/form/table/Table.tsx +2 -1
  47. package/src/components/form/table/styles.ts +19 -0
  48. package/src/components/index.ts +2 -0
  49. package/src/css-variable/CSSVariableProvider.tsx +10 -1
  50. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
  51. package/src/flow/actions/dataScope.tsx +3 -0
  52. package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
  54. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
  55. package/src/flow/components/BlockItemCard.tsx +2 -2
  56. package/src/flow/models/base/ActionModel.tsx +8 -7
  57. package/src/flow/models/base/ActionModelCore.tsx +15 -7
  58. package/src/flow/models/base/GridModel.tsx +93 -36
  59. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
  60. package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
  61. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
  62. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
  63. package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
  64. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
  65. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
  66. package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
  67. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
  68. package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
  69. package/src/hooks/index.ts +2 -0
  70. package/src/hooks/useCurrentAppInfo.ts +36 -0
  71. package/src/json-logic/globalOperators.js +731 -0
  72. package/src/nocobase-buildin-plugin/index.tsx +70 -16
  73. package/src/utils/appVersionHTML.ts +28 -0
  74. package/src/utils/globalDeps.ts +47 -31
  75. package/src/utils/index.tsx +2 -0
  76. package/src/utils/remotePlugins.ts +119 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.0-beta.34",
3
+ "version": "2.1.0-beta.36",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -26,10 +26,11 @@
26
26
  "@formily/antd-v5": "1.2.3",
27
27
  "@formily/react": "^2.2.27",
28
28
  "@formily/shared": "^2.2.27",
29
- "@nocobase/evaluators": "2.1.0-beta.34",
30
- "@nocobase/flow-engine": "2.1.0-beta.34",
31
- "@nocobase/sdk": "2.1.0-beta.34",
32
- "@nocobase/shared": "2.1.0-beta.34",
29
+ "@nocobase/evaluators": "2.1.0-beta.36",
30
+ "@nocobase/flow-engine": "2.1.0-beta.36",
31
+ "@nocobase/sdk": "2.1.0-beta.36",
32
+ "@nocobase/shared": "2.1.0-beta.36",
33
+ "@nocobase/utils": "2.1.0-beta.36",
33
34
  "ahooks": "^3.7.2",
34
35
  "antd": "5.24.2",
35
36
  "antd-style": "3.7.1",
@@ -43,5 +44,5 @@
43
44
  "react-i18next": "^11.15.1",
44
45
  "react-router-dom": "^6.30.1"
45
46
  },
46
- "gitHead": "ca804833299c547f8d49f8d58f73273a4bfcd03c"
47
+ "gitHead": "397d45c744f6eb48b3a0cd785c87cbf1257c3513"
47
48
  }
@@ -40,6 +40,7 @@ import type {
40
40
  RouterOptions,
41
41
  } from './RouterManager';
42
42
  import { WebSocketClient, type WebSocketClientOptions } from './WebSocketClient';
43
+ import { getOperators } from './json-logic/globalOperators';
43
44
  import { compose, normalizeContainer } from './utils';
44
45
  import { defineGlobalDeps } from './utils/globalDeps';
45
46
  import { getRequireJs } from './utils/requirejs';
@@ -57,6 +58,11 @@ type AuthTokenPayload = {
57
58
  token: string;
58
59
  authenticator: string | null;
59
60
  };
61
+ export type JsonLogic = {
62
+ apply: (logic: any, data?: any) => any;
63
+ addOperation: (name: string, fn?: any) => void;
64
+ rmOperation: (name: string) => void;
65
+ };
60
66
 
61
67
  const LEADING_SLASHES_REGEXP = /^\/+/;
62
68
  const TRAILING_SLASHES_REGEXP = /\/+$/;
@@ -123,6 +129,7 @@ export abstract class BaseApplication<
123
129
  public favicon!: string;
124
130
  public flowEngine: FlowEngine;
125
131
  public dataSourceManager: any;
132
+ public jsonLogic!: JsonLogic;
126
133
  public context: FlowEngineContext & {
127
134
  routeRepository: RouteRepository;
128
135
  appInfo: Promise<Record<string, any>>;
@@ -220,6 +227,7 @@ export abstract class BaseApplication<
220
227
 
221
228
  protected afterManagersInitialized() {
222
229
  this.aiManager = new AIManager(this);
230
+ this.jsonLogic = getOperators();
223
231
  }
224
232
 
225
233
  protected configureContext() {
@@ -360,11 +368,11 @@ export abstract class BaseApplication<
360
368
  });
361
369
  }
362
370
 
363
- updateFavicon(favicon?: string) {
371
+ updateFavicon(favicon?: string | null) {
364
372
  let faviconLinkElement = document.querySelector('link[rel="shortcut icon"]') as HTMLLinkElement;
365
373
 
366
- if (favicon) {
367
- this.favicon = favicon;
374
+ if (arguments.length > 0) {
375
+ this.favicon = favicon || '';
368
376
  }
369
377
 
370
378
  const iconHref = this.favicon || '/favicon/favicon.ico';
@@ -26,6 +26,8 @@ export type PluginData = {
26
26
  version: string;
27
27
  url: string;
28
28
  clientV2Url?: string;
29
+ devMode?: 'esm';
30
+ appDevDependencies?: string[];
29
31
  type: 'local' | 'upload' | 'npm';
30
32
  };
31
33
 
@@ -432,7 +432,7 @@ export class PluginSettingsManager<TApp extends BaseApplication<any> = BaseAppli
432
432
  return null;
433
433
  }
434
434
 
435
- const { title, aclSnippet, key, menuKey, name, ...others } = page;
435
+ const { title, aclSnippet, key, menuKey, name, icon, ...others } = page;
436
436
 
437
437
  return {
438
438
  ...others,
@@ -443,6 +443,7 @@ export class PluginSettingsManager<TApp extends BaseApplication<any> = BaseAppli
443
443
  name,
444
444
  title,
445
445
  label: title,
446
+ icon: this.renderIcon(icon),
446
447
  path: this.getRoutePath(name),
447
448
  sort: page.sort,
448
449
  isAllow,
@@ -76,6 +76,25 @@ describe('PluginSettingsManager v2', () => {
76
76
  expect(app.router.get('admin.settings.demo.advanced')).toMatchObject({ path: 'advanced' });
77
77
  });
78
78
 
79
+ it('should render string icon on both menu and page tab via renderIcon', () => {
80
+ // Previously `renderPage` spread the raw `icon` string straight to the antd
81
+ // Menu item, which displayed "LockOutlinedTitle" as text. Both `renderMenuItem`
82
+ // and `renderPage` must coerce string icon names to React elements.
83
+ const app = createMockClient();
84
+
85
+ app.pluginSettingsManager.addMenuItem({ key: 'demo', title: 'Demo', icon: 'TeamOutlined' });
86
+ app.pluginSettingsManager.addPageTabItem({
87
+ menuKey: 'demo',
88
+ key: 'index',
89
+ title: 'Overview',
90
+ icon: 'LockOutlined',
91
+ });
92
+
93
+ const list = app.pluginSettingsManager.getList();
94
+ expect(React.isValidElement(list[0].icon)).toBe(true);
95
+ expect(React.isValidElement(list[0].children?.[0].icon)).toBe(true);
96
+ });
97
+
79
98
  it('should support componentLoader on page item', () => {
80
99
  const app = createMockClient();
81
100
  const componentLoader = async () => ({
@@ -0,0 +1,130 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { render, screen, waitFor } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { createMockClient } from '../MockApplication';
13
+ import { Plugin } from '../Plugin';
14
+ import PoweredBy from '../components/PoweredBy';
15
+
16
+ class PoweredByRoutePlugin extends Plugin {
17
+ async load() {
18
+ this.router.add('root', {
19
+ path: '/',
20
+ Component: PoweredBy,
21
+ });
22
+ }
23
+ }
24
+
25
+ class MockCustomBrandPlugin extends Plugin {}
26
+
27
+ const renderPoweredBy = async (plugins: any[] = [], appInfoData: Record<string, any> = { version: '1.2.3' }) => {
28
+ const app = createMockClient({
29
+ plugins: [PoweredByRoutePlugin as any, ...plugins],
30
+ });
31
+
32
+ app.apiMock.onGet('app:getInfo').reply(200, {
33
+ data: appInfoData,
34
+ });
35
+
36
+ const Root = app.getRootComponent();
37
+ const result = render(<Root />);
38
+
39
+ await waitFor(() => {
40
+ expect(document.querySelector('.ant-spin-spinning')).not.toBeInTheDocument();
41
+ });
42
+
43
+ return result;
44
+ };
45
+
46
+ describe('PoweredBy', () => {
47
+ afterEach(() => {
48
+ vi.restoreAllMocks();
49
+ });
50
+
51
+ it('should render the default brand when custom-brand is not installed', async () => {
52
+ const { container } = await renderPoweredBy();
53
+
54
+ expect(screen.getByRole('link', { name: 'NocoBase' })).toHaveAttribute('href', 'https://www.nocobase.com');
55
+ expect(container).toHaveTextContent('Powered by NocoBase');
56
+ // The `.nb-brand` className is reserved for the custom-brand HTML branch
57
+ // so downstream stylesheets can selectively target customised content
58
+ // without leaking onto the default footer.
59
+ expect(container.querySelector('.nb-brand')).not.toBeInTheDocument();
60
+ });
61
+
62
+ it('should render custom-brand HTML and replace appVersion', async () => {
63
+ const { container } = await renderPoweredBy([
64
+ [
65
+ MockCustomBrandPlugin,
66
+ {
67
+ packageName: '@nocobase/plugin-custom-brand',
68
+ options: {
69
+ brand: '<span>Custom Brand</span>{{appVersion}}',
70
+ },
71
+ },
72
+ ],
73
+ ]);
74
+
75
+ await waitFor(() => {
76
+ expect(container.querySelector('.nb-brand')).toHaveTextContent('Custom Brandv1.2.3');
77
+ });
78
+ expect(container.querySelector('.nb-app-version')).toHaveTextContent('v1.2.3');
79
+ expect(screen.queryByText('Powered by')).not.toBeInTheDocument();
80
+ });
81
+
82
+ it('should not render undefined appVersion when app version is unavailable', async () => {
83
+ const { container } = await renderPoweredBy(
84
+ [
85
+ [
86
+ MockCustomBrandPlugin,
87
+ {
88
+ packageName: '@nocobase/plugin-custom-brand',
89
+ options: {
90
+ brand: '<span>Custom Brand</span>{{appVersion}}',
91
+ },
92
+ },
93
+ ],
94
+ ],
95
+ {},
96
+ );
97
+
98
+ expect(container.querySelector('.nb-brand')).toHaveTextContent('Custom Brand');
99
+ expect(container.querySelector('.nb-brand')).not.toHaveTextContent('undefined');
100
+ expect(container.querySelector('.nb-app-version')).not.toBeInTheDocument();
101
+ });
102
+
103
+ it('should escape custom-brand appVersion placeholder', async () => {
104
+ // Defence in depth: even if the back-end ever returns a tampered
105
+ // `app:getInfo` payload, the version string must be HTML-escaped
106
+ // before being interpolated into the custom-brand template — never
107
+ // produce a live `<script>` node in the DOM.
108
+ const { container } = await renderPoweredBy(
109
+ [
110
+ [
111
+ MockCustomBrandPlugin,
112
+ {
113
+ packageName: '@nocobase/plugin-custom-brand',
114
+ options: {
115
+ brand: '<span>Custom Brand</span>{{appVersion}}',
116
+ },
117
+ },
118
+ ],
119
+ ],
120
+ {
121
+ version: '<script>alert(1)</script>&"',
122
+ },
123
+ );
124
+
125
+ await waitFor(() => {
126
+ expect(container.querySelector('.nb-app-version')).toHaveTextContent('v<script>alert(1)</script>&"');
127
+ });
128
+ expect(container.querySelector('.nb-brand script')).not.toBeInTheDocument();
129
+ });
130
+ });
@@ -12,6 +12,7 @@ import { useFlowEngineContext } from '@nocobase/flow-engine';
12
12
  import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
13
13
  import React from 'react';
14
14
  import { Outlet } from 'react-router-dom';
15
+ import { escapeHTML, getAppVersionHTML } from '../utils';
15
16
 
16
17
  const waitForAppReady = async () => {
17
18
  await waitFor(() => {
@@ -48,6 +49,14 @@ describe('app', () => {
48
49
  expect(app.getHref('/test')).toBe('/test');
49
50
  });
50
51
 
52
+ it('should initialize shared jsonLogic operators', () => {
53
+ const app = new Application({ router });
54
+
55
+ expect(app.jsonLogic.apply({ $eq: [1, '1'] })).toBe(true);
56
+ app.jsonLogic.addOperation('$testAlwaysTrue', () => true);
57
+ expect(app.jsonLogic.apply({ $testAlwaysTrue: [] })).toBe(true);
58
+ });
59
+
51
60
  it('should apply the provided favicon immediately', () => {
52
61
  const app = new Application({ router });
53
62
 
@@ -58,6 +67,36 @@ describe('app', () => {
58
67
  expect(favicon.getAttribute('href')).toBe('/custom-favicon.ico');
59
68
  });
60
69
 
70
+ it('should reset favicon to default when favicon is cleared', () => {
71
+ const app = new Application({ router });
72
+
73
+ app.updateFavicon('/custom-favicon.ico');
74
+ app.updateFavicon(null);
75
+
76
+ const favicon = document.querySelector('link[rel="shortcut icon"]') as HTMLLinkElement;
77
+ expect(favicon).toBeInTheDocument();
78
+ expect(favicon.getAttribute('href')).toBe('/favicon/favicon.ico');
79
+ });
80
+
81
+ it('should reset favicon to default when favicon is explicitly undefined', () => {
82
+ const app = new Application({ router });
83
+
84
+ app.updateFavicon('/custom-favicon.ico');
85
+ app.updateFavicon(undefined);
86
+
87
+ const favicon = document.querySelector('link[rel="shortcut icon"]') as HTMLLinkElement;
88
+ expect(favicon).toBeInTheDocument();
89
+ expect(favicon.getAttribute('href')).toBe('/favicon/favicon.ico');
90
+ });
91
+
92
+ it('should escape app version html placeholder content', () => {
93
+ expect(getAppVersionHTML('<script>alert(1)</script>&"')).toBe(
94
+ '<span class="nb-app-version">v&lt;script&gt;alert(1)&lt;/script&gt;&amp;&quot;</span>',
95
+ );
96
+ expect(getAppVersionHTML(undefined)).toBe('');
97
+ expect(escapeHTML("NocoBase <v2> & 'beta'")).toBe('NocoBase &lt;v2&gt; &amp; &#39;beta&#39;');
98
+ });
99
+
61
100
  it('should reject invalid component objects but keep valid exotic components', () => {
62
101
  const app = new Application({ router });
63
102
  const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -39,30 +39,18 @@ describe('nocobase buildin plugin auth redirect', () => {
39
39
  vi.restoreAllMocks();
40
40
  });
41
41
 
42
- it('should redirect unauthenticated admin access to v2 signin with replace', async () => {
43
- const replace = vi.fn();
44
- Object.defineProperty(globalThis.window, 'location', {
45
- configurable: true,
46
- value: {
47
- ...originalLocation,
48
- pathname: '/v2/admin/7vu4c2sdk6h',
49
- search: '',
50
- hash: '',
51
- replace,
52
- },
53
- });
54
-
42
+ it('should navigate to v2 signin when /auth:check returns no user', async () => {
43
+ // Aligns with v1: use react-router navigate (virtual) rather than
44
+ // `window.location.replace`, so a `window.location.href` queued elsewhere
45
+ // (e.g. 2FA's `code:302` response interceptor) can commit instead of being
46
+ // overridden.
55
47
  const app = createMockClient({
56
48
  publicPath: '/v2/',
57
49
  plugins: [NocoBaseBuildInPlugin as any],
58
50
  router: { type: 'memory', initialEntries: ['/v2/admin/7vu4c2sdk6h'] },
59
51
  });
60
52
  app.apiMock.onGet('app:getLang').reply(200, {
61
- data: {
62
- lang: 'en-US',
63
- resources: { client: {} },
64
- cron: {},
65
- },
53
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
66
54
  });
67
55
  app.apiMock.onGet('/auth:check').reply(200, { data: {} });
68
56
 
@@ -70,68 +58,63 @@ describe('nocobase buildin plugin auth redirect', () => {
70
58
  render(<Root />);
71
59
 
72
60
  await waitFor(() => {
73
- expect(replace).toHaveBeenCalledWith('/v2/signin?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
61
+ expect(app.router.router.state.location.pathname).toBe('/v2/signin');
62
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fv2%2Fadmin%2F7vu4c2sdk6h');
74
63
  });
75
64
  });
76
65
 
77
- it('should redirect unauthenticated v2 root access to v2 signin with default admin redirect', async () => {
78
- const replace = vi.fn();
79
- Object.defineProperty(globalThis.window, 'location', {
80
- configurable: true,
81
- value: {
82
- ...originalLocation,
83
- pathname: '/nocobase/v2/',
84
- search: '',
85
- hash: '',
86
- replace,
87
- },
66
+ it('should short-circuit /auth:check when server returns code:302 instead of redirecting to signin', async () => {
67
+ // When the server signals an intermediate redirect (typically 2FA verify),
68
+ // CurrentUserProvider must NOT treat the missing `user.id` as "logged out"
69
+ // and race the 2FA response interceptor with its own signin redirect.
70
+ const app = createMockClient({
71
+ publicPath: '/v2/',
72
+ plugins: [NocoBaseBuildInPlugin as any],
73
+ router: { type: 'memory', initialEntries: ['/v2/admin'] },
74
+ });
75
+ app.apiMock.onGet('app:getLang').reply(200, {
76
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
77
+ });
78
+ app.apiMock.onGet('/auth:check').reply(200, {
79
+ data: { code: 302, redirect: '/2fa?redirect=/admin' },
88
80
  });
89
81
 
82
+ const Root = app.getRootComponent();
83
+ render(<Root />);
84
+
85
+ // Give CurrentUserProvider time to process the response.
86
+ await new Promise((resolve) => setTimeout(resolve, 50));
87
+ expect(app.router.router.state.location.pathname).toBe('/v2/admin');
88
+ expect(app.router.router.state.location.search).toBe('');
89
+ });
90
+
91
+ it('should redirect unauthenticated v2 root access to v2 signin via <Navigate />', async () => {
90
92
  const app = createMockClient({
91
93
  publicPath: '/nocobase/v2/',
92
94
  plugins: [NocoBaseBuildInPlugin as any],
93
95
  router: { type: 'memory', initialEntries: ['/nocobase/v2/'] },
94
96
  });
95
97
  app.apiMock.onGet('app:getLang').reply(200, {
96
- data: {
97
- lang: 'en-US',
98
- resources: { client: {} },
99
- cron: {},
100
- },
98
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
101
99
  });
102
100
 
103
101
  const Root = app.getRootComponent();
104
102
  render(<Root />);
105
103
 
106
104
  await waitFor(() => {
107
- expect(replace).toHaveBeenCalledWith('/nocobase/v2/signin?redirect=%2Fnocobase%2Fv2%2Fadmin');
105
+ expect(app.router.router.state.location.pathname).toBe('/nocobase/v2/signin');
106
+ expect(app.router.router.state.location.search).toBe('?redirect=%2Fnocobase%2Fv2%2Fadmin');
108
107
  });
109
108
  });
110
109
 
111
110
  it('should render v2 admin root without redirecting away', async () => {
112
- const replace = vi.fn();
113
- Object.defineProperty(globalThis.window, 'location', {
114
- configurable: true,
115
- value: {
116
- ...originalLocation,
117
- pathname: '/v2/admin',
118
- search: '',
119
- hash: '',
120
- replace,
121
- },
122
- });
123
-
124
111
  const app = createMockClient({
125
112
  publicPath: '/v2/',
126
113
  plugins: [NocoBaseBuildInPlugin as any],
127
114
  router: { type: 'memory', initialEntries: ['/v2/admin'] },
128
115
  });
129
116
  app.apiMock.onGet('app:getLang').reply(200, {
130
- data: {
131
- lang: 'en-US',
132
- resources: { client: {} },
133
- cron: {},
134
- },
117
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
135
118
  });
136
119
  app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
137
120
  app.apiMock.onGet('systemSettings:get').reply(200, { data: {} });
@@ -152,36 +135,20 @@ describe('nocobase buildin plugin auth redirect', () => {
152
135
  await waitFor(() => {
153
136
  expect(container.innerHTML).toContain('No pages yet, please configure first');
154
137
  });
155
- expect(replace).not.toHaveBeenCalled();
138
+ expect(app.router.router.state.location.pathname).toBe('/v2/admin');
156
139
  expect(container.innerHTML).not.toContain('Legacy page');
157
140
  });
158
141
 
159
142
  it.each(['/v2/admin/legacy-page/tab/tab-1', '/v2/admin/legacy-page/view/detail'])(
160
143
  'should show 404 for authenticated direct v1-style v2 page access: %s',
161
144
  async (pathname) => {
162
- const replace = vi.fn();
163
- Object.defineProperty(globalThis.window, 'location', {
164
- configurable: true,
165
- value: {
166
- ...originalLocation,
167
- pathname,
168
- search: '?from=direct',
169
- hash: '#dialog',
170
- replace,
171
- },
172
- });
173
-
174
145
  const app = createMockClient({
175
146
  publicPath: '/v2/',
176
147
  plugins: [NocoBaseBuildInPlugin as any],
177
148
  router: { type: 'memory', initialEntries: [pathname] },
178
149
  });
179
150
  app.apiMock.onGet('app:getLang').reply(200, {
180
- data: {
181
- lang: 'en-US',
182
- resources: { client: {} },
183
- cron: {},
184
- },
151
+ data: { lang: 'en-US', resources: { client: {} }, cron: {} },
185
152
  });
186
153
  app.apiMock.onGet('/auth:check').reply(200, { data: { id: 1 } });
187
154
  app.apiMock.onGet('systemSettings:get').reply(200, { data: {} });
@@ -200,7 +167,7 @@ describe('nocobase buildin plugin auth redirect', () => {
200
167
  render(<Root />);
201
168
 
202
169
  expect(await screen.findByText('404')).toBeInTheDocument();
203
- expect(replace).not.toHaveBeenCalled();
170
+ expect(app.router.router.state.location.pathname).toBe(pathname);
204
171
  },
205
172
  );
206
173
  });