feat: add task-manager workspace layout
This commit is contained in:
@@ -1040,6 +1040,234 @@ async function elementGeometry(expression, label, verticalFraction = 0.5) {
|
||||
return geometry;
|
||||
}
|
||||
|
||||
async function readWorkspaceLayout() {
|
||||
return await evaluate(`(() => {
|
||||
function rectangle(element) {
|
||||
if (!element) return null;
|
||||
const bounds = element.getBoundingClientRect();
|
||||
return {
|
||||
left: bounds.left,
|
||||
top: bounds.top,
|
||||
right: bounds.right,
|
||||
bottom: bounds.bottom,
|
||||
width: bounds.width,
|
||||
height: bounds.height
|
||||
};
|
||||
}
|
||||
|
||||
const shell = document.querySelector(".operator-shell");
|
||||
const region = document.getElementById("workspace-region");
|
||||
const navigation = document.getElementById("workspace-navigation");
|
||||
const surface = document.querySelector(".workspace-surface");
|
||||
const toggle = document.getElementById("workspace-nav-toggle");
|
||||
const stockTab = document.getElementById("workspace-stock-tab");
|
||||
const catalogTab = document.getElementById("workspace-catalog-tab");
|
||||
const stock = document.getElementById("stock-workspace");
|
||||
const catalog = document.getElementById("catalog-workspace");
|
||||
const schedule = document.querySelector(".playlist-panel");
|
||||
const scheduleBounds = schedule?.getBoundingClientRect() || null;
|
||||
const scheduleHit = scheduleBounds && scheduleBounds.width > 0 && scheduleBounds.height > 0
|
||||
? document.elementFromPoint(
|
||||
scheduleBounds.left + Math.min(scheduleBounds.width - 2, scheduleBounds.width / 2),
|
||||
scheduleBounds.top + Math.min(scheduleBounds.height - 2, 90))
|
||||
: null;
|
||||
return {
|
||||
viewport: {
|
||||
width: document.documentElement.clientWidth,
|
||||
height: document.documentElement.clientHeight
|
||||
},
|
||||
shell: rectangle(shell),
|
||||
region: rectangle(region),
|
||||
navigation: rectangle(navigation),
|
||||
surface: rectangle(surface),
|
||||
schedule: rectangle(schedule),
|
||||
scheduleHitOwned: Boolean(schedule && scheduleHit && schedule.contains(scheduleHit)),
|
||||
activeWorkspace: region?.dataset?.activeWorkspace || null,
|
||||
navigationExpanded: toggle?.getAttribute("aria-expanded") === "true",
|
||||
navigationCollapsedClass: region?.classList.contains("nav-collapsed") === true,
|
||||
title: (document.getElementById("workspace-title")?.textContent || "").trim(),
|
||||
stock: {
|
||||
hidden: stock?.hidden === true,
|
||||
inert: stock?.hasAttribute("inert") === true,
|
||||
ariaHidden: stock?.getAttribute("aria-hidden") || null,
|
||||
tabCurrent: stockTab?.getAttribute("aria-current") || null,
|
||||
width: stock?.getBoundingClientRect().width ?? null,
|
||||
height: stock?.getBoundingClientRect().height ?? null
|
||||
},
|
||||
catalog: {
|
||||
hidden: catalog?.hidden === true,
|
||||
inert: catalog?.hasAttribute("inert") === true,
|
||||
ariaHidden: catalog?.getAttribute("aria-hidden") || null,
|
||||
tabCurrent: catalogTab?.getAttribute("aria-current") || null,
|
||||
width: catalog?.getBoundingClientRect().width ?? null,
|
||||
height: catalog?.getBoundingClientRect().height ?? null
|
||||
}
|
||||
};
|
||||
})()`);
|
||||
}
|
||||
|
||||
function workspaceLayoutMatches(layout, workspace, expanded = null) {
|
||||
if (!layout || layout.activeWorkspace !== workspace) return false;
|
||||
if (expanded !== null && layout.navigationExpanded !== expanded) return false;
|
||||
const selected = workspace === "stock" ? layout.stock : layout.catalog;
|
||||
const other = workspace === "stock" ? layout.catalog : layout.stock;
|
||||
return selected.hidden === false && selected.inert === false &&
|
||||
selected.ariaHidden === "false" && selected.tabCurrent === "page" &&
|
||||
selected.width > 0 && selected.height > 0 &&
|
||||
other.hidden === true && other.inert === true &&
|
||||
other.ariaHidden === "true" && other.tabCurrent === "false";
|
||||
}
|
||||
|
||||
async function waitForWorkspaceLayout(label, predicate) {
|
||||
const deadline = Math.min(Date.now() + 5_000, hardDeadlineEpochMilliseconds);
|
||||
let lastLayout = null;
|
||||
while (Date.now() < deadline) {
|
||||
lastLayout = await readWorkspaceLayout();
|
||||
if (predicate(lastLayout)) return lastLayout;
|
||||
await sleep(25);
|
||||
}
|
||||
failKnown(`Timed out waiting for ${label}. Last layout: ${JSON.stringify(lastLayout)}`);
|
||||
}
|
||||
|
||||
function assertWorkspaceAndScheduleGeometry(layout, label) {
|
||||
if (!layout?.shell || !layout.region || !layout.navigation || !layout.surface ||
|
||||
!layout.schedule || layout.schedule.width <= 0 || layout.schedule.height <= 0 ||
|
||||
layout.scheduleHitOwned !== true) {
|
||||
failKnown(`${label} does not expose the complete workspace and fixed schedule geometry.`);
|
||||
}
|
||||
const widthRatio = layout.region.width / layout.schedule.width;
|
||||
const workspaceShare = layout.region.width / layout.shell.width;
|
||||
if (!Number.isFinite(widthRatio) || widthRatio < 1.85 || widthRatio > 2.15 ||
|
||||
!Number.isFinite(workspaceShare) || workspaceShare < 0.62 || workspaceShare > 0.70 ||
|
||||
Math.abs(layout.schedule.right - layout.shell.right) > 1.5 ||
|
||||
Math.abs(layout.schedule.top - layout.shell.top) > 1.5 ||
|
||||
Math.abs(layout.schedule.bottom - layout.shell.bottom) > 1.5) {
|
||||
failKnown(`${label} does not preserve the approximately 2:1 workspace/schedule split.`);
|
||||
}
|
||||
return { widthRatio, workspaceShare };
|
||||
}
|
||||
|
||||
function assertFixedSchedule(baseline, current, label) {
|
||||
for (const edge of ["left", "top", "right", "bottom", "width", "height"]) {
|
||||
if (Math.abs(baseline.schedule[edge] - current.schedule[edge]) > 1.5) {
|
||||
failKnown(`${label} moved or resized the always-visible schedule at ${edge}.`);
|
||||
}
|
||||
}
|
||||
assertWorkspaceAndScheduleGeometry(current, label);
|
||||
}
|
||||
|
||||
async function ensureWorkspace(workspace, label) {
|
||||
const tabId = workspace === "stock"
|
||||
? "workspace-stock-tab"
|
||||
: workspace === "catalog"
|
||||
? "workspace-catalog-tab"
|
||||
: null;
|
||||
if (!tabId) failKnown(`${label} requested an unknown workspace.`);
|
||||
|
||||
let layout = await readWorkspaceLayout();
|
||||
if (!workspaceLayoutMatches(layout, workspace)) {
|
||||
await pointerClick(`document.getElementById("${tabId}")`, label);
|
||||
layout = await waitForWorkspaceLayout(label, candidate =>
|
||||
workspaceLayoutMatches(candidate, workspace));
|
||||
}
|
||||
assertWorkspaceAndScheduleGeometry(layout, label);
|
||||
return layout;
|
||||
}
|
||||
|
||||
async function verifyWorkspaceNavigationAndLayout(preflight) {
|
||||
const baseline = await ensureWorkspace("stock", "workspace stock preflight");
|
||||
if (baseline.navigationExpanded !== true || baseline.navigationCollapsedClass === true) {
|
||||
failKnown("The workspace menu was not initially expanded.");
|
||||
}
|
||||
const baselineRatio = assertWorkspaceAndScheduleGeometry(
|
||||
baseline,
|
||||
"expanded stock workspace preflight");
|
||||
|
||||
await pointerClick(
|
||||
'document.getElementById("workspace-nav-toggle")',
|
||||
"collapse workspace menu");
|
||||
const collapsed = await waitForWorkspaceLayout("collapsed workspace menu", layout =>
|
||||
workspaceLayoutMatches(layout, "stock", false) &&
|
||||
layout.navigationCollapsedClass === true);
|
||||
if (collapsed.navigation.width >= baseline.navigation.width - 100 ||
|
||||
collapsed.surface.width <= baseline.surface.width + 100) {
|
||||
failKnown("The pointer collapse did not transfer menu width to the active workspace.");
|
||||
}
|
||||
assertFixedSchedule(baseline, collapsed, "collapsed workspace menu");
|
||||
|
||||
await pointerClick(
|
||||
'document.getElementById("workspace-catalog-tab")',
|
||||
"select catalog workspace from collapsed menu");
|
||||
const catalog = await waitForWorkspaceLayout("catalog workspace", layout =>
|
||||
workspaceLayoutMatches(layout, "catalog", false) &&
|
||||
layout.title === "기타 그래픽");
|
||||
assertFixedSchedule(baseline, catalog, "catalog workspace transition");
|
||||
|
||||
await pointerClick(
|
||||
'document.getElementById("workspace-nav-toggle")',
|
||||
"expand workspace menu");
|
||||
const expandedCatalog = await waitForWorkspaceLayout("expanded catalog workspace", layout =>
|
||||
workspaceLayoutMatches(layout, "catalog", true) &&
|
||||
layout.navigationCollapsedClass === false);
|
||||
if (Math.abs(expandedCatalog.navigation.width - baseline.navigation.width) > 1.5 ||
|
||||
Math.abs(expandedCatalog.surface.width - baseline.surface.width) > 1.5) {
|
||||
failKnown("The pointer expand did not restore the original menu/workspace widths.");
|
||||
}
|
||||
assertFixedSchedule(baseline, expandedCatalog, "expanded catalog workspace");
|
||||
|
||||
await pointerClick(
|
||||
'document.getElementById("workspace-stock-tab")',
|
||||
"return to stock workspace");
|
||||
const restoredStock = await waitForWorkspaceLayout("restored stock workspace", layout =>
|
||||
workspaceLayoutMatches(layout, "stock", true) && layout.title === "종목·컷");
|
||||
assertFixedSchedule(baseline, restoredStock, "restored stock workspace");
|
||||
|
||||
if (await evaluate('document.activeElement?.id === "workspace-stock-tab"') !== true) {
|
||||
failKnown("The pointer-selected stock menu item did not retain keyboard focus.");
|
||||
}
|
||||
await pressKey("ArrowDown", "focus catalog workspace menu item");
|
||||
if (await evaluate('document.activeElement?.id === "workspace-catalog-tab"') !== true) {
|
||||
failKnown("ArrowDown did not move focus to the catalog workspace menu item.");
|
||||
}
|
||||
const focusOnlyStock = await readWorkspaceLayout();
|
||||
if (!workspaceLayoutMatches(focusOnlyStock, "stock", true)) {
|
||||
failKnown("ArrowDown changed the workspace before explicit keyboard activation.");
|
||||
}
|
||||
assertFixedSchedule(baseline, focusOnlyStock, "keyboard catalog focus");
|
||||
|
||||
await pressKey("Enter", "activate catalog workspace from keyboard");
|
||||
const keyboardCatalog = await waitForWorkspaceLayout("keyboard catalog workspace", layout =>
|
||||
workspaceLayoutMatches(layout, "catalog", true) && layout.title === "기타 그래픽");
|
||||
assertFixedSchedule(baseline, keyboardCatalog, "keyboard catalog activation");
|
||||
|
||||
await pressKey("ArrowUp", "focus stock workspace menu item");
|
||||
if (await evaluate('document.activeElement?.id === "workspace-stock-tab"') !== true) {
|
||||
failKnown("ArrowUp did not return focus to the stock workspace menu item.");
|
||||
}
|
||||
await pressKey("Enter", "activate stock workspace from keyboard");
|
||||
const keyboardStock = await waitForWorkspaceLayout("keyboard stock workspace", layout =>
|
||||
workspaceLayoutMatches(layout, "stock", true) && layout.title === "종목·컷");
|
||||
assertFixedSchedule(baseline, keyboardStock, "keyboard stock activation");
|
||||
|
||||
const after = await readSnapshot();
|
||||
assertSafety(after, "workspace navigation/layout input", false);
|
||||
if (after.safety.outboundMessages.length !== preflight.safety.outboundMessages.length ||
|
||||
after.safety.blockedOutboundMessages.length !==
|
||||
preflight.safety.blockedOutboundMessages.length) {
|
||||
failKnown("Workspace-only pointer input crossed the native WebView message boundary.");
|
||||
}
|
||||
|
||||
await checkpoint("workspace-navigation-layout", {
|
||||
widthRatio: Number(baselineRatio.widthRatio.toFixed(4)),
|
||||
workspaceShare: Number(baselineRatio.workspaceShare.toFixed(4)),
|
||||
expandedNavigationWidth: baseline.navigation.width,
|
||||
collapsedNavigationWidth: collapsed.navigation.width,
|
||||
schedule: baseline.schedule,
|
||||
pointerTransitions: ["collapse", "catalog", "expand", "stock"],
|
||||
keyboardTransitions: ["ArrowDown", "Enter catalog", "ArrowUp", "Enter stock"]
|
||||
});
|
||||
}
|
||||
|
||||
async function playlistDropGeometry(label) {
|
||||
const geometry = await evaluate(`(() => {
|
||||
const zone = document.getElementById("playlist-drop-zone");
|
||||
@@ -1230,6 +1458,7 @@ async function pressKey(definitionName, label, modifiers = 0) {
|
||||
}
|
||||
|
||||
async function setSearchText(text) {
|
||||
await ensureWorkspace("stock", "show stock workspace before stock search");
|
||||
await pointerClick(
|
||||
'document.getElementById("stock-search")',
|
||||
"focus stock search");
|
||||
@@ -2035,6 +2264,7 @@ async function executeSafeSequence() {
|
||||
failKnown("A child modal was already open at preflight.");
|
||||
}
|
||||
await checkpoint("preflight");
|
||||
await verifyWorkspaceNavigationAndLayout(preflight);
|
||||
|
||||
await setSearchText(evidence.fixture.query);
|
||||
await pointerClick(
|
||||
@@ -2698,6 +2928,7 @@ function manualScreenButtonExpression(screen) {
|
||||
}
|
||||
|
||||
async function openManualFinancial(screen, label) {
|
||||
await ensureWorkspace("stock", `${label} stock workspace`);
|
||||
await pointerClick(manualScreenButtonExpression(screen), label);
|
||||
return await waitFor(`${label} open`, snapshot =>
|
||||
snapshot.state.isBusy === false &&
|
||||
@@ -2931,6 +3162,7 @@ async function executeManualFinancialCrud() {
|
||||
}
|
||||
|
||||
async function selectMainTab(tabId, label) {
|
||||
await ensureWorkspace("catalog", `${label} catalog workspace`);
|
||||
const isLoaded = snapshot =>
|
||||
snapshot.state.isBusy === false &&
|
||||
snapshot.state.tabs?.some(tab => tab.id === tabId && tab.isActive) === true &&
|
||||
|
||||
Reference in New Issue
Block a user