source: Klonkt/test/mixtape-ui.test.js@ e679bce

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

Het bandje speelt als bandje: een cassette, een object, spoelen in seconden

Robins correctie (21-8): vooruit en achteruit deden hier next()/prev(), en dat
is een playlistgebaar. Dan is het een lijst met een cassetteplaatje erboven.
Een cassette kent geen "volgend nummer" -- je houdt de knop ingedrukt, de band
loopt door, en je laat los waar je bent.

Dat kan omdat audio-player.js een wachtrij al in EEN doorlopende
MediaSource-tijdlijn giet: "een trackwissel is een positie, geen omschakeling".
Een bandje IS die tijdlijn. Dus:

  • seekBy(seconden) op de speler, over de nummergrenzen heen. Op de blob-motor (iOS, geen MSE) bestaat die tijdlijn niet; daar loopt spoelen tot de rand van het nummer en stapt dan naar de buur. Grover, maar eerlijker dan doen alsof.
  • Bandmodus: de speler toont het BANDJE als titel met het lopende nummer eronder, en de teller loopt over de hele band in plaats van per nummer. Dat is wat "een object, geen losse tracks" betekent op het scherm.
  • Vasthouden versnelt (4x tot 16x), loslaten stopt -- ook als je buiten de knop loslaat. Een korte tik spoelt vijf seconden, want vasthouden is met een spatiebalk geen gebaar.

Een echte cassette, als SVG. Verhouding 100,4 x 63,8 mm, de maat van een
compact cassette, dus viewBox 314x200: behuizing, labelvlak met de titel erop,
venster, twee spoelen met tandjes die meedraaien, de bandpakketten, vier
schroefjes en de openingen voor de kop en de capstans. De vorige versie waren
twee CSS-schijfjes.

Twee dingen die alleen door het echt te draaien boven kwamen:

  • audio-player.js kiest zijn knoppen met een lijst KLASSENAMEN (PLAY_SELECTOR), niet met een regel over data-attributen. De cassetteknop droeg alle juiste data-pcms-* en deed niets. Er staat nu een test op.
  • De sessie sloeg displayTimes().cur op als trackpositie. In bandmodus is dat de bandteller, dus een hersteld bandje sprong naar een plek die in dat nummer niet bestaat. Daarvoor is trackTijd() afgesplitst.

MOD_V 53, style.css v107, audio-player v35.

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

  • Property mode set to 100644
File size: 8.6 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});
Note: See TracBrowser for help on using the repository browser.