Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions tests/WaitForHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,33 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';

import {getNetworkMultiplierFromString} from '../src/WaitForHelper.js';

import {serverHooks} from './server.js';
import {html, withMcpContext} from './utils.js';

describe('getNetworkMultiplierFromString', () => {
it('maps each predefined network condition to its multiplier', () => {
assert.strictEqual(getNetworkMultiplierFromString('Fast 4G'), 1);
assert.strictEqual(getNetworkMultiplierFromString('Slow 4G'), 2.5);
assert.strictEqual(getNetworkMultiplierFromString('Fast 3G'), 5);
assert.strictEqual(getNetworkMultiplierFromString('Slow 3G'), 10);
});

it('falls back to 1 for unknown condition strings', () => {
assert.strictEqual(getNetworkMultiplierFromString('2G'), 1);
assert.strictEqual(getNetworkMultiplierFromString('No emulation'), 1);
assert.strictEqual(getNetworkMultiplierFromString(''), 1);
});

it('falls back to 1 when no condition is set', () => {
assert.strictEqual(getNetworkMultiplierFromString(null), 1);
});
});

describe('WaitForHelper', () => {
const server = serverHooks();

it('does not stall when an action opens a dialog without handleDialog', async () => {
await withMcpContext(async (response, context) => {
const mcpPage = context.getSelectedMcpPage();
Expand Down Expand Up @@ -43,4 +67,125 @@ describe('WaitForHelper', () => {
assert.throws(() => mcpPage.throwIfDialogOpen());
});
});

it('reports navigatedToUrl when the action starts a cross-document navigation', async () => {
await withMcpContext(async (response, context) => {
const mcpPage = context.getSelectedMcpPage();
server.addHtmlRoute('/target.html', html`<h1>target</h1>`);
const targetUrl = server.getRoute('/target.html');

const result = await mcpPage.waitForEventsAfterAction(async () => {
await mcpPage.pptrPage.evaluate(url => {
// Navigate asynchronously so the evaluate call returns before the
// execution context is destroyed by the navigation.
setTimeout(() => {
window.location.href = url;
}, 0);
}, targetUrl);
});

assert.strictEqual(result.navigatedToUrl, targetUrl);
assert.strictEqual(result.dialogHandled, false);
assert.strictEqual(mcpPage.pptrPage.url(), targetUrl);
});
});

it('resolves quickly without navigatedToUrl when no navigation happens', async () => {
await withMcpContext(async (response, context) => {
const mcpPage = context.getSelectedMcpPage();
await mcpPage.pptrPage.setContent(html`<div id="root"></div>`);

const start = Date.now();
const result = await mcpPage.waitForEventsAfterAction(async () => {
await mcpPage.pptrPage.evaluate(() => {
document.querySelector('#root')!.append('done');
});
});
const elapsed = Date.now() - start;

assert.strictEqual(result.navigatedToUrl, undefined);
assert.strictEqual(result.dialogHandled, false);
// Expected wait is ~200ms (#expectNavigationIn 100ms + #stableDomFor
// 100ms). Assert we stayed below #stableDomTimeout/#navigationTimeout
// (3s each) to prove neither full timeout was consumed.
assert.ok(
elapsed < 2_000,
`expected a fast return without navigation, took ${elapsed}ms`,
);
});
});

it('swallows navigation timeouts and still resolves with a result', async () => {
await withMcpContext(async (response, context) => {
const mcpPage = context.getSelectedMcpPage();
server.addRoute('/hang.html', () => {
// Never respond so the started navigation cannot complete.
});
const hangUrl = server.getRoute('/hang.html');

const start = Date.now();
const result = await mcpPage.waitForEventsAfterAction(
async () => {
await mcpPage.pptrPage.evaluate(url => {
setTimeout(() => {
window.location.href = url;
}, 0);
}, hangUrl);
},
{timeout: 500},
);
const elapsed = Date.now() - start;

// The navigation started but timed out; the timeout error is logged and
// swallowed rather than thrown, and the pending navigation never
// committed, so the URL is unchanged and no navigatedToUrl is reported.
assert.strictEqual(result.navigatedToUrl, undefined);
assert.strictEqual(result.dialogHandled, false);
// Current behavior: the total wait is the 500ms navigation timeout plus
// the full 3s #stableDomTimeout, because the stable-DOM evaluation does
// not settle while the navigation is still pending (~3.5s in total).
// Assert an upper bound well below protocolTimeout-scale hangs.
assert.ok(
elapsed < 8_000,
`expected the wait to be bounded by the internal timeouts, took ${elapsed}ms`,
);

// Note: no in-page cleanup here. Evaluations do not settle while the
// navigation is pending; the harness closes the page during teardown.
Comment on lines +153 to +154

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Note: no in-page cleanup here. Evaluations do not settle while the
// navigation is pending; the harness closes the page during teardown.

});
});

it('reports same-document hash navigations via the URL comparison', async () => {
await withMcpContext(async (response, context) => {
const mcpPage = context.getSelectedMcpPage();
server.addHtmlRoute('/page.html', html`<p>content</p>`);
const pageUrl = server.getRoute('/page.html');
await mcpPage.pptrPage.goto(pageUrl);

const result = await mcpPage.waitForEventsAfterAction(async () => {
await mcpPage.pptrPage.evaluate(() => {
window.location.hash = '#section';
});
});

// Same-document navigations skip the full navigation wait but are still
// surfaced through the before/after URL comparison.
Comment on lines +171 to +172

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Same-document navigations skip the full navigation wait but are still
// surfaced through the before/after URL comparison.

assert.strictEqual(result.navigatedToUrl, `${pageUrl}#section`);
assert.strictEqual(result.dialogHandled, false);
});
});

it('rethrows errors from the action', async () => {
await withMcpContext(async (response, context) => {
const mcpPage = context.getSelectedMcpPage();
await mcpPage.pptrPage.setContent(html`<p>content</p>`);

await assert.rejects(
mcpPage.waitForEventsAfterAction(() =>
Promise.reject(new Error('action failed')),
),
/action failed/,
);
});
});
});
Loading