219 lines
5.7 KiB
JavaScript
219 lines
5.7 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const {
|
|
ELIGIBILITY_REASONS,
|
|
evaluateEntryEligibility,
|
|
resolveEntrySettings,
|
|
} = require("../src/lifecycle");
|
|
|
|
const NOW = Date.parse("2035-05-06T07:08:09.000Z");
|
|
|
|
function readyAsset(overrides = {}) {
|
|
return { status: "ready", expiration_at: null, ...overrides };
|
|
}
|
|
|
|
function enabledEntry(overrides = {}) {
|
|
return {
|
|
enabled: 1,
|
|
schedule_start_at: null,
|
|
schedule_end_at: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("an enabled, ready, permanent, unscheduled entry is playable", () => {
|
|
assert.deepEqual(
|
|
evaluateEntryEligibility(enabledEntry(), readyAsset(), NOW),
|
|
{ playable: true, reason: null },
|
|
);
|
|
});
|
|
|
|
test("disabled entries are not playable", () => {
|
|
assert.deepEqual(
|
|
evaluateEntryEligibility(enabledEntry({ enabled: 0 }), readyAsset(), NOW),
|
|
{ playable: false, reason: ELIGIBILITY_REASONS.DISABLED },
|
|
);
|
|
});
|
|
|
|
test("schedule start is inclusive", () => {
|
|
const entry = enabledEntry({ schedule_start_at: NOW });
|
|
|
|
assert.deepEqual(evaluateEntryEligibility(entry, readyAsset(), NOW - 1), {
|
|
playable: false,
|
|
reason: ELIGIBILITY_REASONS.SCHEDULED_FUTURE,
|
|
});
|
|
assert.deepEqual(evaluateEntryEligibility(entry, readyAsset(), NOW), {
|
|
playable: true,
|
|
reason: null,
|
|
});
|
|
});
|
|
|
|
test("schedule end is exclusive", () => {
|
|
const entry = enabledEntry({ schedule_end_at: NOW });
|
|
|
|
assert.deepEqual(evaluateEntryEligibility(entry, readyAsset(), NOW - 1), {
|
|
playable: true,
|
|
reason: null,
|
|
});
|
|
assert.deepEqual(evaluateEntryEligibility(entry, readyAsset(), NOW), {
|
|
playable: false,
|
|
reason: ELIGIBILITY_REASONS.SCHEDULED_ENDED,
|
|
});
|
|
});
|
|
|
|
test("start-only, end-only, and bounded schedules use UTC epoch milliseconds", () => {
|
|
assert.equal(
|
|
evaluateEntryEligibility(
|
|
enabledEntry({ schedule_start_at: NOW - 1 }),
|
|
readyAsset(),
|
|
NOW,
|
|
).playable,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
evaluateEntryEligibility(
|
|
enabledEntry({ schedule_end_at: NOW + 1 }),
|
|
readyAsset(),
|
|
NOW,
|
|
).playable,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
evaluateEntryEligibility(
|
|
enabledEntry({ schedule_start_at: NOW, schedule_end_at: NOW + 1 }),
|
|
readyAsset(),
|
|
NOW,
|
|
).playable,
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("only ready assets are playable", () => {
|
|
for (const status of ["processing", "failed", "deleting"]) {
|
|
assert.deepEqual(
|
|
evaluateEntryEligibility(enabledEntry(), readyAsset({ status }), NOW),
|
|
{ playable: false, reason: ELIGIBILITY_REASONS.ASSET_NOT_READY },
|
|
);
|
|
}
|
|
});
|
|
|
|
test("permanent assets stay playable and expiration is exclusive", () => {
|
|
assert.equal(
|
|
evaluateEntryEligibility(enabledEntry(), readyAsset(), NOW).playable,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
evaluateEntryEligibility(
|
|
enabledEntry(),
|
|
readyAsset({ expiration_at: NOW + 1 }),
|
|
NOW,
|
|
).playable,
|
|
true,
|
|
);
|
|
assert.deepEqual(
|
|
evaluateEntryEligibility(
|
|
enabledEntry(),
|
|
readyAsset({ expiration_at: NOW }),
|
|
NOW,
|
|
),
|
|
{ playable: false, reason: ELIGIBILITY_REASONS.ASSET_EXPIRED },
|
|
);
|
|
});
|
|
|
|
test("not-playable reasons follow the documented precedence", () => {
|
|
const asset = readyAsset({ status: "failed", expiration_at: NOW });
|
|
|
|
assert.equal(
|
|
evaluateEntryEligibility(
|
|
enabledEntry({ enabled: 0, schedule_start_at: NOW + 1 }),
|
|
asset,
|
|
NOW,
|
|
).reason,
|
|
ELIGIBILITY_REASONS.DISABLED,
|
|
);
|
|
assert.equal(
|
|
evaluateEntryEligibility(
|
|
enabledEntry({ schedule_start_at: NOW + 1, schedule_end_at: NOW }),
|
|
asset,
|
|
NOW,
|
|
).reason,
|
|
ELIGIBILITY_REASONS.SCHEDULED_FUTURE,
|
|
);
|
|
assert.equal(
|
|
evaluateEntryEligibility(enabledEntry({ schedule_end_at: NOW }), asset, NOW)
|
|
.reason,
|
|
ELIGIBILITY_REASONS.SCHEDULED_ENDED,
|
|
);
|
|
assert.equal(
|
|
evaluateEntryEligibility(enabledEntry(), asset, NOW).reason,
|
|
ELIGIBILITY_REASONS.ASSET_NOT_READY,
|
|
);
|
|
});
|
|
|
|
test("mapped camel-case domain values are supported", () => {
|
|
assert.deepEqual(
|
|
evaluateEntryEligibility(
|
|
{ enabled: true, scheduleStartAt: NOW, scheduleEndAt: NOW + 1 },
|
|
{ status: "ready", expirationAt: NOW + 1 },
|
|
NOW,
|
|
),
|
|
{ playable: true, reason: null },
|
|
);
|
|
});
|
|
|
|
test("eligibility rejects invalid evaluation clocks", () => {
|
|
for (const invalid of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1, "2035-01-01"]) {
|
|
assert.throws(
|
|
() => evaluateEntryEligibility(enabledEntry(), readyAsset(), invalid),
|
|
/nowMs must be a non-negative, safe epoch-millisecond integer/,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("resolved settings inherit global duration and presentation", () => {
|
|
const settings = {
|
|
default_duration_ms: 7_000,
|
|
default_background_mode: "blurred",
|
|
default_solid_background_color: "#123456",
|
|
default_background_darkened: 1,
|
|
default_blur_px: 20,
|
|
};
|
|
|
|
assert.deepEqual(resolveEntrySettings({}, settings), {
|
|
durationMs: 7_000,
|
|
presentation: {
|
|
backgroundMode: "blurred",
|
|
backgroundColor: "#123456",
|
|
backgroundDarkened: true,
|
|
blurPx: 20,
|
|
},
|
|
});
|
|
});
|
|
|
|
test("entry overrides are independent and blur always remains global", () => {
|
|
const settings = {
|
|
defaultDurationMs: 7_000,
|
|
defaultBackgroundMode: "blurred",
|
|
defaultSolidBackgroundColor: "#123456",
|
|
defaultBackgroundDarkened: true,
|
|
defaultBlurPx: 20,
|
|
};
|
|
const entry = {
|
|
durationOverrideMs: 9_000,
|
|
backgroundModeOverride: "solid",
|
|
backgroundColorOverride: "#abcdef",
|
|
backgroundDarkenedOverride: false,
|
|
blurPx: 999,
|
|
};
|
|
|
|
assert.deepEqual(resolveEntrySettings(entry, settings), {
|
|
durationMs: 9_000,
|
|
presentation: {
|
|
backgroundMode: "solid",
|
|
backgroundColor: "#abcdef",
|
|
backgroundDarkened: false,
|
|
blurPx: 20,
|
|
},
|
|
});
|
|
});
|