mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
"Default Content" is persisted by the device route, snapshotted and restored by the settings layer,
offered in the device form in five languages — and read by nothing. Grep the whole tree and it
appears only in those places, the schema, and this checklist. It is absent from assemblePayload,
from every socket payload, and from all four players.
Counting it as "content assigned" therefore told the operator their screen was set up while the
screen itself went on showing "waiting for content" — the checklist confirming the one thing it
exists to confirm, incorrectly. It now counts only a playlist or a layout, both of which really do
put something on a display.
An existing test asserted the opposite ("any of the three ways of assigning counts"). It encoded the
same false premise, so it is replaced by one that pins the corrected behaviour along with the
evidence for it. The column and the form field are left alone — whether to implement or remove the
feature is a product decision, and this change only stops the checklist making a claim on its
behalf.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
113 lines
5.6 KiB
JavaScript
113 lines
5.6 KiB
JavaScript
'use strict';
|
|
|
|
// A user reported not knowing how to get content onto a screen. There WAS onboarding — a modal
|
|
// wizard — but it is gated on a localStorage flag: skip it once and it never returns, and it
|
|
// never knew whether you actually succeeded at anything. Someone who closed it was left with no
|
|
// thread to pull.
|
|
//
|
|
// The replacement reads the account's real state instead of a flag. That is the property worth
|
|
// pinning: it must not congratulate someone who has not finished, must not nag someone who has,
|
|
// and must point at the FIRST thing that is actually possible rather than the first thing missing.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
const { pathToFileURL } = require('node:url');
|
|
|
|
// The component and the i18n module it imports both read localStorage — that is not incidental,
|
|
// it is how the dismissal persists. Give them a real one rather than stubbing the behaviour out,
|
|
// so the dismissal tests below exercise the actual mechanism.
|
|
const store = new Map();
|
|
globalThis.localStorage = {
|
|
getItem: (k) => (store.has(k) ? store.get(k) : null),
|
|
setItem: (k, v) => store.set(k, String(v)),
|
|
removeItem: (k) => store.delete(k),
|
|
clear: () => store.clear(),
|
|
};
|
|
globalThis.navigator = globalThis.navigator || { language: 'en' };
|
|
|
|
const MOD = pathToFileURL(path.join(__dirname, '..', '..', 'frontend', 'js', 'components', 'getting-started.js')).href;
|
|
let GS;
|
|
test('load', async () => {
|
|
GS = await import(MOD);
|
|
assert.ok(typeof GS.computeSteps === 'function');
|
|
});
|
|
|
|
const withDevice = [{ id: 'd1' }];
|
|
const assignedDevice = [{ id: 'd1', playlist_id: 'p1' }];
|
|
|
|
test('an empty account is at step one, and step one is the screen', async () => {
|
|
const s = GS.computeSteps({ devices: [], content: [], playlists: [] });
|
|
assert.equal(s.doneCount, 0);
|
|
assert.equal(s.complete, false);
|
|
assert.equal(s.steps[s.nextIndex].key, 'device', 'nothing else is possible until a screen exists');
|
|
});
|
|
|
|
test('progress reflects what is really there, not what was clicked through', async () => {
|
|
const s = GS.computeSteps({ devices: withDevice, content: [{ id: 'c1' }], playlists: [] });
|
|
assert.equal(s.doneCount, 2);
|
|
assert.equal(s.steps[s.nextIndex].key, 'playlist');
|
|
});
|
|
|
|
test('THE POINT: it is only finished when something is actually ON a screen', async () => {
|
|
// Creating a playlist and walking away is the exact failure the report described — plenty of
|
|
// objects, nothing playing. That must NOT read as complete.
|
|
const almost = GS.computeSteps({ devices: withDevice, content: [{ id: 'c1' }], playlists: [{ id: 'p1' }] });
|
|
assert.equal(almost.complete, false, 'objects exist but no screen is showing anything');
|
|
assert.equal(almost.steps[almost.nextIndex].key, 'assign');
|
|
|
|
const done = GS.computeSteps({ devices: assignedDevice, content: [{ id: 'c1' }], playlists: [{ id: 'p1' }] });
|
|
assert.equal(done.complete, true);
|
|
assert.equal(done.nextIndex, -1);
|
|
});
|
|
|
|
test('assigning a playlist or a layout counts', async () => {
|
|
for (const d of [{ id: 'x', playlist_id: 'p' }, { id: 'x', layout_id: 'l' }]) {
|
|
const s = GS.computeSteps({ devices: [d], content: [{ id: 'c' }], playlists: [{ id: 'p' }] });
|
|
assert.equal(s.complete, true, `${Object.keys(d).join(',')} should count as assigned`);
|
|
}
|
|
});
|
|
|
|
test('default_content_id does NOT count, because no player reads it', async () => {
|
|
// This test previously asserted the opposite, and it was wrong. Grep the whole tree and
|
|
// default_content appears only in this checklist, the device form, the settings snapshot, the
|
|
// schema and the devices route — never in a socket payload, in assemblePayload, or in any of the
|
|
// four players. Setting it changes nothing on the screen, so counting it told the operator
|
|
// "content assigned" while their display went on showing "waiting for content". A checklist that
|
|
// lies about the one thing it exists to confirm is worse than no checklist.
|
|
const s = GS.computeSteps({ devices: [{ id: 'x', default_content_id: 'c' }], content: [{ id: 'c' }], playlists: [{ id: 'p' }] });
|
|
assert.equal(s.complete, false, 'a screen with only default_content is not actually showing anything');
|
|
});
|
|
|
|
test('steps stay in dependency order — never sent somewhere unusable', async () => {
|
|
// Content before a screen exists is not wrong, but it cannot be PUT anywhere, so the next
|
|
// action must remain the screen.
|
|
const s = GS.computeSteps({ devices: [], content: [{ id: 'c1' }], playlists: [{ id: 'p1' }] });
|
|
assert.equal(s.steps[s.nextIndex].key, 'device');
|
|
});
|
|
|
|
test('it shows while there is work left and hides once finished', async () => {
|
|
GS.undismiss();
|
|
assert.equal(GS.shouldShow(GS.computeSteps({ devices: [], content: [], playlists: [] })), true);
|
|
assert.equal(GS.shouldShow(GS.computeSteps({ devices: assignedDevice, content: [{ id: 'c' }], playlists: [{ id: 'p' }] })), false,
|
|
'a finished account is never nagged');
|
|
});
|
|
|
|
test('dismissing sticks — it is guidance, not a demand', async () => {
|
|
GS.undismiss();
|
|
const empty = GS.computeSteps({ devices: [], content: [], playlists: [] });
|
|
assert.equal(GS.shouldShow(empty), true);
|
|
GS.dismiss();
|
|
assert.equal(GS.shouldShow(empty), false, 'stays hidden even with everything still to do');
|
|
GS.undismiss();
|
|
assert.equal(GS.shouldShow(empty), true, 'and can be brought back');
|
|
});
|
|
|
|
test('every step offers a way to act on it', async () => {
|
|
const s = GS.computeSteps({ devices: [], content: [], playlists: [] });
|
|
for (const step of s.steps) {
|
|
assert.ok(step.title && step.desc && step.cta, `${step.key} is explained`);
|
|
assert.ok(step.href, `${step.key} goes somewhere`);
|
|
}
|
|
});
|