source: Klonkt/test/mixtape-ui.test.js@ 16fd4fe

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

De cassettespeler: draaiende spoelen, vooruit en achteruit

Een album toont een genummerde lijst waar je in prikt. Een cassette is het
tegenovergestelde: je spoelt, en je hoort wat er komt in de volgorde waarin het
is opgenomen. Vandaar een eigen vorm en niet het albumjasje met een plaatje.

DE SPELER IS DE BESTAANDE SPELER. De afspeelknop draagt exact dezelfde
data-attributen als een albumhoes, dus audio-player.js pakt hem op; spoelen is
next()/prev(). Een tweede speler ernaast bouwen zou twee dingen tegelijk laten
klinken en de sessie weggooien die een paginawissel overleeft. Daarom spoelt
het per NUMMER en niet per seconde: die speler denkt in een wachtrij.

De nummers staan er als inhoud, niet als knoppen. Geen speel-url, geen knop,
geen hover -- op een cassette kun je niet naar nummer zeven springen. Wat er
speelt lees je op de statusregel, en dat is meteen de toegankelijke kant.

Verder:

  • De pauzeknop vangt zijn klik in de CAPTURE-fase, want audio-player.js luistert in de bubbelfase en zou de wachtrij anders opnieuw vanaf een zetten.
  • Spoelen doet niets als dit bandje niet loopt; anders verzet een spoelknop de wachtrij van een ander blok.
  • init() ruimt de tikker altijd eerst op, ook zonder bandje op de pagina. Zonder dat blijft hij lopen na een paginawissel -- dezelfde fout die in de leesweergave ooit de hele site heeft vastgezet.
  • prefers-reduced-motion zet het draaien uit; de statusregel blijft.

Opmaak in dezelfde familie als .post-album: zelfde rand, radius en papier, en
alleen bestaande variabelen, dus licht, donker en het accent volgen vanzelf.

MOD_V 52, style.css v106.

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

  • Property mode set to 100644
File size: 6.4 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 assert.match(html, /📼 Mixtape/, 'een mixtape hoort zichzelf zo te noemen');
89 assert.doesNotMatch(html, /💿 Album/, 'en niet het jasje van een album te dragen');
90});
91
92test('de renderer kiest zijn label niet meer met een tweewegkeuze', () => {
93 const bron = fs.readFileSync('src/services/AudioEmbedService.js', 'utf8');
94 assert.doesNotMatch(bron, /kind === 'playlist' \? '📃 Playlist' : '💿 Album'/,
95 'die vorm gaf een mixtape het jasje van een album');
96 assert.match(bron, /mixtape: '📼 Mixtape'/);
97});
98
99test('het bandje is een cassette, geen albumlijst', async () => {
100 const AudioEmbedService = (await import('../src/services/AudioEmbedService.js')).default;
101 const pl = {
102 id: 'tape', title: 'De Mixtape', artist: 'robo', kind: 'mixtape', cover: '',
103 tracks: [
104 { id: 't1', title: 'Kant A', artist: 'robo', url: '/a.mp3', duration: 100, cover: '' },
105 { id: 't2', title: 'Kant B', artist: 'robo', url: '/b.mp3', duration: 44, cover: '' },
106 ],
107 };
108 const html = AudioEmbedService.embedPlaylistShortcodes('<p>[[playlist:tape]]</p>', (id) => (id === 'tape' ? pl : null));
109
110 assert.match(html, /class="post-tape"/);
111 assert.match(html, /tape-reel--left/, 'twee spoelen, anders is het geen cassette');
112 assert.match(html, /tape-reel--right/);
113 assert.match(html, /data-tape-go="back"/, 'terugspoelen');
114 assert.match(html, /data-tape-go="fwd"/, 'vooruitspoelen');
115
116 // DE KERN VAN HET IDEE: de nummers staan er als inhoud, niet als knoppen.
117 // Zodra een track een speel-url draagt kun je erop prikken, en dan is het
118 // geen bandje meer maar een lijst met een cassetteplaatje erboven.
119 const lijst = html.slice(html.indexOf('<ol class="tape-tracks"'));
120 assert.doesNotMatch(lijst, /data-pcms-track-url/, 'een aanklikbaar nummer hoort hier niet');
121 assert.doesNotMatch(lijst, /<button/, 'en een knop ook niet');
122
123 // De afspeelknop leunt wel op de bestaande speler, anders bouwen we een
124 // tweede speler naast de site-speler.
125 assert.match(html, /data-pcms-album-id="album-tape"/);
126 assert.match(html, /data-pcms-track-url="\/a\.mp3"/);
127
128 // En geen albumopmaak: dat was de hele reden voor een eigen vorm.
129 assert.doesNotMatch(html, /class="post-album"/);
130});
Note: See TracBrowser for help on using the repository browser.