source: Klonkt/test/mixtape-ui.test.js@ 4fbe2c1

main
Last change on this file since 4fbe2c1 was 4fbe2c1, checked in by Robin <roboburr@…>, 3 weeks ago

Een bandje loopt niet rond: het stopt aan het eind, en aan het begin

Robins eis (22-8). Drie omlopen zaten in de weg, en de stilste was de keten:
na het laatste nummer hing die nummer een er weer achter, en omdat de wachtrij
EEN doorlopende tijdlijn is merk je dat niet eens als een trackwissel -- de
band gaat gewoon door alsof er meer op stond.

  • De keten haakt in bandmodus niets meer aan voorbij het laatste nummer.
  • next() pauzeert daar in plaats van om te lopen. ended roept next() aan, dus dit is wat de band echt laat stoppen.
  • prev() loopt aan het begin niet naar achteren om: terugspoelen voorbij de kop levert de kop op, niet het laatste nummer.
  • Vooruit prefetchen stopt ook aan het eind.

De drie modulo's blijven staan voor een gewone wachtrij; die hoort wel te
blijven rondlopen. Daar staat een test op, anders sneuvelt dat gedrag stil.

TERUGSPOELEN IS NU ECHT SPOELEN, ook over de nummergrenzen. Op de MSE-motor
ging dat al vanzelf (de band is een tijdlijn). Op de blob-motor landde het in
de KOP van het vorige nummer, en dan kom je nooit ergens in het midden uit; nu
zet hij pendingSeek zodat je aan het EIND van dat nummer uitkomt.

EN EEN FOUT DIE IK ER ZELF MEE INTRODUCEERDE, gevonden door het te draaien en
niet door te lezen: bij een MediaSource vuurt ended pas als de stream dicht
is, en die ging alleen dicht bij een wachtrij van een. Sinds de keten aan het
eind niets meer aanhaakt bleef de band dus hangen op de laatste seconde --
teller stil, isPlaying() waar, spoelen draaiend. Gemeten: 125,6 van 125,7.
De stream sluit nu ook aan het eind van een bandje.

Spoelen stopt vanzelf bij de kop en de staart: beweegt de teller twee rondes
niet, dan is er geen band meer. Anders bleven de spoelen draaien terwijl er
niets gebeurde.

Nagemeten op dev: band loopt af -> laatste nummer, gestopt, teller op 125,7 van
125,7, spoelen stil, knop terug op Afspelen. next() op het laatste nummer
blijft staan; prev() op het eerste spoelt naar de kop.

audio-player v37, MOD_V 54.

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

  • Property mode set to 100644
File size: 11.0 KB
RevLine 
[8e1af9c]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));
[e679bce]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');
[8e1af9c]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});
[16fd4fe]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});
[e679bce]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});
[4fbe2c1]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});
Note: See TracBrowser for help on using the repository browser.