feat: expose graphic screens in sidebar

This commit is contained in:
2026-07-21 09:45:12 +09:00
parent 44a7d90413
commit 15ad9934a9
11 changed files with 1044 additions and 117 deletions

View File

@@ -47,8 +47,10 @@ test("legacy shell exposes the original stock, cut, playlist and playout control
assert.match(markup, /TAKE OUT/);
assert.match(markup, /NEXT/);
assert.match(markup, /class="connection-state"[^>]*>미연결<\/div>/);
assert.match(markup, /class="active"[^>]*data-tab-id="overseas">해외<\/button>/);
assert.match(markup, /data-tab-id="tradingHalt">정지<\/button>/);
assert.match(markup,
/class="[^"]*\bactive\b[^"]*"[^>]*data-tab-id="overseas"[\s\S]*?<span class="workspace-nav-label">해외<\/span>[\s\S]*?<\/button>/);
assert.match(markup,
/data-tab-id="tradingHalt"[\s\S]*?<span class="workspace-nav-label">정지<\/span>[\s\S]*?<\/button>/);
});
test("web layer sends intent and does not own scene, DB identity or playlist state", () => {
@@ -840,6 +842,8 @@ test("market tabs send closed identity intents and reconcile C# order", () => {
assert.match(app, /dataset\.tabId/);
assert.match(app, /const existingTabs = new Map\(\)/);
assert.match(app, /marketTabs\.insertBefore/);
assert.match(app, /button\.querySelector\("\.workspace-nav-label"\)/);
assert.match(app, /LegacyWorkspaceNavigation\?\.syncMarket/);
assert.doesNotMatch(app, /selectedTab\s*=/);
});

View File

@@ -11,34 +11,72 @@ const markup = fs.readFileSync(path.join(webRoot, "index.html"), "utf8");
const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8");
const navigation = fs.readFileSync(path.join(webRoot, "workspace-navigation.js"), "utf8");
test("operator shell keeps one switchable workspace beside an always-present schedule", () => {
const expectedMarketTabs = [
"overseas",
"exchange",
"index",
"kospiIndustry",
"kosdaqIndustry",
"comparison",
"theme",
"overseasStocks",
"expert",
"tradingHalt"
];
test("operator shell exposes stock and each graphic screen beside an always-present schedule", () => {
for (const id of [
"workspace-region",
"workspace-navigation",
"workspace-nav-toggle",
"workspace-stock-tab",
"workspace-catalog-tab",
"market-tabs",
"workspace-settings-tab",
"workspace-title",
"stock-workspace",
"catalog-workspace",
"settings-workspace",
"playlist-drop-zone"
]) {
assert.match(markup, new RegExp(`id="${id}"`));
}
const workspaceStart = markup.indexOf('id="workspace-region"');
const navigationStart = markup.indexOf('id="workspace-navigation"');
const marketStart = markup.indexOf('id="market-tabs"');
const settingsTabStart = markup.indexOf('id="workspace-settings-tab"');
const stockStart = markup.indexOf('id="stock-workspace"');
const catalogStart = markup.indexOf('id="catalog-workspace"');
const settingsStart = markup.indexOf('id="settings-workspace"');
const scheduleStart = markup.indexOf('class="playlist-panel"');
assert.ok(workspaceStart >= 0 && stockStart > workspaceStart);
assert.ok(workspaceStart >= 0 && navigationStart > workspaceStart);
assert.ok(marketStart > navigationStart && settingsTabStart > marketStart);
assert.ok(stockStart > settingsTabStart);
assert.ok(catalogStart > stockStart);
assert.ok(scheduleStart > catalogStart);
assert.ok(settingsStart > catalogStart && scheduleStart > settingsStart);
assert.match(markup,
/<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*<section class="playlist-panel"/);
assert.match(markup, /id="catalog-workspace"[^>]*hidden inert/);
assert.match(markup, /id="settings-workspace"[^>]*hidden inert/);
assert.match(markup, /<small>항상 표시<\/small><h2>송출 스케줄<\/h2>/);
});
test("the sidebar owns the ten native market buttons and keeps settings outside their order", () => {
const marketStart = markup.indexOf('<div id="market-tabs"');
const marketEnd = markup.indexOf("</div>", marketStart);
assert.ok(marketStart >= 0 && marketEnd > marketStart);
const marketMarkup = markup.slice(marketStart, marketEnd);
const actualTabs = Array.from(
marketMarkup.matchAll(/data-tab-id="([^"]+)"/g), match => match[1]);
assert.deepEqual(actualTabs, expectedMarketTabs);
assert.equal((marketMarkup.match(/<button\b/g) || []).length, expectedMarketTabs.length);
assert.doesNotMatch(marketMarkup, /workspace-settings-tab|workspace-nav-section-label/);
for (const tabId of expectedMarketTabs) {
assert.match(marketMarkup,
new RegExp(`data-tab-id="${tabId}"[\\s\\S]*?class="workspace-nav-label"`));
}
});
test("layout uses a two-to-one work and schedule split with compact schedule columns", () => {
assert.match(styles,
/grid-template-columns:\s*minmax\(900px, 2fr\) minmax\(600px, 1fr\)/);
@@ -46,6 +84,11 @@ test("layout uses a two-to-one work and schedule split with compact schedule col
/\.workspace-region\s*\{[^}]*grid-template-columns:\s*var\(--workspace-nav-width\) minmax\(0, 1fr\)/);
assert.match(styles, /\.workspace-region\.nav-collapsed/);
assert.match(styles, /--workspace-nav-collapsed:\s*62px/);
assert.match(styles,
/\.workspace-navigation\s*\{[^}]*grid-template-rows:\s*66px minmax\(0, 1fr\) auto/);
assert.match(styles, /\.workspace-nav-footer\s*\{[^}]*border-top/);
assert.match(styles,
/\.workspace-navigation \.market-tabs button\s*\{[^}]*width:\s*100%[^}]*height:\s*46px/);
assert.match(styles,
/\.workspace-nav-item:focus-visible\s*\{[\s\S]*?outline:\s*2px solid var\(--workspace-accent\)/);
assert.match(styles, /\.playlist-toolbar\s*\{[^}]*flex-wrap:\s*wrap/);
@@ -76,19 +119,48 @@ test("modern operator theme keeps clear semantic states and accessible focus", (
assert.match(markup, /class="playlist-heading-icon"[\s\S]*?<svg[^>]*viewBox="0 0 24 24"/);
});
test("the complete sidebar is one labelled and collapsible navigation region", () => {
assert.match(markup,
/id="workspace-navigation"[^>]*role="navigation"[^>]*aria-label=/);
assert.match(markup,
/id="workspace-nav-toggle"[^>]*aria-expanded="true"[\s\S]*?aria-controls="workspace-nav-items workspace-nav-footer"/);
assert.match(markup,
/id="workspace-nav-items"[^>]*role="group"[^>]*aria-label=/);
assert.match(markup,
/id="market-tabs"[^>]*role="group"[^>]*aria-label=/);
assert.match(markup, /id="workspace-nav-footer" class="workspace-nav-footer"/);
});
test("workspace navigation is local presentation state and preserves native contracts", () => {
assert.match(navigation, /entry\.view\.hidden = !isCurrent/);
assert.match(navigation, /entry\.view\.toggleAttribute\("inert", !isCurrent\)/);
assert.match(navigation, /if \(focusWillBeHidden\) selected\.tab\.focus\(\)/);
assert.match(navigation, /setAttribute\("aria-current", isCurrent \? "page" : "false"\)/);
assert.match(navigation, /if \(focusWillBeHidden && selectedButton\) selectedButton\.focus\(\)/);
assert.match(navigation, /button\.setAttribute\("aria-current", isCurrent \? "page" : "false"\)/);
assert.match(navigation, /region\.classList\.toggle\("nav-collapsed", !isExpanded\)/);
assert.match(navigation, /event\.key !== "ArrowUp" && event\.key !== "ArrowDown"/);
assert.match(navigation, /navigation\.querySelectorAll\("\.workspace-nav-item"\)/);
assert.match(navigation, /event\.key === "Enter" \|\| event\.key === " "/);
assert.match(navigation, /setWorkspace\(menuButton === stockTab \? "stock" : "catalog"\)/);
assert.match(navigation, /menuButton\.click\(\)/);
assert.match(navigation, /pendingKeyboardMarketTabId = keyboardMarketTabId/);
assert.match(navigation, /pendingButton\.focus\(\)/);
assert.match(navigation, /document\.activeElement === document\.body/);
assert.match(navigation, /marketTabs\.addEventListener\("click"/);
assert.match(navigation, /settingsTab\.addEventListener\("click"/);
assert.match(navigation, /syncMarket\(button\.dataset\.tabId\)/);
assert.match(navigation, /setWorkspace\("catalog"\)/);
assert.doesNotMatch(navigation, /postMessage|localStorage|sessionStorage|send\s*\(/);
});
test("native tab rendering preserves sidebar icons and reconciles the active market", () => {
const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
assert.match(app, /button\.querySelector\("\.workspace-nav-label"\)/);
assert.match(app, /if \(label\) label\.textContent = tab\.label/);
assert.match(app, /LegacyWorkspaceNavigation\?\.syncMarket/);
assert.match(app, /activeTab && state\.isBusy !== true/);
assert.doesNotMatch(app,
/button\.classList\.toggle\("active"[^\n]*\n\s*button\.setAttribute\("aria-current"/);
});
test("workspace navigation loads before the native-authority application bridge", () => {
const navigationScript = markup.indexOf('<script src="workspace-navigation.js"></script>');
const applicationScript = markup.indexOf('<script src="app.js"></script>');