@nocobase/app-template-default 1.0.0-beta.80 → 1.0.0-beta.82
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/shell.md +2 -0
- package/.agents/rules/workspace.md +9 -0
- package/.prettierignore +3 -0
- package/AGENTS.md +2 -1
- package/README.MD +4 -0
- package/client/components/attachment-list.tsx +589 -0
- package/client/components/back-button.tsx +15 -4
- package/client/components/breadcrumbs.tsx +12 -1
- package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +68 -71
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +26 -12
- package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +6 -6
- package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +3 -3
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +19 -19
- package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +27 -19
- package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +12 -8
- package/client/extensions/nocobase-file-component-ui/index.ts +6 -6
- package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +14 -11
- package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +49 -0
- package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +47 -0
- package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +48 -0
- package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +49 -0
- package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +46 -0
- package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +51 -0
- package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +49 -0
- package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +47 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +43 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +43 -0
- package/client/extensions/nocobase-file-component-ui/types.ts +3 -1
- package/client/locales/en-US.ts +2 -0
- package/client/locales/system/settings-en-US.ts +3 -0
- package/client/locales/system/settings-zh-CN.ts +2 -0
- package/client/locales/zh-CN.ts +2 -0
- package/client/pages/settings/role-form.tsx +5 -1
- package/client/pages/settings/user-form.tsx +13 -1
- package/client/pages/settings/users.tsx +1 -1
- package/client/routing/route-context.tsx +33 -1
- package/eslint.config.js +2 -0
- package/package.json +23 -21
- package/scripts/check-packages.mjs +46 -0
- package/tailwind.config.mjs +3 -9
- package/tests/client/list-return-state.test.tsx +202 -0
- package/tests/client/role-return.test.tsx +104 -0
- package/tests/client/user-drawer.test.tsx +48 -0
- package/tests/project/internal-packages.test.ts +98 -0
- package/tests/project/tailwind-sources.test.ts +2 -2
- package/tests/server/authorization-assignment-safety.test.ts +10 -9
- package/tests/server/member-role.test.ts +299 -0
- package/client/extensions/nocobase-file-component-ui/README.md +0 -13
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
2
|
+
import {
|
|
3
|
+
TestI18nProvider,
|
|
4
|
+
createTestI18nRuntime,
|
|
5
|
+
} from '@nocobase/i18n/testing';
|
|
6
|
+
import { act, render, screen } from '@testing-library/react';
|
|
7
|
+
import userEvent from '@testing-library/user-event';
|
|
8
|
+
import {
|
|
9
|
+
createMemoryRouter,
|
|
10
|
+
Link,
|
|
11
|
+
Outlet,
|
|
12
|
+
RouterProvider,
|
|
13
|
+
useLocation,
|
|
14
|
+
useSearchParams,
|
|
15
|
+
} from 'react-router';
|
|
16
|
+
import { describe, expect, it } from 'vitest';
|
|
17
|
+
import { Breadcrumbs } from '../../client/components/breadcrumbs.js';
|
|
18
|
+
import { BackButton } from '../../client/components/back-button.js';
|
|
19
|
+
import { RouteTreeProvider } from '../../client/routing/route-context.js';
|
|
20
|
+
import enUS from '../../client/locales/en-US.js';
|
|
21
|
+
|
|
22
|
+
const runtime = await createTestI18nRuntime({
|
|
23
|
+
application: { namespace: '@nocobase/navigation-test', resources: enUS },
|
|
24
|
+
strict: false,
|
|
25
|
+
});
|
|
26
|
+
const route = (
|
|
27
|
+
path: string,
|
|
28
|
+
title: string,
|
|
29
|
+
children: AppClientRegisteredRoute[] = [],
|
|
30
|
+
): AppClientRegisteredRoute => ({
|
|
31
|
+
id: path,
|
|
32
|
+
name: path,
|
|
33
|
+
path,
|
|
34
|
+
auth: 'required',
|
|
35
|
+
packageName: 'test',
|
|
36
|
+
source: 'application',
|
|
37
|
+
breadcrumb: { title },
|
|
38
|
+
componentLoader: async () => ({ default: () => null }),
|
|
39
|
+
children,
|
|
40
|
+
});
|
|
41
|
+
const routes = [
|
|
42
|
+
route('/orders', 'Orders', [
|
|
43
|
+
route('/orders/:id', 'Order', [route('/orders/:id/lines', 'Lines')]),
|
|
44
|
+
]),
|
|
45
|
+
route('/other', 'Other', [route('/other/:id', 'Other detail')]),
|
|
46
|
+
];
|
|
47
|
+
const initial = '/orders?q=Acme&status=open&page=2';
|
|
48
|
+
function List() {
|
|
49
|
+
const [query, setQuery] = useSearchParams();
|
|
50
|
+
const { pathname } = useLocation();
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<input
|
|
54
|
+
aria-label='Search'
|
|
55
|
+
value={query.get('q') ?? ''}
|
|
56
|
+
onChange={(event) => {
|
|
57
|
+
const next = new URLSearchParams(query);
|
|
58
|
+
next.set('q', event.target.value);
|
|
59
|
+
next.delete('page');
|
|
60
|
+
setQuery(next);
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
<output data-testid='page'>{query.get('page') ?? '1'}</output>
|
|
64
|
+
<output data-testid='record'>
|
|
65
|
+
{query.get('q') || 'All'}-{query.get('page') ?? '1'}
|
|
66
|
+
</output>
|
|
67
|
+
<Link to={`${pathname}/42?detailPage=7&q=child`}>Open detail</Link>
|
|
68
|
+
<button onClick={() => setQuery({})}>Clear</button>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
function Detail() {
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<BackButton />
|
|
76
|
+
<Link to='lines?linePage=9'>Open lines</Link>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
function setup(url = initial) {
|
|
81
|
+
const router = createMemoryRouter(
|
|
82
|
+
[
|
|
83
|
+
{
|
|
84
|
+
element: (
|
|
85
|
+
<TestI18nProvider runtime={runtime}>
|
|
86
|
+
<RouteTreeProvider routes={routes}>
|
|
87
|
+
<Breadcrumbs />
|
|
88
|
+
<Outlet />
|
|
89
|
+
</RouteTreeProvider>
|
|
90
|
+
</TestI18nProvider>
|
|
91
|
+
),
|
|
92
|
+
children: [
|
|
93
|
+
{
|
|
94
|
+
path: '/orders',
|
|
95
|
+
children: [
|
|
96
|
+
{ index: true, element: <List /> },
|
|
97
|
+
{
|
|
98
|
+
path: ':id',
|
|
99
|
+
children: [
|
|
100
|
+
{ index: true, element: <Detail /> },
|
|
101
|
+
{ path: 'lines', element: <h1>Lines</h1> },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
path: '/other',
|
|
108
|
+
children: [
|
|
109
|
+
{ index: true, element: <List /> },
|
|
110
|
+
{ path: ':id', element: <Detail /> },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
{ initialEntries: [url] },
|
|
117
|
+
);
|
|
118
|
+
render(<RouterProvider router={router} />);
|
|
119
|
+
return router;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe('returning to a list', () => {
|
|
123
|
+
it('restores the actual list query through breadcrumbs across repeated visits', async () => {
|
|
124
|
+
const router = setup();
|
|
125
|
+
for (let i = 0; i < 3; i++) {
|
|
126
|
+
await userEvent.click(screen.getByRole('link', { name: 'Open detail' }));
|
|
127
|
+
expect(screen.getByRole('link', { name: 'Orders' })).toHaveAttribute(
|
|
128
|
+
'href',
|
|
129
|
+
initial,
|
|
130
|
+
);
|
|
131
|
+
await userEvent.click(screen.getByRole('link', { name: 'Orders' }));
|
|
132
|
+
expect(router.state.location.search).toBe('?q=Acme&status=open&page=2');
|
|
133
|
+
expect(screen.getByRole('textbox', { name: 'Search' })).toHaveValue(
|
|
134
|
+
'Acme',
|
|
135
|
+
);
|
|
136
|
+
expect(screen.getByTestId('page')).toHaveTextContent('2');
|
|
137
|
+
expect(screen.getByTestId('record')).toHaveTextContent('Acme-2');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
it('restores list state through browser back and the standard BackButton', async () => {
|
|
141
|
+
const router = setup();
|
|
142
|
+
await userEvent.click(screen.getByRole('link', { name: 'Open detail' }));
|
|
143
|
+
await act(() => router.navigate(-1));
|
|
144
|
+
expect(screen.getByTestId('record')).toHaveTextContent('Acme-2');
|
|
145
|
+
await userEvent.click(screen.getByRole('link', { name: 'Open detail' }));
|
|
146
|
+
await userEvent.click(screen.getByRole('link', { name: 'Back' }));
|
|
147
|
+
expect(router.state.location.pathname).toBe('/orders');
|
|
148
|
+
expect(screen.getByTestId('record')).toHaveTextContent('Acme-2');
|
|
149
|
+
});
|
|
150
|
+
it('keeps each visited ancestor separate from a nested list query', async () => {
|
|
151
|
+
setup();
|
|
152
|
+
await userEvent.click(screen.getByRole('link', { name: 'Open detail' }));
|
|
153
|
+
await userEvent.click(screen.getByRole('link', { name: 'Open lines' }));
|
|
154
|
+
expect(screen.getByRole('link', { name: 'Orders' })).toHaveAttribute(
|
|
155
|
+
'href',
|
|
156
|
+
initial,
|
|
157
|
+
);
|
|
158
|
+
// Middle levels appear in both the desktop trail and the collapsed menu.
|
|
159
|
+
expect(screen.getByRole('link', { name: 'Order' })).toHaveAttribute(
|
|
160
|
+
'href',
|
|
161
|
+
'/orders/42?detailPage=7&q=child',
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
it('does not revive cleared filters or leak one list into another', async () => {
|
|
165
|
+
const router = setup();
|
|
166
|
+
await userEvent.click(screen.getByRole('button', { name: 'Clear' }));
|
|
167
|
+
await userEvent.click(screen.getByRole('link', { name: 'Open detail' }));
|
|
168
|
+
expect(screen.getByRole('link', { name: 'Orders' })).toHaveAttribute(
|
|
169
|
+
'href',
|
|
170
|
+
'/orders',
|
|
171
|
+
);
|
|
172
|
+
await act(() => router.navigate('/other?q=Beta&page=3'));
|
|
173
|
+
await userEvent.click(screen.getByRole('link', { name: 'Open detail' }));
|
|
174
|
+
expect(screen.getByRole('link', { name: 'Other' })).toHaveAttribute(
|
|
175
|
+
'href',
|
|
176
|
+
'/other?q=Beta&page=3',
|
|
177
|
+
);
|
|
178
|
+
await act(() => router.navigate('/orders/42?detailPage=8'));
|
|
179
|
+
expect(screen.getByRole('link', { name: 'Orders' })).toHaveAttribute(
|
|
180
|
+
'href',
|
|
181
|
+
'/orders',
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
it('does not apply a deep-linked detail query to an unvisited ancestor', () => {
|
|
185
|
+
setup('/orders/42?detailPage=7');
|
|
186
|
+
expect(screen.getByRole('link', { name: 'Orders' })).toHaveAttribute(
|
|
187
|
+
'href',
|
|
188
|
+
'/orders',
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
it('uses the latest search after changing conditions and resetting the page', async () => {
|
|
192
|
+
setup();
|
|
193
|
+
await userEvent.clear(screen.getByRole('textbox', { name: 'Search' }));
|
|
194
|
+
await userEvent.type(
|
|
195
|
+
screen.getByRole('textbox', { name: 'Search' }),
|
|
196
|
+
'Beta',
|
|
197
|
+
);
|
|
198
|
+
await userEvent.click(screen.getByRole('link', { name: 'Open detail' }));
|
|
199
|
+
await userEvent.click(screen.getByRole('link', { name: 'Orders' }));
|
|
200
|
+
expect(screen.getByTestId('record')).toHaveTextContent('Beta-1');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { answerApi, renderWithApp } from '@nocobase/app-testing/client';
|
|
2
|
+
import { apiClientToken, type ClientApplication } from '@nocobase/app-client';
|
|
3
|
+
import {
|
|
4
|
+
AuthorizationClient,
|
|
5
|
+
authorizationClientToken,
|
|
6
|
+
} from '@nocobase/app-plugin-authorization/client';
|
|
7
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { Outlet, Routes, Route, useLocation } from 'react-router';
|
|
10
|
+
import { expect, it, vi } from 'vitest';
|
|
11
|
+
import RoleForm from '../../client/pages/settings/role-form.js';
|
|
12
|
+
import enUS from '../../client/locales/en-US.js';
|
|
13
|
+
import zhCN from '../../client/locales/zh-CN.js';
|
|
14
|
+
|
|
15
|
+
const namespace = '@nocobase/role-return-test';
|
|
16
|
+
function Parent() {
|
|
17
|
+
return <Outlet context={{ reload: () => undefined }} />;
|
|
18
|
+
}
|
|
19
|
+
function Location() {
|
|
20
|
+
const { pathname, search } = useLocation();
|
|
21
|
+
return (
|
|
22
|
+
<output data-testid='location'>
|
|
23
|
+
{pathname}
|
|
24
|
+
{search}
|
|
25
|
+
</output>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
it.each(
|
|
29
|
+
['/roles', '/administration/roles'].flatMap((parent) =>
|
|
30
|
+
['back', 'cancel', 'save'].map((action) => ({ parent, action })),
|
|
31
|
+
),
|
|
32
|
+
)('returns to $parent on $action', async ({ parent, action }) => {
|
|
33
|
+
const calls = vi.fn(({ path }: { path: string }) => {
|
|
34
|
+
if (path === 'authorization/permissions')
|
|
35
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
36
|
+
if (path.endsWith('/options'))
|
|
37
|
+
return {
|
|
38
|
+
data: {
|
|
39
|
+
sections: [],
|
|
40
|
+
recordAccess: [],
|
|
41
|
+
subjectTypes: [],
|
|
42
|
+
collections: [],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
data: { key: 'sales', title: 'Sales', grants: [], revision: 'one' },
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
const result = await renderWithApp(
|
|
50
|
+
<>
|
|
51
|
+
<Routes>
|
|
52
|
+
<Route path={parent} element={<Parent />}>
|
|
53
|
+
<Route index element={<h1>Role list</h1>} />
|
|
54
|
+
<Route path=':key/edit' element={<RoleForm />} />
|
|
55
|
+
</Route>
|
|
56
|
+
</Routes>
|
|
57
|
+
<Location />
|
|
58
|
+
</>,
|
|
59
|
+
{
|
|
60
|
+
route: `${parent}/sales/edit?q=Sales&page=2`,
|
|
61
|
+
namespace,
|
|
62
|
+
namespaces: {
|
|
63
|
+
[namespace]: {
|
|
64
|
+
'en-US': async () => ({ default: enUS }),
|
|
65
|
+
'zh-CN': async () => ({ default: zhCN }),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
services: (app: ClientApplication) =>
|
|
69
|
+
app.container.instance(
|
|
70
|
+
authorizationClientToken,
|
|
71
|
+
new AuthorizationClient(app.container.resolve(apiClientToken)),
|
|
72
|
+
),
|
|
73
|
+
fetch: answerApi(calls),
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
expect(
|
|
77
|
+
await screen.findByRole('link', { name: enUS.settings.roles }),
|
|
78
|
+
).toHaveAttribute('href', `${parent}?q=Sales&page=2`);
|
|
79
|
+
if (action === 'back') {
|
|
80
|
+
await userEvent.click(
|
|
81
|
+
screen.getByRole('link', { name: enUS.settings.roles }),
|
|
82
|
+
);
|
|
83
|
+
} else if (action === 'save') {
|
|
84
|
+
const name = await screen.findByRole('textbox', { name: /Role name/ });
|
|
85
|
+
await userEvent.type(name, ' updated');
|
|
86
|
+
await userEvent.click(
|
|
87
|
+
screen.getByRole('button', { name: 'Save', exact: true }),
|
|
88
|
+
);
|
|
89
|
+
await waitFor(() =>
|
|
90
|
+
expect(calls).toHaveBeenCalledWith(
|
|
91
|
+
expect.objectContaining({ method: 'PATCH' }),
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
await userEvent.click(
|
|
96
|
+
await screen.findByRole('button', { name: 'Cancel' }),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
await screen.findByRole('heading', { name: 'Role list' });
|
|
100
|
+
expect(screen.getByTestId('location')).toHaveTextContent(
|
|
101
|
+
`${parent}?q=Sales&page=2`,
|
|
102
|
+
);
|
|
103
|
+
result.unmount();
|
|
104
|
+
});
|
|
@@ -68,6 +68,7 @@ async function renderUsers(
|
|
|
68
68
|
actions: string[] = [],
|
|
69
69
|
initialRoles: Record<string, string[]> = {},
|
|
70
70
|
requiredRole = false,
|
|
71
|
+
defaultRole = false,
|
|
71
72
|
) {
|
|
72
73
|
let current = { ...original, roleScopes: initialRoles };
|
|
73
74
|
let roleFailure = false;
|
|
@@ -105,7 +106,9 @@ async function renderUsers(
|
|
|
105
106
|
label: 'Application',
|
|
106
107
|
selection: 'multiple',
|
|
107
108
|
requiredOnCreate: requiredRole,
|
|
109
|
+
...(defaultRole ? { defaultValue: ['member'] } : {}),
|
|
108
110
|
options: [
|
|
111
|
+
{ value: 'member', label: 'Member' },
|
|
109
112
|
{ value: 'sales', label: 'Sales team' },
|
|
110
113
|
{ value: 'finance', label: 'Finance team' },
|
|
111
114
|
{
|
|
@@ -535,3 +538,48 @@ it('focuses and describes the required role dropdown when creating without a rol
|
|
|
535
538
|
view.calls.mock.calls.filter(([call]) => call.method === 'POST'),
|
|
536
539
|
).toHaveLength(0);
|
|
537
540
|
});
|
|
541
|
+
|
|
542
|
+
it('uses Member only as a creation default and respects explicitly cleared selection', async () => {
|
|
543
|
+
const user = userEvent.setup();
|
|
544
|
+
const view = await renderUsers(
|
|
545
|
+
'/users',
|
|
546
|
+
'/create',
|
|
547
|
+
true,
|
|
548
|
+
true,
|
|
549
|
+
[],
|
|
550
|
+
{},
|
|
551
|
+
false,
|
|
552
|
+
true,
|
|
553
|
+
);
|
|
554
|
+
const roles = await screen.findByRole('combobox', {
|
|
555
|
+
name: 'Application',
|
|
556
|
+
exact: true,
|
|
557
|
+
});
|
|
558
|
+
expect(roles).toHaveTextContent('Member');
|
|
559
|
+
await user.click(roles);
|
|
560
|
+
await user.click(
|
|
561
|
+
await screen.findByRole('option', { name: 'Member', exact: true }),
|
|
562
|
+
);
|
|
563
|
+
await user.keyboard('{Escape}');
|
|
564
|
+
expect(roles).not.toHaveTextContent('Member');
|
|
565
|
+
expect(screen.getByText(enUS.settings.noRoleAccess)).toBeVisible();
|
|
566
|
+
await user.type(screen.getByRole('textbox', { name: 'Name *' }), 'Roleless');
|
|
567
|
+
await user.type(
|
|
568
|
+
screen.getByRole('textbox', { name: 'Email *' }),
|
|
569
|
+
'roleless@example.test',
|
|
570
|
+
);
|
|
571
|
+
await user.type(
|
|
572
|
+
screen.getByLabelText(`${enUS.settings.password} *`),
|
|
573
|
+
'Roleless-test-123!',
|
|
574
|
+
);
|
|
575
|
+
await user.click(screen.getByRole('button', { name: 'Create', exact: true }));
|
|
576
|
+
await waitFor(() =>
|
|
577
|
+
expect(view.calls).toHaveBeenCalledWith(
|
|
578
|
+
expect.objectContaining({
|
|
579
|
+
method: 'POST',
|
|
580
|
+
path: 'users',
|
|
581
|
+
json: expect.objectContaining({ roleScopes: { app: [] } }),
|
|
582
|
+
}),
|
|
583
|
+
),
|
|
584
|
+
);
|
|
585
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { expect, it } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import packageMetadata from '../../package.json' with { type: 'json' };
|
|
16
|
+
|
|
17
|
+
it('checks every internal package without selecting the root application and propagates a failed check', () => {
|
|
18
|
+
const root = mkdtempSync(path.join(os.tmpdir(), 'app-internal-checks-'));
|
|
19
|
+
try {
|
|
20
|
+
cpSync(
|
|
21
|
+
path.resolve(import.meta.dirname, '../../scripts'),
|
|
22
|
+
path.join(root, 'scripts'),
|
|
23
|
+
{ recursive: true },
|
|
24
|
+
);
|
|
25
|
+
writeFileSync(
|
|
26
|
+
path.join(root, 'pnpm-workspace.yaml'),
|
|
27
|
+
"packages: ['packages/*']\nverifyDepsBeforeRun: false\n",
|
|
28
|
+
);
|
|
29
|
+
writeFileSync(
|
|
30
|
+
path.join(root, 'package.json'),
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
name: 'fixture-app',
|
|
33
|
+
private: true,
|
|
34
|
+
scripts: {
|
|
35
|
+
'check:packages': packageMetadata.scripts['check:packages'],
|
|
36
|
+
check: 'node -e "process.exit(99)"',
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
for (const name of ['first', 'second']) {
|
|
41
|
+
const directory = path.join(root, 'packages', name);
|
|
42
|
+
mkdirSync(directory, { recursive: true });
|
|
43
|
+
writeFileSync(
|
|
44
|
+
path.join(directory, 'package.json'),
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
name: `fixture-${name}`,
|
|
47
|
+
private: true,
|
|
48
|
+
scripts: { check: 'node check.cjs' },
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
writeFileSync(
|
|
52
|
+
path.join(directory, 'check.cjs'),
|
|
53
|
+
"require('node:fs').writeFileSync('checked', 'yes');\n",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const run = () =>
|
|
57
|
+
spawnSync('pnpm', ['check:packages'], {
|
|
58
|
+
cwd: root,
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
timeout: 30_000,
|
|
61
|
+
});
|
|
62
|
+
expect(run().status).toBe(0);
|
|
63
|
+
for (const name of ['first', 'second']) {
|
|
64
|
+
expect(
|
|
65
|
+
readFileSync(path.join(root, 'packages', name, 'checked'), 'utf8'),
|
|
66
|
+
).toBe('yes');
|
|
67
|
+
}
|
|
68
|
+
writeFileSync(
|
|
69
|
+
path.join(root, 'packages/second/check.cjs'),
|
|
70
|
+
'process.exit(7);\n',
|
|
71
|
+
);
|
|
72
|
+
expect(run().status).not.toBe(0);
|
|
73
|
+
rmSync(path.join(root, 'packages/first/checked'));
|
|
74
|
+
writeFileSync(
|
|
75
|
+
path.join(root, 'packages/second/package.json'),
|
|
76
|
+
JSON.stringify({ name: 'fixture-second', scripts: { check: ' ' } }),
|
|
77
|
+
);
|
|
78
|
+
const missing = run();
|
|
79
|
+
expect(missing.status).not.toBe(0);
|
|
80
|
+
expect(missing.stderr).toContain('packages/second');
|
|
81
|
+
expect(existsSync(path.join(root, 'packages/first/checked'))).toBe(false);
|
|
82
|
+
writeFileSync(
|
|
83
|
+
path.join(root, 'packages/second/package.json'),
|
|
84
|
+
JSON.stringify({ name: 'fixture-second' }),
|
|
85
|
+
);
|
|
86
|
+
expect(run().status).not.toBe(0);
|
|
87
|
+
// pnpm workspace exclusions apply to both the preflight and the script run.
|
|
88
|
+
writeFileSync(
|
|
89
|
+
path.join(root, 'pnpm-workspace.yaml'),
|
|
90
|
+
"packages: ['packages/*', '!packages/second']\nverifyDepsBeforeRun: false\n",
|
|
91
|
+
);
|
|
92
|
+
expect(run().status).toBe(0);
|
|
93
|
+
rmSync(path.join(root, 'packages'), { recursive: true });
|
|
94
|
+
expect(run().status).toBe(0);
|
|
95
|
+
} finally {
|
|
96
|
+
rmSync(root, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}, 60_000);
|
|
@@ -101,7 +101,7 @@ describe('tailwind content sources', () => {
|
|
|
101
101
|
}
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
it('
|
|
104
|
+
it('ignores obsolete plugin-owned Registry recipes', () => {
|
|
105
105
|
const store = mkdtempSync(path.join(tmpdir(), 'nb3-tailwind-registry-'));
|
|
106
106
|
try {
|
|
107
107
|
const registry = path.join(
|
|
@@ -124,7 +124,7 @@ describe('tailwind content sources', () => {
|
|
|
124
124
|
|
|
125
125
|
const files = contentFilesIn(store);
|
|
126
126
|
|
|
127
|
-
expect(files).
|
|
127
|
+
expect(files).toEqual([]);
|
|
128
128
|
} finally {
|
|
129
129
|
rmSync(store, { recursive: true, force: true });
|
|
130
130
|
}
|
|
@@ -11,14 +11,7 @@ type Assignment = {
|
|
|
11
11
|
function unexpectedGlobalAssignments(
|
|
12
12
|
assignments: readonly Assignment[],
|
|
13
13
|
): Assignment[] {
|
|
14
|
-
return assignments.filter(
|
|
15
|
-
(assignment) =>
|
|
16
|
-
assignment.subjectId === '*' &&
|
|
17
|
-
!(
|
|
18
|
-
assignment.subjectType === 'authenticated' &&
|
|
19
|
-
assignment.permissionSetKey === 'member'
|
|
20
|
-
),
|
|
21
|
-
);
|
|
14
|
+
return assignments.filter((assignment) => assignment.subjectId === '*');
|
|
22
15
|
}
|
|
23
16
|
|
|
24
17
|
const test = createAppTest({
|
|
@@ -76,5 +69,13 @@ it('detects wildcard recipients regardless of the role name, while preserving ex
|
|
|
76
69
|
global,
|
|
77
70
|
{ ...global, subjectType: 'everyone' },
|
|
78
71
|
]),
|
|
79
|
-
).toEqual([
|
|
72
|
+
).toEqual([
|
|
73
|
+
{
|
|
74
|
+
subjectType: 'authenticated',
|
|
75
|
+
subjectId: '*',
|
|
76
|
+
permissionSetKey: 'member',
|
|
77
|
+
},
|
|
78
|
+
global,
|
|
79
|
+
{ ...global, subjectType: 'everyone' },
|
|
80
|
+
]);
|
|
80
81
|
});
|