@nocobase/app-template-default 1.0.0-beta.79 → 1.0.0-beta.80
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.
- package/AGENTS.md +4 -4
- package/README.MD +4 -2
- package/package.json +8 -7
- package/tests/client/account-permissions.test.tsx +174 -0
- package/tests/{components → client}/app-layout-routes.test.tsx +25 -42
- package/tests/{logic → client}/app-locale-coverage.test.ts +1 -4
- package/tests/{logic → client}/client-plugin-registry.test.ts +1 -4
- package/tests/{logic → client}/client-route-recovery.test.tsx +27 -7
- package/tests/{components → client}/header-hover.test.tsx +4 -14
- package/tests/{components → client}/home.test.tsx +27 -41
- package/tests/client/inbox.test.tsx +94 -0
- package/tests/{components → client}/permission-management-tabs.test.tsx +23 -44
- package/tests/client/sidebar-permissions.test.tsx +157 -0
- package/tests/{logic → client}/theme-tokens.test.ts +6 -8
- package/tests/client/user-menu-sign-out.test.tsx +78 -0
- package/tests/{logic → database}/device-code-migration.test.ts +0 -1
- package/tests/helpers/authenticated-client.tsx +148 -0
- package/tests/{logic → project}/agents-md.test.ts +0 -2
- package/tests/{logic → project}/asset-url-build.test.ts +0 -1
- package/tests/{logic → project}/config-example.test.ts +0 -2
- package/tests/{logic → project}/dev-client-watch.test.ts +0 -2
- package/tests/{logic → project}/plugin-commands.test.ts +0 -2
- package/tests/{logic → project}/server-imports.test.ts +0 -1
- package/tests/{logic → project}/tailwind-sources.test.ts +0 -1
- package/tests/{logic → project}/template-package-name.test.ts +0 -2
- package/tests/{logic → server}/app-server.test.ts +0 -1
- package/tests/{logic → server}/authorization-assignment-safety.test.ts +0 -2
- package/tests/{logic → server}/config.test.ts +0 -2
- package/tests/{logic → server}/session-isolation.test.ts +0 -2
- package/tests/setup/application-environment.ts +8 -0
- package/tests/setup/client-config.ts +0 -2
- package/vitest.config.ts +38 -23
- package/tests/components/inbox.test.tsx +0 -124
- package/tests/components/sidebar-permissions.test.tsx +0 -108
- package/tests/logic/account-permissions.test.tsx +0 -204
- package/tests/logic/user-menu-sign-out.test.tsx +0 -107
- /package/tests/{components → client}/app-title.test.tsx +0 -0
- /package/tests/{components → client}/auth-i18n.test.tsx +0 -0
- /package/tests/{components → client}/back-button.test.tsx +0 -0
- /package/tests/{logic → client}/client-auth.test.tsx +0 -0
- /package/tests/{logic → client}/client-routes.test.ts +0 -0
- /package/tests/{logic → client}/client-runtime.test.ts +0 -0
- /package/tests/{logic → client}/client-shell.test.tsx +0 -0
- /package/tests/{logic → client}/client-theme.test.tsx +0 -0
- /package/tests/{components → client}/data-table.test.tsx +0 -0
- /package/tests/{components → client}/default-access-editor.test.tsx +0 -0
- /package/tests/{logic → client}/default-access-model.test.ts +0 -0
- /package/tests/{components → client}/default-access.test.tsx +0 -0
- /package/tests/{components → client}/file-preview.test.tsx +0 -0
- /package/tests/{logic → client}/foundation-integration.test.ts +0 -0
- /package/tests/{components → client}/language-switcher.test.tsx +0 -0
- /package/tests/{components → client}/layout-containers.test.tsx +0 -0
- /package/tests/{components → client}/layout-i18n.test.tsx +0 -0
- /package/tests/{components → client}/loading.test.tsx +0 -0
- /package/tests/{components → client}/navigation-menu.test.tsx +0 -0
- /package/tests/{components → client}/page-harness.test.tsx +0 -0
- /package/tests/{logic → client}/page-permission-navigation.test.ts +0 -0
- /package/tests/{logic → client}/page-permission-titles.test.ts +0 -0
- /package/tests/{components → client}/permission-editor.test.tsx +0 -0
- /package/tests/{components → client}/permission-filter-binding.test.tsx +0 -0
- /package/tests/{components → client}/permission-filter.test.tsx +0 -0
- /package/tests/{logic → client}/permission-management-routes.test.ts +0 -0
- /package/tests/{logic → client}/permission-workspace.test.ts +0 -0
- /package/tests/{components → client}/primitive-labels.test.tsx +0 -0
- /package/tests/{logic → client}/return-to.test.ts +0 -0
- /package/tests/{components → client}/role-list.test.tsx +0 -0
- /package/tests/{components → client}/roles.test.tsx +0 -0
- /package/tests/{logic → client}/route-navigation.test.ts +0 -0
- /package/tests/{logic → client}/route-overlay.test.tsx +0 -0
- /package/tests/{logic → client}/scrollbar.test.ts +0 -0
- /package/tests/{logic → client}/session-preparation.test.tsx +0 -0
- /package/tests/{components → client}/settings-continuity.test.tsx +0 -0
- /package/tests/{components → client}/settings-feedback.test.tsx +0 -0
- /package/tests/{logic → client}/settings-permissions.test.ts +0 -0
- /package/tests/{components → client}/settings.test.tsx +0 -0
- /package/tests/{components → client}/setup-readiness.test.tsx +0 -0
- /package/tests/{components → client}/sidebar-preference.test.tsx +0 -0
- /package/tests/{components → client}/status-badge.test.tsx +0 -0
- /package/tests/{components → client}/table-url-state.test.tsx +0 -0
- /package/tests/{logic → client}/theme-preferences.test.ts +0 -0
- /package/tests/{logic → client}/toaster.test.tsx +0 -0
- /package/tests/{components → client}/user-drawer.test.tsx +0 -0
- /package/tests/{components → client}/user-permissions.test.tsx +0 -0
- /package/tests/{logic → server}/lifecycle.test.ts +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { CookieJar } from 'jsdom';
|
|
3
|
+
import { onTestFinished, vi } from 'vitest';
|
|
4
|
+
import { act, waitFor } from '@testing-library/react';
|
|
5
|
+
import { createTestApp, type TestApp } from '@nocobase/app-testing/server';
|
|
6
|
+
import { renderWithApp } from '@nocobase/app-testing/client';
|
|
7
|
+
import authorization from '@nocobase/app-plugin-authorization/client/plugin';
|
|
8
|
+
import { authorizationToken } from '@nocobase/app-plugin-authorization/server';
|
|
9
|
+
import type { PermissionGrant } from '@nocobase/authorization/core';
|
|
10
|
+
import authentication from '@nocobase/app-plugin-authentication/client/plugin';
|
|
11
|
+
import { useAuthentication } from '@nocobase/app-plugin-authentication/client';
|
|
12
|
+
import {
|
|
13
|
+
signIn,
|
|
14
|
+
DEFAULT_ADMIN_CREDENTIALS,
|
|
15
|
+
type TestSession,
|
|
16
|
+
type SignInCredentials,
|
|
17
|
+
} from '@nocobase/app-plugin-authentication/testing';
|
|
18
|
+
import { createStandaloneServer } from '../../server/standalone.js';
|
|
19
|
+
import packageMetadata from '../../package.json' with { type: 'json' };
|
|
20
|
+
import enUS from '../../client/locales/en-US.js';
|
|
21
|
+
import zhCN from '../../client/locales/zh-CN.js';
|
|
22
|
+
|
|
23
|
+
/** Real application, authentication and cookies; only deliberate transport failures are injected by the caller. */
|
|
24
|
+
export async function renderAuthenticatedClient(
|
|
25
|
+
ui: ReactElement,
|
|
26
|
+
options: {
|
|
27
|
+
member?: boolean;
|
|
28
|
+
grants?: readonly PermissionGrant[];
|
|
29
|
+
route?: string;
|
|
30
|
+
locale?: string;
|
|
31
|
+
prepare?: (server: TestApp, session: TestSession) => Promise<void>;
|
|
32
|
+
namespaces?: NonNullable<Parameters<typeof renderWithApp>[1]>['namespaces'];
|
|
33
|
+
beforeRequest?: (request: Request) => void | Promise<void>;
|
|
34
|
+
afterResponse?: (
|
|
35
|
+
request: Request,
|
|
36
|
+
response: Response,
|
|
37
|
+
) => void | Promise<void>;
|
|
38
|
+
} = {},
|
|
39
|
+
) {
|
|
40
|
+
const server = await createTestApp({
|
|
41
|
+
createServer: createStandaloneServer,
|
|
42
|
+
connections: ['main'],
|
|
43
|
+
config: {
|
|
44
|
+
logging: { enabled: false },
|
|
45
|
+
app: { publicOrigin: 'http://localhost' },
|
|
46
|
+
auth: {
|
|
47
|
+
secret: 'test-auth-secret-at-least-32-characters',
|
|
48
|
+
emailAndPassword: { disableSignUp: false },
|
|
49
|
+
advanced: { disableOriginCheck: false, disableCSRFCheck: false },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
onTestFinished(() => server.close());
|
|
54
|
+
const member = {
|
|
55
|
+
name: 'Test member',
|
|
56
|
+
email: 'member@example.test',
|
|
57
|
+
password: 'test-member-password',
|
|
58
|
+
};
|
|
59
|
+
if (options.member) {
|
|
60
|
+
const response = await server.request('/auth/sign-up/email', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'content-type': 'application/json',
|
|
64
|
+
origin: 'http://localhost',
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify(member),
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
throw new Error(`Member registration failed: ${await response.text()}`);
|
|
70
|
+
}
|
|
71
|
+
const session = await signIn(
|
|
72
|
+
server,
|
|
73
|
+
options.member ? member : DEFAULT_ADMIN_CREDENTIALS,
|
|
74
|
+
);
|
|
75
|
+
if (options.grants) {
|
|
76
|
+
const authz = server.application.container.resolve(authorizationToken);
|
|
77
|
+
await authz.permissionSets.create({
|
|
78
|
+
key: 'test-access',
|
|
79
|
+
grants: options.grants,
|
|
80
|
+
});
|
|
81
|
+
await authz.permissionSets.assign({
|
|
82
|
+
id: 'test-assignment',
|
|
83
|
+
permissionSet: 'test-access',
|
|
84
|
+
subject: { type: 'user', id: session.user.id },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
await options.prepare?.(server, session);
|
|
88
|
+
const jar = new CookieJar();
|
|
89
|
+
const root = `http://localhost${server.publicBasePath.replace(/\/$/u, '')}`;
|
|
90
|
+
for (const cookie of session.cookie.split('; ')) {
|
|
91
|
+
await jar.setCookie(`${cookie}; Path=/`, root);
|
|
92
|
+
}
|
|
93
|
+
const fetch: typeof globalThis.fetch = async (input, init) => {
|
|
94
|
+
const request = new Request(input, init);
|
|
95
|
+
request.headers.set('cookie', await jar.getCookieString(request.url));
|
|
96
|
+
request.headers.set('origin', 'http://localhost');
|
|
97
|
+
await options.beforeRequest?.(request);
|
|
98
|
+
const response = await server.fetch(request);
|
|
99
|
+
for (const cookie of response.headers.getSetCookie()) {
|
|
100
|
+
await jar.setCookie(cookie, request.url);
|
|
101
|
+
}
|
|
102
|
+
await options.afterResponse?.(request, response);
|
|
103
|
+
return response;
|
|
104
|
+
};
|
|
105
|
+
// Better Auth uses fetch directly; route it to the real in-process server with browser cookie handling.
|
|
106
|
+
vi.stubGlobal('fetch', fetch);
|
|
107
|
+
onTestFinished(() => {
|
|
108
|
+
vi.unstubAllGlobals();
|
|
109
|
+
});
|
|
110
|
+
let identity: ReturnType<typeof useAuthentication> | undefined;
|
|
111
|
+
function SessionObserver() {
|
|
112
|
+
identity = useAuthentication();
|
|
113
|
+
return ui;
|
|
114
|
+
}
|
|
115
|
+
const view = await renderWithApp(<SessionObserver />, {
|
|
116
|
+
plugins: [authentication(), authorization()],
|
|
117
|
+
namespace: packageMetadata.name,
|
|
118
|
+
namespaces: {
|
|
119
|
+
...options.namespaces,
|
|
120
|
+
[packageMetadata.name]: {
|
|
121
|
+
'en-US': async () => ({ default: enUS }),
|
|
122
|
+
'zh-CN': async () => ({ default: zhCN }),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
...(options.route ? { route: options.route } : {}),
|
|
126
|
+
...(options.locale ? { locale: options.locale } : {}),
|
|
127
|
+
server: { publicBasePath: server.publicBasePath, fetch },
|
|
128
|
+
});
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
if (identity?.isPending || identity?.session?.user.id !== session.user.id) {
|
|
131
|
+
throw new Error('The signed-in session has not reached the client.');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
...view,
|
|
136
|
+
server,
|
|
137
|
+
session,
|
|
138
|
+
identity: () => identity!,
|
|
139
|
+
async signInAs(credentials: SignInCredentials) {
|
|
140
|
+
const next = await signIn(server, credentials);
|
|
141
|
+
await jar.removeAllCookies();
|
|
142
|
+
for (const cookie of next.cookie.split('; '))
|
|
143
|
+
await jar.setCookie(`${cookie}; Path=/`, root);
|
|
144
|
+
await act(() => identity!.refresh());
|
|
145
|
+
return next;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
|
|
3
1
|
// Users develop, build and start their app through these scripts. Nothing at runtime depends on them, so dropping or
|
|
4
2
|
// renaming one — a bad merge resolution did exactly that once — breaks the documented development steps silently: the app still
|
|
5
3
|
// builds, starts, and passes every other test. These assertions are the alarm.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { builtinEnvironments, type Environment } from 'vitest/runtime';
|
|
2
|
+
|
|
3
|
+
// Render in jsdom while loading the in-process application's Node workers as server modules.
|
|
4
|
+
export default {
|
|
5
|
+
...builtinEnvironments.jsdom,
|
|
6
|
+
name: 'application',
|
|
7
|
+
viteEnvironment: 'ssr',
|
|
8
|
+
} satisfies Environment;
|
|
@@ -8,8 +8,6 @@ const RUNTIME_CONFIG_ELEMENT_ID = 'nocobase-runtime-config';
|
|
|
8
8
|
* at `/main`. A test that needs other values removes it and renders its own.
|
|
9
9
|
*/
|
|
10
10
|
function renderClientConfig(): void {
|
|
11
|
-
// Server-side tests run under `@vitest-environment node`, where there is no page to render into.
|
|
12
|
-
if (typeof document === 'undefined') return;
|
|
13
11
|
if (document.getElementById(RUNTIME_CONFIG_ELEMENT_ID)) return;
|
|
14
12
|
const element = document.createElement('script');
|
|
15
13
|
element.id = RUNTIME_CONFIG_ELEMENT_ID;
|
package/vitest.config.ts
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
|
|
3
|
+
import { createNodeVitestConfig } from '@nocobase/dev-config/vitest/node';
|
|
3
4
|
import { createReactVitestConfig } from '@nocobase/dev-config/vitest/react';
|
|
5
|
+
import { defineConfig } from 'vitest/config';
|
|
4
6
|
|
|
5
7
|
const root = fileURLToPath(new URL('.', import.meta.url));
|
|
6
8
|
|
|
7
|
-
export default
|
|
9
|
+
export default defineConfig({
|
|
8
10
|
test: {
|
|
9
11
|
root,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
projects: [
|
|
13
|
+
createNodeVitestConfig({
|
|
14
|
+
test: {
|
|
15
|
+
root,
|
|
16
|
+
name: 'node',
|
|
17
|
+
include: ['tests/{server,database,cli,project}/**/*.test.{ts,tsx}'],
|
|
18
|
+
// Database fixtures and command output capture run one file at a time.
|
|
19
|
+
fileParallelism: false,
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
createReactVitestConfig({
|
|
23
|
+
test: {
|
|
24
|
+
root,
|
|
25
|
+
name: 'client',
|
|
26
|
+
environment: './tests/setup/application-environment.ts',
|
|
27
|
+
// Client integration tests also start isolated application databases.
|
|
28
|
+
fileParallelism: false,
|
|
29
|
+
include: ['tests/client/**/*.test.{ts,tsx}'],
|
|
30
|
+
setupFiles: ['./tests/setup/client-config.ts'],
|
|
31
|
+
server: {
|
|
32
|
+
deps: {
|
|
33
|
+
// Keep the fixture, application and plugin clients in the same React/router context.
|
|
34
|
+
// Server and shared modules stay external to preserve their service identities.
|
|
35
|
+
inline: [
|
|
36
|
+
/@nocobase\/app-testing\/(?:dist\/)?src\/client\//u,
|
|
37
|
+
/@nocobase\/app-client\//u,
|
|
38
|
+
/@[^/]+\/app-plugin-[^/]+\/(?:dist\/)?client\//u,
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
// Only the directories above contain Vitest suites. Playwright has its own runner.
|
|
46
|
+
// Keep the command usable if an application intentionally removes all scaffold tests.
|
|
32
47
|
passWithNoTests: true,
|
|
33
48
|
coverage: {
|
|
34
49
|
provider: 'v8',
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
-
import { I18nRuntime } from '@nocobase/i18n';
|
|
3
|
-
import { I18nProvider } from '@nocobase/i18n/client';
|
|
4
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
5
|
-
import type { ReactElement } from 'react';
|
|
6
|
-
import { MemoryRouter } from 'react-router';
|
|
7
|
-
import { beforeEach, expect, it, vi } from 'vitest';
|
|
8
|
-
|
|
9
|
-
import locales from '../../client/locales/index.js';
|
|
10
|
-
import { TooltipProvider } from '../../client/components/ui/tooltip';
|
|
11
|
-
|
|
12
|
-
const mocks = vi.hoisted(() => ({
|
|
13
|
-
session: { user: { id: 'user-1' } } as { user: { id: string } } | null,
|
|
14
|
-
unread: 2,
|
|
15
|
-
request: vi.fn(),
|
|
16
|
-
}));
|
|
17
|
-
vi.mock('@nocobase/app-plugin-authentication/client', () => ({
|
|
18
|
-
useAuthentication: () => ({ session: mocks.session, isPending: false }),
|
|
19
|
-
}));
|
|
20
|
-
vi.mock('@nocobase/app-client', async (importOriginal) => {
|
|
21
|
-
const actual = await importOriginal<typeof import('@nocobase/app-client')>();
|
|
22
|
-
const client = { request: mocks.request };
|
|
23
|
-
const realtime = {
|
|
24
|
-
watch: () => ({ close: vi.fn() }),
|
|
25
|
-
subscribe: () => vi.fn(),
|
|
26
|
-
onOpen: () => vi.fn(),
|
|
27
|
-
};
|
|
28
|
-
return {
|
|
29
|
-
...actual,
|
|
30
|
-
useApiClient: () => client,
|
|
31
|
-
useService: (token: unknown) =>
|
|
32
|
-
token === actual.realtimeClientToken ? realtime : client,
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
import InboxPage from '../../client/pages/inbox.tsx';
|
|
37
|
-
|
|
38
|
-
const messages = [
|
|
39
|
-
{
|
|
40
|
-
id: 'm1',
|
|
41
|
-
deliveryId: 'd1',
|
|
42
|
-
notificationId: 'n1',
|
|
43
|
-
title: 'Weekly report is ready',
|
|
44
|
-
body: 'The sales report for week 40 is ready.',
|
|
45
|
-
createdAt: '2026-10-01T08:00:00.000Z',
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: 'm2',
|
|
49
|
-
deliveryId: 'd2',
|
|
50
|
-
notificationId: 'n2',
|
|
51
|
-
title: 'Backup finished',
|
|
52
|
-
body: 'All databases were backed up.',
|
|
53
|
-
readAt: '2026-10-01T09:00:00.000Z',
|
|
54
|
-
createdAt: '2026-10-01T07:00:00.000Z',
|
|
55
|
-
},
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
let runtime: I18nRuntime;
|
|
59
|
-
beforeEach(async () => {
|
|
60
|
-
mocks.session = { user: { id: 'user-1' } };
|
|
61
|
-
mocks.unread = 2;
|
|
62
|
-
mocks.request.mockReset();
|
|
63
|
-
mocks.request.mockImplementation(
|
|
64
|
-
async ({ path }: { readonly path: string }) => {
|
|
65
|
-
if (path.endsWith('/unreadCount'))
|
|
66
|
-
return { data: { count: mocks.unread } };
|
|
67
|
-
if (path.startsWith('notificationInApp/messages?'))
|
|
68
|
-
return { data: messages, meta: {} };
|
|
69
|
-
return { data: { ...messages[0], readAt: '2026-10-01T10:00:00.000Z' } };
|
|
70
|
-
},
|
|
71
|
-
);
|
|
72
|
-
runtime = new I18nRuntime({
|
|
73
|
-
defaultLocale: 'en-US',
|
|
74
|
-
locales: ['en-US', 'zh-CN'],
|
|
75
|
-
applicationNamespace: 'test-app',
|
|
76
|
-
});
|
|
77
|
-
runtime.registerApplicationNamespace('test-app', locales);
|
|
78
|
-
await runtime.init('en-US');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
function Frame({
|
|
82
|
-
children,
|
|
83
|
-
path,
|
|
84
|
-
}: {
|
|
85
|
-
readonly children: ReactElement;
|
|
86
|
-
readonly path: string;
|
|
87
|
-
}): ReactElement {
|
|
88
|
-
return (
|
|
89
|
-
<QueryClientProvider
|
|
90
|
-
client={
|
|
91
|
-
new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
92
|
-
}
|
|
93
|
-
>
|
|
94
|
-
<MemoryRouter initialEntries={[path]}>
|
|
95
|
-
<I18nProvider runtime={runtime}>
|
|
96
|
-
<TooltipProvider>{children}</TooltipProvider>
|
|
97
|
-
</I18nProvider>
|
|
98
|
-
</MemoryRouter>
|
|
99
|
-
</QueryClientProvider>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
it("lists the plugin's messages as notifications and marks one read when it is opened", async () => {
|
|
104
|
-
render(
|
|
105
|
-
<Frame path='/inbox'>
|
|
106
|
-
<InboxPage />
|
|
107
|
-
</Frame>,
|
|
108
|
-
);
|
|
109
|
-
expect(
|
|
110
|
-
await screen.findByRole('heading', { name: /Notifications/u }),
|
|
111
|
-
).toBeInTheDocument();
|
|
112
|
-
expect(screen.getByText('Backup finished')).toBeInTheDocument();
|
|
113
|
-
// The first message is shown, and opening it marks it read.
|
|
114
|
-
expect(
|
|
115
|
-
screen.getByRole('heading', { name: 'Weekly report is ready', level: 2 }),
|
|
116
|
-
).toBeInTheDocument();
|
|
117
|
-
fireEvent.click(screen.getAllByText('Weekly report is ready')[0]!);
|
|
118
|
-
await waitFor(() =>
|
|
119
|
-
expect(mocks.request).toHaveBeenCalledWith({
|
|
120
|
-
path: 'notificationInApp/messages/m1/markRead',
|
|
121
|
-
method: 'POST',
|
|
122
|
-
}),
|
|
123
|
-
);
|
|
124
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createAppSidebarPreferenceStore,
|
|
3
|
-
SidebarPreferenceProvider,
|
|
4
|
-
} from '../../client/layouts/use-sidebar-preference.js';
|
|
5
|
-
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
6
|
-
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
7
|
-
import { MemoryRouter } from 'react-router';
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
-
import {
|
|
10
|
-
useRouteNavigation,
|
|
11
|
-
routeKey,
|
|
12
|
-
} from '../../client/routing/route-navigation';
|
|
13
|
-
import { NavigationMenu } from '../../client/layouts/components/navigation-menu';
|
|
14
|
-
import { AppSidebarProvider } from '../../client/layouts/components/app-sidebar';
|
|
15
|
-
|
|
16
|
-
const sidebarPreference = createAppSidebarPreferenceStore('/main');
|
|
17
|
-
|
|
18
|
-
const auth = vi.hoisted(() => ({ revision: 0, client: { can: vi.fn() } }));
|
|
19
|
-
vi.mock('@nocobase/app-plugin-authorization/client', () => ({
|
|
20
|
-
useAuthorizationClient: () => auth.client,
|
|
21
|
-
useAuthorizationRevision: () => auth.revision,
|
|
22
|
-
}));
|
|
23
|
-
vi.mock('@nocobase/i18n/client', () => ({
|
|
24
|
-
useTranslation: () => ({ t: (key: string) => key }),
|
|
25
|
-
}));
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
auth.revision = 0;
|
|
28
|
-
localStorage.clear();
|
|
29
|
-
vi.stubGlobal('matchMedia', () => ({
|
|
30
|
-
matches: false,
|
|
31
|
-
addEventListener: vi.fn(),
|
|
32
|
-
removeEventListener: vi.fn(),
|
|
33
|
-
}));
|
|
34
|
-
});
|
|
35
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
36
|
-
function page(name: string): AppClientRegisteredRoute {
|
|
37
|
-
return {
|
|
38
|
-
id: name,
|
|
39
|
-
name,
|
|
40
|
-
path: `/${name}`,
|
|
41
|
-
packageName: 'test',
|
|
42
|
-
source: 'application',
|
|
43
|
-
auth: 'required',
|
|
44
|
-
authz: { resource: { type: 'page', id: name }, action: 'access' },
|
|
45
|
-
navigation: { title: name },
|
|
46
|
-
componentLoader: async () => ({ default: () => null }),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
const visible = page('Allowed');
|
|
50
|
-
const hidden = page('Denied');
|
|
51
|
-
const routes: AppClientRegisteredRoute[] = [
|
|
52
|
-
{ ...page('Group'), componentLoader: undefined, children: [visible, hidden] },
|
|
53
|
-
{ ...page('Empty'), componentLoader: undefined, children: [hidden] },
|
|
54
|
-
];
|
|
55
|
-
function Menu() {
|
|
56
|
-
const { items } = useRouteNavigation(routes);
|
|
57
|
-
return (
|
|
58
|
-
<NavigationMenu
|
|
59
|
-
items={items}
|
|
60
|
-
label='Navigation'
|
|
61
|
-
selectedKey={routeKey(visible)}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
describe.each(['expanded', 'collapsed', 'mobile'])(
|
|
66
|
-
'permissions in %s navigation',
|
|
67
|
-
(mode) => {
|
|
68
|
-
it('hides pending and denied entries, removes empty groups, and refreshes open menus', async () => {
|
|
69
|
-
if (mode === 'mobile') vi.stubGlobal('innerWidth', 390);
|
|
70
|
-
if (mode === 'collapsed')
|
|
71
|
-
localStorage.setItem('nocobase:sidebar:collapsed:app:/main', 'true');
|
|
72
|
-
const resolve: ((allowed: boolean) => void)[] = [];
|
|
73
|
-
auth.client.can.mockImplementation(
|
|
74
|
-
() => new Promise<boolean>((done) => resolve.push(done)),
|
|
75
|
-
);
|
|
76
|
-
const tree = () => (
|
|
77
|
-
<MemoryRouter>
|
|
78
|
-
<SidebarPreferenceProvider value={sidebarPreference}>
|
|
79
|
-
<AppSidebarProvider>
|
|
80
|
-
<Menu />
|
|
81
|
-
</AppSidebarProvider>
|
|
82
|
-
</SidebarPreferenceProvider>
|
|
83
|
-
</MemoryRouter>
|
|
84
|
-
);
|
|
85
|
-
const { rerender } = render(tree());
|
|
86
|
-
expect(screen.queryByText('Group')).not.toBeInTheDocument();
|
|
87
|
-
await act(async () => {
|
|
88
|
-
resolve.forEach((done, i) => done(i === 0));
|
|
89
|
-
});
|
|
90
|
-
// Group is a section: its entries show in every mode, as icons in the collapsed sidebar.
|
|
91
|
-
expect(
|
|
92
|
-
await screen.findByRole('link', { name: 'Allowed' }),
|
|
93
|
-
).toHaveAttribute('href', '/Allowed');
|
|
94
|
-
expect(screen.queryByText('Denied')).not.toBeInTheDocument();
|
|
95
|
-
expect(screen.queryByText('Empty')).not.toBeInTheDocument();
|
|
96
|
-
auth.client.can.mockResolvedValue(false);
|
|
97
|
-
auth.revision++;
|
|
98
|
-
rerender(tree());
|
|
99
|
-
await waitFor(() =>
|
|
100
|
-
expect(screen.queryByText('Group')).not.toBeInTheDocument(),
|
|
101
|
-
);
|
|
102
|
-
expect(
|
|
103
|
-
screen.queryByRole('link', { name: 'Allowed' }),
|
|
104
|
-
).not.toBeInTheDocument();
|
|
105
|
-
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
106
|
-
});
|
|
107
|
-
},
|
|
108
|
-
);
|