Skip to content
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/browser/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,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 @@ -227,10 +227,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 @@ -204,9 +204,6 @@ export async function dispatchCloakAction(manager: CloakSessionManager, command:
case 'navigate': {
if (!command.url) return invalidRequest(command, 'Missing url');
const profileId = resolveCloakCommandProfileId(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.
const lease = await manager.navigatePage(
{
profileId,
Expand All @@ -222,7 +219,7 @@ export async function dispatchCloakAction(manager: CloakSessionManager, command:
windowMode: command.windowMode,
},
command.url,
command.waitUntil === 'none' ? 'commit' : 'load',
toGotoWaitUntil(command.waitUntil),
);
return { id: command.id, ok: true, data: { title: await lease.page.title(), url: lease.page.url(), timedOut: false }, page: lease.pageId };
}
Expand Down Expand Up @@ -389,6 +386,7 @@ export async function dispatchCloakAction(manager: CloakSessionManager, command:
runId: command.runId,
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 @@ -270,6 +270,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[0].goto).toHaveBeenCalledWith('https://second.example/', expect.objectContaining({ waitUntil: 'commit' }));
});

it('does not execute a queued command after its daemon deadline expires', async () => {
const { provider, page } = makeProviderWithFakePage();
let releaseFirst!: () => void;
Expand Down
17 changes: 14 additions & 3 deletions src/browser/runtime/local-cloak/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ export function resolveCloakBrowserVersion(): string | undefined {
return cachedCloakBrowserVersion;
}

/**
* 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 @@ -414,15 +425,15 @@ export class CloakSessionManager {
})));
}

async newPage(input: SessionKeyInput & { url?: string }): Promise<CloakPageLease> {
async newPage(input: SessionKeyInput & { url?: string; waitUntil?: 'load' | 'none' }): Promise<CloakPageLease> {
return this.newPageAttempt(input, 0);
}

async navigatePage(input: SessionKeyInput, url: string, waitUntil: 'load' | 'commit'): Promise<CloakPageLease> {
return this.navigatePageAttempt(input, url, waitUntil, 0);
}

private async newPageAttempt(input: SessionKeyInput & { url?: string }, attempt: number): Promise<CloakPageLease> {
private async newPageAttempt(input: SessionKeyInput & { url?: string; waitUntil?: 'load' | 'none' }, attempt: number): Promise<CloakPageLease> {
const profileId = normalizeProfileId(input.profileId);
const session = requireSession(input.session);
const sessionId = requireSessionId(input);
Expand All @@ -433,7 +444,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 (attempt === 0 && isClosedContextError(error)) {
this.invalidateProfileRuntime(profileId, acquired.runtime);
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