feat: add configurable operator appearance and layout
This commit is contained in:
@@ -88,22 +88,101 @@ test("dry-run mode reads as information and clipped status keeps its full toolti
|
||||
assert.match(playout, /status\.title = status\.textContent/);
|
||||
});
|
||||
|
||||
test("workspace header uses a modern semantic lockup without changing native status text", () => {
|
||||
test("appearance themes expose explicit light, dark, and Windows-following palettes", () => {
|
||||
assert.match(styles,
|
||||
/:root\[data-color-theme="light"\],[\s\S]*?body\[data-color-theme="light"\]\s*\{\s*color-scheme:\s*light/);
|
||||
assert.match(styles,
|
||||
/:root\[data-color-theme="dark"\],[\s\S]*?body\[data-color-theme="dark"\]\s*\{[\s\S]*?color-scheme:\s*dark/);
|
||||
assert.match(styles,
|
||||
/@media \(prefers-color-scheme:\s*dark\)\s*\{[\s\S]*?:root\[data-color-theme="system"\],[\s\S]*?body\[data-color-theme="system"\]/);
|
||||
assert.match(styles, /--ui-brand:\s*#dc5826/i);
|
||||
assert.match(styles, /\.brand-lockup strong\s*\{[^}]*color:\s*var\(--ui-brand\)/s);
|
||||
assert.match(styles,
|
||||
/\.playlist-state-legend \.legend-selected\s*\{\s*color:\s*var\(--ui-accent\)/);
|
||||
assert.match(styles,
|
||||
/\.playlist-state-legend \.legend-next\s*\{\s*color:\s*var\(--ui-warning\)/);
|
||||
assert.match(styles,
|
||||
/\.playlist-state-legend \.legend-on-air\s*\{\s*color:\s*var\(--ui-danger\)/);
|
||||
});
|
||||
|
||||
test("dark palette keeps text, actions, and semantic states readable", () => {
|
||||
const match = styles.match(
|
||||
/:root\[data-color-theme="dark"\],[\s\S]*?body\[data-color-theme="dark"\]\s*\{([\s\S]*?)\n\}/);
|
||||
assert.ok(match, "explicit dark palette must exist");
|
||||
const token = name => {
|
||||
const value = match[1].match(new RegExp(`--${name}:\\s*(#[0-9a-f]{6})`, "i"));
|
||||
assert.ok(value, `${name} dark token must exist`);
|
||||
return value[1];
|
||||
};
|
||||
|
||||
assert.ok(contrast(token("ui-text"), token("ui-surface")) >= 7);
|
||||
assert.ok(contrast(token("ui-text-muted"), token("ui-surface")) >= 4.5);
|
||||
assert.ok(contrast(token("ui-accent"), token("ui-canvas")) >= 4.5);
|
||||
assert.ok(contrast(token("ui-on-accent"), token("ui-accent")) >= 4.5);
|
||||
assert.ok(contrast(token("ui-on-accent"), token("ui-success")) >= 4.5);
|
||||
assert.ok(contrast(token("ui-danger"), token("ui-danger-soft")) >= 4.5);
|
||||
assert.ok(contrast(token("ui-warning"), token("ui-warning-soft")) >= 4.5);
|
||||
assert.match(styles,
|
||||
/\.playlist-data-row\.on-air\s*\{[^}]*background:\s*var\(--ui-on-air\)[^}]*var\(--ui-on-air-edge\)/s);
|
||||
});
|
||||
|
||||
test("light palette keeps hints and small semantic labels readable", () => {
|
||||
const match = styles.match(/:root\s*\{([\s\S]*?)\n\}/);
|
||||
assert.ok(match, "default light palette must exist");
|
||||
const token = name => {
|
||||
const value = match[1].match(new RegExp(`--${name}:\\s*(#[0-9a-f]{6})`, "i"));
|
||||
assert.ok(value, `${name} light token must exist`);
|
||||
return value[1];
|
||||
};
|
||||
|
||||
assert.ok(contrast(token("ui-text-subtle"), token("ui-surface")) >= 4.5);
|
||||
assert.ok(contrast(token("ui-text-subtle"), token("ui-surface-subtle")) >= 4.5);
|
||||
assert.ok(contrast(token("ui-success"), token("ui-success-soft")) >= 4.5);
|
||||
assert.ok(contrast(token("ui-warning"), token("ui-warning-soft")) >= 4.5);
|
||||
});
|
||||
|
||||
test("view modes change catalog paint without changing operator row geometry", () => {
|
||||
const start = styles.indexOf("/* View-mode presentation is paint-only:");
|
||||
const end = styles.indexOf("/* End view-mode presentation. */", start);
|
||||
assert.ok(start >= 0 && end > start, "view-mode CSS must have an auditable boundary");
|
||||
const viewModeRules = styles.slice(start, end);
|
||||
|
||||
for (const mode of ["automatic", "compact", "cards"]) {
|
||||
assert.ok(viewModeRules.includes(`data-view-mode="${mode}"`),
|
||||
`${mode} mode must have explicit presentation rules`);
|
||||
}
|
||||
assert.match(viewModeRules, /\.catalog-section/);
|
||||
assert.match(viewModeRules, /\.operator-catalog-results button/);
|
||||
assert.doesNotMatch(viewModeRules, /\.playlist-(?:row|data-row|grid)/);
|
||||
assert.doesNotMatch(viewModeRules,
|
||||
/(?:^|[;{]\s*)(?:height|min-height|padding|margin|grid-template(?:-columns|-rows)?|display)\s*:/m);
|
||||
});
|
||||
|
||||
test("appearance controls and accessibility preferences retain clear feedback", () => {
|
||||
assert.match(styles, /\.settings-choice-list\s*\{/);
|
||||
assert.match(styles, /\.settings-choice-row\s*\{/);
|
||||
assert.match(styles, /\.settings-choice-row select\s*\{/);
|
||||
assert.match(styles,
|
||||
/@media \(forced-colors:\s*active\)[\s\S]*?\.settings-choice-row\s*\{\s*border-bottom-color:\s*CanvasText/);
|
||||
assert.match(styles,
|
||||
/@media \(prefers-reduced-motion:\s*reduce\)[\s\S]*?\.settings-switch::after\s*\{\s*transition:\s*none !important/);
|
||||
});
|
||||
|
||||
test("workspace header keeps one concise title and the live native connection state", () => {
|
||||
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"/);
|
||||
/class="workspace-heading"[\s\S]*?class="workspace-heading-icon"[\s\S]*?<div class="workspace-heading-copy">[\s\S]*?<h1 id="workspace-title">[^<]+<\/h1>/);
|
||||
assert.doesNotMatch(markup, /<small>현재 작업<\/small>/);
|
||||
assert.match(markup,
|
||||
/class="playout-connection-summary"[\s\S]*?<small>송출 연결<\/small>[\s\S]*?<strong>Tornado2<\/strong>[\s\S]*?id="playout-connection-state"/);
|
||||
/class="playout-connection-summary">[\s\S]*?id="playout-connection-state"/);
|
||||
assert.doesNotMatch(markup, /class="playout-connection-copy"/);
|
||||
assert.doesNotMatch(markup, /<strong>Tornado2<\/strong>/);
|
||||
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*\{/);
|
||||
/\.workspace-header::before,\s*\.playlist-heading::before\s*\{\s*display:\s*none;/);
|
||||
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*\{/);
|
||||
|
||||
Reference in New Issue
Block a user