cabloy 5.1.165 → 5.1.167
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/scheduled_tasks.lock +1 -1
- package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
- package/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/repo-docs/frontend/page-params-guide.md +1 -1
- package/repo-docs/frontend/page-query-guide.md +6 -0
- package/repo-docs/frontend/zod-guide.md +9 -7
- package/repo-e2e/specs/routed-dialog.spec.ts +368 -0
- package/vona/pnpm-lock.yaml +105 -32
- 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 +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateBeanGenerals.ts +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateScopeResources.ts +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +2026 -2065
- package/zova/pnpm-workspace.yaml +1 -1
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/index.ts +66 -18
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialog.ts +9 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogDetail.ts +23 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogEntry.ts +23 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialog/controller.tsx +301 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogDetail/controller.tsx +135 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogEntry/controller.tsx +108 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +19 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/component/routedDialog.ts +63 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/index.ts +44 -2
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +306 -18
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx +51 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/config.ts +19 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/lib/routedDialogHistory.ts +76 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/service/appModal.ts +156 -1
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +92 -13
- package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogHistory.test.ts +92 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogRouter.test.ts +35 -0
- 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/model/resource.ts +1 -1
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- 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/.metadata/index.ts +6 -6
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +60 -3
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +12 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/component/routerViewEmpty/controller.tsx +4 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routerViewBase.tsx +5 -1
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
- /package/vona/patches/{zova-core@5.1.88.patch → zova-core@5.1.89.patch} +0 -0
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.167
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sessionId":"
|
|
1
|
+
{"sessionId":"4dc366c1-f1a5-45c5-a2e1-5c89e5694587","pid":67516,"procStart":"Sat Sep 12 08:03:56 2026","acquiredAt":1789284193785}
|
|
@@ -165,6 +165,7 @@ Check whether the feature needs:
|
|
|
165
165
|
|
|
166
166
|
- page route review
|
|
167
167
|
- params/query schema alignment
|
|
168
|
+
- for numeric Zova page params and query fields, use `z.number()` and rely on the Cabloy/Zova route/query parse adapter; do not generalize this behavior to standalone Zod parsing or add manual coercion without a separate input-boundary requirement
|
|
168
169
|
- effective layout, authentication, and SSR-profile default resolution
|
|
169
170
|
- static-name and ordinary-alias exception review
|
|
170
171
|
- alias or guard review
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.167
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add routed dialog support.
|
|
8
|
+
- Add update capabilities.
|
|
9
|
+
- Add bean general generation support.
|
|
10
|
+
|
|
11
|
+
### Improvements
|
|
12
|
+
|
|
13
|
+
- Update resource handling.
|
|
14
|
+
- Update Zod documentation.
|
|
15
|
+
|
|
16
|
+
## 5.1.166
|
|
17
|
+
|
|
18
|
+
### Improvements
|
|
19
|
+
|
|
20
|
+
- Refresh the Zova lockfile and update the Vona Zova Core patch.
|
|
21
|
+
|
|
3
22
|
## 5.1.165
|
|
4
23
|
|
|
5
24
|
### Features
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ export const ControllerPageCounterSchemaParams = z.object({
|
|
|
26
26
|
|
|
27
27
|
This is more than a type annotation.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Route params arrive as strings at the URL level. On Zova's route/query parsing path, the Cabloy Zod query adapter converts a numeric value before the `z.number()` validation runs, so a successful `$params.id` value is a number. Use `z.number()` for numeric page params; `z.coerce.number()` is normally redundant here. For the exact scope and nil/error semantics, see [Zova route/query number parsing](./zod-guide#zova-routequery-number-parsing).
|
|
30
30
|
|
|
31
31
|
## Route record requirements
|
|
32
32
|
|
|
@@ -31,6 +31,12 @@ export const ControllerPageCounterSchemaQuery = z.object({
|
|
|
31
31
|
|
|
32
32
|
This matters because query parsing becomes typed and framework-aware instead of stringly-typed ad hoc access.
|
|
33
33
|
|
|
34
|
+
## Numeric query values
|
|
35
|
+
|
|
36
|
+
Query values are commonly strings at the URL boundary. On Zova's route/query parsing path, the Cabloy Zod query adapter converts a non-nil numeric value with `Number(...)` before ordinary `z.number()` validation. Use `z.number()` for numeric query fields such as `age`; when parsing succeeds, `this.$query.age` is a number.
|
|
37
|
+
|
|
38
|
+
Invalid numeric text still fails number validation rather than silently producing a usable value. `z.coerce.number()` is normally redundant for Zova page query schemas, but may be appropriate at a different input boundary that does not use the Zova adapter. See [Zova route/query number parsing](/frontend/zod-guide#zova-routequery-number-parsing) for the adapter's nil semantics and scope.
|
|
39
|
+
|
|
34
40
|
## Use query values
|
|
35
41
|
|
|
36
42
|
Representative pattern:
|
|
@@ -42,19 +42,21 @@ const user = z.object({
|
|
|
42
42
|
|
|
43
43
|
This is the foundation used by typed page params and page query support.
|
|
44
44
|
|
|
45
|
-
##
|
|
45
|
+
## Zova route/query number parsing
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
Cabloy Basic pins `zod` in `zova/pnpm-workspace.yaml` to `npm:@cabloy/zod@4.3.8`. Zova's route/query parsing path uses the Cabloy Zod query adapter to prepare URL values before ordinary Zod validation.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
That is why schemas such as:
|
|
49
|
+
For a non-nil numeric route or query value, the adapter applies `Number(value)` before `z.number()` validates it. Therefore, use `z.number()` for numeric fields in Zova page params and query schemas:
|
|
52
50
|
|
|
53
51
|
```typescript
|
|
54
|
-
z.
|
|
52
|
+
export const ControllerPageCounterSchemaQuery = z.object({
|
|
53
|
+
age: z.number().optional(),
|
|
54
|
+
});
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
When parsing succeeds, `this.$query.age` is a number. There is normally no need to add `z.coerce.number()` or a manual `Number(...)` conversion at this boundary.
|
|
58
|
+
|
|
59
|
+
The adapter preserves its nil semantics: an empty string, `'undefined'`, or `'null'` is handled as an absent or null value rather than as an ordinary number. Invalid numeric text becomes `NaN` and still fails normal number validation. This behavior belongs to Zova's route/query parsing path; do not infer it for an arbitrary standalone `Zod.parse` call. Use `z.coerce.number()` when handling a different input boundary that does not use the Zova adapter.
|
|
58
60
|
|
|
59
61
|
## Boolean coercion
|
|
60
62
|
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type { Locator, Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
import { expect, test } from '@playwright/test';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
__routedDialogHydrationWarnings?: unknown[];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function collectPageErrors(page: Page) {
|
|
12
|
+
const errors: Error[] = [];
|
|
13
|
+
page.on('pageerror', error => {
|
|
14
|
+
errors.push(error);
|
|
15
|
+
});
|
|
16
|
+
return errors;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function collectConsoleErrors(page: Page) {
|
|
20
|
+
const errors: string[] = [];
|
|
21
|
+
page.on('console', message => {
|
|
22
|
+
const text = message.text();
|
|
23
|
+
if (
|
|
24
|
+
message.type() === 'error' ||
|
|
25
|
+
(message.type() === 'warning' && /hydration (node |children )?mismatch/i.test(text))
|
|
26
|
+
) {
|
|
27
|
+
errors.push(`${message.type()}: ${text}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return errors;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function installHydrationWarningProbe(page: Page) {
|
|
34
|
+
await page.addInitScript(() => {
|
|
35
|
+
const originalWarn = console.warn;
|
|
36
|
+
const warnings: unknown[] = [];
|
|
37
|
+
Object.defineProperty(window, '__routedDialogHydrationWarnings', {
|
|
38
|
+
configurable: true,
|
|
39
|
+
value: warnings,
|
|
40
|
+
});
|
|
41
|
+
console.warn = (...args: unknown[]) => {
|
|
42
|
+
try {
|
|
43
|
+
if (
|
|
44
|
+
args.some(
|
|
45
|
+
arg => typeof arg === 'string' && /Hydration (node|children) mismatch/.test(arg),
|
|
46
|
+
)
|
|
47
|
+
) {
|
|
48
|
+
const node = args.find(arg => arg instanceof Node) as Node | undefined;
|
|
49
|
+
warnings.push({
|
|
50
|
+
args: args.map(arg => (typeof arg === 'string' ? arg : String(arg))),
|
|
51
|
+
node: node
|
|
52
|
+
? {
|
|
53
|
+
type: node.nodeType,
|
|
54
|
+
value: node.nodeValue,
|
|
55
|
+
parent: node.parentElement?.outerHTML.slice(0, 1000),
|
|
56
|
+
}
|
|
57
|
+
: undefined,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
originalWarn.apply(console, args as Parameters<typeof console.warn>);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function getHydrationWarningProbe(page: Page) {
|
|
68
|
+
return await page.evaluate(() => window.__routedDialogHydrationWarnings ?? []);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function dialogByTitle(page: Page, dialog: 'A' | 'B') {
|
|
72
|
+
return page.getByRole('dialog').filter({
|
|
73
|
+
has: page.getByRole('heading', {
|
|
74
|
+
name: `Routed Dialog ${dialog}`,
|
|
75
|
+
exact: true,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function routeRow(scope: Locator, name: string) {
|
|
81
|
+
return scope.locator('tbody tr').filter({ hasText: name }).last();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function expectRouteValue(scope: Locator, name: string, value: string, type?: string) {
|
|
85
|
+
const row = routeRow(scope, name);
|
|
86
|
+
await expect(row.getByRole('cell').nth(1)).toHaveText(value);
|
|
87
|
+
if (type) {
|
|
88
|
+
await expect(row.getByRole('cell').nth(2)).toHaveText(type);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function expectRoutePath(scope: Locator, pathname: string) {
|
|
93
|
+
const row = routeRow(scope, '$pageRoute.fullPath');
|
|
94
|
+
const fullPath = await row.getByRole('cell').nth(1).textContent();
|
|
95
|
+
expect(new URL(fullPath, 'http://e2e.invalid').pathname).toBe(pathname);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function expectNoDialogLayout(dialog: Locator) {
|
|
99
|
+
await expect(dialog.locator('.drawer')).toHaveCount(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function expectDialogMaxWidth(dialog: Locator, expected: string) {
|
|
103
|
+
await expect(dialog).toHaveCSS('max-width', expected);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function getDialogTop(dialog: Locator) {
|
|
107
|
+
return Math.round((await dialog.boundingBox())?.y ?? -1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function expectDialogTop(dialog: Locator, expected: number) {
|
|
111
|
+
await expect.poll(() => getDialogTop(dialog)).toBe(expected);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function openDemo(page: Page) {
|
|
115
|
+
const pageErrors = collectPageErrors(page);
|
|
116
|
+
const consoleErrors = collectConsoleErrors(page);
|
|
117
|
+
await installHydrationWarningProbe(page);
|
|
118
|
+
const response = await page.goto('/demo/basic/routedDialog', { waitUntil: 'load' });
|
|
119
|
+
expect(response?.ok()).toBeTruthy();
|
|
120
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
|
|
121
|
+
await expect(
|
|
122
|
+
page.getByRole('heading', { name: 'Routed Dialog Manual Validation', exact: true }),
|
|
123
|
+
).toBeVisible();
|
|
124
|
+
const browserUrl = page.url();
|
|
125
|
+
await expect(
|
|
126
|
+
page
|
|
127
|
+
.getByRole('heading', { name: 'Main route sentinel', exact: true })
|
|
128
|
+
.locator('..')
|
|
129
|
+
.getByText(browserUrl, { exact: true }),
|
|
130
|
+
).toBeVisible();
|
|
131
|
+
return { browserUrl, consoleErrors, pageErrors };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test(
|
|
135
|
+
'ATP-BASIC-ROUTED-DIALOG-01: a local memory router navigates without changing the browser route',
|
|
136
|
+
{ tag: ['@web', '@flow'] },
|
|
137
|
+
async ({ page }) => {
|
|
138
|
+
const { browserUrl, consoleErrors, pageErrors } = await openDemo(page);
|
|
139
|
+
|
|
140
|
+
await page.getByRole('button', { name: 'Open A', exact: true }).click();
|
|
141
|
+
const dialogA = dialogByTitle(page, 'A');
|
|
142
|
+
await expect(dialogA).toHaveCount(1);
|
|
143
|
+
await expect(
|
|
144
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
|
|
145
|
+
).toBeVisible();
|
|
146
|
+
await expectRoutePath(dialogA, '/demo/basic/routedDialogEntry');
|
|
147
|
+
await expectRouteValue(dialogA, '$query.dialog', 'A', 'string');
|
|
148
|
+
await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry', 'string');
|
|
149
|
+
await expectRouteValue(dialogA, '$query.via', 'open', 'string');
|
|
150
|
+
await expectRouteValue(dialogA, '$query.step', '0', 'number');
|
|
151
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
|
|
152
|
+
await expectNoDialogLayout(dialogA);
|
|
153
|
+
expect(page.url()).toBe(browserUrl);
|
|
154
|
+
|
|
155
|
+
const entryTop = await getDialogTop(dialogA);
|
|
156
|
+
await dialogA.getByRole('button', { name: 'Local push to detail', exact: true }).click();
|
|
157
|
+
await expect(
|
|
158
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
|
|
159
|
+
).toBeVisible();
|
|
160
|
+
await expectDialogTop(dialogA, entryTop);
|
|
161
|
+
await expectRoutePath(dialogA, '/demo/basic/routedDialogDetail/1');
|
|
162
|
+
await expectRouteValue(dialogA, '$params.id', '1', 'number');
|
|
163
|
+
await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry-push', 'string');
|
|
164
|
+
await expectRouteValue(dialogA, '$query.via', 'page-push', 'string');
|
|
165
|
+
await expectRouteValue(dialogA, '$query.step', '1', 'number');
|
|
166
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
|
|
167
|
+
expect(page.url()).toBe(browserUrl);
|
|
168
|
+
|
|
169
|
+
await dialogA.getByRole('button', { name: 'Local replace next detail', exact: true }).click();
|
|
170
|
+
await expectRoutePath(dialogA, '/demo/basic/routedDialogDetail/11');
|
|
171
|
+
await expectRouteValue(dialogA, '$params.id', '11', 'number');
|
|
172
|
+
await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry-push-replace', 'string');
|
|
173
|
+
await expectRouteValue(dialogA, '$query.via', 'detail-replace', 'string');
|
|
174
|
+
await expectRouteValue(dialogA, '$query.step', '11', 'number');
|
|
175
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
|
|
176
|
+
expect(page.url()).toBe(browserUrl);
|
|
177
|
+
|
|
178
|
+
await dialogA.getByRole('button', { name: 'Back', exact: true }).click();
|
|
179
|
+
await expect(
|
|
180
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
|
|
181
|
+
).toBeVisible();
|
|
182
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
|
|
183
|
+
await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry', 'string');
|
|
184
|
+
expect(page.url()).toBe(browserUrl);
|
|
185
|
+
|
|
186
|
+
await dialogA.getByRole('button', { name: 'Local push to detail', exact: true }).click();
|
|
187
|
+
await expect(
|
|
188
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
|
|
189
|
+
).toBeVisible();
|
|
190
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
|
|
191
|
+
await dialogA.getByRole('button', { name: 'Local router.back', exact: true }).click();
|
|
192
|
+
await expect(
|
|
193
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
|
|
194
|
+
).toBeVisible();
|
|
195
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
|
|
196
|
+
expect(page.url()).toBe(browserUrl);
|
|
197
|
+
|
|
198
|
+
await page.keyboard.press('Escape');
|
|
199
|
+
await expect(dialogA).toHaveCount(0);
|
|
200
|
+
expect(page.url()).toBe(browserUrl);
|
|
201
|
+
expect(pageErrors).toEqual([]);
|
|
202
|
+
expect(consoleErrors).toEqual([]);
|
|
203
|
+
expect(await getHydrationWarningProbe(page)).toEqual([]);
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
test(
|
|
208
|
+
'ATP-BASIC-ROUTED-DIALOG-02: routed-dialog handles push and replace locally',
|
|
209
|
+
{ tag: ['@web', '@flow'] },
|
|
210
|
+
async ({ page }) => {
|
|
211
|
+
const { browserUrl, consoleErrors, pageErrors } = await openDemo(page);
|
|
212
|
+
const dialogA = dialogByTitle(page, 'A');
|
|
213
|
+
|
|
214
|
+
await page.getByRole('button', { name: 'Open A with handle.push', exact: true }).click();
|
|
215
|
+
await expect(
|
|
216
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
|
|
217
|
+
).toBeVisible();
|
|
218
|
+
await expectRoutePath(dialogA, '/demo/basic/routedDialogDetail/101');
|
|
219
|
+
await expectRouteValue(dialogA, '$params.id', '101', 'number');
|
|
220
|
+
await expectRouteValue(dialogA, '$query.token', 'dialog-a-handle-push', 'string');
|
|
221
|
+
await expectRouteValue(dialogA, '$query.via', 'handle-push', 'string');
|
|
222
|
+
await expectRouteValue(dialogA, '$query.step', '1', 'number');
|
|
223
|
+
await expect(page.getByText('A: handle.push', { exact: true })).toBeVisible();
|
|
224
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
|
|
225
|
+
const handlePushTop = await getDialogTop(dialogA);
|
|
226
|
+
expect(page.url()).toBe(browserUrl);
|
|
227
|
+
|
|
228
|
+
await page.keyboard.press('Escape');
|
|
229
|
+
await expect(dialogA).toHaveCount(0);
|
|
230
|
+
expect(page.url()).toBe(browserUrl);
|
|
231
|
+
|
|
232
|
+
await page.getByRole('button', { name: 'Open A with handle.replace', exact: true }).click();
|
|
233
|
+
await expect(
|
|
234
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
|
|
235
|
+
).toBeVisible();
|
|
236
|
+
await expectRoutePath(dialogA, '/demo/basic/routedDialogDetail/202');
|
|
237
|
+
await expectRouteValue(dialogA, '$params.id', '202', 'number');
|
|
238
|
+
await expectRouteValue(dialogA, '$query.token', 'dialog-a-handle-replace', 'string');
|
|
239
|
+
await expectRouteValue(dialogA, '$query.via', 'handle-replace', 'string');
|
|
240
|
+
await expectRouteValue(dialogA, '$query.step', '2', 'number');
|
|
241
|
+
await expect(page.getByText('A: handle.replace', { exact: true })).toBeVisible();
|
|
242
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
|
|
243
|
+
await expectDialogTop(dialogA, handlePushTop);
|
|
244
|
+
expect(page.url()).toBe(browserUrl);
|
|
245
|
+
|
|
246
|
+
await page.keyboard.press('Escape');
|
|
247
|
+
await expect(dialogA).toHaveCount(0);
|
|
248
|
+
expect(page.url()).toBe(browserUrl);
|
|
249
|
+
expect(pageErrors).toEqual([]);
|
|
250
|
+
expect(consoleErrors).toEqual([]);
|
|
251
|
+
expect(await getHydrationWarningProbe(page)).toEqual([]);
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
test(
|
|
256
|
+
'ATP-BASIC-ROUTED-DIALOG-04: routed dialogs apply responsive and scalar maximum widths',
|
|
257
|
+
{ tag: ['@web', '@flow'] },
|
|
258
|
+
async ({ page }) => {
|
|
259
|
+
await page.setViewportSize({ width: 700, height: 900 });
|
|
260
|
+
const { browserUrl, consoleErrors, pageErrors } = await openDemo(page);
|
|
261
|
+
const dialogA = dialogByTitle(page, 'A');
|
|
262
|
+
|
|
263
|
+
await page.getByRole('button', { name: 'Open A', exact: true }).click();
|
|
264
|
+
await expect(dialogA).toHaveCount(1);
|
|
265
|
+
await expectDialogMaxWidth(dialogA, '640px');
|
|
266
|
+
await expectDialogTop(dialogA, 16);
|
|
267
|
+
|
|
268
|
+
await page.setViewportSize({ width: 900, height: 900 });
|
|
269
|
+
await expectDialogMaxWidth(dialogA, '768px');
|
|
270
|
+
await expectDialogTop(dialogA, 32);
|
|
271
|
+
|
|
272
|
+
await page.setViewportSize({ width: 1100, height: 900 });
|
|
273
|
+
await expectDialogMaxWidth(dialogA, '1024px');
|
|
274
|
+
await expectDialogTop(dialogA, 48);
|
|
275
|
+
await expectNoDialogLayout(dialogA);
|
|
276
|
+
expect(page.url()).toBe(browserUrl);
|
|
277
|
+
|
|
278
|
+
await page.keyboard.press('Escape');
|
|
279
|
+
await expect(dialogA).toHaveCount(0);
|
|
280
|
+
|
|
281
|
+
await page.getByRole('button', { name: 'Open A (720px width)', exact: true }).click();
|
|
282
|
+
await expect(dialogA).toHaveCount(1);
|
|
283
|
+
await expectDialogMaxWidth(dialogA, '720px');
|
|
284
|
+
await page.keyboard.press('Escape');
|
|
285
|
+
await expect(dialogA).toHaveCount(0);
|
|
286
|
+
|
|
287
|
+
await page.setViewportSize({ width: 900, height: 900 });
|
|
288
|
+
await page.getByRole('button', { name: 'Open A (md 800px width)', exact: true }).click();
|
|
289
|
+
await expect(dialogA).toHaveCount(1);
|
|
290
|
+
await expectDialogMaxWidth(dialogA, '800px');
|
|
291
|
+
await expectDialogTop(dialogA, 32);
|
|
292
|
+
await page.setViewportSize({ width: 700, height: 900 });
|
|
293
|
+
await expectDialogMaxWidth(dialogA, '640px');
|
|
294
|
+
await expectDialogTop(dialogA, 16);
|
|
295
|
+
await page.keyboard.press('Escape');
|
|
296
|
+
await expect(dialogA).toHaveCount(0);
|
|
297
|
+
|
|
298
|
+
await page.setViewportSize({ width: 900, height: 900 });
|
|
299
|
+
await page.getByRole('button', { name: 'Open A (80px top gutter)', exact: true }).click();
|
|
300
|
+
await expect(dialogA).toHaveCount(1);
|
|
301
|
+
await expectDialogTop(dialogA, 80);
|
|
302
|
+
await page.keyboard.press('Escape');
|
|
303
|
+
await expect(dialogA).toHaveCount(0);
|
|
304
|
+
|
|
305
|
+
await page.getByRole('button', { name: 'Open A (md 64px top gutter)', exact: true }).click();
|
|
306
|
+
await expect(dialogA).toHaveCount(1);
|
|
307
|
+
await expectDialogTop(dialogA, 64);
|
|
308
|
+
await page.setViewportSize({ width: 700, height: 900 });
|
|
309
|
+
await expectDialogTop(dialogA, 16);
|
|
310
|
+
expect(page.url()).toBe(browserUrl);
|
|
311
|
+
|
|
312
|
+
await page.keyboard.press('Escape');
|
|
313
|
+
await expect(dialogA).toHaveCount(0);
|
|
314
|
+
expect(pageErrors).toEqual([]);
|
|
315
|
+
expect(consoleErrors).toEqual([]);
|
|
316
|
+
expect(await getHydrationWarningProbe(page)).toEqual([]);
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
test(
|
|
321
|
+
'ATP-BASIC-ROUTED-DIALOG-03: concurrent dialogs keep isolated routes and close last-opened first',
|
|
322
|
+
{ tag: ['@web', '@flow'] },
|
|
323
|
+
async ({ page }) => {
|
|
324
|
+
const { browserUrl, consoleErrors, pageErrors } = await openDemo(page);
|
|
325
|
+
|
|
326
|
+
await page.getByRole('button', { name: 'Open A and B', exact: true }).click();
|
|
327
|
+
const dialogA = dialogByTitle(page, 'A');
|
|
328
|
+
const dialogB = dialogByTitle(page, 'B');
|
|
329
|
+
await expect(dialogA).toHaveCount(1);
|
|
330
|
+
await expect(dialogB).toHaveCount(1);
|
|
331
|
+
await expect(
|
|
332
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
|
|
333
|
+
).toBeVisible();
|
|
334
|
+
await expect(
|
|
335
|
+
dialogB.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
|
|
336
|
+
).toBeVisible();
|
|
337
|
+
await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry', 'string');
|
|
338
|
+
await expectRouteValue(dialogB, '$query.token', 'dialog-b-entry', 'string');
|
|
339
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
|
|
340
|
+
await expect(dialogB.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
|
|
341
|
+
|
|
342
|
+
await dialogB.getByRole('button', { name: 'Local push to detail', exact: true }).click();
|
|
343
|
+
await expect(
|
|
344
|
+
dialogB.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
|
|
345
|
+
).toBeVisible();
|
|
346
|
+
await expectRoutePath(dialogB, '/demo/basic/routedDialogDetail/1');
|
|
347
|
+
await expectRouteValue(dialogB, '$query.token', 'dialog-b-entry-push', 'string');
|
|
348
|
+
await expect(dialogB.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
|
|
349
|
+
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
|
|
350
|
+
await expect(
|
|
351
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
|
|
352
|
+
).toBeVisible();
|
|
353
|
+
await expectRouteValue(dialogA, '$query.token', 'dialog-a-entry', 'string');
|
|
354
|
+
expect(page.url()).toBe(browserUrl);
|
|
355
|
+
|
|
356
|
+
await page.keyboard.press('Escape');
|
|
357
|
+
await expect(dialogB).toHaveCount(0);
|
|
358
|
+
await expect(dialogA).toHaveCount(1);
|
|
359
|
+
expect(page.url()).toBe(browserUrl);
|
|
360
|
+
|
|
361
|
+
await page.keyboard.press('Escape');
|
|
362
|
+
await expect(dialogA).toHaveCount(0);
|
|
363
|
+
expect(page.url()).toBe(browserUrl);
|
|
364
|
+
expect(pageErrors).toEqual([]);
|
|
365
|
+
expect(consoleErrors).toEqual([]);
|
|
366
|
+
expect(await getHydrationWarningProbe(page)).toEqual([]);
|
|
367
|
+
},
|
|
368
|
+
);
|