source: Klonkt/test/mixtape-ui.test.js@ 5e72413

main
Last change on this file since 5e72413 was 5e72413, checked in by Robin <roboburr@…>, 2 weeks ago

Seek over de hele band (shaer-tmyn, stap 4 en laatste)

De balk TOONDE al de hele band -- de teller rekent sinds c0e463b met de
trackduren -- maar aanklikken seekte binnen het lopende nummer. En de keten
houdt het lopende nummer plus een vooruit, dus op een band van twaalf nummers
reikte die balk in werkelijkheid tot nummer twee. Een balk die belooft wat hij
niet doet is erger dan geen balk.

bandPositie(ratio) rekent de verhouding om naar nummer + positie daarin, met
dezelfde duren als de teller. Ligt het doel in het lopende nummer, dan wordt de
positie gewoon verzet: geen herlaadsprong voor een paar seconden. Ligt het
verderop, dan zet hij pendingSeek en laadt dat nummer -- exact de weg die het
sessieherstel al gebruikt -- met de speelstand mee, zodat springen niet stiekem
start of stopt.

Er valt een hoorbaar gat bij zo'n sprong. Dat hoort bij spoelen naar een plek
die niet in de buffer zit, en het is eerlijker dan een balk die daar nooit komt.

DEZELFDE OMREKENING IS OOK DE ENIGE DIE OP iOS KAN: de blob-motor heeft geen
doorlopende tijdlijn, dus daar bestaat "seek over de keten" niet eens. Nu delen
beide motoren een model in plaats van twee.

Nagemeten op dev, klikken op vijf plekken van de balk (band van 373 s,
nummers van 103, 144 en 126 s):

10% -> 39 s, nummer 1 50% -> 189 s, nummer 2
25% -> 95 s, nummer 1 85% -> 319 s, nummer 3
95% -> 356 s, nummer 3

Steeds twee seconden voor op de verwachting, en dat is het doorspelen tijdens
het meten zelf.

