281 lines
13 KiB
JavaScript
281 lines
13 KiB
JavaScript
'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`);
|