Skip to content
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
73 changes: 73 additions & 0 deletions packages/injected/src/bidiInsertText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Copyright (c) Microsoft Corporation.
*
* 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 type { InjectedScript } from './injectedScript';

function bidiInsertText(this: InjectedScript, text: string): Element | undefined {
let element = this.document.activeElement;
while (element?.shadowRoot)
element = element.shadowRoot.activeElement;
if (!element)
return;
const elementType = element.nodeName.toLocaleLowerCase();
if (elementType === 'iframe' || elementType === 'frame') {
// The focused element lives inside a nested frame. Hand the frame element
// back to the caller so it can recurse into that frame.
return element;
} else if (elementType === 'input' || elementType === 'textarea') {
const inputElement = element as HTMLInputElement | HTMLTextAreaElement;
const start = inputElement.selectionStart;
if (start === null) {
inputElement.value += text;
} else {
let value = inputElement.value;
value = value.substring(0, start) + text + value.substring(inputElement.selectionEnd!);
inputElement.value = value;
const caretPosition = start + text.length;
inputElement.setSelectionRange(caretPosition, caretPosition);
}
inputElement.dispatchEvent(new InputEvent('input', { data: text, bubbles: true, composed: true }));
} else if (element instanceof HTMLElement && element.isContentEditable) {
const selection = this.window.getSelection()!;
let range;
if (selection.rangeCount)
range = selection.getRangeAt(0);
if (!range || !element.contains(range.commonAncestorContainer)) {
range = this.document.createRange();
range.selectNodeContents(element);
range.collapse(true);
}
range.deleteContents();
const lines = text.split('\n');
for (let i = lines.length - 1; i >= 0; i--) {
range.insertNode(this.document.createTextNode(lines[i]));
if (i > 0)
range.insertNode(this.document.createElement('br'));
}
range.collapse();
selection.removeAllRanges();
selection.addRange(range);
element.dispatchEvent(new InputEvent('input', { data: text, bubbles: true, composed: true }));
}
}

export class BidiInsertTextInstaller {
constructor(injectedScript: InjectedScript) {
injectedScript.bidiInsertText = bidiInsertText.bind(injectedScript);
}
}

export default BidiInsertTextInstaller;
2 changes: 2 additions & 0 deletions packages/injected/src/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,8 @@ export class InjectedScript {
return 'needsinput'; // Still need to input the value.
}

bidiInsertText?: (text: string) => Element | undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we avoid adding this to the main script? packages/injected/src/recorder/pollingRecorder.ts adds a rich API and doesn't leak any of it into the main InjectedScript, let's employ the same approach here.


