import type { APIRequestContext, Browser, Locator, Page, TestInfo } from '@playwright/test'; import { expect, test } from '@playwright/test'; const privateMarkers = ['cart', 'address', 'order', 'coupon', 'payment']; const addressResourceUrl = /\/commerce-admin\/rest\/resource\/commerce-member(?:%3A|:|%253A)address(?:[/?#]|$)/; const addressMinePath = '/api/commerce/member/address/mine'; const addressActionPath = '/api/commerce/member/address'; const passportTestActivateCurrentPath = '/api/home/user/passportTest/activateCurrent'; interface IAddressFixture { addressLine1: string; addressLine2: string; city: string; countryCode: string; phone: string; postalCode: string; recipientName: string; region: string; updatedCity: string; } 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); } function waitForAddressResponse(page: Page, method: string, path: string | RegExp) { return waitForApiResponse(page, method, path); } function waitForAddressMine(page: Page) { return waitForAddressResponse(page, 'GET', addressMinePath); } async function getAdminOrderRow(page: Page, orderId: number): Promise { const row = page.locator(`tr:has(td a:text-is("${orderId}"))`); await expect(row).toHaveCount(1); return row; } 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(?:\?|$)/); } } async function registerCustomer( request: APIRequestContext, testInfo: TestInfo, ): Promise<{ password: string; username: string }> { const id = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`; const username = `e2e-address-${id}`; const password = 'address-e2e-password'; const captchaResponse = await request.post('/api/captcha/create', { data: { scene: 'captcha-simple:simple' }, }); expect(captchaResponse.ok()).toBeTruthy(); const captcha = (await captchaResponse.json()).data; expect(captcha?.id).toEqual(expect.any(String)); expect(captcha?.token).toEqual(expect.any(String)); const baseURL = testInfo.project.use.baseURL; if (!baseURL) throw new Error('commerce E2E base URL is unavailable'); const consumerUrl = new URL('/home/user/activation', baseURL).toString(); const registerResponse = await request.post('/api/home/user/passport/register', { data: { username, email: `${username}@example.test`, password, passwordConfirm: password, consumerUrl, captcha: { id: captcha.id, token: captcha.token }, }, }); expect(registerResponse.ok()).toBeTruthy(); const registration = (await registerResponse.json()).data; expect(registration?.jwt?.accessToken).toEqual(expect.any(String)); const activateResponse = await request.post(passportTestActivateCurrentPath, { headers: { Authorization: `Bearer ${registration.jwt.accessToken}` }, }); expect(activateResponse.ok()).toBeTruthy(); return { username, password }; } function createAddressFixture(testInfo: TestInfo): IAddressFixture { const id = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`; return { recipientName: `E2E Address ${id}`, phone: '15555550123', countryCode: 'US', region: 'California', city: `Original City ${id}`, postalCode: '94105', addressLine1: `1 Test Street ${id}`, addressLine2: 'Suite E2E', updatedCity: `Updated City ${id}`, }; } function getAddressTextbox(page: Page, name: string): Locator { return page.getByRole('group', { name, exact: true }).getByRole('textbox'); } async function fillAddressForm(page: Page, fixture: IAddressFixture) { await getAddressTextbox(page, 'Recipient Name *').fill(fixture.recipientName); await getAddressTextbox(page, 'Phone *').fill(fixture.phone); await getAddressTextbox(page, 'Country Code *').fill(fixture.countryCode); await getAddressTextbox(page, 'Region *').fill(fixture.region); await getAddressTextbox(page, 'City *').fill(fixture.city); await getAddressTextbox(page, 'Postal Code *').fill(fixture.postalCode); await getAddressTextbox(page, 'Address Line 1 *').fill(fixture.addressLine1); await getAddressTextbox(page, 'Address Line 2').fill(fixture.addressLine2); } async function createAddressThroughCustomerPage( browser: Browser, request: APIRequestContext, testInfo: TestInfo, ) { const customer = await registerCustomer(request, testInfo); const fixture = createAddressFixture(testInfo); const context = await browser.newContext(); const page = await context.newPage(); const pageErrors = collectPageErrors(page); await login( page, '/commerce/commerce/member/address', customer.username, customer.password, 'commerce', ); await expect(page).toHaveURL(/\/commerce\/commerce\/member\/address(?:\?|$)/); await expect(page.getByRole('heading', { name: 'Addresses' })).toBeVisible(); await expect(page.getByText('No addresses yet.')).toBeVisible(); await fillAddressForm(page, fixture); const createResponse = waitForAddressResponse(page, 'POST', `${addressActionPath}/createMine`); const mineResponse = waitForAddressMine(page); await page.getByRole('button', { name: 'Save address', exact: true }).click(); await createResponse; await mineResponse; await expect(page.getByRole('heading', { name: fixture.recipientName })).toBeVisible(); return { context, fixture, page, pageErrors }; } async function deleteAddressThroughCustomerPage(page: Page, fixture: IAddressFixture) { const card = page .locator('article') .filter({ has: page.getByRole('heading', { name: fixture.recipientName }) }); const deleteResponse = waitForAddressResponse(page, 'DELETE', /\/deleteMine\/[^/]+$/); const mineResponse = waitForAddressMine(page); await card.getByRole('button', { name: 'Delete', exact: true }).click(); await deleteResponse; await mineResponse; await expect(page.getByRole('heading', { name: fixture.recipientName })).toHaveCount(0); await expect(page.getByText('No addresses yet.')).toBeVisible(); } test( 'ATP-SSR-01: anonymous Commerce HTML is public and hydration completes', { tag: ['@web', '@smoke'] }, async ({ page, request }) => { const response = await request.get('/commerce'); expect(response.ok()).toBeTruthy(); expect(response.headers()['cache-control']).toBe('no-cache, no-store, must-revalidate'); const html = await response.text(); const normalizedHtml = html.toLowerCase(); expect(normalizedHtml).not.toContain('data-zova-hydrated'); for (const marker of privateMarkers) { expect(normalizedHtml).not.toContain(`"${marker}"`); } const pageErrors = collectPageErrors(page); const documentResponse = await page.goto('/commerce', { waitUntil: 'load' }); expect(documentResponse?.ok()).toBeTruthy(); await expect(page).toHaveURL(/\/commerce(?:\/|$)/); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible(); await expect(page.getByText('Dashboard')).toHaveCount(0); await expect(page.locator('body')).toBeVisible(); await expect(page).not.toHaveTitle(/error/i); expect(pageErrors).toEqual([]); }, ); test( 'Commerce theme: public local preference mirrors session cookie without hydration mismatch', { tag: ['@web', '@ssr', '@theme'] }, async ({ browser, request }, testInfo) => { const customer = await registerCustomer(request, testInfo); const context = await browser.newContext(); await context.addInitScript(() => localStorage.setItem('themedark', 'true')); const page = await context.newPage(); const pageErrors = collectPageErrors(page); const consoleErrors: string[] = []; page.on('console', message => { if (message.type() === 'error' || /hydration mismatch/i.test(message.text())) { consoleErrors.push(message.text()); } }); try { await page.goto('/commerce', { waitUntil: 'load' }); await page.evaluate(() => localStorage.setItem('themedark', 'true')); await page.reload({ waitUntil: 'load' }); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark'); expect((await context.cookies()).find(cookie => cookie.name === 'themedark')?.value).toBe( 'true', ); await login( page, '/commerce/commerce/member/address', customer.username, customer.password, 'commerce', ); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark'); expect(pageErrors).toEqual([]); expect(consoleErrors).toEqual([]); } finally { await context.close(); } }, ); test( 'Commerce theme: session cookie mirrors public local preference without hydration mismatch', { tag: ['@web', '@ssr', '@theme'] }, async ({ browser, request }, testInfo) => { const customer = await registerCustomer(request, testInfo); const context = await browser.newContext(); const page = await context.newPage(); const pageErrors = collectPageErrors(page); const consoleErrors = collectConsoleErrors(page); try { await login( page, '/commerce/commerce/member/address', customer.username, customer.password, 'commerce', ); await context.addCookies([ { name: 'themedark', value: 'false', url: new URL(page.url()).origin, }, ]); await page.reload({ waitUntil: 'load' }); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.locator('body')).toHaveAttribute('data-theme', 'light'); await expect.poll(() => page.evaluate(() => localStorage.getItem('themedark'))).toBe('false'); expect(pageErrors).toEqual([]); expect(consoleErrors).toEqual([]); const publicContext = await browser.newContext(); await publicContext.addInitScript(() => localStorage.setItem('themedark', 'false')); const publicPage = await publicContext.newPage(); const publicPageErrors = collectPageErrors(publicPage); const publicConsoleErrors = collectConsoleErrors(publicPage); try { await publicPage.goto('/commerce', { waitUntil: 'load' }); await expect(publicPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(publicPage.locator('body')).toHaveAttribute('data-theme', 'light'); await expect .poll(() => publicPage.evaluate(() => localStorage.getItem('themedark'))) .toBe('false'); expect( (await publicContext.cookies()).find(cookie => cookie.name === 'themedark')?.value, ).toBe('false'); expect(publicPageErrors).toEqual([]); expect(publicConsoleErrors).toEqual([]); } finally { await publicContext.close(); } } finally { await context.close(); } }, ); test( 'Commerce theme: public auto preference mirrors resolved media value without hydration mismatch', { tag: ['@web', '@ssr', '@theme'] }, async ({ browser }) => { const context = await browser.newContext(); await context.addInitScript(() => localStorage.setItem('themedark', JSON.stringify('auto'))); const page = await context.newPage(); await page.emulateMedia({ colorScheme: 'dark' }); const pageErrors = collectPageErrors(page); const consoleErrors = collectConsoleErrors(page); try { await page.goto('/commerce', { waitUntil: 'load' }); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark'); await expect .poll(() => page.evaluate(() => localStorage.getItem('themedark'))) .toBe('"auto"'); expect((await context.cookies()).find(cookie => cookie.name === 'themedark')?.value).toBe( 'true', ); expect(pageErrors).toEqual([]); expect(consoleErrors).toEqual([]); } finally { await context.close(); } }, ); test( 'Commerce public SSR: theme cookie does not change SSR HTML', { tag: ['@web', '@ssr', '@theme'] }, async ({ request }) => { const responses = await Promise.all([ request.get('/commerce'), request.get('/commerce', { headers: { Cookie: 'themedark=true' } }), request.get('/commerce', { headers: { Cookie: 'themedark=false' } }), ]); const html = await Promise.all(responses.map(response => response.text())); for (const [index, response] of responses.entries()) { expect(response.status(), `public response ${index}`).toBe(200); expect(response.headers()['cache-control'], `public response ${index}`).toBe( 'no-cache, no-store, must-revalidate', ); expect(html[index].toLowerCase()).not.toContain('data-zova-hydrated'); expect(html[index]).toContain('data-ssr-theme-dark-false="light"'); expect(html[index]).toContain('data-ssr-theme-dark-true="dark"'); expect(html[index]).not.toContain('ssr_cookie_themedark'); expect(html[index]).toContain("window.matchMedia('(prefers-color-scheme: dark)')"); expect(html[index]).not.toMatch(/]*data-theme="(?:dark|light)"/); } }, ); test( 'Commerce session SSR: theme cookie selects raw server theme', { tag: ['@web', '@ssr', '@theme'] }, async ({ browser, request }, testInfo) => { const customer = await registerCustomer(request, testInfo); const context = await browser.newContext(); const page = await context.newPage(); try { await login( page, '/commerce/commerce/member/address', customer.username, customer.password, 'commerce', ); const origin = new URL(page.url()).origin; const authCookies = (await context.cookies()).filter(cookie => cookie.name !== 'themedark'); const getSessionHtml = async (value: string) => { const sessionContext = await browser.newContext(); try { await sessionContext.addCookies([ ...authCookies.map(({ domain, path, ...cookie }) => ({ ...cookie, url: origin })), { name: 'themedark', value, url: origin }, ]); const response = await sessionContext.request.get('/commerce/commerce/member/address'); return { response, html: await response.text() }; } finally { await sessionContext.close(); } }; for (const [value, theme] of [ ['true', 'dark'], ['false', 'light'], ] as const) { const { response, html } = await getSessionHtml(value); expect(response.status()).toBe(200); expect(response.headers()['cache-control']).toBe('private, no-store'); expect(html).toContain(`data-theme="${theme}"`); expect(html).not.toContain('data-ssr-theme-dark-false'); expect(html).not.toContain('data-ssr-theme-dark-true'); expect(html).not.toContain("window.matchMedia('(prefers-color-scheme: dark)')"); expect(html).toContain('Addresses'); } } finally { await context.close(); } }, ); test( 'Commerce catalogue: public sellable inventory renders after hydration', { tag: ['@web', '@flow'] }, async ({ page }) => { const pageErrors = collectPageErrors(page); const response = await page.goto('/commerce', { waitUntil: 'load' }); expect(response?.ok()).toBeTruthy(); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Wireless Headphones' })).toBeVisible(); await expect(page.getByText('24 available')).toBeVisible(); await page.getByRole('link', { name: 'Wireless Headphones' }).click(); await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/); await expect(page.getByRole('heading', { name: 'Wireless Headphones' })).toBeVisible(); await expect(page.getByText('HPH-BLK')).toBeVisible(); expect(pageErrors).toEqual([]); }, ); test( 'Commerce catalogue: localized Product navigation preserves locale', { tag: ['@web', '@flow'] }, async ({ page, request }) => { const localizedPath = '/commerce/zh-cn'; const response = await request.get(localizedPath); expect(response.ok()).toBeTruthy(); const html = await response.text(); expect(html.toLowerCase()).not.toContain('data-zova-hydrated'); expect(html).toContain('商品目录'); expect(html).toContain('当前价格和库存以结算时确认为准。'); const pageErrors = collectPageErrors(page); const documentResponse = await page.goto(localizedPath, { waitUntil: 'load' }); expect(documentResponse?.ok()).toBeTruthy(); await expect(page).toHaveURL(/\/commerce\/zh-cn(?:\/|$)/); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByRole('heading', { name: '商品目录' })).toBeVisible(); await expect(page.getByText('当前价格和库存以结算时确认为准。')).toBeVisible(); await expect(page.getByText('库存 24 件')).toBeVisible(); const productLink = page.getByRole('link', { name: 'Wireless Headphones' }); await expect(productLink).toHaveAttribute('href', /\/commerce\/zh-cn\/product\/\d+$/); const productPath = await productLink.getAttribute('href'); const productResponse = await request.get(productPath!); expect(productResponse.ok()).toBeTruthy(); expect((await productResponse.text()).toLowerCase()).not.toContain('data-zova-hydrated'); await productLink.click(); await expect(page).toHaveURL(/\/commerce\/zh-cn\/product\/\d+(?:\/|$)/); await expect(page.getByRole('heading', { name: 'Wireless Headphones' })).toBeVisible(); await expect(page.getByText('HPH-BLK')).toBeVisible(); await expect(page.getByText('库存 24 件')).toBeVisible(); await expect(page.getByRole('link', { name: '购物车' })).toHaveAttribute( 'href', '/commerce/commerce/trade/cart', ); expect(pageErrors).toEqual([]); }, ); test( 'Commerce Product: Cart badge keeps SSR initial render neutral', { tag: ['@web', '@cart'] }, async ({ page, request }) => { const productPath = '/commerce/product/1'; const productResponse = await request.get(productPath); expect(productResponse.ok()).toBeTruthy(); const productHtml = await productResponse.text(); expect(productHtml.toLowerCase()).not.toContain('data-zova-hydrated'); expect(productHtml).toContain('>Cart'); const pageErrors = collectPageErrors(page); const documentResponse = await page.goto(productPath, { waitUntil: 'load' }); expect(documentResponse?.ok()).toBeTruthy(); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page).toHaveURL(productPath); await expect(page.getByRole('heading', { name: 'Wireless Headphones' })).toBeVisible(); const cartLink = page.locator('a[href="/commerce/commerce/trade/cart"]'); await expect(cartLink).toHaveCount(1); await expect(cartLink.locator('.badge')).toHaveCount(0); expect(pageErrors).toEqual([]); }, ); test( 'Commerce Address: session SSR protects an anonymous document request', { tag: ['@web', '@cart'] }, async ({ page, request }) => { const path = '/commerce/commerce/member/address'; const routePath = '/commerce/member/address'; const response = await request.get(path, { maxRedirects: 0 }); expect(response.status()).toBe(302); expect(response.headers()['cache-control']).toBe('private, no-store'); const html = await response.text(); expect(html).not.toContain('ssr-body-ready-observer'); expect(html).not.toContain('__leftDrawerOpenJS'); expect(response.headers().location).toMatch(/^\/commerce\/login\?(?:.*&)?returnTo=/); const pageErrors = collectPageErrors(page); const documentResponse = await page.goto(path, { waitUntil: 'load' }); expect(documentResponse?.ok()).toBeTruthy(); await expect(page).toHaveURL(/\/commerce\/login\?(?:.*&)?returnTo=/); const loginUrl = new URL(page.url()); expect(loginUrl.searchParams.getAll('returnTo')).toEqual([routePath]); await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Addresses' })).toHaveCount(0); expect(pageErrors).toEqual([]); }, ); test( 'ATP-ADDR-01: authenticated customer manages Address through Web self-service', { tag: ['@web', '@flow', '@address'] }, async ({ browser, request }, testInfo) => { const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage( browser, request, testInfo, ); try { const addressResponse = await context.request.get('/commerce/commerce/member/address'); expect(addressResponse.ok()).toBeTruthy(); expect(addressResponse.headers()['cache-control']).toBe('private, no-store'); const addressHtml = await addressResponse.text(); expect(addressHtml.toLowerCase()).not.toContain('data-zova-hydrated'); expect(addressHtml).not.toContain('ssr-body-ready-observer'); expect(addressHtml).not.toContain('__leftDrawerOpenJS'); expect(addressHtml).toContain(fixture.recipientName); expect(addressHtml).toContain(fixture.addressLine1); const coldPage = await context.newPage(); try { const coldPageErrors = collectPageErrors(coldPage); const coldResponse = await coldPage.goto('/commerce/commerce/member/address', { waitUntil: 'load', }); expect(coldResponse?.ok()).toBeTruthy(); await expect(coldPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(coldPage.getByRole('heading', { name: 'Addresses' })).toBeVisible(); const coldCard = coldPage .locator('article') .filter({ has: coldPage.getByRole('heading', { name: fixture.recipientName }) }); await expect(coldCard).toContainText(fixture.addressLine1); await expect(coldPage.getByRole('alert')).toHaveCount(0); expect(coldPageErrors).toEqual([]); } finally { await coldPage.close(); } const card = page .locator('article') .filter({ has: page.getByRole('heading', { name: fixture.recipientName }) }); await card.getByRole('button', { name: 'Edit', exact: true }).click(); await expect(page.getByRole('heading', { name: 'Add address' })).toHaveCount(0); const cityTextbox = getAddressTextbox(page, 'City *'); await expect(cityTextbox).toHaveValue(fixture.city); await cityTextbox.fill(fixture.updatedCity); const updateResponse = waitForAddressResponse(page, 'PATCH', /\/updateMine\/[^/]+$/); const mineResponse = waitForAddressMine(page); await page.getByRole('button', { name: 'Save address', exact: true }).click(); await updateResponse; await mineResponse; await expect(card).toContainText(fixture.updatedCity); await expect(card).not.toContainText(fixture.city); await deleteAddressThroughCustomerPage(page, fixture); await expect(page.getByRole('alert')).toHaveCount(0); expect(pageErrors).toEqual([]); } finally { await context.close(); } }, ); test( 'Phase 50/60: authenticated customer completes payment and observes operator shipment', { tag: ['@web', '@admin', '@flow', '@payment', '@shipment'] }, async ({ browser, request }, testInfo) => { test.setTimeout(60_000); const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage( browser, request, testInfo, ); try { await page.goto('/commerce', { waitUntil: 'load' }); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible(); const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true }); await expect(productLink).toBeVisible(); await productLink.click(); await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/); await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible(); await expect(page.getByText('COF-SET-01')).toBeVisible(); await expect(page.getByText('$45.99')).toBeVisible(); const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items'); await page.getByRole('button', { name: 'Add to cart', exact: true }).click(); await addResponse; await page.getByRole('link', { name: /^Cart/ }).click(); await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/); await expect(page.getByRole('heading', { name: 'Cart' })).toBeVisible(); const cartItem = page.locator('article').filter({ has: page.getByRole('spinbutton') }); await expect(cartItem).toContainText('COF-SET-01'); await expect(cartItem).toContainText('$45.99'); await expect(cartItem.getByRole('spinbutton')).toHaveValue('1'); await page.getByRole('link', { name: 'Checkout', exact: true }).click(); await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/checkout(?:\/|$)/); await expect(page.getByRole('heading', { name: 'Checkout' })).toBeVisible(); const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) }); await expect(addressChoice).toBeVisible(); await addressChoice.check(); const noCouponChoice = page.getByRole('radio', { name: 'No coupon', exact: true }); await noCouponChoice.check(); await expect(noCouponChoice).toBeChecked(); await expect(page.getByRole('button', { name: 'Create order', exact: true })).toBeEnabled(); const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout'); await page.getByRole('button', { name: 'Create order', exact: true }).click(); const checkoutResponseValue = await checkoutResponse; expect(checkoutResponseValue.ok()).toBeTruthy(); const checkout = (await checkoutResponseValue.json()).data; expectTableIdentity(checkout.orderId); expectTableIdentity(checkout.paymentAttemptId); expectTableIdentity(checkout.paymentSessionId); expect(checkout.state).toBe('awaiting_payment'); expect(checkout.paymentAttemptState).toBe('created'); expect(checkout.currency).toBe('USD'); expect(checkout.payableTotalCents).toBe(4599); await expect(page).toHaveURL( new RegExp( `/commerce/commerce/trade/payment/${checkout.paymentSessionId}/${checkout.orderId}(?:/|$)`, ), ); await expect(page.getByRole('heading', { name: 'Payment', exact: true })).toBeVisible(); const startResponse = waitForApiResponse( page, 'POST', new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}/start$`), ); await page.getByRole('button', { name: 'Start payment', exact: true }).click(); expect((await startResponse).ok()).toBeTruthy(); await expect(page.getByText('Payment is being prepared.')).toBeVisible(); await expect(page.getByRole('heading', { name: 'Mock payment simulator' })).toBeVisible(); const completeResponse = waitForApiResponse( page, 'POST', new RegExp(`/api/pay/mock/payment-session/${checkout.paymentSessionId}/complete$`), ); await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click(); expect((await completeResponse).ok()).toBeTruthy(); await expect(page).toHaveURL( new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`), { timeout: 40_000, }, ); const orderPath = `/commerce/commerce/trade/order/${checkout.orderId}`; const orderResponse = await context.request.get(orderPath); expect(orderResponse.ok()).toBeTruthy(); expect(orderResponse.headers()['cache-control']).toBe('private, no-store'); const orderHtml = await orderResponse.text(); expect(orderHtml.toLowerCase()).not.toContain('data-zova-hydrated'); expect(orderHtml).toContain(`Order #${checkout.orderId}`); expect(orderHtml).toContain('Paid · $45.99'); expect(orderHtml).toContain('USD'); expect(orderHtml).toContain(fixture.recipientName); expect(orderHtml).toContain(fixture.addressLine1); expect(orderHtml).toContain('Discount: $0.00'); expect(orderHtml).toContain('Pour-Over Coffee Set'); expect(orderHtml).toContain('COF-SET-01'); expect(orderHtml).toContain('1 × $45.99 = $45.99'); const orderDocumentResponse = await page.reload({ waitUntil: 'load' }); expect(orderDocumentResponse?.ok()).toBeTruthy(); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByRole('heading', { name: `Order #${checkout.orderId}` })).toBeVisible(); await expect(page.getByText('paid · $45.99')).toBeVisible(); await expect(page.getByText(fixture.recipientName)).toBeVisible(); await expect(page.getByText(fixture.addressLine1)).toBeVisible(); await expect(page.getByText('Discount: $0.00')).toBeVisible(); await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible(); await expect(page.getByText('COF-SET-01')).toBeVisible(); await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible(); const shipmentCarrier = 'Cabloy Express'; const shipmentTrackingNumber = `E2E-${checkout.orderId}`; const adminContext = await browser.newContext(); const adminPage = await adminContext.newPage(); const adminPageErrors = collectPageErrors(adminPage); try { await adminPage.setViewportSize({ width: 1440, height: 900 }); await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin'); await adminPage.goto('/commerce-admin/rest/resource/commerce-trade%3Aorder', { waitUntil: 'load', }); await expect(adminPage.locator('html')).toHaveAttribute( 'data-zova-hydrated', 'commerceAdmin', ); const orderRow = await getAdminOrderRow(adminPage, checkout.orderId); await expect(orderRow).toBeVisible(); await expect(orderRow.locator('td')).toHaveCount(6); await expect(orderRow).toContainText('Paid'); await expect(orderRow).toContainText('45.99'); await expect(orderRow).not.toContainText(fixture.recipientName); const orderViewResponse = waitForApiResponse( adminPage, 'GET', `/api/commerce/trade/order/${checkout.orderId}`, ); await orderRow.getByRole('link', { name: String(checkout.orderId), exact: true }).click(); expect((await orderViewResponse).ok()).toBeTruthy(); await expect(adminPage.getByText('Money summary').first()).toBeVisible(); await expect(adminPage.getByText('Delivery address').first()).toBeVisible(); await expect(adminPage.getByText('Purchased lines').first()).toBeVisible(); await expect(adminPage.getByText('addressSnapshot').first()).toBeVisible(); await expect(adminPage.getByText('couponSnapshot').first()).toBeVisible(); await expect(adminPage.getByText('Pour-Over Coffee Set')).toBeVisible(); await expect(adminPage.getByText('COF-SET-01')).toBeVisible(); await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0); await adminPage.getByRole('button', { name: 'Back', exact: true }).click(); await expect(orderRow).toBeVisible(); const carrierInput = orderRow.getByPlaceholder('Carrier'); const trackingNumberInput = orderRow.getByPlaceholder('Tracking number'); const confirmationCheckbox = orderRow.getByRole('checkbox'); await carrierInput.fill(shipmentCarrier); await trackingNumberInput.fill(shipmentTrackingNumber); await confirmationCheckbox.check(); await expect(carrierInput).toHaveValue(shipmentCarrier); await expect(trackingNumberInput).toHaveValue(shipmentTrackingNumber); await expect(confirmationCheckbox).toBeChecked(); const shipmentResponse = waitForApiResponse( adminPage, 'POST', `/api/commerce/trade/order/${checkout.orderId}/ship`, ); await orderRow.getByRole('button', { name: 'Ship order', exact: true }).click(); expect((await shipmentResponse).ok()).toBeTruthy(); await expect(orderRow).toContainText('Shipped'); expect(adminPageErrors).toEqual([]); } finally { await adminContext.close().catch(() => {}); } const shippedDocumentResponse = await page.reload({ waitUntil: 'load' }); expect(shippedDocumentResponse?.ok()).toBeTruthy(); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByText('shipped · $45.99')).toBeVisible(); await expect(page.getByRole('heading', { name: 'Shipment' })).toBeVisible(); await expect(page.getByText(shipmentCarrier)).toBeVisible(); await expect(page.getByText(shipmentTrackingNumber)).toBeVisible(); const ordersDocumentResponse = await page.goto('/commerce/commerce/trade/orders', { waitUntil: 'load', }); expect(ordersDocumentResponse?.ok()).toBeTruthy(); await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/orders(?:\/|$)/); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByRole('heading', { name: 'My orders' })).toBeVisible(); const orderCard = page .locator('article') .filter({ has: page.getByRole('heading', { name: `Order #${checkout.orderId}` }) }); await expect(orderCard).toContainText('shipped · $45.99'); const historyDetailResponse = waitForApiResponse( page, 'GET', new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`), ); await orderCard.getByRole('button', { name: 'View', exact: true }).click(); await expect(page).toHaveURL( new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`), ); await historyDetailResponse; await expect(page.getByRole('heading', { name: `Order #${checkout.orderId}` })).toBeVisible(); await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible(); await expect(page.getByRole('alert')).toHaveCount(0); expect(pageErrors).toEqual([]); } finally { await context.close().catch(() => {}); } }, ); test( 'Payment callback: authenticated direct route reconciles after hydration', { tag: ['@web', '@flow', '@payment'] }, async ({ browser, request }, testInfo) => { test.setTimeout(60_000); const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage( browser, request, testInfo, ); try { await page.goto('/commerce', { waitUntil: 'load' }); const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true }); await expect(productLink).toBeVisible(); await productLink.click(); await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible(); await expect(page.getByText('COF-SET-01')).toBeVisible(); const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true }); await expect(addToCartButton).toBeEnabled(); const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items'); await addToCartButton.click(); expect((await addResponse).ok()).toBeTruthy(); await page.getByRole('link', { name: /^Cart/ }).click(); await page.getByRole('link', { name: 'Checkout', exact: true }).click(); const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) }); await expect(addressChoice).toBeVisible(); await addressChoice.check(); await page.getByRole('radio', { name: 'No coupon', exact: true }).check(); const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout'); await page.getByRole('button', { name: 'Create order', exact: true }).click(); const checkoutResponseValue = await checkoutResponse; expect(checkoutResponseValue.ok()).toBeTruthy(); const checkout = (await checkoutResponseValue.json()).data; expectTableIdentity(checkout.orderId); expectTableIdentity(checkout.paymentSessionId); const startResponse = waitForApiResponse( page, 'POST', new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}/start$`), ); await page.getByRole('button', { name: 'Start payment', exact: true }).click(); expect((await startResponse).ok()).toBeTruthy(); await expect(page.getByRole('heading', { name: 'Mock payment simulator' })).toBeVisible(); const completeResponse = waitForApiResponse( page, 'POST', new RegExp(`/api/pay/mock/payment-session/${checkout.paymentSessionId}/complete$`), ); await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click(); expect((await completeResponse).ok()).toBeTruthy(); await expect(page).toHaveURL( new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`), { timeout: 40_000, }, ); const callbackPath = `/commerce/commerce/trade/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=return`; await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); }); const callbackResponse = await context.request.get(callbackPath); expect(callbackResponse.ok()).toBeTruthy(); const callbackHtml = (await callbackResponse.text()).toLowerCase(); expect(callbackHtml).not.toContain('data-zova-hydrated'); expect(callbackHtml).not.toContain('mock payment simulator'); const reconcilePath = new RegExp( `/api/pay/payment-session/${checkout.paymentSessionId}/reconcile$`, ); const paymentViewPath = new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}$`); const orderViewPath = new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`); let reconcileCount = 0; let paymentViewCount = 0; let orderViewCount = 0; page.on('request', request => { const url = new URL(request.url()); if (request.method() !== 'GET') { if (request.method() === 'POST' && reconcilePath.test(url.pathname)) reconcileCount++; return; } if (paymentViewPath.test(url.pathname)) paymentViewCount++; if (orderViewPath.test(url.pathname)) orderViewCount++; }); const reconcileResponse = waitForApiResponse(page, 'POST', reconcilePath); const documentResponse = await page.goto(callbackPath, { waitUntil: 'load' }); expect(documentResponse?.ok()).toBeTruthy(); expect((await reconcileResponse).ok()).toBeTruthy(); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page).toHaveURL( new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`), ); expect(reconcileCount).toBe(1); expect(paymentViewCount).toBeLessThanOrEqual(10); expect(orderViewCount).toBeLessThanOrEqual(5); await expect(page.getByText('paid · $45.99')).toBeVisible(); await expect(page.getByRole('alert')).toHaveCount(0); expect(pageErrors).toEqual([]); } finally { await context.close().catch(() => {}); } }, ); test( 'PayPal browser cancel: customer can continue payment or open an awaiting order', { tag: ['@web', '@flow', '@payment'] }, async ({ browser, request }, testInfo) => { test.setTimeout(60_000); const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage( browser, request, testInfo, ); try { await page.goto('/commerce', { waitUntil: 'load' }); const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true }); await expect(productLink).toBeVisible(); await productLink.click(); await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible(); await expect(page.getByText('COF-SET-01')).toBeVisible(); const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true }); await expect(addToCartButton).toBeEnabled(); const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items'); await addToCartButton.click(); expect((await addResponse).ok()).toBeTruthy(); await page.getByRole('link', { name: /^Cart/ }).click(); await page.getByRole('link', { name: 'Checkout', exact: true }).click(); const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) }); await expect(addressChoice).toBeVisible(); await addressChoice.check(); await page.getByRole('radio', { name: 'No coupon', exact: true }).check(); const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout'); await page.getByRole('button', { name: 'Create order', exact: true }).click(); const checkoutResponseValue = await checkoutResponse; expect(checkoutResponseValue.ok()).toBeTruthy(); const checkout = (await checkoutResponseValue.json()).data; expectTableIdentity(checkout.orderId); expectTableIdentity(checkout.paymentSessionId); expect(checkout.state).toBe('awaiting_payment'); const callbackPath = `/commerce/commerce/trade/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=cancel`; const paymentSessionPath = new RegExp( `/api/pay/payment-session/${checkout.paymentSessionId}$`, ); const reconcilePath = new RegExp( `/api/pay/payment-session/${checkout.paymentSessionId}/reconcile$`, ); const requiresActionSession = { id: checkout.paymentSessionId, state: 'requires_action', providerName: 'pay-paypal:paypal', nextAction: { kind: 'redirect', url: 'https://example.test/paypal-approval' }, amountMinor: 4599, currency: 'USD', }; const callbackResponse = await context.request.get(callbackPath); expect(callbackResponse.ok()).toBeTruthy(); const callbackHtml = await callbackResponse.text(); expect(callbackHtml).toContain('Start payment'); expect(callbackHtml).not.toContain( 'You returned from the payment provider without completing payment.', ); let reconcileCount = 0; let reconcileSeen = false; let resolvePaymentSessionRefresh: () => void; const paymentSessionRefresh = new Promise(resolve => { resolvePaymentSessionRefresh = resolve; }); await page.route( url => paymentSessionPath.test(url.pathname) || reconcilePath.test(url.pathname), async route => { const request = route.request(); const pathname = new URL(request.url()).pathname; if (request.method() === 'POST' && reconcilePath.test(pathname)) { reconcileCount++; reconcileSeen = true; } else if ( reconcileSeen && request.method() === 'GET' && paymentSessionPath.test(pathname) ) { resolvePaymentSessionRefresh(); } await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ code: 0, message: '', data: requiresActionSession, }), }); }, ); const reconcileResponse = waitForApiResponse(page, 'POST', reconcilePath); await page.goto(callbackPath, { waitUntil: 'load' }); expect((await reconcileResponse).ok()).toBeTruthy(); await paymentSessionRefresh; await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect( page.getByText( 'You returned from the payment provider without completing payment. Your payment status has been refreshed. You can continue to payment or open your order.', ), ).toBeVisible(); await expect(page.getByRole('button', { name: 'Start payment', exact: true })).toHaveCount(0); await expect( page.getByRole('button', { name: 'Continue to payment', exact: true }), ).toBeVisible(); await expect(page.getByRole('button', { name: 'Open order', exact: true })).toBeVisible(); await expect( page.getByText('Waiting for verified provider confirmation and order settlement…'), ).toHaveCount(0); await expect(page.getByRole('alert')).toHaveCount(0); expect(reconcileCount).toBe(1); await page.getByRole('button', { name: 'Open order', exact: true }).click(); await expect(page).toHaveURL( new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`), ); expect(pageErrors).toEqual([]); } finally { await context.close().catch(() => {}); } }, ); test( 'Payment cancellation: customer observes the cancelled order after verified provider confirmation', { tag: ['@web', '@flow', '@payment'] }, async ({ browser, request }, testInfo) => { test.setTimeout(60_000); const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage( browser, request, testInfo, ); try { await page.goto('/commerce', { waitUntil: 'load' }); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true }); await expect(productLink).toBeVisible(); await productLink.click(); await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/); await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible(); const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items'); await page.getByRole('button', { name: 'Add to cart', exact: true }).click(); expect((await addResponse).ok()).toBeTruthy(); await page.getByRole('link', { name: /^Cart/ }).click(); await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/); await page.getByRole('link', { name: 'Checkout', exact: true }).click(); await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/checkout(?:\/|$)/); const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) }); await expect(addressChoice).toBeVisible(); await addressChoice.check(); await page.getByRole('radio', { name: 'No coupon', exact: true }).check(); const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout'); await page.getByRole('button', { name: 'Create order', exact: true }).click(); const checkoutResponseValue = await checkoutResponse; expect(checkoutResponseValue.ok()).toBeTruthy(); const checkout = (await checkoutResponseValue.json()).data; expectTableIdentity(checkout.orderId); expectTableIdentity(checkout.paymentSessionId); const startResponse = waitForApiResponse( page, 'POST', new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}/start$`), ); await page.getByRole('button', { name: 'Start payment', exact: true }).click(); expect((await startResponse).ok()).toBeTruthy(); await expect(page.getByText('Payment is being prepared.')).toBeVisible(); await expect(page.getByRole('heading', { name: 'Mock payment simulator' })).toBeVisible(); const cancelResponse = waitForApiResponse( page, 'POST', new RegExp(`/api/pay/mock/payment-session/${checkout.paymentSessionId}/complete$`), ); await page.getByRole('button', { name: 'Cancel payment', exact: true }).click(); expect((await cancelResponse).ok()).toBeTruthy(); await expect(page).toHaveURL( new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`), { timeout: 40_000, }, ); await expect(page.getByText('cancelled · $45.99')).toBeVisible(); await expect(page.getByRole('alert')).toHaveCount(0); expect(pageErrors).toEqual([]); } finally { await context.close().catch(() => {}); } }, ); test( 'Phase 60: customer requests and operator executes a whole-order refund', { tag: ['@web', '@admin', '@flow', '@payment', '@refund'] }, async ({ browser, request }, testInfo) => { test.setTimeout(120_000); const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage( browser, request, testInfo, ); try { await page.goto('/commerce', { waitUntil: 'load' }); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce'); await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible(); const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true }); await expect(productLink).toBeVisible(); await productLink.click(); await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/); await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible(); await expect(page.getByText('COF-SET-01')).toBeVisible(); await expect(page.getByText('$45.99')).toBeVisible(); const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items'); await page.getByRole('button', { name: 'Add to cart', exact: true }).click(); await addResponse; await page.getByRole('link', { name: /^Cart/ }).click(); await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/); await page.getByRole('link', { name: 'Checkout', exact: true }).click(); await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/checkout(?:\/|$)/); const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) }); await addressChoice.check(); await page.getByRole('radio', { name: 'No coupon', exact: true }).check(); const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout'); await page.getByRole('button', { name: 'Create order', exact: true }).click(); const checkoutResponseValue = await checkoutResponse; expect(checkoutResponseValue.ok()).toBeTruthy(); const checkout = (await checkoutResponseValue.json()).data; expectTableIdentity(checkout.orderId); expectTableIdentity(checkout.paymentAttemptId); expectTableIdentity(checkout.paymentSessionId); await expect(page.getByRole('heading', { name: 'Payment', exact: true })).toBeVisible(); const startResponse = waitForApiResponse( page, 'POST', new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}/start$`), ); await page.getByRole('button', { name: 'Start payment', exact: true }).click(); expect((await startResponse).ok()).toBeTruthy(); await expect(page.getByText('Payment is being prepared.')).toBeVisible(); await expect(page.getByRole('heading', { name: 'Mock payment simulator' })).toBeVisible(); const completeResponse = waitForApiResponse( page, 'POST', new RegExp(`/api/pay/mock/payment-session/${checkout.paymentSessionId}/complete$`), ); await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click(); expect((await completeResponse).ok()).toBeTruthy(); await expect(page.getByText('paid · $45.99')).toBeVisible({ timeout: 40_000 }); const requestResponse = waitForApiResponse( page, 'POST', `/api/commerce/trade/order/${checkout.orderId}/requestRefund`, ); await page.getByPlaceholder('Reason for refund').fill('E2E refund request'); await page.getByRole('button', { name: 'Request refund', exact: true }).click(); expect((await requestResponse).ok()).toBeTruthy(); await expect(page.getByText('Refund requested · $45.99')).toBeVisible(); const adminContext = await browser.newContext(); const adminPage = await adminContext.newPage(); const adminPageErrors = collectPageErrors(adminPage); const adminConsoleErrors = collectConsoleErrors(adminPage); try { await adminPage.setViewportSize({ width: 1440, height: 900 }); await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin'); await adminPage.goto('/commerce-admin/rest/resource/commerce-trade%3Aorder', { waitUntil: 'load', }); await expect(adminPage.locator('html')).toHaveAttribute( 'data-zova-hydrated', 'commerceAdmin', ); const orderRow = await getAdminOrderRow(adminPage, checkout.orderId); await expect(orderRow).toBeVisible(); await expect( orderRow.getByRole('button', { name: 'Review refund', exact: true }), ).toBeVisible(); await expect( orderRow.getByRole('button', { name: 'Approve refund', exact: true }), ).toHaveCount(0); await expect( orderRow.getByRole('button', { name: 'Reject refund', exact: true }), ).toHaveCount(0); await orderRow.getByRole('button', { name: 'Review refund', exact: true }).click(); const refundDecisionTitle = adminPage.getByRole('heading', { name: 'Refund decision' }); await expect(refundDecisionTitle).toBeVisible(); await adminPage.getByPlaceholder('Decision reason').fill('E2E approval'); await adminPage.locator('input[name="decisionConfirmed"]').check(); const approveResponse = waitForApiResponse( adminPage, 'POST', `/api/commerce/trade/order/${checkout.orderId}/approveRefund`, ); await adminPage.getByRole('button', { name: 'Approve refund', exact: true }).click(); expect((await approveResponse).ok()).toBeTruthy(); await expect(refundDecisionTitle).toHaveCount(0); await expect(orderRow).toContainText('Refund approved'); await expect( orderRow.getByRole('button', { name: 'Execute refund', exact: true }), ).toHaveCount(1); await expect( orderRow.getByRole('button', { name: 'Inspect refund recovery', exact: true }), ).toHaveCount(0); await expect(orderRow.getByRole('checkbox')).toHaveCount(0); await orderRow.getByRole('button', { name: 'Execute refund', exact: true }).click(); const refundExecutionTitle = adminPage.getByRole('heading', { name: 'Refund execution' }); await expect(refundExecutionTitle).toBeVisible(); const refundExecutionDialog = adminPage .locator('div.card') .filter({ has: refundExecutionTitle }); const executionButton = refundExecutionDialog.getByRole('button', { name: 'Execute refund', exact: true, }); await expect(executionButton).toBeDisabled(); const unavailableRecoveryResponse = waitForApiResponse( adminPage, 'GET', `/api/commerce/trade/order/${checkout.orderId}/refundRecovery`, ); await refundExecutionDialog .getByRole('button', { name: 'Inspect refund recovery', exact: true }) .click(); expect((await unavailableRecoveryResponse).status()).toBe(404); await expect( refundExecutionDialog.getByText( 'Refund recovery is not available yet. Execute the refund first to create a provider refund submission.', ), ).toBeVisible(); await expect(refundExecutionDialog.getByText(/Provider operation:/)).toHaveCount(0); await expect(refundExecutionDialog.getByPlaceholder('Recovery reason')).toHaveCount(0); await expect( refundExecutionDialog.getByRole('button', { name: 'Reconcile provider refund', exact: true, }), ).toHaveCount(0); await expect( refundExecutionDialog.getByRole('button', { name: 'Retry refund', exact: true }), ).toHaveCount(0); expect( adminConsoleErrors.filter(error => /refund recovery is not available|AxiosError/i.test(error), ), ).toEqual([]); await refundExecutionDialog.locator('input[name="executionConfirmed"]').check(); const executeResponse = waitForApiResponse( adminPage, 'POST', `/api/commerce/trade/order/${checkout.orderId}/executeRefund`, ); await executionButton.click(); const executeResponseValue = await executeResponse; expect(executeResponseValue.ok()).toBeTruthy(); const executeResult = (await executeResponseValue.json()).data; expectTableIdentity(executeResult.refundAttemptId); expectTableIdentity(executeResult.refundOperationId); expect([ executeResult.orderState, executeResult.refundState, executeResult.refundAttemptState, ]).toEqual(['refund_approved', 'approved', 'created']); await expect(refundExecutionTitle).toHaveCount(0); await orderRow.getByRole('button', { name: 'Execute refund', exact: true }).click(); await expect(refundExecutionTitle).toBeVisible(); const recoveryResponse = waitForApiResponse( adminPage, 'GET', `/api/commerce/trade/order/${checkout.orderId}/refundRecovery`, ); await adminPage .getByRole('button', { name: 'Inspect refund recovery', exact: true }) .click(); expect((await recoveryResponse).ok()).toBeTruthy(); await expect(adminPage.getByText(/Provider operation:/)).toBeVisible(); await expect( adminPage.getByText( 'A provider refund identifier is available. Wait for the verified provider webhook; do not resubmit or reconcile by querying.', ), ).toBeVisible(); await expect( refundExecutionDialog.getByRole('button', { name: 'Reconcile provider refund', exact: true, }), ).toHaveCount(0); const completeRefundResponse = waitForApiResponse( adminPage, 'POST', `/api/pay/mock/payment-session/refund-operation/${executeResult.refundOperationId}/complete`, ); await refundExecutionDialog .getByRole('button', { name: 'Complete mock refund', exact: true }) .click(); expect((await completeRefundResponse).ok()).toBeTruthy(); await expect .poll( async () => { await adminPage.reload({ waitUntil: 'load' }); const currentOrderRow = await getAdminOrderRow(adminPage, checkout.orderId); return (await currentOrderRow.textContent()) ?? ''; }, { timeout: 40_000 }, ) .toContain('Refunded'); await expect(await getAdminOrderRow(adminPage, checkout.orderId)).toContainText('Refunded'); expect(adminPageErrors).toEqual([]); } finally { await adminContext.close().catch(() => {}); } // The session SSR reload serializes the refreshed order detail, so no browser API request is // required before the page renders the refunded state. await page.reload({ waitUntil: 'load' }); await expect(page.getByText('refunded · $45.99')).toBeVisible(); await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible(); await expect(page.getByRole('heading', { name: 'Shipment' })).toHaveCount(0); await expect(page.getByRole('alert')).toHaveCount(0); expect(pageErrors).toEqual([]); } finally { await context.close().catch(() => {}); } }, ); test( 'ATP-SPC-05: systemAdmin inspects readonly Address Resource without mutation controls', { tag: ['@admin', '@flow', '@address'] }, async ({ browser, request }, testInfo) => { test.setTimeout(60_000); const customer = await createAddressThroughCustomerPage(browser, request, testInfo); const adminContext = await browser.newContext(); const adminPage = await adminContext.newPage(); const adminPageErrors = collectPageErrors(adminPage); const addressMutationMethods: string[] = []; adminPage.on('request', request => { const url = new URL(request.url()); if ( url.pathname.startsWith(addressActionPath) && ['POST', 'PATCH', 'PUT', 'DELETE'].includes(request.method()) ) { addressMutationMethods.push(request.method()); } }); try { await adminPage.setViewportSize({ width: 1440, height: 900 }); await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin'); await adminPage.goto('/commerce-admin/rest/resource/commerce-member%3Aaddress', { waitUntil: 'load', }); await expect(adminPage).toHaveURL(addressResourceUrl); const fixtureRow = adminPage.getByRole('row', { name: customer.fixture.recipientName }); await expect(fixtureRow).toBeVisible(); await expect(fixtureRow).toContainText(customer.fixture.recipientName); await expect(fixtureRow).toContainText(customer.fixture.phone); await expect(fixtureRow).toContainText(customer.fixture.countryCode); await expect(fixtureRow).toContainText(customer.fixture.city); await expect(fixtureRow).not.toContainText(customer.fixture.region); await expect(fixtureRow).not.toContainText(customer.fixture.postalCode); await expect(fixtureRow).not.toContainText(customer.fixture.addressLine1); await expect(fixtureRow).not.toContainText(customer.fixture.addressLine2); await expect(adminPage.getByRole('button', { name: 'Create', exact: true })).toHaveCount(0); await expect( adminPage.locator('a[href*="/rest/resource/"]').filter({ hasText: 'Create' }), ).toHaveCount(0); const addressId = await fixtureRow.getByRole('cell').first().textContent(); expect(addressId).toMatch(/^\d+$/); await expect(fixtureRow.getByRole('link', { name: addressId! })).toHaveCount(1); await adminPage.goto(`/commerce-admin/rest/resource/commerce-member%3Aaddress/${addressId}`, { waitUntil: 'load', }); await expect(adminPage).toHaveURL( /\/commerce-admin\/rest\/resource\/commerce-member(?:%3A|:|%253A)address\/\d+(?:[/?#]|$)/, ); await expect( adminPage.getByRole('group', { name: 'Address Line 1 *' }).getByRole('textbox'), ).toHaveValue(customer.fixture.addressLine1); await expect( adminPage.getByRole('group', { name: 'City *' }).getByRole('textbox'), ).toHaveValue(customer.fixture.city); await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible(); await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0); expect(addressMutationMethods).toEqual([]); expect(adminPageErrors).toEqual([]); await deleteAddressThroughCustomerPage(customer.page, customer.fixture); await expect(customer.page.getByRole('alert')).toHaveCount(0); expect(customer.pageErrors).toEqual([]); } finally { await adminContext.close().catch(() => {}); await customer.context.close().catch(() => {}); } }, ); test( 'ATP-SPC-01: Coupon Template renders semantic Admin field controls', { tag: ['@admin', '@flow', '@fia'] }, async ({ browser }) => { test.setTimeout(60_000); const adminContext = await browser.newContext(); const adminPage = await adminContext.newPage(); const adminPageErrors = collectPageErrors(adminPage); try { await adminPage.setViewportSize({ width: 1440, height: 900 }); await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin'); await adminPage.goto( '/commerce-admin/rest/resource/commerce-promotion%3AcouponTemplate/create', { waitUntil: 'load', }, ); await expect(adminPage).toHaveURL( /\/commerce-admin\/rest\/resource\/commerce-promotion(?:%3A|:|%253A)couponTemplate\/create(?:[/?#]|$)/, ); for (const groupName of [ 'Basic Information', 'Discount Policy', 'Validity Window', 'Usage Limits', ]) { await expect(adminPage.getByRole('group', { name: groupName })).toBeVisible(); } const state = adminPage .getByRole('group', { name: 'Template State *' }) .getByRole('combobox'); await expect(state).toBeVisible(); await state.selectOption({ label: 'Active' }); await expect(state).toHaveValue('active'); const discountInput = adminPage .getByRole('group', { name: 'Fixed Discount *' }) .getByRole('textbox'); const minSpendInput = adminPage .getByRole('group', { name: 'Minimum Spend *' }) .getByRole('textbox'); await discountInput.fill('12.34'); await minSpendInput.fill('45.67'); await expect(discountInput).toHaveValue('12.34'); await expect(minSpendInput).toHaveValue('45.67'); await expect( adminPage.getByRole('group', { name: 'Valid From *' }).locator('input[type="date"]'), ).toBeVisible(); await expect( adminPage.getByRole('group', { name: 'Valid Until *' }).locator('input[type="date"]'), ).toBeVisible(); await adminPage .getByRole('group', { name: 'Name *' }) .getByRole('textbox') .fill('E2E Coupon Template'); await adminPage.getByRole('group', { name: 'Currency *' }).getByRole('textbox').fill('USD'); await adminPage .getByRole('group', { name: 'Valid From *' }) .getByRole('textbox') .fill('2026-08-04'); await adminPage .getByRole('group', { name: 'Valid Until *' }) .getByRole('textbox') .fill('2026-12-31'); await adminPage .getByRole('group', { name: 'Total Issue Limit' }) .getByRole('textbox') .fill('10'); await adminPage .getByRole('group', { name: 'Total Usage Limit' }) .getByRole('textbox') .fill('10'); await adminPage .getByRole('group', { name: 'Per-customer Issue Limit' }) .getByRole('textbox') .fill('1'); await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toBeVisible(); await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible(); expect(adminPageErrors).toEqual([]); } finally { await adminContext.close().catch(() => {}); } }, ); test( 'ATP-SPC-02: Category renders semantic Admin relation and publication controls', { tag: ['@admin', '@flow', '@category'] }, async ({ browser }, testInfo) => { test.setTimeout(60_000); const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`; const parentName = `E2E Category Parent ${suffix}`; const childName = `E2E Category Child ${suffix}`; const adminContext = await browser.newContext(); const adminPage = await adminContext.newPage(); const adminPageErrors = collectPageErrors(adminPage); const categoryCreatePath = '/commerce-admin/rest/resource/commerce-catalog%3Acategory/create'; const categoryListPath = '/commerce-admin/rest/resource/commerce-catalog%3Acategory'; const categoryActionPath = '/api/commerce/catalog/category'; let parentId: number | string | undefined; let childId: number | string | undefined; let headers: { Authorization: string } | undefined; try { await adminPage.setViewportSize({ width: 1440, height: 900 }); 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 parentCreateResponse = await adminPage.request.post(categoryActionPath, { data: { name: parentName, published: false }, headers, }); expect(parentCreateResponse.ok()).toBeTruthy(); parentId = (await parentCreateResponse.json()).data; expectTableIdentity(parentId); await adminPage.goto(categoryCreatePath, { waitUntil: 'load' }); const parentPublication = adminPage .getByRole('group', { name: 'Published' }) .getByRole('combobox'); await expect(parentPublication).toBeVisible(); await expect(parentPublication.locator('option')).toHaveText([ '', 'Unpublished', 'Published', ]); await parentPublication.selectOption({ label: 'Unpublished' }); await expect(parentPublication).toHaveValue('false'); const parentPicker = adminPage .getByRole('group', { name: 'Parent category' }) .getByRole('combobox'); await expect(parentPicker).toBeVisible(); await parentPicker.selectOption({ label: parentName }); const parentValue = await parentPicker.inputValue(); expect(parentValue).toBe(String(parentId)); const childPublication = adminPage .getByRole('group', { name: 'Published' }) .getByRole('combobox'); await childPublication.selectOption({ label: 'Published' }); await expect(childPublication).toHaveValue('true'); const childCreateResponse = await adminPage.request.post(categoryActionPath, { data: { name: childName, parentId: parentValue, published: true }, headers, }); expect(childCreateResponse.ok()).toBeTruthy(); childId = (await childCreateResponse.json()).data; expectTableIdentity(childId); await adminPage.goto(categoryListPath, { waitUntil: 'load' }); const childRow = adminPage.getByRole('row', { name: new RegExp(childName) }); await expect(childRow).toBeVisible(); await expect(childRow.getByText(parentName, { exact: true })).toBeVisible(); await expect(childRow.getByText('Published', { exact: true })).toBeVisible(); await adminPage.goto(`${categoryListPath}/${parentId}/edit`, { waitUntil: 'load' }); await expect( adminPage.getByRole('group', { name: 'Published' }).getByRole('combobox'), ).toHaveValue('false'); await adminPage.goto(`${categoryListPath}/${childId}/edit`, { waitUntil: 'load' }); await expect( adminPage.getByRole('group', { name: 'Published' }).getByRole('combobox'), ).toHaveValue('true'); await adminPage.goto(`${categoryListPath}/${childId}`, { waitUntil: 'load' }); await expect( adminPage.getByRole('group', { name: 'Parent category' }).getByRole('textbox'), ).toHaveValue(parentName); await expect( adminPage.getByRole('group', { name: 'Published' }).getByRole('textbox'), ).toHaveValue('Published'); await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0); expect(adminPageErrors).toEqual([]); } finally { if (childId && headers) { const response = await adminPage.request.delete(`${categoryActionPath}/${childId}`, { headers, }); expect(response.ok()).toBeTruthy(); } if (parentId && headers) { const response = await adminPage.request.delete(`${categoryActionPath}/${parentId}`, { headers, }); expect(response.ok()).toBeTruthy(); } await adminContext.close().catch(() => {}); } }, ); test( 'ATP-SPC-02: Product renders semantic Admin relation and publication controls', { tag: ['@admin', '@flow', '@product'] }, async ({ browser }, testInfo) => { test.setTimeout(60_000); const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`; const categoryName = `E2E Product Category ${suffix}`; const productTitle = `E2E Product ${suffix}`; const adminContext = await browser.newContext(); const adminPage = await adminContext.newPage(); const adminPageErrors = collectPageErrors(adminPage); const productCreatePath = '/commerce-admin/rest/resource/commerce-catalog%3Aproduct/create'; const productListPath = '/commerce-admin/rest/resource/commerce-catalog%3Aproduct'; const categoryActionPath = '/api/commerce/catalog/category'; const productActionPath = '/api/commerce/catalog/product'; let categoryId: number | string | undefined; let productId: number | string | undefined; let headers: { Authorization: string } | undefined; try { await adminPage.setViewportSize({ width: 1440, height: 900 }); 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); await adminPage.goto(productCreatePath, { waitUntil: 'load' }); const categoryPicker = adminPage .getByRole('group', { name: 'Category' }) .getByRole('combobox'); await expect(categoryPicker).toBeVisible(); await categoryPicker.selectOption({ label: categoryName }); await expect(categoryPicker).toHaveValue(String(categoryId)); const publication = adminPage.getByRole('group', { name: 'Published' }).getByRole('combobox'); await expect(publication).toBeVisible(); await publication.selectOption({ label: 'Unpublished' }); await expect(publication).toHaveValue('false'); const productResponse = await adminPage.request.post(productActionPath, { data: { categoryId, title: productTitle, published: false }, headers, }); expect(productResponse.ok()).toBeTruthy(); productId = (await productResponse.json()).data; expectTableIdentity(productId); await adminPage.goto(productListPath, { waitUntil: 'load' }); const productRow = adminPage.getByRole('row', { name: new RegExp(productTitle) }); await expect(productRow).toBeVisible(); await expect(productRow.getByText(categoryName, { exact: true })).toBeVisible(); await expect(productRow.getByText('Unpublished', { exact: true })).toBeVisible(); await adminPage.goto(`${productListPath}/${productId}`, { waitUntil: 'load' }); await expect( adminPage.getByRole('group', { name: 'Category' }).getByRole('textbox'), ).toHaveValue(categoryName); await expect( adminPage.getByRole('group', { name: 'Published' }).getByRole('textbox'), ).toHaveValue('Unpublished'); await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0); expect(adminPageErrors).toEqual([]); } finally { if (productId && headers) { const response = await adminPage.request.delete(`${productActionPath}/${productId}`, { headers, }); expect(response.ok()).toBeTruthy(); } if (categoryId && headers) { const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, { headers, }); expect(response.ok()).toBeTruthy(); } await adminContext.close().catch(() => {}); } }, ); test( 'ATP-SPC-02: SKU renders semantic Admin currency and lifecycle controls', { tag: ['@admin', '@flow', '@sku'] }, async ({ browser }, testInfo) => { test.setTimeout(60_000); const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`; const categoryName = `E2E SKU Category ${suffix}`; const productTitle = `E2E SKU Product ${suffix}`; const skuCode = `E2E-SKU-${suffix}`; const stockActionPath = '/api/commerce/trade/stockBalance/adjustStock'; const adminContext = await browser.newContext(); const adminPage = await adminContext.newPage(); const adminPageErrors = collectPageErrors(adminPage); const skuCreatePath = '/commerce-admin/rest/resource/commerce-catalog%3Asku/create'; const skuListPath = '/commerce-admin/rest/resource/commerce-catalog%3Asku'; const categoryActionPath = '/api/commerce/catalog/category'; const productActionPath = '/api/commerce/catalog/product'; const skuActionPath = '/api/commerce/catalog/sku'; let categoryId: number | string | undefined; let productId: number | string | undefined; let skuId: number | string | undefined; let headers: { Authorization: string } | undefined; try { await adminPage.setViewportSize({ width: 1440, height: 900 }); 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 }, headers, }); expect(productResponse.ok()).toBeTruthy(); productId = (await productResponse.json()).data; expectTableIdentity(productId); await adminPage.goto(skuCreatePath, { waitUntil: 'load' }); const productField = adminPage.getByRole('group', { name: 'Product' }); await expect(productField).toBeVisible(); const productPicker = productField.getByRole('combobox'); await expect(productPicker).toBeVisible(); await expect(productPicker).toHaveValue(''); const lifecycle = adminPage .getByRole('group', { name: 'SKU lifecycle' }) .getByRole('combobox'); await expect(lifecycle).toBeVisible(); await expect(lifecycle.locator('option')).toHaveText([ '', 'Draft', 'Active', 'Inactive', 'Archived', ]); await expect(adminPage.getByRole('group', { name: 'Price (cents)' })).toBeVisible(); const skuResponse = await adminPage.request.post(skuActionPath, { data: { code: skuCode, productId, priceCents: 1234, lifecycle: 'draft' }, headers, }); expect(skuResponse.ok()).toBeTruthy(); skuId = (await skuResponse.json()).data; expectTableIdentity(skuId); await adminPage.goto(skuListPath, { waitUntil: 'load' }); const skuRow = adminPage.getByRole('row', { name: new RegExp(skuCode) }); await expect(skuRow).toBeVisible(); await expect(skuRow.getByText('12.34', { exact: true })).toBeVisible(); await expect(skuRow.getByText('0', { exact: true })).toBeVisible(); await expect(skuRow.getByText('Draft', { exact: true })).toBeVisible(); await expect(skuRow.getByRole('link', { name: skuCode, exact: true })).toBeVisible(); const adjustStockButton = skuRow.getByRole('button', { name: 'Adjust stock', exact: true }); await expect(adjustStockButton).toBeVisible(); await expect(adminPage.locator('html')).toHaveAttribute( 'data-zova-hydrated', 'commerceAdmin', ); await adjustStockButton.click(); await expect(adminPage.getByRole('dialog')).toBeVisible(); await expect(adminPage.getByRole('dialog').getByText(skuCode, { exact: true })).toBeVisible(); await expect(adminPage.getByRole('dialog').getByText('0', { exact: true })).toBeVisible(); const deltaInput = adminPage.getByRole('group', { name: 'Delta' }).getByRole('spinbutton'); const reasonInput = adminPage.getByRole('group', { name: 'Reason' }).getByRole('textbox'); await deltaInput.fill('7'); await reasonInput.fill('Initial E2E stock'); const stockRequest = adminPage.waitForRequest(request => { return request.method() === 'POST' && new URL(request.url()).pathname === stockActionPath; }); const stockResponse = adminPage.waitForResponse(response => { return ( response.request().method() === 'POST' && new URL(response.url()).pathname === stockActionPath ); }); await adminPage .getByRole('dialog') .getByRole('button', { name: 'Adjust stock', exact: true }) .click(); const stockRequestValue = await stockRequest; expect(stockRequestValue.postDataJSON()).toMatchObject({ skuId, delta: 7, reason: 'Initial E2E stock', correlationId: expect.any(String), }); expect((await stockResponse).ok()).toBeTruthy(); await expect(adminPage.getByRole('dialog')).toHaveCount(0); await expect(skuRow.getByText('7', { exact: true })).toBeVisible(); await expect( adminPage.locator('section').getByText('SKU code', { exact: true }), ).toBeVisible(); await expect(adminPage.getByText('Created At', { exact: true })).toHaveCount(1); await expect(adminPage.getByRole('group', { name: 'Attributes' })).toHaveCount(0); await adminPage.goto(`${skuListPath}/${skuId}`, { waitUntil: 'load' }); await expect( adminPage.getByRole('group', { name: 'SKU lifecycle' }).getByRole('textbox'), ).toHaveValue('Draft'); await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0); await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible(); expect(adminPageErrors).toEqual([]); } finally { if (skuId && headers) { await adminPage.request.delete(`${stockActionPath}/${skuId}`, { headers }).catch(() => {}); const response = await adminPage.request.delete(`${skuActionPath}/${skuId}`, { headers }); expect(response.ok()).toBeTruthy(); } if (productId && headers) { const response = await adminPage.request.delete(`${productActionPath}/${productId}`, { headers, }); expect(response.ok()).toBeTruthy(); } if (categoryId && headers) { const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, { headers, }); expect(response.ok()).toBeTruthy(); } await adminContext.close().catch(() => {}); } }, ); test( 'ATP-SPC-04: Stock Admin resources remain readonly and mutation-free', { tag: ['@admin', '@flow', '@stock'] }, async ({ browser }) => { test.setTimeout(60_000); const adminContext = await browser.newContext(); const adminPage = await adminContext.newPage(); const adminPageErrors = collectPageErrors(adminPage); const mutationMethods: string[] = []; adminPage.on('request', request => { const url = new URL(request.url()); if ( url.pathname.includes('/api/commerce/trade/stock') && ['POST', 'PATCH', 'PUT', 'DELETE'].includes(request.method()) ) { mutationMethods.push(request.method()); } }); try { await adminPage.setViewportSize({ width: 1440, height: 900 }); await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin'); for (const [resourcePath, title] of [ ['commerce-trade%3AstockBalance', 'Stock Balance'], ['commerce-trade%3AstockAudit', 'Stock Audit'], ] as const) { await adminPage.goto(`/commerce-admin/rest/resource/${resourcePath}`, { waitUntil: 'load', }); await expect(adminPage).toHaveURL( new RegExp( `/commerce-admin/rest/resource/commerce-trade(?:%3A|:|%253A)${title === 'Stock Balance' ? 'stockBalance' : 'stockAudit'}(?:[/?#]|$)`, ), ); await expect(adminPage.getByText(title, { exact: true }).first()).toBeVisible(); await expect(adminPage.getByRole('button', { name: 'Create', exact: true })).toHaveCount(0); await expect( adminPage.locator('a[href*="/rest/resource/"]').filter({ hasText: 'Create' }), ).toHaveCount(0); await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0); } expect(mutationMethods).toEqual([]); expect(adminPageErrors).toEqual([]); } finally { await adminContext.close().catch(() => {}); } }, ); test( 'Commerce Cart: anonymous browser is redirected to login', { tag: ['@web', '@cart'] }, async ({ page, request }) => { const path = '/commerce/commerce/trade/cart'; const response = await request.get(path); expect(response.ok()).toBeTruthy(); expect((await response.text()).toLowerCase()).not.toContain('data-zova-hydrated'); const pageErrors = collectPageErrors(page); const documentResponse = await page.goto(path, { waitUntil: 'load' }); expect(documentResponse?.ok()).toBeTruthy(); await expect(page).toHaveURL(/\/commerce\/login\?(?:.*&)?returnTo=/); expect(pageErrors).toEqual([]); }, ); test( 'Phase 50: anonymous payment and order routes remain SSR-neutral and protected', { tag: ['@web', '@flow'] }, async ({ page, request }) => { const routes = [ ['/commerce/commerce/trade/checkout', '/commerce/trade/checkout'], ['/commerce/commerce/trade/payment/1/1', '/commerce/trade/payment/1/1'], ['/commerce/commerce/trade/orders', '/commerce/trade/orders'], ['/commerce/commerce/trade/order/1', '/commerce/trade/order/1'], ] as const; for (const [path, routePath] of routes) { const response = await request.get(path, { maxRedirects: 0 }); expect(response.status(), path).toBe(302); expect(response.headers()['cache-control'], path).toBe('private, no-store'); expect(response.headers().location, path).toMatch(/^\/commerce\/login\?(?:.*&)?returnTo=/); const html = await response.text(); expect(html.toLowerCase(), path).not.toContain('data-zova-hydrated'); expect(html, path).not.toContain('Payment Customer'); expect(html, path).not.toContain('Payment succeeded'); expect(html, path).not.toContain('Order #'); expect(html, path).not.toContain('Checkout'); expect(html, path).not.toContain('Mock payment simulator'); expect(html, path).not.toContain('My orders'); const pageErrors = collectPageErrors(page); const documentResponse = await page.goto(path, { waitUntil: 'load' }); expect(documentResponse?.ok(), path).toBeTruthy(); await expect(page, path).toHaveURL(/\/commerce\/login\?(?:.*&)?returnTo=/); const loginUrl = new URL(page.url()); expect(loginUrl.searchParams.getAll('returnTo'), path).toEqual([routePath]); expect(pageErrors, path).toEqual([]); } }, ); test( 'ATP-SSR-02: Commerce Admin is an independent SSR site', { tag: ['@admin', '@smoke'] }, async ({ page, request }) => { const response = await request.get('/commerce-admin/', { maxRedirects: 0 }); expect(response.status()).toBe(302); const loginPath = response.headers().location; expect(loginPath).toMatch(/^\/commerce-admin\/login(?:\?|$)/); const loginResponse = await request.get(loginPath!); expect(loginResponse.ok()).toBeTruthy(); expect((await loginResponse.text()).toLowerCase()).not.toContain('data-zova-hydrated'); const pageErrors = collectPageErrors(page); const documentResponse = await page.goto('/commerce-admin/', { waitUntil: 'load' }); expect(documentResponse?.ok()).toBeTruthy(); await expect(page).toHaveURL(/\/commerce-admin\/login(?:\?|$)/); await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerceAdmin'); await expect(page.locator('body')).toBeVisible(); expect(pageErrors).toEqual([]); }, );