115 lines
5.6 KiB
JavaScript
115 lines
5.6 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const test = require("node:test");
|
|
|
|
const repositoryRoot = path.resolve(__dirname, "..", "..");
|
|
const webRoot = path.join(
|
|
repositoryRoot,
|
|
"src",
|
|
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
|
"Web");
|
|
const markup = fs.readFileSync(path.join(webRoot, "index.html"), "utf8");
|
|
const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8");
|
|
const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
|
|
const playout = fs.readFileSync(path.join(webRoot, "playout-ui.js"), "utf8");
|
|
|
|
function luminance(hex) {
|
|
const channels = [1, 3, 5].map(index =>
|
|
Number.parseInt(hex.slice(index, index + 2), 16) / 255);
|
|
const linear = channels.map(channel => channel <= 0.04045
|
|
? channel / 12.92
|
|
: ((channel + 0.055) / 1.055) ** 2.4);
|
|
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
|
|
}
|
|
|
|
function contrast(left, right) {
|
|
const bright = Math.max(luminance(left), luminance(right));
|
|
const dark = Math.min(luminance(left), luminance(right));
|
|
return (bright + 0.05) / (dark + 0.05);
|
|
}
|
|
|
|
test("operator inputs retain a visible three-to-one boundary", () => {
|
|
const match = styles.match(/--ui-border-strong:\s*(#[0-9a-f]{6})/i);
|
|
assert.ok(match, "strong control border token must exist");
|
|
assert.ok(contrast(match[1], "#ffffff") >= 3,
|
|
`control border contrast was ${contrast(match[1], "#ffffff").toFixed(2)}:1`);
|
|
});
|
|
|
|
test("playlist exposes independent candidate, selection, and PGM states", () => {
|
|
assert.match(app, /element\.dataset\.active = isCandidate \? "true" : "false"/);
|
|
assert.match(app, /"aria-selected",\s*presentation\.isSelected \? "true" : "false"/s);
|
|
assert.match(app, /if \(presentation\.isOnAir\) accessibleStates\.push\("현재 PGM"\)/);
|
|
assert.match(app, /if \(isCandidate\) accessibleStates\.push\("송출 후보"\)/);
|
|
assert.match(app, /if \(presentation\.isSelected\) accessibleStates\.push\("삭제 선택됨"\)/);
|
|
|
|
for (const selector of [
|
|
'.playlist-data-row[data-active="true"]:not(.on-air)',
|
|
'.playlist-data-row[aria-selected="true"]:not(.on-air)',
|
|
'.playlist-data-row[data-active="true"][aria-selected="true"]:not(.on-air)',
|
|
'.playlist-data-row.on-air',
|
|
'.playlist-data-row.on-air[data-active="true"]',
|
|
'.playlist-data-row.on-air[aria-selected="true"]',
|
|
'.playlist-data-row.on-air[data-active="true"][aria-selected="true"]'
|
|
]) {
|
|
assert.ok(styles.includes(selector), `${selector} must have an explicit visual state`);
|
|
}
|
|
assert.match(styles, /\.playlist-data-row\.on-air\s*\{[^}]*background:\s*#fdc8d2/i);
|
|
});
|
|
|
|
test("keyboard focus, high contrast, and scroll affordances remain visible", () => {
|
|
assert.match(styles,
|
|
/\.playlist-data-row:focus-visible[\s\S]*?outline:\s*3px solid var\(--ui-accent\)/);
|
|
assert.match(styles, /@media \(forced-colors:\s*active\)/);
|
|
assert.match(styles,
|
|
/\.playlist-data-row\[data-active="true"\]\s*\{\s*border-top:\s*3px solid Highlight/);
|
|
assert.match(styles,
|
|
/\.playlist-data-row\[aria-selected="true"\]\s*\{\s*border-right:\s*3px solid Highlight/);
|
|
assert.match(styles, /\.playlist-data-row\.on-air\s*\{\s*border-left:\s*4px double Highlight/);
|
|
assert.match(styles, /scrollbar-width:\s*auto/);
|
|
assert.match(styles, /scrollbar-gutter:\s*stable/);
|
|
assert.match(styles, /scroll-padding-top:\s*29px/);
|
|
});
|
|
|
|
test("playout controls publish their real keyboard shortcuts", () => {
|
|
assert.match(markup,
|
|
/id="playout-take-in"[^>]*aria-label="TAKE IN"[^>]*aria-keyshortcuts="F8"/s);
|
|
assert.match(markup,
|
|
/id="playout-take-out"[^>]*aria-label="TAKE OUT"[^>]*aria-keyshortcuts="Escape"/s);
|
|
assert.match(markup, /id="playout-next"[^>]*aria-label="NEXT"/s);
|
|
});
|
|
|
|
test("dry-run mode reads as information and clipped status keeps its full tooltip", () => {
|
|
assert.match(playout,
|
|
/connection\.classList\.toggle\("dry-run", playout\.mode === "dryRun"\)/);
|
|
assert.match(styles, /\.connection-state\.dry-run:not\(\.outcome-unknown\)\s*\{/);
|
|
assert.match(playout, /status\.title = status\.textContent/);
|
|
});
|
|
|
|
test("workspace header uses a modern semantic lockup without changing native status text", () => {
|
|
assert.match(markup,
|
|
/class="brand"[^>]*role="img"[^>]*aria-label="매일경제TV VRi"[\s\S]*?class="brand-symbol"[\s\S]*?<svg/);
|
|
assert.match(markup,
|
|
/class="workspace-heading"[\s\S]*?class="workspace-heading-icon"[\s\S]*?<div class="workspace-heading-copy">[\s\S]*?<small>현재 작업<\/small>[\s\S]*?id="workspace-title"/);
|
|
assert.match(markup,
|
|
/class="playout-connection-summary"[\s\S]*?<small>송출 연결<\/small>[\s\S]*?<strong>Tornado2<\/strong>[\s\S]*?id="playout-connection-state"/);
|
|
assert.match(markup,
|
|
/id="playout-connection-state"[^>]*role="status"[^>]*aria-label="Tornado2 연결 상태"[^>]*aria-live="polite"[^>]*aria-atomic="true">미연결<\/div>/);
|
|
assert.match(styles,
|
|
/\.workspace-header\s*\{[^}]*radial-gradient[\s\S]*?linear-gradient/);
|
|
assert.match(styles, /\.workspace-header::before\s*\{/);
|
|
assert.match(styles, /\.playlist-heading::before\s*\{/);
|
|
assert.match(styles, /\.connection-state::before\s*\{/);
|
|
assert.match(styles,
|
|
/\.playout-connection-copy small\s*\{[^}]*color:\s*var\(--ui-text-muted\)/);
|
|
assert.match(styles, /\.connection-state\.connected::before\s*\{/);
|
|
assert.match(styles,
|
|
/\.connection-state\.dry-run:not\(\.outcome-unknown\)::before\s*\{/);
|
|
assert.match(styles,
|
|
/\.workspace-surface\s*\{[^}]*grid-template-rows:\s*66px minmax\(0, 1fr\)/);
|
|
assert.match(playout,
|
|
/const nextConnectionText = connectionLabel\(playout\);[\s\S]*?if \(connection\.textContent !== nextConnectionText\)[\s\S]*?connection\.textContent = nextConnectionText/);
|
|
});
|