cabloy 5.1.172 → 5.1.174
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/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/repo-docs/frontend/component-guide.md +33 -0
- package/repo-docs/frontend/model-resource-cookbook.md +0 -8
- package/repo-docs/frontend/model-resource-usage-guide.md +0 -4
- package/repo-e2e/specs/cabloy-basic.spec.ts +158 -0
- package/repo-e2e/specs/controller-boundary.spec.ts +261 -0
- package/test-results/.last-run.json +1 -1
- package/vona/packages-utils/lint/package.json +1 -1
- package/vona/packages-utils/lint/src/oxc/lint.ts +1 -1
- package/vona/packages-utils/lint/src/oxc/lintVue.ts +1 -1
- package/vona/pnpm-lock.yaml +238 -46
- package/vona/pnpm-workspace.yaml +1 -1
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +3 -3
- package/zova/packages-utils/babel-plugin-zova-behavior/package.json +2 -1
- package/zova/packages-utils/babel-plugin-zova-behavior/src/index.ts +21 -8
- package/zova/packages-utils/babel-plugin-zova-behavior/test/behaviorSlots.test.ts +111 -0
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-utils/zova-vite/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +1 -1
- package/zova/packages-zova/zova-core/src/bean/beanBase.ts +1 -1
- package/zova/packages-zova/zova-core/src/bean/beanContainer.ts +95 -39
- package/zova/packages-zova/zova-core/src/bean/resource/error/type.ts +9 -0
- package/zova/packages-zova/zova-core/src/bean/type.ts +18 -0
- package/zova/packages-zova/zova-core/src/components/component.ts +2 -1
- package/zova/packages-zova/zova-core/src/composables/useController.ts +155 -25
- package/zova/packages-zova/zova-core/src/core/component/error.ts +62 -5
- package/zova/packages-zova/zova-core/src/core/context/component.ts +37 -2
- package/zova/packages-zova/zova-core/src/core/context/hooks.ts +13 -2
- package/zova/packages-zova/zova-core/src/core/context/meta.ts +1 -0
- package/zova/packages-zova/zova-core/src/core/context/state.ts +161 -0
- package/zova/packages-zova/zova-core/src/core/sys/config.ts +32 -7
- package/zova/packages-zova/zova-core/src/types/interface/monkey.ts +15 -0
- package/zova/packages-zova/zova-core/src/types/utils/powerPartial.ts +1 -1
- package/zova/pnpm-lock.yaml +40 -25
- package/zova/src/front/config/config/config.ts +11 -0
- package/zova/src/front/config/config/render.tsx +79 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +0 -1
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/component/controllerBoundaryProbe.ts +31 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/component/controllerBoundaryProbeInline.ts +34 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/component/controllerBoundaryProbeOverride.ts +34 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/index.ts +58 -1
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/controllerBoundary.ts +23 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/component/controllerBoundaryProbe/controller.tsx +36 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/component/controllerBoundaryProbeInline/controller.tsx +34 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/component/controllerBoundaryProbeOverride/controller.tsx +46 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/page/controllerBoundary/controller.tsx +185 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +6 -0
- package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-training/modules/training-student/src/model/student.ts +0 -3
- package/zova/src/suite/cabloy-basic/modules/basic-button/package.json +58 -0
- package/zova/src/suite/cabloy-basic/modules/basic-button/src/.metadata/component/button.ts +31 -0
- package/zova/src/suite/cabloy-basic/modules/basic-button/src/.metadata/index.ts +122 -0
- package/zova/src/suite/cabloy-basic/modules/basic-button/src/.metadata/this.ts +2 -0
- package/zova/src/suite/cabloy-basic/modules/basic-button/src/bean/behavior.perform.tsx +103 -0
- package/zova/src/suite/cabloy-basic/modules/basic-button/src/component/button/controller.tsx +39 -0
- package/zova/src/suite/cabloy-basic/modules/basic-button/src/index.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-button/tsconfig.build.json +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-button/tsconfig.json +5 -0
- package/zova/src/suite/cabloy-basic/modules/basic-details/package.json +2 -1
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionDelete.tsx +4 -4
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +5 -1
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +5 -1
- package/zova/src/suite/cabloy-basic/modules/basic-table/package.json +2 -1
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/bean/tableCell.actionDelete.tsx +4 -4
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionRefresh/controller.tsx +10 -8
- package/zova/src/suite/cabloy-basic/package.json +1 -0
- package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/model/layoutprofile.ts +0 -3
- 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/.metadata/page/resource.ts +6 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +0 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resource/controller.tsx +5 -1
- package/zova/src/suite-vendor/a-cabloy/package.json +3 -3
- package/zova/src/suite-vendor/a-zova/modules/a-bean/cli/controller/metadata/generateFilePage.ts +3 -1
- package/zova/src/suite-vendor/a-zova/modules/a-bean/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useMutation.ts +1 -11
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +1 -17
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +2 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +2 -7
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/mutation.types.ts +14 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryEnsureLoaded.test.ts +56 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkey.ts +8 -0
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +85 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/monkey.ts +25 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/types/ssr.ts +15 -1
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +6 -6
- /package/vona/patches/{zova-core@5.1.89.patch → zova-core@5.1.90.patch} +0 -0
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.174
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.174
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- Refresh dependency lockfiles and core patches to prepare for compensation reruns.
|
|
8
|
+
|
|
9
|
+
## 5.1.173
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- Consolidate boundary handling and retry behavior.
|
|
14
|
+
|
|
3
15
|
## 5.1.172
|
|
4
16
|
|
|
5
17
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cabloy",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.174",
|
|
4
4
|
"gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
|
|
5
5
|
"description": "A Node.js fullstack framework",
|
|
6
6
|
"keywords": [
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@cabloy/dotenv": "^1.2.8",
|
|
79
|
-
"@cabloy/lint": "^5.1.
|
|
79
|
+
"@cabloy/lint": "^5.1.33",
|
|
80
80
|
"@cabloy/process-helper": "^3.1.3",
|
|
81
81
|
"@playwright/test": "^1.62.1",
|
|
82
82
|
"@types/fs-extra": "^11.0.4",
|
|
@@ -78,6 +78,39 @@ Component wrappers can also participate in asynchronous loading behavior automat
|
|
|
78
78
|
|
|
79
79
|
This matters because the wrapper is not just a naming convenience. It is part of how Zova turns modular code into practical runtime behavior.
|
|
80
80
|
|
|
81
|
+
## Controller load boundaries
|
|
82
|
+
|
|
83
|
+
A component Controller, optional Style bean, and optional Render bean load in that order. While that asynchronous work is pending, Zova can render a loading boundary; an unhandled initialization failure renders an error boundary.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
@Controller()
|
|
87
|
+
class ControllerReport extends BeanControllerBase {
|
|
88
|
+
static $componentOptions: IComponentOptions = {
|
|
89
|
+
boundary: {
|
|
90
|
+
loading: { delay: 500 },
|
|
91
|
+
renderMode: 'inline',
|
|
92
|
+
retry: true,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`retry` is disabled by default. Enable it only when creating a fresh Controller → Style → Render graph and running the Controller initialization again is safe. In particular, do not opt in for initialization that can repeat a non-idempotent external effect without its own idempotency or recovery contract.
|
|
99
|
+
|
|
100
|
+
Error rendering keeps Controller ownership first. A Controller override may receive the retry action as its trailing argument; otherwise the application boundary renderer receives it:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
protected renderError(error, renderMode, retry?) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
config.boundary.renderError(ctx, error, renderMode, retry?);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The callback is optional because retry may be disabled or unavailable. Existing error renderers that accept only `error` or `error, renderMode` remain valid. When a Controller provides its own error renderer, Zova does not wrap that output with an application retry control; the Controller decides whether to present the action.
|
|
111
|
+
|
|
112
|
+
For SSR, a server-selected error fallback remains the client’s first hydration result. Retry becomes interactive only after the component has mounted, so the server HTML and hydration-time client output stay equivalent. A retry then creates a fresh client-side load graph; it does not rerun the failed Controller instance in place.
|
|
113
|
+
|
|
81
114
|
## Reference the component instance
|
|
82
115
|
|
|
83
116
|
Instead of relying on template refs in the usual Vue style, Zova prefers direct access to the component controller instance.
|
|
@@ -86,10 +86,6 @@ export class ModelStudent extends BeanModelBase {
|
|
|
86
86
|
const res = await this.scope.api.trainingStudent.summary({ params: { id } });
|
|
87
87
|
return res ?? null;
|
|
88
88
|
},
|
|
89
|
-
meta: {
|
|
90
|
-
// The Summary dialog renders query errors itself.
|
|
91
|
-
disableErrorEffect: true,
|
|
92
|
-
},
|
|
93
89
|
});
|
|
94
90
|
}
|
|
95
91
|
|
|
@@ -187,10 +183,6 @@ summary(id: TableIdentity) {
|
|
|
187
183
|
const res = await this.scope.api.trainingStudent.summary({ params: { id } });
|
|
188
184
|
return res ?? null;
|
|
189
185
|
},
|
|
190
|
-
meta: {
|
|
191
|
-
// The Summary dialog renders query errors itself.
|
|
192
|
-
disableErrorEffect: true,
|
|
193
|
-
},
|
|
194
186
|
});
|
|
195
187
|
}
|
|
196
188
|
```
|
|
@@ -128,10 +128,6 @@ export class ModelStudent extends BeanModelBase {
|
|
|
128
128
|
const res = await this.scope.api.trainingStudent.summary({ params: { id } });
|
|
129
129
|
return res ?? null;
|
|
130
130
|
},
|
|
131
|
-
meta: {
|
|
132
|
-
// The Summary dialog renders query errors itself.
|
|
133
|
-
disableErrorEffect: true,
|
|
134
|
-
},
|
|
135
131
|
});
|
|
136
132
|
}
|
|
137
133
|
|
|
@@ -563,6 +563,164 @@ test(
|
|
|
563
563
|
},
|
|
564
564
|
);
|
|
565
565
|
|
|
566
|
+
test(
|
|
567
|
+
'ATP-BASIC-TABLE-03: Training Student row delete handles cancellation, local failures, and retry',
|
|
568
|
+
{ tag: ['@admin', '@flow'] },
|
|
569
|
+
async ({ page }) => {
|
|
570
|
+
test.setTimeout(120_000);
|
|
571
|
+
page.setDefaultTimeout(10_000);
|
|
572
|
+
await page.setViewportSize({ width: 1440, height: 900 });
|
|
573
|
+
const pageErrors = collectPageErrors(page);
|
|
574
|
+
await loginAsAdmin(page);
|
|
575
|
+
|
|
576
|
+
const studentName = `Row Delete E2E ${Date.now()}`;
|
|
577
|
+
const accessToken = (await page.context().cookies()).find(
|
|
578
|
+
cookie => cookie.name === 'token',
|
|
579
|
+
)?.value;
|
|
580
|
+
expect(accessToken).toBeTruthy();
|
|
581
|
+
const headers = { Authorization: `Bearer ${accessToken}` };
|
|
582
|
+
let studentId: string | number | undefined;
|
|
583
|
+
try {
|
|
584
|
+
const createResponse = await page.request.post('/api/training/student', {
|
|
585
|
+
data: { name: studentName, mobile: '13812345678', level: 1 },
|
|
586
|
+
headers,
|
|
587
|
+
});
|
|
588
|
+
expect(createResponse.ok()).toBeTruthy();
|
|
589
|
+
studentId = (await createResponse.json()).data;
|
|
590
|
+
expect(['string', 'number']).toContain(typeof studentId);
|
|
591
|
+
|
|
592
|
+
const initialSelect = waitForStudentSelect(page);
|
|
593
|
+
await page.getByRole('link', { name: 'Student', exact: true }).click();
|
|
594
|
+
await expect(page).toHaveURL(studentResourceUrl);
|
|
595
|
+
await initialSelect;
|
|
596
|
+
|
|
597
|
+
await page.getByLabel('Student Name').fill(studentName);
|
|
598
|
+
const filteredResponse = waitForStudentSelect(page);
|
|
599
|
+
await page.getByRole('button', { name: 'Search', exact: true }).click();
|
|
600
|
+
await filteredResponse;
|
|
601
|
+
|
|
602
|
+
const row = page.locator('tbody tr').filter({
|
|
603
|
+
has: page.getByRole('link', { name: studentName, exact: true }),
|
|
604
|
+
});
|
|
605
|
+
const deleteButton = row.locator('button.btn-outline.btn-error').first();
|
|
606
|
+
const confirmDialog = page.getByRole('dialog').filter({
|
|
607
|
+
hasText: 'Are you sure you want to delete this item?',
|
|
608
|
+
});
|
|
609
|
+
await expect(row).toHaveCount(1);
|
|
610
|
+
await expect(deleteButton).toBeVisible();
|
|
611
|
+
|
|
612
|
+
let cancelledRequestCount = 0;
|
|
613
|
+
const cancelledRequestListener = (request: Request) => {
|
|
614
|
+
const url = new URL(request.url());
|
|
615
|
+
if (
|
|
616
|
+
request.method() === 'DELETE' &&
|
|
617
|
+
url.pathname === `/api/training/student/${studentId}` &&
|
|
618
|
+
!request.headers()['x-vona-openapi-schema']
|
|
619
|
+
) {
|
|
620
|
+
cancelledRequestCount++;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
page.on('request', cancelledRequestListener);
|
|
624
|
+
try {
|
|
625
|
+
await deleteButton.click();
|
|
626
|
+
await expect(confirmDialog).toBeVisible();
|
|
627
|
+
await confirmDialog.getByRole('button', { name: 'No', exact: true }).click();
|
|
628
|
+
await expect(confirmDialog).toHaveCount(0);
|
|
629
|
+
await expect(row).toHaveCount(1);
|
|
630
|
+
expect(cancelledRequestCount).toBe(0);
|
|
631
|
+
} finally {
|
|
632
|
+
page.off('request', cancelledRequestListener);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
let failureRequestCount = 0;
|
|
636
|
+
let releaseFailedDelete: (() => void) | undefined;
|
|
637
|
+
const failedDeleteHeld = new Promise<void>(resolve => {
|
|
638
|
+
releaseFailedDelete = resolve;
|
|
639
|
+
});
|
|
640
|
+
const deleteRoute = (url: URL) => url.pathname === `/api/training/student/${studentId}`;
|
|
641
|
+
const failedDeleteHandler = async (route: Route) => {
|
|
642
|
+
failureRequestCount++;
|
|
643
|
+
await failedDeleteHeld;
|
|
644
|
+
await route.fulfill({
|
|
645
|
+
status: 500,
|
|
646
|
+
contentType: 'application/json',
|
|
647
|
+
body: JSON.stringify({ code: 500, message: 'Row deletion temporarily unavailable' }),
|
|
648
|
+
});
|
|
649
|
+
};
|
|
650
|
+
await page.route(deleteRoute, failedDeleteHandler);
|
|
651
|
+
try {
|
|
652
|
+
await deleteButton.click();
|
|
653
|
+
await confirmDialog.getByRole('button', { name: 'Yes', exact: true }).click();
|
|
654
|
+
await expect.poll(() => failureRequestCount).toBe(1);
|
|
655
|
+
await expect(deleteButton).toBeDisabled();
|
|
656
|
+
await expect(deleteButton).toHaveAttribute('aria-busy', 'true');
|
|
657
|
+
await expect(deleteButton.locator('.loading.loading-spinner')).toHaveCount(1);
|
|
658
|
+
await deleteButton.click({ force: true });
|
|
659
|
+
await expect.poll(() => failureRequestCount).toBe(1);
|
|
660
|
+
|
|
661
|
+
releaseFailedDelete?.();
|
|
662
|
+
const errorDialog = page.getByRole('dialog').filter({
|
|
663
|
+
hasText: 'Row deletion temporarily unavailable',
|
|
664
|
+
});
|
|
665
|
+
await expect(errorDialog).toBeVisible();
|
|
666
|
+
await expect(errorDialog.getByRole('button', { name: 'Close', exact: true })).toBeVisible();
|
|
667
|
+
await expect(row).toHaveCount(1);
|
|
668
|
+
await errorDialog.getByRole('button', { name: 'Close', exact: true }).click();
|
|
669
|
+
await expect(errorDialog).toHaveCount(0);
|
|
670
|
+
await expect(deleteButton).toBeEnabled();
|
|
671
|
+
await expect(deleteButton).not.toHaveAttribute('aria-busy', 'true');
|
|
672
|
+
await expect(deleteButton.locator('.loading.loading-spinner')).toHaveCount(0);
|
|
673
|
+
} finally {
|
|
674
|
+
releaseFailedDelete?.();
|
|
675
|
+
await page.unroute(deleteRoute, failedDeleteHandler);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const deleteRequest = page.waitForRequest(request => {
|
|
679
|
+
const url = new URL(request.url());
|
|
680
|
+
return (
|
|
681
|
+
request.method() === 'DELETE' &&
|
|
682
|
+
url.pathname === `/api/training/student/${studentId}` &&
|
|
683
|
+
!request.headers()['x-vona-openapi-schema']
|
|
684
|
+
);
|
|
685
|
+
});
|
|
686
|
+
const deleteResponse = page.waitForResponse(response => {
|
|
687
|
+
const url = new URL(response.url());
|
|
688
|
+
return (
|
|
689
|
+
response.request().method() === 'DELETE' &&
|
|
690
|
+
url.pathname === `/api/training/student/${studentId}` &&
|
|
691
|
+
!response.request().headers()['x-vona-openapi-schema']
|
|
692
|
+
);
|
|
693
|
+
});
|
|
694
|
+
const refetchResponse = waitForStudentSelect(page, false);
|
|
695
|
+
await deleteButton.click();
|
|
696
|
+
await confirmDialog.getByRole('button', { name: 'Yes', exact: true }).click();
|
|
697
|
+
const [, response, refetch] = await Promise.all([
|
|
698
|
+
deleteRequest,
|
|
699
|
+
deleteResponse,
|
|
700
|
+
refetchResponse,
|
|
701
|
+
]);
|
|
702
|
+
expect(response.ok()).toBeTruthy();
|
|
703
|
+
expect(refetch.ok()).toBeTruthy();
|
|
704
|
+
await expect(row).toHaveCount(0);
|
|
705
|
+
expect(pageErrors).toEqual([]);
|
|
706
|
+
} finally {
|
|
707
|
+
if (studentId !== undefined) {
|
|
708
|
+
const deleteResponse = await page.request.delete(
|
|
709
|
+
`/api/training/student/deleteForce/${studentId}`,
|
|
710
|
+
{
|
|
711
|
+
headers,
|
|
712
|
+
timeout: 10_000,
|
|
713
|
+
},
|
|
714
|
+
);
|
|
715
|
+
expect(
|
|
716
|
+
deleteResponse.ok() || deleteResponse.status() === 404,
|
|
717
|
+
`Failed to clean up Training Student ${studentName} (${studentId}): ${deleteResponse.status()}`,
|
|
718
|
+
).toBeTruthy();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
);
|
|
723
|
+
|
|
566
724
|
test(
|
|
567
725
|
'ATP-BASIC-TABLE-02: Training Student selection mode preserves page state and bulk deletes selected rows',
|
|
568
726
|
{ tag: ['@admin', '@flow'] },
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
import { expect, test } from '@playwright/test';
|
|
4
|
+
|
|
5
|
+
const probeErrorMessage = 'Controller boundary probe initialization failed';
|
|
6
|
+
|
|
7
|
+
function collectPageErrors(page: Page) {
|
|
8
|
+
const errors: Error[] = [];
|
|
9
|
+
page.on('pageerror', error => {
|
|
10
|
+
errors.push(error);
|
|
11
|
+
});
|
|
12
|
+
return errors;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function collectConsoleErrors(page: Page) {
|
|
16
|
+
const errors: string[] = [];
|
|
17
|
+
page.on('console', message => {
|
|
18
|
+
if (message.type() === 'error') {
|
|
19
|
+
errors.push(message.text());
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return errors;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test(
|
|
26
|
+
'ATP-BASIC-CONTROLLER-BOUNDARY-01: global boundaries render loading, error, and recovery',
|
|
27
|
+
{ tag: ['@web', '@flow'] },
|
|
28
|
+
async ({ page, request }) => {
|
|
29
|
+
const serverResponse = await request.get('/demo/basic/controllerBoundary');
|
|
30
|
+
expect(serverResponse.ok()).toBeTruthy();
|
|
31
|
+
const serverHtml = await serverResponse.text();
|
|
32
|
+
expect(serverHtml).not.toContain('Controller boundary probe ready');
|
|
33
|
+
expect(serverHtml).not.toContain(probeErrorMessage);
|
|
34
|
+
|
|
35
|
+
const pageErrors = collectPageErrors(page);
|
|
36
|
+
const consoleErrors = collectConsoleErrors(page);
|
|
37
|
+
const response = await page.goto('/demo/basic/controllerBoundary', { waitUntil: 'load' });
|
|
38
|
+
expect(response?.ok()).toBeTruthy();
|
|
39
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
|
|
40
|
+
await expect(
|
|
41
|
+
page.getByRole('heading', { name: 'Controller Boundary Manual Validation', exact: true }),
|
|
42
|
+
).toBeVisible();
|
|
43
|
+
await page.waitForTimeout(1_000);
|
|
44
|
+
|
|
45
|
+
const loading = page.getByRole('status');
|
|
46
|
+
const error = page.locator('.alert.alert-error');
|
|
47
|
+
const ready = page.getByText('Controller boundary probe ready', { exact: true });
|
|
48
|
+
|
|
49
|
+
await expect(loading).toHaveCount(0);
|
|
50
|
+
await expect(error).toHaveCount(0);
|
|
51
|
+
await expect(ready).toHaveCount(0);
|
|
52
|
+
|
|
53
|
+
await page.getByRole('button', { name: 'Mount fast success probe', exact: true }).click();
|
|
54
|
+
await page.waitForTimeout(400);
|
|
55
|
+
await expect(loading).toHaveCount(0);
|
|
56
|
+
await expect(ready).toBeVisible();
|
|
57
|
+
await expect(error).toHaveCount(0);
|
|
58
|
+
|
|
59
|
+
await page.getByRole('button', { name: 'Mount fast failing probe', exact: true }).click();
|
|
60
|
+
await page.waitForTimeout(400);
|
|
61
|
+
await expect(error).toHaveText(probeErrorMessage);
|
|
62
|
+
await expect(loading).toHaveCount(0);
|
|
63
|
+
await expect(ready).toHaveCount(0);
|
|
64
|
+
|
|
65
|
+
const delayedSuccessLoading = loading.waitFor({ state: 'visible' });
|
|
66
|
+
await page.getByRole('button', { name: 'Mount delayed success probe', exact: true }).click();
|
|
67
|
+
await delayedSuccessLoading;
|
|
68
|
+
await expect(ready).toBeVisible();
|
|
69
|
+
await expect(loading).toHaveCount(0);
|
|
70
|
+
await expect(error).toHaveCount(0);
|
|
71
|
+
|
|
72
|
+
const failingProbeLoading = loading.waitFor({ state: 'visible' });
|
|
73
|
+
await page.getByRole('button', { name: 'Mount failing probe', exact: true }).click();
|
|
74
|
+
await failingProbeLoading;
|
|
75
|
+
await expect.poll(() => loading.evaluate(element => element.tagName)).toBe('DIV');
|
|
76
|
+
await expect(error).toHaveText(probeErrorMessage);
|
|
77
|
+
await expect.poll(() => error.evaluate(element => element.tagName)).toBe('DIV');
|
|
78
|
+
await expect(ready).toHaveCount(0);
|
|
79
|
+
const retry = page.getByRole('button', { name: 'Retry', exact: true });
|
|
80
|
+
await expect(retry).toBeVisible();
|
|
81
|
+
|
|
82
|
+
const failedRetryLoading = loading.waitFor({ state: 'visible' });
|
|
83
|
+
await retry.click();
|
|
84
|
+
await failedRetryLoading;
|
|
85
|
+
await expect(error).toHaveText(probeErrorMessage);
|
|
86
|
+
await expect(retry).toBeVisible();
|
|
87
|
+
|
|
88
|
+
await page.getByRole('button', { name: 'Prepare probe retry success', exact: true }).click();
|
|
89
|
+
const successfulRetryLoading = loading.waitFor({ state: 'visible' });
|
|
90
|
+
await retry.evaluate(button => {
|
|
91
|
+
button.click();
|
|
92
|
+
button.click();
|
|
93
|
+
});
|
|
94
|
+
await successfulRetryLoading;
|
|
95
|
+
await expect(ready).toBeVisible();
|
|
96
|
+
await expect(loading).toHaveCount(0);
|
|
97
|
+
await expect(error).toHaveCount(0);
|
|
98
|
+
|
|
99
|
+
expect(pageErrors).toEqual([]);
|
|
100
|
+
expect(consoleErrors.every(error => error.includes(probeErrorMessage))).toBeTruthy();
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
test(
|
|
105
|
+
'ATP-BASIC-CONTROLLER-BOUNDARY-02: inline boundaries preserve phrasing-content markup',
|
|
106
|
+
{ tag: ['@web', '@flow'] },
|
|
107
|
+
async ({ page }) => {
|
|
108
|
+
const pageErrors = collectPageErrors(page);
|
|
109
|
+
const consoleErrors = collectConsoleErrors(page);
|
|
110
|
+
const response = await page.goto('/demo/basic/controllerBoundary', { waitUntil: 'load' });
|
|
111
|
+
expect(response?.ok()).toBeTruthy();
|
|
112
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
|
|
113
|
+
await page.waitForTimeout(1_000);
|
|
114
|
+
|
|
115
|
+
const inlineHost = page.locator('p', { hasText: 'Inline boundary:' });
|
|
116
|
+
const loading = inlineHost.getByRole('status');
|
|
117
|
+
const error = inlineHost.getByRole('alert');
|
|
118
|
+
const ready = inlineHost.getByText('Inline Controller boundary probe ready', { exact: true });
|
|
119
|
+
await page.getByRole('button', { name: 'Mount inline failing probe', exact: true }).click();
|
|
120
|
+
await expect(error).toHaveText('Inline Controller boundary probe initialization failed');
|
|
121
|
+
await expect.poll(() => error.evaluate(element => element.tagName)).toBe('SPAN');
|
|
122
|
+
await expect(ready).toHaveCount(0);
|
|
123
|
+
await expect(inlineHost.locator('div')).toHaveCount(0);
|
|
124
|
+
await expect(inlineHost.getByRole('button', { name: 'Retry', exact: true })).toBeVisible();
|
|
125
|
+
|
|
126
|
+
await page.getByRole('button', { name: 'Prepare inline retry success', exact: true }).click();
|
|
127
|
+
await inlineHost.getByRole('button', { name: 'Retry', exact: true }).click();
|
|
128
|
+
await expect(ready).toBeVisible();
|
|
129
|
+
await expect(loading).toHaveCount(0);
|
|
130
|
+
await expect(error).toHaveCount(0);
|
|
131
|
+
|
|
132
|
+
expect(pageErrors).toEqual([]);
|
|
133
|
+
expect(
|
|
134
|
+
consoleErrors.every(error =>
|
|
135
|
+
error.includes('Inline Controller boundary probe initialization failed'),
|
|
136
|
+
),
|
|
137
|
+
).toBeTruthy();
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
test(
|
|
142
|
+
'ATP-BASIC-CONTROLLER-BOUNDARY-03: Controller overrides receive the mode and retain legacy error arity',
|
|
143
|
+
{ tag: ['@web', '@flow'] },
|
|
144
|
+
async ({ page }) => {
|
|
145
|
+
const pageErrors = collectPageErrors(page);
|
|
146
|
+
const consoleErrors = collectConsoleErrors(page);
|
|
147
|
+
const response = await page.goto('/demo/basic/controllerBoundary', { waitUntil: 'load' });
|
|
148
|
+
expect(response?.ok()).toBeTruthy();
|
|
149
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
|
|
150
|
+
await page.waitForTimeout(1_000);
|
|
151
|
+
|
|
152
|
+
const overrideHost = page.locator('p', { hasText: 'Override boundary:' });
|
|
153
|
+
const loading = overrideHost.getByRole('status');
|
|
154
|
+
const error = overrideHost.getByRole('alert');
|
|
155
|
+
const ready = overrideHost.getByText('Override Controller boundary probe ready', {
|
|
156
|
+
exact: true,
|
|
157
|
+
});
|
|
158
|
+
const initialOverrideLoading = loading.waitFor({ state: 'visible' });
|
|
159
|
+
await page.getByRole('button', { name: 'Mount override failing probe', exact: true }).click();
|
|
160
|
+
await initialOverrideLoading;
|
|
161
|
+
await expect(loading).toHaveText('Override loading mode: inline');
|
|
162
|
+
await expect(error).toHaveText(
|
|
163
|
+
'Override legacy error: Override Controller boundary probe initialization failed',
|
|
164
|
+
);
|
|
165
|
+
await expect(ready).toHaveCount(0);
|
|
166
|
+
await expect(overrideHost.getByRole('button', { name: 'Retry', exact: true })).toHaveCount(0);
|
|
167
|
+
|
|
168
|
+
const recoveredOverrideLoading = loading.waitFor({ state: 'visible' });
|
|
169
|
+
await page
|
|
170
|
+
.getByRole('button', { name: 'Recover override with delayed success', exact: true })
|
|
171
|
+
.click();
|
|
172
|
+
await recoveredOverrideLoading;
|
|
173
|
+
await expect(loading).toHaveText('Override loading mode: inline');
|
|
174
|
+
await expect(ready).toBeVisible();
|
|
175
|
+
await expect(loading).toHaveCount(0);
|
|
176
|
+
await expect(error).toHaveCount(0);
|
|
177
|
+
|
|
178
|
+
expect(pageErrors).toEqual([]);
|
|
179
|
+
expect(
|
|
180
|
+
consoleErrors.every(error =>
|
|
181
|
+
error.includes('Override Controller boundary probe initialization failed'),
|
|
182
|
+
),
|
|
183
|
+
).toBeTruthy();
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
test(
|
|
188
|
+
'ATP-BASIC-CONTROLLER-BOUNDARY-04: inline server load failure remains hydration-safe',
|
|
189
|
+
{ tag: ['@web', '@flow'] },
|
|
190
|
+
async ({ page, request }) => {
|
|
191
|
+
const errorMessage = 'Inline Controller boundary probe initialization failed';
|
|
192
|
+
const serverResponse = await request.get(
|
|
193
|
+
'/demo/basic/controllerBoundary?mode=inlineServerFailure',
|
|
194
|
+
);
|
|
195
|
+
expect(serverResponse.ok()).toBeTruthy();
|
|
196
|
+
const serverHtml = await serverResponse.text();
|
|
197
|
+
expect(serverHtml).toContain(errorMessage);
|
|
198
|
+
expect(serverHtml).not.toContain('Inline Controller boundary probe ready');
|
|
199
|
+
expect(serverHtml).not.toContain('>Retry<');
|
|
200
|
+
|
|
201
|
+
const pageErrors = collectPageErrors(page);
|
|
202
|
+
const consoleErrors = collectConsoleErrors(page);
|
|
203
|
+
const response = await page.goto('/demo/basic/controllerBoundary?mode=inlineServerFailure', {
|
|
204
|
+
waitUntil: 'load',
|
|
205
|
+
});
|
|
206
|
+
expect(response?.ok()).toBeTruthy();
|
|
207
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
|
|
208
|
+
await page.waitForTimeout(1_000);
|
|
209
|
+
const inlineHost = page.locator('p', { hasText: 'Inline boundary:' });
|
|
210
|
+
const error = inlineHost.getByRole('alert');
|
|
211
|
+
await expect(error).toHaveText(errorMessage);
|
|
212
|
+
await expect.poll(() => error.evaluate(element => element.tagName)).toBe('SPAN');
|
|
213
|
+
await expect(
|
|
214
|
+
inlineHost.getByText('Inline Controller boundary probe ready', { exact: true }),
|
|
215
|
+
).toHaveCount(0);
|
|
216
|
+
const retry = inlineHost.getByRole('button', { name: 'Retry', exact: true });
|
|
217
|
+
await expect(retry).toBeVisible();
|
|
218
|
+
|
|
219
|
+
await retry.click();
|
|
220
|
+
await expect(
|
|
221
|
+
inlineHost.getByText('Inline Controller boundary probe ready', { exact: true }),
|
|
222
|
+
).toBeVisible();
|
|
223
|
+
await expect(error).toHaveCount(0);
|
|
224
|
+
|
|
225
|
+
expect(pageErrors).toEqual([]);
|
|
226
|
+
expect(consoleErrors.every(error => error.includes(errorMessage))).toBeTruthy();
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
test(
|
|
231
|
+
'ATP-BASIC-CONTROLLER-BOUNDARY-05: server load failure remains the initial hydration boundary',
|
|
232
|
+
{ tag: ['@web', '@flow'] },
|
|
233
|
+
async ({ page, request }) => {
|
|
234
|
+
const serverResponse = await request.get('/demo/basic/controllerBoundary?mode=serverFailure');
|
|
235
|
+
expect(serverResponse.ok()).toBeTruthy();
|
|
236
|
+
const serverHtml = await serverResponse.text();
|
|
237
|
+
expect(serverHtml).toContain(probeErrorMessage);
|
|
238
|
+
expect(serverHtml).not.toContain('Controller boundary probe ready');
|
|
239
|
+
expect(serverHtml).not.toContain('>Retry<');
|
|
240
|
+
|
|
241
|
+
const pageErrors = collectPageErrors(page);
|
|
242
|
+
const consoleErrors = collectConsoleErrors(page);
|
|
243
|
+
const response = await page.goto('/demo/basic/controllerBoundary?mode=serverFailure', {
|
|
244
|
+
waitUntil: 'load',
|
|
245
|
+
});
|
|
246
|
+
expect(response?.ok()).toBeTruthy();
|
|
247
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
|
|
248
|
+
await page.waitForTimeout(1_000);
|
|
249
|
+
await expect(page.locator('.alert.alert-error')).toHaveText(probeErrorMessage);
|
|
250
|
+
await expect(page.getByText('Controller boundary probe ready', { exact: true })).toHaveCount(0);
|
|
251
|
+
const retry = page.getByRole('button', { name: 'Retry', exact: true });
|
|
252
|
+
await expect(retry).toBeVisible();
|
|
253
|
+
|
|
254
|
+
await retry.click();
|
|
255
|
+
await expect(page.getByText('Controller boundary probe ready', { exact: true })).toBeVisible();
|
|
256
|
+
await expect(page.locator('.alert.alert-error')).toHaveCount(0);
|
|
257
|
+
|
|
258
|
+
expect(pageErrors).toEqual([]);
|
|
259
|
+
expect(consoleErrors.every(error => error.includes(probeErrorMessage))).toBeTruthy();
|
|
260
|
+
},
|
|
261
|
+
);
|