diff --git a/docs/src/codegen.md b/docs/src/codegen.md index 1b1d06d445d4d..731ffe6e26d77 100644 --- a/docs/src/codegen.md +++ b/docs/src/codegen.md @@ -438,6 +438,26 @@ playwright codegen --user-data-dir=/path/to/your/browser/data/ github.com/micros pwsh bin/Debug/netX/playwright.ps1 codegen --user-data-dir=/path/to/your/browser/data/ github.com/microsoft/playwright ``` +#### Authenticate with HTTP credentials + +Run `codegen` with `--http-credentials` to authenticate with [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication). Unlike credentials embedded in the URL, they are sent to any origin that requests them during the recording session and are included in the generated code. + +```bash js +npx playwright codegen --http-credentials="username:password" example.com +``` + +```bash java +mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args='codegen --http-credentials="username:password" example.com' +``` + +```bash python +playwright codegen --http-credentials="username:password" example.com +``` + +```bash csharp +pwsh bin/Debug/netX/playwright.ps1 codegen --http-credentials="username:password" example.com +``` + ## Record using custom setup If you would like to use codegen in some non-standard setup (for example, use [`method: BrowserContext.route`]), it is possible to call [`method: Page.pause`] that will open a separate window with codegen controls. diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index b88ba6d0126ef..42e3a0842cc5a 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -135,6 +135,8 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp } const childAriaNode = visible ? toAriaNode(element, options, nameSourceElements) : null; + if (childAriaNode && element.getAttribute('aria-hidden')?.toLowerCase() === 'true') + childAriaNode.props['aria-hidden'] = 'true'; let elementInfo: { element: Element, nameFromContentRefs: string[] } | undefined; if (childAriaNode) { if (childAriaNode.ref) { @@ -502,6 +504,8 @@ export function renderAriaTreeAsJSON(ariaSnapshot: AriaSnapshot, publicOptions: node.url = ariaNode.props.url; if (ariaNode.props.placeholder !== undefined) node.placeholder = ariaNode.props.placeholder; + if (ariaNode.props['aria-hidden'] !== undefined) + node.ariaHidden = true; const singleTextChild = ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string' ? ariaNode.children[0] : undefined; const isAtDepthLimit = !!publicOptions.depth && depth === publicOptions.depth; diff --git a/packages/isomorphic/ariaSnapshot.ts b/packages/isomorphic/ariaSnapshot.ts index d6b2eef0e9131..86aea7852ed08 100644 --- a/packages/isomorphic/ariaSnapshot.ts +++ b/packages/isomorphic/ariaSnapshot.ts @@ -66,6 +66,7 @@ export type AriaNodeJSON = { level?: number; pressed?: true | 'mixed'; selected?: true; + ariaHidden?: true; ref?: string; cursor?: 'pointer'; box?: { x: number, y: number, width: number, height: number }; diff --git a/packages/isomorphic/ariaSnapshotRenderer.ts b/packages/isomorphic/ariaSnapshotRenderer.ts index bbf4816db7090..cc0cf073a4ada 100644 --- a/packages/isomorphic/ariaSnapshotRenderer.ts +++ b/packages/isomorphic/ariaSnapshotRenderer.ts @@ -67,6 +67,8 @@ export function renderAriaSnapshotAsYaml(snapshot: AriaSnapshotJSON, options: Ar key += ` [pressed]`; if (node.selected === true) key += ` [selected]`; + if (node.ariaHidden) + key += ` [aria-hidden]`; if (node.ref) { key += ` [ref=${node.ref}]`; if (node.cursor === 'pointer') diff --git a/packages/playwright-core/src/cli/browserActions.ts b/packages/playwright-core/src/cli/browserActions.ts index 56d32b5654f6c..741cc6293c497 100644 --- a/packages/playwright-core/src/cli/browserActions.ts +++ b/packages/playwright-core/src/cli/browserActions.ts @@ -38,6 +38,7 @@ export type Options = { colorScheme?: string; device?: string; geolocation?: string; + httpCredentials?: string; ignoreHttpsErrors?: boolean; lang?: string; loadStorage?: string; @@ -125,6 +126,18 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro contextOptions.permissions = ['geolocation']; } + // HTTP credentials + + if (options.httpCredentials) { + const separator = options.httpCredentials.indexOf(':'); + if (separator === -1) + throw new Error('Invalid http credentials format: use "username:password", for example --http-credentials="admin:secret"'); + contextOptions.httpCredentials = { + username: options.httpCredentials.substring(0, separator), + password: options.httpCredentials.substring(separator + 1), + }; + } + // User agent if (options.userAgent) diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 93a2323ba1d55..efb1040a77678 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -276,6 +276,7 @@ function commandWithOpenOptions(command: string, description: string, options: a .option('--color-scheme ', 'emulate preferred color scheme, "light" or "dark"') .option('--device ', 'emulate device, for example "iPhone 11"') .option('--geolocation ', 'specify geolocation coordinates, for example "37.819722,-122.478611"') + .option('--http-credentials ', 'specify HTTP authentication credentials as "username:password", for example --http-credentials="admin:secret"') .option('--ignore-https-errors', 'ignore https errors') .option('--load-storage ', 'load context storage state from the file, previously saved with --save-storage') .option('--lang ', 'specify language / locale, for example "en-GB"') diff --git a/tests/library/inspector/cli-codegen-javascript.spec.ts b/tests/library/inspector/cli-codegen-javascript.spec.ts index 31eddfa5f5a1f..29d7114a61ccb 100644 --- a/tests/library/inspector/cli-codegen-javascript.spec.ts +++ b/tests/library/inspector/cli-codegen-javascript.spec.ts @@ -48,6 +48,26 @@ test('should print the correct context options for custom settings', async ({ br }); +test('should work with --http-credentials', async ({ browserName, channel, runCLI, server }) => { + server.setAuth('/empty.html', 'user', 'pass'); + const cli = runCLI(['--http-credentials=user:pass', '--target=javascript', server.EMPTY_PAGE]); + const expectedResult = `const { ${browserName} } = require('playwright'); + +(async () => { + const browser = await ${browserName}.launch({ + ${launchOptions(channel)} + }); + const context = await browser.newContext({ + httpCredentials: { + password: 'pass', + username: 'user' + } + }); + const page = await context.newPage(); + await page.goto('${server.EMPTY_PAGE}');`; + await cli.waitFor(expectedResult); +}); + test('should print the correct context options when using a device', async ({ browserName, channel, runCLI, server }) => { test.skip(browserName !== 'chromium'); diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index e0051d11afe21..27f0eea5ca411 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -931,3 +931,43 @@ it('should limit depth', async ({ page }) => { - listitem [ref=e8] `); }); + +it('should annotate aria-hidden elements', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42223' } }, async ({ page }) => { + await page.setContent(` +

Visible heading

+ +

After hidden

+ `); + + expect(await snapshotForAI(page)).toContainYaml(` + - generic [active] [ref=e1]: + - heading "Visible heading" [level=2] [ref=e2] + - generic [aria-hidden] [ref=e3]: + - heading [level=1] [ref=e4]: Hidden heading + - paragraph [ref=e5]: Hidden content + - heading "After hidden" [level=2] [ref=e6] + `); + + // Default snapshot excludes aria-hidden elements entirely. + const defaultSnapshot = await page.locator('body').ariaSnapshot(); + expect(defaultSnapshot).not.toContain('Hidden content'); +}); + +it('should only annotate the top element in a hidden subtree', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42223' } }, async ({ page }) => { + await page.setContent(` + + `); + + // Only the element with aria-hidden="true" gets the annotation, not its descendants. + expect(await snapshotForAI(page)).toContainYaml(` + - generic [aria-hidden] [ref=e2]: + - heading [level=1] [ref=e3]: Heading + - paragraph [ref=e4]: Paragraph + `); +});