Files
MBN_STOCK_WEBVIEW/tests/Web/named-playlist-bridge-integration.test.cjs

94 lines
5.2 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const workflow = require("../../Web/named-playlist-workflow.js");
const repositoryRoot = path.resolve(__dirname, "..", "..");
const mainWindow = fs.readFileSync(path.join(repositoryRoot, "MainWindow.xaml.cs"), "utf8");
const namedBridge = fs.readFileSync(path.join(repositoryRoot, "MainWindow.NamedPlaylists.cs"), "utf8");
const pagePlanBridge = fs.readFileSync(path.join(repositoryRoot, "MainWindow.PlaylistPagePlans.cs"), "utf8");
const appScript = fs.readFileSync(path.join(repositoryRoot, "Web", "app.js"), "utf8");
test("native switch and JavaScript contract use the same five requests and four responses", () => {
for (const requestType of Object.values(workflow.bridgeContract.requests)) {
assert.match(mainWindow, new RegExp(`case "${requestType}"`));
}
for (const responseType of Object.values(workflow.bridgeContract.responses)) {
assert.match(namedBridge, new RegExp(`"${responseType}"`));
}
assert.match(mainWindow, /new LegacyNamedPlaylistPersistenceService\(/);
assert.match(mainWindow, /new OracleNamedPlaylistMutationExecutor\(/);
});
test("named reads use latest-request cancellation and the shared playout-priority database gate", () => {
assert.match(namedBridge,
/Interlocked\.Exchange\(\s*ref _namedPlaylistReadCancellation,\s*requestCancellation\)/s);
assert.match(namedBridge, /CancelRequest\(previousCancellation\)/);
assert.ok((namedBridge.match(/await _databaseActivityGate\.WaitAsync\(requestToken\)/g) || []).length >= 2);
assert.ok((namedBridge.match(/Volatile\.Read\(ref _playoutCommandInFlight\) != 0/g) || []).length >= 4);
assert.match(mainWindow, /CancelRequest\(Volatile\.Read\(ref _namedPlaylistReadCancellation\)\)/);
assert.match(mainWindow, /CancelRequest\(Volatile\.Read\(ref _namedPlaylistWriteCancellation\)\)/);
});
test("writes are single-flight, never superseded, and permanently quarantine OutcomeUnknown", () => {
assert.match(namedBridge, /_namedPlaylistWriteGate\.WaitAsync\(0\)/);
assert.match(namedBridge, /catch \(NamedPlaylistMutationException exception\)/);
assert.match(namedBridge, /if \(exception\.OutcomeUnknown\)/);
assert.match(namedBridge,
/Interlocked\.CompareExchange\(ref _namedPlaylistWriteQuarantined, 1, 0\)/);
assert.match(namedBridge, /IsNamedPlaylistWriteQuarantined\(\)/);
assert.match(namedBridge, /do not retry|was not retried/i);
assert.doesNotMatch(namedBridge, /for\s*\([^)]*retry|while\s*\([^)]*retry/i);
});
test("browser correlation loss cancels active requests and latches an in-flight write", () => {
assert.match(mainWindow,
/private void QuarantineIfBrowserCorrelationCanBeLost\(\)\s*\{\s*InvalidateNamedPlaylistBrowserRequests\(\)/s);
assert.match(namedBridge, /Interlocked\.Increment\(ref _namedPlaylistBrowserGeneration\)/);
assert.match(namedBridge, /Volatile\.Read\(ref _namedPlaylistWriteInFlight\) != 0/);
assert.match(namedBridge,
/Interlocked\.Exchange\(\s*ref _namedPlaylistWriteCancellation,\s*null\)/s);
});
test("native replacement accepts only the exact eight raw-row properties and 1000 rows", () => {
for (const property of [
"itemIndex", "enabled", "groupCode", "subject",
"graphicType", "subtype", "pageText", "dataCode"
]) {
assert.match(namedBridge, new RegExp(`"${property}"`));
}
assert.match(namedBridge,
/itemsElement\.GetArrayLength\(\) > LegacyNamedPlaylistPersistenceService\.MaximumItems/);
assert.match(namedBridge, /itemIndex != expectedIndex/);
assert.match(namedBridge, /value\.Contains\('\^'\)/);
assert.match(namedBridge,
/return string\.Equals\(page\.ToString\(\), value, StringComparison\.Ordinal\)/);
});
test("page preflight uses one correlated read-only bridge with playout priority", () => {
assert.match(mainWindow, /case "request-playlist-page-plans"/);
assert.match(mainWindow, /CancelRequest\(Volatile\.Read\(ref _playlistPagePlanCancellation\)\)/);
assert.match(pagePlanBridge,
/Interlocked\.Exchange\(\s*ref _playlistPagePlanCancellation,\s*requestCancellation\)/s);
assert.match(pagePlanBridge, /await _databaseActivityGate\.WaitAsync\(requestToken\)/);
assert.ok((pagePlanBridge.match(/Volatile\.Read\(ref _playoutCommandInFlight\) != 0/g) || []).length >= 2);
assert.match(pagePlanBridge, /PreflightPagePlansAsync\(/);
assert.match(pagePlanBridge, /PostMessage\("playlist-page-plans-results"/);
assert.match(pagePlanBridge, /PostMessage\("playlist-page-plans-error"/);
assert.doesNotMatch(pagePlanBridge, /PrepareAsync\(|PlayAsync\(|ClearAsync\(|UnloadAsync\(/);
});
test("browser page-plan handling requires exact correlation and exposes explicit retry", () => {
assert.match(appScript, /normalizePagePlanResponse\(payload, pending\.request\)/);
assert.match(appScript, /payload\?\.requestId !== pending\.requestId/);
assert.match(appScript, /applyPagePlanResponse\(/);
assert.match(appScript, /page-result-empty/);
assert.match(appScript, /namedPlaylistPageRetryButton/);
assert.match(appScript, /case "playlist-page-plans-results"/);
assert.match(appScript, /case "playlist-page-plans-error"/);
assert.doesNotMatch(appScript, /markPageRecalculated\(/);
});