Skip to content
Open
Show file tree
Hide file tree
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
13 changes: 13 additions & 0 deletions src/browser/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,19 @@ describe('Page active target tracking', () => {
}));
});

it('forwards waitUntil to the tabs/new command', async () => {
sendCommandFullMock.mockResolvedValueOnce({ data: {}, page: 'page-2' });

const page = new Page('default');
await page.newTab?.('https://second.example', { waitUntil: 'none' });

expect(sendCommandFullMock).toHaveBeenCalledWith('tabs', expect.objectContaining({
op: 'new',
url: 'https://second.example',
waitUntil: 'none',
}));
});

it('closes a tab by explicit page identity', async () => {
sendCommandMock.mockResolvedValueOnce({ closed: 'page-2' });

Expand Down
3 changes: 2 additions & 1 deletion src/browser/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,11 @@ export class Page extends BasePage {
return Array.isArray(result) ? result : [];
}

async newTab(url?: string): Promise<string | undefined> {
async newTab(url?: string, options?: { waitUntil?: 'load' | 'none' }): Promise<string | undefined> {
const result = await sendCommandFull('tabs', {
op: 'new',
...(url !== undefined && { url }),
...(options?.waitUntil && { waitUntil: options.waitUntil }),
...this._sessionOpts(),
});
this._lastUrl = null;
Expand Down
8 changes: 3 additions & 5 deletions src/browser/runtime/local-cloak/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { redactText, redactUrl } from '../../../observation/redaction.js';
import { articleHtmlToMarkdown } from '../../../download/article-download.js';
import { waitForDownload } from './downloads.js';
import type { CloakSessionManager } from './session-manager.js';
import { toGotoWaitUntil, type CloakSessionManager } from './session-manager.js';
import type { BrowserContext, Frame, Page as PlaywrightPage } from 'playwright-core';
import { runBrowserProgram } from '../../run/runner.js';
import { BROWSER_RUN_MAX_SOURCE_BYTES } from '../../run/types.js';
Expand Down Expand Up @@ -182,10 +182,7 @@ export async function dispatchCloakAction(manager: CloakSessionManager, command:
case 'navigate': {
if (!command.url) return invalidRequest(command, 'Missing url');
const lease = await resolveLease(manager, command);
// 'none' maps to Playwright's 'commit': sites that stream analytics forever
// never fire the load event, so adapters gating readiness on their own
// selector waits must be able to skip it.
await lease.page.goto(command.url, { waitUntil: command.waitUntil === 'none' ? 'commit' : 'load' });
await lease.page.goto(command.url, { waitUntil: toGotoWaitUntil(command.waitUntil) });
return { id: command.id, ok: true, data: { title: await lease.page.title(), url: lease.page.url(), timedOut: false }, page: lease.pageId };
}
case 'exec': {
Expand Down Expand Up @@ -336,6 +333,7 @@ export async function dispatchCloakAction(manager: CloakSessionManager, command:
siteSession: command.siteSession,
idleTimeout: command.idleTimeout,
url: command.url,
waitUntil: command.waitUntil,
windowMode: command.windowMode,
});
return { id: command.id, ok: true, data: { title: await lease.page.title(), url: lease.page.url() }, page: lease.pageId };
Expand Down
16 changes: 16 additions & 0 deletions src/browser/runtime/local-cloak/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ describe('LocalCloakRuntimeProvider', () => {
expect(page.goto).toHaveBeenCalledWith('https://example.com/', expect.objectContaining({ waitUntil: 'commit' }));
});

it("maps waitUntil 'none' to a commit-only wait when opening a tab", async () => {
const { provider, pages } = makeProviderWithFakePage();
const result = await provider.dispatch({
id: 'new',
action: 'tabs',
op: 'new',
session: 'work',
surface: 'browser',
url: 'https://second.example/',
waitUntil: 'none',
profileId: 'default',
});
expect(result).toMatchObject({ id: 'new', ok: true, page: expect.any(String) });
expect(pages[1].goto).toHaveBeenCalledWith('https://second.example/', expect.objectContaining({ waitUntil: 'commit' }));
});

it('evaluates JavaScript in the resolved page', async () => {
const { provider } = makeProviderWithFakePage();
const nav = await provider.dispatch({ id: 'nav', action: 'navigate', session: 'work', surface: 'browser', url: 'https://example.com/', profileId: 'default' });
Expand Down
15 changes: 13 additions & 2 deletions src/browser/runtime/local-cloak/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ export function resolveCloakBrowserVersion(): string | undefined {
}
}

/**
* Map the protocol's navigation wait condition onto Playwright's `goto` option.
* 'none' becomes 'commit': sites that stream analytics forever never fire the
* load event, so callers gating readiness on their own selector waits must be
* able to skip it. Every `goto` in this runtime routes through here so a new
* call site cannot quietly reintroduce a hardcoded 'load'.
*/
export function toGotoWaitUntil(waitUntil?: 'load' | 'none'): 'load' | 'commit' {
return waitUntil === 'none' ? 'commit' : 'load';
}

export type LaunchPersistentContext = typeof cloakLaunchPersistentContext;
export type RecoverLockedProfile = (userDataDir: string) => Promise<boolean>;

Expand Down Expand Up @@ -257,7 +268,7 @@ export class CloakSessionManager {
})));
}

async newPage(input: SessionKeyInput & { url?: string }): Promise<CloakPageLease> {
async newPage(input: SessionKeyInput & { url?: string; waitUntil?: 'load' | 'none' }): Promise<CloakPageLease> {
const profileId = normalizeProfileId(input.profileId);
const session = requireSession(input.session);
const surface = normalizeSurface(input.surface);
Expand All @@ -269,7 +280,7 @@ export class CloakSessionManager {
);
if (input.url) {
try {
await acquired.page.goto(input.url, { waitUntil: 'load' });
await acquired.page.goto(input.url, { waitUntil: toGotoWaitUntil(input.waitUntil) });
} catch (error) {
if (!pageIsClosed(acquired.page)) await acquired.page.close().catch(() => {});
throw error;
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export interface IPage {
waitForDownload?(pattern?: string, timeoutMs?: number): Promise<BrowserDownloadWaitResult>;
tabs(): Promise<any>;
closeTab?(target?: number | string): Promise<void>;
newTab?(url?: string): Promise<string | undefined>;
newTab?(url?: string, options?: { waitUntil?: 'load' | 'none' }): Promise<string | undefined>;
selectTab(target: number | string): Promise<void>;
networkRequests(includeStatic?: boolean): Promise<any>;
consoleMessages(level?: string): Promise<any>;
Expand Down