chrome-devtools-frontend
Version:
Chrome DevTools UI
305 lines (270 loc) • 11.7 kB
text/typescript
// 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');
}