@nocobase/app-template-default 1.0.0-beta.88 → 1.0.0-beta.89
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/rules/components.md +3 -1
- package/.agents/rules/shell.md +8 -2
- package/AGENTS.md +1 -1
- package/client/components/back-button.tsx +37 -12
- package/client/components/brand-loading-mark.tsx +5 -2
- package/client/components/brand-mark-geometry.ts +3 -3
- package/client/components/breadcrumbs.tsx +47 -3
- package/client/components/data-table/data-table.tsx +90 -19
- package/client/components/data-table/pagination.tsx +17 -2
- package/client/components/list-feedback.tsx +31 -22
- package/client/components/loading.tsx +127 -16
- package/client/components/pending-button.tsx +111 -0
- package/client/components/role-list.tsx +7 -0
- package/client/components/route-child-page.tsx +72 -1
- package/client/components/route-overlay.tsx +149 -15
- package/client/components/task-progress.tsx +155 -0
- package/client/components/use-route-overlay.ts +8 -11
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +62 -32
- package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +2 -0
- package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +2 -0
- package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +1 -0
- package/client/extensions/nocobase-workflow-management/README.md +5 -5
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +2 -9
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +2 -4
- package/client/layouts/app-layout.tsx +0 -4
- package/client/locales/system/en-US.ts +19 -0
- package/client/locales/system/zh-CN.ts +19 -0
- package/client/pages/settings/default-access.tsx +1 -1
- package/client/pages/settings/roles.tsx +27 -27
- package/client/pages/settings/routes.ts +15 -7
- package/client/pages/settings/user-detail.tsx +3 -17
- package/client/pages/settings/user-form.tsx +9 -15
- package/client/pages/settings/users.tsx +11 -27
- package/client/routing/client-route.tsx +199 -37
- package/client/routing/navigation-destination.ts +42 -0
- package/client/routing/route-leave-boundary.tsx +13 -1
- package/client/styles.css +18 -1
- package/package.json +8 -8
- package/tests/client/auth-i18n.test.tsx +1 -1
- package/tests/client/authenticated-client-fixture.test.tsx +37 -0
- package/tests/client/back-button.test.tsx +34 -0
- package/tests/client/client-route-recovery.test.tsx +93 -1
- package/tests/client/list-feedback.test.tsx +5 -5
- package/tests/client/list-loading.test.tsx +397 -0
- package/tests/client/loading.test.tsx +20 -11
- package/tests/client/manual-list-retry.test.tsx +2 -2
- package/tests/client/navigation-parent.test.tsx +172 -0
- package/tests/client/pending-button.test.tsx +106 -0
- package/tests/client/permission-management-tabs.test.tsx +48 -12
- package/tests/client/primitive-labels.test.tsx +2 -2
- package/tests/client/role-return.test.tsx +21 -3
- package/tests/client/roles.test.tsx +84 -3
- package/tests/client/route-leave-recheck.test.tsx +74 -0
- package/tests/client/route-presentation.test.tsx +557 -0
- package/tests/client/session-preparation.test.tsx +20 -17
- package/tests/client/settings-continuity.test.tsx +35 -11
- package/tests/client/settings-home.test.tsx +4 -2
- package/tests/client/settings-idle-list.test.tsx +124 -0
- package/tests/client/user-drawer.test.tsx +3 -4
- package/tests/fixtures/dialog-footer.html +12 -0
- package/tests/fixtures/dialog-footer.tsx +215 -0
- package/tests/helpers/authenticated-client.tsx +70 -22
- package/tests/playwright/dialog-footer.test.ts +227 -0
|
@@ -95,6 +95,7 @@ class LeaveController {
|
|
|
95
95
|
class LeaveScope {
|
|
96
96
|
private readonly entries = new Set<LeaveController>();
|
|
97
97
|
private hidden = false;
|
|
98
|
+
private generation = 0;
|
|
98
99
|
constructor(private readonly dirtyChanged: (dirty: boolean) => void) {}
|
|
99
100
|
|
|
100
101
|
changed = (): void => {
|
|
@@ -118,7 +119,12 @@ class LeaveScope {
|
|
|
118
119
|
this.changed();
|
|
119
120
|
}
|
|
120
121
|
update(hidden: boolean, pathname: string): void {
|
|
121
|
-
this.hidden
|
|
122
|
+
if (this.hidden !== hidden) {
|
|
123
|
+
this.generation += 1;
|
|
124
|
+
this.hidden = hidden;
|
|
125
|
+
// A confirmation started before concealment must not discard or resume its old navigation on recovery.
|
|
126
|
+
if (hidden) for (const entry of this.entries) entry.cancel();
|
|
127
|
+
}
|
|
122
128
|
for (const entry of this.entries) {
|
|
123
129
|
if (pathname !== entry.origin && !pathname.startsWith(`${entry.origin}/`))
|
|
124
130
|
this.remove(entry);
|
|
@@ -130,8 +136,14 @@ class LeaveScope {
|
|
|
130
136
|
for (const entry of this.entries) if (!entry.active) this.remove(entry);
|
|
131
137
|
}
|
|
132
138
|
guard = (): Decision => {
|
|
139
|
+
const generation = this.generation;
|
|
133
140
|
const remaining = [...this.entries];
|
|
134
141
|
const next = (): Decision => {
|
|
142
|
+
if (
|
|
143
|
+
generation !== this.generation ||
|
|
144
|
+
(this.hidden && this.entries.size > 0)
|
|
145
|
+
)
|
|
146
|
+
return false;
|
|
135
147
|
const entry = remaining.shift();
|
|
136
148
|
if (!entry) return true;
|
|
137
149
|
const value = entry.guard();
|
package/client/styles.css
CHANGED
|
@@ -252,9 +252,26 @@ label:has(> .sr-only:focus-visible),
|
|
|
252
252
|
:is([data-slot='dialog-description'], [data-slot='alert-dialog-description']) {
|
|
253
253
|
line-height: 1.65;
|
|
254
254
|
}
|
|
255
|
-
|
|
255
|
+
/* Size semantic actions equally, including Button render={<Link />} and the
|
|
256
|
+
AlertDialog wrappers, which replace Button's slot. Footer sizing takes
|
|
257
|
+
precedence over primitive size utilities; long labels may grow and wrap. */
|
|
258
|
+
:is([data-slot='dialog-footer'], [data-slot='alert-dialog-footer'])
|
|
259
|
+
> :is(
|
|
260
|
+
[data-slot='button'],
|
|
261
|
+
[data-slot='dialog-close'],
|
|
262
|
+
[data-slot='alert-dialog-action'],
|
|
263
|
+
[data-slot='alert-dialog-cancel']
|
|
264
|
+
) {
|
|
265
|
+
height: auto;
|
|
256
266
|
min-height: calc(var(--spacing) * 10);
|
|
267
|
+
min-width: 0;
|
|
268
|
+
align-self: stretch;
|
|
269
|
+
flex-shrink: 1;
|
|
270
|
+
padding-block: calc(var(--spacing) * 2);
|
|
257
271
|
padding-inline: calc(var(--spacing) * 4);
|
|
272
|
+
white-space: normal;
|
|
273
|
+
overflow-wrap: anywhere;
|
|
274
|
+
text-align: center;
|
|
258
275
|
}
|
|
259
276
|
/* Route overlays compose the dialog primitives, so their close button sits where a dialog's does. */
|
|
260
277
|
:is([data-slot='dialog-content'], [data-slot='route-overlay-content'])
|
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.89",
|
|
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.89"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "4.0.131",
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"elkjs": "^0.10.2",
|
|
75
75
|
"@types/jsdom": "^30.0.0",
|
|
76
76
|
"@noble/hashes": "2.3.0",
|
|
77
|
-
"@nocobase/app-client": "^4.0.0-beta.
|
|
77
|
+
"@nocobase/app-client": "^4.0.0-beta.9",
|
|
78
78
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.2",
|
|
79
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
79
|
+
"@nocobase/app-skills": "0.1.0-beta.60",
|
|
80
80
|
"@nocobase/app-testing": "0.1.0-beta.8",
|
|
81
81
|
"@nocobase/app-websocket": "^0.1.0-beta.1",
|
|
82
82
|
"@nocobase/dev-config": "0.1.0-beta.25",
|
|
@@ -144,13 +144,13 @@
|
|
|
144
144
|
"hono": "^4.13.2",
|
|
145
145
|
"@nocobase/concurrency": "^0.1.0-beta.1",
|
|
146
146
|
"@nocobase/app-plugin-concurrency": "^0.1.0-beta.2",
|
|
147
|
-
"@nocobase/app-plugin-agents": "^2.0.0-beta.
|
|
147
|
+
"@nocobase/app-plugin-agents": "^2.0.0-beta.6",
|
|
148
148
|
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.21",
|
|
149
149
|
"@nocobase/app-plugin-authentication": "^2.0.0-beta.19",
|
|
150
150
|
"@nocobase/app-plugin-authorization": "^1.0.0-beta.36",
|
|
151
151
|
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.19",
|
|
152
152
|
"@nocobase/app-plugin-file": "^1.0.0-beta.29",
|
|
153
|
-
"@nocobase/app-plugin-foundation": "^0.1.0-beta.
|
|
153
|
+
"@nocobase/app-plugin-foundation": "^0.1.0-beta.16",
|
|
154
154
|
"@nocobase/app-plugin-i18n": "^1.0.0-beta.21",
|
|
155
155
|
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.17",
|
|
156
156
|
"@nocobase/app-plugin-mail": "^1.0.0-beta.21",
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
"@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.2",
|
|
159
159
|
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.18",
|
|
160
160
|
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.23",
|
|
161
|
-
"@nocobase/app-plugin-users": "^2.0.0-beta.
|
|
161
|
+
"@nocobase/app-plugin-users": "^2.0.0-beta.16",
|
|
162
162
|
"@nocobase/app-server": "^5.0.0-beta.1",
|
|
163
163
|
"@nocobase/config": "^0.1.0-beta.3",
|
|
164
164
|
"@nocobase/db": "^1.0.0-beta.25",
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
"@nocobase/caching": "^1.0.0-beta.4",
|
|
177
177
|
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
178
178
|
"@nocobase/app-cli": "^1.0.0-beta.32",
|
|
179
|
-
"@nocobase/realtime": "^1.0.0-beta.
|
|
179
|
+
"@nocobase/realtime": "^1.0.0-beta.1"
|
|
180
180
|
},
|
|
181
181
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
|
182
182
|
"imports": {
|
|
@@ -149,6 +149,6 @@ describe('authentication translations', () => {
|
|
|
149
149
|
await act(() => value.changeLanguage('zh-CN'));
|
|
150
150
|
expect(screen.getByText('此密码重置链接无效或已过期。')).toBeVisible();
|
|
151
151
|
expect(screen.getByText('如果账户存在,重置链接已发送。')).toBeVisible();
|
|
152
|
-
expect(screen.
|
|
152
|
+
expect(await screen.findByRole('status', { name: '加载中' })).toBeVisible();
|
|
153
153
|
});
|
|
154
154
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { createAuthenticatedClientRenderer } from '../helpers/authenticated-client.js';
|
|
3
|
+
|
|
4
|
+
const render = createAuthenticatedClientRenderer();
|
|
5
|
+
const identities = new Set<string>();
|
|
6
|
+
const permissionSets = new Set<string>();
|
|
7
|
+
const resource = { type: 'settings', id: 'authorization.default-access' };
|
|
8
|
+
|
|
9
|
+
it.each([
|
|
10
|
+
{ name: 'administrator', member: false, granted: false },
|
|
11
|
+
{ name: 'permitted member', member: true, granted: true },
|
|
12
|
+
{ name: 'unpermitted member', member: true, granted: false },
|
|
13
|
+
])(
|
|
14
|
+
'isolates the $name session and grants in a reused application',
|
|
15
|
+
async ({ member, granted }) => {
|
|
16
|
+
const view = await render(<span>Isolated client</span>, {
|
|
17
|
+
member,
|
|
18
|
+
grants: granted ? [{ resource, actions: [{ action: 'read' }] }] : [],
|
|
19
|
+
});
|
|
20
|
+
expect(identities.has(view.session.user.id)).toBe(false);
|
|
21
|
+
expect(permissionSets.has(view.permissionSetKey)).toBe(false);
|
|
22
|
+
identities.add(view.session.user.id);
|
|
23
|
+
permissionSets.add(view.permissionSetKey);
|
|
24
|
+
expect(view.identity().session?.user.id).toBe(view.session.user.id);
|
|
25
|
+
|
|
26
|
+
const response = await view.session.fetch('/authorization/permissions');
|
|
27
|
+
expect(response.status).toBe(200);
|
|
28
|
+
const { data } = await response.json();
|
|
29
|
+
expect(data.unrestricted).toBe(!member);
|
|
30
|
+
const permission = expect.objectContaining({
|
|
31
|
+
resource,
|
|
32
|
+
actions: expect.arrayContaining(['read']),
|
|
33
|
+
});
|
|
34
|
+
if (granted) expect(data.permissions).toContainEqual(permission);
|
|
35
|
+
else expect(data.permissions).not.toContainEqual(permission);
|
|
36
|
+
},
|
|
37
|
+
);
|
|
@@ -8,6 +8,8 @@ import type { ReactElement, ReactNode } from 'react';
|
|
|
8
8
|
import { createMemoryRouter, Outlet, RouterProvider } from 'react-router';
|
|
9
9
|
import { describe, expect, it } from 'vitest';
|
|
10
10
|
|
|
11
|
+
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
12
|
+
import { RouteTreeProvider } from '../../client/routing/route-context.js';
|
|
11
13
|
import { BackButton } from '#components/back-button';
|
|
12
14
|
|
|
13
15
|
import enUS from '../../client/locales/en-US.js';
|
|
@@ -41,6 +43,25 @@ function renderAt(url: string, child: ReactElement) {
|
|
|
41
43
|
],
|
|
42
44
|
{ initialEntries: [url] },
|
|
43
45
|
);
|
|
46
|
+
const registered = ['/projects', '/overview'].map(
|
|
47
|
+
(path) =>
|
|
48
|
+
({
|
|
49
|
+
path,
|
|
50
|
+
id: path,
|
|
51
|
+
name: path,
|
|
52
|
+
packageName: 'test',
|
|
53
|
+
auth: 'required',
|
|
54
|
+
authz: 'skip',
|
|
55
|
+
componentLoader: async () => ({ default: () => null }),
|
|
56
|
+
}) as AppClientRegisteredRoute,
|
|
57
|
+
);
|
|
58
|
+
// The provider needs to be within the router, so wrap each route's page.
|
|
59
|
+
const routes = router.routes;
|
|
60
|
+
for (const entry of routes) {
|
|
61
|
+
entry.element = (
|
|
62
|
+
<RouteTreeProvider routes={registered}>{entry.element}</RouteTreeProvider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
44
65
|
render(<RouterProvider router={router} />, { wrapper: I18n });
|
|
45
66
|
return router;
|
|
46
67
|
}
|
|
@@ -77,4 +98,17 @@ describe('BackButton', () => {
|
|
|
77
98
|
screen.getByRole('link', { name: 'Back to the overview' }),
|
|
78
99
|
).toHaveAttribute('href', '/overview');
|
|
79
100
|
});
|
|
101
|
+
it.each(['https://example.com/overview', '//example.com', '/missing'])(
|
|
102
|
+
'falls back safely for %s without naming the invalid destination',
|
|
103
|
+
(to) => {
|
|
104
|
+
renderAt(
|
|
105
|
+
'/projects/12',
|
|
106
|
+
<BackButton to={to}>Private destination</BackButton>,
|
|
107
|
+
);
|
|
108
|
+
expect(
|
|
109
|
+
screen.getByRole('link', { name: enUS.navigation.back }),
|
|
110
|
+
).toHaveAttribute('href', '/projects');
|
|
111
|
+
expect(screen.queryByText('Private destination')).not.toBeInTheDocument();
|
|
112
|
+
},
|
|
113
|
+
);
|
|
80
114
|
});
|
|
@@ -2,7 +2,8 @@ import type {
|
|
|
2
2
|
AppClientRegisteredRoute,
|
|
3
3
|
AppClientRouteComponentModule,
|
|
4
4
|
} from '@nocobase/app-client/plugins';
|
|
5
|
-
import { act, screen } from '@testing-library/react';
|
|
5
|
+
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import { Activity, useState } from 'react';
|
|
6
7
|
import { describe, expect, it } from 'vitest';
|
|
7
8
|
|
|
8
9
|
import { ClientRoute } from '../../client/routing/client-route.tsx';
|
|
@@ -49,6 +50,97 @@ const healthyModule: AppClientRouteComponentModule = {
|
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
describe('client route loading recovery', () => {
|
|
53
|
+
it('keeps a loaded wrapped component when an ancestor Activity reconnects its effects', async () => {
|
|
54
|
+
let loads = 0;
|
|
55
|
+
const componentLoader = async () => {
|
|
56
|
+
loads += 1;
|
|
57
|
+
return {
|
|
58
|
+
default: function Editor() {
|
|
59
|
+
const [draft, setDraft] = useState('');
|
|
60
|
+
return (
|
|
61
|
+
<input
|
|
62
|
+
aria-label='Draft'
|
|
63
|
+
value={draft}
|
|
64
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const definition = { ...route, componentLoader };
|
|
71
|
+
const view = (hidden: boolean) => (
|
|
72
|
+
<Activity mode={hidden ? 'hidden' : 'visible'}>
|
|
73
|
+
<ClientRoute route={definition} />
|
|
74
|
+
</Activity>
|
|
75
|
+
);
|
|
76
|
+
const { rerender } = await renderWithApp(view(false), options);
|
|
77
|
+
const input = await screen.findByRole(
|
|
78
|
+
'textbox',
|
|
79
|
+
{ name: 'Draft' },
|
|
80
|
+
ROUTE_LOAD_TIMEOUT,
|
|
81
|
+
);
|
|
82
|
+
fireEvent.change(input, { target: { value: 'Keep this draft' } });
|
|
83
|
+
rerender(view(true));
|
|
84
|
+
await waitFor(() => expect(input).not.toBeVisible());
|
|
85
|
+
rerender(view(false));
|
|
86
|
+
await waitFor(() => expect(input).toBeVisible());
|
|
87
|
+
expect(screen.getByRole('textbox', { name: 'Draft' })).toBe(input);
|
|
88
|
+
expect(input).toHaveValue('Keep this draft');
|
|
89
|
+
expect(loads).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('loads a replacement after a successful module and ignores a superseded successful response', async () => {
|
|
93
|
+
const pending = Promise.withResolvers<AppClientRouteComponentModule>();
|
|
94
|
+
const { rerender } = await renderWithApp(
|
|
95
|
+
<ClientRoute
|
|
96
|
+
route={{ ...route, componentLoader: async () => healthyModule }}
|
|
97
|
+
/>,
|
|
98
|
+
options,
|
|
99
|
+
);
|
|
100
|
+
await screen.findByText('Recovered page', {}, ROUTE_LOAD_TIMEOUT);
|
|
101
|
+
rerender(
|
|
102
|
+
<ClientRoute
|
|
103
|
+
route={{ ...route, componentLoader: () => pending.promise }}
|
|
104
|
+
/>,
|
|
105
|
+
);
|
|
106
|
+
const replacement = { default: () => <h2>Replacement page</h2> };
|
|
107
|
+
rerender(
|
|
108
|
+
<ClientRoute
|
|
109
|
+
route={{ ...route, componentLoader: async () => replacement }}
|
|
110
|
+
/>,
|
|
111
|
+
);
|
|
112
|
+
await screen.findByText('Replacement page', {}, ROUTE_LOAD_TIMEOUT);
|
|
113
|
+
await act(async () =>
|
|
114
|
+
pending.resolve({ default: () => <h2>Stale page</h2> }),
|
|
115
|
+
);
|
|
116
|
+
expect(screen.getByText('Replacement page')).toBeVisible();
|
|
117
|
+
expect(screen.queryByText('Stale page')).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('invokes the same loader again on retry after a failed attempt', async () => {
|
|
121
|
+
let loads = 0;
|
|
122
|
+
const componentLoader = async () => {
|
|
123
|
+
loads += 1;
|
|
124
|
+
if (loads === 1) throw new Error('Temporary failure');
|
|
125
|
+
return healthyModule;
|
|
126
|
+
};
|
|
127
|
+
await renderWithApp(
|
|
128
|
+
<ClientRoute route={{ ...route, componentLoader }} />,
|
|
129
|
+
options,
|
|
130
|
+
);
|
|
131
|
+
fireEvent.click(
|
|
132
|
+
await screen.findByRole(
|
|
133
|
+
'button',
|
|
134
|
+
{ name: 'Retry', exact: true },
|
|
135
|
+
ROUTE_LOAD_TIMEOUT,
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
expect(
|
|
139
|
+
await screen.findByText('Recovered page', {}, ROUTE_LOAD_TIMEOUT),
|
|
140
|
+
).toBeVisible();
|
|
141
|
+
expect(loads).toBe(2);
|
|
142
|
+
});
|
|
143
|
+
|
|
52
144
|
it('clears a previous failure when the same mounted route loads successfully', async () => {
|
|
53
145
|
const { rerender } = await renderWithApp(
|
|
54
146
|
<ClientRoute route={route} />,
|
|
@@ -97,7 +97,7 @@ describe('shared table and card feedback', () => {
|
|
|
97
97
|
/>,
|
|
98
98
|
options,
|
|
99
99
|
);
|
|
100
|
-
expect(screen.
|
|
100
|
+
expect((await screen.findByRole('status')).textContent).toBe(
|
|
101
101
|
enUS.listFeedback.loading,
|
|
102
102
|
);
|
|
103
103
|
expect(screen.getByRole('columnheader', { name: 'ID' })).toBeVisible();
|
|
@@ -159,7 +159,7 @@ describe('shared table and card feedback', () => {
|
|
|
159
159
|
expect(
|
|
160
160
|
screen.getByRole('button', { name: 'Search tools' }),
|
|
161
161
|
).toBeVisible();
|
|
162
|
-
expect(screen.
|
|
162
|
+
expect((await screen.findByRole(role)).closest('td')).not.toBeNull();
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
view.rerender(
|
|
@@ -178,9 +178,8 @@ describe('shared table and card feedback', () => {
|
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
it('renders each built-in state as one icon and one short line without inline actions', async () => {
|
|
181
|
-
const view = await renderWithApp(<ListFeedback state='
|
|
181
|
+
const view = await renderWithApp(<ListFeedback state='empty' />, options);
|
|
182
182
|
const states = [
|
|
183
|
-
['loading', 'status'],
|
|
184
183
|
['empty', 'status'],
|
|
185
184
|
['no-results', 'status'],
|
|
186
185
|
['forbidden', 'alert'],
|
|
@@ -273,8 +272,9 @@ describe('shared table and card feedback', () => {
|
|
|
273
272
|
expect(screen.getByRole('alert').textContent).toBe(
|
|
274
273
|
enUS.listFeedback.failed,
|
|
275
274
|
);
|
|
275
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
276
276
|
expect(
|
|
277
|
-
screen.
|
|
277
|
+
await screen.findByRole('button', { name: enUS.listFeedback.retrying }),
|
|
278
278
|
).toBeDisabled();
|
|
279
279
|
});
|
|
280
280
|
|