Created
April 30, 2025 17:33
-
-
Save tgriesser/7ccc598f95abadf064d954581b71a424 to your computer and use it in GitHub Desktop.
Generated Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
test, | |
expect, | |
chromium, | |
Browser, | |
type CDPSession, | |
type TestInfo, | |
type Locator, | |
type Page, | |
type Response, | |
type BrowserContext, | |
} from "@playwright/test"; | |
import fs from "fs"; | |
import os from "os"; | |
import path from "path"; | |
import fsPromises from "fs/promises"; | |
import { Readable } from "stream"; | |
import { finished } from "stream/promises"; | |
import { URL } from "url"; | |
declare global { | |
interface Window { | |
octomindWrittenValueTracker?: WeakMap<HTMLElement, string>; | |
} | |
} | |
const handleHydrationIssues = async ( | |
enterTextLocator: Locator, | |
textToEnter: string, | |
timeout: number | |
): Promise<void> => { | |
await enterTextLocator.evaluate( | |
(inputElement: HTMLElement, textToEnter: string) => { | |
if (!window.octomindWrittenValueTracker) { | |
window.octomindWrittenValueTracker = new WeakMap<HTMLElement, string>(); | |
} | |
window.octomindWrittenValueTracker.set(inputElement, textToEnter); | |
if (!("value" in inputElement)) { | |
return; | |
} | |
const hydrationObserverStartedAttribute = "octo-hydration-watcher"; | |
if (inputElement.getAttribute(hydrationObserverStartedAttribute)) { | |
// no need to register a second observer, we already interacted with this element | |
// and it was not reloaded | |
return; | |
} | |
inputElement.setAttribute(hydrationObserverStartedAttribute, "true"); | |
const defaultValue = | |
"defaultValue" in inputElement | |
? (inputElement.defaultValue as string) | |
: ""; | |
const observer = new MutationObserver((mutationsList) => { | |
for (const mutation of mutationsList) { | |
if ( | |
mutation.type === "attributes" && | |
mutation.attributeName === "value" | |
) { | |
if ( | |
"value" in mutation.target && | |
mutation.target.value === defaultValue | |
) { | |
const lastEnteredValue = | |
window.octomindWrittenValueTracker?.get(inputElement); | |
if (lastEnteredValue !== undefined) { | |
inputElement.setAttribute("value", lastEnteredValue); | |
inputElement.value = lastEnteredValue; | |
} | |
observer.disconnect(); | |
inputElement.removeAttribute(hydrationObserverStartedAttribute); | |
window.octomindWrittenValueTracker?.delete(inputElement); | |
} | |
} | |
} | |
}); | |
observer.observe(inputElement, { | |
attributes: true, | |
attributeFilter: ["value"], | |
}); | |
}, | |
textToEnter, | |
{ timeout } | |
); | |
}; | |
const innerDownloadFile = async (url: string): Promise<string | undefined> => { | |
const parsedUrl = new URL(url); | |
const lastPathElement = parsedUrl.pathname.split("/").pop(); | |
const dir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "download-")); | |
const destination = path.join( | |
dir, | |
lastPathElement ? lastPathElement : "file" | |
); | |
let status = 0; | |
try { | |
const res = await fetch(url); | |
status = res.status; | |
if (res.ok && res.body) { | |
const fileStream = fs.createWriteStream(destination, { flags: "wx" }); | |
await finished(Readable.fromWeb(res.body as any).pipe(fileStream)); | |
return destination; | |
} | |
} catch (e) { | |
console.error({ url, e }, "download failed"); | |
} | |
throw new Error(`download failed: status: ${status}`); | |
}; | |
type Callback = (...args: any[]) => Promise<string | undefined>; | |
const retryWithExponentialBackoff = async ( | |
fn: Callback, | |
maxAttempts = 5, | |
baseDelayMs = 1000 | |
) => { | |
let attempt = 1; | |
const execute = async () => { | |
try { | |
return await fn(); | |
} catch (error) { | |
if (attempt >= maxAttempts) { | |
throw error; | |
} | |
const delayMs = baseDelayMs * 2 ** attempt; | |
console.log(`${error}: Retry attempt ${attempt} after ${delayMs}ms`); | |
await new Promise((resolve) => setTimeout(resolve, delayMs)); | |
attempt++; | |
return execute(); | |
} | |
}; | |
return execute(); | |
}; | |
const downloadFile = async (url: string): Promise<string | undefined> => { | |
return retryWithExponentialBackoff(async () => innerDownloadFile(url)); | |
}; | |
const saveScrollIntoView = async ({ | |
locator, | |
timeout, | |
}: { | |
locator: Locator; | |
timeout: number; | |
}): Promise<void> => { | |
try { | |
await locator.scrollIntoViewIfNeeded({ timeout }); | |
} catch (error) { | |
//scrolling can fail if e.g. the assertion is "NOT_VISIBLE" but we don't actually care and want to continue | |
console.error({ error }, "Could not scroll into view, ignoring"); | |
} | |
}; | |
const attachBoundingBox = (locator: Locator, timeout: number): Promise<void> => | |
Promise.resolve(); | |
const utilizeTimeBudget = async ( | |
budgetMs: number, | |
callback: () => Promise<void> | |
): Promise<number> => { | |
const start = performance.now(); | |
await callback(); | |
const end = performance.now(); | |
const duration = end - start; | |
const leftOver = budgetMs - duration; | |
return Math.max(leftOver, 1000); | |
}; | |
const testStartTime = new Date(); | |
const generateEmailUrl = ({ | |
subjectContaining, | |
}: { | |
subjectContaining: string | undefined; | |
}): string => { | |
let emailReceivedAfter = new Date(testStartTime.getTime() - 5000); | |
let url = | |
"https://app.octomind.dev/signed/testtargets/52d6e23c-bf4b-44e7-bb2d-ca08f006e699/environments/3a3f31af-6d83-4607-9546-9ae7896a5134/after/" + | |
emailReceivedAfter.toISOString() + | |
"/email?signature=" + | |
"AAAAAGgSevkR7SZQ5-IE9rprwvVcMmfgFwzqxdpmuKQgY2UoFzhfMg"; | |
if (subjectContaining) { | |
url += `&subject=${encodeURIComponent(subjectContaining)}`; | |
} | |
return url.toString(); | |
}; | |
test.describe("octomind-generated", () => { | |
const dynamicVariables: Map<string, string> = new Map(); | |
const setDynamicVariable = (name: string, value: string): void => { | |
const stringToMatch = `$$${name}`; | |
if ( | |
/\$\$(?!OCTO)([a-zA-Z][a-zA-Z0-9_]*)/.exec(stringToMatch)?.[0] !== | |
stringToMatch | |
) { | |
throw new Error( | |
`Variable name must be alphanumeric with underscores and start with a letter, but was: ${name}` | |
); | |
} | |
dynamicVariables.set(name, value); | |
}; | |
const getDynamicVariable = (name: string): string => { | |
if (!dynamicVariables.has(name)) { | |
throw new Error(`Variable not set: ${name}`); | |
} | |
return dynamicVariables.get(name) as string; | |
}; | |
const getClipboardContent = async (page: Page): Promise<string> => { | |
return page.evaluate(() => navigator.clipboard.readText()); | |
}; | |
const getShortStableId = ({ retry }: { retry: number }): string => { | |
const idsByRetry = ["0ae83f2c", "331cd011"]; | |
return ( | |
idsByRetry[retry] ?? | |
`<no (\$OCTO_STABLE_UUID_SHORT) generated for retry ${retry}>` | |
); | |
}; | |
const getStableId = ({ retry }: { retry: number }): string => { | |
const idsByRetry = [ | |
"c005861e-2f46-4be3-8a4a-eceaa5a6fc21", | |
"7047380c-96a2-4d5c-bbb2-5a74e2d4466d", | |
]; | |
return ( | |
idsByRetry[retry] ?? | |
`<no (\$OCTO_STABLE_UUID) generated for retry ${retry}>` | |
); | |
}; | |
let activePage: Page; | |
let newPage: Page | undefined; | |
test.beforeEach(async ({ context, page }) => { | |
await page.addInitScript(() => { | |
// prevent links from opening new tabs | |
const callback = (): void => { | |
const elements = document.querySelectorAll("a[target=_blank]"); | |
for (const element of elements) { | |
element.removeAttribute("target"); | |
} | |
}; | |
const observer = new window.MutationObserver(callback); | |
observer.observe(document, { childList: true, subtree: true }); | |
}); | |
newPage = undefined; | |
context.on("page", async (newlyOpenedPage) => { | |
newPage = newlyOpenedPage; | |
await newlyOpenedPage.addInitScript(() => { | |
// prevent links from opening new tabs | |
const callback = (): void => { | |
const elements = document.querySelectorAll("a[target=_blank]"); | |
for (const element of elements) { | |
element.removeAttribute("target"); | |
} | |
}; | |
const observer = new window.MutationObserver(callback); | |
observer.observe(document, { childList: true, subtree: true }); | |
}); | |
}); | |
activePage = page; | |
}); | |
// eslint-disable-next-line no-empty-pattern | |
test.afterEach(({}, testInfo) => { | |
console.log( | |
`Finished test with status: [${testInfo.title}: ${String( | |
testInfo.status | |
)}]` | |
); | |
}); | |
test.afterAll( | |
"Close all browser contexts", | |
async ({ browser }, _testInfo) => { | |
// try to more reliably get trace files, especially in timeout error cases | |
// https://github.com/microsoft/playwright/issues/14773#issuecomment-1178108058 | |
console.log("Closing all browser contexts"); | |
for (const context of browser.contexts()) { | |
await context.close(); | |
} | |
} | |
); | |
test.describe("docs billing flow - 39e49e29-8191-4ee6-bd74-fb811b558a6d - describe", () => { | |
const filterSteps = <T extends { type: "LOGIN" | "COOKIE_BANNER" | null }>( | |
steps: T[], | |
typesToSkip: Set<"LOGIN" | "COOKIE_BANNER" | null> | |
): (T & { type: "LOGIN" | "COOKIE_BANNER" | null })[] => { | |
if (!fs.existsSync(".auth/state.json")) { | |
return steps; | |
} | |
const isSkippableStep = (e: { | |
type: "LOGIN" | "COOKIE_BANNER" | null; | |
}): boolean => typesToSkip.has(e.type); | |
const firstIndexSkipped = steps.findIndex(isSkippableStep); | |
if (firstIndexSkipped === -1) { | |
return steps; | |
} else { | |
const lastIndexSkipped = steps.findLastIndex(isSkippableStep); | |
return [ | |
...steps.slice(0, firstIndexSkipped), | |
{ | |
fn: async (page: Page): Promise<void> => { | |
const savedAfterLoginUrl = ( | |
JSON.parse( | |
await fsPromises.readFile(".auth/state.json", { | |
encoding: "utf8", | |
}) | |
) as { octomind: { urlAfterLogin: string } } | |
).octomind.urlAfterLogin; | |
await page | |
.context() | |
.tracing.group("1. Go to URL", { | |
location: { | |
file: '#octo: {"elementId":"generated-for-navigation","ignoreFailure":false,"plannedIndex":0,"actionOrExpectation":"GO_TO"}', | |
line: 1, | |
column: 1, | |
}, | |
}); | |
try { | |
await page.goto(savedAfterLoginUrl, { timeout: 30000 }); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: false, | |
ignoreFailure: false, | |
}, | |
...steps.slice(lastIndexSkipped + 1), | |
]; | |
} | |
}; | |
test("docs billing flow - 39e49e29-8191-4ee6-bd74-fb811b558a6d", async ({ | |
context, | |
}, testInfo) => { | |
const step0 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group("1. navigate to URL", { | |
location: { | |
file: '#octo: {"elementId":"generated-for-navigation","ignoreFailure":false,"plannedIndex":0,"actionOrExpectation":"GO_TO"}', | |
line: 1, | |
column: 1, | |
}, | |
}); | |
try { | |
let timeBudget = 30000; | |
await utilizeTimeBudget(timeBudget, async () => { | |
await page.goto("https://www.netlify.com/", { | |
timeout: timeBudget, | |
}); | |
await page.waitForLoadState(); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: true, | |
ignoreFailure: false, | |
} as const; | |
const step1 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group("2. left-click on 'Docs' link", { | |
location: { | |
file: '#octo: {"elementId":"9f5b28cf-7ff1-4b9b-9c9c-d7c4995031f5","ignoreFailure":false,"plannedIndex":1,"actionOrExpectation":"CLICK"}', | |
line: 1, | |
column: 1, | |
}, | |
}); | |
try { | |
const locator1 = page | |
.getByLabel("Site navigation") | |
.getByRole("link", { name: "Docs" }) | |
.locator("visible=true"); | |
let timeBudget = 30000; | |
timeBudget = await utilizeTimeBudget( | |
timeBudget, | |
async () => | |
await saveScrollIntoView({ | |
locator: locator1, | |
timeout: timeBudget, | |
}) | |
); | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
attachBoundingBox({ | |
locator: locator1, | |
timeout: timeBudget, | |
attachmentName: | |
"boundingrectprefixdivider9f5b28cf-7ff1-4b9b-9c9c-d7c4995031f5timingdividerbefore", | |
testInfo: testInfo, | |
}) | |
); | |
await utilizeTimeBudget(timeBudget, async () => { | |
await locator1.click({ timeout: timeBudget, ...{} }); | |
await page.waitForLoadState(); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: true, | |
ignoreFailure: false, | |
} as const; | |
const step2 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group("3. left-click on 'search input' combobox", { | |
location: { | |
file: '#octo: {"elementId":"872794e7-75cd-4b6c-b54e-8f039de276da","ignoreFailure":false,"plannedIndex":2,"actionOrExpectation":"CLICK"}', | |
line: 1, | |
column: 1, | |
}, | |
}); | |
try { | |
const locator2 = page | |
.getByRole("combobox", { name: "search input" }) | |
.locator("visible=true"); | |
let timeBudget = 30000; | |
timeBudget = await utilizeTimeBudget( | |
timeBudget, | |
async () => | |
await saveScrollIntoView({ | |
locator: locator2, | |
timeout: timeBudget, | |
}) | |
); | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
attachBoundingBox({ | |
locator: locator2, | |
timeout: timeBudget, | |
attachmentName: | |
"boundingrectprefixdivider872794e7-75cd-4b6c-b54e-8f039de276datimingdividerbefore", | |
testInfo: testInfo, | |
}) | |
); | |
await utilizeTimeBudget(timeBudget, async () => { | |
await locator2.click({ timeout: timeBudget, ...{} }); | |
await page.waitForLoadState(); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: true, | |
ignoreFailure: false, | |
} as const; | |
const step3 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group( | |
"4. enter 'Billing FAQ' into 'search input' combobox", | |
{ | |
location: { | |
file: '#octo: {"elementId":"d7b6fceb-66cb-4fab-819c-0b83a8283342","ignoreFailure":false,"plannedIndex":3,"actionOrExpectation":"ENTER_TEXT"}', | |
line: 1, | |
column: 1, | |
}, | |
} | |
); | |
try { | |
const locator3 = page | |
.getByRole("combobox", { name: "search input" }) | |
.locator("visible=true"); | |
let timeBudget = 30000; | |
timeBudget = await utilizeTimeBudget( | |
timeBudget, | |
async () => | |
await saveScrollIntoView({ | |
locator: locator3, | |
timeout: timeBudget, | |
}) | |
); | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
attachBoundingBox({ | |
locator: locator3, | |
timeout: timeBudget, | |
attachmentName: | |
"boundingrectprefixdividerd7b6fceb-66cb-4fab-819c-0b83a8283342timingdividerbefore", | |
testInfo: testInfo, | |
}) | |
); | |
await utilizeTimeBudget(timeBudget, async () => { | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
handleHydrationIssues(locator3, "Billing FAQ", timeBudget) | |
); | |
await locator3.fill("Billing FAQ", { timeout: timeBudget }); | |
await page.waitForLoadState(); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: true, | |
ignoreFailure: false, | |
} as const; | |
const step4 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group("5. press key Enter on 'search input' combobox", { | |
location: { | |
file: '#octo: {"elementId":"0af58d9d-7196-4b8f-84a2-a946eebbdd74","ignoreFailure":false,"plannedIndex":4,"actionOrExpectation":"KEY_PRESS"}', | |
line: 1, | |
column: 1, | |
}, | |
}); | |
try { | |
const locator4 = page | |
.getByRole("combobox", { name: "search input" }) | |
.locator("visible=true"); | |
let timeBudget = 30000; | |
timeBudget = await utilizeTimeBudget( | |
timeBudget, | |
async () => | |
await saveScrollIntoView({ | |
locator: locator4, | |
timeout: timeBudget, | |
}) | |
); | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
attachBoundingBox({ | |
locator: locator4, | |
timeout: timeBudget, | |
attachmentName: | |
"boundingrectprefixdivider0af58d9d-7196-4b8f-84a2-a946eebbdd74timingdividerbefore", | |
testInfo: testInfo, | |
}) | |
); | |
await utilizeTimeBudget(timeBudget, async () => { | |
await locator4.click({ timeout: timeBudget }); | |
await locator4.press("Enter", { timeout: timeBudget }); | |
await page.waitForLoadState(); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: true, | |
ignoreFailure: false, | |
} as const; | |
const step5 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group( | |
"6. left-click on 'What’s an extra usage package?' link", | |
{ | |
location: { | |
file: '#octo: {"elementId":"e9c0f744-4801-4d5d-86d1-5c9ea2b628e6","ignoreFailure":false,"plannedIndex":5,"actionOrExpectation":"CLICK"}', | |
line: 1, | |
column: 1, | |
}, | |
} | |
); | |
try { | |
const locator5 = page | |
.getByRole("link", { name: "What’s an extra usage package?" }) | |
.locator("visible=true"); | |
let timeBudget = 30000; | |
timeBudget = await utilizeTimeBudget( | |
timeBudget, | |
async () => | |
await saveScrollIntoView({ | |
locator: locator5, | |
timeout: timeBudget, | |
}) | |
); | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
attachBoundingBox({ | |
locator: locator5, | |
timeout: timeBudget, | |
attachmentName: | |
"boundingrectprefixdividere9c0f744-4801-4d5d-86d1-5c9ea2b628e6timingdividerbefore", | |
testInfo: testInfo, | |
}) | |
); | |
await utilizeTimeBudget(timeBudget, async () => { | |
await locator5.click({ timeout: timeBudget, ...{} }); | |
await page.waitForLoadState(); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: true, | |
ignoreFailure: false, | |
} as const; | |
const step6 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group( | |
"7. left-click on filtered by hasText: 'With metered features on paid' link", | |
{ | |
location: { | |
file: '#octo: {"elementId":"c735c10f-beeb-48ec-bbae-bf4b2c176930","ignoreFailure":false,"plannedIndex":6,"actionOrExpectation":"CLICK"}', | |
line: 1, | |
column: 1, | |
}, | |
} | |
); | |
try { | |
const scrollLocator6 = page.locator("html"); | |
try { | |
await scrollLocator6.evaluate( | |
(e) => (e.scrollTop = 1836), | |
undefined, | |
{ timeout: 30000 } | |
); | |
} catch (e) { | |
//scrolling can fail if e.g. the assertion is "NOT_VISIBLE" but we don't actually care and want to continue | |
} | |
const locator6 = page | |
.getByRole("paragraph") | |
.filter({ hasText: "With metered features on paid" }) | |
.getByRole("link") | |
.locator("visible=true"); | |
let timeBudget = 30000; | |
timeBudget = await utilizeTimeBudget( | |
timeBudget, | |
async () => | |
await saveScrollIntoView({ | |
locator: locator6, | |
timeout: timeBudget, | |
}) | |
); | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
attachBoundingBox({ | |
locator: locator6, | |
timeout: timeBudget, | |
attachmentName: | |
"boundingrectprefixdividerc735c10f-beeb-48ec-bbae-bf4b2c176930timingdividerbefore", | |
testInfo: testInfo, | |
}) | |
); | |
await utilizeTimeBudget(timeBudget, async () => { | |
await locator6.click({ timeout: timeBudget, ...{} }); | |
await page.waitForLoadState(); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: true, | |
ignoreFailure: false, | |
} as const; | |
const step7 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group("8. left-click on 'Sign up' link", { | |
location: { | |
file: '#octo: {"elementId":"23d9d173-3373-4b3d-a653-f438f08171d8","ignoreFailure":false,"plannedIndex":7,"actionOrExpectation":"CLICK"}', | |
line: 1, | |
column: 1, | |
}, | |
}); | |
try { | |
const locator7 = page | |
.getByRole("link", { name: "Sign up" }) | |
.locator("visible=true"); | |
let timeBudget = 30000; | |
timeBudget = await utilizeTimeBudget( | |
timeBudget, | |
async () => | |
await saveScrollIntoView({ | |
locator: locator7, | |
timeout: timeBudget, | |
}) | |
); | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
attachBoundingBox({ | |
locator: locator7, | |
timeout: timeBudget, | |
attachmentName: | |
"boundingrectprefixdivider23d9d173-3373-4b3d-a653-f438f08171d8timingdividerbefore", | |
testInfo: testInfo, | |
}) | |
); | |
await utilizeTimeBudget(timeBudget, async () => { | |
await locator7.click({ timeout: timeBudget, ...{} }); | |
await page.waitForLoadState(); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: true, | |
ignoreFailure: false, | |
} as const; | |
const step8 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group( | |
"9. check that 'Sign up with email' button is visible", | |
{ | |
location: { | |
file: '#octo: {"elementId":"492a43fa-3dac-4506-a3d4-c218f381bd17","ignoreFailure":false,"plannedIndex":8,"actionOrExpectation":"VISIBLE"}', | |
line: 1, | |
column: 1, | |
}, | |
} | |
); | |
try { | |
const locator8 = page | |
.getByRole("button", { name: "Sign up with email" }) | |
.locator("visible=true"); | |
let timeBudget = 30000; | |
timeBudget = await utilizeTimeBudget( | |
timeBudget, | |
async () => | |
await saveScrollIntoView({ | |
locator: locator8, | |
timeout: timeBudget, | |
}) | |
); | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
attachBoundingBox({ | |
locator: locator8, | |
timeout: timeBudget, | |
attachmentName: | |
"boundingrectprefixdivider492a43fa-3dac-4506-a3d4-c218f381bd17timingdividerbefore", | |
testInfo: testInfo, | |
}) | |
); | |
await utilizeTimeBudget(timeBudget, async () => { | |
await expect(locator8).toBeVisible({ timeout: timeBudget }); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: false, | |
ignoreFailure: false, | |
} as const; | |
const step9 = { | |
fn: async (page: Page): Promise<void> => { | |
await page | |
.context() | |
.tracing.group( | |
"10. check that 'Sign up to deploy your project' heading contains text ''Sign up to deploy your project''", | |
{ | |
location: { | |
file: '#octo: {"elementId":"c27ca8dd-9c56-4080-9d0a-b05ef585ba6d","ignoreFailure":false,"plannedIndex":9,"actionOrExpectation":"TO_CONTAIN_TEXT"}', | |
line: 1, | |
column: 1, | |
}, | |
} | |
); | |
try { | |
const locator9 = page | |
.getByRole("heading", { name: "Sign up to deploy your project" }) | |
.locator("visible=true"); | |
let timeBudget = 30000; | |
timeBudget = await utilizeTimeBudget( | |
timeBudget, | |
async () => | |
await saveScrollIntoView({ | |
locator: locator9, | |
timeout: timeBudget, | |
}) | |
); | |
timeBudget = await utilizeTimeBudget(timeBudget, async () => | |
attachBoundingBox({ | |
locator: locator9, | |
timeout: timeBudget, | |
attachmentName: | |
"boundingrectprefixdividerc27ca8dd-9c56-4080-9d0a-b05ef585ba6dtimingdividerbefore", | |
testInfo: testInfo, | |
}) | |
); | |
await utilizeTimeBudget(timeBudget, async () => { | |
await expect(locator9).toContainText( | |
"Sign up to deploy your project", | |
{ timeout: timeBudget } | |
); | |
}); | |
} finally { | |
await page.context().tracing.groupEnd(); | |
} | |
}, | |
type: null, | |
retryable: false, | |
ignoreFailure: false, | |
} as const; | |
const unfilteredSteps = [ | |
step0, | |
step1, | |
step2, | |
step3, | |
step4, | |
step5, | |
step6, | |
step7, | |
step8, | |
step9, | |
]; | |
let stepsToExecute = filterSteps( | |
unfilteredSteps, | |
new Set<"LOGIN" | "COOKIE_BANNER" | null>(["LOGIN", "COOKIE_BANNER"]) | |
); | |
const onlyCookieBannerSkipped = filterSteps( | |
unfilteredSteps, | |
new Set<"LOGIN" | "COOKIE_BANNER" | null>(["COOKIE_BANNER"]) | |
); | |
let originalExceptionBeforeSharedAuthStateRetry: unknown = undefined; | |
for (let i = 0; i < stepsToExecute.length; i++) { | |
try { | |
try { | |
await stepsToExecute[i]?.fn(activePage); | |
} catch (innerError) { | |
const errorMessage = | |
innerError instanceof Error | |
? innerError.message | |
: "unknown error"; | |
const strictModeViolation = /.+strict mode violation.+/; | |
if (errorMessage.match(strictModeViolation) !== null) { | |
// retry due to https://github.com/microsoft/playwright/issues/32852 | |
await activePage.waitForTimeout(5000); | |
await stepsToExecute[i]?.fn(activePage); | |
} else { | |
throw innerError; | |
} | |
} | |
} catch (e) { | |
if (newPage || activePage.isClosed()) { | |
if (newPage) { | |
activePage = newPage; | |
newPage = undefined; | |
} else if (activePage.isClosed()) { | |
const previousPage = context.pages().at(-1); | |
if (!previousPage) { | |
throw new Error("No open page left, aborting run!"); | |
} | |
activePage = previousPage; | |
} | |
i -= 1; | |
continue; | |
} | |
if (stepsToExecute[i]?.ignoreFailure) { | |
continue; | |
} | |
if (originalExceptionBeforeSharedAuthStateRetry) { | |
throw originalExceptionBeforeSharedAuthStateRetry; | |
} | |
if (stepsToExecute !== onlyCookieBannerSkipped) { | |
originalExceptionBeforeSharedAuthStateRetry = e; | |
stepsToExecute = onlyCookieBannerSkipped; | |
i = 0; | |
continue; | |
} | |
throw e; | |
} | |
} | |
if (stepsToExecute.at(-1)?.type === "LOGIN") { | |
if (!fs.existsSync(".auth/state.json")) { | |
const storageState = await context.storageState(); | |
try { | |
await fsPromises.mkdir(path.parse(".auth/state.json").dir, { | |
recursive: true, | |
}); | |
} catch (error: unknown) { | |
if ( | |
error && | |
typeof error === "object" && | |
"code" in error && | |
error.code == "EEXIST" | |
) { | |
return; | |
} | |
throw error; | |
} | |
await fsPromises.writeFile( | |
".auth/state.json", | |
JSON.stringify({ | |
...storageState, | |
octomind: { | |
urlAfterLogin: activePage.url(), | |
}, | |
}) | |
); | |
} | |
} | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment