feat(h5): align report care and single-product section with updated design
This commit is contained in:
@@ -171,18 +171,18 @@ try {
|
||||
await route.fulfill({ response, body: await response.text() + '\nMOCK_FIXTURE.result.skin_age = { value: ' + age + ' };' });
|
||||
});
|
||||
await page.goto(base + '?mock=1&screen=report'); await loadFonts();
|
||||
await page.waitForFunction(() => document.querySelector('#product-image').naturalWidth === 504);
|
||||
assert((await page.locator('#product-image').getAttribute('src')).endsWith('product-' + ({ 1: 25, 2: 35, 3: 50 }[tier]) + '.jpg'));
|
||||
await page.waitForFunction(() => document.querySelector('#product-image').naturalWidth === 481);
|
||||
assert((await page.locator('#product-image').getAttribute('src')).endsWith('product-' + ({ 1: 25, 2: 35, 3: 50 }[tier]) + '.png'));
|
||||
for (const width of widths) {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
await page.locator('.card-product').scrollIntoViewIfNeeded();
|
||||
const info = await rect('.prod-info'), name = await rect('#product-name'), title = await rect('#product-title');
|
||||
assert(title.right <= info.right && name.right <= info.right, 'product text overlaps art at ' + width + ': ' + JSON.stringify({ info, name, title }));
|
||||
assert(Math.abs(title.x - info.x - 26.736111) < .1);
|
||||
assert.equal(await page.locator('#product-name').evaluate(e => getComputedStyle(e).fontWeight), '600');
|
||||
assert.equal((await actualLines('#product-name')).length, 2);
|
||||
assert(Math.abs(title.x - info.x - 81 / 1080 * width) < .1);
|
||||
assert.equal(await page.locator('#product-name').evaluate(e => getComputedStyle(e).fontWeight), '700');
|
||||
assert.equal((await actualLines('#product-name')).length, 1);
|
||||
const card = await rect('.card-product');
|
||||
assert(Math.abs(card.width - 984 / 1080 * width) < .1, 'exact PSD card width');
|
||||
assert(Math.abs(card.width - 945 / 1080 * width) < .1, 'exact current Figma card width');
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||
}
|
||||
await page.setViewportSize({ width: 375, height: 726 }); await screenshot('05-product-' + tier, '.card-product');
|
||||
|
||||
@@ -129,10 +129,10 @@ try {
|
||||
const heading = el.querySelector('.card-heading'), body = el.querySelector('#care-list');
|
||||
return { margin: parseFloat(getComputedStyle(heading).marginBottom), gap: body.getBoundingClientRect().top - heading.getBoundingClientRect().bottom };
|
||||
});
|
||||
assert(Math.abs(spacing.margin - 9) < .1 && Math.abs(spacing.gap - 9) < 1, 'care title gap is halved from 18px to 9px');
|
||||
assert(Math.abs(spacing.margin - 5.208333) < .1 && Math.abs(spacing.gap - 5.208333) < 1, 'daily care title gap follows current Figma');
|
||||
if (artifacts) await page.locator('.card-care').screenshot({ path: path.join(artifacts, 'care-spacing.png') });
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: four-character bold/underlined privacy link, half care gap; guest success/failure no disk writes or saved history/identity; WeChat retains snapshots/photos/admin records; screenshots; no paid calls');
|
||||
console.log('PASS: four-character bold/underlined privacy link, current Figma care gap; guest success/failure no disk writes or saved history/identity; WeChat retains snapshots/photos/admin records; screenshots; no paid calls');
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (child.exitCode == null) { const stopped = once(child, 'exit'); child.kill(); await stopped; }
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Figma 1:65 visual layout, synthetic reports; no backend writes or paid calls.
|
||||
import assert from 'node:assert/strict';import fs from 'node:fs';import path from 'node:path';
|
||||
import {execSync} from 'node:child_process';import{fileURLToPath,pathToFileURL}from'node:url';
|
||||
const{chromium}=await import(pathToFileURL(execSync('npm root -g',{encoding:'utf8'}).trim()+'/playwright/index.mjs').href);
|
||||
const browser=await chromium.launch();const artifacts=process.env.H5_ARTIFACT_DIR;
|
||||
if(artifacts)fs.mkdirSync(artifacts,{recursive:true});
|
||||
try{
|
||||
const p=await browser.newPage({viewport:{width:375,height:1400},isMobile:true,hasTouch:true});const errors=[];
|
||||
p.on('pageerror',e=>errors.push(e.message));
|
||||
await p.route('**/api/**',r=>{assert.equal(r.request().method(),'GET');return r.fulfill({json:{ok:true,user:{kind:'browser'}}});});
|
||||
const source=fs.readFileSync(fileURLToPath(new URL('../js/app.js',import.meta.url)),'utf8');
|
||||
await p.route('**/js/app.js*',r=>r.fulfill({contentType:'text/javascript',body:source.replace(/\}\)\(\);\s*$/,`
|
||||
window.__bottomTest=function(age,legacy){
|
||||
var raw=JSON.parse(JSON.stringify(MOCK_FIXTURE.result));
|
||||
if(age==null)delete raw.skin_age;else raw.skin_age={value:age};
|
||||
var vm=SkinScore.buildViewModel(raw,MOCK_FIXTURE.side,{realAge:30});
|
||||
var presentation=JSON.parse(JSON.stringify(ReportUI.build(vm,raw)));
|
||||
if(legacy&&presentation.product){presentation.product.name='旧版名称';presentation.product.image='missing-legacy.png';}
|
||||
var before=JSON.stringify(presentation);
|
||||
renderReport(vm,MOCK_FIXTURE.photos[0],null,{result:raw,presentation:presentation});showScreen('s-report');
|
||||
return {before:before,after:JSON.stringify(presentation),tier:presentation.product&&presentation.product.tier};
|
||||
};
|
||||
})();`)}));
|
||||
await p.goto((process.env.H5_BASE_URL||'http://127.0.0.1:8080/')+'?mock=1&screen=report');
|
||||
await p.evaluate(()=>document.fonts.ready);
|
||||
const names=['焕活光泽 透亮紧致','恢复弹性 紧实利落','强韧屏障 轮廓向上'];
|
||||
for(const[age,tier]of[[24,1],[40,2],[60,3]]){
|
||||
const state=await p.evaluate(age=>window.__bottomTest(age,true),age);assert.equal(state.tier,tier);assert.equal(state.before,state.after);
|
||||
await p.waitForFunction(()=>document.querySelector('#product-image').complete&&document.querySelector('#product-image').naturalWidth===481);
|
||||
assert.equal(await p.locator('#product-name').textContent(),names[tier-1]);
|
||||
assert.equal(await p.locator('.card-product:visible').count(),1);
|
||||
assert.equal(await p.locator('.report-care-products .card-product').count(),1);
|
||||
assert.equal(await p.locator('.report-care-products .card-heading').textContent(),'日常护理');
|
||||
assert.equal(await p.locator('.product-section-heading').textContent(),'推荐产品');
|
||||
assert.equal(await p.locator('.care-summary').count(),0);
|
||||
assert((await p.locator('.report-disclaimer').textContent()).includes('仅根据您最近的测肤记录推荐'));
|
||||
for(const width of[320,375,414,480]){
|
||||
await p.setViewportSize({width,height:1400});await p.locator('.card-product').scrollIntoViewIfNeeded();
|
||||
const geo=await p.evaluate(()=>{
|
||||
const rect=s=>{const r=document.querySelector(s).getBoundingClientRect();return{x:r.x,y:r.y,w:r.width,h:r.height,right:r.right,bottom:r.bottom};};
|
||||
return{heading:rect('.prod-title'),care:rect('.card-care'),product:rect('.card-product'),info:rect('.prod-info'),title:rect('#product-title'),name:rect('#product-name'),image:rect('#product-image'),badge:rect('.prod-badge'),disclaimer:rect('.report-disclaimer'),align:getComputedStyle(document.querySelector('.prod-title')).textAlign,headingFont:parseFloat(getComputedStyle(document.querySelector('.prod-title')).fontSize)};
|
||||
});
|
||||
assert.equal(geo.align,'center');assert(geo.heading.bottom<=geo.care.y+1);
|
||||
assert(Math.abs(geo.heading.w-595/1080*width)<.1,'Figma text group width scales with preview');
|
||||
const stabilityFont=await p.locator('.dimension-stability-title').evaluate(e=>parseFloat(getComputedStyle(e).fontSize));
|
||||
assert(Math.abs(geo.headingFont-stabilityFont)<.01,'bottom title matches stability heading font size at every width');
|
||||
assert(Math.abs((geo.heading.x+geo.heading.w/2)-(geo.care.x+geo.care.w/2)-width/1080)<.1,'whole title group centered, retaining 1px design offset');
|
||||
assert(geo.product.bottom<=geo.care.bottom);assert(geo.disclaimer.y>=geo.care.bottom);
|
||||
assert(geo.title.right<=geo.info.right+1&&geo.name.right<=geo.info.right+1,'text must not overlap image');
|
||||
assert(Math.abs(geo.product.w/geo.product.h-945/511)<.01);
|
||||
assert(Math.abs(geo.image.w/geo.image.h-481/511)<.01);
|
||||
assert(Math.abs(geo.badge.w/geo.badge.h-101/77)<.01);
|
||||
assert(await p.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));
|
||||
}
|
||||
await p.setViewportSize({width:375,height:1400});await p.locator('.card-product').scrollIntoViewIfNeeded();
|
||||
if(artifacts){
|
||||
const top=await p.locator('.prod-title').evaluate(e=>e.getBoundingClientRect().top);
|
||||
const end=await p.locator('.report-disclaimer').evaluate(e=>e.getBoundingClientRect().bottom);
|
||||
await p.screenshot({path:path.join(artifacts,'report-bottom-'+tier+'.png'),clip:{x:0,y:Math.max(0,top-8),width:375,height:end-Math.max(0,top-8)+8}});
|
||||
}
|
||||
}
|
||||
await p.evaluate(()=>window.__bottomTest(null,false));
|
||||
assert.equal(await p.locator('.card-product').isVisible(),false);assert(await p.locator('#product-unavailable').isVisible());
|
||||
assert(await p.locator('.card-care').isVisible());assert(await p.locator('.prod-title').isVisible());
|
||||
assert.equal(await p.locator('#product-image').getAttribute('src'),null);
|
||||
// Long actual advice must push the product down rather than overlap it.
|
||||
await p.evaluate(()=>{window.__bottomTest(24,false);document.querySelector('#care-list').textContent='测试长建议。'.repeat(300);});
|
||||
const safe=await p.evaluate(()=>document.querySelector('#care-list').getBoundingClientRect().bottom<=document.querySelector('.product-section-heading').getBoundingClientRect().top);assert(safe);
|
||||
assert.deepEqual(errors,[]);console.log('PASS bottom: 3 selected products, 4 widths, exact image ratios, merged card, titles/disclaimer order, legacy recommendation immutable, missing/long content');
|
||||
}finally{await browser.close();}
|
||||
Reference in New Issue
Block a user