Harden live Playwright action sweeps for cold-loaded surfaces
This commit is contained in:
@@ -15,6 +15,7 @@ const authStatePath = path.join(outputDir, 'live-mission-control-action-sweep.st
|
||||
const authReportPath = path.join(outputDir, 'live-mission-control-action-sweep.auth.json');
|
||||
const scopeQuery = 'tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d';
|
||||
const STEP_TIMEOUT_MS = 30_000;
|
||||
const ELEMENT_WAIT_MS = 8_000;
|
||||
|
||||
function isStaticAsset(url) {
|
||||
return /\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url);
|
||||
@@ -86,7 +87,7 @@ function attachRuntimeObservers(page, runtime) {
|
||||
|
||||
async function settle(page) {
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 15_000 }).catch(() => {});
|
||||
await page.waitForTimeout(1_000);
|
||||
await page.waitForTimeout(1_500);
|
||||
}
|
||||
|
||||
async function headingText(page) {
|
||||
@@ -136,29 +137,35 @@ async function navigate(page, route) {
|
||||
await settle(page);
|
||||
}
|
||||
|
||||
async function resolveLink(page, options) {
|
||||
if (options.hrefIncludes) {
|
||||
const candidates = page.locator(`a[href*="${options.hrefIncludes}"]`);
|
||||
const count = await candidates.count();
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const candidate = candidates.nth(index);
|
||||
const text = ((await candidate.innerText().catch(() => '')) || '').trim();
|
||||
if (!options.name || text === options.name || text.includes(options.name)) {
|
||||
return candidate;
|
||||
async function resolveLink(page, options, timeoutMs = ELEMENT_WAIT_MS) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
if (options.hrefIncludes) {
|
||||
const candidates = page.locator(`a[href*="${options.hrefIncludes}"]`);
|
||||
const count = await candidates.count();
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const candidate = candidates.nth(index);
|
||||
const text = ((await candidate.innerText().catch(() => '')) || '').trim();
|
||||
if (!options.name || text === options.name || text.includes(options.name)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.name) {
|
||||
const roleLocator = page.getByRole('link', { name: options.name }).first();
|
||||
if ((await roleLocator.count()) > 0) {
|
||||
return roleLocator;
|
||||
if (options.name) {
|
||||
const roleLocator = page.getByRole('link', { name: options.name }).first();
|
||||
if ((await roleLocator.count()) > 0) {
|
||||
return roleLocator;
|
||||
}
|
||||
|
||||
const textLocator = page.locator('a', { hasText: options.name }).first();
|
||||
if ((await textLocator.count()) > 0) {
|
||||
return textLocator;
|
||||
}
|
||||
}
|
||||
|
||||
const textLocator = page.locator('a', { hasText: options.name }).first();
|
||||
if ((await textLocator.count()) > 0) {
|
||||
return textLocator;
|
||||
}
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -279,21 +286,24 @@ async function main() {
|
||||
{
|
||||
action: 'link:Stage detail',
|
||||
name: 'Detail',
|
||||
hrefIncludes: '/setup/topology/environments/stage/posture',
|
||||
hrefIncludes:
|
||||
'/setup/topology/environments/stage/posture?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d®ion=us-east&environment=stage',
|
||||
expectedPath: '/setup/topology/environments/stage/posture',
|
||||
expectQuery: { environment: 'stage', region: 'us-east' },
|
||||
},
|
||||
{
|
||||
action: 'link:Stage findings',
|
||||
name: 'Findings',
|
||||
hrefIncludes: 'environment=stage',
|
||||
hrefIncludes:
|
||||
'/security/findings?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d®ion=us-east&environment=stage',
|
||||
expectedPath: '/security/findings',
|
||||
expectQuery: { environment: 'stage', region: 'us-east' },
|
||||
},
|
||||
{
|
||||
action: 'link:Risk table open stage',
|
||||
name: 'Open',
|
||||
hrefIncludes: '/setup/topology/environments/stage/posture',
|
||||
hrefIncludes:
|
||||
'/setup/topology/environments/stage/posture?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d®ion=us-east&environment=stage',
|
||||
expectedPath: '/setup/topology/environments/stage/posture',
|
||||
expectQuery: { environment: 'stage', region: 'us-east' },
|
||||
},
|
||||
@@ -310,6 +320,7 @@ async function main() {
|
||||
{
|
||||
action: 'link:Watchlist alert',
|
||||
name: 'Identity watchlist alert requires signer review',
|
||||
hrefIncludes: 'alertId=alert-001&returnTo=%2Fmission-control%2Falerts',
|
||||
expectedPath: '/setup/trust-signing/watchlist/alerts',
|
||||
expectQuery: {
|
||||
alertId: 'alert-001',
|
||||
|
||||
@@ -15,6 +15,7 @@ const authStatePath = path.join(outputDir, 'live-ops-policy-action-sweep.state.j
|
||||
const authReportPath = path.join(outputDir, 'live-ops-policy-action-sweep.auth.json');
|
||||
const scopeQuery = 'tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d';
|
||||
const STEP_TIMEOUT_MS = 45_000;
|
||||
const ELEMENT_WAIT_MS = 8_000;
|
||||
|
||||
async function settle(page) {
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 15_000 }).catch(() => {});
|
||||
@@ -179,9 +180,55 @@ async function findNavigationTarget(page, name, index = 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function waitForNavigationTarget(page, name, index = 0, timeoutMs = ELEMENT_WAIT_MS) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const target = await findNavigationTarget(page, name, index);
|
||||
if (target) {
|
||||
return target;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function waitForButton(page, name, index = 0, timeoutMs = ELEMENT_WAIT_MS) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const locator = page.getByRole('button', { name }).nth(index);
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
if ((await locator.count()) > 0) {
|
||||
return locator;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function waitForAnyButton(page, names, timeoutMs = ELEMENT_WAIT_MS) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
for (const name of names) {
|
||||
const locator = page.getByRole('button', { name }).first();
|
||||
if ((await locator.count()) > 0) {
|
||||
return { name, locator };
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function clickLink(context, page, route, name, index = 0) {
|
||||
await navigate(page, route);
|
||||
const target = await findNavigationTarget(page, name, index);
|
||||
const target = await waitForNavigationTarget(page, name, index);
|
||||
if (!target) {
|
||||
return {
|
||||
action: `link:${name}`,
|
||||
@@ -221,8 +268,8 @@ async function clickLink(context, page, route, name, index = 0) {
|
||||
|
||||
async function clickButton(page, route, name, index = 0) {
|
||||
await navigate(page, route);
|
||||
const locator = page.getByRole('button', { name }).nth(index);
|
||||
if ((await locator.count()) === 0) {
|
||||
const locator = await waitForButton(page, name, index);
|
||||
if (!locator) {
|
||||
return {
|
||||
action: `button:${name}`,
|
||||
ok: false,
|
||||
@@ -261,13 +308,9 @@ async function clickButton(page, route, name, index = 0) {
|
||||
|
||||
async function clickFirstAvailableButton(page, route, names) {
|
||||
await navigate(page, route);
|
||||
|
||||
for (const name of names) {
|
||||
const locator = page.getByRole('button', { name }).first();
|
||||
if ((await locator.count()) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = await waitForAnyButton(page, names);
|
||||
if (target) {
|
||||
const { name, locator } = target;
|
||||
const disabledBeforeClick = await locator.isDisabled().catch(() => false);
|
||||
const startUrl = page.url();
|
||||
const downloadPromise = page.waitForEvent('download', { timeout: 4_000 }).catch(() => null);
|
||||
|
||||
Reference in New Issue
Block a user