From 6c9306a72e0f7195a4a6c300645f6089845c9abc Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Wed, 19 Jul 2023 19:42:31 +0200 Subject: [PATCH] feat: add autofill support (#10565) --- docs/api/index.md | 1 + docs/api/puppeteer.autofilldata.md | 17 +++++++ docs/api/puppeteer.elementhandle.autofill.md | 44 +++++++++++++++++ docs/api/puppeteer.elementhandle.md | 1 + .../puppeteer-core/src/api/ElementHandle.ts | 41 ++++++++++++++++ .../src/common/ElementHandle.ts | 14 ++++++ .../src/common/bidi/ElementHandle.ts | 15 ++++++ test/TestExpectations.json | 18 +++++++ test/assets/credit-card.html | 42 ++++++++++++++++ test/src/autofill.spec.ts | 48 +++++++++++++++++++ 10 files changed, 241 insertions(+) create mode 100644 docs/api/puppeteer.autofilldata.md create mode 100644 docs/api/puppeteer.elementhandle.autofill.md create mode 100644 test/assets/credit-card.html create mode 100644 test/src/autofill.spec.ts diff --git a/docs/api/index.md b/docs/api/index.md index 8c254849198..a849c4e6fdc 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -67,6 +67,7 @@ sidebar_label: API | Interface | Description | | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [ActionOptions](./puppeteer.actionoptions.md) | | +| [AutofillData](./puppeteer.autofilldata.md) | | | [BoundingBox](./puppeteer.boundingbox.md) | | | [BoxModel](./puppeteer.boxmodel.md) | | | [BrowserConnectOptions](./puppeteer.browserconnectoptions.md) | Generic browser options that can be passed when launching any browser or when connecting to an existing browser instance. | diff --git a/docs/api/puppeteer.autofilldata.md b/docs/api/puppeteer.autofilldata.md new file mode 100644 index 00000000000..3a2d7c031d1 --- /dev/null +++ b/docs/api/puppeteer.autofilldata.md @@ -0,0 +1,17 @@ +--- +sidebar_label: AutofillData +--- + +# AutofillData interface + +#### Signature: + +```typescript +export interface AutofillData +``` + +## Properties + +| Property | Modifiers | Type | Description | Default | +| ---------- | --------- | --------------------------------------------------------------------------------------- | ----------- | ------- | +| creditCard | | { number: string; name: string; expiryMonth: string; expiryYear: string; cvc: string; } | | | diff --git a/docs/api/puppeteer.elementhandle.autofill.md b/docs/api/puppeteer.elementhandle.autofill.md new file mode 100644 index 00000000000..13a7306620a --- /dev/null +++ b/docs/api/puppeteer.elementhandle.autofill.md @@ -0,0 +1,44 @@ +--- +sidebar_label: ElementHandle.autofill +--- + +# ElementHandle.autofill() method + +If the element is a form input, you can use [ElementHandle.autofill()](./puppeteer.elementhandle.autofill.md) to test if the form is compatible with the browser's autofill implementation. Throws an error if the form cannot be autofilled. + +#### Signature: + +```typescript +class ElementHandle { + autofill(data: AutofillData): Promise; +} +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ------------------------------------------- | ----------- | +| data | [AutofillData](./puppeteer.autofilldata.md) | | + +**Returns:** + +Promise<void> + +## Remarks + +Currently, Puppeteer supports auto-filling credit card information only and in Chrome in the new headless and headful modes only. + +```ts +// Select an input on the credit card form. +const name = await page.waitForSelector('form #name'); +// Trigger autofill with the desired data. +await name.autofill({ + creditCard: { + number: '4444444444444444', + name: 'John Smith', + expiryMonth: '01', + expiryYear: '2030', + cvc: '123', + }, +}); +``` diff --git a/docs/api/puppeteer.elementhandle.md b/docs/api/puppeteer.elementhandle.md index 3e22365b7b8..fa40388fdac 100644 --- a/docs/api/puppeteer.elementhandle.md +++ b/docs/api/puppeteer.elementhandle.md @@ -55,6 +55,7 @@ The constructor for this class is marked as internal. Third-party code should no | [$eval(selector, pageFunction, args)](./puppeteer.elementhandle._eval.md) | |

Runs the given function on the first element matching the given selector in the current element.

If the given function returns a promise, then this method will wait till the promise resolves.

| | [$x(expression)](./puppeteer.elementhandle._x.md) | | | | [asElement()](./puppeteer.elementhandle.aselement.md) | | | +| [autofill(data)](./puppeteer.elementhandle.autofill.md) | | If the element is a form input, you can use [ElementHandle.autofill()](./puppeteer.elementhandle.autofill.md) to test if the form is compatible with the browser's autofill implementation. Throws an error if the form cannot be autofilled. | | [boundingBox()](./puppeteer.elementhandle.boundingbox.md) | | This method returns the bounding box of the element (relative to the main frame), or null if the element is not visible. | | [boxModel()](./puppeteer.elementhandle.boxmodel.md) | | This method returns boxes of the element, or null if the element is not visible. | | [click(this, options)](./puppeteer.elementhandle.click.md) | | This method scrolls element into view if needed, and then uses [Page.mouse](./puppeteer.page.md) to click in the center of the element. If the element is detached from DOM, the method throws an error. | diff --git a/packages/puppeteer-core/src/api/ElementHandle.ts b/packages/puppeteer-core/src/api/ElementHandle.ts index a781c1e58f8..f670fba21b1 100644 --- a/packages/puppeteer-core/src/api/ElementHandle.ts +++ b/packages/puppeteer-core/src/api/ElementHandle.ts @@ -1042,4 +1042,45 @@ export class ElementHandle< assertElementHasWorld(): asserts this { assert(this.executionContext()._world); } + + /** + * If the element is a form input, you can use {@link ElementHandle.autofill} + * to test if the form is compatible with the browser's autofill + * implementation. Throws an error if the form cannot be autofilled. + * + * @remarks + * + * Currently, Puppeteer supports auto-filling credit card information only and + * in Chrome in the new headless and headful modes only. + * + * ```ts + * // Select an input on the credit card form. + * const name = await page.waitForSelector('form #name'); + * // Trigger autofill with the desired data. + * await name.autofill({ + * creditCard: { + * number: '4444444444444444', + * name: 'John Smith', + * expiryMonth: '01', + * expiryYear: '2030', + * cvc: '123', + * }, + * }); + * ``` + */ + autofill(data: AutofillData): Promise; + autofill(): Promise { + throw new Error('Not implemented'); + } +} + +export interface AutofillData { + creditCard: { + // See https://chromedevtools.github.io/devtools-protocol/tot/Autofill/#type-CreditCard. + number: string; + name: string; + expiryMonth: string; + expiryYear: string; + cvc: string; + }; } diff --git a/packages/puppeteer-core/src/common/ElementHandle.ts b/packages/puppeteer-core/src/common/ElementHandle.ts index b0a4af6580b..af4d74e5f40 100644 --- a/packages/puppeteer-core/src/common/ElementHandle.ts +++ b/packages/puppeteer-core/src/common/ElementHandle.ts @@ -17,6 +17,7 @@ import {Protocol} from 'devtools-protocol'; import { + AutofillData, BoundingBox, BoxModel, ClickOptions, @@ -571,6 +572,19 @@ export class CDPElementHandle< return imageData; } + + override async autofill(data: AutofillData): Promise { + const nodeInfo = await this.client.send('DOM.describeNode', { + objectId: this.handle.id, + }); + const fieldId = nodeInfo.node.backendNodeId; + const frameId = this.#frame._id; + await this.client.send('Autofill.trigger', { + fieldId, + frameId, + card: data.creditCard, + }); + } } function computeQuadArea(quad: Point[]): number { diff --git a/packages/puppeteer-core/src/common/bidi/ElementHandle.ts b/packages/puppeteer-core/src/common/bidi/ElementHandle.ts index 2b709c5cf50..08faf01f173 100644 --- a/packages/puppeteer-core/src/common/bidi/ElementHandle.ts +++ b/packages/puppeteer-core/src/common/bidi/ElementHandle.ts @@ -17,6 +17,7 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import { + AutofillData, ElementHandle as BaseElementHandle, ClickOptions, } from '../../api/ElementHandle.js'; @@ -70,6 +71,20 @@ export class ElementHandle< return; } + override async autofill(data: AutofillData): Promise { + const client = this.#frame.context().cdpSession; + const nodeInfo = await client.send('DOM.describeNode', { + objectId: this.handle.id, + }); + const fieldId = nodeInfo.node.backendNodeId; + const frameId = this.#frame._id; + await client.send('Autofill.trigger', { + fieldId, + frameId, + card: data.creditCard, + }); + } + // /////////////////// // // Input methods // // /////////////////// diff --git a/test/TestExpectations.json b/test/TestExpectations.json index dfb79be5be5..1776e48a3ae 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -5,6 +5,18 @@ "parameters": ["webDriverBiDi"], "expectations": ["SKIP", "TIMEOUT"] }, + { + "testIdPattern": "[autofill.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[autofill.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["FAIL"] + }, { "testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests *", "platforms": ["darwin", "linux", "win32"], @@ -299,6 +311,12 @@ "parameters": ["webDriverBiDi"], "expectations": ["PASS"] }, + { + "testIdPattern": "[autofill.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "headless"], + "expectations": ["FAIL"] + }, { "testIdPattern": "[browser.spec] Browser specs Browser.isConnected should set the browser connected state", "platforms": ["darwin", "linux", "win32"], diff --git a/test/assets/credit-card.html b/test/assets/credit-card.html new file mode 100644 index 00000000000..101013a0ca2 --- /dev/null +++ b/test/assets/credit-card.html @@ -0,0 +1,42 @@ + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ +
+ + \ No newline at end of file diff --git a/test/src/autofill.spec.ts b/test/src/autofill.spec.ts new file mode 100644 index 00000000000..7c80c73b86d --- /dev/null +++ b/test/src/autofill.spec.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Autofill', function () { + setupTestBrowserHooks(); + describe('ElementHandle.autofill', () => { + it('should fill out a credit card', async () => { + const {page, server} = await getTestState(); + await page.goto(server.PREFIX + '/credit-card.html'); + const name = await page.waitForSelector('#name'); + await name!.autofill({ + creditCard: { + number: '4444444444444444', + name: 'John Smith', + expiryMonth: '01', + expiryYear: '2030', + cvc: '123', + }, + }); + expect( + await page.evaluate(() => { + const result = []; + for (const el of document.querySelectorAll('input')) { + result.push(el.value); + } + return result.join(','); + }) + ).toBe('John Smith,4444444444444444,01,2030,Submit'); + }); + }); +});