@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
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ This file stays at most 32 KB and must not grow without bound, because every age
|
|
|
16
16
|
|
|
17
17
|
- Application-specific guidance — including what a change to this application's structure, shell or conventions needs the next agent to know — goes in `.agents/rules/<topic>.md`. Update the relevant file, or add one, and add or update its entry in the [index](#index) below rather than writing the detail here.
|
|
18
18
|
- Framework-wide guidance lives in the synchronized Skills under `.agents/skills/`. Link to it, never copy it here; when it is wrong for every application, correct it upstream in `@nocobase/app-skills`.
|
|
19
|
-
- `tests/
|
|
19
|
+
- `tests/project/agents-md.test.ts` enforces the size limit and checks that the links here and in `.agents/rules/` resolve.
|
|
20
20
|
|
|
21
21
|
## Load the development skills
|
|
22
22
|
|
|
@@ -61,7 +61,7 @@ A bare create-app shows Home to signed-in users and System settings → App foun
|
|
|
61
61
|
|
|
62
62
|
Declare every page in `client/routes.ts` with `defineAppRoutes()` and keep its component behind a lazy `componentLoader` whose module default-exports it. Never write the deployment base path such as `/main` into a route — the runtime restores it. `auth` controls browser navigation only and is not server security. Declare `navigation` on the route to put it in a menu, and always declare `authz` on the first page of every path; the page id identifies stored page grants, so changing it requires migrating the grants that reference it. Read [pages and routes](.agents/skills/nocobase-app-development/references/frontend/references/page.md), and [overlays](.agents/skills/nocobase-app-development/references/frontend/references/overlay.md) before implementing dialogs, drawers or close guards.
|
|
63
63
|
|
|
64
|
-
Every route listed as a page permission needs a localized display name: use its existing navigation/breadcrumb title, or add its application route name to `client/pages/settings/page-permission-titles.ts` for an overlay or other page with no title metadata. Do not add breadcrumbs/menus just to label an overlay. Run `tests/
|
|
64
|
+
Every route listed as a page permission needs a localized display name: use its existing navigation/breadcrumb title, or add its application route name to `client/pages/settings/page-permission-titles.ts` for an overlay or other page with no title metadata. Do not add breadcrumbs/menus just to label an overlay. Run `tests/client/page-permission-titles.test.ts`; it requires non-empty English/Chinese labels and rejects missing or stale mappings. Never rename route/permission ids or change `authz` to fix display text.
|
|
65
65
|
|
|
66
66
|
### Components and styling
|
|
67
67
|
|
|
@@ -101,7 +101,7 @@ When reporting or opening this application's address, use the `Local:` URL print
|
|
|
101
101
|
|
|
102
102
|
Scope all verification — formatting, lint, type checking, tests, builds and runtime checks — to the affected files, projects, or packages and their affected consumers. Do not run full-application or workspace-wide checks, or an aggregate `pnpm check`, as a routine step after each edit. Documentation-only changes need formatting and link checks for the changed documents, not type checking, runtime tests, or builds. Report which checks ran, their scope, and any unverified behavior.
|
|
103
103
|
|
|
104
|
-
Add tests for what you changed — a route's authenticated, unauthenticated, and unauthorized responses; a migration's `up` and `down` against a real database; a page's actual behavior — in `tests/`, never beside the source; browser tests go in `tests/playwright/`. Start page tests from `tests/
|
|
104
|
+
Add tests for what you changed — a route's authenticated, unauthenticated, and unauthorized responses; a migration's `up` and `down` against a real database; a page's actual behavior — in `tests/`, never beside the source; browser tests go in `tests/playwright/`. Start page tests from `tests/client/page-harness.test.tsx`, which keeps application hooks real and supplies external responses through the test fixture. See [testing and verification](.agents/skills/nocobase-app-development/references/testing.md).
|
|
105
105
|
|
|
106
106
|
For any UI work, start with [the frontend workflow](.agents/skills/nocobase-app-development/references/frontend/ui-workflow.md), which decides the workflow and routes to the styling rules, the theme token contract and the rest.
|
|
107
107
|
|
|
@@ -128,4 +128,4 @@ When the built-in mechanism genuinely cannot express what is being asked, changi
|
|
|
128
128
|
|
|
129
129
|
Assign application business roles to explicitly identified users through the prepared User management role-assignment UI. Never seed a business permission set onto `authenticated:*`, broaden the inherited platform `member` role, or write startup reconciliation that grants access to everyone merely to make a feature usable. Unchecked actions in one role do not revoke grants inherited from another. If the user explicitly requests a global default role, first deliver a permission-protected administration control that shows its recipients and allows changing or removing the assignment; show inherited roles and their source in user access details. Without that UI, use explicit user assignments and explain the limitation. Keep page access independent from business operations.
|
|
130
130
|
|
|
131
|
-
Run `pnpm exec vitest run tests/
|
|
131
|
+
Run `pnpm exec vitest run tests/server/authorization-assignment-safety.test.ts` after permission initialization changes and before delivery. It boots the real application on an isolated database and inspects initialized wildcard assignments and the inherited member role. Keep this test in generated applications. An intentional global default requires explicit user authorization, a working administration surface and corresponding acceptance tests before deliberately updating the expected baseline; do not delete, skip or loosen the guard to make tests pass. Test an ordinary user with only the configured role: inspect all effective grant sources and verify both hidden buttons and rejected direct API calls.
|
package/README.MD
CHANGED
|
@@ -399,9 +399,11 @@ An environment variable a section declares takes precedence over the file, and t
|
|
|
399
399
|
|
|
400
400
|
## Testing
|
|
401
401
|
|
|
402
|
-
Tests live in `tests/`, never beside the source they cover.
|
|
402
|
+
Tests live in `tests/`, never beside the source they cover. Group tests by the source they cover: `tests/client/` for pages, components and client logic; `tests/server/` for services and routes; `tests/database/` for migrations and seeds; `tests/cli/` for application commands; and `tests/project/` for package configuration and build artifacts. Create optional directories with their first test. Vitest projects select jsdom for client tests and Node for the other groups, so no per-file environment override is needed. Both projects run files sequentially because client integration tests also provision databases; client setup files apply only to the client project. Keep shared helpers and data in `tests/helpers/` and `tests/fixtures/`, without test files. Browser tests belong in `tests/playwright/`, which Vitest skips. Playwright does not start the application: start it first (`pnpm dev`, or `pnpm build` and then the built application), then run `pnpm test:e2e`; each test reads the application URL, including its base path, from `APP_URL`. Tests that only need a real server, real authentication, or a real database run under Vitest with `createAppTest()` from `@nocobase/app-testing/server`.
|
|
403
403
|
|
|
404
|
-
Application tests use the `/server`, `/client` and `/cli` entries of `@nocobase/app-testing`. Start page tests from `tests/
|
|
404
|
+
Application tests use the `/server`, `/client` and `/cli` entries of `@nocobase/app-testing`. Start page tests from `tests/client/page-harness.test.tsx`: it uses `renderWithApp()` with real application hooks, strict translations, permission checks and a test toaster. Prefer an actual test server for application requests. Use `answerApi()` for cases that require controlled protocol responses, and replace an external service through `services` only when the real boundary cannot be exercised, without also registering its owning plugin. The template's Vitest configuration includes client plugins in custom package scopes so their contexts and service tokens remain shared. See `.agents/skills/nocobase-app-development/references/testing.md` for fixture ownership and complete server, migration and CLI examples.
|
|
405
|
+
|
|
406
|
+
For authentication, permissions and persisted page behavior, use `tests/helpers/authenticated-client.tsx`. It starts the real application on isolated databases, signs in through its API, and forwards requests with a cookie jar to the in-process server. `tests/setup/application-environment.ts` keeps the DOM in jsdom and loads server dependencies through Vite's server environment so Node workers are not rewritten as browser workers. Prefer real components, hooks and services; inject a transport failure or delay only when the scenario requires it. Verify the final UI and server state after retries and permission reloads, not just that a request was called.
|
|
405
407
|
|
|
406
408
|
### Notification verification
|
|
407
409
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/app-template-default",
|
|
3
3
|
"displayName": "Default Template",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.80",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"prettier": "@nocobase/dev-config/prettier",
|
|
7
7
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"nocobase": {
|
|
11
11
|
"templateKind": "app",
|
|
12
|
-
"defaultTemplateVersion": "1.0.0-beta.
|
|
12
|
+
"defaultTemplateVersion": "1.0.0-beta.80"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "4.0.131",
|
|
@@ -72,9 +72,10 @@
|
|
|
72
72
|
"tar": "^7.5.22",
|
|
73
73
|
"@xyflow/react": "^12.11.5",
|
|
74
74
|
"elkjs": "^0.10.2",
|
|
75
|
+
"@types/jsdom": "^30.0.0",
|
|
75
76
|
"@nocobase/app-client": "^4.0.0-beta.3",
|
|
76
77
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.2",
|
|
77
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
78
|
+
"@nocobase/app-skills": "0.1.0-beta.50",
|
|
78
79
|
"@nocobase/app-testing": "0.1.0-beta.6",
|
|
79
80
|
"@nocobase/app-websocket": "^0.1.0-beta.1",
|
|
80
81
|
"@nocobase/dev-config": "0.1.0-beta.25",
|
|
@@ -144,9 +145,9 @@
|
|
|
144
145
|
"@nocobase/app-plugin-authentication": "^2.0.0-beta.14",
|
|
145
146
|
"@nocobase/app-plugin-authorization": "^1.0.0-beta.31",
|
|
146
147
|
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.16",
|
|
147
|
-
"@nocobase/app-plugin-
|
|
148
|
-
"@nocobase/app-plugin-foundation": "^0.1.0-beta.6",
|
|
148
|
+
"@nocobase/app-plugin-foundation": "^0.1.0-beta.7",
|
|
149
149
|
"@nocobase/app-plugin-i18n": "^1.0.0-beta.18",
|
|
150
|
+
"@nocobase/app-plugin-file": "^1.0.0-beta.26",
|
|
150
151
|
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.12",
|
|
151
152
|
"@nocobase/app-plugin-mail": "^1.0.0-beta.17",
|
|
152
153
|
"@nocobase/app-plugin-notification": "^2.0.0-beta.0",
|
|
@@ -160,16 +161,16 @@
|
|
|
160
161
|
"@nocobase/db-sqlite": "^0.1.0-beta.6",
|
|
161
162
|
"@nocobase/service-provider": "^0.0.2-beta.2",
|
|
162
163
|
"@nocobase/snowflake": "^1.0.0-beta.4",
|
|
163
|
-
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.15",
|
|
164
164
|
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.15",
|
|
165
|
+
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.15",
|
|
165
166
|
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.14",
|
|
166
167
|
"@nocobase/authorization": "^1.0.0-beta.15",
|
|
167
168
|
"@nocobase/queue": "^0.1.0-beta.9",
|
|
168
169
|
"@nocobase/jobs": "^0.1.0-beta.4",
|
|
169
170
|
"@nocobase/lifecycle": "^0.1.0-beta.3",
|
|
170
171
|
"@nocobase/i18n": "^2.0.0-beta.1",
|
|
171
|
-
"@nocobase/caching": "^0.1.0-beta.3",
|
|
172
172
|
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
173
|
+
"@nocobase/caching": "^0.1.0-beta.3",
|
|
173
174
|
"@nocobase/app-cli": "^1.0.0-beta.27",
|
|
174
175
|
"@nocobase/realtime": "^0.1.0-beta.3"
|
|
175
176
|
},
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
2
|
+
import { authorizationClientToken } from '@nocobase/app-plugin-authorization/client';
|
|
3
|
+
import { authorizationToken } from '@nocobase/app-plugin-authorization/server';
|
|
4
|
+
import { DEFAULT_ADMIN_CREDENTIALS } from '@nocobase/app-plugin-authentication/testing';
|
|
5
|
+
import { act, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { ClientRoute } from '../../client/routing/client-route.js';
|
|
8
|
+
import { useRouteNavigation } from '../../client/routing/route-navigation.js';
|
|
9
|
+
import { renderAuthenticatedClient } from '../helpers/authenticated-client.js';
|
|
10
|
+
|
|
11
|
+
const member = {
|
|
12
|
+
email: 'member@example.test',
|
|
13
|
+
password: 'test-member-password',
|
|
14
|
+
};
|
|
15
|
+
const routes: AppClientRegisteredRoute[] = ['apps', 'users'].map((name) => ({
|
|
16
|
+
id: name,
|
|
17
|
+
name,
|
|
18
|
+
path: `/${name}`,
|
|
19
|
+
auth: 'required',
|
|
20
|
+
authz: { resource: { type: 'page', id: name }, action: 'access' },
|
|
21
|
+
packageName: 'test',
|
|
22
|
+
source: 'application',
|
|
23
|
+
navigation: { title: name },
|
|
24
|
+
componentLoader: async () => ({ default: () => <div>{name} content</div> }),
|
|
25
|
+
}));
|
|
26
|
+
const grants = (full: boolean) =>
|
|
27
|
+
(full ? ['apps', 'users'] : ['apps']).map((id) => ({
|
|
28
|
+
resource: { type: 'page', id },
|
|
29
|
+
actions: [{ action: 'access' }],
|
|
30
|
+
}));
|
|
31
|
+
function Menu() {
|
|
32
|
+
const { items, loading } = useRouteNavigation(routes);
|
|
33
|
+
return (
|
|
34
|
+
<nav aria-label='Pages'>
|
|
35
|
+
{loading
|
|
36
|
+
? 'Checking'
|
|
37
|
+
: items.map(({ route }) => <span key={route.id}>{route.name}</span>)}
|
|
38
|
+
</nav>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function setup(options: Parameters<typeof renderAuthenticatedClient>[1] = {}) {
|
|
42
|
+
return renderAuthenticatedClient(
|
|
43
|
+
<>
|
|
44
|
+
<Menu />
|
|
45
|
+
<ClientRoute route={routes[1]} />
|
|
46
|
+
</>,
|
|
47
|
+
{
|
|
48
|
+
member: true,
|
|
49
|
+
grants: grants(true),
|
|
50
|
+
namespaces: { test: { apps: 'apps', users: 'users' } },
|
|
51
|
+
...options,
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
async function expectMenu(full: boolean) {
|
|
56
|
+
// Realtime permission events may invalidate the snapshot again during a refresh.
|
|
57
|
+
// Read and assert both surfaces together, rather than retaining a detached node from findByText().
|
|
58
|
+
await waitFor(() => {
|
|
59
|
+
expect(screen.getByRole('navigation', { name: 'Pages' }).textContent).toBe(
|
|
60
|
+
full ? 'appsusers' : 'apps',
|
|
61
|
+
);
|
|
62
|
+
if (full) expect(screen.getByText('users content')).toBeVisible();
|
|
63
|
+
else {
|
|
64
|
+
expect(screen.getByText('Access denied')).toBeVisible();
|
|
65
|
+
expect(screen.queryByText('users content')).not.toBeInTheDocument();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function changePermissions(
|
|
70
|
+
view: Awaited<ReturnType<typeof setup>>,
|
|
71
|
+
full: boolean,
|
|
72
|
+
) {
|
|
73
|
+
await view.server.application.container
|
|
74
|
+
.resolve(authorizationToken)
|
|
75
|
+
.permissionSets.update('test-access', {
|
|
76
|
+
key: 'test-access',
|
|
77
|
+
grants: grants(full),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function invalidate(view: Awaited<ReturnType<typeof setup>>) {
|
|
81
|
+
act(() => view.app.container.resolve(authorizationClientToken).invalidate());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('account permission changes without a browser reload', () => {
|
|
85
|
+
it('updates menus and route guards when signing in as admin, member and admin again', async () => {
|
|
86
|
+
const view = await setup({ grants: grants(false) });
|
|
87
|
+
await expectMenu(false);
|
|
88
|
+
await view.signInAs(DEFAULT_ADMIN_CREDENTIALS);
|
|
89
|
+
await expectMenu(true);
|
|
90
|
+
await view.signInAs(member);
|
|
91
|
+
await expectMenu(false);
|
|
92
|
+
await view.signInAs(DEFAULT_ADMIN_CREDENTIALS);
|
|
93
|
+
await expectMenu(true);
|
|
94
|
+
});
|
|
95
|
+
it('clears permissions on logout and same-user re-login with a new session', async () => {
|
|
96
|
+
const view = await setup();
|
|
97
|
+
await expectMenu(true);
|
|
98
|
+
const firstSession = view.identity().session!.session.id;
|
|
99
|
+
await act(async () => {
|
|
100
|
+
await view.identity().client.signOut();
|
|
101
|
+
await view.identity().refresh();
|
|
102
|
+
});
|
|
103
|
+
await waitFor(() => expect(view.identity().session).toBeNull());
|
|
104
|
+
expect(await screen.findByText('Access denied')).toBeVisible();
|
|
105
|
+
expect(
|
|
106
|
+
screen.getByRole('navigation', { name: 'Pages' }),
|
|
107
|
+
).toBeEmptyDOMElement();
|
|
108
|
+
await changePermissions(view, false);
|
|
109
|
+
await view.signInAs(member);
|
|
110
|
+
expect(view.identity().session!.session.id).not.toBe(firstSession);
|
|
111
|
+
await expectMenu(false);
|
|
112
|
+
});
|
|
113
|
+
it('revokes and restores a mounted menu and page after persisted permission changes', async () => {
|
|
114
|
+
const view = await setup();
|
|
115
|
+
await expectMenu(true);
|
|
116
|
+
await changePermissions(view, false);
|
|
117
|
+
invalidate(view);
|
|
118
|
+
expect(screen.queryByText('users content')).not.toBeInTheDocument();
|
|
119
|
+
await expectMenu(false);
|
|
120
|
+
await changePermissions(view, true);
|
|
121
|
+
invalidate(view);
|
|
122
|
+
await expectMenu(true);
|
|
123
|
+
});
|
|
124
|
+
it('hides previously allowed pages on network failure and recovers on retry', async () => {
|
|
125
|
+
let offline = false;
|
|
126
|
+
const view = await setup({
|
|
127
|
+
beforeRequest(request) {
|
|
128
|
+
if (offline && request.url.endsWith('/authorization/permissions'))
|
|
129
|
+
throw new TypeError('Failed to fetch');
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
await expectMenu(true);
|
|
133
|
+
offline = true;
|
|
134
|
+
invalidate(view);
|
|
135
|
+
await waitFor(() =>
|
|
136
|
+
expect(
|
|
137
|
+
screen.getByRole('navigation', { name: 'Pages' }),
|
|
138
|
+
).toBeEmptyDOMElement(),
|
|
139
|
+
);
|
|
140
|
+
expect(await screen.findByText('Access denied')).toBeVisible();
|
|
141
|
+
expect(screen.queryByText('users content')).not.toBeInTheDocument();
|
|
142
|
+
offline = false;
|
|
143
|
+
invalidate(view);
|
|
144
|
+
await expectMenu(true);
|
|
145
|
+
});
|
|
146
|
+
it('ignores an old permissive response after a new session has fewer permissions', async () => {
|
|
147
|
+
const old = Promise.withResolvers<void>();
|
|
148
|
+
const received = Promise.withResolvers<void>();
|
|
149
|
+
let held = false;
|
|
150
|
+
const view = await setup({
|
|
151
|
+
afterResponse(request) {
|
|
152
|
+
// Only delay delivery: the response itself is produced by the real permission service.
|
|
153
|
+
if (!held && request.url.endsWith('/authorization/permissions')) {
|
|
154
|
+
held = true;
|
|
155
|
+
received.resolve();
|
|
156
|
+
return old.promise;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
await received.promise;
|
|
162
|
+
await changePermissions(view, false);
|
|
163
|
+
await view.signInAs(member);
|
|
164
|
+
await expectMenu(false);
|
|
165
|
+
await act(async () => {
|
|
166
|
+
old.resolve();
|
|
167
|
+
await old.promise;
|
|
168
|
+
});
|
|
169
|
+
await expectMenu(false);
|
|
170
|
+
} finally {
|
|
171
|
+
old.resolve();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { usePageBreadcrumb } from '@nocobase/app-client';
|
|
2
2
|
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '
|
|
7
|
-
import { render, screen, within } from '@testing-library/react';
|
|
8
|
-
import type { ReactElement, ReactNode } from 'react';
|
|
9
|
-
import { MemoryRouter, Route, Routes } from 'react-router';
|
|
3
|
+
import { renderWithApp, answerApi } from '@nocobase/app-testing/client';
|
|
4
|
+
import { screen, within } from '@testing-library/react';
|
|
5
|
+
import type { ReactElement } from 'react';
|
|
6
|
+
import { Route, Routes } from 'react-router';
|
|
10
7
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
11
8
|
|
|
12
9
|
import { AppLayout } from '../../client/layouts/app-layout.js';
|
|
@@ -15,28 +12,8 @@ import {
|
|
|
15
12
|
SidebarPreferenceProvider,
|
|
16
13
|
} from '../../client/layouts/use-sidebar-preference.js';
|
|
17
14
|
import enUS from '../../client/locales/en-US.js';
|
|
15
|
+
import packageMetadata from '../../package.json' with { type: 'json' };
|
|
18
16
|
|
|
19
|
-
// Breadcrumb titles are route data, not keys any namespace owns: the trail translates each through its package's
|
|
20
|
-
// namespace with the title itself as `defaultValue`, so the runtime is not strict.
|
|
21
|
-
const runtime = await createTestI18nRuntime({
|
|
22
|
-
application: { namespace: '@nocobase/app-template-default', resources: enUS },
|
|
23
|
-
strict: false,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
function I18n({ children }: { readonly children: ReactNode }) {
|
|
27
|
-
return <TestI18nProvider runtime={runtime}>{children}</TestI18nProvider>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
vi.mock('@nocobase/app-plugin-i18n/client', async (importOriginal) => ({
|
|
31
|
-
...(await importOriginal<
|
|
32
|
-
typeof import('@nocobase/app-plugin-i18n/client')
|
|
33
|
-
>()),
|
|
34
|
-
useSyncServerLocale: () => {},
|
|
35
|
-
}));
|
|
36
|
-
vi.mock('@nocobase/app-client', async (original) => ({
|
|
37
|
-
...(await original<typeof import('@nocobase/app-client')>()),
|
|
38
|
-
useClientApplication: () => ({ runtime: {} }),
|
|
39
|
-
}));
|
|
40
17
|
vi.mock('../../client/routing/route-navigation.js', async (original) => ({
|
|
41
18
|
...(await original<
|
|
42
19
|
typeof import('../../client/routing/route-navigation.js')
|
|
@@ -90,25 +67,31 @@ function OrderPage(): ReactElement {
|
|
|
90
67
|
return <h1>Order #42</h1>;
|
|
91
68
|
}
|
|
92
69
|
|
|
93
|
-
function renderLayout(page: ReactElement): HTMLElement {
|
|
70
|
+
async function renderLayout(page: ReactElement): Promise<HTMLElement> {
|
|
94
71
|
const sidebarPreference = createAppSidebarPreferenceStore('/test-app');
|
|
95
|
-
|
|
72
|
+
await renderWithApp(
|
|
96
73
|
<SidebarPreferenceProvider value={sidebarPreference}>
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
<Route
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
</Routes>
|
|
103
|
-
</MemoryRouter>
|
|
74
|
+
<Routes>
|
|
75
|
+
<Route element={<AppLayout routes={routes} />}>
|
|
76
|
+
<Route path='/orders/:id' element={page} />
|
|
77
|
+
</Route>
|
|
78
|
+
</Routes>
|
|
104
79
|
</SidebarPreferenceProvider>,
|
|
105
|
-
{
|
|
80
|
+
{
|
|
81
|
+
route: '/orders/42',
|
|
82
|
+
namespace: packageMetadata.name,
|
|
83
|
+
namespaces: {
|
|
84
|
+
[packageMetadata.name]: enUS,
|
|
85
|
+
test: { Orders: 'Orders', Detail: 'Detail' },
|
|
86
|
+
},
|
|
87
|
+
fetch: answerApi(() => ({ data: {} })),
|
|
88
|
+
},
|
|
106
89
|
);
|
|
107
90
|
return screen.getByRole('banner');
|
|
108
91
|
}
|
|
109
92
|
|
|
110
|
-
it('shows the route trail of a nested page in its header', () => {
|
|
111
|
-
const header = renderLayout(<h1>Detail page</h1>);
|
|
93
|
+
it('shows the route trail of a nested page in its header', async () => {
|
|
94
|
+
const header = await renderLayout(<h1>Detail page</h1>);
|
|
112
95
|
const trail = within(header).getByRole('navigation', { name: 'Breadcrumb' });
|
|
113
96
|
expect(within(trail).getByRole('link', { name: 'Orders' })).toHaveAttribute(
|
|
114
97
|
'href',
|
|
@@ -124,8 +107,8 @@ it('shows the route trail of a nested page in its header', () => {
|
|
|
124
107
|
).toHaveLength(1);
|
|
125
108
|
});
|
|
126
109
|
|
|
127
|
-
it('shows the trail a page declares, with its record names, in place of the route trail', () => {
|
|
128
|
-
const header = renderLayout(<OrderPage />);
|
|
110
|
+
it('shows the trail a page declares, with its record names, in place of the route trail', async () => {
|
|
111
|
+
const header = await renderLayout(<OrderPage />);
|
|
129
112
|
const trail = within(header).getByRole('navigation', { name: 'Breadcrumb' });
|
|
130
113
|
expect(within(trail).getByRole('link', { name: 'Orders' })).toHaveAttribute(
|
|
131
114
|
'href',
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
|
|
3
1
|
import fs from 'node:fs';
|
|
4
2
|
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
3
|
|
|
7
4
|
import { describe, expect, it } from 'vitest';
|
|
8
5
|
|
|
9
6
|
import enUS from '../../client/locales/en-US.ts';
|
|
10
7
|
import zhCN from '../../client/locales/zh-CN.ts';
|
|
11
8
|
|
|
12
|
-
const clientDirectory =
|
|
9
|
+
const clientDirectory = path.resolve(import.meta.dirname, '../../client');
|
|
13
10
|
|
|
14
11
|
/** Every addressable key of a locale, as the dot-separated path `t()` is called with. */
|
|
15
12
|
function translationKeys(
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
|
|
3
1
|
import { readFileSync } from 'node:fs';
|
|
4
2
|
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
3
|
|
|
7
4
|
import { describe, expect, it } from 'vitest';
|
|
8
5
|
|
|
9
6
|
import clientPlugins from '../../client/plugins.js';
|
|
10
7
|
|
|
11
|
-
const appRoot =
|
|
8
|
+
const appRoot = path.resolve(import.meta.dirname, '../..');
|
|
12
9
|
|
|
13
10
|
interface AppPackageJson {
|
|
14
11
|
readonly dependencies?: Record<string, string>;
|
|
@@ -2,14 +2,30 @@ import type {
|
|
|
2
2
|
AppClientRegisteredRoute,
|
|
3
3
|
AppClientRouteComponentModule,
|
|
4
4
|
} from '@nocobase/app-client/plugins';
|
|
5
|
-
import { act,
|
|
6
|
-
import { describe, expect, it
|
|
5
|
+
import { act, screen } from '@testing-library/react';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
7
|
|
|
8
8
|
import { ClientRoute } from '../../client/routing/client-route.tsx';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
import { renderWithApp } from '@nocobase/app-testing/client';
|
|
11
|
+
import { apiClientToken } from '@nocobase/app-client';
|
|
12
|
+
import {
|
|
13
|
+
AuthorizationClient,
|
|
14
|
+
authorizationClientToken,
|
|
15
|
+
} from '@nocobase/app-plugin-authorization/client';
|
|
16
|
+
import packageMetadata from '../../package.json' with { type: 'json' };
|
|
17
|
+
import enUS from '../../client/locales/en-US.js';
|
|
18
|
+
|
|
19
|
+
const options = {
|
|
20
|
+
namespace: packageMetadata.name,
|
|
21
|
+
namespaces: { [packageMetadata.name]: enUS },
|
|
22
|
+
services(app: import('@nocobase/app-client').ClientApplication) {
|
|
23
|
+
app.container.singleton(
|
|
24
|
+
authorizationClientToken,
|
|
25
|
+
(resolver) => new AuthorizationClient(resolver.resolve(apiClientToken)),
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
13
29
|
|
|
14
30
|
const route: AppClientRegisteredRoute = {
|
|
15
31
|
id: 'recoverable',
|
|
@@ -34,7 +50,10 @@ const healthyModule: AppClientRouteComponentModule = {
|
|
|
34
50
|
|
|
35
51
|
describe('client route loading recovery', () => {
|
|
36
52
|
it('clears a previous failure when the same mounted route loads successfully', async () => {
|
|
37
|
-
const { rerender } =
|
|
53
|
+
const { rerender } = await renderWithApp(
|
|
54
|
+
<ClientRoute route={route} />,
|
|
55
|
+
options,
|
|
56
|
+
);
|
|
38
57
|
expect(
|
|
39
58
|
await screen.findByText('Unable to load page', {}, ROUTE_LOAD_TIMEOUT),
|
|
40
59
|
).toBeVisible();
|
|
@@ -58,8 +77,9 @@ describe('client route loading recovery', () => {
|
|
|
58
77
|
rejectLoad = reject;
|
|
59
78
|
},
|
|
60
79
|
);
|
|
61
|
-
const { rerender } =
|
|
80
|
+
const { rerender } = await renderWithApp(
|
|
62
81
|
<ClientRoute route={{ ...route, componentLoader: () => pending }} />,
|
|
82
|
+
options,
|
|
63
83
|
);
|
|
64
84
|
rerender(
|
|
65
85
|
<ClientRoute
|
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
import { fireEvent,
|
|
1
|
+
import { fireEvent, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
4
4
|
import { AppThemeProvider } from '../../client/theme/theme-provider';
|
|
5
5
|
import { ThemeSettings } from '../../client/theme/theme-settings';
|
|
6
6
|
import { UserMenu } from '../../client/layouts/components/user-menu';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
vi.mock('@nocobase/app-plugin-authentication/client', () => ({
|
|
10
|
-
useAuthentication: () => ({
|
|
11
|
-
session: { user: { id: 'operator', name: 'Operator' } },
|
|
12
|
-
isPending: false,
|
|
13
|
-
}),
|
|
14
|
-
}));
|
|
15
|
-
// Language selection has its own integration tests with the real i18n runtime.
|
|
16
|
-
vi.mock('../../client/layouts/components/language-switcher.js', () => ({
|
|
17
|
-
LanguageSwitcher: () => null,
|
|
18
|
-
}));
|
|
8
|
+
import { renderAuthenticatedClient } from '../helpers/authenticated-client.js';
|
|
19
9
|
|
|
20
10
|
beforeEach(() => {
|
|
21
11
|
// jsdom does not implement the media queries used by the real theme provider.
|
|
@@ -43,7 +33,7 @@ it.each(panels)(
|
|
|
43
33
|
'opens and closes %s on hover',
|
|
44
34
|
async (label, Component, role) => {
|
|
45
35
|
const user = userEvent.setup();
|
|
46
|
-
|
|
36
|
+
await renderAuthenticatedClient(
|
|
47
37
|
<AppThemeProvider>
|
|
48
38
|
<Component />
|
|
49
39
|
</AppThemeProvider>,
|
|
@@ -70,7 +60,7 @@ it.each(panels)(
|
|
|
70
60
|
'opens %s by keyboard and closes with Escape',
|
|
71
61
|
async (label, Component, role) => {
|
|
72
62
|
const user = userEvent.setup();
|
|
73
|
-
|
|
63
|
+
await renderAuthenticatedClient(
|
|
74
64
|
<AppThemeProvider>
|
|
75
65
|
<Component />
|
|
76
66
|
</AppThemeProvider>,
|