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.
Files changed (28) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/playwright-e2e.yml +2 -2
  3. package/CHANGELOG.md +14 -0
  4. package/cabloy-docs/backend/markdown-guide.md +3 -2
  5. package/cabloy-docs/frontend/markdown-guide.md +1 -1
  6. package/e2e/specs/a-commerce.spec.ts +0 -496
  7. package/e2e/specs/markdown.spec.ts +575 -0
  8. package/package.json +3 -3
  9. package/vona/pnpm-lock.yaml +25 -0
  10. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/package.json +2 -1
  11. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/bean/bean.markdown.ts +47 -2
  12. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  13. package/zova/packages-utils/zova-jsx/package.json +2 -2
  14. package/zova/packages-zova/zova/package.json +3 -3
  15. package/zova/packages-zova/zova-core/package.json +1 -1
  16. package/zova/packages-zova/zova-core/src/core/component/locale.ts +2 -0
  17. package/zova/src/suite/a-home/modules/home-base/src/.metadata/index.ts +2 -17
  18. package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +0 -6
  19. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +1 -4
  20. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.locale.tsx +1 -1
  21. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +1 -4
  22. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.locale.tsx +1 -1
  23. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  24. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/monkey.ts +1 -0
  25. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  26. package/zova/src/suite-vendor/a-zova/package.json +3 -3
  27. package/zova/src/suite/a-home/modules/home-base/src/service/locale.ts +0 -11
  28. /package/e2e/specs/{account.spec.ts → home-user-account.spec.ts} +0 -0
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.144
1
+ 5.1.145
@@ -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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.145
4
+
5
+ ### Features
6
+
7
+ - Add Markdown support and code styling.
8
+ - Add the `$serviceLocale` service locale capability.
9
+ - Add home-user specifications.
10
+ - Add update functionality.
11
+
12
+ ### Improvements
13
+
14
+ - Update SSR menu product configuration.
15
+ - Update package configuration and Playwright end-to-end workflow.
16
+
3
17
  ## 5.1.144
4
18
 
5
19
  ### 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
- Arbitrary classes and inline CSS do not become part of the output contract.
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
  ![Product image](https://example.com/product.png)
256
256
  ````
257
257
 
258
- The interactive editor decorates supported code blocks with lowlight token spans for syntax highlighting. The backend projection continues to render semantic `<code class="language-*">` output from Markdown; it does not persist editor token spans. Do not infer support for arbitrary raw HTML, iframe embeds, inline styles, `data:` URLs, or custom classes from the presence of a TipTap extension.
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'] },