diff --git a/public/js/audio-player.js b/public/js/audio-player.js index 9f69af7..e0f898d 100644 --- a/public/js/audio-player.js +++ b/public/js/audio-player.js @@ -7,6 +7,7 @@ function mediaUrl(url) { if (!url) return ''; + if (url.startsWith('data:') || url.startsWith('blob:')) return url; const separator = url.includes('?') ? '&' : '?'; return `${url}${separator}v=${Date.now()}`; } @@ -23,7 +24,6 @@ load(url) { unload(); if (!url) return; - const audio = element(); audio.src = mediaUrl(url); $(audio).removeAttr('hidden').css('display', 'block'); diff --git a/public/js/slideshow.js b/public/js/slideshow.js index 588ae68..cd4b2bf 100644 --- a/public/js/slideshow.js +++ b/public/js/slideshow.js @@ -7,6 +7,7 @@ function mediaUrl(url) { if (!url) return ''; + if (url.startsWith('data:') || url.startsWith('blob:')) return url; const separator = url.includes('?') ? '&' : '?'; return `${url}${separator}v=${Date.now()}`; } diff --git a/test/local-media-display.test.js b/test/local-media-display.test.js new file mode 100644 index 0000000..265a3db --- /dev/null +++ b/test/local-media-display.test.js @@ -0,0 +1,128 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import test from 'node:test'; +import vm from 'node:vm'; + +const root = path.resolve(import.meta.dirname, '..'); + +function createHarness() { + const document = {}; + const elements = new Map(); + + function makeElement(id) { + if (elements.has(id)) return elements.get(id); + const element = { + id, + attrs: {}, + css: {}, + hidden: false, + textValue: '', + toggled: null, + paused: false, + loadCount: 0, + pause() { this.paused = true; }, + load() { this.loadCount += 1; }, + play() { return Promise.resolve(); }, + removeAttribute(name) { + delete this.attrs[name]; + if (name === 'src') delete this.src; + }, + getAttribute(name) { + if (name === 'src') return this.src ?? this.attrs.src ?? null; + return this.attrs[name] ?? null; + } + }; + elements.set(id, element); + return element; + } + + class Wrapper { + constructor(target) { + this.target = target; + this[0] = target; + this.length = target ? 1 : 0; + } + + on() { return this; } + off() { return this; } + button() { return this; } + hide() { if (this.target) this.target.hidden = true; return this; } + removeAttr(name) { + this.target?.removeAttribute?.(name); + if (this.target && name === 'hidden') this.target.hidden = false; + return this; + } + attr(name, value) { + if (!this.target) return this; + if (value === undefined) return this.target.attrs?.[name]; + this.target.attrs[name] = String(value); + if (name === 'src') this.target.src = String(value); + if (name === 'hidden') this.target.hidden = true; + return this; + } + css(name, value) { if (this.target) this.target.css[name] = value; return this; } + toggle(value) { if (this.target) this.target.toggled = Boolean(value); return this; } + text(value) { if (this.target) this.target.textValue = String(value); return this; } + prop(name, value) { if (this.target) this.target[name] = value; return this; } + hasClass() { return false; } + closest() { return { length: 0 }; } + } + + function jquery(target) { + if (target === document) return new Wrapper(document); + if (typeof target === 'string' && target.startsWith('#')) return new Wrapper(makeElement(target.slice(1))); + return new Wrapper(target); + } + + const context = { + Date, + Math, + Promise, + document, + globalThis: null, + jQuery: jquery, + Wegwichtel: {} + }; + context.globalThis = context; + return { context, elements }; +} + +async function loadModule(relativePath, harness) { + const sourcePath = path.join(root, relativePath); + const source = await fs.readFile(sourcePath, 'utf8'); + vm.runInNewContext(source, harness.context, { filename: sourcePath }); +} + +test('audio player preserves local data/blob URLs and cache-busts server URLs', async () => { + const harness = createHarness(); + await loadModule('public/js/audio-player.js', harness); + + const dataUrl = 'data:audio/mpeg;base64,QUJDRA=='; + harness.context.Wegwichtel.AudioPlayer.load(dataUrl); + assert.equal(harness.elements.get('poi-audio').src, dataUrl); + + const blobUrl = 'blob:https://wegwichtel.de/1234'; + harness.context.Wegwichtel.AudioPlayer.load(blobUrl); + assert.equal(harness.elements.get('poi-audio').src, blobUrl); + + harness.context.Wegwichtel.AudioPlayer.load('/api/routes/1/pois/2/audio'); + assert.match(harness.elements.get('poi-audio').src, /^\/api\/routes\/1\/pois\/2\/audio\?v=\d+$/); +}); + +test('slideshow preserves local data/blob URLs and cache-busts server URLs', async () => { + const harness = createHarness(); + await loadModule('public/js/slideshow.js', harness); + + const dataUrl = 'data:image/jpeg;base64,QUJDRA=='; + harness.context.Wegwichtel.Slideshow.show([{ url: dataUrl, caption: 'Lokal' }]); + assert.equal(harness.elements.get('slide-image').src, dataUrl); + assert.equal(harness.elements.get('slideshow').toggled, true); + + const blobUrl = 'blob:https://wegwichtel.de/5678'; + harness.context.Wegwichtel.Slideshow.show([{ url: blobUrl }]); + assert.equal(harness.elements.get('slide-image').src, blobUrl); + + harness.context.Wegwichtel.Slideshow.show([{ url: '/api/routes/1/pois/2/pictures/3' }]); + assert.match(harness.elements.get('slide-image').src, /^\/api\/routes\/1\/pois\/2\/pictures\/3\?v=\d+$/); +});