@nocobase/app-template-default 1.0.0-beta.78 → 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.
Files changed (88) hide show
  1. package/AGENTS.md +4 -4
  2. package/README.MD +4 -2
  3. package/config.example.yml +28 -0
  4. package/package.json +11 -10
  5. package/server/config/index.ts +3 -0
  6. package/server/config/knowledge.ts +3 -0
  7. package/tests/client/account-permissions.test.tsx +174 -0
  8. package/tests/{components → client}/app-layout-routes.test.tsx +25 -42
  9. package/tests/{logic → client}/app-locale-coverage.test.ts +1 -4
  10. package/tests/{logic → client}/client-plugin-registry.test.ts +1 -4
  11. package/tests/{logic → client}/client-route-recovery.test.tsx +27 -7
  12. package/tests/{components → client}/header-hover.test.tsx +4 -14
  13. package/tests/{components → client}/home.test.tsx +27 -41
  14. package/tests/client/inbox.test.tsx +94 -0
  15. package/tests/{components → client}/permission-management-tabs.test.tsx +23 -44
  16. package/tests/client/sidebar-permissions.test.tsx +157 -0
  17. package/tests/{logic → client}/theme-tokens.test.ts +6 -8
  18. package/tests/client/user-menu-sign-out.test.tsx +78 -0
  19. package/tests/{logic → database}/device-code-migration.test.ts +0 -1
  20. package/tests/helpers/authenticated-client.tsx +148 -0
  21. package/tests/{logic → project}/agents-md.test.ts +0 -2
  22. package/tests/{logic → project}/asset-url-build.test.ts +0 -1
  23. package/tests/project/config-example.test.ts +67 -0
  24. package/tests/{logic → project}/dev-client-watch.test.ts +0 -2
  25. package/tests/{logic → project}/plugin-commands.test.ts +0 -2
  26. package/tests/{logic → project}/server-imports.test.ts +0 -1
  27. package/tests/{logic → project}/tailwind-sources.test.ts +0 -1
  28. package/tests/{logic → project}/template-package-name.test.ts +0 -2
  29. package/tests/{logic → server}/app-server.test.ts +0 -1
  30. package/tests/{logic → server}/authorization-assignment-safety.test.ts +0 -2
  31. package/tests/{logic → server}/config.test.ts +0 -2
  32. package/tests/{logic → server}/session-isolation.test.ts +0 -2
  33. package/tests/setup/application-environment.ts +8 -0
  34. package/tests/setup/client-config.ts +0 -2
  35. package/vitest.config.ts +38 -23
  36. package/tests/components/inbox.test.tsx +0 -124
  37. package/tests/components/sidebar-permissions.test.tsx +0 -108
  38. package/tests/logic/account-permissions.test.tsx +0 -204
  39. package/tests/logic/config-example.test.ts +0 -30
  40. package/tests/logic/user-menu-sign-out.test.tsx +0 -107
  41. /package/tests/{components → client}/app-title.test.tsx +0 -0
  42. /package/tests/{components → client}/auth-i18n.test.tsx +0 -0
  43. /package/tests/{components → client}/back-button.test.tsx +0 -0
  44. /package/tests/{logic → client}/client-auth.test.tsx +0 -0
  45. /package/tests/{logic → client}/client-routes.test.ts +0 -0
  46. /package/tests/{logic → client}/client-runtime.test.ts +0 -0
  47. /package/tests/{logic → client}/client-shell.test.tsx +0 -0
  48. /package/tests/{logic → client}/client-theme.test.tsx +0 -0
  49. /package/tests/{components → client}/data-table.test.tsx +0 -0
  50. /package/tests/{components → client}/default-access-editor.test.tsx +0 -0
  51. /package/tests/{logic → client}/default-access-model.test.ts +0 -0
  52. /package/tests/{components → client}/default-access.test.tsx +0 -0
  53. /package/tests/{components → client}/file-preview.test.tsx +0 -0
  54. /package/tests/{logic → client}/foundation-integration.test.ts +0 -0
  55. /package/tests/{components → client}/language-switcher.test.tsx +0 -0
  56. /package/tests/{components → client}/layout-containers.test.tsx +0 -0
  57. /package/tests/{components → client}/layout-i18n.test.tsx +0 -0
  58. /package/tests/{components → client}/loading.test.tsx +0 -0
  59. /package/tests/{components → client}/navigation-menu.test.tsx +0 -0
  60. /package/tests/{components → client}/page-harness.test.tsx +0 -0
  61. /package/tests/{logic → client}/page-permission-navigation.test.ts +0 -0
  62. /package/tests/{logic → client}/page-permission-titles.test.ts +0 -0
  63. /package/tests/{components → client}/permission-editor.test.tsx +0 -0
  64. /package/tests/{components → client}/permission-filter-binding.test.tsx +0 -0
  65. /package/tests/{components → client}/permission-filter.test.tsx +0 -0
  66. /package/tests/{logic → client}/permission-management-routes.test.ts +0 -0
  67. /package/tests/{logic → client}/permission-workspace.test.ts +0 -0
  68. /package/tests/{components → client}/primitive-labels.test.tsx +0 -0
  69. /package/tests/{logic → client}/return-to.test.ts +0 -0
  70. /package/tests/{components → client}/role-list.test.tsx +0 -0
  71. /package/tests/{components → client}/roles.test.tsx +0 -0
  72. /package/tests/{logic → client}/route-navigation.test.ts +0 -0
  73. /package/tests/{logic → client}/route-overlay.test.tsx +0 -0
  74. /package/tests/{logic → client}/scrollbar.test.ts +0 -0
  75. /package/tests/{logic → client}/session-preparation.test.tsx +0 -0
  76. /package/tests/{components → client}/settings-continuity.test.tsx +0 -0
  77. /package/tests/{components → client}/settings-feedback.test.tsx +0 -0
  78. /package/tests/{logic → client}/settings-permissions.test.ts +0 -0
  79. /package/tests/{components → client}/settings.test.tsx +0 -0
  80. /package/tests/{components → client}/setup-readiness.test.tsx +0 -0
  81. /package/tests/{components → client}/sidebar-preference.test.tsx +0 -0
  82. /package/tests/{components → client}/status-badge.test.tsx +0 -0
  83. /package/tests/{components → client}/table-url-state.test.tsx +0 -0
  84. /package/tests/{logic → client}/theme-preferences.test.ts +0 -0
  85. /package/tests/{logic → client}/toaster.test.tsx +0 -0
  86. /package/tests/{components → client}/user-drawer.test.tsx +0 -0
  87. /package/tests/{components → client}/user-permissions.test.tsx +0 -0
  88. /package/tests/{logic → server}/lifecycle.test.ts +0 -0
