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

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

Toetsen: layout-pins eruit, overlap samengevoegd (mixtape, migrate, c2s-titel)

Dezelfde maat als bij de Berichten-toetsen (Robins besluit, 25-8): een
toets pint een beslissing, niet waar iets staat of hoe de code gespeld
is. Drie bestanden, netto drie toetsen minder en vier pins weg:

  • mixtape-ui: de bron-regex-toets op de labelmap weg (die toetste dezelfde regel als de render-toets, maar dan op de spelling van de code); de klasse-pin rond het woord Mixtape los (dat het er staat is het besluit, waar het staat is layout); de spoel-asserts weg -- de cassette-decoratie is layout, de haken van tape.js (.post-tape, data-tape-go) blijven, want dat is het contract met de module.
  • migrate-page: de twee volgorde-toetsen samengevoegd. De volgorde ZELF blijft getoetst, want daar is hij inhoud: wie de stappen verkeerd om doet breekt zijn verhuizing. De positie-assert van het stappenplan (bovenaan) is weg, het bestaan ervan blijft.
  • c2s-title: de twee lege-titel-gevallen (geen name, alleen witruimte) zijn een regel met twee kanten, nu een toets.

Bij het echt lezen bleken twee eerder verdachte toetsen juist
beslissingstoetsen: 'de teksten praten geen jargon' bewaakt Robins eigen
regel en toetst op betekenis-niveau, en 'stap 5' is daar destijds al
bewust op herschreven. Die blijven onaangeroerd, net als de tien
speler-toetsen die broncode lezen -- brosse dekking, maar de enige die
browser-JS hier heeft; schrappen kan pas als er iets voor in de plaats
komt.

Tegenbewijs per snede: het mixtape-label valt tegen de renderer van voor
8e1af9c, de volgorde valt tegen een omgedraaide pagina, en de lege titel
valt tegen een mutatie die er een verzint. Volle suite 1229 groen.

  • Property mode set to 100644
