cabloy 5.1.154 → 5.1.155

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 CHANGED
@@ -1 +1 @@
1
- 5.1.154
1
+ 5.1.155
@@ -0,0 +1 @@
1
+ {"sessionId":"834e10b3-c7de-4e46-8255-347f58ac99c9","pid":21283,"procStart":"Tue Sep 1 21:20:05 2026","acquiredAt":1788301241462}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.155
4
+
5
+ ### Features
6
+
7
+ - Add support for REST resource keys.
8
+
3
9
  ## 5.1.154
4
10
 
5
11
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.154",
3
+ "version": "5.1.155",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -1,6 +1,4 @@
1
1
  {
2
- "status": "failed",
3
- "failedTests": [
4
- "0d2518050eca8cd1231a-03158dd238416d7c8c3e"
5
- ]
2
+ "status": "passed",
3
+ "failedTests": []
6
4
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-rest-resource",
3
- "version": "5.1.48",
3
+ "version": "5.1.49",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "keywords": [
@@ -0,0 +1,9 @@
1
+ export const resourceRouteMeta = {
2
+ tabKey: resourceTabKey,
3
+ componentKey: resourceTabKey,
4
+ ssrProfile: 'session' as const,
5
+ };
6
+
7
+ export function resourceTabKey(route: { params: Record<string, string | string[]> }) {
8
+ return `/rest/resource/${encodeURIComponent(route.params.resource as string)}`;
9
+ }
@@ -3,23 +3,21 @@ import type { IModuleRoute } from 'zova-module-a-router';
3
3
  import { ZPageEntry } from './.metadata/page/entry.js';
4
4
  import { ZPageEntryCreate } from './.metadata/page/entryCreate.js';
5
5
  import { ZPageResource } from './.metadata/page/resource.js';
6
+ import { resourceRouteMeta, resourceTabKey } from './lib/resourceRouteMeta.js';
6
7
 
7
8
  export const routes: IModuleRoute[] = [
8
9
  {
9
10
  name: 'resource',
10
11
  path: ':resource',
11
12
  component: ZPageResource,
12
- meta: {
13
- tabKey,
14
- ssrProfile: 'session',
15
- },
13
+ meta: resourceRouteMeta,
16
14
  },
17
15
  {
18
16
  name: 'entryCreate',
19
17
  path: ':resource/create',
20
18
  component: ZPageEntryCreate,
21
19
  meta: {
22
- tabKey,
20
+ tabKey: resourceTabKey,
23
21
  ssrProfile: 'session',
24
22
  },
25
23
  },
@@ -28,12 +26,8 @@ export const routes: IModuleRoute[] = [
28
26
  path: ':resource/:id/:formScene?',
29
27
  component: ZPageEntry,
30
28
  meta: {
31
- tabKey,
29
+ tabKey: resourceTabKey,
32
30
  ssrProfile: 'session',
33
31
  },
34
32
  },
35
33
  ];
36
-
37
- function tabKey(route) {
38
- return `/rest/resource/${encodeURIComponent(route.params.resource)}`;
39
- }
@@ -0,0 +1,37 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { resourceRouteMeta } from '../../src/lib/resourceRouteMeta.ts';
5
+
6
+ function getResourceKey(url: string) {
7
+ const resource = decodeURIComponent(url.substring('/rest/resource/'.length));
8
+ const route = { params: { resource } };
9
+ return {
10
+ resource,
11
+ componentKey: resourceRouteMeta.componentKey(route),
12
+ tabKey: resourceRouteMeta.tabKey(route),
13
+ };
14
+ }
15
+
16
+ test('resource root canonicalizes colon URL spellings to one component key', () => {
17
+ const expectedKey = '/rest/resource/admin-user%3Auser';
18
+
19
+ for (const url of [
20
+ '/rest/resource/admin-user:user',
21
+ '/rest/resource/admin-user%3Auser',
22
+ '/rest/resource/admin-user%3auser',
23
+ ]) {
24
+ const actual = getResourceKey(url);
25
+ assert.equal(actual.resource, 'admin-user:user');
26
+ assert.equal(actual.componentKey, expectedKey);
27
+ assert.equal(actual.tabKey, expectedKey);
28
+ }
29
+ });
30
+
31
+ test('resource root keeps distinct resource workspaces separate', () => {
32
+ const user = getResourceKey('/rest/resource/admin-user:user');
33
+ const role = getResourceKey('/rest/resource/admin-user:role');
34
+
35
+ assert.notEqual(user.componentKey, role.componentKey);
36
+ assert.notEqual(user.tabKey, role.tabKey);
37
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-cabloy",
3
- "version": "5.1.49",
3
+ "version": "5.1.50",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "author": "",
@@ -8,7 +8,7 @@
8
8
  "type": "module",
9
9
  "dependencies": {
10
10
  "zova-module-a-rbac": "^5.0.1",
11
- "zova-module-rest-resource": "^5.1.48"
11
+ "zova-module-rest-resource": "^5.1.49"
12
12
  },
13
13
  "title": "a-cabloy"
14
14
  }
@@ -1,195 +0,0 @@
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
- ```