feat: complete legacy parity and modernize operator UI

This commit is contained in:
2026-07-22 12:34:46 +09:00
parent fc4007d676
commit 939c252d23
149 changed files with 26515 additions and 1736 deletions

View File

@@ -9,7 +9,12 @@ 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 navigation = fs.readFileSync(path.join(webRoot, "workspace-navigation.js"), "utf8");
const packageInputHarness = fs.readFileSync(path.join(
repositoryRoot, "scripts", "Test-LegacyPackageInput.mjs"), "utf8");
const uiOnlySmokeHarness = fs.readFileSync(path.join(
repositoryRoot, "scripts", "Test-LegacyPackageUiOnlySmoke.mjs"), "utf8");
const expectedMarketTabs = [
"overseas",
@@ -24,6 +29,63 @@ const expectedMarketTabs = [
"tradingHalt"
];
function pendingMenuFocusHarness() {
const start = navigation.indexOf("function syncMarket");
const end = navigation.indexOf("function select", start);
assert.ok(start >= 0 && end > start);
const source = navigation.slice(start, end);
const documentObject = {
body: {},
documentElement: {},
activeElement: null
};
const catalogChild = {};
const unrelatedNavigationItem = {};
let scheduled = null;
const pendingButton = {
disabled: false,
focused: false,
focus() {
this.focused = true;
documentObject.activeElement = this;
}
};
const api = new Function(
"document",
"window",
"catalogWorkspace",
"pendingButton",
`
let activeWorkspace = "catalog";
let activeMarketTabId = "old";
let pendingKeyboardMarketTabId = "theme";
const region = { dataset: {} };
const title = { textContent: "" };
function marketButton(tabId) {
return tabId === "theme" ? pendingButton : null;
}
function refreshCurrentMenu() {}
function marketLabel() { return "테마"; }
${source}
return { syncMarket, hasPendingKeyboardMarketFocus };
`)(
documentObject,
{ requestAnimationFrame(callback) { scheduled = callback; } },
{ contains(element) { return element === catalogChild; } },
pendingButton);
return {
...api,
documentObject,
catalogChild,
unrelatedNavigationItem,
pendingButton,
runFrame() {
assert.equal(typeof scheduled, "function");
scheduled();
}
};
}
test("operator shell exposes stock and each graphic screen beside an always-present schedule", () => {
for (const id of [
"workspace-region",
@@ -77,9 +139,9 @@ test("the sidebar owns the ten native market buttons and keeps settings outside
}
});
test("layout uses a two-to-one work and schedule split with compact schedule columns", () => {
test("layout gives the always-visible schedule a wider responsive share", () => {
assert.match(styles,
/grid-template-columns:\s*minmax\(900px, 2fr\) minmax\(600px, 1fr\)/);
/grid-template-columns:\s*minmax\(900px, 1\.75fr\) minmax\(600px, 1fr\)/);
assert.match(styles,
/\.workspace-region\s*\{[^}]*grid-template-columns:\s*var\(--workspace-nav-width\) minmax\(0, 1fr\)/);
assert.match(styles, /\.workspace-region\.nav-collapsed/);
@@ -93,6 +155,14 @@ test("layout uses a two-to-one work and schedule split with compact schedule col
/\.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/);
assert.match(styles, /\.playlist-row\s*\{[^}]*min-width:\s*600px/);
assert.match(packageInputHarness,
/widthRatio < 1\.60 \|\| widthRatio > 1\.90/);
assert.match(uiOnlySmokeHarness,
/workspaceToScheduleWidthRatio < 1\.60 \|\|[\s\S]*?workspaceToScheduleWidthRatio > 1\.90/);
assert.match(packageInputHarness, /approximately 1\.75:1 workspace\/schedule split/);
assert.match(uiOnlySmokeHarness, /approximately 1\.75:1 workspace layout/);
assert.doesNotMatch(packageInputHarness, /approximately 2:1 workspace\/schedule split/);
assert.doesNotMatch(uiOnlySmokeHarness, /approximately 2:1 workspace layout/);
});
test("modern operator theme keeps clear semantic states and accessible focus", () => {
@@ -114,9 +184,19 @@ test("modern operator theme keeps clear semantic states and accessible focus", (
assert.match(styles, /\.playout-actions \.take-in:not\(:disabled\)\s*\{/);
assert.match(styles, /\.playout-actions \.next:not\(:disabled\)\s*\{/);
assert.match(styles, /\.playout-actions \.take-out:not\(:disabled\)\s*\{/);
assert.match(styles,
/Contemporary density refinement: keep operator geometry, clarify visual hierarchy/);
assert.match(styles, /\.stock-result\.selected\s*\{/);
assert.match(styles, /\.playlist-heading-count\s*\{/);
assert.match(styles, /\.playlist-rows:empty::after\s*\{/);
assert.match(styles, /@media \(prefers-reduced-motion:\s*reduce\)/);
assert.match(markup, /id="stock-search-button"[\s\S]*?<svg[^>]*viewBox="0 0 24 24"/);
assert.match(markup, /class="playlist-heading-icon"[\s\S]*?<svg[^>]*viewBox="0 0 24 24"/);
assert.match(markup,
/id="playlist-heading-count"[^>]*role="status"[^>]*aria-live="polite"[^>]*aria-atomic="true"/);
assert.match(app, /const nextHeadingCount = `[^`]*\$\{state\.playlist\.length\}[^`]*\$\{enabledCount\}[^`]*`/);
assert.match(app,
/if \(playlistHeadingCount\.textContent !== nextHeadingCount\)\s*\{\s*playlistHeadingCount\.textContent = nextHeadingCount;/);
});
test("the complete sidebar is one labelled and collapsible navigation region", () => {
@@ -143,7 +223,11 @@ test("workspace navigation is local presentation state and preserves native cont
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, /window\.requestAnimationFrame\(function/);
assert.match(navigation, /options\.authoritativeNativeRender === true/);
assert.match(navigation, /hasPendingKeyboardMarketFocus/);
assert.match(navigation, /document\.addEventListener\("pointerdown"/);
assert.doesNotMatch(navigation, /catalogWorkspace\.contains\(active\)/);
assert.match(navigation, /marketTabs\.addEventListener\("click"/);
assert.match(navigation, /settingsTab\.addEventListener\("click"/);
assert.match(navigation, /syncMarket\(button\.dataset\.tabId\)/);
@@ -156,7 +240,11 @@ test("native tab rendering preserves sidebar icons and reconciles the active mar
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, /authoritativeNativeRender: true/);
assert.match(app, /activeTab && state\.isBusy !== true/);
assert.match(app, /hasPendingKeyboardMarketFocus\?\.\(\) === true/);
assert.match(app,
/!modalFocusManager\.getActiveModal\(\) && !keyboardMarketFocusPending/);
assert.doesNotMatch(app,
/button\.classList\.toggle\("active"[^\n]*\n\s*button\.setAttribute\("aria-current"/);
});
@@ -166,3 +254,29 @@ test("workspace navigation loads before the native-authority application bridge"
const applicationScript = markup.indexOf('<script src="app.js"></script>');
assert.ok(navigationScript >= 0 && applicationScript > navigationScript);
});
test("keyboard menu focus wins after a new graphics tree renders, but not after a deliberate move", () => {
const stolen = pendingMenuFocusHarness();
stolen.syncMarket("theme");
assert.equal(stolen.hasPendingKeyboardMarketFocus("theme"), true,
"the optimistic capture-phase selection retains keyboard ownership");
assert.equal(stolen.pendingButton.focused, false,
"the optimistic capture-phase selection cannot restore focus early");
stolen.syncMarket("theme", { authoritativeNativeRender: true });
stolen.documentObject.activeElement = stolen.documentObject.body;
stolen.runFrame();
assert.equal(stolen.pendingButton.focused, true,
"the final native render restores focus lost while the menu was disabled");
for (const deliberateFocus of ["sidebar", "content"]) {
const deliberate = pendingMenuFocusHarness();
deliberate.syncMarket("theme");
deliberate.syncMarket("theme", { authoritativeNativeRender: true });
deliberate.documentObject.activeElement = deliberateFocus === "sidebar"
? deliberate.unrelatedNavigationItem
: deliberate.catalogChild;
deliberate.runFrame();
assert.equal(deliberate.pendingButton.focused, false,
"a subsequent deliberate sidebar focus move remains authoritative");
}
});