@nocobase/app-template-default 1.0.0-beta.82 → 1.0.0-beta.84
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/build-and-deploy.md +1 -1
- package/.agents/rules/shell.md +4 -0
- package/AGENTS.md +9 -1
- package/README.MD +10 -2
- package/client/components/back-button.tsx +3 -5
- package/client/components/default-access-editor.tsx +13 -2
- package/client/components/feedback-form.tsx +474 -0
- package/client/components/lifecycle-diagram.tsx +10 -10
- package/client/components/permission-editor.tsx +396 -210
- package/client/extensions/nocobase-auth-forms/password-login-form.tsx +17 -6
- package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +24 -7
- package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +19 -7
- package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +15 -7
- package/client/extensions/nocobase-device-approval/device-approval.tsx +7 -2
- package/client/extensions/nocobase-device-approval/locales/en-US.ts +16 -0
- package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +7 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +20 -17
- package/client/hooks/use-list-data.ts +77 -0
- package/client/hooks/use-list-location.ts +23 -0
- package/client/hooks/use-list-tabs.ts +42 -0
- package/client/hooks/use-table-url-state.ts +143 -30
- package/client/locales/system/en-US.ts +10 -0
- package/client/locales/system/settings-en-US.ts +8 -0
- package/client/locales/system/settings-zh-CN.ts +8 -0
- package/client/locales/system/zh-CN.ts +8 -0
- package/client/pages/settings/default-access-form.tsx +44 -26
- package/client/pages/settings/default-access.tsx +56 -27
- package/client/pages/settings/hooks.tsx +4 -59
- package/client/pages/settings/page-permissions.ts +17 -0
- package/client/pages/settings/permissions-model.ts +54 -58
- package/client/pages/settings/role-form.tsx +98 -56
- package/client/pages/settings/roles.tsx +11 -3
- package/client/pages/settings/shared.tsx +43 -33
- package/client/pages/settings/user-form.tsx +51 -30
- package/client/pages/settings/users.tsx +39 -27
- package/client/routing/session-preparation.tsx +10 -3
- package/database/main/migrations/202611020002_create_mutation_receipts.ts +19 -0
- package/package.json +13 -12
- package/server/providers/index.ts +4 -1
- package/server/providers/submission.ts +61 -0
- package/tests/client/auth-i18n.test.tsx +9 -0
- package/tests/client/back-button.test.tsx +3 -3
- package/tests/client/default-access.test.tsx +60 -4
- package/tests/client/form-feedback.test.tsx +589 -0
- package/tests/client/list-recovery.test.tsx +373 -0
- package/tests/client/list-tabs.test.tsx +121 -0
- package/tests/client/page-permission-navigation.test.ts +8 -0
- package/tests/client/permission-editor.test.tsx +191 -21
- package/tests/client/permission-filter-binding.test.tsx +1 -1
- package/tests/client/role-return.test.tsx +2 -1
- package/tests/client/session-preparation.test.tsx +24 -1
- package/tests/client/settings-continuity.test.tsx +5 -4
- package/tests/client/settings-feedback.test.tsx +25 -0
- package/tests/client/settings-permissions.test.ts +35 -0
- package/tests/client/settings.test.tsx +2 -1
- package/tests/client/table-url-state.test.tsx +117 -3
- package/tests/client/user-drawer.test.tsx +2 -1
- package/tests/client/user-permissions.test.tsx +30 -5
- package/tests/database/mutation-receipts.test.ts +45 -0
- package/tests/fixtures/form-feedback/README.md +33 -0
- package/tests/fixtures/form-feedback/equipment-detail.tsx +11 -0
- package/tests/fixtures/form-feedback/equipment-layout.tsx +15 -0
- package/tests/fixtures/form-feedback/equipment-migration.ts +27 -0
- package/tests/fixtures/form-feedback/equipment-server.ts +197 -0
- package/tests/fixtures/form-feedback/equipment-validation.tsx +94 -0
- package/tests/fixtures/form-feedback/equipment.tsx +369 -0
- package/tests/fixtures/form-feedback/verify-inline.mjs +174 -0
- package/tests/fixtures/form-feedback/verify.mjs +373 -0
- package/tests/fixtures/submission/README.md +35 -0
- package/tests/fixtures/submission/browser.test.ts +478 -0
- package/tests/fixtures/submission/migration.ts +61 -0
- package/tests/fixtures/submission/page.tsx +138 -0
- package/tests/fixtures/submission/playwright.config.ts +11 -0
- package/tests/fixtures/submission/routes.ts +208 -0
- package/tests/helpers/settings-client.tsx +31 -0
- package/tests/server/member-role.test.ts +7 -1
- package/tests/server/submission-settings.test.ts +241 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import console from 'node:console';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { URL } from 'node:url';
|
|
5
|
+
/* global document */
|
|
6
|
+
// Run only against an authorized isolated application with the equipment fixture installed.
|
|
7
|
+
import { chromium, expect } from '@playwright/test';
|
|
8
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const [base, output, name = 'application', legacy] = process.argv.slice(2);
|
|
12
|
+
if (
|
|
13
|
+
!base ||
|
|
14
|
+
!output ||
|
|
15
|
+
!/^http:\/\/(127\.0\.0\.1|localhost):\d+\/main\/$/.test(base)
|
|
16
|
+
)
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Supply the isolated server entry URL and an evidence directory.',
|
|
19
|
+
);
|
|
20
|
+
await mkdir(output, { recursive: true });
|
|
21
|
+
const browser = await chromium.launch();
|
|
22
|
+
const page = await browser.newPage({ viewport: { width: 1280, height: 960 } });
|
|
23
|
+
const failures = [];
|
|
24
|
+
page.on('pageerror', (error) => failures.push(error.message));
|
|
25
|
+
await page.goto(`${base}login`, { waitUntil: 'domcontentloaded' });
|
|
26
|
+
if (legacy !== '--legacy') {
|
|
27
|
+
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
|
28
|
+
await expect(page.getByLabel('Username or email')).toBeFocused();
|
|
29
|
+
await expect(page.getByLabel('Username or email')).toHaveAttribute(
|
|
30
|
+
'aria-invalid',
|
|
31
|
+
'true',
|
|
32
|
+
);
|
|
33
|
+
await page
|
|
34
|
+
.getByLabel('Username or email')
|
|
35
|
+
.fill(process.env.TEST_ADMIN_USERNAME ?? 'nocobase');
|
|
36
|
+
await page
|
|
37
|
+
.getByLabel('Password', { exact: true })
|
|
38
|
+
.fill('incorrect-fixture-password');
|
|
39
|
+
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
|
40
|
+
await expect(page.locator('form [role="alert"]').first()).toBeVisible();
|
|
41
|
+
await expect(page.getByLabel('Password', { exact: true })).toHaveValue(
|
|
42
|
+
'incorrect-fixture-password',
|
|
43
|
+
);
|
|
44
|
+
await page.screenshot({
|
|
45
|
+
path: join(output, `${name}-native-login.png`),
|
|
46
|
+
fullPage: true,
|
|
47
|
+
animations: 'disabled',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
await page
|
|
51
|
+
.getByLabel('Username or email')
|
|
52
|
+
.fill(process.env.TEST_ADMIN_USERNAME ?? 'nocobase');
|
|
53
|
+
await page
|
|
54
|
+
.getByLabel('Password', { exact: true })
|
|
55
|
+
.fill(process.env.TEST_ADMIN_PASSWORD ?? 'admin123');
|
|
56
|
+
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
|
57
|
+
await expect(page.getByLabel('Username or email')).not.toBeVisible();
|
|
58
|
+
|
|
59
|
+
async function open(container = 'page') {
|
|
60
|
+
await page.goto(`${base}equipment`, { waitUntil: 'domcontentloaded' });
|
|
61
|
+
await expect(page.getByTestId('retained-record')).toContainText(
|
|
62
|
+
'Legacy camera',
|
|
63
|
+
);
|
|
64
|
+
if (container !== 'page') {
|
|
65
|
+
await page.getByLabel('Container', { exact: true }).selectOption(container);
|
|
66
|
+
await expect(page).toHaveURL(
|
|
67
|
+
new RegExp(
|
|
68
|
+
container === 'detail-edit'
|
|
69
|
+
? '/equipment/retained/edit$'
|
|
70
|
+
: `/equipment/${container}$`,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
await expect(
|
|
74
|
+
page.getByLabel('Equipment name', { exact: true }),
|
|
75
|
+
).toBeVisible();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function save() {
|
|
79
|
+
await page.getByRole('button', { name: 'Save request', exact: true }).click();
|
|
80
|
+
}
|
|
81
|
+
async function inline(locator) {
|
|
82
|
+
await expect(locator).toHaveAttribute('aria-invalid', 'true');
|
|
83
|
+
const ids = (await locator.getAttribute('aria-describedby')).split(/\s+/);
|
|
84
|
+
expect(
|
|
85
|
+
await page.evaluate(
|
|
86
|
+
(ids) => ids.every((id) => document.getElementById(id)),
|
|
87
|
+
ids,
|
|
88
|
+
),
|
|
89
|
+
).toBe(true);
|
|
90
|
+
}
|
|
91
|
+
async function delayedRequest(action) {
|
|
92
|
+
let release;
|
|
93
|
+
let reached;
|
|
94
|
+
let complete;
|
|
95
|
+
const completed = new Promise((resolve) => {
|
|
96
|
+
complete = resolve;
|
|
97
|
+
});
|
|
98
|
+
const reachedPromise = new Promise((resolve) => {
|
|
99
|
+
reached = resolve;
|
|
100
|
+
});
|
|
101
|
+
const gate = new Promise((resolve) => {
|
|
102
|
+
release = resolve;
|
|
103
|
+
});
|
|
104
|
+
await page.route('**/api/equipmentFixture', async (route) => {
|
|
105
|
+
if (route.request().method() !== 'POST') return route.continue();
|
|
106
|
+
reached();
|
|
107
|
+
await gate;
|
|
108
|
+
await route.continue();
|
|
109
|
+
complete();
|
|
110
|
+
});
|
|
111
|
+
await save();
|
|
112
|
+
await reachedPromise;
|
|
113
|
+
await action();
|
|
114
|
+
release();
|
|
115
|
+
await completed;
|
|
116
|
+
await page.unroute('**/api/equipmentFixture');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await open();
|
|
120
|
+
const before = await page.getByTestId('retained-record').innerText();
|
|
121
|
+
await writeFile(join(output, `${name}-record.json`), before);
|
|
122
|
+
if (legacy === '--legacy') {
|
|
123
|
+
await save();
|
|
124
|
+
await expect(page.getByRole('alert')).toHaveText('Unable to save equipment.');
|
|
125
|
+
await expect(page.locator('#row-camera')).toHaveCount(0);
|
|
126
|
+
await page.screenshot({
|
|
127
|
+
path: join(output, `${name}-before.png`),
|
|
128
|
+
fullPage: true,
|
|
129
|
+
animations: 'disabled',
|
|
130
|
+
});
|
|
131
|
+
await browser.close();
|
|
132
|
+
console.log(`${name}: legacy failure reproduced; record captured`);
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const checked = [
|
|
137
|
+
'native template sign-in validation and request failure preserve input',
|
|
138
|
+
];
|
|
139
|
+
for (const container of ['page', 'dialog', 'drawer', 'detail-edit']) {
|
|
140
|
+
await open(container);
|
|
141
|
+
const routeBeforeFailure = page.url();
|
|
142
|
+
const title = page.getByLabel('Equipment name', { exact: true });
|
|
143
|
+
await title.fill('');
|
|
144
|
+
await save();
|
|
145
|
+
await expect(title).toBeFocused();
|
|
146
|
+
await inline(title);
|
|
147
|
+
expect(await title.getAttribute('aria-describedby')).toContain('name-help');
|
|
148
|
+
await title.fill('Retained equipment draft');
|
|
149
|
+
await page.getByLabel('Attachment', { exact: true }).setInputFiles({
|
|
150
|
+
name: 'equipment.txt',
|
|
151
|
+
mimeType: 'text/plain',
|
|
152
|
+
buffer: Buffer.from('attachment fixture'),
|
|
153
|
+
});
|
|
154
|
+
await save();
|
|
155
|
+
await expect(page.locator('#row-camera')).toBeFocused();
|
|
156
|
+
await inline(page.locator('#row-camera'));
|
|
157
|
+
await expect(
|
|
158
|
+
page.getByRole('tab', { name: 'Items', exact: true }),
|
|
159
|
+
).toHaveAttribute('aria-selected', 'true');
|
|
160
|
+
await expect(
|
|
161
|
+
page.getByRole('button', { name: 'Borrowed items', exact: true }),
|
|
162
|
+
).toHaveAttribute('aria-expanded', 'true');
|
|
163
|
+
await expect(title).toHaveValue('Retained equipment draft');
|
|
164
|
+
expect(
|
|
165
|
+
await page
|
|
166
|
+
.getByLabel('Attachment', { exact: true })
|
|
167
|
+
.evaluate((input) => input.files[0]?.name),
|
|
168
|
+
).toBe('equipment.txt');
|
|
169
|
+
await page.locator('#row-camera').fill('Corrected camera');
|
|
170
|
+
await expect(page.locator('#row-camera')).toBeFocused();
|
|
171
|
+
await expect(page.locator('#row-camera')).not.toHaveAttribute('aria-invalid');
|
|
172
|
+
await page.screenshot({
|
|
173
|
+
path: join(output, `${name}-${container}.png`),
|
|
174
|
+
fullPage: true,
|
|
175
|
+
animations: 'disabled',
|
|
176
|
+
});
|
|
177
|
+
expect(page.url()).toBe(routeBeforeFailure);
|
|
178
|
+
checked.push(
|
|
179
|
+
`${container}: native errors, region reveal, inline association, scroll/focus, input and file retention`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await open();
|
|
184
|
+
await delayedRequest(() =>
|
|
185
|
+
page.getByRole('button', { name: 'Reorder rows', exact: true }).click(),
|
|
186
|
+
);
|
|
187
|
+
await expect(page.locator('#row-tripod')).toBeFocused();
|
|
188
|
+
await inline(page.locator('#row-camera'));
|
|
189
|
+
checked.push('request snapshot follows stable rows after reordering');
|
|
190
|
+
await open();
|
|
191
|
+
await delayedRequest(() =>
|
|
192
|
+
page.getByRole('button', { name: 'Delete camera row', exact: true }).click(),
|
|
193
|
+
);
|
|
194
|
+
await expect(page.locator('#row-tripod')).toBeFocused();
|
|
195
|
+
await expect(page.locator('#row-camera')).toHaveCount(0);
|
|
196
|
+
checked.push('deleted rows do not receive response errors');
|
|
197
|
+
await open();
|
|
198
|
+
await page.getByRole('tab', { name: 'Items', exact: true }).click();
|
|
199
|
+
await page.getByRole('button', { name: 'Borrowed items', exact: true }).click();
|
|
200
|
+
await delayedRequest(() =>
|
|
201
|
+
page.locator('#row-camera').fill('Edited while request was pending'),
|
|
202
|
+
);
|
|
203
|
+
await expect(page.locator('#row-tripod')).toBeFocused();
|
|
204
|
+
await expect(page.locator('#row-camera')).not.toHaveAttribute('aria-invalid');
|
|
205
|
+
checked.push('edits during a request discard stale field errors');
|
|
206
|
+
|
|
207
|
+
await open();
|
|
208
|
+
await page.route('**/api/equipmentFixture', (route) =>
|
|
209
|
+
route.request().method() === 'POST'
|
|
210
|
+
? route.abort('failed')
|
|
211
|
+
: route.continue(),
|
|
212
|
+
);
|
|
213
|
+
await page.getByLabel('Response', { exact: true }).selectOption('success');
|
|
214
|
+
await save();
|
|
215
|
+
await expect(page.getByRole('alert')).toBeFocused();
|
|
216
|
+
await expect(page.getByRole('alert')).toContainText('Unable to connect');
|
|
217
|
+
await page.unroute('**/api/equipmentFixture');
|
|
218
|
+
checked.push(
|
|
219
|
+
'aborted HTTP transport preserves input and reports a connection failure',
|
|
220
|
+
);
|
|
221
|
+
for (const mode of ['unknown', 'denied', 'network']) {
|
|
222
|
+
await open();
|
|
223
|
+
await page.getByLabel('Response', { exact: true }).selectOption(mode);
|
|
224
|
+
await save();
|
|
225
|
+
await expect(page.getByRole('alert')).toBeFocused();
|
|
226
|
+
await expect(page.getByLabel('Equipment name', { exact: true })).toHaveValue(
|
|
227
|
+
'Equipment draft',
|
|
228
|
+
);
|
|
229
|
+
await expect(page.locator('body')).not.toContainText('Private developer');
|
|
230
|
+
await expect(page.locator('body')).not.toContainText('Secret salary');
|
|
231
|
+
checked.push(`${mode}: safe form error and retained input`);
|
|
232
|
+
}
|
|
233
|
+
await open();
|
|
234
|
+
await page
|
|
235
|
+
.getByRole('button', { name: 'Toggle field access', exact: true })
|
|
236
|
+
.click();
|
|
237
|
+
await save();
|
|
238
|
+
await expect(page.getByRole('alert')).toBeFocused();
|
|
239
|
+
await expect(page.locator('#row-camera')).toHaveCount(0);
|
|
240
|
+
await expect(page.getByRole('alert')).not.toContainText('Camera item');
|
|
241
|
+
await expect(
|
|
242
|
+
page.getByRole('tab', { name: 'Details', exact: true }),
|
|
243
|
+
).toHaveAttribute('aria-selected', 'true');
|
|
244
|
+
checked.push('inaccessible fields do not appear or open their region');
|
|
245
|
+
await open();
|
|
246
|
+
await page
|
|
247
|
+
.getByRole('button', { name: 'Toggle disabled', exact: true })
|
|
248
|
+
.click();
|
|
249
|
+
await save();
|
|
250
|
+
await expect(page.getByRole('alert')).toBeFocused();
|
|
251
|
+
await page.keyboard.press('Tab');
|
|
252
|
+
await expect(page.getByLabel('Equipment name', { exact: true })).toBeFocused();
|
|
253
|
+
checked.push('disabled fields fall back to a keyboard-accessible safe message');
|
|
254
|
+
await open();
|
|
255
|
+
await page.getByLabel('Response', { exact: true }).selectOption('business');
|
|
256
|
+
await save();
|
|
257
|
+
await expect(page.getByLabel('Department', { exact: true })).toBeFocused();
|
|
258
|
+
await inline(page.getByLabel('Department', { exact: true }));
|
|
259
|
+
await page.getByLabel('Department', { exact: true }).fill('Operations');
|
|
260
|
+
await page.getByLabel('Response', { exact: true }).selectOption('success');
|
|
261
|
+
await save();
|
|
262
|
+
await expect(page.getByRole('status')).toHaveText('Saved; workflow submitted.');
|
|
263
|
+
await expect(page.getByRole('alert')).toHaveCount(0);
|
|
264
|
+
await expect(page.getByTestId('workflow-status')).toContainText('submitted');
|
|
265
|
+
const submittedId = (
|
|
266
|
+
await page.getByTestId('workflow-status').innerText()
|
|
267
|
+
).split(':')[0];
|
|
268
|
+
await page
|
|
269
|
+
.getByRole('button', { name: 'Approve request', exact: true })
|
|
270
|
+
.click();
|
|
271
|
+
await expect(page.getByTestId('workflow-status')).toContainText('approved');
|
|
272
|
+
const persisted = await (
|
|
273
|
+
await page.request.get(`${base}api/equipmentFixture`)
|
|
274
|
+
).json();
|
|
275
|
+
expect(persisted.requests.find((row) => row.id === submittedId)?.workflow).toBe(
|
|
276
|
+
'approved',
|
|
277
|
+
);
|
|
278
|
+
expect(
|
|
279
|
+
persisted.history
|
|
280
|
+
.filter((row) => row.recordId === submittedId)
|
|
281
|
+
.map((row) => row.event)
|
|
282
|
+
.sort(),
|
|
283
|
+
).toEqual(['approved', 'submitted']);
|
|
284
|
+
await page.screenshot({
|
|
285
|
+
path: join(output, `${name}-approved-workflow.png`),
|
|
286
|
+
fullPage: true,
|
|
287
|
+
animations: 'disabled',
|
|
288
|
+
});
|
|
289
|
+
checked.push(
|
|
290
|
+
'real equipment submission and approval persist with both history events',
|
|
291
|
+
);
|
|
292
|
+
checked.push(
|
|
293
|
+
'trusted business copy and successful retry clear obsolete errors',
|
|
294
|
+
);
|
|
295
|
+
await open();
|
|
296
|
+
expect(await page.getByTestId('retained-record').innerText()).toBe(before);
|
|
297
|
+
await expect(page.getByTestId('custom-style')).toHaveCSS(
|
|
298
|
+
'font-family',
|
|
299
|
+
/serif/,
|
|
300
|
+
);
|
|
301
|
+
const restricted = await browser.newContext();
|
|
302
|
+
const member = {
|
|
303
|
+
email: 'restricted@example.test',
|
|
304
|
+
password: 'fixture-member-password',
|
|
305
|
+
};
|
|
306
|
+
const nativeHeaders = { origin: new URL(base).origin };
|
|
307
|
+
let signedIn = await restricted.request.post(`${base}api/auth/sign-in/email`, {
|
|
308
|
+
headers: nativeHeaders,
|
|
309
|
+
data: member,
|
|
310
|
+
});
|
|
311
|
+
if (!signedIn.ok()) {
|
|
312
|
+
const registered = await restricted.request.post(
|
|
313
|
+
`${base}api/auth/sign-up/email`,
|
|
314
|
+
{
|
|
315
|
+
headers: nativeHeaders,
|
|
316
|
+
data: {
|
|
317
|
+
...member,
|
|
318
|
+
username: 'restricted_fixture',
|
|
319
|
+
name: 'Restricted fixture member',
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
expect(registered.ok()).toBe(true);
|
|
324
|
+
signedIn = await restricted.request.post(`${base}api/auth/sign-in/email`, {
|
|
325
|
+
headers: nativeHeaders,
|
|
326
|
+
data: member,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
expect(signedIn.ok()).toBe(true);
|
|
330
|
+
const deniedRead = await restricted.request.get(`${base}api/equipmentFixture`);
|
|
331
|
+
expect(deniedRead.status()).toBe(403);
|
|
332
|
+
expect(await deniedRead.text()).not.toContain('Legacy camera');
|
|
333
|
+
const deniedWrite = await restricted.request.post(
|
|
334
|
+
`${base}api/equipmentFixture`,
|
|
335
|
+
{
|
|
336
|
+
headers: nativeHeaders,
|
|
337
|
+
data: {
|
|
338
|
+
mode: 'success',
|
|
339
|
+
name: 'Denied request',
|
|
340
|
+
department: 'Operations',
|
|
341
|
+
items: [],
|
|
342
|
+
attachment: '',
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
expect(deniedWrite.status()).toBe(403);
|
|
347
|
+
const restrictedPage = await restricted.newPage();
|
|
348
|
+
await restrictedPage.goto(`${base}equipment`, {
|
|
349
|
+
waitUntil: 'domcontentloaded',
|
|
350
|
+
});
|
|
351
|
+
await expect(
|
|
352
|
+
restrictedPage.getByRole('heading', { name: 'Access denied', exact: true }),
|
|
353
|
+
).toBeVisible();
|
|
354
|
+
await expect(
|
|
355
|
+
restrictedPage.getByLabel('Equipment name', { exact: true }),
|
|
356
|
+
).toHaveCount(0);
|
|
357
|
+
await restricted.close();
|
|
358
|
+
checked.push(
|
|
359
|
+
'native ordinary identity is denied both business reads and writes without record disclosure',
|
|
360
|
+
);
|
|
361
|
+
expect(failures).toEqual([]);
|
|
362
|
+
await writeFile(
|
|
363
|
+
join(output, `${name}-checks.json`),
|
|
364
|
+
JSON.stringify(
|
|
365
|
+
{ base, checked, recordPreserved: true, pageErrors: failures },
|
|
366
|
+
null,
|
|
367
|
+
2,
|
|
368
|
+
),
|
|
369
|
+
);
|
|
370
|
+
await browser.close();
|
|
371
|
+
console.log(
|
|
372
|
+
`${name}: ${checked.length} scenario groups passed; business record and custom style preserved`,
|
|
373
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Submission acceptance fixture
|
|
2
|
+
|
|
3
|
+
This is a synthetic equipment-borrowing module for disposable applications, using native application authentication. It adds an asset tag, `CUSTOM-` validation, a business permission gate, custom blue border, and a lifecycle approval transition whose transaction inserts an inventory entry. Never install these test-only routes into an application with real data; the permission switch exists only to exercise revocation. The real equipment-borrowing application is not required or accessed.
|
|
4
|
+
|
|
5
|
+
## Fresh application
|
|
6
|
+
|
|
7
|
+
Create the application with `pnpm create @nocobase/app submission-new --template=<target-template-directory-or-package> --no-install --json`, from a scratch working directory. Verify its `nocobase.templatePackage`, read its `AGENTS.md` and synchronized application Skills, and install the target's coordinated dependencies. For an unreleased source-workspace verification, link the generated application's `node_modules` to the target template's installed workspace modules; this exercises the pending source and is not a published-version certification. Do not run package-manager mutations through that shared link.
|
|
8
|
+
|
|
9
|
+
Copy `migration.ts` to `database/main/migrations/202610260099_submission_fixture.ts`, `routes.ts` to `server/routes/submission-fixture.ts`, and `page.tsx` to `client/pages/submission-fixture.tsx`. Register the server contribution in `server/routes/index.ts`, preserving the template's submission provider. Add an authenticated application route at `/submission-check`, with `authz: 'skip'` and a component loader for the fixture page. The fixture's business endpoints own their authentication and synthetic permission gate independently of that page setting.
|
|
10
|
+
|
|
11
|
+
Run `pnpm nocobase config init --dialect sqlite --json`, then start with `APP_PUBLIC_ORIGIN=http://127.0.0.1:<chosen-port> APP_SERVER_PORT=<chosen-port> I18N_DEFAULT_LOCALE=en-US pnpm dev`. Use the actual printed application entry URL, including its base path. Configuration initialization generates local secrets; do not publish its file or native session cookies.
|
|
12
|
+
|
|
13
|
+
## Version upgrade acceptance
|
|
14
|
+
|
|
15
|
+
For release acceptance, run this fixture through the shared [versioned template-upgrade procedure](../../../../../app/app-skills/skills/nocobase-app-upgrade/references/versioned-upgrade-verification.md). Start Default and Examples separately from published BASE tarballs and their original application Skills. Resolve an explicitly newer TARGET from the isolated candidate registry or the official registry, discover its instructions through the normal template diff, and coordinate dependencies and copied source within that same upgrade. Do not install target Skills or patch individual pages before starting the upgrade. Generate a fresh comparison application from each identical TARGET package.
|
|
16
|
+
|
|
17
|
+
Record exact BASE/TARGET versions, registry and tarball provenance, integrity, lockfile hashes, BASE-to-TARGET and BASE-to-PROJECT differences, resolved custom conflicts and the validated final baseline. Missing targets and unresolved conflicts must leave the baseline unchanged. Preserve the original business fields, validation, native roles and user assignments, lifecycle history, styling and rows. Candidate-package results and official-package results are separate acceptance records; repeat the matrix from real target packages after publication. Source-workspace checks below are useful specialist checks, but do not satisfy version upgrade acceptance.
|
|
18
|
+
|
|
19
|
+
The shared upgrade Skill, template entry documentation and versioned packaging procedure supply the common entry. This fixture supplies only the submission-specific business module, browser matrix and before/after evidence inputs. Integrate those inputs with the shared fixture rather than creating another packaging or upgrade mechanism.
|
|
20
|
+
|
|
21
|
+
## Customized source verification
|
|
22
|
+
|
|
23
|
+
Prepare a separate application from the BASE template, before this feature. Add the same custom business schema, validation, permission gate, style and lifecycle hook. For the pre-upgrade page, replace only the shared controller with a direct submit function and omit operation headers on Repository mutations. For its custom action call `runtime.fire` without the new outer receipt adapter. Start it on its own database, sign in through its own native login and reproduce two records from a double click while holding the first response. Preserve these synthetic record ids and complete JSON rows as the data baseline.
|
|
24
|
+
|
|
25
|
+
Stop the old application and commit its source baseline on a local branch. Back up its closed database and migration history. Reconcile BASE → TARGET and BASE → PROJECT through the [submission source-upgrade checklist](../../../../../app/app-skills/skills/nocobase-app-upgrade/references/submission-upgrade.md). Merge the provider/registration, receipt migration, three settings forms and client peer dependencies. For the customized page change only the controller and key forwarding; preserve the fields, validation and styling. For its custom server action add the receipt adapter around the existing lifecycle transition and join its transaction, preserving the permission check and inventory hook. Keep all business tables and records. Inspect `db apply --dry-run` and `db repair --dry-run`; this additive feature should add only the receipt migration with no old checksum changes. Apply it, restart the same application and retain its original database.
|
|
26
|
+
|
|
27
|
+
For an unreleased source target, report its commit and keep the existing release marker instead of inventing a published target version. For a published target, advance `defaultTemplateVersion` only after source reconciliation and validation. Restore the paired old source and database backup for recovery.
|
|
28
|
+
|
|
29
|
+
## Run the browser matrix
|
|
30
|
+
|
|
31
|
+
Set `SUBMISSION_APPLICATIONS` to a JSON array of `{ "name": "new-template", "url": "http://127.0.0.1:<port>/main" }` and `{ "name": "upgraded-customized", "url": "http://127.0.0.1:<other-port>/main", "beforeData": "<path-to-pre-upgrade-evidence-JSON>", "beforePermissions": "<path-to-pre-upgrade-native-permissions-JSON>" }`. `beforeData` contains the fixture's `{ rows, entries, history }`, captured from its authenticated evidence endpoint before upgrading. `beforePermissions` contains the persisted `borrowing-clerk` permission set, explicit user assignment and native role scopes captured before the upgrade; the second browser case compares these and signs in as that ordinary user to exercise allowed reads/updates and forbidden deletion/approval. Fresh applications create equivalent synthetic native permissions in that case. Write the environment setup to a local file when it spans multiple lines. Use the synthetic initial administrator from the disposable template; never reuse a management-instance identity or credential.
|
|
32
|
+
|
|
33
|
+
Run the application's local `pnpm exec playwright test -c tests/fixtures/submission/playwright.config.ts`. `PLAYWRIGHT_BROWSERS_PATH` must name an allowed scratch directory when installing a browser; use that same directory to run it. `SUBMISSION_EVIDENCE_DIR` selects a scratch output directory. The dedicated configuration keeps this opt-in fixture out of ordinary template browser tests. The test neither sleeps nor automatically retries failures.
|
|
34
|
+
|
|
35
|
+
Each application checks validation and custom styling, double click, Enter plus programmatic submit while a committed response is held, normal identical new records, update, a definite server rejection with retained input, response loss after a server-confirmed commit, unchanged keys and bodies on retry, refusal of changed input during an uncertain operation, permission revocation, and lifecycle approval with exactly one history entry and inventory effect. Every original row must remain identical. English submitting/error/success screenshots and JSON invariant summaries are attached to the Playwright result. The service-level suites separately cover parallel HTTP attempts, rollback, changed-content conflicts, expiration after cleanup, strict/compatibility modes and query costs on SQLite, PostgreSQL and MySQL.
|