'use strict'; /** * Unit tests for the PCO Plan module's document matching — placing what * ProPresenter has live onto a plan item, and the message rule. * * node modules/pco-plan/matching.test.js * * Plain Node, no dependencies; exits non-zero on the first failure. The two * plans are the campuses' real 27 Sept 2026 plans and ProPresenter playlists * as they stood on 22 Sept, so the expectations read like a Sunday would. */ const assert = require('assert'); const { MESSAGE_THRESHOLD, leadingDateCode, messageDocumentScore, messageItemOf, playlistMirrorsPlan, placeDocument, } = require('./matching'); let passed = 0; function test(name, fn) { try { fn(); passed += 1; console.log(` ok ${name}`); } catch (err) { console.log(` FAIL ${name}\n ${err.message}`); process.exitCode = 1; throw err; } } const SUNDAY = new Date(2026, 8, 27, 9, 30).getTime(); // 27 Sept 2026, 9:30 local — the plan's sort date const PLAN = { id: '91274523', title: 'Part 6', seriesTitle: 'Where is Your Faith?', sortDate: SUNDAY }; const header = (id, title) => ({ id, itemType: 'header', title, song: null }); const item = (id, title, itemType = 'item', song = null) => ({ id, itemType, title, song: song ? { title: song } : null }); const pp = (name, type = 'presentation', isPco = true) => ({ name, type, isPco, isHidden: false }); // North Attleboro — the message is an item called "Message", linked in // ProPresenter to the bumper video; the sermon slides are their own document. const NA = [ header('h0', 'Doors Open'), item('1', 'Preservice'), item('2', '5 Minute Countdown', 'media'), item('3', 'MC Announcements', 'media'), header('h4', 'Worship'), item('5', 'Call To Worship', 'media'), item('6', 'Alleluia', 'song', 'Alleluia'), item('7', 'What a God', 'song', 'What a God'), item('8', 'Baptisms', 'media'), item('9', 'Promised Land', 'song', 'Promised Land'), item('10', 'Be Praised', 'song', 'Be Praised'), header('h11', 'Message'), item('12', 'Message'), item('13', 'Standing Salvation Moment', 'media'), header('h14', 'Closing'), item('15', 'Postservice', 'song', '-Postservice'), ]; const NA_PP = [ pp('Doors Open', 'header'), pp('Preservice'), pp('5 Minute Countdown'), pp('MC Announcements'), pp('Worship', 'header'), pp('Call To Worship'), pp('Alleluia - [ Full ]'), pp('What a God - [ Full ]'), pp('Baptisms'), pp('Promised Land - [ Full ]'), pp('Be Praised - [ Full ]'), pp('Message', 'header'), pp('Message Bumper'), pp('Standing Salvation Moment'), pp('Closing', 'header'), pp('Postservice'), ]; // Apollo Beach — the message is the media item "Resi Message". const AB = [ item('0', 'Pre Service Meeting', 'media'), header('h1', 'Doors Open'), item('2', 'Pre Service', 'media'), item('3', '5 Minute Countdown', 'media'), item('4', 'MC Announcements', 'media'), header('h5', 'Worship'), item('6', 'Call To Worship', 'media'), item('7', 'Alleluia', 'song', 'Alleluia'), item('8', 'What a God', 'song', 'What a God'), item('9', 'The Blood', 'song', 'The Blood'), item('10', 'Be Praised (feat. Kymberli Joye)', 'song', 'Be Praised'), header('h11', 'Message'), item('12', 'Resi Message', 'media'), item('13', 'Standing Salvation Moment', 'media'), header('h14', 'Closing'), item('15', 'Post Service', 'media'), ]; const SERMON = '260927 Where is Your Faith? - Part 6'; const place = (name, items, extra = {}) => placeDocument(name, { items, plan: PLAN, currentItemId: '', ...extra }); const titleOf = (hit) => (hit ? hit.item.title : null); console.log('date codes'); test('YYMMDD and YYYYMMDD lead a document name; anything else is not a date', () => { assert.deepStrictEqual(leadingDateCode(SERMON), { y: 2026, m: 9, d: 27 }); assert.deepStrictEqual(leadingDateCode('20260927 Where is Your Faith? - Part 6'), { y: 2026, m: 9, d: 27 }); assert.strictEqual(leadingDateCode('9-27-26 Where is Your Faith?'), null); assert.strictEqual(leadingDateCode('1234567 seven digits'), null); assert.strictEqual(leadingDateCode('261340 no such month'), null); assert.strictEqual(leadingDateCode('Alleluia'), null); assert.strictEqual(leadingDateCode(''), null); }); console.log('the message rule'); test('a document named like the plan, dated with its day, is the message', () => { const hit = messageDocumentScore(SERMON, PLAN); assert.ok(hit, 'recognised'); assert.strictEqual(hit.dated, true); assert.strictEqual(hit.score, 1); }); test("last week's document is not this plan's message", () => { assert.strictEqual(messageDocumentScore('260920 Where is Your Faith? - Part 5', PLAN), null); assert.strictEqual(messageDocumentScore('260920 Where is Your Faith? - Part 6', PLAN), null, 'even with the right part: the date names another day'); }); test('undated, series and part together still read as the message', () => { const plain = messageDocumentScore('Where is Your Faith? - Part 6', PLAN); assert.ok(plain && plain.dated === false && plain.score === 1); const tagged = messageDocumentScore('Where Is Your Faith - Part 6 - [ Notes ]', PLAN); assert.ok(tagged && tagged.score >= MESSAGE_THRESHOLD, 'tags and punctuation do not matter'); }); test('a series bumper is not the sermon', () => { assert.strictEqual(messageDocumentScore('Where is Your Faith?', PLAN), null, 'the series alone, undated'); assert.strictEqual(messageDocumentScore('Where is Your Faith? Bumper', PLAN), null); }); test("dated with the plan's day, the part alone is enough", () => { const hit = messageDocumentScore('260927 Part 6', PLAN); assert.ok(hit && hit.dated); }); test('a dated document about something else is not the message', () => { assert.strictEqual(messageDocumentScore('260927 Announcements', PLAN), null); assert.strictEqual(messageDocumentScore('260927 Baptism Video', PLAN), null); }); test('songs are not the message', () => { assert.strictEqual(messageDocumentScore('What a God - [ Full ]', PLAN), null); assert.strictEqual(messageDocumentScore('Alleluia', PLAN), null); assert.strictEqual(messageDocumentScore('Message Bumper', PLAN), null); }); test('a plan with neither series nor title has no message document', () => { assert.strictEqual(messageDocumentScore(SERMON, { sortDate: SUNDAY }), null); assert.strictEqual(messageDocumentScore(SERMON, null), null); }); test('a plan without a date still recognises its own name', () => { const hit = messageDocumentScore(SERMON, { title: 'Part 6', seriesTitle: 'Where is Your Faith?' }); assert.ok(hit && hit.dated === false && hit.score === 1); }); console.log('the message item'); test('an item called Message is the message item; so is Resi Message', () => { assert.strictEqual(messageItemOf(NA).id, '12'); assert.strictEqual(messageItemOf(AB).id, '12'); }); test('a bare "Message" beats an item that only contains the word', () => { const withBumper = [...NA.slice(0, 12), item('12a', 'Message Bumper', 'media'), ...NA.slice(12)]; assert.strictEqual(messageItemOf(withBumper).id, '12'); assert.strictEqual(messageItemOf([item('a', 'Message Bumper', 'media')]).id, 'a', 'alone, it is the slot'); }); test('under a Sermon header, the first item is the message whatever it is called', () => { const plan = [header('h', 'Worship'), item('1', 'Alleluia', 'song', 'Alleluia'), header('s', 'Sermon'), item('2', 'Living Hope'), item('3', 'Response')]; assert.strictEqual(messageItemOf(plan).id, '2'); }); test('a plan without a message slot has none', () => { assert.strictEqual(messageItemOf([header('h', 'Worship'), item('1', 'Alleluia', 'song', 'Alleluia')]), null); assert.strictEqual(messageItemOf([header('m', 'Message')]), null, 'a header with nothing under it'); assert.strictEqual(messageItemOf([]), null); assert.strictEqual(messageItemOf(null), null); }); console.log('the playlist mirror'); test("ProPresenter's Planning Center playlist mirrors the plan, headers and all", () => { assert.strictEqual(playlistMirrorsPlan(NA_PP, NA), true); }); test('an item the operator added, a header out of place or a non-PCO item breaks the mirror', () => { const extra = [...NA_PP.slice(0, 10), pp('The Blood TAG'), ...NA_PP.slice(10)]; assert.strictEqual(playlistMirrorsPlan(extra, NA), false, 'a song tag shifts everything after it'); const shifted = [...NA_PP]; [shifted[3], shifted[4]] = [shifted[4], shifted[3]]; assert.strictEqual(playlistMirrorsPlan(shifted, NA), false); const local = NA_PP.map((x, i) => (i === 8 ? pp('Baptisms', 'presentation', false) : x)); assert.strictEqual(playlistMirrorsPlan(local, NA), false); assert.strictEqual(playlistMirrorsPlan([], []), false); assert.strictEqual(playlistMirrorsPlan(null, NA), false); }); console.log('placing what is live'); test('songs and media land by name, tags and all', () => { const god = place('What a God - [ Full ]', NA); assert.strictEqual(titleOf(god), 'What a God'); assert.strictEqual(god.via, 'name'); assert.strictEqual(titleOf(place('Resi Message - [ MEDIA ]', AB)), 'Resi Message'); assert.strictEqual(titleOf(place('Greatly To Be Praised - [ Full ]', AB)), 'Be Praised (feat. Kymberli Joye)', 'by the song title'); assert.strictEqual(titleOf(place('Pre Service - [ Sunday ]', AB)), 'Pre Service'); }); test('the sermon document lands on the message item at both campuses', () => { const na = place(SERMON, NA); assert.strictEqual(titleOf(na), 'Message'); assert.strictEqual(na.via, 'message document, dated'); assert.strictEqual(titleOf(place(SERMON, AB)), 'Resi Message'); assert.strictEqual(place('Where is Your Faith? - Part 6', NA).via, 'message document', 'undated still lands, and says so'); }); test('the bumper lands on the message item too, by name', () => { const hit = place('Message Bumper', NA); assert.strictEqual(titleOf(hit), 'Message'); assert.strictEqual(hit.via, 'name'); }); test('a song called Faith does not catch the sermon', () => { const trap = [...NA.slice(0, 11), item('f', 'Faith', 'song', 'Faith'), ...NA.slice(11)]; assert.strictEqual(titleOf(place('Faith - [ Full ]', trap)), 'Faith', 'the song itself still lands'); assert.strictEqual(titleOf(place(SERMON, trap)), 'Message', 'dated: the message wins outright'); assert.strictEqual(titleOf(place('Where is Your Faith? - Part 6', trap)), 'Message', 'undated: it reads better than the song'); }); test('an item titled exactly like the document wins over the message slot', () => { const titled = [...NA.slice(0, 12), item('s', 'Where is Your Faith? - Part 6'), ...NA.slice(12)]; assert.strictEqual(place(SERMON, titled).item.id, 's'); }); test('a plan without a message slot leaves the sermon document unplaced', () => { const noSlot = NA.filter((it) => it.id !== '12' && it.id !== 'h11'); assert.strictEqual(place(SERMON, noSlot), null); }); test("an unplanned document, or last week's sermon, lands nowhere", () => { assert.strictEqual(place('Offering Slide', NA), null); assert.strictEqual(place('260920 Where is Your Faith? - Part 5', NA), null); }); test('nothing to place on an empty plan', () => { assert.strictEqual(place(SERMON, []), null); assert.strictEqual(place(SERMON, [header('h', 'Message')]), null); }); // The same song twice: a reprise after "What a God". const REPRISE = [...NA.slice(0, 8), item('6b', 'Alleluia', 'song', 'Alleluia'), ...NA.slice(8)]; const REPRISE_PP = [...NA_PP.slice(0, 8), pp('Alleluia - [ Reprise ]'), ...NA_PP.slice(8)]; test('a tie between equal names goes to the one ahead of the current item', () => { assert.strictEqual(place('Alleluia - [ Full ]', REPRISE).item.id, '6', 'nothing current yet: the first'); assert.strictEqual(place('Alleluia - [ Full ]', REPRISE, { currentItemId: '7' }).item.id, '6b', 'after What a God: the reprise'); assert.strictEqual(place('Alleluia - [ Full ]', REPRISE, { currentItemId: '6b' }).item.id, '6b', 'the current one keeps a tie'); }); test('position breaks a tie only while the playlist mirrors the plan', () => { const mirror = playlistMirrorsPlan(REPRISE_PP, REPRISE); assert.strictEqual(mirror, true); assert.strictEqual(place('Alleluia - [ Reprise ]', REPRISE, { hint: { index: 8, mirror } }).item.id, '6b'); assert.strictEqual(place('Alleluia - [ Full ]', REPRISE, { hint: { index: 6, mirror } }).item.id, '6'); assert.strictEqual(place('Alleluia - [ Full ]', REPRISE, { currentItemId: '7', hint: { index: 6, mirror } }).item.id, '6', 'position wins the tie over "ahead"'); assert.strictEqual(place('Alleluia - [ Reprise ]', REPRISE, { hint: { index: 8, mirror: false } }).item.id, '6', 'no mirror: back to "ahead"'); }); test('position never places a document on its own', () => { const mirror = playlistMirrorsPlan(NA_PP, NA); assert.strictEqual(place('Offering Slide', NA, { hint: { index: 8, mirror } }), null); assert.strictEqual(titleOf(place('What a God - [ Full ]', NA, { hint: { index: 12, mirror } })), 'What a God', 'a wrong position cannot move a clear name'); }); console.log(`\n${passed} tests passed`);