Skip to content

Commit 7c65078

Browse files
ergunshDevtools-frontend LUCI CQ
authored and
Devtools-frontend LUCI CQ
committed
[AutoRun] Refactor auto-run.ts to use executor modules
Fixed: 419775868 Change-Id: I0c0232de3a2a1144da06f28df1a55fbbbc9cad82 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6582364 Reviewed-by: Jack Franklin <jacktfranklin@chromium.org> Commit-Queue: Ergün Erdoğmuş <ergunsh@chromium.org> Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
1 parent 66818b3 commit 7c65078

15 files changed

+856
-475
lines changed

scripts/ai_assistance/auto-run/auto-run.ts

Lines changed: 84 additions & 455 deletions
Large diffs are not rendered by default.

scripts/ai_assistance/auto-run/auto-run.test.ts renamed to scripts/ai_assistance/auto-run/shared/comment-parsers.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010
import {assert} from 'chai';
1111

12-
import {parseComment, parseFollowUps} from './auto-run-helpers.ts';
12+
import {parseComment, parseFollowUps} from './comment-parsers.ts';
1313

1414
describe('parsing comments', () => {
1515
it('parses out the prompt and evaluation sections using the "old" syntax', () => {
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as path from 'path';
6+
import type {ElementHandle, Page} from 'puppeteer-core';
7+
8+
import type {IndividualPromptRequestResponse} from '../../types.d.ts';
9+
import {TraceDownloader} from '../trace-downloader.ts';
10+
11+
import {parseComment, parseFollowUps} from './comment-parsers.ts';
12+
13+
const DEFAULT_FOLLOW_UP_QUERY = 'Fix the issue using JavaScript code execution.';
14+
15+
/**
16+
* Waits for an element to have a clientHeight greater than the specified height.
17+
* @param {ElementHandle<HTMLElement>} elem The Puppeteer element handle.
18+
* @param {number} height The minimum height.
19+
* @param {number} tries The number of tries so far.
20+
* @returns {Promise<boolean>} True if the element reaches the height, false otherwise.
21+
*/
22+
export async function waitForElementToHaveHeight(
23+
elem: ElementHandle<HTMLElement>, height: number, tries = 5): Promise<boolean> {
24+
const h = await elem.evaluate(e => e.clientHeight);
25+
if (h > height) {
26+
return true;
27+
}
28+
if (tries > 10) {
29+
return false;
30+
}
31+
return await new Promise(r => {
32+
setTimeout(() => r(waitForElementToHaveHeight(elem, height, tries + 1)), 100);
33+
});
34+
}
35+
36+
/**
37+
* Executes a single prompt cycle in the AI Assistant.
38+
* This includes typing the query, handling auto-accept evaluations, and retrieving results.
39+
* @param {Page} devtoolsPage The Puppeteer page object for the DevTools frontend.
40+
* @param {string} query The query to send to the AI Assistant.
41+
* @param {string} inputSelector The CSS selector for the prompt input field.
42+
* @param {string} exampleId The ID of the current example, used for tagging results.
43+
* @param {boolean} isMultimodal Whether the current test target is multimodal (e.g., requires a screenshot).
44+
* @param {(text: string) => void} commonLog A logging function.
45+
* @returns {Promise<IndividualPromptRequestResponse[]>} A promise that resolves to an array of prompt responses.
46+
*/
47+
export async function executePromptCycle(
48+
devtoolsPage: Page,
49+
query: string,
50+
inputSelector: string,
51+
exampleId: string,
52+
isMultimodal: boolean,
53+
commonLog: (text: string) => void,
54+
): Promise<IndividualPromptRequestResponse[]> {
55+
commonLog(
56+
`[Info]: Running the user prompt "${query}" (This step might take a long time)`,
57+
);
58+
59+
if (isMultimodal) {
60+
await devtoolsPage.locator('aria/Take screenshot').click();
61+
}
62+
63+
await devtoolsPage.locator(inputSelector).click();
64+
// Add randomness to bust cache
65+
await devtoolsPage.locator(inputSelector).fill(`${query} ${`${(Math.random() * 1000)}`.split('.')[0]}`);
66+
67+
const abort = new AbortController();
68+
const autoAcceptEvals = async (signal: AbortSignal) => {
69+
while (!signal.aborted) {
70+
await devtoolsPage.locator('aria/Continue').click({signal});
71+
}
72+
};
73+
74+
const autoAcceptPromise = autoAcceptEvals(abort.signal).catch(err => {
75+
// Catch errors from the promise itself, though individual errors are caught inside the loop
76+
if (err instanceof Error && (err.message.includes('Target closed') || err.message.includes('signal'))) {
77+
return;
78+
}
79+
console.error('autoAcceptEvals promise error:', err);
80+
});
81+
82+
const done = devtoolsPage.evaluate(() => {
83+
return new Promise<void>(resolve => {
84+
window.addEventListener(
85+
'aiassistancedone',
86+
() => {
87+
resolve();
88+
},
89+
{
90+
once: true,
91+
},
92+
);
93+
});
94+
});
95+
96+
await devtoolsPage.keyboard.press('Enter');
97+
await done;
98+
abort.abort();
99+
await autoAcceptPromise; // Ensure the auto-accept loop finishes cleaning up
100+
101+
const logs = await devtoolsPage.evaluate(() => {
102+
return localStorage.getItem('aiAssistanceStructuredLog');
103+
});
104+
105+
if (!logs) {
106+
throw new Error('No aiAssistanceStructuredLog entries were found.');
107+
}
108+
const results = JSON.parse(logs) as IndividualPromptRequestResponse[];
109+
110+
return results.map(r => ({...r, exampleId}));
111+
}
112+
113+
/**
114+
* Retrieves all comment strings from the page and stores comment elements globally.
115+
* This function evaluates code in the browser context.
116+
* @param {Page} page The Puppeteer page object.
117+
* @returns {Promise<string[]>} A promise that resolves to an array of comment strings.
118+
*/
119+
export async function getCommentStringsFromPage(page: Page): Promise<string[]> {
120+
const commentStringsFromPage = await page.evaluate(() => {
121+
function collectComments(root: Node|ShadowRoot):
122+
Array<{comment: string, commentElement: Comment, targetElement: Element | null}> {
123+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
124+
const results: Array<{comment: string, commentElement: Comment, targetElement: Element | null}> = [];
125+
while (walker.nextNode()) {
126+
const comment = walker.currentNode;
127+
if (!(comment instanceof Comment)) {
128+
continue;
129+
}
130+
131+
results.push({
132+
comment: comment.textContent?.trim() ?? '',
133+
commentElement: comment,
134+
targetElement: comment.nextElementSibling,
135+
});
136+
}
137+
return results;
138+
}
139+
const elementWalker = document.createTreeWalker(
140+
document.documentElement,
141+
NodeFilter.SHOW_ELEMENT,
142+
);
143+
const results = [...collectComments(document.documentElement)];
144+
while (elementWalker.nextNode()) {
145+
const el = elementWalker.currentNode;
146+
if (el instanceof Element && 'shadowRoot' in el && el.shadowRoot) {
147+
results.push(...collectComments(el.shadowRoot));
148+
}
149+
}
150+
globalThis.__commentElements = results;
151+
return results.map(result => result.comment);
152+
});
153+
return commentStringsFromPage;
154+
}
155+
156+
/**
157+
* Loads a performance trace into the Performance panel.
158+
*/
159+
export async function loadPerformanceTrace(
160+
devtoolsPage: Page,
161+
traceDownloader: TraceDownloader,
162+
exampleUrl: string,
163+
page: Page,
164+
commonLog: (text: string) => void,
165+
): Promise<void> {
166+
await devtoolsPage.keyboard.press('Escape');
167+
await devtoolsPage.keyboard.press('Escape');
168+
commonLog('[Loading performance trace] Ensuring DevTools is in a clean state.');
169+
170+
await devtoolsPage.locator(':scope >>> #tab-timeline').setTimeout(5000).click();
171+
commonLog('[Loading performance trace] Opened Performance panel');
172+
173+
const fileName = await traceDownloader.download(exampleUrl, page);
174+
commonLog(`[Loading performance trace] Downloaded trace file: ${fileName}`);
175+
176+
const fileUploader = await devtoolsPage.locator('input[type=file]').waitHandle();
177+
const tracePath = path.join(TraceDownloader.location, fileName);
178+
await fileUploader.uploadFile(tracePath);
179+
commonLog(`[Loading performance trace] Imported ${fileName} to performance panel`);
180+
181+
const canvas = await devtoolsPage.waitForSelector(':scope >>> canvas.flame-chart-canvas');
182+
if (!canvas) {
183+
throw new Error('[Loading performance trace] Could not find flame chart canvas.');
184+
}
185+
const canvasVisible = await waitForElementToHaveHeight(canvas as ElementHandle<HTMLElement>, 200);
186+
if (!canvasVisible) {
187+
throw new Error('[Loading performance trace] Flame chart canvas did not become visible (height > 200px).');
188+
}
189+
commonLog('[Loading performance trace] Flame chart canvas is visible.');
190+
}
191+
192+
/**
193+
* Extracts comment metadata (queries, explanation, rawComment) from the page.
194+
*/
195+
export async function extractCommentMetadata(
196+
page: Page,
197+
includeFollowUp: boolean,
198+
commonLog: (text: string) => void,
199+
): Promise<{queries: string[], explanation: string, rawComment: Record<string, string>}> {
200+
const commentStrings = await getCommentStringsFromPage(page);
201+
if (commentStrings.length === 0) {
202+
throw new Error('[Extracting comment metadata] No comments found on the page.');
203+
}
204+
commonLog(`[Extracting comment metadata] Extracted ${commentStrings.length} comment strings.`);
205+
206+
const comments = commentStrings.map(comment => parseComment(comment));
207+
const rawComment = comments[0]; // Assuming the first comment is the main one
208+
if (!rawComment?.prompt) {
209+
throw new Error('[Extracting comment metadata] Could not parse a valid prompt from the page comments.');
210+
}
211+
commonLog(`[Extracting comment metadata] Parsed main comment: ${JSON.stringify(rawComment)}`);
212+
213+
const queries = [rawComment.prompt];
214+
const followUpPromptsFromExample = parseFollowUps(rawComment);
215+
if (includeFollowUp && followUpPromptsFromExample.length === 0) {
216+
queries.push(DEFAULT_FOLLOW_UP_QUERY);
217+
} else {
218+
queries.push(...followUpPromptsFromExample);
219+
}
220+
commonLog(`[Extracting comment metadata] Determined queries: ${JSON.stringify(queries)}`);
221+
222+
return {
223+
queries,
224+
explanation: rawComment.explanation || '',
225+
rawComment,
226+
};
227+
}
228+
229+
/**
230+
* Opens the AI Assistance panel via the DevTools menu.
231+
*/
232+
export async function openAiAssistancePanelFromMenu(
233+
devtoolsPage: Page,
234+
commonLog: (text: string) => void,
235+
): Promise<void> {
236+
commonLog('Opening AI Assistance panel via menu...');
237+
await devtoolsPage.locator('aria/Customize and control DevTools').click();
238+
await devtoolsPage.locator('aria/More tools').click();
239+
await devtoolsPage.locator('aria/AI assistance').click();
240+
commonLog('AI Assistance panel opened.');
241+
}
242+
243+
/**
244+
* Strips comment elements from the page DOM.
245+
* Relies on globalThis.__commentElements being populated by getCommentStringsFromPage.
246+
*/
247+
export async function stripCommentsFromPage(
248+
page: Page,
249+
commonLog: (text: string) => void,
250+
): Promise<void> {
251+
commonLog('Stripping comment elements from the page...');
252+
await page.evaluate(() => {
253+
for (const {commentElement} of globalThis.__commentElements ?? []) {
254+
if (commentElement?.remove) {
255+
commentElement.remove();
256+
}
257+
}
258+
});
259+
commonLog('Comment elements stripped.');
260+
}
261+
262+
/**
263+
* Sets up the Elements panel and inspects a target element.
264+
* Calls getCommentStringsFromPage to populate globalThis.__commentElements.
265+
*/
266+
export async function setupElementsPanelAndInspect(
267+
devtoolsPage: Page,
268+
page: Page,
269+
commonLog: (text: string) => void,
270+
): Promise<void> {
271+
commonLog('[Setup elements panel] Setting up Elements panel');
272+
await devtoolsPage.locator(':scope >>> #tab-elements').setTimeout(5000).click();
273+
commonLog('[Setup elements panel] Opened Elements panel');
274+
275+
await devtoolsPage.locator('aria/<body>').click();
276+
commonLog('[Setup elements panel] Clicked body in Elements panel');
277+
278+
commonLog('[Setup elements panel] Expanding all elements...');
279+
let expand = await devtoolsPage.$$('pierce/.expand-button');
280+
while (expand.length) {
281+
for (const btn of expand) {
282+
await btn.click();
283+
}
284+
await new Promise(resolve => setTimeout(resolve, 100)); // Wait for new buttons to appear
285+
expand = await devtoolsPage.$$('pierce/.expand-button');
286+
}
287+
commonLog('[Setup elements panel] Finished expanding all elements');
288+
289+
// Ensure __commentElements is populated before trying to inspect
290+
await getCommentStringsFromPage(page);
291+
commonLog('[Setup elements panel] Populated globalThis.__commentElements by calling getCommentStringsFromPage.');
292+
293+
commonLog('[Setup elements panel] Locating console to inspect the element');
294+
await devtoolsPage.locator(':scope >>> #tab-console').click();
295+
await devtoolsPage.locator('aria/Console prompt').click();
296+
await devtoolsPage.keyboard.type(
297+
'inspect(globalThis.__commentElements[0].targetElement)',
298+
);
299+
await devtoolsPage.keyboard.press('Enter');
300+
commonLog('[Setup elements panel] Typed inspect command in console');
301+
302+
await devtoolsPage.locator(':scope >>> #tab-elements').click();
303+
commonLog('[Setup elements panel] Switched back to Elements panel');
304+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import type {Page} from 'puppeteer-core';
6+
7+
import type {IndividualPromptRequestResponse, TestTarget} from '../../types.d.ts';
8+
import {
9+
executePromptCycle,
10+
extractCommentMetadata,
11+
openAiAssistancePanelFromMenu,
12+
setupElementsPanelAndInspect,
13+
stripCommentsFromPage
14+
} from '../shared/puppeteer-helpers.ts';
15+
16+
import type {TargetExecutor, TargetPreparationResult} from './interface.ts';
17+
18+
export class ElementsExecutor implements TargetExecutor {
19+
async prepare(exampleUrl: string, page: Page, devtoolsPage: Page, commonLog: (text: string) => void, userArgs: {
20+
includeFollowUp: boolean,
21+
testTarget: TestTarget,
22+
}): Promise<TargetPreparationResult> {
23+
commonLog(`[ElementsExecutor] Preparing example: ${exampleUrl} for target: ${userArgs.testTarget}`);
24+
await setupElementsPanelAndInspect(devtoolsPage, page, commonLog);
25+
const metadata = await extractCommentMetadata(page, userArgs.includeFollowUp, commonLog);
26+
await openAiAssistancePanelFromMenu(devtoolsPage, commonLog);
27+
await stripCommentsFromPage(page, commonLog);
28+
29+
return {
30+
queries: metadata.queries,
31+
explanation: metadata.explanation,
32+
rawComment: metadata.rawComment,
33+
};
34+
}
35+
36+
async execute(
37+
devtoolsPage: Page,
38+
preparationResult: TargetPreparationResult,
39+
exampleId: string,
40+
commonLog: (text: string) => void,
41+
): Promise<IndividualPromptRequestResponse[]> {
42+
const allResults: IndividualPromptRequestResponse[] = [];
43+
const inputSelector = 'aria/Ask a question about the selected element'; // Specific to Elements/Multimodal
44+
45+
for (const query of preparationResult.queries) {
46+
commonLog(`[ElementsExecutor] Executing query: "${query}" for example: ${exampleId}`);
47+
const results = await executePromptCycle(
48+
devtoolsPage,
49+
query,
50+
inputSelector,
51+
exampleId,
52+
/* isMultimodal */ false,
53+
commonLog,
54+
);
55+
allResults.push(...results);
56+
}
57+
commonLog(`[ElementsExecutor] Finished executing all queries for example: ${exampleId}`);
58+
return allResults;
59+
}
60+
}

0 commit comments

Comments
 (0)