cabloy 5.1.144 → 5.1.146
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 +21 -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 +114 -33
- package/vona/pnpm-workspace.yaml +1 -1
- 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/pnpm-lock.yaml +52 -52
- 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
- /package/vona/patches/{zova-core@5.1.87.patch → zova-core@5.1.88.patch} +0 -0
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.146
|
|
@@ -27,7 +27,7 @@ jobs:
|
|
|
27
27
|
- name: install Chromium
|
|
28
28
|
run: npx playwright install --with-deps chromium
|
|
29
29
|
- name: run Basic E2E baseline
|
|
30
|
-
run: npm run test:e2e -- cabloy-basic account
|
|
30
|
+
run: npm run test:e2e -- cabloy-basic home-user-account
|
|
31
31
|
- name: upload Playwright artifacts
|
|
32
32
|
if: failure()
|
|
33
33
|
uses: actions/upload-artifact@v7
|
|
@@ -65,7 +65,7 @@ jobs:
|
|
|
65
65
|
- name: synchronize Vona dependencies
|
|
66
66
|
run: npm run deps:vona
|
|
67
67
|
- name: run Commerce E2E baseline
|
|
68
|
-
run: npm run test:e2e a-commerce
|
|
68
|
+
run: npm run test:e2e a-commerce markdown
|
|
69
69
|
- name: upload Playwright artifacts
|
|
70
70
|
if: failure()
|
|
71
71
|
uses: actions/upload-artifact@v7
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.146
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- Refresh the Zova lockfile to prepare for compensation reruns.
|
|
8
|
+
- Update the Vona Zova Core patch dependency to version 5.1.88.
|
|
9
|
+
|
|
10
|
+
## 5.1.145
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- Add Markdown support and code styling.
|
|
15
|
+
- Add the `$serviceLocale` service locale capability.
|
|
16
|
+
- Add home-user specifications.
|
|
17
|
+
- Add update functionality.
|
|
18
|
+
|
|
19
|
+
### Improvements
|
|
20
|
+
|
|
21
|
+
- Update SSR menu product configuration.
|
|
22
|
+
- Update package configuration and Playwright end-to-end workflow.
|
|
23
|
+
|
|
3
24
|
## 5.1.144
|
|
4
25
|
|
|
5
26
|
### Features
|
|
@@ -50,6 +50,7 @@ The parser and static renderer are initialized once at module load. The configur
|
|
|
50
50
|
|
|
51
51
|
- `Markdown`
|
|
52
52
|
- `StarterKit`
|
|
53
|
+
- server-side Lowlight rendering with the common language set for fenced code blocks
|
|
53
54
|
- `TaskList`
|
|
54
55
|
- nested `TaskItem`
|
|
55
56
|
- `Image`
|
|
@@ -78,7 +79,7 @@ The allowlist retains the structures needed by the configured rich-text output:
|
|
|
78
79
|
|
|
79
80
|
- document blocks: `p`, `br`, `hr`, `div`;
|
|
80
81
|
- headings: `h1` through `h6`;
|
|
81
|
-
- text marks: `em`, `strong`, `del`, `s`;
|
|
82
|
+
- text marks: `em`, `strong`, `del`, `s`, `mark`;
|
|
82
83
|
- links: `a`;
|
|
83
84
|
- lists and quotes: `ol`, `ul`, `li`, `blockquote`;
|
|
84
85
|
- code: `code`, `pre`;
|
|
@@ -105,7 +106,7 @@ Class values are restricted by tag:
|
|
|
105
106
|
- code classes must match `language-*`;
|
|
106
107
|
- span classes must match `hljs-*`.
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
Highlight Markdown such as `==text==` renders as a bare `<mark>text</mark>` element. The renderer emits `hljs-*` token spans for supported fenced code blocks, and the narrow class policy preserves those generated syntax-highlighting tokens. `mark` has no allowed attributes; arbitrary classes, event handlers, and inline CSS do not become part of the output contract.
|
|
109
110
|
|
|
110
111
|
### URL policy
|
|
111
112
|
|
|
@@ -255,7 +255,7 @@ const ready = true;
|
|
|
255
255
|

