Fix topology scope hydration and live sweep readiness
This commit is contained in:
@@ -21,6 +21,39 @@ const topologyScope = {
|
||||
};
|
||||
const topologyScopeQuery = new URLSearchParams(topologyScope).toString();
|
||||
const STEP_TIMEOUT_MS = 30_000;
|
||||
const GENERIC_TITLES = new Set(['StellaOps', 'Stella Ops Dashboard']);
|
||||
const ROUTE_READINESS = [
|
||||
{
|
||||
path: '/setup/topology/overview',
|
||||
title: 'Topology Overview - StellaOps',
|
||||
markers: ['Open Promotion Paths', 'Open Agents', 'Top Hotspots'],
|
||||
},
|
||||
{
|
||||
path: '/setup/topology/environments',
|
||||
title: 'Environments - StellaOps',
|
||||
markers: ['Environment Signals', 'Open Targets', 'Open Runs'],
|
||||
},
|
||||
{
|
||||
path: '/setup/topology/environments/stage/posture',
|
||||
title: 'Environment Detail - StellaOps',
|
||||
markers: ['Operator Actions', 'Open Security Triage', 'Data Quality'],
|
||||
},
|
||||
{
|
||||
path: '/setup/topology/targets',
|
||||
title: 'Targets - StellaOps',
|
||||
markers: ['No targets for current filters.', 'Select a target row to view its topology mapping details.'],
|
||||
},
|
||||
{
|
||||
path: '/setup/topology/hosts',
|
||||
title: 'Hosts - StellaOps',
|
||||
markers: ['No hosts for current filters.', 'Select a host row to inspect runtime drift and impact.'],
|
||||
},
|
||||
{
|
||||
path: '/setup/topology/agents',
|
||||
title: 'Agent Fleet - StellaOps',
|
||||
markers: ['No groups for current filters.', 'All Agents', 'View Targets'],
|
||||
},
|
||||
];
|
||||
|
||||
function isStaticAsset(url) {
|
||||
return /\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url);
|
||||
@@ -61,11 +94,16 @@ function attachRuntimeObservers(page, runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorText = request.failure()?.errorText ?? 'unknown';
|
||||
if (errorText === 'net::ERR_ABORTED') {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.requestFailures.push({
|
||||
page: page.url(),
|
||||
method: request.method(),
|
||||
url: request.url(),
|
||||
error: request.failure()?.errorText ?? 'unknown',
|
||||
error: errorText,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,7 +123,66 @@ function attachRuntimeObservers(page, runtime) {
|
||||
});
|
||||
}
|
||||
|
||||
async function settle(page) {
|
||||
function routePath(routeOrPath) {
|
||||
if (routeOrPath.startsWith('http://') || routeOrPath.startsWith('https://')) {
|
||||
return new URL(routeOrPath).pathname;
|
||||
}
|
||||
|
||||
return new URL(withScope(routeOrPath)).pathname;
|
||||
}
|
||||
|
||||
function routeReadiness(pathname) {
|
||||
return ROUTE_READINESS.find((entry) => entry.path === pathname) ?? null;
|
||||
}
|
||||
|
||||
async function waitForRouteReady(page, routeOrPath) {
|
||||
const expectedPath = routePath(routeOrPath);
|
||||
const readiness = routeReadiness(expectedPath);
|
||||
|
||||
if (!readiness) {
|
||||
await page.waitForFunction(
|
||||
({ expectedPathValue, genericTitles }) => {
|
||||
if (window.location.pathname !== expectedPathValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const title = document.title.trim();
|
||||
return title.length > 0 && !genericTitles.includes(title);
|
||||
},
|
||||
{
|
||||
expectedPathValue: expectedPath,
|
||||
genericTitles: [...GENERIC_TITLES],
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
await page.waitForFunction(
|
||||
({ expectedPathValue, expectedTitle, markers, genericTitles }) => {
|
||||
if (window.location.pathname !== expectedPathValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const title = document.title.trim();
|
||||
if (title.length === 0 || genericTitles.includes(title) || title !== expectedTitle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bodyText = document.body?.innerText?.replace(/\s+/g, ' ') ?? '';
|
||||
return markers.some((marker) => bodyText.includes(marker));
|
||||
},
|
||||
{
|
||||
expectedPathValue: expectedPath,
|
||||
expectedTitle: readiness.title,
|
||||
markers: readiness.markers,
|
||||
genericTitles: [...GENERIC_TITLES],
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
async function settle(page, routeOrPath = null) {
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 15_000 }).catch(() => {});
|
||||
await page.waitForTimeout(750);
|
||||
|
||||
@@ -96,6 +193,11 @@ async function settle(page) {
|
||||
}
|
||||
|
||||
await page.waitForTimeout(750);
|
||||
|
||||
if (routeOrPath) {
|
||||
await waitForRouteReady(page, routeOrPath);
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
}
|
||||
|
||||
async function headingText(page) {
|
||||
@@ -146,7 +248,7 @@ async function navigate(page, route) {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30_000,
|
||||
});
|
||||
await settle(page);
|
||||
await settle(page, route);
|
||||
}
|
||||
|
||||
function hasExpectedQuery(urlString, expectedQuery = {}) {
|
||||
@@ -184,7 +286,12 @@ async function resolveLink(page, options) {
|
||||
|
||||
async function clickLinkAction(page, route, options) {
|
||||
await navigate(page, route);
|
||||
const link = await resolveLink(page, options);
|
||||
let link = await resolveLink(page, options);
|
||||
if (!link) {
|
||||
await page.waitForTimeout(1_000);
|
||||
await settle(page, route);
|
||||
link = await resolveLink(page, options);
|
||||
}
|
||||
if (!link) {
|
||||
return {
|
||||
action: options.action,
|
||||
@@ -195,7 +302,8 @@ async function clickLinkAction(page, route, options) {
|
||||
}
|
||||
|
||||
await link.click({ timeout: 10_000 });
|
||||
await settle(page);
|
||||
await page.waitForURL((url) => url.pathname === options.expectedPath, { timeout: 15_000 }).catch(() => {});
|
||||
await settle(page, options.expectedPath);
|
||||
|
||||
const url = new URL(page.url());
|
||||
const ok = url.pathname === options.expectedPath && hasExpectedQuery(page.url(), options.expectedQuery);
|
||||
|
||||
Reference in New Issue
Block a user