| 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 | // 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 |
|
|---|
| 98 | test('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 |
|
|---|
| 133 | test('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 |
|
|---|
| 145 | test('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 |
|
|---|
| 160 | test('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 |
|
|---|
| 168 | test('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 |
|
|---|
| 192 | test('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 |
|
|---|
| 201 | test('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 |
|
|---|
| 212 | test('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 |
|
|---|
| 233 | test('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 |
|
|---|
| 247 | test('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 | });
|
|---|