Files
juzi/h5/test/harmony-picker.mjs
T

105 lines
7.3 KiB
JavaScript

// Real app + actual touch/mouse dispatch with varying UA/capabilities.
// Chromium UA regression, not a substitute for HarmonyOS hardware verification.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn, execSync } from 'node:child_process';
import { once } from 'node:events';
import { fileURLToPath, pathToFileURL } from 'node:url';
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
const h5 = fileURLToPath(new URL('../', import.meta.url));
const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-harmony-test-'));
let child, browser;
let base = process.env.H5_BASE_URL;
try {
if (!base) {
base = 'http://127.0.0.1:18116/';
child = spawn(process.execPath, ['server.js'], { cwd: h5, env: { ...process.env, PORT: '18116', HOST: '127.0.0.1', JUZI_DATA_ROOT: dataRoot,
PUBLIC_BASE_URL: '', WECHAT_APP_ID: '', WECHAT_APP_SECRET: '', SKIN_API_KEY: 'fixture', SKIN_API_SECRET: 'fixture' }, stdio: ['ignore', 'pipe', 'pipe'] });
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(Error('test startup timeout')), 10000);
child.stdout.on('data', b => { if (b.toString().includes('已启动')) { clearTimeout(timer); resolve(); } });
child.once('exit', code => { clearTimeout(timer); reject(Error('server exited ' + code)); });
});
}
browser = await chromium.launch({ headless: true });
const android = 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Mobile Safari/537.36';
const desktop = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36';
const cases = [
{ name: 'Android touch', ua: android, touch: true },
{ name: 'OpenHarmony 5 touch', ua: 'Mozilla/5.0 (Phone; OpenHarmony 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ArkWeb/4.1.6.1 Mobile', touch: true },
{ name: 'OpenHarmony 6 WeChat touch', ua: 'Mozilla/5.0 (Phone;OpenHarmony 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 ArkWeb/6.0.0.42 Mobile MicroMessenger/8.0', touch: true },
{ name: 'Desktop UA on touch hardware', ua: desktop, touch: true },
{ name: 'Desktop mouse without TouchEvent', ua: desktop, touch: false },
{ name: 'Android UA without touch hardware', ua: android, touch: false }
];
for (const fixture of cases) {
const context = await browser.newContext({ viewport: { width: 375, height: 726 }, isMobile: fixture.touch, hasTouch: fixture.touch, userAgent: fixture.ua });
const page = await context.newPage(), errors = [], writes = [];
page.setDefaultTimeout(30000);
page.on('pageerror', e => errors.push(e.message));
if (!fixture.touch) await page.addInitScript(() => { delete window.TouchEvent; });
await page.route('**/*', route => {
const req = route.request();
if (req.method() !== 'GET') { writes.push(req.url()); return route.abort(); }
if (/\.woff2?(\?|$)/.test(req.url())) return route.abort();
if (req.url().endsWith('/api/auth/session')) return route.fulfill({ json: { ok: true, user: { id: 'harmony-picker-fixture', kind: 'wechat' } } });
return route.continue();
});
await page.route('**/mobile-select.iife.min.js', async route => {
const response = await route.fetch();
await route.fulfill({ response, body: await response.text() + ';var OriginalMobileSelect=MobileSelect;window.__pickers={};MobileSelect=function(c){var p=new OriginalMobileSelect(c);window.__pickers[c.trigger]=p;return p;};' });
});
await page.goto(base, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#s-splash.active');
await page.click('[data-action="open-privacy"]'); await page.click('#privacy-check'); await page.click('[data-action="agree-continue"]');
await page.waitForSelector('#s-profile.active');
const cdp = await context.newCDPSession(page);
for (const trigger of ['#pick-birth', '#pick-region']) {
await page.click(trigger); await page.waitForTimeout(350);
const events = await page.evaluate(t => __pickers[t].eventHandleMap.panel.event, trigger);
assert.deepEqual(events, fixture.touch ? ['touchstart', 'touchend', 'touchmove'] : ['mousedown', 'mousemove', 'mouseup'], fixture.name);
assert.equal(await page.locator('.ms-mobile-select.ms-show').evaluate(el => !!el.closest('#s-profile')), false, 'profile scroll lock cannot intercept picker touch');
for (const column of [0, 1]) {
if (trigger === '#pick-region' && column === 1) await page.evaluate(() => __pickers['#pick-region'].locatePosition(0, REGION_DATA.findIndex(p => p[0] === '浙江省')));
const before = await page.evaluate(t => JSON.stringify(__pickers[t].getCurValue()), trigger);
const wheel = page.locator('.ms-mobile-select.ms-show .ms-wheel').nth(column), box = await wheel.boundingBox();
const x = box.x + box.width / 2, y = box.y + box.height / 2;
if (fixture.touch) {
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x, y, id: 1 }] });
for (let i = 1; i <= 4; i++) {
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y: y - i * 20, id: 1 }] });
await page.waitForTimeout(25);
}
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
} else {
await page.mouse.move(x, y); await page.mouse.down(); await page.mouse.move(x, y - 80, { steps: 4 }); await page.mouse.up();
}
await page.waitForTimeout(250);
assert.notEqual(await page.evaluate(t => JSON.stringify(__pickers[t].getCurValue()), trigger), before, fixture.name + '/' + trigger + '/' + column + ' must move');
}
const chosen = await page.evaluate(t => __pickers[t].getCurValue(), trigger);
await page.click('.ms-mobile-select.ms-show .ms-ensure'); await page.waitForTimeout(250);
if (trigger === '#pick-birth') {
const value = parseInt(chosen[0]) + '-' + String(parseInt(chosen[1])).padStart(2, '0');
assert.equal(await page.locator('#f-birth').inputValue(), value);
} else {
const value = await page.locator('#f-region').inputValue();
assert(value.includes(chosen[0].value) && value.includes(chosen[1].value));
}
await page.click(trigger); await page.waitForTimeout(300);
assert.deepEqual(await page.evaluate(t => __pickers[t].getCurValue(), trigger), chosen, 'reopening retains confirmed values');
await page.click('.ms-mobile-select.ms-show .ms-cancel'); await page.waitForTimeout(250);
}
await page.click('#pick-gender'); await page.click('.ms-mobile-select.ms-show .ms-ensure');
await page.click('[data-action="submit-profile"]'); await page.waitForSelector('#p-photoguide.show');
assert.deepEqual(errors, []); assert.deepEqual(writes, []);
console.log('PASS ' + fixture.name + ': year/month/province/city drag, cascade, confirm/reopen, profile validation');
await context.close();
}
} finally {
if (browser) await browser.close();
if (child && child.exitCode == null) { const stopped = once(child, 'exit'); child.kill(); await stopped; }
fs.rmSync(dataRoot, { recursive: true, force: true });
}