selectText(node: Node): 'error:notconnected' | 'done' {
const element = this.retarget(node, 'follow-label');
if (!element)
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/server/bidi/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@utils/**
@isomorphic/**
../
../../generated/
./third_party/

[bidiOverCdp.ts]
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/bidi/bidiBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { bidiBytesValueToString } from './bidiNetworkManager';
import { BidiPage, kPlaywrightBindingChannel } from './bidiPage';
import { PageBinding } from '../page';
import * as bidi from './third_party/bidiProtocol';
import * as rawBidiInsertTextSource from '../../generated/bidiInsertTextSource';

import type { RegisteredListener } from '@utils/eventsHelper';
import type { BrowserOptions } from '../browser';
Expand Down Expand Up @@ -226,6 +227,7 @@ export class BidiBrowserContext extends BrowserContext {
const promises: Promise<any>[] = [
super.initialize(),
];
promises.push(this.extendInjectedScript(rawBidiInsertTextSource.source));
const downloadBehavior: bidi.Browser.DownloadBehavior = this._options.acceptDownloads === 'accept' ?
{ type: 'allowed', destinationFolder: this._browser.options.downloadsPath } :
{ type: 'denied' };
Expand Down
39 changes: 24 additions & 15 deletions packages/playwright-core/src/server/bidi/bidiInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,19 @@ import { resolveSmartModifierString } from '../input';
import { getBidiKeyValue } from './third_party/bidiKeyboard';
import * as bidi from './third_party/bidiProtocol';

import type { Frame } from '../frames';
import type * as input from '../input';
import type * as types from '../types';
import type { BidiSession } from './bidiConnection';
import type { BidiPage } from './bidiPage';
import type { Progress } from '../progress';
import type { FrameExecutionContext } from '../dom';

export class RawKeyboardImpl implements input.RawKeyboard {
private _session: BidiSession;
private _page: BidiPage;

constructor(session: BidiSession) {
this._session = session;
}

setSession(session: BidiSession) {
this._session = session;
constructor(page: BidiPage) {
this._page = page;
}

async keydown(progress: Progress, modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise<void> {
Expand All @@ -49,18 +48,28 @@ export class RawKeyboardImpl implements input.RawKeyboard {
}

async sendText(progress: Progress, text: string): Promise<void> {
const actions: bidi.Input.KeySourceAction[] = [];
for (const char of text) {
const value = getBidiKeyValue(char);
actions.push({ type: 'keyDown', value });
actions.push({ type: 'keyUp', value });
let frame: Frame | null = this._page._page.mainFrame();
while (frame) {
const handle = await progress.race((async () => {
const context: FrameExecutionContext = await frame!.mainContext();
const injected = await context.injectedScript();
return injected.evaluateHandle((injected, value) => injected.bidiInsertText!(value), text);
})());
// insertText returns the focused frame element when the focus lives inside a nested frame,
// otherwise the text was inserted (if possible) and we are done.
const element = handle.asElement();
if (!element) {
handle.dispose();
return;
}
frame = await element.contentFrame(progress);
element.dispose();
}
await this._performActions(progress, actions);
}

private async _performActions(progress: Progress, actions: bidi.Input.KeySourceAction[]) {
await progress.race(this._session.send('input.performActions', {
context: this._session.sessionId,
await progress.race(this._page._session.send('input.performActions', {
context: this._page._session.sessionId,
actions: [
{
type: 'key',
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/bidi/bidiPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class BidiPage implements PageDelegate {
constructor(browserContext: BidiBrowserContext, bidiSession: BidiSession, opener: BidiPage | null) {
this._session = bidiSession;
this._opener = opener;
this.rawKeyboard = new RawKeyboardImpl(bidiSession);
this.rawKeyboard = new RawKeyboardImpl(this);
this.rawMouse = new RawMouseImpl(bidiSession);
this.rawTouchscreen = new RawTouchscreenImpl(bidiSession);
this._contextIdToContext = new Map();
Expand Down
2 changes: 0 additions & 2 deletions tests/bidi/expectations/moz-firefox-nightly-page.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,7 @@ page/page-goto.spec.ts › should send referer of cross-origin URL [fail]
page/page-history.spec.ts › goBack/goForward should work with bfcache-able pages [timeout]
page/page-history.spec.ts › page.goBack should work for file urls [timeout]
page/page-history.spec.ts › regression test for issue 20791 [flaky]
page/page-keyboard.spec.ts › insertText should only emit input event [fail]
page/page-keyboard.spec.ts › should press audio and media control keys [fail]
page/page-keyboard.spec.ts › should send a character with insertText [fail]
page/page-mouse.spec.ts › should always round down [fail]
page/page-mouse.spec.ts › should dblclick the div [timeout]
page/page-network-request.spec.ts › should not allow to access frame on popup main request [fail]
Expand Down
4 changes: 2 additions & 2 deletions tests/page/page-fill.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ it('should not double-fill in contenteditable with beforeinput handler in Firefo
type: 'issue',
description: 'https://github.com/microsoft/playwright/issues/36715'
}
}, async ({ page, browserName }) => {
it.fixme(browserName === 'firefox', 'https://github.com/microsoft/playwright/issues/36715');
}, async ({ page, browserName, isBidi }) => {
it.fixme(browserName === 'firefox' && !isBidi, 'https://github.com/microsoft/playwright/issues/36715');

await page.setContent(`
<div id="editor" contenteditable="true"></div>
Expand Down
6 changes: 6 additions & 0 deletions utils/generate_injected.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ const injectedScripts = [
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true,
],
[
path.join(ROOT, 'packages', 'injected', 'src', 'bidiInsertText.ts'),
path.join(ROOT, 'packages', 'injected', 'lib'),
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true,
],
[
path.join(ROOT, 'packages', 'injected', 'src', 'webview', 'webViewInput.ts'),
path.join(ROOT, 'packages', 'injected', 'lib'),
Expand Down
Loading