UNPKG

chrome-devtools-frontend

Version:
305 lines (270 loc) • 11.7 kB
// Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as path from 'path'; import type {ElementHandle, Page} from 'puppeteer-core'; import type {IndividualPromptRequestResponse} from '../../types.d.ts'; import {TraceDownloader} from '../trace-downloader.ts'; import {parseComment, parseFollowUps} from './comment-parsers.ts'; const DEFAULT_FOLLOW_UP_QUERY = 'Fix the issue using JavaScript code execution.'; /** * Waits for an element to have a clientHeight greater than the specified height. * @param {ElementHandle<HTMLElement>} elem The Puppeteer element handle. * @param {number} height The minimum height. * @param {number} tries The number of tries so far. * @returns {Promise<boolean>} True if the element reaches the height, false otherwise. */ export async function waitForElementToHaveHeight( elem: ElementHandle<HTMLElement>, height: number, tries = 5): Promise<boolean> { const h = await elem.evaluate(e => e.clientHeight); if (h > height) { return true; } if (tries > 10) { return false; } return await new Promise(r => { setTimeout(() => r(waitForElementToHaveHeight(elem, height, tries + 1)), 100); }); } /** * Executes a single prompt cycle in the AI Assistant. * This includes typing the query, handling auto-accept evaluations, and retrieving results. * @param {Page} devtoolsPage The Puppeteer page object for the DevTools frontend. * @param {string} query The query to send to the AI Assistant. * @param {string} inputSelector The CSS selector for the prompt input field. * @param {string} exampleId The ID of the current example, used for tagging results. * @param {boolean} isMultimodal Whether the current test target is multimodal (e.g., requires a screenshot). * @param {(text: string) => void} commonLog A logging function. * @returns {Promise<IndividualPromptRequestResponse[]>} A promise that resolves to an array of prompt responses. */ export async function executePromptCycle( devtoolsPage: Page, query: string, inputSelector: string, exampleId: string, isMultimodal: boolean, commonLog: (text: string) => void, ): Promise<IndividualPromptRequestResponse[]> { commonLog( `[Info]: Running the user prompt "${query}" (This step might take a long time)`, ); if (isMultimodal) { await devtoolsPage.locator('aria/Take screenshot').click(); } await devtoolsPage.locator(inputSelector).click(); // Add randomness to bust cache await devtoolsPage.locator(inputSelector).fill(`${query} ${`${(Math.random() * 1000)}`.split('.')[0]}`); const abort = new AbortController(); const autoAcceptEvals = async (signal: AbortSignal) => { while (!signal.aborted) { await devtoolsPage.locator('aria/Continue').click({signal}); } }; const autoAcceptPromise = autoAcceptEvals(abort.signal).catch(err => { // Catch errors from the promise itself, though individual errors are caught inside the loop if (err instanceof Error && (err.message.includes('Target closed') || err.message.includes('signal'))) { return; } console.error('autoAcceptEvals promise error:', err); }); const done = devtoolsPage.evaluate(() => { return new Promise<void>(resolve => { window.addEventListener( 'aiassistancedone', () => { resolve(); }, { once: true, }, ); }); }); await devtoolsPage.keyboard.press('Enter'); await done; abort.abort(); await autoAcceptPromise; // Ensure the auto-accept loop finishes cleaning up const logs = await devtoolsPage.evaluate(() => { return localStorage.getItem('aiAssistanceStructuredLog'); }); if (!logs) { throw new Error('No aiAssistanceStructuredLog entries were found.'); } const results = JSON.parse(logs) as IndividualPromptRequestResponse[]; return results.map(r => ({...r, exampleId})); } /** * Retrieves all comment strings from the page and stores comment elements globally. * This function evaluates code in the browser context. * @param {Page} page The Puppeteer page object. * @returns {Promise<string[]>} A promise that resolves to an array of comment strings. */ export async function getCommentStringsFromPage(page: Page): Promise<string[]> { const commentStringsFromPage = await page.evaluate(() => { function collectComments(root: Node|ShadowRoot): Array<{comment: string, commentElement: Comment, targetElement: Element | null}> { const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT); const results: Array<{comment: string, commentElement: Comment, targetElement: Element | null}> = []; while (walker.nextNode()) { const comment = walker.currentNode; if (!(comment instanceof Comment)) { continue; } results.push({ comment: comment.textContent?.trim() ?? '', commentElement: comment, targetElement: comment.nextElementSibling, }); } return results; } const elementWalker = document.createTreeWalker( document.documentElement, NodeFilter.SHOW_ELEMENT, ); const results = [...collectComments(document.documentElement)]; while (elementWalker.nextNode()) { const el = elementWalker.currentNode; if (el instanceof Element && 'shadowRoot' in el && el.shadowRoot) { results.push(...collectComments(el.shadowRoot)); } } globalThis.__commentElements = results; return results.map(result => result.comment); }); return commentStringsFromPage; } /** * Loads a performance trace into the Performance panel. */ export async function loadPerformanceTrace( devtoolsPage: Page, traceDownloader: TraceDownloader, exampleUrl: string, page: Page, commonLog: (text: string) => void, ): Promise<void> { await devtoolsPage.keyboard.press('Escape'); await devtoolsPage.keyboard.press('Escape'); commonLog('[Loading performance trace] Ensuring DevTools is in a clean state.'); await devtoolsPage.locator(':scope >>> #tab-timeline').setTimeout(5000).click(); commonLog('[Loading performance trace] Opened Performance panel'); const fileName = await traceDownloader.download(exampleUrl, page); commonLog(`[Loading performance trace] Downloaded trace file: ${fileName}`); const fileUploader = await devtoolsPage.locator('input[type=file]').waitHandle(); const tracePath = path.join(TraceDownloader.location, fileName); await fileUploader.uploadFile(tracePath); commonLog(`[Loading performance trace] Imported ${fileName} to performance panel`); const canvas = await devtoolsPage.waitForSelector(':scope >>> canvas.flame-chart-canvas'); if (!canvas) { throw new Error('[Loading performance trace] Could not find flame chart canvas.'); } const canvasVisible = await waitForElementToHaveHeight(canvas as ElementHandle<HTMLElement>, 200); if (!canvasVisible) { throw new Error('[Loading performance trace] Flame chart canvas did not become visible (height > 200px).'); } commonLog('[Loading performance trace] Flame chart canvas is visible.'); } /** * Extracts comment metadata (queries, explanation, rawComment) from the page. */ export async function extractCommentMetadata( page: Page, includeFollowUp: boolean, commonLog: (text: string) => void, ): Promise<{queries: string[], explanation: string, rawComment: Record<string, string>}> { const commentStrings = await getCommentStringsFromPage(page); if (commentStrings.length === 0) { throw new Error('[Extracting comment metadata] No comments found on the page.'); } commonLog(`[Extracting comment metadata] Extracted ${commentStrings.length} comment strings.`); const comments = commentStrings.map(comment => parseComment(comment)); const rawComment = comments[0]; // Assuming the first comment is the main one if (!rawComment?.prompt) { throw new Error('[Extracting comment metadata] Could not parse a valid prompt from the page comments.'); } commonLog(`[Extracting comment metadata] Parsed main comment: ${JSON.stringify(rawComment)}`); const queries = [rawComment.prompt]; const followUpPromptsFromExample = parseFollowUps(rawComment); if (includeFollowUp && followUpPromptsFromExample.length === 0) { queries.push(DEFAULT_FOLLOW_UP_QUERY); } else { queries.push(...followUpPromptsFromExample); } commonLog(`[Extracting comment metadata] Determined queries: ${JSON.stringify(queries)}`); return { queries, explanation: rawComment.explanation || '', rawComment, }; } /** * Opens the AI Assistance panel via the DevTools menu. */ export async function openAiAssistancePanelFromMenu( devtoolsPage: Page, commonLog: (text: string) => void, ): Promise<void> { commonLog('Opening AI Assistance panel via menu...'); await devtoolsPage.locator('aria/Customize and control DevTools').click(); await devtoolsPage.locator('aria/More tools').click(); await devtoolsPage.locator('aria/AI assistance').click(); commonLog('AI Assistance panel opened.'); } /** * Strips comment elements from the page DOM. * Relies on globalThis.__commentElements being populated by getCommentStringsFromPage. */ export async function stripCommentsFromPage( page: Page, commonLog: (text: string) => void, ): Promise<void> { commonLog('Stripping comment elements from the page...'); await page.evaluate(() => { for (const {commentElement} of globalThis.__commentElements ?? []) { if (commentElement?.remove) { commentElement.remove(); } } }); commonLog('Comment elements stripped.'); } /** * Sets up the Elements panel and inspects a target element. * Calls getCommentStringsFromPage to populate globalThis.__commentElements. */ export async function setupElementsPanelAndInspect( devtoolsPage: Page, page: Page, commonLog: (text: string) => void, ): Promise<void> { commonLog('[Setup elements panel] Setting up Elements panel'); await devtoolsPage.locator(':scope >>> #tab-elements').setTimeout(5000).click(); commonLog('[Setup elements panel] Opened Elements panel'); await devtoolsPage.locator('aria/<body>').click(); commonLog('[Setup elements panel] Clicked body in Elements panel'); commonLog('[Setup elements panel] Expanding all elements...'); let expand = await devtoolsPage.$$('pierce/.expand-button'); while (expand.length) { for (const btn of expand) { await btn.click(); } await new Promise(resolve => setTimeout(resolve, 100)); // Wait for new buttons to appear expand = await devtoolsPage.$$('pierce/.expand-button'); } commonLog('[Setup elements panel] Finished expanding all elements'); // Ensure __commentElements is populated before trying to inspect await getCommentStringsFromPage(page); commonLog('[Setup elements panel] Populated globalThis.__commentElements by calling getCommentStringsFromPage.'); commonLog('[Setup elements panel] Locating console to inspect the element'); await devtoolsPage.locator(':scope >>> #tab-console').click(); await devtoolsPage.locator('aria/Console prompt').click(); await devtoolsPage.keyboard.type( 'inspect(globalThis.__commentElements[0].targetElement)', ); await devtoolsPage.keyboard.press('Enter'); commonLog('[Setup elements panel] Typed inspect command in console'); await devtoolsPage.locator(':scope >>> #tab-elements').click(); commonLog('[Setup elements panel] Switched back to Elements panel'); }