Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes for Groq demo #499

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
307 changes: 212 additions & 95 deletions lib/a11y/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
PlaywrightCommandMethodNotSupportedException,
PlaywrightCommandException,
} from "@/types/playwright";
import { actMethods, getAvailableMethods } from "../actions";
import { exhaustiveMatchingGuard } from "../utils";

// Parser function for str output
export function formatSimplifiedTree(
Expand Down Expand Up @@ -464,24 +466,54 @@ export async function performPlaywrightMethod(
},
});

if (method === "scrollIntoView") {
logger({
category: "action",
message: "scrolling element into view",
level: 2,
auxiliary: {
xpath: {
value: xpath,
type: "string",
},
},
});
try {
await locator
.evaluate((element: HTMLElement) => {
element.scrollIntoView({ behavior: "smooth", block: "center" });
})
.catch((e: Error) => {
if (actMethods.includes(method as (typeof actMethods)[number])) {
const action = method as (typeof actMethods)[number];
switch (action) {
case "goBack": {
await stagehandPage.goBack({
waitUntil: "domcontentloaded",
});
break;
}
case "scrollIntoView": {
logger({
category: "action",
message: "scrolling element into view",
level: 2,
auxiliary: {
xpath: {
value: xpath,
type: "string",
},
},
});
try {
await locator
.evaluate((element: HTMLElement) => {
element.scrollIntoView({ behavior: "smooth", block: "center" });
})
.catch((e: Error) => {
logger({
category: "action",
message: "error scrolling element into view",
level: 1,
auxiliary: {
error: {
value: e.message,
type: "string",
},
trace: {
value: e.stack,
type: "string",
},
xpath: {
value: xpath,
type: "string",
},
},
});
});
} catch (e) {
logger({
category: "action",
message: "error scrolling element into view",
Expand All @@ -501,89 +533,173 @@ export async function performPlaywrightMethod(
},
},
});
});
} catch (e) {
logger({
category: "action",
message: "error scrolling element into view",
level: 1,
auxiliary: {
error: {
value: e.message,
type: "string",
},
trace: {
value: e.stack,
type: "string",
},
xpath: {
value: xpath,
type: "string",
},
},
});

throw new PlaywrightCommandException(e.message);
}
} else if (method === "fill" || method === "type") {
try {
await locator.fill("");
await locator.click();
const text = args[0]?.toString();
for (const char of text) {
await stagehandPage.keyboard.type(char, {
delay: Math.random() * 50 + 25,
});
throw new PlaywrightCommandException(e.message);
}
break;
}
} catch (e) {
logger({
category: "action",
message: "error filling element",
level: 1,
auxiliary: {
error: {
value: e.message,
type: "string",
},
trace: {
value: e.stack,
type: "string",
},
xpath: {
value: xpath,
type: "string",
},
},
});
case "fill": // fall through to type
case "type": {
try {
await locator.fill("");
await locator.click();
const text = args[0]?.toString();
for (const char of text) {
await stagehandPage.keyboard.type(char, {
delay: Math.random() * 50 + 25,
});
}
} catch (e) {
logger({
category: "action",
message: "error filling element",
level: 1,
auxiliary: {
error: {
value: e.message,
type: "string",
},
trace: {
value: e.stack,
type: "string",
},
xpath: {
value: xpath,
type: "string",
},
},
});

throw new PlaywrightCommandException(e.message);
}
} else if (method === "press") {
try {
const key = args[0]?.toString();
await stagehandPage.keyboard.press(key);
} catch (e) {
logger({
category: "action",
message: "error pressing key",
level: 1,
auxiliary: {
error: {
value: e.message,
type: "string",
},
trace: {
value: e.stack,
type: "string",
throw new PlaywrightCommandException(e.message);
}
break;
}
// Handle navigation if a new page is opened
case "click": {
logger({
category: "action",
message: "clicking element, checking for page navigation",
level: 1,
auxiliary: {
xpath: {
value: xpath,
type: "string",
},
},
key: {
value: args[0]?.toString() ?? "unknown",
type: "string",
});

const newOpenedTab = await Promise.race([
new Promise<Page | null>((resolve) => {
Promise.resolve(stagehandPage.context()).then((context) => {
context.once("page", (page: Page) => resolve(page));
setTimeout(() => resolve(null), 1_500);
});
}),
]);

logger({
category: "action",
message: "clicked element",
level: 1,
auxiliary: {
newOpenedTab: {
value: newOpenedTab ? "opened a new tab" : "no new tabs opened",
type: "string",
},
},
},
});
});

throw new PlaywrightCommandException(e.message);
if (newOpenedTab) {
logger({
category: "action",
message: "new page detected (new tab) with URL",
level: 1,
auxiliary: {
url: {
value: newOpenedTab.url(),
type: "string",
},
},
});
await newOpenedTab.close();
await stagehandPage.goto(newOpenedTab.url());
await stagehandPage.waitForLoadState("domcontentloaded");
}

await Promise.race([
stagehandPage.waitForLoadState("networkidle"),
new Promise((resolve) => setTimeout(resolve, 5_000)),
]).catch((e) => {
logger({
category: "action",
message: "network idle timeout hit",
level: 1,
auxiliary: {
trace: {
value: e.stack,
type: "string",
},
message: {
value: e.message,
type: "string",
},
},
});
});

logger({
category: "action",
message: "finished waiting for (possible) page navigation",
level: 1,
});

if (stagehandPage.url() !== initialUrl) {
logger({
category: "action",
message: "new page detected with URL",
level: 1,
auxiliary: {
url: {
value: stagehandPage.url(),
type: "string",
},
},
});
}
break;
}
case "press": {
try {
const key = args[0]?.toString();
await stagehandPage.keyboard.press(key);
} catch (e) {
logger({
category: "action",
message: "error pressing key",
level: 1,
auxiliary: {
error: {
value: e.message,
type: "string",
},
trace: {
value: e.stack,
type: "string",
},
key: {
value: args[0]?.toString() ?? "unknown",
type: "string",
},
},
});

throw new PlaywrightCommandException(e.message);
}
break;
}
default: {
throw exhaustiveMatchingGuard(action);
}
Comment on lines +700 to +702
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes this a type safe switch statement. Every defined action must be implemented or this will not compile

}
} else if (typeof locator[method as keyof typeof locator] === "function") {
// Log current URL before action
Expand Down Expand Up @@ -746,6 +862,7 @@ export async function performPlaywrightMethod(

throw new PlaywrightCommandMethodNotSupportedException(
`Method ${method} not supported`,
getAvailableMethods(locator),
);
}
}
23 changes: 23 additions & 0 deletions lib/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Locator } from "@playwright/test";

const defaultMethods: Set<string> = new Set(getAvailableMethods({} as Locator));

export const actMethods = [
"scrollIntoView",
"press",
"click",
"fill",
"type",
"goBack",
] as const;
Comment on lines +5 to +12
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this defines the additional actions so the LLM can know what they are


export function getAvailableMethods(locator: Locator) {
return Object.keys(locator)
.filter(
(key) =>
!defaultMethods.has(key) &&
key in locator &&
typeof locator[key as keyof Locator] === "function",
)
.concat(actMethods);
}
Loading