cabloy 5.1.151 → 5.1.153
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/.cabloy-version +1 -1
- package/.claude/skills/cabloy-module-removal/SKILL.md +1 -1
- package/.claude/skills/cabloy-resource-field-update/SKILL.md +1 -1
- package/.claude/skills/cabloy-spec-generation/SKILL.md +1 -1
- package/.claude/skills/cabloy-spec-generation/references/repo-aware-discovery.md +1 -1
- package/.claude/skills/cabloy-workflow/SKILL.md +13 -14
- package/.claude/skills/cabloy-workflow/evals/evals.json +4 -4
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +3 -3
- package/package.json +1 -1
- package/repo-docs/.vitepress/config.mjs +5 -0
- package/repo-docs/ai/cli-to-skill-map.md +19 -0
- package/repo-docs/ai/docs-skills-rules-mapping.md +21 -6
- package/repo-docs/ai/future-skill-roadmap.md +17 -72
- package/repo-docs/ai/introduction.md +15 -5
- package/repo-docs/ai/playbook-spec-execution.md +144 -0
- package/repo-docs/ai/playbook-spec-generation.md +155 -0
- package/repo-docs/ai/repo-guidance.md +9 -5
- package/repo-docs/ai/rules-and-config.md +2 -2
- package/repo-docs/ai/skills.md +4 -0
- package/repo-docs/ai/virtual-decorator-guidance.md +1 -1
- package/repo-docs/backend/introduction.md +1 -1
- package/repo-docs/frontend/model-resource-cookbook.md +38 -7
- package/repo-docs/frontend/model-resource-usage-guide.md +2 -1
- package/repo-docs/frontend/router-tabs-introduction.md +1 -1
- package/repo-docs/frontend/use-state-data-best-practices.md +39 -6
- package/repo-docs/reference/package-map.md +4 -1
- package/repo-docs/reference/repo-scripts.md +25 -0
- package/repo-e2e/specs/cabloy-basic.spec.ts +69 -20
- package/test-results/.last-run.json +4 -2
- package/test-results/cabloy-basic-ATP-BASIC-SUM-58d8a-ry-states-and-Markdown-HTML/error-context.md +195 -0
- package/vona/pnpm-lock.yaml +81 -145
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +3 -2
- package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +29 -6
- package/zova/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-training/modules/training-student/src/model/student.ts +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/mutationSuccess.ts +11 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +12 -7
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/mutationSuccess.test.ts +95 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
|
@@ -414,7 +414,7 @@ test(
|
|
|
414
414
|
);
|
|
415
415
|
|
|
416
416
|
test(
|
|
417
|
-
'ATP-BASIC-SUMMARY-01: Training Student summary renders
|
|
417
|
+
'ATP-BASIC-SUMMARY-01: Training Student summary dialog renders query states and Markdown HTML',
|
|
418
418
|
{ tag: ['@admin', '@flow'] },
|
|
419
419
|
async ({ page }) => {
|
|
420
420
|
const pageErrors = collectPageErrors(page);
|
|
@@ -466,25 +466,74 @@ test(
|
|
|
466
466
|
await page.getByRole('button', { name: 'Search', exact: true }).click();
|
|
467
467
|
await searchResponse;
|
|
468
468
|
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
469
|
+
const summaryPath = `/api/training/student/summary/${studentId}`;
|
|
470
|
+
let summaryMode: 'failure' | 'success' = 'failure';
|
|
471
|
+
let releaseSummaryRequest: (() => void) | undefined;
|
|
472
|
+
let summaryRequest: Promise<void> | undefined;
|
|
473
|
+
let releaseSummaryResponse: (() => void) | undefined;
|
|
474
|
+
let summaryResponseRelease: Promise<void> | undefined;
|
|
475
|
+
const summaryHandler = async (route: Route) => {
|
|
476
|
+
releaseSummaryRequest?.();
|
|
477
|
+
if (summaryMode === 'failure') {
|
|
478
|
+
await route.fulfill({
|
|
479
|
+
status: 500,
|
|
480
|
+
contentType: 'application/json',
|
|
481
|
+
body: JSON.stringify({ code: 500, message: 'Summary temporarily unavailable' }),
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const response = await route.fetch();
|
|
486
|
+
await summaryResponseRelease;
|
|
487
|
+
await route.fulfill({ response });
|
|
488
|
+
};
|
|
489
|
+
const summaryRouteUrl = (url: URL) => url.pathname === summaryPath;
|
|
490
|
+
await page.route(summaryRouteUrl, summaryHandler);
|
|
491
|
+
try {
|
|
492
|
+
let row = page.locator('tr').filter({ hasText: studentName });
|
|
493
|
+
await expect(row).toHaveCount(1);
|
|
494
|
+
await row.getByRole('button', { name: 'Summary', exact: true }).click();
|
|
495
|
+
|
|
496
|
+
const dialog = page.getByRole('dialog');
|
|
497
|
+
await expect(dialog).toBeVisible();
|
|
498
|
+
await expect(dialog.locator('.alert.alert-error')).toHaveCount(1);
|
|
499
|
+
await expect(dialog.locator('.student-summary-description')).toHaveCount(0);
|
|
500
|
+
await page.keyboard.press('Escape');
|
|
501
|
+
await expect(dialog).toHaveCount(0);
|
|
502
|
+
|
|
503
|
+
summaryMode = 'success';
|
|
504
|
+
summaryRequest = new Promise(resolve => {
|
|
505
|
+
releaseSummaryRequest = resolve;
|
|
506
|
+
});
|
|
507
|
+
summaryResponseRelease = new Promise(resolve => {
|
|
508
|
+
releaseSummaryResponse = resolve;
|
|
509
|
+
});
|
|
510
|
+
const listResponse = await page.goto('/admin/rest/resource/training-student%3Astudent', {
|
|
511
|
+
waitUntil: 'load',
|
|
512
|
+
});
|
|
513
|
+
expect(listResponse?.ok()).toBeTruthy();
|
|
514
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
515
|
+
await page.getByLabel('Student Name').fill(studentName);
|
|
516
|
+
const nextSearchResponse = waitForStudentSelect(page);
|
|
517
|
+
await page.getByRole('button', { name: 'Search', exact: true }).click();
|
|
518
|
+
await nextSearchResponse;
|
|
519
|
+
|
|
520
|
+
row = page.locator('tr').filter({ hasText: studentName });
|
|
521
|
+
await expect(row).toHaveCount(1);
|
|
522
|
+
await row.getByRole('button', { name: 'Summary', exact: true }).click();
|
|
523
|
+
await summaryRequest;
|
|
524
|
+
await expect(dialog).toHaveCount(0, { timeout: 250 });
|
|
525
|
+
releaseSummaryResponse?.();
|
|
526
|
+
|
|
527
|
+
const description = dialog.locator('.student-summary-description');
|
|
528
|
+
await expect(dialog).toBeVisible();
|
|
529
|
+
await expect(description).toBeVisible();
|
|
530
|
+
await expect(description.locator('h2')).toHaveText('Summary heading');
|
|
531
|
+
await expect(description).toContainText(`Summary paragraph ${studentName}`);
|
|
532
|
+
expect(pageErrors).toEqual([]);
|
|
533
|
+
} finally {
|
|
534
|
+
releaseSummaryResponse?.();
|
|
535
|
+
await page.unroute(summaryRouteUrl, summaryHandler);
|
|
536
|
+
}
|
|
488
537
|
} finally {
|
|
489
538
|
try {
|
|
490
539
|
if (studentId !== undefined) {
|
package/test-results/cabloy-basic-ATP-BASIC-SUM-58d8a-ry-states-and-Markdown-HTML/error-context.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Instructions
|
|
2
|
+
|
|
3
|
+
- Following Playwright test failed.
|
|
4
|
+
- Explain why, be concise, respect Playwright best practices.
|
|
5
|
+
- Provide a snippet of code with the fix, if possible.
|
|
6
|
+
|
|
7
|
+
# Test info
|
|
8
|
+
|
|
9
|
+
- Name: cabloy-basic.spec.ts >> ATP-BASIC-SUMMARY-01: Training Student summary dialog renders query states and Markdown HTML
|
|
10
|
+
- Location: repo-e2e/specs/cabloy-basic.spec.ts:416:1
|
|
11
|
+
|
|
12
|
+
# Error details
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Error: expect(page).not.toHaveURL(expected) failed
|
|
16
|
+
|
|
17
|
+
Expected pattern: not /\/admin\/login(?:\?|$)/
|
|
18
|
+
Received string: "http://127.0.0.1:7102/admin/login"
|
|
19
|
+
Timeout: 5000ms
|
|
20
|
+
|
|
21
|
+
Call log:
|
|
22
|
+
- Expect "not toHaveURL" with timeout 5000ms
|
|
23
|
+
14 × locator resolved to <html lang="en-us" data-zova-hydrated="admin">…</html>
|
|
24
|
+
- unexpected value "http://127.0.0.1:7102/admin/login"
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
- heading "Zova" [level=1]
|
|
30
|
+
- heading "Less is more, while more is less" [level=5]
|
|
31
|
+
- heading "Login" [level=2]
|
|
32
|
+
- textbox:
|
|
33
|
+
- /placeholder: Your Username
|
|
34
|
+
- text: admin
|
|
35
|
+
- textbox:
|
|
36
|
+
- /placeholder: Your Password
|
|
37
|
+
- text: "123456"
|
|
38
|
+
- textbox:
|
|
39
|
+
- /placeholder: Please input captcha
|
|
40
|
+
- text: TrAs
|
|
41
|
+
- img
|
|
42
|
+
- button "Login"
|
|
43
|
+
- button "Create account"
|
|
44
|
+
- button "Forgot password?"
|
|
45
|
+
- text: OR
|
|
46
|
+
- button "Sign in with GitHub"
|
|
47
|
+
- dialog:
|
|
48
|
+
- heading "Cabloy Basic" [level=3]
|
|
49
|
+
- paragraph: "select `aAuthProvider`.`id` from `aAuthProvider` where `providerName` = 'auth-simple:simple' and `clientName` = 'default' and `aAuthProvider`.`iid` = 1 - no such table: aAuthProvider"
|
|
50
|
+
- button "Close"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
# Test source
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
1 | import type { Locator, Page, Route } from '@playwright/test';
|
|
57
|
+
2 |
|
|
58
|
+
3 | import { expect, test } from '@playwright/test';
|
|
59
|
+
4 |
|
|
60
|
+
5 | const studentResourceUrl =
|
|
61
|
+
6 | /\/admin\/rest\/resource\/training-student(?:%3A|:|%253A)student(?:[/?#]|$)/;
|
|
62
|
+
7 |
|
|
63
|
+
8 | interface IFieldGeometry {
|
|
64
|
+
9 | container: {
|
|
65
|
+
10 | display: string;
|
|
66
|
+
11 | flexWrap: string;
|
|
67
|
+
12 | left: number;
|
|
68
|
+
13 | right: number;
|
|
69
|
+
14 | };
|
|
70
|
+
15 | field: {
|
|
71
|
+
16 | top: number;
|
|
72
|
+
17 | left: number;
|
|
73
|
+
18 | right: number;
|
|
74
|
+
19 | bottom: number;
|
|
75
|
+
20 | };
|
|
76
|
+
21 | }
|
|
77
|
+
22 |
|
|
78
|
+
23 | function collectPageErrors(page: Page) {
|
|
79
|
+
24 | const errors: Error[] = [];
|
|
80
|
+
25 | page.on('pageerror', error => {
|
|
81
|
+
26 | errors.push(error);
|
|
82
|
+
27 | });
|
|
83
|
+
28 | return errors;
|
|
84
|
+
29 | }
|
|
85
|
+
30 |
|
|
86
|
+
31 | async function loginAsAdmin(page: Page) {
|
|
87
|
+
32 | await page.goto('/admin/', { waitUntil: 'load' });
|
|
88
|
+
33 | if (page.url().includes('/admin/login')) {
|
|
89
|
+
34 | await page.getByPlaceholder('Your Username').fill('admin');
|
|
90
|
+
35 | await page.getByPlaceholder('Your Password').fill('123456');
|
|
91
|
+
36 | await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
|
|
92
|
+
37 | await page.getByRole('button', { name: 'Login', exact: true }).click();
|
|
93
|
+
> 38 | await expect(page).not.toHaveURL(/\/admin\/login(?:\?|$)/);
|
|
94
|
+
| ^ Error: expect(page).not.toHaveURL(expected) failed
|
|
95
|
+
39 | }
|
|
96
|
+
40 |
|
|
97
|
+
41 | await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
98
|
+
42 | const dashboardResponse = await page.goto('/admin/', { waitUntil: 'load' });
|
|
99
|
+
43 | await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
100
|
+
44 | return dashboardResponse;
|
|
101
|
+
45 | }
|
|
102
|
+
46 |
|
|
103
|
+
47 | function waitForStudentSelect(page: Page) {
|
|
104
|
+
48 | return page.waitForResponse(response => {
|
|
105
|
+
49 | const url = new URL(response.url());
|
|
106
|
+
50 | return (
|
|
107
|
+
51 | response.request().method() === 'GET' &&
|
|
108
|
+
52 | response.ok() &&
|
|
109
|
+
53 | url.pathname === '/api/training/student' &&
|
|
110
|
+
54 | !response.request().headers()['x-vona-openapi-schema']
|
|
111
|
+
55 | );
|
|
112
|
+
56 | });
|
|
113
|
+
57 | }
|
|
114
|
+
58 |
|
|
115
|
+
59 | function installStudentCreateResponseCapture(page: Page) {
|
|
116
|
+
60 | let studentId: string | number | undefined;
|
|
117
|
+
61 | const handler = async (route: Route) => {
|
|
118
|
+
62 | const request = route.request();
|
|
119
|
+
63 | const url = new URL(request.url());
|
|
120
|
+
64 | if (
|
|
121
|
+
65 | request.method() !== 'POST' ||
|
|
122
|
+
66 | url.pathname !== '/api/training/student' ||
|
|
123
|
+
67 | request.headers()['x-vona-openapi-schema']
|
|
124
|
+
68 | ) {
|
|
125
|
+
69 | await route.continue();
|
|
126
|
+
70 | return;
|
|
127
|
+
71 | }
|
|
128
|
+
72 | const response = await route.fetch();
|
|
129
|
+
73 | const payload = await response.json();
|
|
130
|
+
74 | if (response.ok() && ['string', 'number'].includes(typeof payload.data)) {
|
|
131
|
+
75 | studentId = payload.data;
|
|
132
|
+
76 | }
|
|
133
|
+
77 | await route.fulfill({ response });
|
|
134
|
+
78 | };
|
|
135
|
+
79 | return {
|
|
136
|
+
80 | async install() {
|
|
137
|
+
81 | await page.route('**/api/training/student', handler);
|
|
138
|
+
82 | },
|
|
139
|
+
83 | async uninstall() {
|
|
140
|
+
84 | await page.unroute('**/api/training/student', handler);
|
|
141
|
+
85 | },
|
|
142
|
+
86 | get studentId() {
|
|
143
|
+
87 | return studentId;
|
|
144
|
+
88 | },
|
|
145
|
+
89 | };
|
|
146
|
+
90 | }
|
|
147
|
+
91 |
|
|
148
|
+
92 | async function getFieldGeometry(locator: Locator): Promise<IFieldGeometry> {
|
|
149
|
+
93 | return locator.evaluate(element => {
|
|
150
|
+
94 | const field = element.closest('label')?.parentElement;
|
|
151
|
+
95 | const container = field?.parentElement;
|
|
152
|
+
96 | if (!field || !container) throw new Error('Could not resolve the filter field layout');
|
|
153
|
+
97 |
|
|
154
|
+
98 | const fieldRect = field.getBoundingClientRect();
|
|
155
|
+
99 | const containerRect = container.getBoundingClientRect();
|
|
156
|
+
100 | const containerStyle = getComputedStyle(container);
|
|
157
|
+
101 | return {
|
|
158
|
+
102 | container: {
|
|
159
|
+
103 | display: containerStyle.display,
|
|
160
|
+
104 | flexWrap: containerStyle.flexWrap,
|
|
161
|
+
105 | left: containerRect.left,
|
|
162
|
+
106 | right: containerRect.right,
|
|
163
|
+
107 | },
|
|
164
|
+
108 | field: {
|
|
165
|
+
109 | top: fieldRect.top,
|
|
166
|
+
110 | left: fieldRect.left,
|
|
167
|
+
111 | right: fieldRect.right,
|
|
168
|
+
112 | bottom: fieldRect.bottom,
|
|
169
|
+
113 | },
|
|
170
|
+
114 | };
|
|
171
|
+
115 | });
|
|
172
|
+
116 | }
|
|
173
|
+
117 |
|
|
174
|
+
118 | async function getFlowGeometry(name: Locator, level: Locator, createdAtStart: Locator) {
|
|
175
|
+
119 | return Promise.all([
|
|
176
|
+
120 | getFieldGeometry(name),
|
|
177
|
+
121 | getFieldGeometry(level),
|
|
178
|
+
122 | getFieldGeometry(createdAtStart),
|
|
179
|
+
123 | ]);
|
|
180
|
+
124 | }
|
|
181
|
+
125 |
|
|
182
|
+
126 | function assertVisualOrder(geometry: IFieldGeometry[]) {
|
|
183
|
+
127 | for (let index = 1; index < geometry.length; index++) {
|
|
184
|
+
128 | const previous = geometry[index - 1].field;
|
|
185
|
+
129 | const current = geometry[index].field;
|
|
186
|
+
130 | expect(
|
|
187
|
+
131 | current.top > previous.top + 2 ||
|
|
188
|
+
132 | (Math.abs(current.top - previous.top) <= 2 && current.left > previous.left),
|
|
189
|
+
133 | ).toBeTruthy();
|
|
190
|
+
134 | }
|
|
191
|
+
135 | }
|
|
192
|
+
136 |
|
|
193
|
+
137 | async function getDocumentHorizontalOverflow(page: Page) {
|
|
194
|
+
138 | return page.evaluate(() => {
|
|
195
|
+
```
|