@@ -1,204 +0,0 @@
1
- import {
2
- ClientApplicationContext,
3
- type ClientApplication,
4
- } from '@nocobase/app-client';
5
- import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
6
- import {
7
- AuthorizationClient,
8
- authorizationClientToken,
9
- } from '@nocobase/app-plugin-authorization/client';
10
- import { reactProviders } from '@nocobase/app-plugin-authorization/client/react-providers';
11
- import { ServiceContainer } from '@nocobase/service-provider';
12
- import {
13
- TestI18nProvider,
14
- createTestI18nRuntime,
15
- } from '@nocobase/i18n/testing';
16
- import { act, render, screen, waitFor } from '@testing-library/react';
17
- import type { ReactNode } from 'react';
18
- import { MemoryRouter } from 'react-router';
19
- import { beforeEach, describe, expect, it, vi } from 'vitest';
20
- import { ClientRoute } from '../../client/routing/client-route.js';
21
- import { useRouteNavigation } from '../../client/routing/route-navigation.js';
22
- import enUS from '../../client/locales/en-US.js';
23
-
24
- const AuthorizationProvider = reactProviders[0].component;
25
-
26
- const runtime = await createTestI18nRuntime({
27
- application: { namespace: '@nocobase/app-template-default', resources: enUS },
28
- });
29
-
30
- function I18n({ children }: { readonly children: ReactNode }) {
31
- return <TestI18nProvider runtime={runtime}>{children}</TestI18nProvider>;
32
- }
33
-
34
- const authentication = vi.hoisted(() => ({
35
- session: null as null | { user: { id: string }; session: { id: string } },
36
- isPending: false,
37
- }));
38
- vi.mock('@nocobase/app-plugin-authentication/client', () => ({
39
- useAuthentication: () => authentication,
40
- }));
41
- const routes: AppClientRegisteredRoute[] = ['apps', 'users'].map((name) => ({
42
- id: name,
43
- name,
44
- path: `/${name}`,
45
- auth: 'required',
46
- authz: { resource: { type: 'page', id: name }, action: 'access' },
47
- packageName: 'test',
48
- source: 'application',
49
- navigation: { title: name },
50
- componentLoader: async () => ({ default: () => <div>{name} content</div> }),
51
- }));
52
- const permissions = (admin: boolean) => ({
53
- data: {
54
- permissions: (admin ? ['apps', 'users'] : ['apps']).map((id) => ({
55
- resource: { type: 'page', id },
56
- actions: ['access'],
57
- })),
58
- },
59
- });
60
- function Menu() {
61
- const { items, loading } = useRouteNavigation(routes);
62
- return (
63
- <nav aria-label='Pages'>
64
- {loading
65
- ? 'Checking'
66
- : items.map(({ route }) => <span key={route.id}>{route.name}</span>)}
67
- </nav>
68
- );
69
- }
70
- function session(user: string, id = user) {
71
- authentication.session = { user: { id: user }, session: { id } };
72
- }
73
- function setup(
74
- request = vi
75
- .fn()
76
- .mockImplementation(() =>
77
- Promise.resolve(permissions(authentication.session?.user.id === 'admin')),
78
- ),
79
- ) {
80
- const client = new AuthorizationClient({ request } as never);
81
- const container = new ServiceContainer();
82
- container.instance(authorizationClientToken, client);
83
- const app = { services: container } as unknown as ClientApplication;
84
- const tree = () => (
85
- <ClientApplicationContext.Provider value={app}>
86
- <MemoryRouter>
87
- <AuthorizationProvider>
88
- <Menu />
89
- <ClientRoute route={routes[1]} />
90
- </AuthorizationProvider>
91
- </MemoryRouter>
92
- </ClientApplicationContext.Provider>
93
- );
94
- const view = render(tree(), { wrapper: I18n });
95
- return { client, request, rerender: () => view.rerender(tree()) };
96
- }
97
- async function expectMenu(admin: boolean) {
98
- await waitFor(() =>
99
- expect(screen.getByRole('navigation', { name: 'Pages' }).textContent).toBe(
100
- admin ? 'appsusers' : 'apps',
101
- ),
102
- );
103
- if (admin)
104
- await waitFor(() =>
105
- expect(screen.getByText('users content')).toBeInTheDocument(),
106
- );
107
- else {
108
- await waitFor(() =>
109
- expect(screen.getByText('Access denied')).toBeInTheDocument(),
110
- );
111
- expect(screen.queryByText('users content')).not.toBeInTheDocument();
112
- }
113
- }
114
-
115
- describe('account permission changes without a browser reload', () => {
116
- beforeEach(() => {
117
- authentication.session = null;
118
- authentication.isPending = false;
119
- });
120
-
121
- it('updates menus and cached route guards from admin to operator and back', async () => {
122
- session('admin');
123
- const view = setup();
124
- await expectMenu(true);
125
- session('operator');
126
- view.rerender();
127
- expect(screen.queryByText('users content')).not.toBeInTheDocument();
128
- await expectMenu(false);
129
- session('admin');
130
- view.rerender();
131
- await expectMenu(true);
132
- });
133
-
134
- it('clears permissions on logout and same-user re-login with a new session', async () => {
135
- session('admin', 'first');
136
- const view = setup();
137
- await expectMenu(true);
138
- authentication.isPending = true;
139
- view.rerender();
140
- expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
141
- authentication.session = null;
142
- authentication.isPending = false;
143
- view.rerender();
144
- await expectMenu(false);
145
- view.request.mockResolvedValue(permissions(false));
146
- session('admin', 'second');
147
- view.rerender();
148
- await expectMenu(false);
149
- expect(view.request).toHaveBeenCalledTimes(3);
150
- });
151
-
152
- it('updates a mounted menu and page when permissions change', async () => {
153
- session('admin');
154
- const view = setup();
155
- await expectMenu(true);
156
- view.request.mockResolvedValue(permissions(false));
157
- act(() => view.client.invalidate());
158
- expect(screen.queryByText('users content')).not.toBeInTheDocument();
159
- await expectMenu(false);
160
- view.request.mockResolvedValue(permissions(true));
161
- act(() => view.client.invalidate());
162
- await expectMenu(true);
163
- });
164
-
165
- it('hides previously allowed pages when revalidation fails and recovers on retry', async () => {
166
- session('admin');
167
- const view = setup();
168
- await expectMenu(true);
169
- view.request.mockRejectedValue(new Error('Offline'));
170
- act(() => view.client.invalidate());
171
- await waitFor(() =>
172
- expect(
173
- screen.getByRole('navigation', { name: 'Pages' }).textContent,
174
- ).toBe(''),
175
- );
176
- await waitFor(() =>
177
- expect(screen.getByText('Access denied')).toBeInTheDocument(),
178
- );
179
- expect(screen.queryByText('users content')).not.toBeInTheDocument();
180
- view.request.mockResolvedValue(permissions(true));
181
- act(() => view.client.invalidate());
182
- await expectMenu(true);
183
- });
184
-
185
- it('does not restore admin menus when an old request returns after switching accounts', async () => {
186
- const old = Promise.withResolvers<ReturnType<typeof permissions>>();
187
- const request = vi
188
- .fn()
189
- .mockReturnValueOnce(old.promise)
190
- .mockResolvedValue(permissions(false));
191
- session('admin');
192
- const view = setup(request);
193
- await waitFor(() => expect(request).toHaveBeenCalledTimes(1));
194
- session('operator');
195
- view.rerender();
196
- await expectMenu(false);
197
- await act(async () => {
198
- old.resolve(permissions(true));
199
- await old.promise;
200
- });
201
- await expectMenu(false);
202
- expect(request).toHaveBeenCalledTimes(2);
203
- });
204
- });
@@ -1,30 +0,0 @@
1
- // @vitest-environment node
2
-
3
- import { readFile } from 'node:fs/promises';
4
-
5
- import { describe, expect, it } from 'vitest';
6
-
7
- // `pnpm nocobase config init` writes config.yml from this file: it replaces the instruction on the first line with
8
- // its own header and fills the placeholder key of the live `secrets` section, keeping everything else verbatim. These
9
- // are the parts of the example it relies on.
10
- describe('config.example.yml', () => {
11
- const read = () =>
12
- readFile(new URL('../../config.example.yml', import.meta.url), 'utf8');
13
-
14
- it('opens with the instruction config init replaces', async () => {
15
- expect((await read()).split('\n')[0]).toMatch(
16
- /^# Copy this file to config\.yml\b/u,
17
- );
18
- });
19
-
20
- it('carries a live secrets section whose first key is a placeholder', async () => {
21
- expect(await read()).toContain(
22
- [
23
- 'secrets:',
24
- ' keys:',
25
- ' - version: 1',
26
- ' key: replace-with-a-unique-secret',
27
- ].join('\n'),
28
- );
29
- });
30
- });
@@ -1,107 +0,0 @@
1
- import {
2
- act,
3
- fireEvent,
4
- render,
5
- screen,
6
- waitFor,
7
- } from '@testing-library/react';
8
- import userEvent from '@testing-library/user-event';
9
- import { beforeEach, describe, expect, it, vi } from 'vitest';
10
- import {
11
- TestI18nProvider,
12
- createTestI18nRuntime,
13
- } from '@nocobase/i18n/testing';
14
- import type { ReactNode } from 'react';
15
- import { UserMenu } from '../../client/layouts/components/user-menu.tsx';
16
- import enUS from '../../client/locales/en-US.js';
17
-
18
- const runtime = await createTestI18nRuntime({
19
- application: { namespace: '@nocobase/app-template-default', resources: enUS },
20
- });
21
-
22
- function I18n({ children }: { readonly children: ReactNode }) {
23
- return <TestI18nProvider runtime={runtime}>{children}</TestI18nProvider>;
24
- }
25
-
26
- const { signOut, refresh, showToast, resolveError } = vi.hoisted(() => ({
27
- resolveError: vi.fn(),
28
- signOut: vi.fn(),
29
- refresh: vi.fn(),
30
- showToast: vi.fn(),
31
- }));
32
- vi.mock('@nocobase/app-plugin-authentication/client/actions', () => ({
33
- useAuthenticationErrorResolver: () => resolveError,
34
- }));
35
- vi.mock('@nocobase/app-client', () => {
36
- const toaster = { show: showToast, close: vi.fn() };
37
- return { useToaster: () => toaster };
38
- });
39
- vi.mock('@nocobase/app-plugin-authentication/client', () => ({
40
- useAuthentication: () => ({
41
- client: { signOut },
42
- session: {
43
- user: { id: 'operator', name: 'Operator', email: 'operator@example.com' },
44
- },
45
- isPending: false,
46
- refresh,
47
- }),
48
- }));
49
- vi.mock('../../client/layouts/components/language-switcher.js', () => ({
50
- LanguageSwitcher: () => null,
51
- }));
52
-
53
- /**
54
- * Opens the account menu from the keyboard. A pointer opens it on hover and again on the press, one frame later, which
55
- * can reopen the menu after a test has already chosen an item and closed it.
56
- */
57
- async function openAccountMenu(): Promise<void> {
58
- act(() => screen.getByRole('button', { name: 'Open account menu' }).focus());
59
- await userEvent.keyboard('{Enter}');
60
- }
61
-
62
- describe('account menu sign out', () => {
63
- beforeEach(() => {
64
- vi.resetAllMocks();
65
- refresh.mockResolvedValue(undefined);
66
- resolveError.mockReturnValue({ message: 'Localized authentication error' });
67
- });
68
- async function signOutFromMenu() {
69
- render(<UserMenu />, { wrapper: I18n });
70
- await openAccountMenu();
71
- fireEvent.click(await screen.findByRole('menuitem', { name: 'Sign out' }));
72
- }
73
- it('refreshes the server session after successful sign-out', async () => {
74
- signOut.mockResolvedValue({ data: { success: true }, error: null });
75
- await signOutFromMenu();
76
- await waitFor(() => expect(refresh).toHaveBeenCalledTimes(1));
77
- expect(showToast).not.toHaveBeenCalled();
78
- });
79
- it('reports a rejected origin without pretending the session ended', async () => {
80
- signOut.mockResolvedValue({
81
- data: null,
82
- error: { status: 403, code: 'INVALID_ORIGIN' },
83
- });
84
- await signOutFromMenu();
85
- await waitFor(() =>
86
- expect(showToast).toHaveBeenCalledWith({
87
- type: 'error',
88
- title: 'Localized authentication error',
89
- }),
90
- );
91
- expect(resolveError).toHaveBeenCalledWith({
92
- status: 403,
93
- code: 'INVALID_ORIGIN',
94
- });
95
- expect(refresh).not.toHaveBeenCalled();
96
- await openAccountMenu();
97
- expect(
98
- await screen.findByRole('menuitem', { name: 'Sign out' }),
99
- ).not.toHaveAttribute('aria-disabled', 'true');
100
- });
101
- it('reports network failures and allows retry', async () => {
102
- signOut.mockRejectedValue(new Error('Network unavailable'));
103
- await signOutFromMenu();
104
- await waitFor(() => expect(showToast).toHaveBeenCalledTimes(1));
105
- expect(refresh).not.toHaveBeenCalled();
106
- });
107
- });
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes