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>');

View File

@@ -53,8 +53,12 @@ public sealed class LegacyWebContractTests
Assert.Contains("TAKE OUT", Markup, StringComparison.Ordinal);
Assert.Contains("NEXT", Markup, StringComparison.Ordinal);
Assert.Matches("class=\"connection-state\"[^>]*>미연결</div>", Markup);
Assert.Matches("class=\"active\"[^>]*data-tab-id=\"overseas\">해외</button>", Markup);
Assert.Matches("data-tab-id=\"tradingHalt\">정지</button>", Markup);
Assert.Matches(
@"class=""[^""]*\bactive\b[^""]*""[^>]*data-tab-id=""overseas""[\s\S]*?<span class=""workspace-nav-label"">해외<\/span>[\s\S]*?<\/button>",
Markup);
Assert.Matches(
@"data-tab-id=""tradingHalt""[\s\S]*?<span class=""workspace-nav-label"">정지<\/span>[\s\S]*?<\/button>",
Markup);
}
[Fact]
@@ -265,6 +269,17 @@ public sealed class LegacyWebContractTests
Assert.DoesNotContain("send(\"swap-tabs\"", App, StringComparison.Ordinal);
}
[Fact]
public void MarketTabRenderingPreservesSidebarLabelsAndReconcilesNativeSelection()
{
Assert.Contains("button.querySelector(\".workspace-nav-label\")", App,
StringComparison.Ordinal);
Assert.Contains("if (label) label.textContent = tab.label", App,
StringComparison.Ordinal);
Assert.Contains("window.LegacyWorkspaceNavigation?.syncMarket", App,
StringComparison.Ordinal);
}
[Fact]
public void FixedCatalogUsesOpaqueCSharpActionAndSectionIntents()
{

View File

@@ -17,25 +17,56 @@ public sealed class LegacyWorkspaceLayoutContractTests
Path.Combine(WebRoot, "workspace-navigation.js"));
[Fact]
public void ShellUsesSwitchableWorkspaceBesidePersistentSchedule()
public void ShellUsesSidebarScreensBesidePersistentSchedule()
{
var workspaceStart = Markup.IndexOf("id=\"workspace-region\"", StringComparison.Ordinal);
var navigationStart = Markup.IndexOf("id=\"workspace-navigation\"", StringComparison.Ordinal);
var marketStart = Markup.IndexOf("id=\"market-tabs\"", StringComparison.Ordinal);
var settingsTabStart = Markup.IndexOf("id=\"workspace-settings-tab\"", StringComparison.Ordinal);
var stockStart = Markup.IndexOf("id=\"stock-workspace\"", StringComparison.Ordinal);
var catalogStart = Markup.IndexOf("id=\"catalog-workspace\"", StringComparison.Ordinal);
var settingsStart = Markup.IndexOf("id=\"settings-workspace\"", StringComparison.Ordinal);
var scheduleStart = Markup.IndexOf("class=\"playlist-panel\"", StringComparison.Ordinal);
Assert.True(workspaceStart >= 0);
Assert.True(stockStart > workspaceStart);
Assert.True(navigationStart > workspaceStart);
Assert.True(marketStart > navigationStart);
Assert.True(settingsTabStart > marketStart);
Assert.True(stockStart > settingsTabStart);
Assert.True(catalogStart > stockStart);
Assert.True(scheduleStart > catalogStart);
Assert.True(settingsStart > catalogStart);
Assert.True(scheduleStart > settingsStart);
Assert.Matches(
@"<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*<section class=""playlist-panel""",
Markup);
Assert.Matches("id=\"catalog-workspace\"[^>]*hidden inert", Markup);
Assert.Matches("id=\"settings-workspace\"[^>]*hidden inert", Markup);
Assert.Contains("<small>항상 표시</small><h2>송출 스케줄</h2>", Markup,
StringComparison.Ordinal);
}
[Fact]
public void SidebarOwnsEachNativeMarketButtonAndKeepsSettingsOutsideTheirOrder()
{
var marketStart = Markup.IndexOf("<div id=\"market-tabs\"", StringComparison.Ordinal);
var marketEnd = Markup.IndexOf("</div>", marketStart, StringComparison.Ordinal);
Assert.True(marketStart >= 0 && marketEnd > marketStart);
var marketMarkup = Markup[marketStart..marketEnd];
var expected = new[]
{
"overseas", "exchange", "index", "kospiIndustry", "kosdaqIndustry",
"comparison", "theme", "overseasStocks", "expert", "tradingHalt"
};
var actual = Regex.Matches(marketMarkup, "data-tab-id=\"([^\"]+)\"")
.Select(match => match.Groups[1].Value)
.ToArray();
Assert.Equal(expected, actual);
Assert.Equal(expected.Length, Regex.Matches(marketMarkup, "<button\\b").Count);
Assert.DoesNotContain("workspace-settings-tab", marketMarkup, StringComparison.Ordinal);
Assert.Contains("workspace-nav-label", marketMarkup, StringComparison.Ordinal);
}
[Fact]
public void WorkAndScheduleUseResponsiveTwoToOneColumns()
{
@@ -43,6 +74,13 @@ public sealed class LegacyWorkspaceLayoutContractTests
"grid-template-columns: minmax(900px, 2fr) minmax(600px, 1fr);",
Styles,
StringComparison.Ordinal);
Assert.Contains(
"grid-template-rows: 66px minmax(0, 1fr) auto;",
Styles,
StringComparison.Ordinal);
Assert.Contains(".workspace-nav-footer", Styles, StringComparison.Ordinal);
Assert.Contains(".workspace-navigation .market-tabs button", Styles,
StringComparison.Ordinal);
Assert.Matches(
new Regex(
@"\.playlist-toolbar\s*\{[^}]*flex-wrap:\s*wrap",
@@ -55,6 +93,28 @@ public sealed class LegacyWorkspaceLayoutContractTests
Styles);
}
[Fact]
public void SidebarIsOneLabelledAndFullyControlledNavigationRegion()
{
Assert.Matches(
"id=\"workspace-navigation\"[^>]*role=\"navigation\"[^>]*aria-label=",
Markup);
Assert.Matches(
"id=\"workspace-nav-toggle\"[^>]*aria-expanded=\"true\"[\\s\\S]*?" +
"aria-controls=\"workspace-nav-items workspace-nav-footer\"",
Markup);
Assert.Matches(
"id=\"workspace-nav-items\"[^>]*role=\"group\"[^>]*aria-label=",
Markup);
Assert.Matches(
"id=\"market-tabs\"[^>]*role=\"group\"[^>]*aria-label=",
Markup);
Assert.Contains(
"id=\"workspace-nav-footer\" class=\"workspace-nav-footer\"",
Markup,
StringComparison.Ordinal);
}
[Fact]
public void ModernOperatorThemePreservesAccessibleAndSemanticStates()
{
@@ -119,6 +179,14 @@ public sealed class LegacyWorkspaceLayoutContractTests
"entry.view.toggleAttribute(\"inert\", !isCurrent)",
Navigation,
StringComparison.Ordinal);
Assert.Contains("settingsTab.addEventListener(\"click\"", Navigation,
StringComparison.Ordinal);
Assert.Contains("syncMarket(button.dataset.tabId)", Navigation,
StringComparison.Ordinal);
Assert.Contains("menuButton.click()", Navigation, StringComparison.Ordinal);
Assert.Contains("pendingKeyboardMarketTabId", Navigation, StringComparison.Ordinal);
Assert.Contains("pendingButton.focus()", Navigation, StringComparison.Ordinal);
Assert.Contains("LegacyWorkspaceNavigation", Navigation, StringComparison.Ordinal);
Assert.Contains("nav-collapsed", Navigation, StringComparison.Ordinal);
Assert.DoesNotContain("postMessage", Navigation, StringComparison.Ordinal);
Assert.DoesNotContain("localStorage", Navigation, StringComparison.Ordinal);