|
|
256
256
|
````
|
|
257
257
|
|
|
258
|
-
The interactive editor
|
|
258
|
+
The interactive editor and the backend renderer both preserve supported Highlight content and decorate fenced code blocks with Lowlight token spans. Markdown such as `==text==` becomes a bare `<mark>text</mark>` element in the sanitized server projection, and the existing shared rich-text style supplies its presentation. The backend projection also preserves the semantic `<code class="language-*">` element and its sanitized `hljs-*` token spans. `ZMarkdownHtml` displays that trusted projection; it does not parse Markdown or syntax-highlight arbitrary HTML itself. Do not infer support for arbitrary raw HTML, iframe embeds, inline styles, `data:` URLs, or custom classes from the presence of a TipTap extension.
|
|
259
259
|
|
|
260
260
|
## Common mistakes
|
|
261
261
|
|
|
@@ -1728,502 +1728,6 @@ test(
|
|
|
1728
1728
|
},
|
|
1729
1729
|
);
|
|
1730
1730
|
|
|
1731
|
-
test(
|
|
1732
|
-
'Commerce Product Content: SSR renders stored safe HTML and hydrates without errors',
|
|
1733
|
-
{ tag: ['@web', '@flow', '@product'] },
|
|
1734
|
-
async ({ browser }, testInfo) => {
|
|
1735
|
-
test.setTimeout(60_000);
|
|
1736
|
-
const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
|
|
1737
|
-
const categoryName = `E2E Product Content Web Category ${suffix}`;
|
|
1738
|
-
const productTitle = `E2E Product Content Web ${suffix}`;
|
|
1739
|
-
const skuCode = `E2E-PC-WEB-${suffix}`;
|
|
1740
|
-
const markdown = `# Product details ${suffix}\n\nA **durable** product.\n\n- [ ] Open task\n- [x] Completed task`;
|
|
1741
|
-
const unsafeHtml = '<a href="javascript:alert(\'unsafe\')">unsafe</a>';
|
|
1742
|
-
const adminContext = await browser.newContext();
|
|
1743
|
-
const adminPage = await adminContext.newPage();
|
|
1744
|
-
const webContext = await browser.newContext();
|
|
1745
|
-
const webPage = await webContext.newPage();
|
|
1746
|
-
const webPageErrors = collectPageErrors(webPage);
|
|
1747
|
-
const webConsoleErrors = collectConsoleErrors(webPage);
|
|
1748
|
-
const categoryActionPath = '/api/commerce/catalog/category';
|
|
1749
|
-
const productActionPath = '/api/commerce/catalog/product';
|
|
1750
|
-
const skuActionPath = '/api/commerce/catalog/sku';
|
|
1751
|
-
const stockAdjustPath = '/api/commerce/trade/stockBalance/adjustStock';
|
|
1752
|
-
let categoryId: number | string | undefined;
|
|
1753
|
-
let productId: number | string | undefined;
|
|
1754
|
-
let skuId: number | string | undefined;
|
|
1755
|
-
let headers: { Authorization: string } | undefined;
|
|
1756
|
-
try {
|
|
1757
|
-
await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
1758
|
-
const accessToken = (await adminContext.cookies()).find(
|
|
1759
|
-
cookie => cookie.name === 'token',
|
|
1760
|
-
)?.value;
|
|
1761
|
-
expect(accessToken).toBeTruthy();
|
|
1762
|
-
headers = { Authorization: `Bearer ${accessToken}` };
|
|
1763
|
-
|
|
1764
|
-
const categoryResponse = await adminPage.request.post(categoryActionPath, {
|
|
1765
|
-
data: { name: categoryName, published: true },
|
|
1766
|
-
headers,
|
|
1767
|
-
});
|
|
1768
|
-
expect(categoryResponse.ok()).toBeTruthy();
|
|
1769
|
-
categoryId = (await categoryResponse.json()).data;
|
|
1770
|
-
expectTableIdentity(categoryId);
|
|
1771
|
-
|
|
1772
|
-
const productResponse = await adminPage.request.post(productActionPath, {
|
|
1773
|
-
data: {
|
|
1774
|
-
categoryId,
|
|
1775
|
-
title: productTitle,
|
|
1776
|
-
published: true,
|
|
1777
|
-
productContentForm: {
|
|
1778
|
-
descriptionMarkdown: `${markdown}\n\n${unsafeHtml}`,
|
|
1779
|
-
},
|
|
1780
|
-
},
|
|
1781
|
-
headers,
|
|
1782
|
-
});
|
|
1783
|
-
expect(productResponse.ok()).toBeTruthy();
|
|
1784
|
-
productId = (await productResponse.json()).data;
|
|
1785
|
-
expectTableIdentity(productId);
|
|
1786
|
-
|
|
1787
|
-
const skuResponse = await adminPage.request.post(skuActionPath, {
|
|
1788
|
-
data: { code: skuCode, productId, priceCents: 1999, lifecycle: 'active' },
|
|
1789
|
-
headers,
|
|
1790
|
-
});
|
|
1791
|
-
expect(skuResponse.ok()).toBeTruthy();
|
|
1792
|
-
skuId = (await skuResponse.json()).data;
|
|
1793
|
-
expectTableIdentity(skuId);
|
|
1794
|
-
|
|
1795
|
-
const stockResponse = await adminPage.request.post(stockAdjustPath, {
|
|
1796
|
-
data: {
|
|
1797
|
-
skuId,
|
|
1798
|
-
delta: 1,
|
|
1799
|
-
reason: 'Product content SSR E2E fixture',
|
|
1800
|
-
correlationId: `product-content-web-${suffix}`,
|
|
1801
|
-
},
|
|
1802
|
-
headers,
|
|
1803
|
-
});
|
|
1804
|
-
expect(stockResponse.ok()).toBeTruthy();
|
|
1805
|
-
|
|
1806
|
-
const productPath = `/commerce/product/${productId}`;
|
|
1807
|
-
const ssrResponse = await webPage.request.get(productPath);
|
|
1808
|
-
expect(ssrResponse.ok()).toBeTruthy();
|
|
1809
|
-
const ssrHtml = await ssrResponse.text();
|
|
1810
|
-
expect(ssrHtml.toLowerCase()).not.toContain('data-zova-hydrated');
|
|
1811
|
-
expect(ssrHtml).toContain(`<h1>Product details ${suffix}</h1>`);
|
|
1812
|
-
expect(ssrHtml).toContain('<strong>durable</strong>');
|
|
1813
|
-
expect(ssrHtml).toContain('<ul data-type="taskList">');
|
|
1814
|
-
expect(ssrHtml).toContain('<input type="checkbox" disabled="disabled" />');
|
|
1815
|
-
expect(ssrHtml).toContain('<input type="checkbox" checked="checked" disabled="disabled" />');
|
|
1816
|
-
expect(ssrHtml).not.toMatch(/<a[^>]+javascript:/i);
|
|
1817
|
-
|
|
1818
|
-
const documentResponse = await webPage.goto(productPath, { waitUntil: 'load' });
|
|
1819
|
-
expect(documentResponse?.ok()).toBeTruthy();
|
|
1820
|
-
await expect(webPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
1821
|
-
await expect(webPage.getByRole('heading', { name: productTitle })).toBeVisible();
|
|
1822
|
-
await expect(webPage.locator('.product-description h1')).toHaveText(
|
|
1823
|
-
`Product details ${suffix}`,
|
|
1824
|
-
);
|
|
1825
|
-
await expect(webPage.locator('.product-description strong')).toHaveText('durable');
|
|
1826
|
-
const description = webPage.locator('.product-description');
|
|
1827
|
-
await expect(description).not.toHaveClass(/\bprose\b/);
|
|
1828
|
-
await expect(description.locator('ul[data-type="taskList"]')).toHaveCount(1);
|
|
1829
|
-
await expect(description.locator('input[type="checkbox"]')).toHaveCount(2);
|
|
1830
|
-
await expect(description.locator('input[type="checkbox"]').nth(0)).not.toBeChecked();
|
|
1831
|
-
await expect(description.locator('input[type="checkbox"]').nth(0)).toBeDisabled();
|
|
1832
|
-
await expect(description.locator('input[type="checkbox"]').nth(1)).toBeChecked();
|
|
1833
|
-
await expect(description.locator('input[type="checkbox"]').nth(1)).toBeDisabled();
|
|
1834
|
-
expect(webPageErrors).toEqual([]);
|
|
1835
|
-
expect(webConsoleErrors).toEqual([]);
|
|
1836
|
-
} finally {
|
|
1837
|
-
if (skuId && headers) {
|
|
1838
|
-
const response = await adminPage.request.delete(`${skuActionPath}/${skuId}`, { headers });
|
|
1839
|
-
expect(response.ok()).toBeTruthy();
|
|
1840
|
-
}
|
|
1841
|
-
if (productId && headers) {
|
|
1842
|
-
const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
|
|
1843
|
-
headers,
|
|
1844
|
-
});
|
|
1845
|
-
expect(response.ok()).toBeTruthy();
|
|
1846
|
-
}
|
|
1847
|
-
if (categoryId && headers) {
|
|
1848
|
-
const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
|
|
1849
|
-
headers,
|
|
1850
|
-
});
|
|
1851
|
-
expect(response.ok()).toBeTruthy();
|
|
1852
|
-
}
|
|
1853
|
-
await webContext.close().catch(() => {});
|
|
1854
|
-
await adminContext.close().catch(() => {});
|
|
1855
|
-
}
|
|
1856
|
-
},
|
|
1857
|
-
);
|
|
1858
|
-
|
|
1859
|
-
test(
|
|
1860
|
-
'ATP-SPC-02: Product Content opens the rich-text editor and saves Markdown',
|
|
1861
|
-
{ tag: ['@admin', '@flow', '@product'] },
|
|
1862
|
-
async ({ browser }, testInfo) => {
|
|
1863
|
-
test.setTimeout(60_000);
|
|
1864
|
-
const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
|
|
1865
|
-
const categoryName = `E2E Product Content Category ${suffix}`;
|
|
1866
|
-
const productTitle = `E2E Product Content ${suffix}`;
|
|
1867
|
-
const markdown = `# Product content ${suffix}
|
|
1868
|
-
|
|
1869
|
-
- First benefit
|
|
1870
|
-
- Second benefit
|
|
1871
|
-
|
|
1872
|
-
- [ ] Open task
|
|
1873
|
-
- [x] Completed task
|
|
1874
|
-
|
|
1875
|
-
> Product quote
|
|
1876
|
-
|
|
1877
|
-
\`\`\`ts
|
|
1878
|
-
const product = '${suffix}';
|
|
1879
|
-
\`\`\`
|
|
1880
|
-
|
|
1881
|
-
==highlighted text==
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
| Feature | Value |
|
|
1885
|
-
| -------- | ------- |
|
|
1886
|
-
| Material | Durable |`;
|
|
1887
|
-
const adminContext = await browser.newContext();
|
|
1888
|
-
const adminPage = await adminContext.newPage();
|
|
1889
|
-
const adminPageErrors = collectPageErrors(adminPage);
|
|
1890
|
-
const adminConsoleErrors = collectConsoleErrors(adminPage);
|
|
1891
|
-
const productListPath = '/commerce-admin/rest/resource/commerce-catalog%3Aproduct';
|
|
1892
|
-
const categoryActionPath = '/api/commerce/catalog/category';
|
|
1893
|
-
const productActionPath = '/api/commerce/catalog/product';
|
|
1894
|
-
let categoryId: number | string | undefined;
|
|
1895
|
-
let productId: number | string | undefined;
|
|
1896
|
-
let headers: { Authorization: string } | undefined;
|
|
1897
|
-
try {
|
|
1898
|
-
await adminPage.setViewportSize({ width: 1440, height: 900 });
|
|
1899
|
-
await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
1900
|
-
const accessToken = (await adminContext.cookies()).find(
|
|
1901
|
-
cookie => cookie.name === 'token',
|
|
1902
|
-
)?.value;
|
|
1903
|
-
expect(accessToken).toBeTruthy();
|
|
1904
|
-
headers = { Authorization: `Bearer ${accessToken}` };
|
|
1905
|
-
|
|
1906
|
-
const categoryResponse = await adminPage.request.post(categoryActionPath, {
|
|
1907
|
-
data: { name: categoryName, published: false },
|
|
1908
|
-
headers,
|
|
1909
|
-
});
|
|
1910
|
-
expect(categoryResponse.ok()).toBeTruthy();
|
|
1911
|
-
categoryId = (await categoryResponse.json()).data;
|
|
1912
|
-
expectTableIdentity(categoryId);
|
|
1913
|
-
|
|
1914
|
-
const productResponse = await adminPage.request.post(productActionPath, {
|
|
1915
|
-
data: {
|
|
1916
|
-
categoryId,
|
|
1917
|
-
title: productTitle,
|
|
1918
|
-
published: false,
|
|
1919
|
-
productContentForm: { descriptionMarkdown: markdown },
|
|
1920
|
-
},
|
|
1921
|
-
headers,
|
|
1922
|
-
});
|
|
1923
|
-
expect(productResponse.ok()).toBeTruthy();
|
|
1924
|
-
productId = (await productResponse.json()).data;
|
|
1925
|
-
expectTableIdentity(productId);
|
|
1926
|
-
|
|
1927
|
-
await adminPage.goto(`${productListPath}/${productId}/edit`, { waitUntil: 'load' });
|
|
1928
|
-
await expect(adminPage.locator('html')).toHaveAttribute(
|
|
1929
|
-
'data-zova-hydrated',
|
|
1930
|
-
'commerceAdmin',
|
|
1931
|
-
);
|
|
1932
|
-
expect(adminPageErrors).toEqual([]);
|
|
1933
|
-
expect(adminConsoleErrors).toEqual([]);
|
|
1934
|
-
const editor = adminPage.locator('[contenteditable="true"]');
|
|
1935
|
-
await expect(editor).toBeVisible();
|
|
1936
|
-
const toolbar = adminPage.getByRole('toolbar', { name: 'Markdown toolbar' });
|
|
1937
|
-
await expect(toolbar).toBeVisible();
|
|
1938
|
-
const undoButton = toolbar.getByRole('button', { name: 'Undo' });
|
|
1939
|
-
await expect(toolbar.getByRole('combobox', { name: 'Code block language' })).toHaveCount(0);
|
|
1940
|
-
const codeBlockToolbar = adminPage.getByRole('toolbar', { name: 'Code block language' });
|
|
1941
|
-
await expect(codeBlockToolbar).toBeHidden();
|
|
1942
|
-
await expect(toolbar.getByRole('button', { name: 'Bold' })).toHaveAttribute('type', 'button');
|
|
1943
|
-
await expect(undoButton).toBeEnabled();
|
|
1944
|
-
const insertTableButton = toolbar.getByRole('button', { name: 'Insert table' });
|
|
1945
|
-
await expect(insertTableButton).toHaveAttribute('type', 'button');
|
|
1946
|
-
await expect(insertTableButton).toHaveAttribute('aria-haspopup', 'grid');
|
|
1947
|
-
await expect(insertTableButton).toHaveAttribute('aria-expanded', 'false');
|
|
1948
|
-
await insertTableButton.click();
|
|
1949
|
-
const tablePickerDialog = adminPage.locator('#markdown-table-size-picker');
|
|
1950
|
-
const tablePicker = tablePickerDialog.locator('[role="grid"]');
|
|
1951
|
-
await expect(tablePickerDialog).toBeVisible();
|
|
1952
|
-
await expect(tablePicker).toHaveCount(1);
|
|
1953
|
-
await expect(insertTableButton).toHaveAttribute('aria-expanded', 'true');
|
|
1954
|
-
await expect(editor.locator('table')).toHaveCount(1);
|
|
1955
|
-
const tablePickerCell = tablePicker.locator('[data-table-picker-cell="2-3"]');
|
|
1956
|
-
await tablePickerCell.hover();
|
|
1957
|
-
await expect(tablePickerDialog.locator('[aria-live="polite"]')).toHaveText(
|
|
1958
|
-
'Selected table size: 2 × 3',
|
|
1959
|
-
);
|
|
1960
|
-
await expect(tablePicker.locator('[aria-selected="true"]')).toHaveCount(6);
|
|
1961
|
-
await expect(tablePicker.locator('[aria-selected="false"]').first()).toBeVisible();
|
|
1962
|
-
await tablePickerCell.click();
|
|
1963
|
-
await expect(tablePickerDialog).toHaveCount(0);
|
|
1964
|
-
await expect(insertTableButton).toHaveAttribute('aria-expanded', 'false');
|
|
1965
|
-
await expect(editor.locator('table')).toHaveCount(2);
|
|
1966
|
-
await expect(editor.locator('table').last().locator('th')).toHaveCount(3);
|
|
1967
|
-
await expect(editor.locator('table').last().locator('td')).toHaveCount(3);
|
|
1968
|
-
const editorSurface = editor.locator('..');
|
|
1969
|
-
await editor.locator('h1').click();
|
|
1970
|
-
await expect(editorSurface).not.toHaveClass(/\bprose\b/);
|
|
1971
|
-
const editorSurfaceClass = await editorSurface.getAttribute('class');
|
|
1972
|
-
expect(editorSurfaceClass?.trim()).toBeTruthy();
|
|
1973
|
-
await expect(editor).toBeFocused();
|
|
1974
|
-
await expect(editor.locator('h1')).toHaveText(`Product content ${suffix}`);
|
|
1975
|
-
await expect(editor.locator('ul:not([data-type="taskList"])')).toHaveCount(1);
|
|
1976
|
-
await expect(editor.locator('blockquote')).toHaveText('Product quote');
|
|
1977
|
-
const codeBlock = editor.locator('pre code');
|
|
1978
|
-
const codeBlockLanguage = codeBlockToolbar.getByRole('combobox', {
|
|
1979
|
-
name: 'Code block language',
|
|
1980
|
-
});
|
|
1981
|
-
await expect(codeBlockToolbar).toBeHidden();
|
|
1982
|
-
await expect(codeBlock).toHaveClass(/\blanguage-ts\b/);
|
|
1983
|
-
await expect(codeBlock).toHaveText(`const product = '${suffix}';`);
|
|
1984
|
-
await expect(codeBlock.locator('span[class*="hljs-"]')).not.toHaveCount(0);
|
|
1985
|
-
await codeBlock.click();
|
|
1986
|
-
await expect(codeBlockToolbar).toBeVisible();
|
|
1987
|
-
await expect(codeBlockLanguage).toBeEnabled();
|
|
1988
|
-
await expect(codeBlockLanguage).toHaveValue('typescript');
|
|
1989
|
-
await codeBlockLanguage.selectOption('python');
|
|
1990
|
-
await expect(codeBlock).toHaveClass(/\blanguage-python\b/);
|
|
1991
|
-
await expect(codeBlock.locator('span[class*="hljs-"]')).not.toHaveCount(0);
|
|
1992
|
-
await editor.locator('h1').click();
|
|
1993
|
-
await expect(codeBlockToolbar).toBeHidden();
|
|
1994
|
-
await expect(editor.locator('mark')).toHaveText('highlighted text');
|
|
1995
|
-
await expect(editor.locator('table th')).toHaveCount(5);
|
|
1996
|
-
await expect(editor.locator('table td')).toHaveCount(5);
|
|
1997
|
-
await expect(editor.locator('h1')).toHaveCSS('font-weight', '700');
|
|
1998
|
-
await expect(editor.locator('ul:not([data-type="taskList"])')).toHaveCSS(
|
|
1999
|
-
'list-style-type',
|
|
2000
|
-
'disc',
|
|
2001
|
-
);
|
|
2002
|
-
await expect(editor.locator('ul[data-type="taskList"]')).toHaveCount(1);
|
|
2003
|
-
await expect(editor.locator('input[type="checkbox"]')).toHaveCount(2);
|
|
2004
|
-
await expect(editor.locator('input[type="checkbox"]').nth(0)).not.toBeChecked();
|
|
2005
|
-
await expect(editor.locator('input[type="checkbox"]').nth(1)).toBeChecked();
|
|
2006
|
-
await expect(editor.locator('blockquote')).toHaveCSS('border-left-width', '4px');
|
|
2007
|
-
await expect(editor.locator('pre')).toHaveCSS('overflow-x', 'auto');
|
|
2008
|
-
await expect(editor.locator('table th').first()).toHaveCSS('font-weight', '600');
|
|
2009
|
-
await editor.locator('h1').selectText();
|
|
2010
|
-
const boldButton = toolbar.getByRole('button', { name: 'Bold' });
|
|
2011
|
-
await boldButton.click();
|
|
2012
|
-
await expect(boldButton).toHaveAttribute('aria-pressed', 'true');
|
|
2013
|
-
await undoButton.click();
|
|
2014
|
-
await expect(boldButton).toHaveAttribute('aria-pressed', 'false');
|
|
2015
|
-
await expect(toolbar.getByRole('button', { name: 'Redo' })).toBeEnabled();
|
|
2016
|
-
|
|
2017
|
-
const tableToolbar = adminPage.getByRole('toolbar', { name: 'Table toolbar' });
|
|
2018
|
-
const insertedTable = editor.locator('table').last();
|
|
2019
|
-
await editor.locator('h1').click();
|
|
2020
|
-
await expect(tableToolbar).toBeHidden();
|
|
2021
|
-
await insertedTable.locator('td').first().click();
|
|
2022
|
-
expect(adminPageErrors).toEqual([]);
|
|
2023
|
-
expect(adminConsoleErrors).toEqual([]);
|
|
2024
|
-
await expect(tableToolbar).toBeVisible();
|
|
2025
|
-
expect(adminPageErrors).toEqual([]);
|
|
2026
|
-
expect(adminConsoleErrors).toEqual([]);
|
|
2027
|
-
await expect(tableToolbar.getByRole('button', { name: 'Add row before' })).toHaveAttribute(
|
|
2028
|
-
'type',
|
|
2029
|
-
'button',
|
|
2030
|
-
);
|
|
2031
|
-
await tableToolbar.getByRole('button', { name: 'Add row before' }).click();
|
|
2032
|
-
await expect(insertedTable.locator('td')).toHaveCount(6);
|
|
2033
|
-
expect(adminPageErrors).toEqual([]);
|
|
2034
|
-
await tableToolbar.getByRole('button', { name: 'Delete row' }).click();
|
|
2035
|
-
await expect(insertedTable.locator('td')).toHaveCount(3);
|
|
2036
|
-
expect(adminPageErrors).toEqual([]);
|
|
2037
|
-
await tableToolbar.getByRole('button', { name: 'Add row after' }).click();
|
|
2038
|
-
await expect(insertedTable.locator('td')).toHaveCount(6);
|
|
2039
|
-
expect(adminPageErrors).toEqual([]);
|
|
2040
|
-
await tableToolbar.getByRole('button', { name: 'Delete row' }).click();
|
|
2041
|
-
await expect(insertedTable.locator('td')).toHaveCount(3);
|
|
2042
|
-
expect(adminPageErrors).toEqual([]);
|
|
2043
|
-
await tableToolbar.getByRole('button', { name: 'Add column before' }).click();
|
|
2044
|
-
await expect(insertedTable.locator('th')).toHaveCount(4);
|
|
2045
|
-
await expect(insertedTable.locator('td')).toHaveCount(4);
|
|
2046
|
-
expect(adminPageErrors).toEqual([]);
|
|
2047
|
-
await tableToolbar.getByRole('button', { name: 'Delete column' }).click();
|
|
2048
|
-
await expect(insertedTable.locator('th')).toHaveCount(3);
|
|
2049
|
-
await expect(insertedTable.locator('td')).toHaveCount(3);
|
|
2050
|
-
expect(adminPageErrors).toEqual([]);
|
|
2051
|
-
await tableToolbar.getByRole('button', { name: 'Add column after' }).click();
|
|
2052
|
-
await expect(insertedTable.locator('th')).toHaveCount(4);
|
|
2053
|
-
await expect(insertedTable.locator('td')).toHaveCount(4);
|
|
2054
|
-
expect(adminPageErrors).toEqual([]);
|
|
2055
|
-
await tableToolbar.getByRole('button', { name: 'Delete column' }).click();
|
|
2056
|
-
await expect(insertedTable.locator('th')).toHaveCount(3);
|
|
2057
|
-
await expect(insertedTable.locator('td')).toHaveCount(3);
|
|
2058
|
-
expect(adminPageErrors).toEqual([]);
|
|
2059
|
-
await tableToolbar.getByRole('button', { name: 'Delete table' }).click();
|
|
2060
|
-
await expect(editor.locator('table')).toHaveCount(1);
|
|
2061
|
-
await expect(tableToolbar).toBeHidden();
|
|
2062
|
-
expect(adminPageErrors).toEqual([]);
|
|
2063
|
-
|
|
2064
|
-
const productUpdate = waitForApiResponse(
|
|
2065
|
-
adminPage,
|
|
2066
|
-
'PATCH',
|
|
2067
|
-
`${productActionPath}/${productId}`,
|
|
2068
|
-
);
|
|
2069
|
-
await adminPage.getByRole('button', { name: 'Submit', exact: true }).click();
|
|
2070
|
-
await productUpdate;
|
|
2071
|
-
const productResponseAfterUpdate = await adminPage.request.get(
|
|
2072
|
-
`${productActionPath}/${productId}`,
|
|
2073
|
-
{ headers },
|
|
2074
|
-
);
|
|
2075
|
-
expect(productResponseAfterUpdate.ok()).toBeTruthy();
|
|
2076
|
-
expect((await productResponseAfterUpdate.json()).productContentForm.descriptionMarkdown).toBe(
|
|
2077
|
-
markdown.replace('```ts', '```python'),
|
|
2078
|
-
);
|
|
2079
|
-
expect(adminPageErrors).toEqual([]);
|
|
2080
|
-
} finally {
|
|
2081
|
-
if (productId && headers) {
|
|
2082
|
-
const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
|
|
2083
|
-
headers,
|
|
2084
|
-
});
|
|
2085
|
-
expect(response.ok()).toBeTruthy();
|
|
2086
|
-
}
|
|
2087
|
-
if (categoryId && headers) {
|
|
2088
|
-
const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
|
|
2089
|
-
headers,
|
|
2090
|
-
});
|
|
2091
|
-
expect(response.ok()).toBeTruthy();
|
|
2092
|
-
}
|
|
2093
|
-
await adminContext.close().catch(() => {});
|
|
2094
|
-
}
|
|
2095
|
-
},
|
|
2096
|
-
);
|
|
2097
|
-
|
|
2098
|
-
test(
|
|
2099
|
-
'ATP-SPC-02: Product Content link toolbar edits Markdown',
|
|
2100
|
-
{ tag: ['@admin', '@flow', '@product'] },
|
|
2101
|
-
async ({ browser }, testInfo) => {
|
|
2102
|
-
test.setTimeout(180_000);
|
|
2103
|
-
const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
|
|
2104
|
-
const categoryName = `E2E Product Link Category ${suffix}`;
|
|
2105
|
-
const productTitle = `E2E Product Link ${suffix}`;
|
|
2106
|
-
const markdown = `# Product content ${suffix}\n\nA product description.`;
|
|
2107
|
-
const adminContext = await browser.newContext();
|
|
2108
|
-
const adminPage = await adminContext.newPage();
|
|
2109
|
-
const adminPageErrors = collectPageErrors(adminPage);
|
|
2110
|
-
const adminConsoleErrors = collectConsoleErrors(adminPage);
|
|
2111
|
-
const productListPath = '/commerce-admin/rest/resource/commerce-catalog%3Aproduct';
|
|
2112
|
-
const categoryActionPath = '/api/commerce/catalog/category';
|
|
2113
|
-
const productActionPath = '/api/commerce/catalog/product';
|
|
2114
|
-
let categoryId: number | string | undefined;
|
|
2115
|
-
let productId: number | string | undefined;
|
|
2116
|
-
let headers: { Authorization: string } | undefined;
|
|
2117
|
-
try {
|
|
2118
|
-
await adminPage.setViewportSize({ width: 1440, height: 900 });
|
|
2119
|
-
await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
2120
|
-
const accessToken = (await adminContext.cookies()).find(
|
|
2121
|
-
cookie => cookie.name === 'token',
|
|
2122
|
-
)?.value;
|
|
2123
|
-
expect(accessToken).toBeTruthy();
|
|
2124
|
-
headers = { Authorization: `Bearer ${accessToken}` };
|
|
2125
|
-
|
|
2126
|
-
const categoryResponse = await adminPage.request.post(categoryActionPath, {
|
|
2127
|
-
data: { name: categoryName, published: false },
|
|
2128
|
-
headers,
|
|
2129
|
-
});
|
|
2130
|
-
expect(categoryResponse.ok()).toBeTruthy();
|
|
2131
|
-
categoryId = (await categoryResponse.json()).data;
|
|
2132
|
-
expectTableIdentity(categoryId);
|
|
2133
|
-
|
|
2134
|
-
const productResponse = await adminPage.request.post(productActionPath, {
|
|
2135
|
-
data: {
|
|
2136
|
-
categoryId,
|
|
2137
|
-
title: productTitle,
|
|
2138
|
-
published: false,
|
|
2139
|
-
productContentForm: { descriptionMarkdown: markdown },
|
|
2140
|
-
},
|
|
2141
|
-
headers,
|
|
2142
|
-
});
|
|
2143
|
-
expect(productResponse.ok()).toBeTruthy();
|
|
2144
|
-
productId = (await productResponse.json()).data;
|
|
2145
|
-
expectTableIdentity(productId);
|
|
2146
|
-
|
|
2147
|
-
await adminPage.goto(`${productListPath}/${productId}/edit`, { waitUntil: 'load' });
|
|
2148
|
-
await expect(adminPage.locator('html')).toHaveAttribute(
|
|
2149
|
-
'data-zova-hydrated',
|
|
2150
|
-
'commerceAdmin',
|
|
2151
|
-
);
|
|
2152
|
-
const editor = adminPage.locator('[contenteditable="true"]');
|
|
2153
|
-
await expect(editor).toBeVisible();
|
|
2154
|
-
const toolbar = adminPage.getByRole('toolbar', { name: 'Markdown toolbar' });
|
|
2155
|
-
await expect(toolbar).toBeVisible();
|
|
2156
|
-
const linkButton = toolbar.getByRole('button', { name: 'Link' });
|
|
2157
|
-
await expect(linkButton).toHaveAttribute('type', 'button');
|
|
2158
|
-
await expect(linkButton).toBeDisabled();
|
|
2159
|
-
|
|
2160
|
-
await editor.locator('h1').selectText();
|
|
2161
|
-
await expect(linkButton).toBeEnabled();
|
|
2162
|
-
await linkButton.click();
|
|
2163
|
-
const linkPrompt = adminPage.locator('.fixed.inset-0.z-50');
|
|
2164
|
-
await expect(linkPrompt).toBeVisible();
|
|
2165
|
-
const linkInput = linkPrompt.locator('input[type="text"]');
|
|
2166
|
-
await linkInput.fill('https://example.com/docs');
|
|
2167
|
-
await linkInput.press('Enter');
|
|
2168
|
-
await expect(editor.locator('h1 a[href="https://example.com/docs"]')).toHaveText(
|
|
2169
|
-
`Product content ${suffix}`,
|
|
2170
|
-
);
|
|
2171
|
-
await expect(linkButton).toHaveAttribute('aria-pressed', 'true');
|
|
2172
|
-
await editor.press('ArrowLeft');
|
|
2173
|
-
await expect(linkButton).toHaveAttribute('aria-pressed', 'true');
|
|
2174
|
-
|
|
2175
|
-
await linkButton.click();
|
|
2176
|
-
await expect(linkPrompt).toBeVisible();
|
|
2177
|
-
await expect(linkInput).toHaveValue('https://example.com/docs');
|
|
2178
|
-
await linkInput.fill('https://example.com/updated');
|
|
2179
|
-
await linkPrompt.getByRole('button', { name: 'OK' }).click();
|
|
2180
|
-
await expect(editor.locator('h1 a[href="https://example.com/updated"]')).toHaveText(
|
|
2181
|
-
`Product content ${suffix}`,
|
|
2182
|
-
);
|
|
2183
|
-
|
|
2184
|
-
await linkButton.click();
|
|
2185
|
-
await expect(linkPrompt).toBeVisible();
|
|
2186
|
-
await linkPrompt.getByRole('button', { name: 'Cancel' }).click();
|
|
2187
|
-
await expect(editor.locator('h1 a[href="https://example.com/updated"]')).toHaveText(
|
|
2188
|
-
`Product content ${suffix}`,
|
|
2189
|
-
);
|
|
2190
|
-
|
|
2191
|
-
await editor.locator('h1').selectText();
|
|
2192
|
-
await expect(linkButton).toHaveAttribute('aria-pressed', 'true');
|
|
2193
|
-
await linkButton.click();
|
|
2194
|
-
await expect(linkPrompt).toBeVisible();
|
|
2195
|
-
await linkInput.fill('');
|
|
2196
|
-
await linkInput.press('Enter');
|
|
2197
|
-
await expect(editor.locator('h1 a')).toHaveCount(0);
|
|
2198
|
-
await adminPage.getByRole('button', { name: 'Submit', exact: true }).click();
|
|
2199
|
-
const productResponseAfterLinkRemoval = await adminPage.request.get(
|
|
2200
|
-
`${productActionPath}/${productId}`,
|
|
2201
|
-
{ headers },
|
|
2202
|
-
);
|
|
2203
|
-
expect(productResponseAfterLinkRemoval.ok()).toBeTruthy();
|
|
2204
|
-
expect(
|
|
2205
|
-
(await productResponseAfterLinkRemoval.json()).productContentForm.descriptionMarkdown,
|
|
2206
|
-
).not.toContain('https://example.com/updated');
|
|
2207
|
-
expect(adminPageErrors).toEqual([]);
|
|
2208
|
-
expect(adminConsoleErrors).toEqual([]);
|
|
2209
|
-
} finally {
|
|
2210
|
-
if (productId && headers) {
|
|
2211
|
-
const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
|
|
2212
|
-
headers,
|
|
2213
|
-
});
|
|
2214
|
-
expect(response.ok()).toBeTruthy();
|
|
2215
|
-
}
|
|
2216
|
-
if (categoryId && headers) {
|
|
2217
|
-
const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
|
|
2218
|
-
headers,
|
|
2219
|
-
});
|
|
2220
|
-
expect(response.ok()).toBeTruthy();
|
|
2221
|
-
}
|
|
2222
|
-
await adminContext.close().catch(() => {});
|
|
2223
|
-
}
|
|
2224
|
-
},
|
|
2225
|
-
);
|
|
2226
|
-
|
|
2227
1731
|
test(
|
|
2228
1732
|
'ATP-SPC-02: SKU renders semantic Admin currency and lifecycle controls',
|
|
2229
1733
|
{ tag: ['@admin', '@flow', '@sku'] },
|