mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
285 lines
11 KiB
JavaScript
285 lines
11 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { beforeEach, afterEach, describe, test } from 'node:test';
|
|
|
|
import { advanceSlide, createCarousel, destroyCarousel } from '../js/blog/carousel.js';
|
|
import { mountProgressBar } from '../js/blog/progressBar.js';
|
|
import {
|
|
createBlogDomEnvironment,
|
|
createSpy,
|
|
readDynamicStyleRules,
|
|
} from './test-helpers/blog-dom.mjs';
|
|
|
|
function assertNoInlineStyleAttributes(elements, messagePrefix = 'element') {
|
|
elements.forEach((element, index) => {
|
|
assert.ok(element, `${messagePrefix} at index ${index} should exist`);
|
|
assert.strictEqual(
|
|
element.hasAttribute('style'),
|
|
false,
|
|
`${messagePrefix} at index ${index} should not set inline style attributes`,
|
|
);
|
|
});
|
|
}
|
|
|
|
const CAROUSEL_HTML = `
|
|
<div data-carousel>
|
|
<div data-carousel-track>
|
|
<article data-slide="0">Slide 1</article>
|
|
<article data-slide="1">Slide 2</article>
|
|
<article data-slide="2">Slide 3</article>
|
|
</div>
|
|
<button type="button" data-carousel-prev>Prev</button>
|
|
<button type="button" data-carousel-next>Next</button>
|
|
</div>
|
|
`;
|
|
|
|
const PROGRESS_HTML = '<div id="progress-root"></div>';
|
|
|
|
describe('blog carousel', () => {
|
|
let env;
|
|
let container;
|
|
let track;
|
|
let slides;
|
|
let prevButton;
|
|
let nextButton;
|
|
|
|
beforeEach(() => {
|
|
env = createBlogDomEnvironment(CAROUSEL_HTML);
|
|
const doc = env.document;
|
|
container = doc.querySelector('[data-carousel]');
|
|
track = container.querySelector('[data-carousel-track]');
|
|
slides = Array.from(track.children);
|
|
prevButton = container.querySelector('[data-carousel-prev]');
|
|
nextButton = container.querySelector('[data-carousel-next]');
|
|
|
|
env.inlineStyleGuard.guard(container);
|
|
env.inlineStyleGuard.guard(track);
|
|
env.inlineStyleGuard.guard(prevButton);
|
|
env.inlineStyleGuard.guard(nextButton);
|
|
slides.forEach((slide) => env.inlineStyleGuard.guard(slide));
|
|
});
|
|
|
|
afterEach(() => {
|
|
env.cleanup();
|
|
});
|
|
|
|
test('initializes slides with dynamic styling and accessibility state', () => {
|
|
const state = createCarousel(container, {
|
|
perPage: 1,
|
|
loop: true,
|
|
controls: { prev: prevButton, next: nextButton },
|
|
});
|
|
|
|
assert.ok(state, 'state should be created when container markup is valid');
|
|
assert.strictEqual(state.perPage, 1);
|
|
assert.strictEqual(state.slides.length, 3);
|
|
assert.ok(container.classList.contains('splide'));
|
|
assert.ok(container.classList.contains('is-initialized'));
|
|
assert.ok(container.classList.contains('is-active'));
|
|
|
|
const trackDynamicClasses = Array.from(track.classList).filter((className) => className.startsWith('bvds-'));
|
|
assert.ok(trackDynamicClasses.length >= 1, 'track should receive dynamic style classes');
|
|
|
|
assertNoInlineStyleAttributes([container, track, ...slides], 'carousel element');
|
|
|
|
for (const slide of slides) {
|
|
assert.ok(slide.classList.contains('splide__slide'));
|
|
const dynamicClasses = Array.from(slide.classList).filter((className) => className.startsWith('bvds-'));
|
|
assert.ok(dynamicClasses.length >= 1, 'slide should receive dynamic sizing styles');
|
|
}
|
|
|
|
assert.strictEqual(slides[0].getAttribute('aria-hidden'), 'false');
|
|
assert.strictEqual(slides[1].getAttribute('aria-hidden'), 'true');
|
|
assert.strictEqual(slides[2].getAttribute('aria-hidden'), 'true');
|
|
|
|
state.goTo(2);
|
|
assert.strictEqual(state.index, 2);
|
|
assert.strictEqual(slides[2].getAttribute('aria-hidden'), 'false');
|
|
assert.strictEqual(slides[0].getAttribute('aria-hidden'), 'true');
|
|
|
|
destroyCarousel(state);
|
|
});
|
|
|
|
test('control buttons advance slides and respect looping', () => {
|
|
const state = createCarousel(container, {
|
|
perPage: 1,
|
|
loop: true,
|
|
controls: { prev: prevButton, next: nextButton },
|
|
});
|
|
|
|
nextButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
|
|
assert.strictEqual(state.index, 1);
|
|
nextButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
|
|
assert.strictEqual(state.index, 2);
|
|
nextButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
|
|
assert.strictEqual(state.index, 0, 'looping should wrap forward navigation to the first slide');
|
|
|
|
prevButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
|
|
assert.strictEqual(state.index, 2, 'looping should wrap backward navigation to the last slide');
|
|
|
|
destroyCarousel(state);
|
|
});
|
|
|
|
test('auto play progress resets when navigation occurs', () => {
|
|
const state = createCarousel(container, {
|
|
perPage: 1,
|
|
controls: { prev: prevButton, next: nextButton },
|
|
});
|
|
const events = [];
|
|
const progress = {
|
|
reset: () => events.push('reset'),
|
|
start: (duration) => events.push(['start', duration]),
|
|
destroy: () => events.push('destroy'),
|
|
};
|
|
state.setProgress(progress);
|
|
state.autoPlayInterval = 4200;
|
|
|
|
state.goTo(1);
|
|
assert.deepStrictEqual(events, ['reset', ['start', 4200]]);
|
|
|
|
events.length = 0;
|
|
state.goTo(2, { restartProgress: false });
|
|
assert.deepStrictEqual(events, [], 'progress should not restart when restartProgress is false');
|
|
|
|
destroyCarousel(state);
|
|
});
|
|
|
|
test('destroyCarousel tears down listeners and progress state', () => {
|
|
const state = createCarousel(container, {
|
|
perPage: 1,
|
|
controls: { prev: prevButton, next: nextButton },
|
|
});
|
|
const progressEvents = [];
|
|
const progress = {
|
|
reset: () => progressEvents.push('reset'),
|
|
start: () => progressEvents.push('start'),
|
|
destroy: () => progressEvents.push('destroy'),
|
|
};
|
|
state.setProgress(progress);
|
|
state.autoPlayInterval = 2000;
|
|
state.goTo(1);
|
|
|
|
destroyCarousel(state);
|
|
assert.ok(progressEvents.includes('destroy'), 'progress.destroy should be called during teardown');
|
|
assert.ok(!container.classList.contains('is-initialized'));
|
|
assert.ok(!container.classList.contains('is-active'));
|
|
assert.strictEqual(state.cleanup.length, 0);
|
|
|
|
assertNoInlineStyleAttributes([container, track, ...slides], 'carousel element');
|
|
|
|
const previousIndex = state.index;
|
|
nextButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
|
|
assert.strictEqual(state.index, previousIndex, 'clicks after destroy should not change the index');
|
|
});
|
|
|
|
test('advanceSlide delegates to goTo with relative offsets', () => {
|
|
const state = createCarousel(container, { perPage: 1 });
|
|
const goToSpy = createSpy();
|
|
state.goTo = goToSpy;
|
|
state.index = 2;
|
|
|
|
advanceSlide(state, -1, { restartProgress: false });
|
|
assert.strictEqual(goToSpy.callCount, 1);
|
|
assert.deepStrictEqual(goToSpy.calls[0], [1, { restartProgress: false }]);
|
|
});
|
|
});
|
|
|
|
|
|
describe('progress bar', () => {
|
|
let env;
|
|
let container;
|
|
|
|
beforeEach(() => {
|
|
env = createBlogDomEnvironment(PROGRESS_HTML);
|
|
container = env.document.getElementById('progress-root');
|
|
env.inlineStyleGuard.guard(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
env.cleanup();
|
|
});
|
|
|
|
test('mountProgressBar renders semantic progress meter without style injection', () => {
|
|
const progress = mountProgressBar(container, { initialDuration: 500, autoStart: false });
|
|
assert.ok(progress, 'progress instance should be returned');
|
|
|
|
const wrapper = container.querySelector('.splide__progress');
|
|
assert.ok(wrapper, 'wrapper element should be appended');
|
|
const meter = wrapper.querySelector('progress.splide__progress__bar');
|
|
assert.ok(meter, 'progress meter should be created');
|
|
assert.strictEqual(meter.max, 100, 'progress meter should expose a 0-100 range');
|
|
assert.strictEqual(meter.value, 0, 'progress meter should start at zero');
|
|
assert.strictEqual(meter.dataset.state, 'idle');
|
|
assert.strictEqual(meter.dataset.progress, '0');
|
|
assert.strictEqual(
|
|
meter.getAttribute('aria-valuetext'),
|
|
'Carousel progress 0% complete',
|
|
'progress meter should emit accessible status text',
|
|
);
|
|
|
|
progress.reset();
|
|
|
|
assert.strictEqual(readDynamicStyleRules(env.document), '', 'no dynamic style rules should be injected');
|
|
assert.strictEqual(env.document.getElementById('bitvid-style-system'), null);
|
|
|
|
assertNoInlineStyleAttributes([container, wrapper, meter], 'progress element');
|
|
});
|
|
|
|
test('autoStart runs animation and triggers completion callback', () => {
|
|
const onComplete = createSpy();
|
|
const progress = mountProgressBar(container, { initialDuration: 600, autoStart: true, onComplete });
|
|
const wrapper = progress.element;
|
|
const meter = wrapper.querySelector('progress.splide__progress__bar');
|
|
assert.ok(meter, 'progress meter should exist when autoStart is enabled');
|
|
|
|
assertNoInlineStyleAttributes([container, wrapper, meter], 'progress element');
|
|
|
|
env.raf.step(0);
|
|
env.raf.step(300);
|
|
assert.strictEqual(onComplete.callCount, 0);
|
|
assert.ok(meter.value > 0 && meter.value < 100, 'meter should report intermediate progress');
|
|
assert.strictEqual(meter.dataset.state, 'active');
|
|
|
|
env.raf.step(300);
|
|
assert.strictEqual(onComplete.callCount, 1);
|
|
assert.strictEqual(meter.value, 100);
|
|
assert.strictEqual(meter.dataset.state, 'complete');
|
|
assert.strictEqual(meter.dataset.progress, '100');
|
|
|
|
assert.strictEqual(readDynamicStyleRules(env.document), '', 'no dynamic style rules should be injected');
|
|
assert.strictEqual(env.document.getElementById('bitvid-style-system'), null);
|
|
});
|
|
|
|
test('manual controls support stop, restart, and cleanup', () => {
|
|
const onComplete = createSpy();
|
|
const progress = mountProgressBar(container, { initialDuration: 800, onComplete });
|
|
const meter = progress.element.querySelector('progress.splide__progress__bar');
|
|
|
|
progress.start(800);
|
|
env.raf.step(0);
|
|
env.raf.step(400);
|
|
assert.strictEqual(meter.value, 50, 'halfway through should yield a 50% reading');
|
|
assert.strictEqual(meter.dataset.state, 'active');
|
|
|
|
progress.stop();
|
|
assert.strictEqual(meter.dataset.state, 'paused');
|
|
const pausedValue = meter.value;
|
|
|
|
env.raf.step(600);
|
|
assert.strictEqual(meter.value, pausedValue, 'progress should not advance after stop');
|
|
assert.strictEqual(onComplete.callCount, 0, 'stop should cancel pending completion');
|
|
|
|
progress.setOnComplete(onComplete);
|
|
progress.start(0);
|
|
assert.strictEqual(onComplete.callCount, 1, 'zero duration should fire completion immediately');
|
|
assert.strictEqual(meter.value, 100);
|
|
assert.strictEqual(meter.dataset.state, 'complete');
|
|
|
|
progress.destroy();
|
|
assert.strictEqual(container.children.length, 0, 'destroy should remove wrapper from DOM');
|
|
|
|
assert.strictEqual(readDynamicStyleRules(env.document), '', 'no dynamic style rules should be injected');
|
|
assert.strictEqual(env.document.getElementById('bitvid-style-system'), null);
|
|
assertNoInlineStyleAttributes([container], 'progress container');
|
|
});
|
|
});
|