File size: 14.1 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 // Zonder de klassenaam erbij (25-8): WAAR het woord staat is layout, DAT het
91 // er staat is het besluit. De bron-regex-toets op de labelmap is weg om
92 // dezelfde reden: die toetste deze regel nog een keer, maar dan op de
93 // spelling van de code.
94 assert.match(html, />Mixtape</, 'een mixtape hoort zichzelf zo te noemen');
95 assert.doesNotMatch(html, /💿 Album/, 'en niet het jasje van een album te dragen');
96});
97
98test('het bandje is een cassette, geen albumlijst', async () => {
99 const AudioEmbedService = (await import('../src/services/AudioEmbedService.js')).default;
100 const pl = {
101 id: 'tape', title: 'De Mixtape', artist: 'robo', kind: 'mixtape', cover: '',
102 tracks: [
103 { id: 't1', title: 'Kant A', artist: 'robo', url: '/a.mp3', duration: 100, cover: '' },
104 { id: 't2', title: 'Kant B', artist: 'robo', url: '/b.mp3', duration: 44, cover: '' },
105 ],
106 };
107 const html = AudioEmbedService.embedPlaylistShortcodes('<p>[[playlist:tape]]</p>', (id) => (id === 'tape' ? pl : null));
108
109 // Alleen de haken waar tape.js zich aan vastpakt (.post-tape, data-tape-go):
110 // dat is het contract tussen markup en module. De spoelen zelf zijn decor en
111 // worden hier sinds 25-8 niet meer geteld -- hoe de cassette eruitziet is
112 // layout, dat hij bedienbaar is niet.
113 assert.match(html, /class="post-tape"/);
114 assert.match(html, /data-tape-go="back"/, 'terugspoelen');
115 assert.match(html, /data-tape-go="fwd"/, 'vooruitspoelen');
116
117 // DE KERN VAN HET IDEE: de nummers staan er als inhoud, niet als knoppen.
118 // Zodra een track een speel-url draagt kun je erop prikken, en dan is het
119 // geen bandje meer maar een lijst met een cassetteplaatje erboven.
120 const lijst = html.slice(html.indexOf('<ol class="tape-tracks"'));
121 assert.doesNotMatch(lijst, /data-pcms-track-url/, 'een aanklikbaar nummer hoort hier niet');
122 assert.doesNotMatch(lijst, /<button/, 'en een knop ook niet');
123
124 // De afspeelknop leunt wel op de bestaande speler, anders bouwen we een
125 // tweede speler naast de site-speler.
126 assert.match(html, /data-pcms-album-id="album-tape"/);
127 assert.match(html, /data-pcms-track-url="\/a\.mp3"/);
128
129 // En geen albumopmaak: dat was de hele reden voor een eigen vorm.
130 assert.doesNotMatch(html, /class="post-album"/);
131});
132
133test('de cassetteknop staat in de selectorlijst van de speler', () => {
134 // DIT IS DE TEST DIE ONTBRAK. De markup-tests hierboven waren groen terwijl
135 // op de knop drukken niets deed: audio-player.js kiest zijn knoppen met een
136 // lijst met KLASSENAMEN, niet met een regel over data-attributen. Alle juiste
137 // data-pcms-* dragen helpt dan niets.
138 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
139 const m = speler.match(/const PLAY_SELECTOR =\s*\n?\s*'([^']+)'/);
140 assert.ok(m, 'PLAY_SELECTOR moet te vinden zijn -- is hij hernoemd, dan dekt deze test niets meer');
141 assert.match(m[1], /\.tape-btn--play/,
142 'zonder deze klasse doet de afspeelknop van het bandje niets');
143});
144
145test('een bandje gaat als EEN object de speler in', () => {
146 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
147 // De stand moet vanaf het blok worden doorgegeven; zonder dit is een mixtape
148 // in de speler gewoon weer een rij nummers.
149 assert.match(speler, /asTape: album\.dataset\.pcmsAlbumKind === 'mixtape'/);
150 // En hij moet een paginawissel overleven, anders valt de speler halverwege
151 // terug op de nummertitel.
152 assert.match(speler, /queue, currentIndex, albumName, tapeMode,/, 'in de sessie opslaan');
153 assert.match(speler, /tapeMode = !!s\.tapeMode;/, 'en terugzetten');
154 // De opgeslagen tijd is de positie BINNEN het nummer, ook in bandmodus: bij
155 // herstellen begint de keten opnieuw. Slaat hij de bandteller op, dan springt
156 // een hersteld bandje naar een plek die in dat nummer niet bestaat.
157 assert.match(speler, /time: trackTijd\(\)\.cur \|\| 0,/);
158});
159
160test('spoelen gaat in seconden, niet per nummer', () => {
161 const mod = fs.readFileSync('src/assets/js/mod/tape.js', 'utf8');
162 assert.match(mod, /seekBy\(/, 'spoelen hoort over de tijdlijn te gaan');
163 // next()/prev() mag hier niet meer voorkomen als spoelgebaar: dat is een
164 // playlistknop en maakt van de cassette een lijst met een plaatje.
165 assert.doesNotMatch(mod, /p\.next\(\); else p\.prev\(\)/, 'de oude sprong per nummer');
166});
167
168test('een bandje loopt niet rond, aan geen van beide kanten', () => {
169 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
170
171 // De keten mag na het laatste nummer niet nummer een er weer achter hangen.
172 // Dat was de stilste van de drie: omdat het EEN doorlopende tijdlijn is merk
173 // je die omloop niet eens als een trackwissel, de band gaat gewoon door.
174 assert.match(speler, /if \(tapeMode && volgendeInRij > queue\.length - 1\) return;/,
175 'de keten hoort te stoppen aan het eind van de band');
176
177 // ended roept next() aan, dus zonder deze tak begint de band opnieuw.
178 assert.match(speler, /if \(tapeMode && currentIndex >= queue\.length - 1\) \{ pause\(\); return; \}/,
179 'aan het eind stoppen in plaats van omlopen');
180
181 // En terugspoelen voorbij het begin levert de kop van de band op, niet het
182 // laatste nummer.
183 assert.match(speler, /if \(tapeMode && currentIndex === 0\) \{/,
184 'aan het begin niet naar achteren omlopen');
185
186 // Alle drie de modulo's zijn nog aanwezig voor de NIET-bandmodus: een gewone
187 // playlist hoort wel te blijven rondlopen.
188 assert.equal((speler.match(/% queue\.length/g) || []).length, 3,
189 'de omloop van een gewone wachtrij mag niet gesneuveld zijn');
190});
191
192test('terugspoelen komt in het vorige nummer uit aan het EIND', () => {
193 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
194 // Zonder dit sprong terugspoelen naar de kop van het vorige nummer, en kwam
195 // je nooit ergens in het midden uit -- dat is geen terugspoelen maar
196 // terugspringen. Geldt voor de blob-motor; op MSE is de band een tijdlijn en
197 // gaat het vanzelf goed.
198 assert.match(speler, /pendingSeek = Number\.MAX_SAFE_INTEGER;/);
199});
200
201test('aan het eind van de band gaat de stream dicht', () => {
202 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
203 // ZONDER DIT HANGT DE BAND. Bij een MediaSource vuurt `ended` pas als de
204 // stream gesloten is. Sinds een bandje niet meer rondloopt haakt de keten na
205 // het laatste nummer niets meer aan, en dan bleef hij op de laatste seconde
206 // staan: teller stil, isPlaying() waar, spoelen draaiend. Gemeten op dev
207 // (22-8): 125,6 van 125,7 en daar bleef hij staan.
208 assert.match(speler, /const laatsteVanDeBand = tapeMode && qIndex >= queue\.length - 1;/);
209 assert.match(speler, /if \(\(queue\.length === 1 \|\| laatsteVanDeBand\) && ms && ms\.readyState === 'open'\)/);
210});
211
212test('de speler-UI schakelt mee in bandmodus (shaer-tmyn)', () => {
213 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
214
215 // 1. De tracklijst blijft staan maar is geen keuzelijst meer.
216 assert.match(speler, /sheetQueueList\.classList\.toggle\('is-tape', tapeMode\);\s*\n\s*if \(tapeMode\) return;/,
217 'zonder deze return kun je op een cassette alsnog naar nummer zeven springen');
218
219 // 2. Vorige/volgende worden terugspoelen/vooruitspoelen, op EEN plek geregeld.
220 assert.match(speler, /function zetKnopStanden\(\)/);
221 assert.match(speler, /'Vooruitspoelen' : 'Terugspoelen'/);
222 // en vasthouden spoelt door
223 assert.match(speler, /addEventListener\('pointerdown', \(\) => \{ if \(tapeMode\) startWind/);
224
225 // 3. De spoellus staat in de speler, niet meer in de module -- anders lopen
226 // twee kopieen van dezelfde versnelling uit elkaar.
227 assert.match(speler, /function startWind\(richting\)/);
228 const mod = fs.readFileSync('src/assets/js/mod/tape.js', 'utf8');
229 assert.doesNotMatch(mod, /VERSNELLING/, 'de versnelling hoort niet meer in de module te staan');
230 assert.match(mod, /p\.startWind\(richting\)/, 'de cassette bedient de gedeelde lus');
231});
232
233test('de bandteller komt uit de trackduren, niet uit de buffer', () => {
234 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
235 assert.match(speler, /function bandDuur\(\)/);
236 assert.match(speler, /function bandOffset\(i\)/);
237 // Een som met gaten is een verzonnen getal: ontbreekt er een duur, dan valt
238 // hij terug in plaats van te gokken.
239 assert.match(speler, /if \(d <= 0\) return null;/);
240
241 // En de duur moet mee in de wachtrij, anders kan de speler het niet weten.
242 const embed = fs.readFileSync('src/services/AudioEmbedService.js', 'utf8');
243 assert.match(embed, /duration: Number\(t\.duration\) \|\| 0,/,
244 'zonder dit las de teller de lengte van wat toevallig gebufferd was');
245});
246
247test('de balk is in bandmodus de hele band (shaer-tmyn, stap 4)', () => {
248 const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
249
250 // De omrekening ratio -> nummer + positie erin. Zonder dit seekt de balk
251 // binnen het lopende nummer, terwijl hij de hele band TOONT: een balk die
252 // belooft wat hij niet doet is erger dan geen balk.
253 assert.match(speler, /function bandPositie\(ratio\)/);
254 assert.match(speler, /const plek = tapeMode \? bandPositie\(ratio\) : null;/);
255
256 // Zit het doel in het lopende nummer, dan geen herlaadsprong -- anders hoor
257 // je een gat bij het verzetten van een paar seconden.
258 assert.match(speler, /if \(plek\.index === currentIndex\)/);
259
260 // En anders langs pendingSeek, dezelfde weg die het sessieherstel gebruikt.
261 assert.match(speler, /pendingSeek = plek\.binnen;\s*\n\s*loadTrack\(plek\.index, !audio\.paused\);/,
262 'de speelstand hoort mee te gaan: springen mag niet stiekem starten of stoppen');
263
264 // De niet-bandmodus mag hier niets van merken.
265 assert.match(speler, /if \(useMse\(\) && chain\.length\) \{\s*\n\s*\/\/ Seek within the CURRENT track/,
266 'het oude gedrag hoort te blijven staan voor een gewone wachtrij');
267});
Note: See TracBrowser for help on using the repository browser.