import test from 'node:test'; import assert from 'node:assert/strict'; import { createMediaAdapter, ImageCarouselMediaAdapter, ImageMediaAdapter, VideoMediaAdapter } from '../vr180player/media/media-adapter.js'; function createClassList() { return { values: [], add(...values) { this.values.push(...values); } }; } function createVideo({ ended = false, paused = false, source = 'https://cdn.example.com/videos/demo-video.mp4', title = '' } = {}) { return { HAVE_METADATA: 1, classList: createClassList(), ended, loadCount: 0, paused, readyState: 0, style: { display: '' }, tagName: 'VIDEO', addEventListener() {}, getAttribute(name) { return name === 'title' ? title : ''; }, load() { this.loadCount += 1; }, querySelector(selector) { if (selector !== 'source' || !source) { return null; } return { src: source }; } }; } function createImage({ alt = '', complete = true, naturalWidth = 1920, source = 'https://cdn.example.com/images/demo-image.png', title = '' } = {}) { return { alt, classList: createClassList(), complete, currentSrc: source, listeners: {}, naturalWidth, src: source, style: { display: '' }, tagName: 'IMG', addEventListener(type, listener) { this.listeners[type] ??= []; this.listeners[type].push(listener); }, dispatch(type) { for (const listener of this.listeners[type] ?? []) { listener({ currentTarget: this }); } }, getAttribute(name) { if (name === 'title') return title; if (name === 'alt') return alt; return ''; } }; } test('VideoMediaAdapter exposes video capabilities and lifecycle helpers', () => { const video = createVideo({ title: 'Demo Title' }); const adapter = new VideoMediaAdapter(video); assert.deepEqual(adapter.capabilities, { audio: true, carousel: false, dynamicTexture: true, navigation: true, playback: true, timeline: true }); assert.equal(adapter.element, video); assert.equal(adapter.textureSource, video); assert.equal(adapter.getTitle(), 'Demo Title'); adapter.hideElement(); assert.equal(video.style.display, 'none'); adapter.showElement(); assert.equal(video.style.display, ''); adapter.load(); assert.equal(video.loadCount, 1); }); test('VideoMediaAdapter falls back to source filename and tracks texture update state', () => { const video = createVideo({ source: 'https://cdn.example.com/media/flat-sbs-demo.mp4' }); const adapter = new VideoMediaAdapter(video); assert.equal(adapter.getTitle(), 'flat sbs demo'); assert.equal(adapter.shouldUpdateTexture(), true); video.paused = true; assert.equal(adapter.shouldUpdateTexture(), false); video.paused = false; video.ended = true; assert.equal(adapter.shouldUpdateTexture(), false); }); test('ImageMediaAdapter exposes static image capabilities and lifecycle helpers', async () => { const image = createImage({ alt: 'Alt Title' }); const adapter = new ImageMediaAdapter(image); let readyCount = 0; adapter.bindLoadState({ onError: () => {}, onReady: () => { readyCount += 1; } }); await new Promise((resolve) => setImmediate(resolve)); assert.deepEqual(adapter.capabilities, { audio: false, carousel: false, dynamicTexture: false, navigation: false, playback: false, timeline: false }); assert.equal(adapter.element, image); assert.equal(adapter.textureSource, image); assert.equal(adapter.getTitle(), 'Alt Title'); assert.equal(adapter.shouldUpdateTexture(), false); assert.equal(readyCount, 1); adapter.hideElement(); assert.equal(image.style.display, 'none'); adapter.showElement(); assert.equal(image.style.display, ''); }); test('ImageMediaAdapter falls back to source filename', () => { const image = createImage({ alt: '', source: 'https://cdn.example.com/media/static-sbs-demo.png' }); const adapter = new ImageMediaAdapter(image); assert.equal(adapter.getTitle(), 'static sbs demo'); }); test('ImageCarouselMediaAdapter exposes carousel image navigation', () => { const firstImage = createImage({ title: 'First image', source: 'https://cdn.example.com/media/first.png' }); const secondImage = createImage({ title: 'Second image', source: 'https://cdn.example.com/media/second.png' }); const adapter = new ImageCarouselMediaAdapter([firstImage, secondImage]); assert.deepEqual(adapter.capabilities, { audio: false, carousel: true, dynamicTexture: false, navigation: true, playback: false, timeline: false }); assert.equal(adapter.element, firstImage); assert.equal(adapter.textureSource, firstImage); assert.equal(adapter.getTitle(), 'First image'); assert.equal(firstImage.style.display, ''); assert.equal(secondImage.style.display, 'none'); assert.deepEqual(firstImage.classList.values, ['vrwp-media', 'vrwp-image', 'vrwp-carousel-image']); assert.deepEqual(secondImage.classList.values, ['vrwp-media', 'vrwp-image', 'vrwp-carousel-image']); assert.equal(adapter.next(), true); assert.equal(adapter.element, secondImage); assert.equal(adapter.textureSource, secondImage); assert.equal(adapter.getTitle(), 'Second image'); assert.equal(firstImage.style.display, 'none'); assert.equal(secondImage.style.display, ''); assert.equal(adapter.next(), true); assert.equal(adapter.element, firstImage); adapter.hideElement(); assert.equal(firstImage.style.display, 'none'); assert.equal(secondImage.style.display, 'none'); adapter.previous(); adapter.showElement(); assert.equal(adapter.element, secondImage); assert.equal(firstImage.style.display, 'none'); assert.equal(secondImage.style.display, ''); adapter.load(); assert.equal(firstImage.loading, 'eager'); assert.equal(secondImage.loading, 'eager'); }); test('ImageCarouselMediaAdapter waits for all images before reporting ready', async () => { const firstImage = createImage({ complete: false, naturalWidth: 0 }); const secondImage = createImage({ complete: false, naturalWidth: 0 }); const adapter = new ImageCarouselMediaAdapter([firstImage, secondImage]); let readyCount = 0; adapter.bindLoadState({ onError: () => {}, onReady: () => { readyCount += 1; } }); await new Promise((resolve) => setImmediate(resolve)); assert.equal(readyCount, 0); firstImage.complete = true; firstImage.naturalWidth = 1920; firstImage.dispatch('load'); assert.equal(readyCount, 0); secondImage.complete = true; secondImage.naturalWidth = 1920; secondImage.dispatch('load'); assert.equal(readyCount, 1); firstImage.dispatch('load'); assert.equal(readyCount, 1); }); test('createMediaAdapter finds and marks the supported video element', () => { const video = createVideo(); const playerContainer = { querySelectorAll(selector) { return selector === 'video,img' ? [video] : []; } }; const adapter = createMediaAdapter(playerContainer); assert.ok(adapter instanceof VideoMediaAdapter); assert.equal(adapter.element, video); assert.deepEqual(video.classList.values, ['vrwp-media', 'vrwp-video']); }); test('createMediaAdapter finds and marks the supported image element', () => { const image = createImage(); const playerContainer = { querySelectorAll(selector) { return selector === 'video,img' ? [image] : []; } }; const adapter = createMediaAdapter(playerContainer); assert.ok(adapter instanceof ImageMediaAdapter); assert.equal(adapter.element, image); assert.deepEqual(image.classList.values, ['vrwp-media', 'vrwp-image']); }); test('createMediaAdapter creates an image carousel when requested', () => { const firstImage = createImage({ title: 'First image' }); const secondImage = createImage({ title: 'Second image' }); const playerContainer = { dataset: { carousel: '' }, querySelectorAll(selector) { return selector === 'video,img' ? [firstImage, secondImage] : []; } }; const adapter = createMediaAdapter(playerContainer); assert.ok(adapter instanceof ImageCarouselMediaAdapter); assert.equal(adapter.element, firstImage); assert.equal(adapter.next(), true); assert.equal(adapter.element, secondImage); }); test('createMediaAdapter refuses missing or ambiguous media elements', () => { const video = createVideo(); const image = createImage(); const secondImage = createImage(); assert.equal(createMediaAdapter({ querySelectorAll: () => [] }), null); assert.equal(createMediaAdapter({ querySelectorAll: () => [video, image] }), null); assert.equal(createMediaAdapter({ querySelectorAll: () => [image, secondImage] }), null); assert.equal(createMediaAdapter({ dataset: { carousel: '' }, querySelectorAll: () => [image] }), null); assert.equal(createMediaAdapter({ dataset: { carousel: '' }, querySelectorAll: () => [video, image] }), null); });