audio-player v39.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 14.0 KB
Line 
1// De mixtape als POSTTYPE: kiesbaar, opslaanbaar, en zichzelf noemend.
2//
3// De waarschuwing staat al in config/post-types.js en is hier het hele punt:
4// kent de opslag een type niet, dan wordt de post ZONDER MELDING een gewone
5// post. De keuze verdwijnt dan in plaats van geweigerd te worden, en niemand
6// ziet het tot een post van soort blijkt te zijn veranderd.
7import { test } from 'node:test';
8import assert from 'node:assert/strict';
9import fs from 'node:fs';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://test.example';
13const dbMod = await import('../src/config/database.js');
14const db = dbMod.default;
15dbMod.initializeDatabase();
16
17const { KEUZE_TYPES, POST_TYPES, MUZIEK_TYPES } = await import('../src/config/post-types.js');
18
19test('mixtape is een kiesbaar type, en draagt muziek', () => {
20 assert.ok(KEUZE_TYPES.includes('mixtape'), 'anders staat de knop er niet');
21 assert.ok(POST_TYPES.has('mixtape'), 'anders gooit het opslaan hem stil weg');
22 assert.ok(MUZIEK_TYPES.has('mixtape'), 'anders deelt hij het muziekpaneel niet');
23});
24
25test('de composer toont de knop en deelt het muziekpaneel', () => {
26 const ejsBron = fs.readFileSync('src/views/pages/post-edit.ejs', 'utf8');
27 assert.match(ejsBron, /mixtape: '📼'/, 'een eigen teken, niet dat van een album');
28 assert.match(ejsBron, /data-panel="album playlist mixtape audio"/,
29 'zonder mixtape in deze lijst verdwijnt het uploadpaneel zodra je het type kiest');
30});
31
32test('het type mag meeveranderen met de muziek', () => {
33 const mod = fs.readFileSync('src/assets/js/mod/post-edit.js', 'utf8');
34 assert.match(mod, /VOLGBAAR = new Set\(\[[^\]]*'mixtape'/,
35 'anders blijft het type op post staan als je een mixtape invoegt');
36 // De opzoeker van de editor moet dezelfde soorten kennen als de server, anders
37 // ziet het scherm album terwijl de server mixtape opslaat.
38 assert.doesNotMatch(mod, /kindVan\.set\(p\.id, p\.kind === 'playlist'/,
39 'de binaire vorm hoort weg te zijn');
40});
41
42test('een post van type mixtape houdt zijn type bij het opslaan', async () => {
43 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
44 .run('u1', 'baas', 'b@t.nl', 'x', 'god');
45 db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)')
46 .run('s1', 'robo', 'Soundfabrics', 'u1');
47
48 const express = (await import('express')).default;
49 const router = (await import('../src/routes/posts.js')).default;
50 const app = express();
51 app.use(express.urlencoded({ extended: true }));
52 app.use((req, res, next) => {
53 req.session = { user: { id: 'u1', role: 'god', username: 'baas' } };
54 res.locals.site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('robo');
55 res.locals.siteUrlBase = '';
56 next();
57 });
58 app.use('/', router);
59 const server = app.listen(0);
60 server.unref();
61
62 const r = await fetch(`http://127.0.0.1:${server.address().port}/posts/create`, {
63 method: 'POST', redirect: 'manual',
64 headers: { 'content-type': 'application/x-www-form-urlencoded' },
65 body: new URLSearchParams({
66 title: 'De Mixtape', content: '<p>kant A</p>', type: 'mixtape', status: 'published',
67 }).toString(),
68 signal: AbortSignal.timeout(5000),
69 }).catch((e) => assert.fail(`de route antwoordde niet (${e.name})`));
70 assert.ok(r.status === 302 || r.status === 200, 'opslaan mag niet stranden, kreeg ' + r.status);
71
72 const post = db.prepare("SELECT type FROM posts WHERE title = ?").get('De Mixtape');
73 assert.ok(post, 'de post hoort te bestaan');
74 assert.equal(post.type, 'mixtape', 'het type mag niet stil terugvallen op post');
75 server.close();
76});
77
78test('een ingesloten mixtape noemt zich mixtape en geen album', async () => {
79 const AudioEmbedService = (await import('../src/services/AudioEmbedService.js')).default;
80 const pl = {
81 id: 'tape', title: 'De Mixtape', artist: 'robo', kind: 'mixtape',
82 tracks: [{ id: 't1', title: 'Kant A', artist: 'robo', url: '/a.mp3', duration: 100, cover: '' }],
83 };
84 // Door de ECHTE weg: de shortcode in de tekst, uitgeklapt zoals bij het tonen
85 // van een post. De vorige versie van deze test riep een methode aan die niet
86 // bestaat en sloeg zichzelf dan over -- groen zonder iets te meten.
87 const html = AudioEmbedService.embedPlaylistShortcodes('<p>[[playlist:tape]]</p>', (id) => (id === 'tape' ? pl : null));
88 // Het teken zat eerst in de tekst; nu staat er een echte cassette getekend en
89 // is het woord genoeg. De eis blijft dezelfde: hij noemt zichzelf goed.
90 assert.match(html, /class="tape-kind">Mixtape</, 'een mixtape hoort zichzelf zo te noemen');
91 assert.doesNotMatch(html, /💿 Album/, 'en niet het jasje van een album te dragen');
92});
93
94test('de renderer kiest zijn label niet meer met een tweewegkeuze', () => {
95 const bron = fs.readFileSync('src/services/AudioEmbedService.js', 'utf8');
96 assert.doesNotMatch(bron, /kind === 'playlist' \? '📃 Playlist' : '💿 Album'/,
97 'die vorm gaf een mixtape het jasje van een album');
98 assert.match(bron, /mixtape: '📼 Mixtape'/);
99});
100
101test('het bandje is een cassette, geen albumlijst', async () => {
102 const AudioEmbedService = (await import('../src/services/AudioEmbedService.js')).default;
103 const pl = {
104 id: 'tape', title: 'De Mixtape', artist: 'robo', kind: 'mixtape', cover: '',
105 tracks: [
106 { id: 't1', title: 'Kant A', artist: 'robo', url: '/a.mp3', duration: 100, cover: '' },
107 { id: 't2', title: 'Kant B', artist: 'robo', url: '/b.mp3', duration: 44, cover: '' },
108 ],
109 };
110 const html = AudioEmbedService.embedPlaylistShortcodes('<p>[[playlist:tape]]</p>', (id) => (id === 'tape' ? pl : null));
111
112 assert.match(html, /class="post-tape"/);
113 assert.match(html, /tape-reel--left/, 'twee spoelen, anders is het geen cassette');
114 assert.match(html, /tape-reel--right/);
115 assert.match(html, /data-tape-go="back"/, 'terugspoelen');
116 assert.match(html, /data-tape-go="fwd"/, 'vooruitspoelen');
117
118 // DE KERN VAN HET IDEE: de nummers staan er als inhoud, niet als knoppen.
119 // Zodra een track een speel-url draagt kun je erop prikken, en dan is het
120 // geen bandje meer maar een lijst met een cassetteplaatje erboven.
121 const lijst = html.slice(html.indexOf('<ol class="tape-tracks"'));
122 assert.doesNotMatch(lijst, /data-pcms-track-url/, 'een aanklikbaar nummer hoort hier niet');
123 assert.doesNotMatch(lijst, /<button/, 'en een knop ook niet');
124
125 // De afspeelknop leunt wel op de bestaande speler, anders bouwen we een
126 // tweede speler naast de site-speler.
127 assert.match(html, /data-pcms-album-id="album-tape"/);
128 assert.match(html, /data-pcms-track-url="\/a\.mp3"/);
129
130 // En geen albumopmaak: dat was de hele reden voor een eigen vorm.
131 assert.doesNotMatch(html, /class="post-album"/);
132});
133
134test('de cassetteknop staat in de selectorlijst van de speler', () => {
135 // DIT IS DE TEST DIE ONTBRAK. De markup-tests hierboven waren groen terwijl
136 // op de knop drukken niets deed: audio-player.js kiest zijn knoppen met een
137 // lijst met KLASSENAMEN, niet met een regel over data-attributen. Alle juiste
138 // data-pcms-* dragen helpt dan niets.
139 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
140 const m = speler.match(/const PLAY_SELECTOR =\s*\n?\s*'([^']+)'/);
141 assert.ok(m, 'PLAY_SELECTOR moet te vinden zijn -- is hij hernoemd, dan dekt deze test niets meer');
142 assert.match(m[1], /\.tape-btn--play/,
143 'zonder deze klasse doet de afspeelknop van het bandje niets');
144});
145
146test('een bandje gaat als EEN object de speler in', () => {
147 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
148 // De stand moet vanaf het blok worden doorgegeven; zonder dit is een mixtape
149 // in de speler gewoon weer een rij nummers.
150 assert.match(speler, /asTape: album\.dataset\.pcmsAlbumKind === 'mixtape'/);
151 // En hij moet een paginawissel overleven, anders valt de speler halverwege
152 // terug op de nummertitel.
153 assert.match(speler, /queue, currentIndex, albumName, tapeMode,/, 'in de sessie opslaan');
154 assert.match(speler, /tapeMode = !!s\.tapeMode;/, 'en terugzetten');
155 // De opgeslagen tijd is de positie BINNEN het nummer, ook in bandmodus: bij
156 // herstellen begint de keten opnieuw. Slaat hij de bandteller op, dan springt
157 // een hersteld bandje naar een plek die in dat nummer niet bestaat.
158 assert.match(speler, /time: trackTijd\(\)\.cur \|\| 0,/);
159});
160
161test('spoelen gaat in seconden, niet per nummer', () => {
162 const mod = fs.readFileSync('src/assets/js/mod/tape.js', 'utf8');
163 assert.match(mod, /seekBy\(/, 'spoelen hoort over de tijdlijn te gaan');
164 // next()/prev() mag hier niet meer voorkomen als spoelgebaar: dat is een
165 // playlistknop en maakt van de cassette een lijst met een plaatje.
166 assert.doesNotMatch(mod, /p\.next\(\); else p\.prev\(\)/, 'de oude sprong per nummer');
167});
168
169test('een bandje loopt niet rond, aan geen van beide kanten', () => {
170 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
171
172 // De keten mag na het laatste nummer niet nummer een er weer achter hangen.
173 // Dat was de stilste van de drie: omdat het EEN doorlopende tijdlijn is merk
174 // je die omloop niet eens als een trackwissel, de band gaat gewoon door.
175 assert.match(speler, /if \(tapeMode && volgendeInRij > queue\.length - 1\) return;/,
176 'de keten hoort te stoppen aan het eind van de band');
177
178 // ended roept next() aan, dus zonder deze tak begint de band opnieuw.
179 assert.match(speler, /if \(tapeMode && currentIndex >= queue\.length - 1\) \{ pause\(\); return; \}/,
180 'aan het eind stoppen in plaats van omlopen');
181
182 // En terugspoelen voorbij het begin levert de kop van de band op, niet het
183 // laatste nummer.
184 assert.match(speler, /if \(tapeMode && currentIndex === 0\) \{/,
185 'aan het begin niet naar achteren omlopen');
186
187 // Alle drie de modulo's zijn nog aanwezig voor de NIET-bandmodus: een gewone
188 // playlist hoort wel te blijven rondlopen.
189 assert.equal((speler.match(/% queue\.length/g) || []).length, 3,
190 'de omloop van een gewone wachtrij mag niet gesneuveld zijn');
191});
192
193test('terugspoelen komt in het vorige nummer uit aan het EIND', () => {
194 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
195 // Zonder dit sprong terugspoelen naar de kop van het vorige nummer, en kwam
196 // je nooit ergens in het midden uit -- dat is geen terugspoelen maar
197 // terugspringen. Geldt voor de blob-motor; op MSE is de band een tijdlijn en
198 // gaat het vanzelf goed.
199 assert.match(speler, /pendingSeek = Number\.MAX_SAFE_INTEGER;/);
200});
201
202test('aan het eind van de band gaat de stream dicht', () => {
203 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
204 // ZONDER DIT HANGT DE BAND. Bij een MediaSource vuurt `ended` pas als de
205 // stream gesloten is. Sinds een bandje niet meer rondloopt haakt de keten na
206 // het laatste nummer niets meer aan, en dan bleef hij op de laatste seconde
207 // staan: teller stil, isPlaying() waar, spoelen draaiend. Gemeten op dev
208 // (22-8): 125,6 van 125,7 en daar bleef hij staan.
209 assert.match(speler, /const laatsteVanDeBand = tapeMode && qIndex >= queue\.length - 1;/);
210 assert.match(speler, /if \(\(queue\.length === 1 \|\| laatsteVanDeBand\) && ms && ms\.readyState === 'open'\)/);
211});
212
213test('de speler-UI schakelt mee in bandmodus (shaer-tmyn)', () => {
214 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
215
216 // 1. De tracklijst blijft staan maar is geen keuzelijst meer.
217 assert.match(speler, /sheetQueueList\.classList\.toggle\('is-tape', tapeMode\);\s*\n\s*if \(tapeMode\) return;/,
218 'zonder deze return kun je op een cassette alsnog naar nummer zeven springen');
219
220 // 2. Vorige/volgende worden terugspoelen/vooruitspoelen, op EEN plek geregeld.
221 assert.match(speler, /function zetKnopStanden\(\)/);
222 assert.match(speler, /'Vooruitspoelen' : 'Terugspoelen'/);
223 // en vasthouden spoelt door
224 assert.match(speler, /addEventListener\('pointerdown', \(\) => \{ if \(tapeMode\) startWind/);
225
226 // 3. De spoellus staat in de speler, niet meer in de module -- anders lopen
227 // twee kopieen van dezelfde versnelling uit elkaar.
228 assert.match(speler, /function startWind\(richting\)/);
229 const mod = fs.readFileSync('src/assets/js/mod/tape.js', 'utf8');
230 assert.doesNotMatch(mod, /VERSNELLING/, 'de versnelling hoort niet meer in de module te staan');
231 assert.match(mod, /p\.startWind\(richting\)/, 'de cassette bedient de gedeelde lus');
232});
233
234test('de bandteller komt uit de trackduren, niet uit de buffer', () => {
235 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
236 assert.match(speler, /function bandDuur\(\)/);
237 assert.match(speler, /function bandOffset\(i\)/);
238 // Een som met gaten is een verzonnen getal: ontbreekt er een duur, dan valt
239 // hij terug in plaats van te gokken.
240 assert.match(speler, /if \(d <= 0\) return null;/);
241
242 // En de duur moet mee in de wachtrij, anders kan de speler het niet weten.
243 const embed = fs.readFileSync('src/services/AudioEmbedService.js', 'utf8');
244 assert.match(embed, /duration: Number\(t\.duration\) \|\| 0,/,
245 'zonder dit las de teller de lengte van wat toevallig gebufferd was');
246});
247
248test('de balk is in bandmodus de hele band (shaer-tmyn, stap 4)', () => {
249 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
250
251 // De omrekening ratio -> nummer + positie erin. Zonder dit seekt de balk
252 // binnen het lopende nummer, terwijl hij de hele band TOONT: een balk die
253 // belooft wat hij niet doet is erger dan geen balk.
254 assert.match(speler, /function bandPositie\(ratio\)/);
255 assert.match(speler, /const plek = tapeMode \? bandPositie\(ratio\) : null;/);
256
257 // Zit het doel in het lopende nummer, dan geen herlaadsprong -- anders hoor
258 // je een gat bij het verzetten van een paar seconden.
259 assert.match(speler, /if \(plek\.index === currentIndex\)/);
260
261 // En anders langs pendingSeek, dezelfde weg die het sessieherstel gebruikt.
262 assert.match(speler, /pendingSeek = plek\.binnen;\s*\n\s*loadTrack\(plek\.index, !audio\.paused\);/,
263 'de speelstand hoort mee te gaan: springen mag niet stiekem starten of stoppen');
264
265 // De niet-bandmodus mag hier niets van merken.
266 assert.match(speler, /if \(useMse\(\) && chain\.length\) \{\s*\n\s*\/\/ Seek within the CURRENT track/,
267 'het oude gedrag hoort te blijven staan voor een gewone wachtrij');
268});
Note: See TracBrowser for help on using the repository browser.