| 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.
|
|---|
| 7 | import { test } from 'node:test';
|
|---|
| 8 | import assert from 'node:assert/strict';
|
|---|
| 9 | import fs from 'node:fs';
|
|---|
| 10 |
|
|---|
| 11 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 12 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 13 | const dbMod = await import('../src/config/database.js');
|
|---|
| 14 | const db = dbMod.default;
|
|---|
| 15 | dbMod.initializeDatabase();
|
|---|
| 16 |
|
|---|
| 17 | const { KEUZE_TYPES, POST_TYPES, MUZIEK_TYPES } = await import('../src/config/post-types.js');
|
|---|
| 18 |
|
|---|
| 19 | test('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 |
|
|---|
| 25 | test('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 |
|
|---|
| 32 | test('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 |
|
|---|
| 42 | test('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 |
|
|---|
| 78 | test('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 |
|
|---|
| 94 | test('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 |
|
|---|
| 101 | test('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 |
|
|---|
| 134 | test('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 |
|
|---|
| 146 | test('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 |
|
|---|
| 161 | test('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 |
|
|---|
| 169 | test('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 |
|
|---|
| 193 | test('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 |
|
|---|
| 202 | test('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 |
|
|---|
| 213 | test('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 |
|
|---|
| 234 | test('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 |
|
|---|
| 248 | test('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 | });
|
|---|