mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* 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>
170 lines
5.4 KiB
TypeScript
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);
|
|
});
|
|
});
|