mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
improvements
This commit is contained in:
parent
042ffc3410
commit
77883774b4
3 changed files with 103 additions and 38 deletions
|
|
@ -53,7 +53,7 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
switch (input.action) {
|
switch (input.action) {
|
||||||
case 'open': {
|
case 'open': {
|
||||||
await browserViewManager.ensureActiveTabReady(signal);
|
await browserViewManager.ensureActiveTabReady(signal);
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult('open', 'Opened the browser pane.', page);
|
return buildSuccessResult('open', 'Opened the browser pane.', page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
return buildErrorResult('new-tab', result.error ?? 'Failed to open a new tab.');
|
return buildErrorResult('new-tab', result.error ?? 'Failed to open a new tab.');
|
||||||
}
|
}
|
||||||
await browserViewManager.ensureActiveTabReady(signal);
|
await browserViewManager.ensureActiveTabReady(signal);
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult(
|
return buildSuccessResult(
|
||||||
'new-tab',
|
'new-tab',
|
||||||
target ? `Opened a new tab for ${target}.` : 'Opened a new tab.',
|
target ? `Opened a new tab for ${target}.` : 'Opened a new tab.',
|
||||||
|
|
@ -85,7 +85,7 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
return buildErrorResult('switch-tab', `No browser tab exists with id ${tabId}.`);
|
return buildErrorResult('switch-tab', `No browser tab exists with id ${tabId}.`);
|
||||||
}
|
}
|
||||||
await browserViewManager.ensureActiveTabReady(signal);
|
await browserViewManager.ensureActiveTabReady(signal);
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult('switch-tab', `Switched to tab ${tabId}.`, page);
|
return buildSuccessResult('switch-tab', `Switched to tab ${tabId}.`, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
return buildErrorResult('close-tab', `Could not close tab ${tabId}.`);
|
return buildErrorResult('close-tab', `Could not close tab ${tabId}.`);
|
||||||
}
|
}
|
||||||
await browserViewManager.ensureActiveTabReady(signal);
|
await browserViewManager.ensureActiveTabReady(signal);
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult('close-tab', `Closed tab ${tabId}.`, page);
|
return buildSuccessResult('close-tab', `Closed tab ${tabId}.`, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
return buildErrorResult('navigate', result.error ?? `Failed to navigate to ${target}.`);
|
return buildErrorResult('navigate', result.error ?? `Failed to navigate to ${target}.`);
|
||||||
}
|
}
|
||||||
await browserViewManager.ensureActiveTabReady(signal);
|
await browserViewManager.ensureActiveTabReady(signal);
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult('navigate', `Navigated to ${target}.`, page);
|
return buildSuccessResult('navigate', `Navigated to ${target}.`, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +124,7 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
return buildErrorResult('back', 'The active tab cannot go back.');
|
return buildErrorResult('back', 'The active tab cannot go back.');
|
||||||
}
|
}
|
||||||
await browserViewManager.ensureActiveTabReady(signal);
|
await browserViewManager.ensureActiveTabReady(signal);
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult('back', 'Went back in the active tab.', page);
|
return buildSuccessResult('back', 'Went back in the active tab.', page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,14 +134,14 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
return buildErrorResult('forward', 'The active tab cannot go forward.');
|
return buildErrorResult('forward', 'The active tab cannot go forward.');
|
||||||
}
|
}
|
||||||
await browserViewManager.ensureActiveTabReady(signal);
|
await browserViewManager.ensureActiveTabReady(signal);
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult('forward', 'Went forward in the active tab.', page);
|
return buildSuccessResult('forward', 'Went forward in the active tab.', page);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'reload': {
|
case 'reload': {
|
||||||
browserViewManager.reload();
|
browserViewManager.reload();
|
||||||
await browserViewManager.ensureActiveTabReady(signal);
|
await browserViewManager.ensureActiveTabReady(signal);
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult('reload', 'Reloaded the active tab.', page);
|
return buildSuccessResult('reload', 'Reloaded the active tab.', page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
return buildErrorResult('click', result.error ?? 'Failed to click the requested element.');
|
return buildErrorResult('click', result.error ?? 'Failed to click the requested element.');
|
||||||
}
|
}
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult(
|
return buildSuccessResult(
|
||||||
'click',
|
'click',
|
||||||
result.description ? `Clicked ${result.description}.` : 'Clicked the requested element.',
|
result.description ? `Clicked ${result.description}.` : 'Clicked the requested element.',
|
||||||
|
|
@ -196,7 +196,7 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
return buildErrorResult('type', result.error ?? 'Failed to type into the requested element.');
|
return buildErrorResult('type', result.error ?? 'Failed to type into the requested element.');
|
||||||
}
|
}
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult(
|
return buildSuccessResult(
|
||||||
'type',
|
'type',
|
||||||
result.description ? `Typed into ${result.description}.` : 'Typed into the requested element.',
|
result.description ? `Typed into ${result.description}.` : 'Typed into the requested element.',
|
||||||
|
|
@ -221,7 +221,7 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
return buildErrorResult('press', result.error ?? `Failed to press ${key}.`);
|
return buildErrorResult('press', result.error ?? `Failed to press ${key}.`);
|
||||||
}
|
}
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult(
|
return buildSuccessResult(
|
||||||
'press',
|
'press',
|
||||||
result.description ? `Pressed ${result.description}.` : `Pressed ${key}.`,
|
result.description ? `Pressed ${result.description}.` : `Pressed ${key}.`,
|
||||||
|
|
@ -238,14 +238,14 @@ export class ElectronBrowserControlService implements IBrowserControlService {
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
return buildErrorResult('scroll', result.error ?? 'Failed to scroll the page.');
|
return buildErrorResult('scroll', result.error ?? 'Failed to scroll the page.');
|
||||||
}
|
}
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult('scroll', `Scrolled ${input.direction ?? 'down'}.`, page);
|
return buildSuccessResult('scroll', `Scrolled ${input.direction ?? 'down'}.`, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'wait': {
|
case 'wait': {
|
||||||
const duration = input.ms ?? 1000;
|
const duration = input.ms ?? 1000;
|
||||||
await browserViewManager.wait(duration, signal);
|
await browserViewManager.wait(duration, signal);
|
||||||
const page = await browserViewManager.readPageSummary(signal) ?? undefined;
|
const page = await browserViewManager.readPageSummary(signal, { waitForReady: false }) ?? undefined;
|
||||||
return buildSuccessResult('wait', `Waited ${duration}ms for the page to settle.`, page);
|
return buildSuccessResult('wait', `Waited ${duration}ms for the page to settle.`, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
const SEARCH_ENGINE_BASE_URL = 'https://www.google.com/search?q=';
|
const SEARCH_ENGINE_BASE_URL = 'https://www.google.com/search?q=';
|
||||||
|
|
||||||
|
const HAS_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
|
||||||
|
const IPV4_HOST_RE = /^\d{1,3}(?:\.\d{1,3}){3}(?::\d+)?(?:\/.*)?$/;
|
||||||
|
const LOCALHOST_RE = /^localhost(?::\d+)?(?:\/.*)?$/i;
|
||||||
|
const DOMAIN_LIKE_RE = /^[\w.-]+\.[a-z]{2,}(?::\d+)?(?:\/.*)?$/i;
|
||||||
|
|
||||||
export function normalizeNavigationTarget(target: string): string {
|
export function normalizeNavigationTarget(target: string): string {
|
||||||
const trimmed = target.trim();
|
const trimmed = target.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
|
|
@ -16,17 +21,20 @@ export function normalizeNavigationTarget(target: string): string {
|
||||||
throw new Error('That URL scheme is not allowed in the embedded browser.');
|
throw new Error('That URL scheme is not allowed in the embedded browser.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) {
|
if (HAS_SCHEME_RE.test(trimmed)) {
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
const looksLikeHost =
|
const looksLikeHost =
|
||||||
trimmed.startsWith('localhost')
|
LOCALHOST_RE.test(trimmed)
|
||||||
|| /^[\w.-]+\.[a-z]{2,}/i.test(trimmed)
|
|| DOMAIN_LIKE_RE.test(trimmed)
|
||||||
|| /^\d{1,3}(?:\.\d{1,3}){3}(?::\d+)?(?:\/.*)?$/.test(trimmed);
|
|| IPV4_HOST_RE.test(trimmed);
|
||||||
|
|
||||||
if (looksLikeHost && !/\s/.test(trimmed)) {
|
if (looksLikeHost && !/\s/.test(trimmed)) {
|
||||||
return trimmed;
|
const scheme = LOCALHOST_RE.test(trimmed) || IPV4_HOST_RE.test(trimmed)
|
||||||
|
? 'http://'
|
||||||
|
: 'https://';
|
||||||
|
return `${scheme}${trimmed}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${SEARCH_ENGINE_BASE_URL}${encodeURIComponent(trimmed)}`;
|
return `${SEARCH_ENGINE_BASE_URL}${encodeURIComponent(trimmed)}`;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import { BrowserWindow, WebContentsView, session, shell, type Session, type WebContents } from 'electron';
|
import { BrowserWindow, WebContentsView, session, shell, type Session } from 'electron';
|
||||||
import type {
|
import type {
|
||||||
BrowserPageElement,
|
BrowserPageElement,
|
||||||
BrowserPageSnapshot,
|
BrowserPageSnapshot,
|
||||||
|
|
@ -202,6 +202,8 @@ export interface BrowserBounds {
|
||||||
type BrowserTab = {
|
type BrowserTab = {
|
||||||
id: string;
|
id: string;
|
||||||
view: WebContentsView;
|
view: WebContentsView;
|
||||||
|
domReadyAt: number | null;
|
||||||
|
loadError: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CachedSnapshot = {
|
type CachedSnapshot = {
|
||||||
|
|
@ -483,7 +485,7 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
return /^https?:\/\//i.test(url) || url === 'about:blank';
|
return /^https?:\/\//i.test(url) || url === 'about:blank';
|
||||||
}
|
}
|
||||||
|
|
||||||
private createView(tabId: string): WebContentsView {
|
private createView(): WebContentsView {
|
||||||
const view = new WebContentsView({
|
const view = new WebContentsView({
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
session: this.getSession(),
|
session: this.getSession(),
|
||||||
|
|
@ -494,11 +496,11 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
});
|
});
|
||||||
|
|
||||||
view.webContents.setUserAgent(SPOOF_UA);
|
view.webContents.setUserAgent(SPOOF_UA);
|
||||||
this.wireEvents(tabId, view);
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
private wireEvents(tabId: string, view: WebContentsView): void {
|
private wireEvents(tab: BrowserTab): void {
|
||||||
|
const { id: tabId, view } = tab;
|
||||||
const wc = view.webContents;
|
const wc = view.webContents;
|
||||||
|
|
||||||
const reapplyBounds = () => {
|
const reapplyBounds = () => {
|
||||||
|
|
@ -517,17 +519,40 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
this.emitState();
|
this.emitState();
|
||||||
};
|
};
|
||||||
|
|
||||||
wc.on('did-start-navigation', () => {
|
wc.on('did-start-navigation', (_event, _url, _isInPlace, isMainFrame) => {
|
||||||
|
if (isMainFrame !== false) {
|
||||||
|
tab.domReadyAt = null;
|
||||||
|
tab.loadError = null;
|
||||||
|
}
|
||||||
this.invalidateSnapshot(tabId);
|
this.invalidateSnapshot(tabId);
|
||||||
reapplyBounds();
|
reapplyBounds();
|
||||||
});
|
});
|
||||||
wc.on('did-navigate', () => { reapplyBounds(); invalidateAndEmit(); });
|
wc.on('did-navigate', () => { reapplyBounds(); invalidateAndEmit(); });
|
||||||
wc.on('did-navigate-in-page', () => { reapplyBounds(); invalidateAndEmit(); });
|
wc.on('did-navigate-in-page', () => { reapplyBounds(); invalidateAndEmit(); });
|
||||||
wc.on('did-start-loading', () => { this.invalidateSnapshot(tabId); reapplyBounds(); this.emitState(); });
|
wc.on('did-start-loading', () => {
|
||||||
|
tab.loadError = null;
|
||||||
|
this.invalidateSnapshot(tabId);
|
||||||
|
reapplyBounds();
|
||||||
|
this.emitState();
|
||||||
|
});
|
||||||
wc.on('did-stop-loading', () => { reapplyBounds(); invalidateAndEmit(); });
|
wc.on('did-stop-loading', () => { reapplyBounds(); invalidateAndEmit(); });
|
||||||
wc.on('did-finish-load', () => { reapplyBounds(); invalidateAndEmit(); });
|
wc.on('did-finish-load', () => { reapplyBounds(); invalidateAndEmit(); });
|
||||||
|
wc.on('dom-ready', () => {
|
||||||
|
tab.domReadyAt = Date.now();
|
||||||
|
reapplyBounds();
|
||||||
|
invalidateAndEmit();
|
||||||
|
});
|
||||||
wc.on('did-frame-finish-load', reapplyBounds);
|
wc.on('did-frame-finish-load', reapplyBounds);
|
||||||
wc.on('did-fail-load', () => { reapplyBounds(); invalidateAndEmit(); });
|
wc.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||||
|
if (isMainFrame && errorCode !== -3) {
|
||||||
|
const target = validatedURL || wc.getURL() || 'page';
|
||||||
|
tab.loadError = errorDescription
|
||||||
|
? `Failed to load ${target}: ${errorDescription}.`
|
||||||
|
: `Failed to load ${target}.`;
|
||||||
|
}
|
||||||
|
reapplyBounds();
|
||||||
|
invalidateAndEmit();
|
||||||
|
});
|
||||||
wc.on('page-title-updated', this.emitState.bind(this));
|
wc.on('page-title-updated', this.emitState.bind(this));
|
||||||
|
|
||||||
wc.setWindowOpenHandler(({ url }) => {
|
wc.setWindowOpenHandler(({ url }) => {
|
||||||
|
|
@ -593,9 +618,12 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
const tabId = randomUUID();
|
const tabId = randomUUID();
|
||||||
const tab: BrowserTab = {
|
const tab: BrowserTab = {
|
||||||
id: tabId,
|
id: tabId,
|
||||||
view: this.createView(tabId),
|
view: this.createView(),
|
||||||
|
domReadyAt: null,
|
||||||
|
loadError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.wireEvents(tab);
|
||||||
this.tabs.set(tabId, tab);
|
this.tabs.set(tabId, tab);
|
||||||
this.tabOrder.push(tabId);
|
this.tabOrder.push(tabId);
|
||||||
this.activeTabId = tabId;
|
this.activeTabId = tabId;
|
||||||
|
|
@ -607,7 +635,10 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
initialUrl === 'about:blank'
|
initialUrl === 'about:blank'
|
||||||
? HOME_URL
|
? HOME_URL
|
||||||
: normalizeNavigationTarget(initialUrl);
|
: normalizeNavigationTarget(initialUrl);
|
||||||
void tab.view.webContents.loadURL(targetUrl).catch(() => {
|
void tab.view.webContents.loadURL(targetUrl).catch((error) => {
|
||||||
|
tab.loadError = error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: `Failed to load ${targetUrl}.`;
|
||||||
this.emitState();
|
this.emitState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -629,17 +660,29 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForWebContentsSettle(
|
private async waitForWebContentsSettle(
|
||||||
wc: WebContents,
|
tab: BrowserTab,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
idleMs = POST_ACTION_IDLE_MS,
|
idleMs = POST_ACTION_IDLE_MS,
|
||||||
timeoutMs = NAVIGATION_TIMEOUT_MS,
|
timeoutMs = NAVIGATION_TIMEOUT_MS,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const wc = tab.view.webContents;
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
let sawLoading = wc.isLoading();
|
let sawLoading = wc.isLoading();
|
||||||
|
|
||||||
while (Date.now() - startedAt < timeoutMs) {
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
abortIfNeeded(signal);
|
abortIfNeeded(signal);
|
||||||
if (wc.isDestroyed()) return;
|
if (wc.isDestroyed()) return;
|
||||||
|
if (tab.loadError) {
|
||||||
|
throw new Error(tab.loadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab.domReadyAt != null) {
|
||||||
|
const domReadyForMs = Date.now() - tab.domReadyAt;
|
||||||
|
const requiredIdleMs = sawLoading ? idleMs : Math.min(idleMs, 200);
|
||||||
|
if (domReadyForMs >= requiredIdleMs) return;
|
||||||
|
await sleep(Math.min(100, requiredIdleMs - domReadyForMs), signal);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (wc.isLoading()) {
|
if (wc.isLoading()) {
|
||||||
sawLoading = true;
|
sawLoading = true;
|
||||||
|
|
@ -648,15 +691,24 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
await sleep(sawLoading ? idleMs : Math.min(idleMs, 200), signal);
|
await sleep(sawLoading ? idleMs : Math.min(idleMs, 200), signal);
|
||||||
if (!wc.isLoading()) return;
|
if (tab.loadError) {
|
||||||
|
throw new Error(tab.loadError);
|
||||||
|
}
|
||||||
|
if (!wc.isLoading() || tab.domReadyAt != null) return;
|
||||||
sawLoading = true;
|
sawLoading = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeOnActiveTab<T>(script: string, signal?: AbortSignal): Promise<T> {
|
private async executeOnActiveTab<T>(
|
||||||
|
script: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
options?: { waitForReady?: boolean },
|
||||||
|
): Promise<T> {
|
||||||
abortIfNeeded(signal);
|
abortIfNeeded(signal);
|
||||||
const activeTab = this.getActiveTab() ?? this.ensureInitialTab();
|
const activeTab = this.getActiveTab() ?? this.ensureInitialTab();
|
||||||
await this.waitForWebContentsSettle(activeTab.view.webContents, signal);
|
if (options?.waitForReady !== false) {
|
||||||
|
await this.waitForWebContentsSettle(activeTab, signal);
|
||||||
|
}
|
||||||
abortIfNeeded(signal);
|
abortIfNeeded(signal);
|
||||||
return activeTab.view.webContents.executeJavaScript(script, true) as Promise<T>;
|
return activeTab.view.webContents.executeJavaScript(script, true) as Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
@ -734,7 +786,7 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
|
|
||||||
async ensureActiveTabReady(signal?: AbortSignal): Promise<void> {
|
async ensureActiveTabReady(signal?: AbortSignal): Promise<void> {
|
||||||
const activeTab = this.getActiveTab() ?? this.ensureInitialTab();
|
const activeTab = this.getActiveTab() ?? this.ensureInitialTab();
|
||||||
await this.waitForWebContentsSettle(activeTab.view.webContents, signal);
|
await this.waitForWebContentsSettle(activeTab, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
async newTab(rawUrl?: string): Promise<{ ok: boolean; tabId?: string; error?: string }> {
|
async newTab(rawUrl?: string): Promise<{ ok: boolean; tabId?: string; error?: string }> {
|
||||||
|
|
@ -820,7 +872,7 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async readPage(
|
async readPage(
|
||||||
options?: { maxElements?: number; maxTextLength?: number },
|
options?: { maxElements?: number; maxTextLength?: number; waitForReady?: boolean },
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<{ ok: boolean; page?: BrowserPageSnapshot; error?: string }> {
|
): Promise<{ ok: boolean; page?: BrowserPageSnapshot; error?: string }> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -831,6 +883,7 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
options?.maxTextLength ?? DEFAULT_READ_MAX_TEXT_LENGTH,
|
options?.maxTextLength ?? DEFAULT_READ_MAX_TEXT_LENGTH,
|
||||||
),
|
),
|
||||||
signal,
|
signal,
|
||||||
|
{ waitForReady: options?.waitForReady },
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -844,11 +897,15 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readPageSummary(signal?: AbortSignal): Promise<BrowserPageSnapshot | null> {
|
async readPageSummary(
|
||||||
|
signal?: AbortSignal,
|
||||||
|
options?: { waitForReady?: boolean },
|
||||||
|
): Promise<BrowserPageSnapshot | null> {
|
||||||
const result = await this.readPage(
|
const result = await this.readPage(
|
||||||
{
|
{
|
||||||
maxElements: POST_ACTION_MAX_ELEMENTS,
|
maxElements: POST_ACTION_MAX_ELEMENTS,
|
||||||
maxTextLength: POST_ACTION_MAX_TEXT_LENGTH,
|
maxTextLength: POST_ACTION_MAX_TEXT_LENGTH,
|
||||||
|
waitForReady: options?.waitForReady,
|
||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
);
|
);
|
||||||
|
|
@ -871,7 +928,7 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
);
|
);
|
||||||
if (!result.ok) return result;
|
if (!result.ok) return result;
|
||||||
this.invalidateSnapshot(activeTab.id);
|
this.invalidateSnapshot(activeTab.id);
|
||||||
await this.waitForWebContentsSettle(activeTab.view.webContents, signal);
|
await this.waitForWebContentsSettle(activeTab, signal);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -897,7 +954,7 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
);
|
);
|
||||||
if (!result.ok) return result;
|
if (!result.ok) return result;
|
||||||
this.invalidateSnapshot(activeTab.id);
|
this.invalidateSnapshot(activeTab.id);
|
||||||
await this.waitForWebContentsSettle(activeTab.view.webContents, signal);
|
await this.waitForWebContentsSettle(activeTab, signal);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -948,7 +1005,7 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
wc.sendInputEvent({ type: 'keyUp', keyCode });
|
wc.sendInputEvent({ type: 'keyUp', keyCode });
|
||||||
|
|
||||||
this.invalidateSnapshot(activeTab.id);
|
this.invalidateSnapshot(activeTab.id);
|
||||||
await this.waitForWebContentsSettle(wc, signal);
|
await this.waitForWebContentsSettle(activeTab, signal);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -990,7 +1047,7 @@ export class BrowserViewManager extends EventEmitter {
|
||||||
await sleep(ms, signal);
|
await sleep(ms, signal);
|
||||||
const activeTab = this.getActiveTab();
|
const activeTab = this.getActiveTab();
|
||||||
if (!activeTab) return;
|
if (!activeTab) return;
|
||||||
await this.waitForWebContentsSettle(activeTab.view.webContents, signal);
|
await this.waitForWebContentsSettle(activeTab, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(): BrowserState {
|
getState(): BrowserState {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue