fix: mask hover text to the slash edges
Reference site / build (push) Canceled after 0s

This commit is contained in:
2026-10-10 19:33:17 +09:00
parent 5d418beeba
commit e03ff8e650
4 changed files with 78 additions and 11 deletions
+56 -1
View File
@@ -34,13 +34,68 @@ for (const trigger of ['click', 'wheel', 'key']) {
});
}
test('hover streams are masked at the actual slash edges throughout the loop', async ({ page }, testInfo) => {
await armed(page);
const root = page.locator('.root-mark');
const box = (await root.boundingBox())!;
const shape = (await root.locator('.root-shape').boundingBox())!;
const clip = { x: Math.floor(box.x) - 8, y: Math.floor(box.y) - 8, width: Math.ceil(box.width) + 16, height: Math.ceil(box.height) + 16 };
const before = (await page.screenshot({ clip })).toString('base64');
await root.hover();
await expect(page.locator('.tap-mask')).toHaveAttribute('mask', 'url(#tap-edge-mask)');
const mask = await page.locator('#tap-edge-mask rect').evaluate((rect) => {
const svg = rect.closest('svg')!;
const matrix = svg.getScreenCTM()!;
return { top: new DOMPoint(0, 4).matrixTransform(matrix).y, bottom: new DOMPoint(0, 60).matrixTransform(matrix).y };
});
expect(mask.top).toBeCloseTo(shape.y, 1);
expect(mask.bottom).toBeCloseTo(shape.y + shape.height, 1);
for (const time of [0, 700, 1399]) {
await page.locator('.tap-stream text').evaluateAll((texts, time) => {
texts.forEach((text) => text.getAnimations().forEach((animation) => {
animation.pause();
animation.currentTime = time;
}));
}, time);
const after = (await page.screenshot({ clip, path: testInfo.outputPath(`hover-mask-${time}.png`) })).toString('base64');
const ink = await page.evaluate(async ({ before, after }) => {
const pixels = async (base64: string) => {
const image = new Image();
image.src = 'data:image/png;base64,' + base64;
await image.decode();
const canvas = document.createElement('canvas');
canvas.width = image.width; canvas.height = image.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(image, 0, 0);
return { data: ctx.getImageData(0, 0, image.width, image.height).data, width: image.width };
};
const a = await pixels(before), b = await pixels(after);
let minY = Infinity, maxY = -Infinity, count = 0;
for (let i = 0; i < a.data.length; i += 4) {
if (Math.abs(a.data[i] - b.data[i]) + Math.abs(a.data[i + 1] - b.data[i + 1]) + Math.abs(a.data[i + 2] - b.data[i + 2]) > 30) {
const y = Math.floor(i / 4 / a.width);
minY = Math.min(minY, y); maxY = Math.max(maxY, y); count++;
}
}
return { minY, maxY, count };
}, { before, after });
expect(ink.count).toBeGreaterThan(50);
expect(ink.minY + clip.y).toBeGreaterThanOrEqual(Math.floor(shape.y));
expect(ink.maxY + clip.y).toBeLessThanOrEqual(Math.ceil(shape.y + shape.height));
}
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'armed');
await page.mouse.move(0, 0);
await expect(page.locator('.tap-stream-a')).toHaveCSS('opacity', '0');
await expect(page.locator('.tap-stream-b')).toHaveCSS('opacity', '0');
});
test('continuous motion overlaps growth, travel and reveal without intermediate stops', async ({ page }, testInfo) => {
await page.clock.install({ time: new Date('2026-01-01T00:00:00Z') });
await page.clock.pauseAt(new Date('2026-01-01T00:00:01Z'));
await armed(page);
const root = page.locator('.root-mark');
await expect(root.locator('svg .root-shape')).toHaveCount(1);
await expect(root).toHaveText(''); // Geometry, never a font glyph.
await expect(root.locator('.root-shape')).toHaveAttribute('d', 'M31 4H41L17 60H7Z'); // The slash is geometry, not a font glyph.
await expect(page.locator('.call-panel')).toHaveAttribute('inert', '');
const initial = await root.boundingBox();
expect(initial).not.toBeNull();