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
20 changes: 16 additions & 4 deletions src/browser/runtime/local-cloak/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ function invalidRequest(command: BrowserRuntimeCommand, error: string): BrowserR
return { id: command.id, ok: false, errorCode: 'invalid_request', error };
}

/**
* Translate the command vocabulary ('load' | 'none') into Playwright's for a
* `page.goto` call. 'none' maps to '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.
*
* Every Playwright-backed navigation in this runtime goes through here, so a
* future waitUntil value reaches all of them at once — the hardcoded literal at
* the second call site is what left `tab new --url` hanging after #106/#107.
*/
function toGotoWaitUntil(waitUntil: BrowserRuntimeCommand['waitUntil']): 'load' | 'commit' {
return waitUntil === 'none' ? 'commit' : 'load';
}

async function resolveLease(manager: CloakSessionManager, command: BrowserRuntimeCommand) {
const profileId = resolveCloakCommandProfileId(manager, command);
if (command.page) {
Expand Down Expand Up @@ -204,9 +218,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 +233,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 +400,7 @@ export async function dispatchCloakAction(manager: CloakSessionManager, command:
runId: command.runId,
idleTimeout: command.idleTimeout,
url: command.url,
waitUntil: toGotoWaitUntil(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
79 changes: 79 additions & 0 deletions src/browser/runtime/local-cloak/session-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1531,3 +1531,82 @@ describe('CloakSessionManager', () => {
expect(launchPersistentContext).toHaveBeenCalledTimes(2);
});
});

describe('waitUntil plumbing', () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

function managerWithPage() {
const launched = fakeContext();
launched.context.newPage.mockResolvedValue(launched.page);
const manager = new CloakSessionManager({
baseDir: '/tmp/webcmd-test',
launchPersistentContext: vi.fn().mockResolvedValue(launched.context),
});
return { manager, page: launched.page };
}

// 'none' has to reach Playwright as 'commit'. Waiting for 'load' on a site that
// never goes idle is the hang #106 was filed about.
it('maps waitUntil none to commit when opening a tab with a url', async () => {
const { manager, page } = managerWithPage();

await dispatchCloakAction(manager, {
id: 'cmd-tab-none',
action: 'tabs',
op: 'new',
session: 'work',
surface: 'browser',
url: 'https://example.com/',
waitUntil: 'none',
});

expect(page.goto).toHaveBeenCalledWith('https://example.com/', { waitUntil: 'commit' });
});

it('defaults a tab opened without waitUntil to load', async () => {
const { manager, page } = managerWithPage();

await dispatchCloakAction(manager, {
id: 'cmd-tab-default',
action: 'tabs',
op: 'new',
session: 'work',
surface: 'browser',
url: 'https://example.com/',
});

expect(page.goto).toHaveBeenCalledWith('https://example.com/', { waitUntil: 'load' });
});

it('still maps waitUntil none to commit on navigate', async () => {
const { manager, page } = managerWithPage();

await dispatchCloakAction(manager, {
id: 'cmd-navigate-none',
action: 'navigate',
session: 'work',
surface: 'browser',
url: 'https://example.com/',
waitUntil: 'none',
});

expect(page.goto).toHaveBeenCalledWith('https://example.com/', { waitUntil: 'commit' });
});

it('defaults navigate without waitUntil to load', async () => {
const { manager, page } = managerWithPage();

await dispatchCloakAction(manager, {
id: 'cmd-navigate-default',
action: 'navigate',
session: 'work',
surface: 'browser',
url: 'https://example.com/',
});

expect(page.goto).toHaveBeenCalledWith('https://example.com/', { waitUntil: 'load' });
});
});
16 changes: 13 additions & 3 deletions src/browser/runtime/local-cloak/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ export interface SessionKeyInput {
freshPage?: boolean;
}

export type NewPageInput = SessionKeyInput & {
url?: string;
/**
* Playwright `goto` readiness for `url`, already translated from the command
* vocabulary by the caller. Defaults to 'load'; 'commit' skips waiting for the
* load event on sites that never go idle.
*/
waitUntil?: 'load' | 'commit';
};

type PageEntry = {
page: PlaywrightPage;
pageId: string;
Expand Down Expand Up @@ -414,15 +424,15 @@ export class CloakSessionManager {
})));
}

async newPage(input: SessionKeyInput & { url?: string }): Promise<CloakPageLease> {
async newPage(input: NewPageInput): 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: NewPageInput, attempt: number): Promise<CloakPageLease> {
const profileId = normalizeProfileId(input.profileId);
const session = requireSession(input.session);
const sessionId = requireSessionId(input);
Expand All @@ -433,7 +443,7 @@ export class CloakSessionManager {
});
if (input.url) {
try {
await acquired.page.goto(input.url, { waitUntil: 'load' });
await acquired.page.goto(input.url, { waitUntil: input.waitUntil ?? 'load' });
} catch (error) {
if (attempt === 0 && isClosedContextError(error)) {
this.invalidateProfileRuntime(profileId, acquired.runtime);
Expand Down