94 lines
6.0 KiB
JavaScript
94 lines
6.0 KiB
JavaScript
import { chromium } from 'playwright';
|
|
import assert from 'node:assert/strict';
|
|
import { execFileSync, spawn } from 'node:child_process';
|
|
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
import { createHash, randomBytes } from 'node:crypto';
|
|
import net from 'node:net';
|
|
|
|
const php = process.env.PHP_BINARY || 'php';
|
|
const hash = async () => createHash('sha256').update(await readFile('var/kantit.db')).digest('hex');
|
|
const before = await hash();
|
|
const fixture = JSON.parse(execFileSync(php, ['tests/pilot-fixture.php'], { encoding: 'utf8', windowsHide: true }));
|
|
const env = { ...process.env, APP_ENV: 'prod', APP_DEBUG: '0', APP_SECRET: randomBytes(32).toString('hex'), DATABASE_URL: fixture.databaseUrl, MEDIA_DIRECTORY: fixture.mediaDirectory };
|
|
const probe = net.createServer(); await new Promise(resolve => probe.listen(0, '127.0.0.1', resolve));
|
|
const port = probe.address().port; await new Promise(resolve => probe.close(resolve));
|
|
const origin = `http://127.0.0.1:${port}`;
|
|
const server = spawn(php, ['-d', 'memory_limit=256M', '-S', `127.0.0.1:${port}`, '-t', 'public', 'bin/dev-router.php'], { windowsHide: true, stdio: 'ignore', env: { ...env, SITE_ORIGIN: origin } });
|
|
const report = 'reports/motion'; await mkdir(report, { recursive: true });
|
|
const results = [], errors = [];
|
|
let browser;
|
|
try {
|
|
for (let attempt = 0; attempt < 60; attempt++) {
|
|
try { if ((await fetch(origin)).ok) break; } catch {}
|
|
if (attempt === 59) throw new Error('Pilot server did not start');
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
}
|
|
browser = await chromium.launch();
|
|
for (const width of [1600, 390]) {
|
|
const context = await browser.newContext({ viewport: { width, height: 900 }, reducedMotion: 'no-preference' });
|
|
await context.addInitScript(() => {
|
|
window.motionCalls = [];
|
|
const animate = Element.prototype.animate;
|
|
Element.prototype.animate = function (frames, options) {
|
|
const animation = animate.call(this, frames, options);
|
|
window.motionCalls.push({ id: this.id, frames, options, scrollY, top: this.getBoundingClientRect().top });
|
|
return animation;
|
|
};
|
|
});
|
|
const page = await context.newPage(); page.on('pageerror', error => errors.push(error.message));
|
|
await page.goto(origin); await page.waitForTimeout(1200);
|
|
const inventory = await page.locator('[class*="fx-entrance-"]').evaluateAll(nodes => nodes.map(node => ({ id: node.id, classes: node.className })));
|
|
assert.ok(inventory.length > 0, 'Pilot has entrance effects');
|
|
assert.equal(await page.locator('script[src*="motion.js"]').count(), 1);
|
|
const target = page.locator('#kontakt');
|
|
assert.equal(await page.evaluate(() => motionCalls.some(call => call.id === 'kontakt')), false, 'Below-fold section must wait for scrolling');
|
|
await target.scrollIntoViewIfNeeded();
|
|
await page.waitForFunction(() => motionCalls.some(call => call.id === 'kontakt'));
|
|
const calls = await page.evaluate(() => motionCalls);
|
|
assert.equal(calls.find(call => call.id === 'kontakt').frames[0].transform, 'translateY(28px)');
|
|
await page.waitForTimeout(800);
|
|
await page.evaluate(() => scrollTo(0, 0)); await target.scrollIntoViewIfNeeded(); await page.waitForTimeout(100);
|
|
assert.equal(await page.evaluate(() => motionCalls.filter(call => call.id === 'kontakt').length), 1, 'Entrance runs once');
|
|
await page.screenshot({ path: `${report}/pilot-${width}.png` });
|
|
|
|
// Exercise both existing choices on the real pilot element without editing saved content.
|
|
for (const direction of ['left', 'right']) {
|
|
await page.evaluate(async direction => {
|
|
const node = document.getElementById('kontakt');
|
|
node.classList.remove('fx-entrance-rise', 'fx-entrance-left', 'fx-entrance-right');
|
|
node.classList.add(`fx-entrance-${direction}`);
|
|
const { initMotion } = await import(document.querySelector('script[src*="motion.js"]').src);
|
|
window.stopMotionCheck?.(); window.motionCalls = [];
|
|
window.stopMotionCheck = initMotion();
|
|
}, direction);
|
|
await page.waitForFunction(() => motionCalls.some(call => call.id === 'kontakt'));
|
|
const actual = await target.evaluate(node => {
|
|
const animation = node.getAnimations()[0]; animation.pause(); animation.currentTime = 150;
|
|
return { transform: getComputedStyle(node).transform, frames: animation.effect.getKeyframes() };
|
|
});
|
|
assert.equal(actual.frames[0].transform, direction === 'left' ? 'translateX(-36px)' : 'translateX(36px)');
|
|
const offset = Number(actual.transform.split(',')[4]);
|
|
assert.ok(direction === 'left' ? offset < 0 : offset > 0, 'Computed movement follows selected direction');
|
|
await page.screenshot({ path: `${report}/${direction}-${width}.png` });
|
|
await page.evaluate(() => window.stopMotionCheck());
|
|
}
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
await page.reload(); await target.scrollIntoViewIfNeeded(); await page.waitForTimeout(800);
|
|
assert.equal(await page.evaluate(() => motionCalls.length), 0, 'Reduced motion suppresses animations');
|
|
assert.equal(await target.evaluate(node => getComputedStyle(node).opacity), '1');
|
|
results.push({ width, inventory, calls, checks: ['scroll trigger', 'once only', 'left/right computed movement', 'reduced motion'] });
|
|
await context.close();
|
|
}
|
|
const noJs = await browser.newContext({ javaScriptEnabled: false });
|
|
const page = await noJs.newPage(); await page.goto(origin);
|
|
assert.ok(await page.locator('main').innerText());
|
|
assert.equal(await page.locator('#kontakt').evaluate(node => getComputedStyle(node).opacity), '1');
|
|
await noJs.close();
|
|
assert.deepEqual(errors, []);
|
|
assert.equal(await hash(), before, 'Original database unchanged');
|
|
await writeFile(`${report}/result.json`, JSON.stringify({ results, errors, noJavaScript: true, originalUnchanged: true }, null, 2));
|
|
console.log(JSON.stringify({ passed: true, devices: results.map(result => result.width), effects: results[0].inventory, report }));
|
|
} finally {
|
|
await browser?.close(); server.kill();
|
|
}
|