import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; function collectPageErrors(page: Page) { const errors: Error[] = []; page.on('pageerror', error => { errors.push(error); }); return errors; } function collectConsoleErrors(page: Page) { const errors: string[] = []; page.on('console', message => { if (message.type() === 'error' || /hydration mismatch/i.test(message.text())) { errors.push(message.text()); } }); return errors; } function waitForApiResponse(page: Page, method: string, path: string | RegExp) { return page.waitForResponse(response => { const url = new URL(response.url()); return ( response.request().method() === method && (typeof path === 'string' ? url.pathname === path : path.test(url.pathname)) && !response.request().headers()['x-vona-openapi-schema'] ); }); } function expectTableIdentity(value: unknown) { expect(['string', 'number']).toContain(typeof value); } async function login( page: Page, path: string, username: string, password: string, hydratedSite: string, ) { await page.goto(path, { waitUntil: 'load' }); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', hydratedSite); if (!page.url().includes('/login')) { await page.waitForURL(/\/login(?:\?|$)/); } if (page.url().includes('/login')) { const usernameInput = page.getByPlaceholder('Your Username'); const passwordInput = page.getByPlaceholder('Your Password'); await usernameInput.fill(username); await passwordInput.fill(password); await expect(usernameInput).toHaveValue(username); await expect(passwordInput).toHaveValue(password); await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue(''); const loginResponse = waitForApiResponse(page, 'POST', '/api/home/user/passport/login'); await page.getByRole('button', { name: 'Login', exact: true }).click(); const loginResponseValue = await loginResponse; if (!loginResponseValue.ok()) { throw new Error( `login failed: ${loginResponseValue.status()} ${await loginResponseValue.text()}`, ); } await expect(page).not.toHaveURL(/\/login(?:\?|$)/); } } test( 'Commerce Product Content: SSR renders stored safe HTML and hydrates without errors', { tag: ['@web', '@flow', '@product'] }, async ({ browser }, testInfo) => { test.setTimeout(60_000); const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`; const categoryName = `E2E Product Content Web Category ${suffix}`; const productTitle = `E2E Product Content Web ${suffix}`; const skuCode = `E2E-PC-WEB-${suffix}`; 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`; const unsafeHtml = 'unsafe'; const adminContext = await browser.newContext(); const adminPage = await adminContext.newPage(); const webContext = await browser.newContext(); const webPage = await webContext.newPage(); const webPageErrors = collectPageErrors(webPage); const webConsoleErrors = collectConsoleErrors(webPage); const categoryActionPath = '/api/commerce/catalog/category'; const productActionPath = '/api/commerce/catalog/product'; const skuActionPath = '/api/commerce/catalog/sku'; const stockAdjustPath = '/api/commerce/trade/stockBalance/adjustStock'; let categoryId: number | string | undefined; let productId: number | string | undefined; let skuId: number | string | undefined; let headers: { Authorization: string } | undefined; try { await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin'); const accessToken = (await adminContext.cookies()).find( cookie => cookie.name === 'token', )?.value; expect(accessToken).toBeTruthy(); headers = { Authorization: `Bearer ${accessToken}` }; const categoryResponse = await adminPage.request.post(categoryActionPath, { data: { name: categoryName, published: true }, headers, }); expect(categoryResponse.ok()).toBeTruthy(); categoryId = (await categoryResponse.json()).data; expectTableIdentity(categoryId); const productResponse = await adminPage.request.post(productActionPath, { data: { categoryId, title: productTitle, published: true, productContentForm: { descriptionMarkdown: `${markdown}\n\n${unsafeHtml}`, }, }, headers, }); expect(productResponse.ok()).toBeTruthy(); productId = (await productResponse.json()).data; expectTableIdentity(productId); const skuResponse = await adminPage.request.post(skuActionPath, { data: { code: skuCode, productId, priceCents: 1999, lifecycle: 'active' }, headers, }); expect(skuResponse.ok()).toBeTruthy(); skuId = (await skuResponse.json()).data; expectTableIdentity(skuId); const stockResponse = await adminPage.request.post(stockAdjustPath, { data: { skuId, delta: 1, reason: 'Product content SSR E2E fixture', correlationId: `product-content-web-${suffix}`, }, headers, }); expect(stockResponse.ok()).toBeTruthy(); const productPath = `/commerce/product/${productId}`; const ssrResponse = await webPage.request.get(productPath); expect(ssrResponse.ok()).toBeTruthy(); const ssrHtml = await ssrResponse.text(); expect(ssrHtml.toLowerCase()).not.toContain('data-zova-hydrated'); expect(ssrHtml).toContain(`

Product details ${suffix}

`); expect(ssrHtml).toContain('durable'); expect(ssrHtml).toContain(`highlighted product ${suffix}`); expect(ssrHtml).toMatch(/[\\s\\S]*${suffix}[\\s\\S]*`), ); expect(ssrHtml).toContain('