| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- import assert from 'node:assert/strict';
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
- import { pathToFileURL } from 'node:url';
- import { chromium } from 'playwright';
- import { checkCustomer } from './customer-functional.mjs';
- // 读取浏览器公开布局及命中元素,检查被容器裁切的内容和实际可点击范围。
- export async function checkCustomerResponsive(page, baseUrl, accounts) {
- await checkCustomer(page, baseUrl, accounts);
- const pages = ['home','categories','product','cart','profile','messages','address','favorites','settings','chat','login','orders','order','policy','apply','proposal','ticket','memory','identity'];
- const sizes = [[320,568],[360,640],[375,667],[390,844],[414,896],[430,932],[490,886],[768,1024],[844,390],[390,420],[1440,900]];
- const checks = [], failures = [];
- for (const [width, height] of sizes) {
- await page.setViewportSize({ width, height });
- for (const name of pages) {
- await page.evaluate(value => { location.hash = `/customer/${value}`; }, name);
- await page.locator(`.mobile[data-page="${name}"]`).waitFor();
- await page.waitForFunction(() => !document.querySelector('.content[aria-busy="true"]'));
- await page.evaluate(() => window.scrollTo(0, 0));
- const issues = await page.locator('.mobile').evaluate(mobile => {
- const problems = [], bounds = mobile.getBoundingClientRect();
- const visible = node => node.getClientRects().length && getComputedStyle(node).visibility !== 'hidden';
- const label = node => `${node.className}: ${node.textContent.trim().slice(0,45)}`;
- if (document.documentElement.scrollWidth > innerWidth + 1) problems.push('页面横向溢出');
- for (const node of mobile.querySelectorAll('.goods-card,.goods-info,.van-button,.van-button__text,.mobile-header strong,.order-product,.van-field__control,.box,.van-tabbar-item,.catalog')) {
- if (!visible(node)) continue;
- const rect = node.getBoundingClientRect();
- if (rect.left < bounds.left - 1 || rect.right > bounds.right + 1) problems.push(`容器裁切 ${label(node)}`);
- if (node.matches('.van-button')) {
- if (rect.height < 43.5 || rect.width < 43.5) problems.push(`触控不足44px ${label(node)} ${rect.width}×${rect.height}`);
- const text = node.querySelector('.van-button__text');
- if (text) { const t = text.getBoundingClientRect(); if (t.top < rect.top - 1 || t.bottom > rect.bottom + 1 || t.left < rect.left - 1 || t.right > rect.right + 1) problems.push(`按钮文字超出 ${label(node)}`); }
- }
- }
- const tabs = mobile.querySelector('.shop-tabs');
- if (tabs) { const rect = tabs.getBoundingClientRect(); if (rect.top < 0 || rect.bottom > innerHeight + 1) problems.push('底部导航未显示在视口内'); }
- return [...new Set(problems)];
- });
- checks.push({ page:name, width, height, passed:issues.length === 0 });
- if (issues.length) failures.push({ page:name, width, height, issues });
- }
- await page.evaluate(() => { location.hash='/customer/home'; });
- await page.locator('.mobile[data-page="home"]').waitFor();
- await page.locator('.mobile .content').evaluate(node => node.scrollTo(0,node.scrollHeight));
- const tabs = page.locator('.shop-tabs');
- await tabs.getByRole('tab',{name:'分类',exact:true}).click();
- await page.locator('.mobile[data-page="categories"]').waitFor();
- await page.waitForFunction(() => window.scrollY === 0);
- await page.waitForFunction(() => document.querySelector('.mobile .content').scrollTop === 0);
- const header = await page.locator('.mobile-header').boundingBox();
- if (header.y < 0) failures.push({width,height,issues:['页面切换后标题不可见']});
- // 检查视口底部入口的实际命中范围,包含内容滚动后的状态。
- for (const selector of ['.shop-tabs [role="tab"]','.ai-float']) {
- const uncovered = await page.locator(selector).evaluateAll(nodes => nodes.every(node => {
- const rect=node.getBoundingClientRect(), hit=document.elementFromPoint(rect.x+rect.width/2,rect.y+rect.height/2);
- return hit && node.contains(hit);
- }));
- if (!uncovered) failures.push({width,height,issues:[`入口受到遮挡 ${selector}`]});
- }
- }
- const popupChecks = [];
- for (const [width,height] of [[320,568],[390,420],[844,390]]) {
- await page.setViewportSize({width,height});
- await page.evaluate(() => { location.hash='/customer/memory'; });
- await page.locator('.mobile[data-page="memory"]').waitFor();
- await page.waitForFunction(() => !document.querySelector('.content[aria-busy="true"]'));
- const authorize=page.getByRole('button',{name:'授权保存',exact:true});
- if (await authorize.count()) {
- await authorize.click();
- await page.getByRole('dialog').getByRole('button',{name:'确认操作',exact:true}).click();
- await page.getByRole('dialog').waitFor({state:'hidden'});
- }
- await page.getByRole('button',{name:'撤销授权',exact:true}).click();
- const dialog=page.getByRole('dialog');
- await dialog.waitFor();
- const dialogFits=await dialog.evaluate(node => {const r=node.getBoundingClientRect(); return r.x>=0 && r.y>=0 && r.right<=innerWidth+1 && r.bottom<=innerHeight+1;});
- if (!dialogFits) failures.push({width,height,issues:['确认窗口超出视口']});
- await dialog.getByRole('button',{name:'返回检查',exact:true}).click();
- await dialog.waitFor({state:'hidden'});
- for (const [field,option] of [['.memory-style-field','详细回复'],['.memory-language-field','English']]) {
- if (field === '.memory-language-field') await page.locator(field).getByRole('button').evaluate(node => node.scrollIntoView({block:'end'}));
- await page.locator(field).getByRole('button').click();
- const menu=page.locator(field).getByRole('menu');
- await menu.waitFor();
- // 等待组件自身进入动画结束后测量最终布局。
- await page.waitForFunction(() => !document.querySelector('.van-popup-slide-top-enter-active,.van-popup-slide-bottom-enter-active'));
- const fits=await menu.evaluate(node => {const r=node.getBoundingClientRect(),m=document.querySelector('.mobile').getBoundingClientRect(); return r.left>=m.left-1 && r.right<=m.right+1 && r.top>=0 && r.bottom<=innerHeight+1;});
- if (!fits) failures.push({width,height,issues:[`下拉菜单超出手机容器 ${field}`]});
- await menu.getByRole('menuitem').filter({hasText:option}).click();
- await menu.waitFor({state:'hidden'});
- }
- popupChecks.push({width,height,dialogFits});
- }
- await page.getByRole('button',{name:'撤销授权',exact:true}).click();
- await page.getByRole('dialog').getByRole('button',{name:'确认操作',exact:true}).click();
- await page.getByRole('dialog').waitFor({state:'hidden'});
- await mkdir(new URL('../../.work/frontend-verification/', import.meta.url), { recursive:true });
- await writeFile(new URL('../../.work/frontend-verification/customer-responsive.json', import.meta.url), JSON.stringify({ checks, popupChecks, failures, screenshots:0 }, null, 2));
- assert.equal(failures.length, 0, `适配问题见 .work/frontend-verification/customer-responsive.json:${JSON.stringify(failures.slice(0,5))}`);
- return checks;
- }
- if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
- const accounts = JSON.parse(await readFile(new URL('../../.work/frontend-accounts.json', import.meta.url), 'utf8'));
- const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || 'C:/Users/Administrator/AppData/Local/ms-playwright/chromium-1228/chrome-win64/chrome.exe'});
- try {
- const page = await browser.newPage();
- const errors = []; page.on('pageerror', error => errors.push(error.message));
- const checks = await checkCustomerResponsive(page, process.env.FRONTEND_URL || 'http://127.0.0.1:18763', accounts);
- assert.deepEqual(errors, []);
- console.log(JSON.stringify({passed:checks.length,browserErrors:errors}));
- } finally { await browser.close(); }
- }
|