import { test, expect } from '@playwright/test'; test.beforeEach(async ({ request }) => { await expect .poll( async () => { try { return (await request.get('http://localhost:5174')).status(); } catch { return 0; } }, { timeout: 20_000 }, ) .toBe(200); }); test('draft, immutable publish and mobile update', async ({ page, context, request }) => { const api = 'http://localhost:3000/api/pages/demo-store'; const draft = await request.get(api + '/draft').then((r) => r.json()); await request.put(api + '/draft', { data: { ...draft, blocks: [] } }); await page.goto('/'); await page.getByText('标题', { exact: true }).first().dragTo(page.locator('.phone')); await expect(page.locator('.materials li')).toHaveCount(1); await page.getByRole('button', { name: '发布' }).click(); await expect(page.locator('.saved')).toContainText('已发布版本'); await expect(page.getByRole('status')).toContainText(/发布成功 · 当前版本 v\d+/); const first = await request.get(api + '/published').then((r) => r.json()); const mobile = await context.newPage(); await mobile.goto('http://localhost:5174/?id=demo-store'); await expect(mobile.locator('h2')).toHaveCount(1); await page.locator('.materials li').first().click(); await page.getByRole('button', { name: '复制' }).click(); await page.getByRole('button', { name: '保存草稿' }).click(); await mobile.reload(); await expect(mobile.locator('h2')).toHaveCount(1); expect((await request.get(api + '/published').then((r) => r.json())).version).toBe(first.version); await page.getByRole('button', { name: '发布' }).click(); await expect .poll(async () => (await request.get(api + '/published').then((r) => r.json())).version) .toBe(first.version + 1); await mobile.reload(); await expect(mobile.locator('h2')).toHaveCount(2); expect((await request.get(api + '/published').then((r) => r.json())).version).toBe( first.version + 1, ); }); test('two-column regions can be edited independently', async ({ page, request }) => { const api = 'http://localhost:3000/api/pages/demo-store'; const draft = await request.get(api + '/draft').then((r) => r.json()); await request.put(api + '/draft', { data: { ...draft, blocks: [] } }); await page.goto('/'); await page.getByText('双列容器', { exact: true }).first().click(); await page.getByLabel('左栏内容').fill('左侧活动专区'); await page.getByLabel('右栏内容').fill('右侧新品推荐'); const preview = page.frameLocator('iframe[title="H5 实时预览"]'); await expect(preview.locator('.cols div').first()).toHaveText('左侧活动专区'); await expect(preview.locator('.cols div').nth(1)).toHaveText('右侧新品推荐'); }); test('all material-specific editors update the shared preview', async ({ page, request }) => { const api = 'http://localhost:3000/api/pages/demo-store'; const draft = await request.get(api + '/draft').then((r) => r.json()); await request.put(api + '/draft', { data: { ...draft, blocks: [] } }); await page.goto('/'); const material = (name: string) => page.locator('.material-grid').getByRole('button', { name, exact: true }); const preview = page.frameLocator('iframe[title="H5 实时预览"]'); await material('横幅 Banner').click(); await page.getByLabel('Banner 主标题').fill('可编辑横幅'); await page.getByLabel('Banner 高度').fill('220'); await expect(preview.locator('.r-banner h1')).toHaveText('可编辑横幅'); await expect(preview.locator('.r-banner')).toHaveCSS('height', '220px'); await material('标题').click(); await page.getByLabel('标题文字').fill('可编辑标题'); await page.getByLabel('标题字号').fill('32'); await expect(preview.locator('h2')).toHaveText('可编辑标题'); await expect(preview.locator('h2')).toHaveCSS('font-size', '32px'); await material('文本').click(); await page.getByLabel('正文内容').fill('正文内容与排版均可调整'); await page.getByLabel('正文行高').fill('2'); await expect(preview.locator('.r-text')).toHaveText('正文内容与排版均可调整'); await expect(preview.locator('.r-text')).toHaveCSS('line-height', '28px'); await material('图片').click(); await page.getByLabel('图片说明').fill('编辑后的图片说明'); await page.getByLabel('图片高度').fill('180'); await page.getByLabel('图片圆角').fill('16'); await expect(preview.locator('.r-image')).toHaveAttribute('alt', '编辑后的图片说明'); await expect(preview.locator('.r-image')).toHaveCSS('height', '180px'); await material('按钮').click(); await page.getByLabel('按钮文字', { exact: true }).fill('查看详情'); await page.getByLabel('按钮背景颜色').fill('#245f57'); await page.getByLabel('点击行为').selectOption('link'); await page.getByLabel('目标链接').fill('/detail'); await expect(preview.locator('.r-button')).toHaveText('查看详情'); await expect(preview.locator('.r-button')).toHaveCSS('background-color', 'rgb(36, 95, 87)'); await material('商品列表').click(); await page.getByLabel('商品 1 名称').fill('手工咖啡杯'); await page.getByRole('button', { name: '添加商品' }).click(); await page.getByLabel('商品 3 名称').fill('夏日随行杯'); await page.getByLabel('商品列数').selectOption('1'); await expect(preview.locator('.products article')).toHaveCount(3); await expect(preview.locator('.products article').first().locator('b')).toHaveText('手工咖啡杯'); await expect(preview.locator('.products')).toHaveCSS('grid-template-columns', '350px'); await material('分割线').click(); await page.getByLabel('分割线粗细').fill('4'); await page.getByLabel('分割线线型').selectOption('dashed'); await expect(preview.locator('hr')).toHaveCSS('border-top-width', '4px'); await expect(preview.locator('hr')).toHaveCSS('border-top-style', 'dashed'); await material('间距').click(); await page.getByLabel('间距高度').evaluate((el) => { const input = el as HTMLInputElement; input.value = '96'; input.dispatchEvent(new Event('input', { bubbles: true })); }); await expect(preview.locator('.r-spacer')).toHaveCSS('height', '96px'); await material('双列容器').click(); await page.getByLabel('左栏内容').fill('品牌故事'); await page.getByLabel('右栏内容').fill('新品指南'); await page.getByLabel('双列间距').fill('20'); await expect(preview.locator('.cols div').first()).toHaveText('品牌故事'); await expect(preview.locator('.cols')).toHaveCSS('gap', '20px'); }); test('banner supports multiple images, looping and autoplay interval', async ({ page, request, }) => { const api = 'http://localhost:3000/api/pages/demo-store'; const draft = await request.get(api + '/draft').then((r) => r.json()); await request.put(api + '/draft', { data: { ...draft, blocks: [] } }); await page.goto('/'); await page .locator('.material-grid') .getByRole('button', { name: '横幅 Banner', exact: true }) .click(); await page.getByRole('button', { name: '添加轮播图片' }).click(); await page .getByLabel('Banner 图片 2', { exact: true }) .fill('https://example.com/banner-two.webp'); await page.getByLabel('Banner 图片 2 说明').fill('第二张轮播图'); await page.getByLabel('开启轮播').check(); const preview = page.frameLocator('iframe[title="H5 实时预览"]'); const image = preview.locator('.carousel-slide'); await expect(preview.locator('.carousel-dots button')).toHaveCount(2); await preview.getByRole('button', { name: '下一张' }).click(); await expect(image).toHaveAttribute('src', 'https://example.com/banner-two.webp'); await expect(image).toHaveAttribute('alt', '第二张轮播图'); await preview.getByRole('button', { name: '上一张' }).click(); await page.getByLabel('循环播放').uncheck(); await page.getByLabel('自动轮播').check(); await page.getByLabel('自动轮播时间').fill('1'); await expect .poll(() => image.getAttribute('src'), { timeout: 2500 }) .toBe('https://example.com/banner-two.webp'); await page.waitForTimeout(1200); await expect(image).toHaveAttribute('src', 'https://example.com/banner-two.webp'); await page.getByLabel('循环播放').check(); await expect .poll(() => image.getAttribute('src'), { timeout: 2500 }) .not.toBe('https://example.com/banner-two.webp'); }); test('editor and H5 service URLs follow the current LAN host', async ({ page }) => { await page.goto('http://127.0.0.1:5173'); await expect(page.locator('iframe[title="H5 实时预览"]')).toHaveAttribute( 'src', 'http://127.0.0.1:5174/?preview=1', ); await expect(page.getByRole('link', { name: '打开移动端' })).toHaveAttribute( 'href', 'http://127.0.0.1:5174/?id=demo-store', ); await page.goto('http://127.0.0.1:5174/?id=demo-store'); await expect(page.locator('.page-renderer')).toBeVisible(); }); test('benefit library keeps marketing materials and renders direct recharge components', async ({ page, request, }) => { const api = 'http://localhost:3000/api/pages/demo-store'; const draft = await request.get(api + '/draft').then((r) => r.json()); await request.put(api + '/draft', { data: { ...draft, blocks: [] } }); await page.goto('/'); await expect(page.getByRole('button', { name: '营销组件', exact: true })).toHaveClass(/active/); await expect(page.getByRole('button', { name: '横幅 Banner', exact: true })).toBeVisible(); await page.getByRole('button', { name: /直充 \/ 卡密/ }).click(); await expect(page.getByRole('button', { name: /直充 \/ 卡密/ })).toHaveClass(/active/); await expect(page.getByRole('button', { name: '权益商品卡片', exact: true })).toBeVisible(); await page.getByRole('button', { name: '添加业务示例组合' }).click(); const preview = page.frameLocator('iframe[title="H5 实时预览"]'); await expect(page.locator('.materials li')).toHaveCount(4); await expect(preview.locator('.benefit-product-card')).toBeVisible(); await expect(preview.locator('.benefit-actions')).toBeVisible(); await expect(preview.locator('.benefit-notice')).toBeVisible(); await expect(preview.locator('.benefit-product-list')).toBeVisible(); await page.locator('.materials li').first().click(); await page.getByLabel('权益商品名称').fill('网易云音乐黑胶月卡'); await page.getByLabel('权益交付方式').selectOption('code'); await page.getByLabel('权益业务标签').fill('卡密'); await expect(preview.locator('.benefit-product-card .benefit-name-row b')).toHaveText( '网易云音乐黑胶月卡', ); await expect(preview.locator('.benefit-product-card .delivery-badge')).toHaveText('卡密'); await page.getByRole('button', { name: '营销组件', exact: true }).click(); await expect(page.getByRole('button', { name: '横幅 Banner', exact: true })).toBeVisible(); await expect(page.locator('.materials li')).toHaveCount(4); });