From 142c90ae1c0e0fc9441bcedaff1430dd3eab7abd Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Tue, 10 Dec 2024 20:32:01 +0530 Subject: [PATCH 1/2] fix: handle context destroyed, frame navigation URL --- .../classes/RemoteBrowser.ts | 94 +++++++++++++------ 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 7e0a7d1ad..417e8964d 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -66,6 +66,8 @@ export class RemoteBrowser { maxRepeats: 1, }; + private lastEmittedUrl: string | null = null; + /** * {@link WorkflowGenerator} instance specific to the remote browser. */ @@ -88,6 +90,64 @@ export class RemoteBrowser { this.generator = new WorkflowGenerator(socket); } + /** + * Normalizes URLs to prevent navigation loops while maintaining consistent format + */ + private normalizeUrl(url: string): string { + try { + const parsedUrl = new URL(url); + // Remove trailing slashes except for root path + parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, '') || '/'; + // Ensure consistent protocol handling + parsedUrl.protocol = parsedUrl.protocol.toLowerCase(); + return parsedUrl.toString(); + } catch { + return url; + } + } + + /** + * Determines if a URL change is significant enough to emit + */ + private shouldEmitUrlChange(newUrl: string): boolean { + if (!this.lastEmittedUrl) { + return true; + } + const normalizedNew = this.normalizeUrl(newUrl); + const normalizedLast = this.normalizeUrl(this.lastEmittedUrl); + return normalizedNew !== normalizedLast; + } + + private async setupPageEventListeners(page: Page) { + page.on('framenavigated', async (frame) => { + if (frame === page.mainFrame()) { + const currentUrl = page.url(); + if (this.shouldEmitUrlChange(currentUrl)) { + this.lastEmittedUrl = currentUrl; + this.socket.emit('urlChanged', currentUrl); + } + } + }); + + // Handle page load events with retry mechanism + page.on('load', async () => { + const injectScript = async (): Promise => { + try { + await page.waitForLoadState('networkidle', { timeout: 5000 }); + + await page.evaluate(getInjectableScript()); + return true; + } catch (error: any) { + logger.log('warn', `Script injection attempt failed: ${error.message}`); + return false; + } + }; + + const success = await injectScript(); + console.log("Script injection result:", success); + }); + } + /** * An asynchronous constructor for asynchronously initialized properties. * Must be called right after creating an instance of RemoteBrowser class. @@ -167,15 +227,7 @@ export class RemoteBrowser { this.context = await this.browser.newContext(contextOptions); this.currentPage = await this.context.newPage(); - this.currentPage.on('framenavigated', (frame) => { - if (frame === this.currentPage?.mainFrame()) { - this.socket.emit('urlChanged', this.currentPage.url()); - } - }); - - this.currentPage.on('load', (page) => { - page.evaluate(getInjectableScript()) - }) + await this.setupPageEventListeners(this.currentPage); // await this.currentPage.setExtraHTTPHeaders({ // 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3' @@ -375,15 +427,7 @@ export class RemoteBrowser { await this.stopScreencast(); this.currentPage = page; - this.currentPage.on('framenavigated', (frame) => { - if (frame === this.currentPage?.mainFrame()) { - this.socket.emit('urlChanged', this.currentPage.url()); - } - }); - - this.currentPage.on('load', (page) => { - page.evaluate(getInjectableScript()) - }) + await this.setupPageEventListeners(this.currentPage); //await this.currentPage.setViewportSize({ height: 400, width: 900 }) this.client = await this.currentPage.context().newCDPSession(this.currentPage); @@ -411,18 +455,8 @@ export class RemoteBrowser { await this.currentPage?.close(); this.currentPage = newPage; if (this.currentPage) { - this.currentPage.on('framenavigated', (frame) => { - if (frame === this.currentPage?.mainFrame()) { - this.socket.emit('urlChanged', this.currentPage.url()); - } - }); - - this.currentPage.on('load', (page) => { - page.evaluate(getInjectableScript()) - }) - // this.currentPage.on('load', (page) => { - // this.socket.emit('urlChanged', page.url()); - // }) + await this.setupPageEventListeners(this.currentPage); + this.client = await this.currentPage.context().newCDPSession(this.currentPage); await this.subscribeToScreencast(); } else { From c7af54ebe061abcf1c5c5bcb26e4182fa3f8d254 Mon Sep 17 00:00:00 2001 From: RohitR311 Date: Tue, 10 Dec 2024 20:33:35 +0530 Subject: [PATCH 2/2] feat: add fallback mechanism for click and waitForLoadState action --- maxun-core/src/interpret.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index bd90e40f7..afea8e476 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -469,6 +469,16 @@ export default class Interpreter extends EventEmitter { }), }; + const executeAction = async (invokee: any, methodName: string, args: any) => { + console.log("Executing action:", methodName, args); + if (!args || Array.isArray(args)) { + await (invokee[methodName])(...(args ?? [])); + } else { + await (invokee[methodName])(args); + } + }; + + for (const step of steps) { this.log(`Launching ${String(step.action)}`, Level.LOG); @@ -486,10 +496,20 @@ export default class Interpreter extends EventEmitter { invokee = invokee[level]; } - if (!step.args || Array.isArray(step.args)) { - await (invokee[methodName])(...(step.args ?? [])); + if (methodName === 'waitForLoadState') { + try { + await executeAction(invokee, methodName, step.args); + } catch (error) { + await executeAction(invokee, methodName, 'domcontentloaded'); + } + } else if (methodName === 'click') { + try { + await executeAction(invokee, methodName, step.args); + } catch (error) { + await executeAction(invokee, methodName, [step.args[0], { force: true }]); + } } else { - await (invokee[methodName])(step.args); + await executeAction(invokee, methodName, step.args); } } @@ -571,7 +591,7 @@ export default class Interpreter extends EventEmitter { return allResults; } // Click the 'Load More' button to load additional items - await loadMoreButton.click(); + await loadMoreButton.dispatchEvent('click'); await page.waitForTimeout(2000); // Wait for new items to load // After clicking 'Load More', scroll down to load more items await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));