open-design/apps/web/tests/runtime/design-template-deck-nav.test.ts
ziyan2006 071db7ca1b
[codex] Stabilize HTML deck navigation state (#3142)
* fix: stabilize html deck navigation state

* fix: avoid misclassifying transform decks as scroll decks

* fix: detect default root-scroller decks

---------

Co-authored-by: Nongzi <3051966228@qq.com>
2026-05-29 07:41:10 +00:00

170 lines
5.4 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { JSDOM, VirtualConsole } from 'jsdom';
import type { DOMWindow } from 'jsdom';
const tasteEditorialExamplePath = fileURLToPath(
new URL('../../../../design-templates/html-ppt-taste-editorial/example.html', import.meta.url),
);
const simpleDeckExamplePath = fileURLToPath(
new URL('../../../../design-templates/simple-deck/example.html', import.meta.url),
);
function setupTasteEditorialDeck() {
const html = readFileSync(tasteEditorialExamplePath, 'utf8');
const dom = new JSDOM(html, {
pretendToBeVisual: true,
runScripts: 'dangerously',
url: 'https://example.test/taste-editorial.html',
virtualConsole: new VirtualConsole(),
});
return dom;
}
function setupSimpleDeck() {
const html = readFileSync(simpleDeckExamplePath, 'utf8');
const dom = new JSDOM(html, {
pretendToBeVisual: true,
runScripts: 'dangerously',
url: 'https://example.test/simple-deck.html',
virtualConsole: new VirtualConsole(),
});
const { window: win } = dom;
Object.defineProperty(win, 'innerWidth', {
configurable: true,
value: 1000,
});
Object.defineProperty(win.document.body, 'scrollWidth', {
configurable: true,
value: 6000,
});
Object.defineProperty(win.document.body, 'clientWidth', {
configurable: true,
value: 1000,
});
Object.defineProperty(win.document.documentElement, 'scrollWidth', {
configurable: true,
value: 6000,
});
Object.defineProperty(win.document.documentElement, 'clientWidth', {
configurable: true,
value: 1000,
});
Object.defineProperty(win.document, 'scrollingElement', {
configurable: true,
value: win.document.documentElement,
});
let bodyScrollLeft = 0;
let documentScrollLeft = 0;
Object.defineProperty(win.document.body, 'scrollLeft', {
configurable: true,
get: () => bodyScrollLeft,
set: (_value: number) => {
bodyScrollLeft = 0;
},
});
Object.defineProperty(win.document.documentElement, 'scrollLeft', {
configurable: true,
get: () => documentScrollLeft,
set: (value: number) => {
documentScrollLeft = value;
},
});
Object.defineProperty(win.document.body, 'scrollTo', {
configurable: true,
value: () => {},
});
Object.defineProperty(win.document.documentElement, 'scrollTo', {
configurable: true,
value: ({ left }: { left?: number }) => {
if (typeof left === 'number') {
documentScrollLeft = left;
}
},
});
return dom;
}
function activeSlideIndex(win: DOMWindow) {
const slides = Array.from(win.document.querySelectorAll<HTMLElement>('.deck > .slide'));
return slides.findIndex((slide) => slide.classList.contains('active'));
}
function fireTouch(win: DOMWindow, startX: number, endX: number) {
const start = new win.Event('touchstart', { bubbles: true, cancelable: true });
Object.defineProperty(start, 'touches', {
value: [{ clientX: startX, clientY: 120 }],
});
win.dispatchEvent(start);
const end = new win.Event('touchend', { bubbles: true, cancelable: true });
Object.defineProperty(end, 'changedTouches', {
value: [{ clientX: endX, clientY: 124 }],
});
win.dispatchEvent(end);
}
describe('design template deck navigation', () => {
it('wires the taste editorial example to the deck input contract', () => {
const dom = setupTasteEditorialDeck();
const { window: win } = dom;
const dots = Array.from(win.document.querySelectorAll<HTMLButtonElement>('#deck-nav .dot'));
expect(win.document.querySelectorAll('.deck > .slide')).toHaveLength(10);
expect(activeSlideIndex(win)).toBe(0);
expect(dots).toHaveLength(10);
expect(dots[0]?.classList.contains('active')).toBe(true);
win.dispatchEvent(new win.KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: 'ArrowRight',
}));
expect(activeSlideIndex(win)).toBe(1);
win.dispatchEvent(new win.WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaY: 90,
}));
expect(activeSlideIndex(win)).toBe(2);
fireTouch(win, 500, 360);
expect(activeSlideIndex(win)).toBe(3);
dots[6]?.dispatchEvent(new win.MouseEvent('click', { bubbles: true, cancelable: true }));
expect(activeSlideIndex(win)).toBe(6);
expect(dots[6]?.classList.contains('active')).toBe(true);
expect(dots[6]?.getAttribute('aria-current')).toBe('true');
});
it('keeps simple-deck keyboard navigation single-step and synced to documentElement scroll', () => {
const dom = setupSimpleDeck();
const { window: win } = dom;
const counter = win.document.getElementById('counter');
expect(counter?.textContent?.trim()).toBe('1 / 6');
expect(win.document.documentElement.scrollLeft).toBe(0);
win.document.body.dispatchEvent(new win.KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: 'ArrowRight',
}));
win.document.dispatchEvent(new win.Event('scroll', { bubbles: true }));
expect(counter?.textContent?.trim()).toBe('2 / 6');
expect(win.document.documentElement.scrollLeft).toBe(1000);
win.document.body.dispatchEvent(new win.KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: 'ArrowRight',
}));
win.document.dispatchEvent(new win.Event('scroll', { bubbles: true }));
expect(counter?.textContent?.trim()).toBe('3 / 6');
expect(win.document.documentElement.scrollLeft).toBe(2000);
});
});