cabloy 5.1.144 → 5.1.145
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/.github/workflows/playwright-e2e.yml +2 -2
- package/CHANGELOG.md +14 -0
- package/cabloy-docs/backend/markdown-guide.md +3 -2
- package/cabloy-docs/frontend/markdown-guide.md +1 -1
- package/e2e/specs/a-commerce.spec.ts +0 -496
- package/e2e/specs/markdown.spec.ts +575 -0
- package/package.json +3 -3
- package/vona/pnpm-lock.yaml +25 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/package.json +2 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/bean/bean.markdown.ts +47 -2
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/packages-utils/zova-jsx/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/core/component/locale.ts +2 -0
- package/zova/src/suite/a-home/modules/home-base/src/.metadata/index.ts +2 -17
- package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +0 -6
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +1 -4
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.locale.tsx +1 -1
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +1 -4
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.locale.tsx +1 -1
- 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/monkey.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +3 -3
- package/zova/src/suite/a-home/modules/home-base/src/service/locale.ts +0 -11
- /package/e2e/specs/{account.spec.ts → home-user-account.spec.ts} +0 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
import { expect, test } from '@playwright/test';
|
|
4
|
+
|
|
5
|
+
function collectPageErrors(page: Page) {
|
|
6
|
+
const errors: Error[] = [];
|
|
7
|
+
page.on('pageerror', error => {
|
|
8
|
+
errors.push(error);
|
|
9
|
+
});
|
|
10
|
+
return errors;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function collectConsoleErrors(page: Page) {
|
|
14
|
+
const errors: string[] = [];
|
|
15
|
+
page.on('console', message => {
|
|
16
|
+
if (message.type() === 'error' || /hydration mismatch/i.test(message.text())) {
|
|
17
|
+
errors.push(message.text());
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return errors;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function waitForApiResponse(page: Page, method: string, path: string | RegExp) {
|
|
24
|
+
return page.waitForResponse(response => {
|
|
25
|
+
const url = new URL(response.url());
|
|
26
|
+
return (
|
|
27
|
+
response.request().method() === method &&
|
|
28
|
+
(typeof path === 'string' ? url.pathname === path : path.test(url.pathname)) &&
|
|
29
|
+
!response.request().headers()['x-vona-openapi-schema']
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function expectTableIdentity(value: unknown) {
|
|
35
|
+
expect(['string', 'number']).toContain(typeof value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function login(
|
|
39
|
+
page: Page,
|
|
40
|
+
path: string,
|
|
41
|
+
username: string,
|
|
42
|
+
password: string,
|
|
43
|
+
hydratedSite: string,
|
|
44
|
+
) {
|
|
45
|
+
await page.goto(path, { waitUntil: 'load' });
|
|
46
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', hydratedSite);
|
|
47
|
+
if (!page.url().includes('/login')) {
|
|
48
|
+
await page.waitForURL(/\/login(?:\?|$)/);
|
|
49
|
+
}
|
|
50
|
+
if (page.url().includes('/login')) {
|
|
51
|
+
const usernameInput = page.getByPlaceholder('Your Username');
|
|
52
|
+
const passwordInput = page.getByPlaceholder('Your Password');
|
|
53
|
+
await usernameInput.fill(username);
|
|
54
|
+
await passwordInput.fill(password);
|
|
55
|
+
await expect(usernameInput).toHaveValue(username);
|
|
56
|
+
await expect(passwordInput).toHaveValue(password);
|
|
57
|
+
await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
|
|
58
|
+
const loginResponse = waitForApiResponse(page, 'POST', '/api/home/user/passport/login');
|
|
59
|
+
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
|
60
|
+
const loginResponseValue = await loginResponse;
|
|
61
|
+
if (!loginResponseValue.ok()) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`login failed: ${loginResponseValue.status()} ${await loginResponseValue.text()}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
await expect(page).not.toHaveURL(/\/login(?:\?|$)/);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test(
|
|
71
|
+
'Commerce Product Content: SSR renders stored safe HTML and hydrates without errors',
|
|
72
|
+
{ tag: ['@web', '@flow', '@product'] },
|
|
73
|
+
async ({ browser }, testInfo) => {
|
|
74
|
+
test.setTimeout(60_000);
|
|
75
|
+
const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
|
|
76
|
+
const categoryName = `E2E Product Content Web Category ${suffix}`;
|
|
77
|
+
const productTitle = `E2E Product Content Web ${suffix}`;
|
|
78
|
+
const skuCode = `E2E-PC-WEB-${suffix}`;
|
|
79
|
+
const markdown = `# Product details ${suffix}\n\nA **durable** product.\n\n==highlighted product ${suffix}==\n\n\`\`\`ts\nconst product = '${suffix}';\n\`\`\`\n\n- [ ] Open task\n- [x] Completed task`;
|
|
80
|
+
const unsafeHtml = '<a href="javascript:alert(\'unsafe\')">unsafe</a>';
|
|
81
|
+
const adminContext = await browser.newContext();
|
|
82
|
+
const adminPage = await adminContext.newPage();
|
|
83
|
+
const webContext = await browser.newContext();
|
|
84
|
+
const webPage = await webContext.newPage();
|
|
85
|
+
const webPageErrors = collectPageErrors(webPage);
|
|
86
|
+
const webConsoleErrors = collectConsoleErrors(webPage);
|
|
87
|
+
const categoryActionPath = '/api/commerce/catalog/category';
|
|
88
|
+
const productActionPath = '/api/commerce/catalog/product';
|
|
89
|
+
const skuActionPath = '/api/commerce/catalog/sku';
|
|
90
|
+
const stockAdjustPath = '/api/commerce/trade/stockBalance/adjustStock';
|
|
91
|
+
let categoryId: number | string | undefined;
|
|
92
|
+
let productId: number | string | undefined;
|
|
93
|
+
let skuId: number | string | undefined;
|
|
94
|
+
let headers: { Authorization: string } | undefined;
|
|
95
|
+
try {
|
|
96
|
+
await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
97
|
+
const accessToken = (await adminContext.cookies()).find(
|
|
98
|
+
cookie => cookie.name === 'token',
|
|
99
|
+
)?.value;
|
|
100
|
+
expect(accessToken).toBeTruthy();
|
|
101
|
+
headers = { Authorization: `Bearer ${accessToken}` };
|
|
102
|
+
|
|
103
|
+
const categoryResponse = await adminPage.request.post(categoryActionPath, {
|
|
104
|
+
data: { name: categoryName, published: true },
|
|
105
|
+
headers,
|
|
106
|
+
});
|
|
107
|
+
expect(categoryResponse.ok()).toBeTruthy();
|
|
108
|
+
categoryId = (await categoryResponse.json()).data;
|
|
109
|
+
expectTableIdentity(categoryId);
|
|
110
|
+
|
|
111
|
+
const productResponse = await adminPage.request.post(productActionPath, {
|
|
112
|
+
data: {
|
|
113
|
+
categoryId,
|
|
114
|
+
title: productTitle,
|
|
115
|
+
published: true,
|
|
116
|
+
productContentForm: {
|
|
117
|
+
descriptionMarkdown: `${markdown}\n\n${unsafeHtml}`,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
headers,
|
|
121
|
+
});
|
|
122
|
+
expect(productResponse.ok()).toBeTruthy();
|
|
123
|
+
productId = (await productResponse.json()).data;
|
|
124
|
+
expectTableIdentity(productId);
|
|
125
|
+
|
|
126
|
+
const skuResponse = await adminPage.request.post(skuActionPath, {
|
|
127
|
+
data: { code: skuCode, productId, priceCents: 1999, lifecycle: 'active' },
|
|
128
|
+
headers,
|
|
129
|
+
});
|
|
130
|
+
expect(skuResponse.ok()).toBeTruthy();
|
|
131
|
+
skuId = (await skuResponse.json()).data;
|
|
132
|
+
expectTableIdentity(skuId);
|
|
133
|
+
|
|
134
|
+
const stockResponse = await adminPage.request.post(stockAdjustPath, {
|
|
135
|
+
data: {
|
|
136
|
+
skuId,
|
|
137
|
+
delta: 1,
|
|
138
|
+
reason: 'Product content SSR E2E fixture',
|
|
139
|
+
correlationId: `product-content-web-${suffix}`,
|
|
140
|
+
},
|
|
141
|
+
headers,
|
|
142
|
+
});
|
|
143
|
+
expect(stockResponse.ok()).toBeTruthy();
|
|
144
|
+
|
|
145
|
+
const productPath = `/commerce/product/${productId}`;
|
|
146
|
+
const ssrResponse = await webPage.request.get(productPath);
|
|
147
|
+
expect(ssrResponse.ok()).toBeTruthy();
|
|
148
|
+
const ssrHtml = await ssrResponse.text();
|
|
149
|
+
expect(ssrHtml.toLowerCase()).not.toContain('data-zova-hydrated');
|
|
150
|
+
expect(ssrHtml).toContain(`<h1>Product details ${suffix}</h1>`);
|
|
151
|
+
expect(ssrHtml).toContain('<strong>durable</strong>');
|
|
152
|
+
expect(ssrHtml).toContain(`<mark>highlighted product ${suffix}</mark>`);
|
|
153
|
+
expect(ssrHtml).toMatch(/<code class="language-ts"><span class="hljs-/);
|
|
154
|
+
expect(ssrHtml).toMatch(
|
|
155
|
+
new RegExp(`<code class="language-ts">[\\s\\S]*${suffix}[\\s\\S]*</code>`),
|
|
156
|
+
);
|
|
157
|
+
expect(ssrHtml).toContain('<ul data-type="taskList">');
|
|
158
|
+
expect(ssrHtml).toContain('<input type="checkbox" disabled="disabled" />');
|
|
159
|
+
expect(ssrHtml).toContain('<input type="checkbox" checked="checked" disabled="disabled" />');
|
|
160
|
+
expect(ssrHtml).not.toMatch(/<a[^>]+javascript:/i);
|
|
161
|
+
|
|
162
|
+
const documentResponse = await webPage.goto(productPath, { waitUntil: 'load' });
|
|
163
|
+
expect(documentResponse?.ok()).toBeTruthy();
|
|
164
|
+
await expect(webPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
165
|
+
await expect(webPage.getByRole('heading', { name: productTitle })).toBeVisible();
|
|
166
|
+
await expect(webPage.locator('.product-description h1')).toHaveText(
|
|
167
|
+
`Product details ${suffix}`,
|
|
168
|
+
);
|
|
169
|
+
await expect(webPage.locator('.product-description strong')).toHaveText('durable');
|
|
170
|
+
await expect(webPage.locator('.product-description mark')).toHaveText(
|
|
171
|
+
`highlighted product ${suffix}`,
|
|
172
|
+
);
|
|
173
|
+
const description = webPage.locator('.product-description');
|
|
174
|
+
await expect(description).not.toHaveClass(/\bprose\b/);
|
|
175
|
+
const codeBlock = description.locator('pre > code.language-ts');
|
|
176
|
+
await expect(codeBlock).toHaveText(`const product = '${suffix}';`);
|
|
177
|
+
await expect(codeBlock.locator('span[class*="hljs-"]')).not.toHaveCount(0);
|
|
178
|
+
await expect(description.locator('ul[data-type="taskList"]')).toHaveCount(1);
|
|
179
|
+
await expect(description.locator('input[type="checkbox"]')).toHaveCount(2);
|
|
180
|
+
await expect(description.locator('input[type="checkbox"]').nth(0)).not.toBeChecked();
|
|
181
|
+
await expect(description.locator('input[type="checkbox"]').nth(0)).toBeDisabled();
|
|
182
|
+
await expect(description.locator('input[type="checkbox"]').nth(1)).toBeChecked();
|
|
183
|
+
await expect(description.locator('input[type="checkbox"]').nth(1)).toBeDisabled();
|
|
184
|
+
expect(webPageErrors).toEqual([]);
|
|
185
|
+
expect(webConsoleErrors).toEqual([]);
|
|
186
|
+
} finally {
|
|
187
|
+
if (skuId && headers) {
|
|
188
|
+
const response = await adminPage.request.delete(`${skuActionPath}/${skuId}`, { headers });
|
|
189
|
+
expect(response.ok()).toBeTruthy();
|
|
190
|
+
}
|
|
191
|
+
if (productId && headers) {
|
|
192
|
+
const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
|
|
193
|
+
headers,
|
|
194
|
+
});
|
|
195
|
+
expect(response.ok()).toBeTruthy();
|
|
196
|
+
}
|
|
197
|
+
if (categoryId && headers) {
|
|
198
|
+
const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
|
|
199
|
+
headers,
|
|
200
|
+
});
|
|
201
|
+
expect(response.ok()).toBeTruthy();
|
|
202
|
+
}
|
|
203
|
+
await webContext.close().catch(() => {});
|
|
204
|
+
await adminContext.close().catch(() => {});
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
test(
|
|
210
|
+
'ATP-SPC-02: Product Content opens the rich-text editor and saves Markdown',
|
|
211
|
+
{ tag: ['@admin', '@flow', '@product'] },
|
|
212
|
+
async ({ browser }, testInfo) => {
|
|
213
|
+
test.setTimeout(60_000);
|
|
214
|
+
const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
|
|
215
|
+
const categoryName = `E2E Product Content Category ${suffix}`;
|
|
216
|
+
const productTitle = `E2E Product Content ${suffix}`;
|
|
217
|
+
const markdown = `# Product content ${suffix}
|
|
218
|
+
|
|
219
|
+
- First benefit
|
|
220
|
+
- Second benefit
|
|
221
|
+
|
|
222
|
+
- [ ] Open task
|
|
223
|
+
- [x] Completed task
|
|
224
|
+
|
|
225
|
+
> Product quote
|
|
226
|
+
|
|
227
|
+
\`\`\`ts
|
|
228
|
+
const product = '${suffix}';
|
|
229
|
+
\`\`\`
|
|
230
|
+
|
|
231
|
+
==highlighted text==
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
| Feature | Value |
|
|
235
|
+
| -------- | ------- |
|
|
236
|
+
| Material | Durable |`;
|
|
237
|
+
const adminContext = await browser.newContext();
|
|
238
|
+
const adminPage = await adminContext.newPage();
|
|
239
|
+
const adminPageErrors = collectPageErrors(adminPage);
|
|
240
|
+
const adminConsoleErrors = collectConsoleErrors(adminPage);
|
|
241
|
+
const productListPath = '/commerce-admin/rest/resource/commerce-catalog%3Aproduct';
|
|
242
|
+
const categoryActionPath = '/api/commerce/catalog/category';
|
|
243
|
+
const productActionPath = '/api/commerce/catalog/product';
|
|
244
|
+
let categoryId: number | string | undefined;
|
|
245
|
+
let productId: number | string | undefined;
|
|
246
|
+
let headers: { Authorization: string } | undefined;
|
|
247
|
+
try {
|
|
248
|
+
await adminPage.setViewportSize({ width: 1440, height: 900 });
|
|
249
|
+
await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
250
|
+
const accessToken = (await adminContext.cookies()).find(
|
|
251
|
+
cookie => cookie.name === 'token',
|
|
252
|
+
)?.value;
|
|
253
|
+
expect(accessToken).toBeTruthy();
|
|
254
|
+
headers = { Authorization: `Bearer ${accessToken}` };
|
|
255
|
+
|
|
256
|
+
const categoryResponse = await adminPage.request.post(categoryActionPath, {
|
|
257
|
+
data: { name: categoryName, published: false },
|
|
258
|
+
headers,
|
|
259
|
+
});
|
|
260
|
+
expect(categoryResponse.ok()).toBeTruthy();
|
|
261
|
+
categoryId = (await categoryResponse.json()).data;
|
|
262
|
+
expectTableIdentity(categoryId);
|
|
263
|
+
|
|
264
|
+
const productResponse = await adminPage.request.post(productActionPath, {
|
|
265
|
+
data: {
|
|
266
|
+
categoryId,
|
|
267
|
+
title: productTitle,
|
|
268
|
+
published: false,
|
|
269
|
+
productContentForm: { descriptionMarkdown: markdown },
|
|
270
|
+
},
|
|
271
|
+
headers,
|
|
272
|
+
});
|
|
273
|
+
expect(productResponse.ok()).toBeTruthy();
|
|
274
|
+
productId = (await productResponse.json()).data;
|
|
275
|
+
expectTableIdentity(productId);
|
|
276
|
+
|
|
277
|
+
await adminPage.goto(`${productListPath}/${productId}/edit`, { waitUntil: 'load' });
|
|
278
|
+
await expect(adminPage.locator('html')).toHaveAttribute(
|
|
279
|
+
'data-zova-hydrated',
|
|
280
|
+
'commerceAdmin',
|
|
281
|
+
);
|
|
282
|
+
expect(adminPageErrors).toEqual([]);
|
|
283
|
+
expect(adminConsoleErrors).toEqual([]);
|
|
284
|
+
const editor = adminPage.locator('[contenteditable="true"]');
|
|
285
|
+
await expect(editor).toBeVisible();
|
|
286
|
+
const toolbar = adminPage.getByRole('toolbar', { name: 'Markdown toolbar' });
|
|
287
|
+
await expect(toolbar).toBeVisible();
|
|
288
|
+
const undoButton = toolbar.getByRole('button', { name: 'Undo' });
|
|
289
|
+
await expect(toolbar.getByRole('combobox', { name: 'Code block language' })).toHaveCount(0);
|
|
290
|
+
const codeBlockToolbar = adminPage.getByRole('toolbar', { name: 'Code block language' });
|
|
291
|
+
await expect(codeBlockToolbar).toBeHidden();
|
|
292
|
+
await expect(toolbar.getByRole('button', { name: 'Bold' })).toHaveAttribute('type', 'button');
|
|
293
|
+
await expect(undoButton).toBeEnabled();
|
|
294
|
+
const insertTableButton = toolbar.getByRole('button', { name: 'Insert table' });
|
|
295
|
+
await expect(insertTableButton).toHaveAttribute('type', 'button');
|
|
296
|
+
await expect(insertTableButton).toHaveAttribute('aria-haspopup', 'grid');
|
|
297
|
+
await expect(insertTableButton).toHaveAttribute('aria-expanded', 'false');
|
|
298
|
+
await insertTableButton.click();
|
|
299
|
+
const tablePickerDialog = adminPage.locator('#markdown-table-size-picker');
|
|
300
|
+
const tablePicker = tablePickerDialog.locator('[role="grid"]');
|
|
301
|
+
await expect(tablePickerDialog).toBeVisible();
|
|
302
|
+
await expect(tablePicker).toHaveCount(1);
|
|
303
|
+
await expect(insertTableButton).toHaveAttribute('aria-expanded', 'true');
|
|
304
|
+
await expect(editor.locator('table')).toHaveCount(1);
|
|
305
|
+
const tablePickerCell = tablePicker.locator('[data-table-picker-cell="2-3"]');
|
|
306
|
+
await tablePickerCell.hover();
|
|
307
|
+
await expect(tablePickerDialog.locator('[aria-live="polite"]')).toHaveText(
|
|
308
|
+
'Selected table size: 2 × 3',
|
|
309
|
+
);
|
|
310
|
+
await expect(tablePicker.locator('[aria-selected="true"]')).toHaveCount(6);
|
|
311
|
+
await expect(tablePicker.locator('[aria-selected="false"]').first()).toBeVisible();
|
|
312
|
+
await tablePickerCell.click();
|
|
313
|
+
await expect(tablePickerDialog).toHaveCount(0);
|
|
314
|
+
await expect(insertTableButton).toHaveAttribute('aria-expanded', 'false');
|
|
315
|
+
await expect(editor.locator('table')).toHaveCount(2);
|
|
316
|
+
await expect(editor.locator('table').last().locator('th')).toHaveCount(3);
|
|
317
|
+
await expect(editor.locator('table').last().locator('td')).toHaveCount(3);
|
|
318
|
+
const editorSurface = editor.locator('..');
|
|
319
|
+
await editor.locator('h1').click();
|
|
320
|
+
await expect(editorSurface).not.toHaveClass(/\bprose\b/);
|
|
321
|
+
const editorSurfaceClass = await editorSurface.getAttribute('class');
|
|
322
|
+
expect(editorSurfaceClass?.trim()).toBeTruthy();
|
|
323
|
+
await expect(editor).toBeFocused();
|
|
324
|
+
await expect(editor.locator('h1')).toHaveText(`Product content ${suffix}`);
|
|
325
|
+
await expect(editor.locator('ul:not([data-type="taskList"])')).toHaveCount(1);
|
|
326
|
+
await expect(editor.locator('blockquote')).toHaveText('Product quote');
|
|
327
|
+
const codeBlock = editor.locator('pre code');
|
|
328
|
+
const codeBlockLanguage = codeBlockToolbar.getByRole('combobox', {
|
|
329
|
+
name: 'Code block language',
|
|
330
|
+
});
|
|
331
|
+
await expect(codeBlockToolbar).toBeHidden();
|
|
332
|
+
await expect(codeBlock).toHaveClass(/\blanguage-ts\b/);
|
|
333
|
+
await expect(codeBlock).toHaveText(`const product = '${suffix}';`);
|
|
334
|
+
await expect(codeBlock.locator('span[class*="hljs-"]')).not.toHaveCount(0);
|
|
335
|
+
await codeBlock.click();
|
|
336
|
+
await expect(codeBlockToolbar).toBeVisible();
|
|
337
|
+
await expect(codeBlockLanguage).toBeEnabled();
|
|
338
|
+
await expect(codeBlockLanguage).toHaveValue('typescript');
|
|
339
|
+
await codeBlockLanguage.selectOption('python');
|
|
340
|
+
await expect(codeBlock).toHaveClass(/\blanguage-python\b/);
|
|
341
|
+
await expect(codeBlock.locator('span[class*="hljs-"]')).not.toHaveCount(0);
|
|
342
|
+
await editor.locator('h1').click();
|
|
343
|
+
await expect(codeBlockToolbar).toBeHidden();
|
|
344
|
+
await expect(editor.locator('mark')).toHaveText('highlighted text');
|
|
345
|
+
await expect(editor.locator('table th')).toHaveCount(5);
|
|
346
|
+
await expect(editor.locator('table td')).toHaveCount(5);
|
|
347
|
+
await expect(editor.locator('h1')).toHaveCSS('font-weight', '700');
|
|
348
|
+
await expect(editor.locator('ul:not([data-type="taskList"])')).toHaveCSS(
|
|
349
|
+
'list-style-type',
|
|
350
|
+
'disc',
|
|
351
|
+
);
|
|
352
|
+
await expect(editor.locator('ul[data-type="taskList"]')).toHaveCount(1);
|
|
353
|
+
await expect(editor.locator('input[type="checkbox"]')).toHaveCount(2);
|
|
354
|
+
await expect(editor.locator('input[type="checkbox"]').nth(0)).not.toBeChecked();
|
|
355
|
+
await expect(editor.locator('input[type="checkbox"]').nth(1)).toBeChecked();
|
|
356
|
+
await expect(editor.locator('blockquote')).toHaveCSS('border-left-width', '4px');
|
|
357
|
+
await expect(editor.locator('pre')).toHaveCSS('overflow-x', 'auto');
|
|
358
|
+
await expect(editor.locator('table th').first()).toHaveCSS('font-weight', '600');
|
|
359
|
+
await editor.locator('h1').selectText();
|
|
360
|
+
const boldButton = toolbar.getByRole('button', { name: 'Bold' });
|
|
361
|
+
await boldButton.click();
|
|
362
|
+
await expect(boldButton).toHaveAttribute('aria-pressed', 'true');
|
|
363
|
+
await undoButton.click();
|
|
364
|
+
await expect(boldButton).toHaveAttribute('aria-pressed', 'false');
|
|
365
|
+
await expect(toolbar.getByRole('button', { name: 'Redo' })).toBeEnabled();
|
|
366
|
+
|
|
367
|
+
const tableToolbar = adminPage.getByRole('toolbar', { name: 'Table toolbar' });
|
|
368
|
+
const insertedTable = editor.locator('table').last();
|
|
369
|
+
await editor.locator('h1').click();
|
|
370
|
+
await expect(tableToolbar).toBeHidden();
|
|
371
|
+
await insertedTable.locator('td').first().click();
|
|
372
|
+
expect(adminPageErrors).toEqual([]);
|
|
373
|
+
expect(adminConsoleErrors).toEqual([]);
|
|
374
|
+
await expect(tableToolbar).toBeVisible();
|
|
375
|
+
expect(adminPageErrors).toEqual([]);
|
|
376
|
+
expect(adminConsoleErrors).toEqual([]);
|
|
377
|
+
await expect(tableToolbar.getByRole('button', { name: 'Add row before' })).toHaveAttribute(
|
|
378
|
+
'type',
|
|
379
|
+
'button',
|
|
380
|
+
);
|
|
381
|
+
await tableToolbar.getByRole('button', { name: 'Add row before' }).click();
|
|
382
|
+
await expect(insertedTable.locator('td')).toHaveCount(6);
|
|
383
|
+
expect(adminPageErrors).toEqual([]);
|
|
384
|
+
await tableToolbar.getByRole('button', { name: 'Delete row' }).click();
|
|
385
|
+
await expect(insertedTable.locator('td')).toHaveCount(3);
|
|
386
|
+
expect(adminPageErrors).toEqual([]);
|
|
387
|
+
await tableToolbar.getByRole('button', { name: 'Add row after' }).click();
|
|
388
|
+
await expect(insertedTable.locator('td')).toHaveCount(6);
|
|
389
|
+
expect(adminPageErrors).toEqual([]);
|
|
390
|
+
await tableToolbar.getByRole('button', { name: 'Delete row' }).click();
|
|
391
|
+
await expect(insertedTable.locator('td')).toHaveCount(3);
|
|
392
|
+
expect(adminPageErrors).toEqual([]);
|
|
393
|
+
await tableToolbar.getByRole('button', { name: 'Add column before' }).click();
|
|
394
|
+
await expect(insertedTable.locator('th')).toHaveCount(4);
|
|
395
|
+
await expect(insertedTable.locator('td')).toHaveCount(4);
|
|
396
|
+
expect(adminPageErrors).toEqual([]);
|
|
397
|
+
await tableToolbar.getByRole('button', { name: 'Delete column' }).click();
|
|
398
|
+
await expect(insertedTable.locator('th')).toHaveCount(3);
|
|
399
|
+
await expect(insertedTable.locator('td')).toHaveCount(3);
|
|
400
|
+
expect(adminPageErrors).toEqual([]);
|
|
401
|
+
await tableToolbar.getByRole('button', { name: 'Add column after' }).click();
|
|
402
|
+
await expect(insertedTable.locator('th')).toHaveCount(4);
|
|
403
|
+
await expect(insertedTable.locator('td')).toHaveCount(4);
|
|
404
|
+
expect(adminPageErrors).toEqual([]);
|
|
405
|
+
await tableToolbar.getByRole('button', { name: 'Delete column' }).click();
|
|
406
|
+
await expect(insertedTable.locator('th')).toHaveCount(3);
|
|
407
|
+
await expect(insertedTable.locator('td')).toHaveCount(3);
|
|
408
|
+
expect(adminPageErrors).toEqual([]);
|
|
409
|
+
await tableToolbar.getByRole('button', { name: 'Delete table' }).click();
|
|
410
|
+
await expect(editor.locator('table')).toHaveCount(1);
|
|
411
|
+
await expect(tableToolbar).toBeHidden();
|
|
412
|
+
expect(adminPageErrors).toEqual([]);
|
|
413
|
+
|
|
414
|
+
const productUpdate = waitForApiResponse(
|
|
415
|
+
adminPage,
|
|
416
|
+
'PATCH',
|
|
417
|
+
`${productActionPath}/${productId}`,
|
|
418
|
+
);
|
|
419
|
+
await adminPage.getByRole('button', { name: 'Submit', exact: true }).click();
|
|
420
|
+
await productUpdate;
|
|
421
|
+
const productResponseAfterUpdate = await adminPage.request.get(
|
|
422
|
+
`${productActionPath}/${productId}`,
|
|
423
|
+
{ headers },
|
|
424
|
+
);
|
|
425
|
+
expect(productResponseAfterUpdate.ok()).toBeTruthy();
|
|
426
|
+
expect((await productResponseAfterUpdate.json()).productContentForm.descriptionMarkdown).toBe(
|
|
427
|
+
markdown.replace('```ts', '```python'),
|
|
428
|
+
);
|
|
429
|
+
expect(adminPageErrors).toEqual([]);
|
|
430
|
+
} finally {
|
|
431
|
+
if (productId && headers) {
|
|
432
|
+
const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
|
|
433
|
+
headers,
|
|
434
|
+
});
|
|
435
|
+
expect(response.ok()).toBeTruthy();
|
|
436
|
+
}
|
|
437
|
+
if (categoryId && headers) {
|
|
438
|
+
const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
|
|
439
|
+
headers,
|
|
440
|
+
});
|
|
441
|
+
expect(response.ok()).toBeTruthy();
|
|
442
|
+
}
|
|
443
|
+
await adminContext.close().catch(() => {});
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
test(
|
|
449
|
+
'ATP-SPC-02: Product Content link toolbar edits Markdown',
|
|
450
|
+
{ tag: ['@admin', '@flow', '@product'] },
|
|
451
|
+
async ({ browser }, testInfo) => {
|
|
452
|
+
test.setTimeout(180_000);
|
|
453
|
+
const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
|
|
454
|
+
const categoryName = `E2E Product Link Category ${suffix}`;
|
|
455
|
+
const productTitle = `E2E Product Link ${suffix}`;
|
|
456
|
+
const markdown = `# Product content ${suffix}\n\nA product description.`;
|
|
457
|
+
const adminContext = await browser.newContext();
|
|
458
|
+
const adminPage = await adminContext.newPage();
|
|
459
|
+
const adminPageErrors = collectPageErrors(adminPage);
|
|
460
|
+
const adminConsoleErrors = collectConsoleErrors(adminPage);
|
|
461
|
+
const productListPath = '/commerce-admin/rest/resource/commerce-catalog%3Aproduct';
|
|
462
|
+
const categoryActionPath = '/api/commerce/catalog/category';
|
|
463
|
+
const productActionPath = '/api/commerce/catalog/product';
|
|
464
|
+
let categoryId: number | string | undefined;
|
|
465
|
+
let productId: number | string | undefined;
|
|
466
|
+
let headers: { Authorization: string } | undefined;
|
|
467
|
+
try {
|
|
468
|
+
await adminPage.setViewportSize({ width: 1440, height: 900 });
|
|
469
|
+
await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
470
|
+
const accessToken = (await adminContext.cookies()).find(
|
|
471
|
+
cookie => cookie.name === 'token',
|
|
472
|
+
)?.value;
|
|
473
|
+
expect(accessToken).toBeTruthy();
|
|
474
|
+
headers = { Authorization: `Bearer ${accessToken}` };
|
|
475
|
+
|
|
476
|
+
const categoryResponse = await adminPage.request.post(categoryActionPath, {
|
|
477
|
+
data: { name: categoryName, published: false },
|
|
478
|
+
headers,
|
|
479
|
+
});
|
|
480
|
+
expect(categoryResponse.ok()).toBeTruthy();
|
|
481
|
+
categoryId = (await categoryResponse.json()).data;
|
|
482
|
+
expectTableIdentity(categoryId);
|
|
483
|
+
|
|
484
|
+
const productResponse = await adminPage.request.post(productActionPath, {
|
|
485
|
+
data: {
|
|
486
|
+
categoryId,
|
|
487
|
+
title: productTitle,
|
|
488
|
+
published: false,
|
|
489
|
+
productContentForm: { descriptionMarkdown: markdown },
|
|
490
|
+
},
|
|
491
|
+
headers,
|
|
492
|
+
});
|
|
493
|
+
expect(productResponse.ok()).toBeTruthy();
|
|
494
|
+
productId = (await productResponse.json()).data;
|
|
495
|
+
expectTableIdentity(productId);
|
|
496
|
+
|
|
497
|
+
await adminPage.goto(`${productListPath}/${productId}/edit`, { waitUntil: 'load' });
|
|
498
|
+
await expect(adminPage.locator('html')).toHaveAttribute(
|
|
499
|
+
'data-zova-hydrated',
|
|
500
|
+
'commerceAdmin',
|
|
501
|
+
);
|
|
502
|
+
const editor = adminPage.locator('[contenteditable="true"]');
|
|
503
|
+
await expect(editor).toBeVisible();
|
|
504
|
+
const toolbar = adminPage.getByRole('toolbar', { name: 'Markdown toolbar' });
|
|
505
|
+
await expect(toolbar).toBeVisible();
|
|
506
|
+
const linkButton = toolbar.getByRole('button', { name: 'Link' });
|
|
507
|
+
await expect(linkButton).toHaveAttribute('type', 'button');
|
|
508
|
+
await expect(linkButton).toBeDisabled();
|
|
509
|
+
|
|
510
|
+
await editor.locator('h1').selectText();
|
|
511
|
+
await expect(linkButton).toBeEnabled();
|
|
512
|
+
await linkButton.click();
|
|
513
|
+
const linkPrompt = adminPage.locator('.fixed.inset-0.z-50');
|
|
514
|
+
await expect(linkPrompt).toBeVisible();
|
|
515
|
+
const linkInput = linkPrompt.locator('input[type="text"]');
|
|
516
|
+
await linkInput.fill('https://example.com/docs');
|
|
517
|
+
await linkInput.press('Enter');
|
|
518
|
+
await expect(editor.locator('h1 a[href="https://example.com/docs"]')).toHaveText(
|
|
519
|
+
`Product content ${suffix}`,
|
|
520
|
+
);
|
|
521
|
+
await expect(linkButton).toHaveAttribute('aria-pressed', 'true');
|
|
522
|
+
await editor.press('ArrowLeft');
|
|
523
|
+
await expect(linkButton).toHaveAttribute('aria-pressed', 'true');
|
|
524
|
+
|
|
525
|
+
await linkButton.click();
|
|
526
|
+
await expect(linkPrompt).toBeVisible();
|
|
527
|
+
await expect(linkInput).toHaveValue('https://example.com/docs');
|
|
528
|
+
await linkInput.fill('https://example.com/updated');
|
|
529
|
+
await linkPrompt.getByRole('button', { name: 'OK' }).click();
|
|
530
|
+
await expect(editor.locator('h1 a[href="https://example.com/updated"]')).toHaveText(
|
|
531
|
+
`Product content ${suffix}`,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
await linkButton.click();
|
|
535
|
+
await expect(linkPrompt).toBeVisible();
|
|
536
|
+
await linkPrompt.getByRole('button', { name: 'Cancel' }).click();
|
|
537
|
+
await expect(editor.locator('h1 a[href="https://example.com/updated"]')).toHaveText(
|
|
538
|
+
`Product content ${suffix}`,
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
await editor.locator('h1').selectText();
|
|
542
|
+
await expect(linkButton).toHaveAttribute('aria-pressed', 'true');
|
|
543
|
+
await linkButton.click();
|
|
544
|
+
await expect(linkPrompt).toBeVisible();
|
|
545
|
+
await linkInput.fill('');
|
|
546
|
+
await linkInput.press('Enter');
|
|
547
|
+
await expect(editor.locator('h1 a')).toHaveCount(0);
|
|
548
|
+
await adminPage.getByRole('button', { name: 'Submit', exact: true }).click();
|
|
549
|
+
const productResponseAfterLinkRemoval = await adminPage.request.get(
|
|
550
|
+
`${productActionPath}/${productId}`,
|
|
551
|
+
{ headers },
|
|
552
|
+
);
|
|
553
|
+
expect(productResponseAfterLinkRemoval.ok()).toBeTruthy();
|
|
554
|
+
expect(
|
|
555
|
+
(await productResponseAfterLinkRemoval.json()).productContentForm.descriptionMarkdown,
|
|
556
|
+
).not.toContain('https://example.com/updated');
|
|
557
|
+
expect(adminPageErrors).toEqual([]);
|
|
558
|
+
expect(adminConsoleErrors).toEqual([]);
|
|
559
|
+
} finally {
|
|
560
|
+
if (productId && headers) {
|
|
561
|
+
const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
|
|
562
|
+
headers,
|
|
563
|
+
});
|
|
564
|
+
expect(response.ok()).toBeTruthy();
|
|
565
|
+
}
|
|
566
|
+
if (categoryId && headers) {
|
|
567
|
+
const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
|
|
568
|
+
headers,
|
|
569
|
+
});
|
|
570
|
+
expect(response.ok()).toBeTruthy();
|
|
571
|
+
}
|
|
572
|
+
await adminContext.close().catch(() => {});
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cabloy",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.145",
|
|
4
4
|
"gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
|
|
5
5
|
"description": "A Node.js fullstack framework",
|
|
6
6
|
"keywords": [
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"build:zova:commerce": "pnpm --dir zova run build:ssr:cabloyCommerceBatch",
|
|
44
44
|
"build:zova:commerce:web": "pnpm --dir zova run build:ssr:cabloyCommerce && pnpm --dir zova run build:rest:cabloyCommerce",
|
|
45
45
|
"build:zova:commerce:admin": "pnpm --dir zova run build:ssr:cabloyCommerceAdmin && pnpm --dir zova run build:rest:cabloyCommerceAdmin",
|
|
46
|
-
"test:e2e": "node e2e/scripts/runE2e.ts --clean",
|
|
47
|
-
"test:e2e:fast": "node e2e/scripts/runE2e.ts --fast",
|
|
48
46
|
"start": "cd vona && npm run start",
|
|
49
47
|
"start:one": "cd vona && npm run start:one",
|
|
50
48
|
"test": "cd vona && npm run test",
|
|
49
|
+
"test:e2e": "node e2e/scripts/runE2e.ts --clean",
|
|
50
|
+
"test:e2e:fast": "node e2e/scripts/runE2e.ts --fast",
|
|
51
51
|
"db:reset": "cd vona && npm run db:reset",
|
|
52
52
|
"tsc": "npm run tsc:zova && pnpm --dir vona run tsc",
|
|
53
53
|
"tsc:zova": "pnpm --dir zova run tsc",
|
package/vona/pnpm-lock.yaml
CHANGED
|
@@ -1687,6 +1687,9 @@ importers:
|
|
|
1687
1687
|
'@tiptap/static-renderer':
|
|
1688
1688
|
specifier: ^3.30.0
|
|
1689
1689
|
version: 3.30.0(@tiptap/core@3.30.0(@tiptap/pm@3.30.0))(@tiptap/pm@3.30.0)(react-dom@19.2.8(react@19.2.7))(react@19.2.7)
|
|
1690
|
+
lowlight:
|
|
1691
|
+
specifier: ^3.3.0
|
|
1692
|
+
version: 3.3.0
|
|
1690
1693
|
sanitize-html:
|
|
1691
1694
|
specifier: ^2.17.6
|
|
1692
1695
|
version: 2.17.6
|
|
@@ -5143,6 +5146,9 @@ packages:
|
|
|
5143
5146
|
'@types/fs-extra@11.0.4':
|
|
5144
5147
|
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
|
5145
5148
|
|
|
5149
|
+
'@types/hast@3.0.5':
|
|
5150
|
+
resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==}
|
|
5151
|
+
|
|
5146
5152
|
'@types/http-assert@1.5.6':
|
|
5147
5153
|
resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==}
|
|
5148
5154
|
|
|
@@ -6607,6 +6613,10 @@ packages:
|
|
|
6607
6613
|
header-case@2.0.4:
|
|
6608
6614
|
resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==}
|
|
6609
6615
|
|
|
6616
|
+
highlight.js@11.11.2:
|
|
6617
|
+
resolution: {integrity: sha512-oaXMACAU0kzOMXBjWpNcX+vlwSBCIAiZ9BHa7gA15NOTtT2L/l8OSZDuqS2XppOhZBPJ7hm4o8ep2kyuip2uEQ==}
|
|
6618
|
+
engines: {node: '>=12.0.0'}
|
|
6619
|
+
|
|
6610
6620
|
hono@4.12.32:
|
|
6611
6621
|
resolution: {integrity: sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==}
|
|
6612
6622
|
engines: {node: '>=16.9.0'}
|
|
@@ -7167,6 +7177,9 @@ packages:
|
|
|
7167
7177
|
lower-case@2.0.2:
|
|
7168
7178
|
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
|
7169
7179
|
|
|
7180
|
+
lowlight@3.3.0:
|
|
7181
|
+
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
|
|
7182
|
+
|
|
7170
7183
|
lru-cache@10.4.3:
|
|
7171
7184
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
|
7172
7185
|
|
|
@@ -10967,6 +10980,10 @@ snapshots:
|
|
|
10967
10980
|
'@types/jsonfile': 6.1.4
|
|
10968
10981
|
'@types/node': 22.20.0
|
|
10969
10982
|
|
|
10983
|
+
'@types/hast@3.0.5':
|
|
10984
|
+
dependencies:
|
|
10985
|
+
'@types/unist': 3.0.3
|
|
10986
|
+
|
|
10970
10987
|
'@types/http-assert@1.5.6': {}
|
|
10971
10988
|
|
|
10972
10989
|
'@types/http-errors@2.0.5': {}
|
|
@@ -12562,6 +12579,8 @@ snapshots:
|
|
|
12562
12579
|
capital-case: 1.0.4
|
|
12563
12580
|
tslib: 2.8.1
|
|
12564
12581
|
|
|
12582
|
+
highlight.js@11.11.2: {}
|
|
12583
|
+
|
|
12565
12584
|
hono@4.12.32: {}
|
|
12566
12585
|
|
|
12567
12586
|
hookified@1.15.1: {}
|
|
@@ -13103,6 +13122,12 @@ snapshots:
|
|
|
13103
13122
|
dependencies:
|
|
13104
13123
|
tslib: 2.8.1
|
|
13105
13124
|
|
|
13125
|
+
lowlight@3.3.0:
|
|
13126
|
+
dependencies:
|
|
13127
|
+
'@types/hast': 3.0.5
|
|
13128
|
+
devlop: 1.1.0
|
|
13129
|
+
highlight.js: 11.11.2
|
|
13130
|
+
|
|
13106
13131
|
lru-cache@10.4.3: {}
|
|
13107
13132
|
|
|
13108
13133
|
lru-cache@11.5.1: {}
|