diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 6eb07b7ff0..338712fff9 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -40,6 +40,7 @@ export class CRBrowser extends BrowserBase { _backgroundPages = new Map(); _serviceWorkers = new Map(); _devtools?: CRDevTools; + _isMac = false; private _tracingRecording = false; private _tracingPath: string | null = ''; @@ -50,6 +51,8 @@ export class CRBrowser extends BrowserBase { const browser = new CRBrowser(connection, options); browser._devtools = devtools; const session = connection.rootSession; + const version = await session.send('Browser.getVersion'); + browser._isMac = version.userAgent.includes('Macintosh'); if (!options.persistent) { await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }); return browser; diff --git a/src/chromium/crInput.ts b/src/chromium/crInput.ts index 7a0af061da..15e30894a2 100644 --- a/src/chromium/crInput.ts +++ b/src/chromium/crInput.ts @@ -18,6 +18,8 @@ import * as input from '../input'; import * as types from '../types'; import { CRSession } from './crConnection'; +import { macEditingCommands } from '../macEditingCommands'; +import { helper } from '../helper'; function toModifiersMask(modifiers: Set): number { let mask = 0; @@ -33,18 +35,36 @@ function toModifiersMask(modifiers: Set): number { } export class RawKeyboardImpl implements input.RawKeyboard { - private _client: CRSession; + constructor( + private _client: CRSession, + private _isMac: boolean, + ) { } - constructor(client: CRSession) { - this._client = client; + _commandsForCode(code: string, modifiers: Set) { + if (!this._isMac) + return []; + const parts = []; + for (const modifier of (['Shift', 'Control', 'Alt', 'Meta']) as types.KeyboardModifier[]) { + if (modifiers.has(modifier)) + parts.push(modifier); + } + parts.push(code); + const shortcut = parts.join('+'); + let commands = macEditingCommands[shortcut] || []; + if (helper.isString(commands)) + commands = [commands]; + // remove the trailing : to match the Chromium command names. + return commands.map(c => c.substring(0, c.length - 1)); } async keydown(modifiers: Set, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise { + const commands = this._commandsForCode(code, modifiers); await this._client.send('Input.dispatchKeyEvent', { type: text ? 'keyDown' : 'rawKeyDown', modifiers: toModifiersMask(modifiers), windowsVirtualKeyCode: keyCodeWithoutLocation, code, + commands, key, text, unmodifiedText: text, diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 96eb895831..30d3f03976 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -65,7 +65,7 @@ export class CRPage implements PageDelegate { constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, hasUIWindow: boolean) { this._targetId = targetId; this._opener = opener; - this.rawKeyboard = new RawKeyboardImpl(client); + this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._isMac); this.rawMouse = new RawMouseImpl(client); this._pdf = new CRPDF(client); this._coverage = new CRCoverage(client); diff --git a/src/macEditingCommands.ts b/src/macEditingCommands.ts index 6004964ff9..d7867da029 100644 --- a/src/macEditingCommands.ts +++ b/src/macEditingCommands.ts @@ -17,7 +17,6 @@ export const macEditingCommands: {[key: string]: string|string[]} = { 'Backspace': 'deleteBackward:', - 'Tab': 'insertTab:', 'Enter': 'insertNewline:', 'NumpadEnter': 'insertNewline:', 'Escape': 'cancelOperation:', @@ -34,7 +33,6 @@ export const macEditingCommands: {[key: string]: string|string[]} = { 'Shift+Backspace': 'deleteBackward:', 'Shift+Enter': 'insertNewline:', 'Shift+NumpadEnter': 'insertNewline:', - 'Shift+Tab': 'insertBacktab:', 'Shift+Escape': 'cancelOperation:', 'Shift+ArrowUp': 'moveUpAndModifySelection:', 'Shift+ArrowDown': 'moveDownAndModifySelection:', @@ -87,7 +85,6 @@ export const macEditingCommands: {[key: string]: string|string[]} = { 'Shift+Control+ArrowLeft': 'moveToLeftEndOfLineAndModifySelection:', 'Shift+Control+ArrowRight': 'moveToRightEndOfLineAndModifySelection:', 'Alt+Backspace': 'deleteWordBackward:', - 'Alt+Tab': 'insertTabIgnoringFieldEditor:', 'Alt+Enter': 'insertNewlineIgnoringFieldEditor:', 'Alt+NumpadEnter': 'insertNewlineIgnoringFieldEditor:', 'Alt+Escape': 'complete:', @@ -99,7 +96,6 @@ export const macEditingCommands: {[key: string]: string|string[]} = { 'Alt+PageUp': 'pageUp:', 'Alt+PageDown': 'pageDown:', 'Shift+Alt+Backspace': 'deleteWordBackward:', - 'Shift+Alt+Tab': 'insertTabIgnoringFieldEditor:', 'Shift+Alt+Enter': 'insertNewlineIgnoringFieldEditor:', 'Shift+Alt+NumpadEnter': 'insertNewlineIgnoringFieldEditor:', 'Shift+Alt+Escape': 'complete:', diff --git a/test/focus.jest.js b/test/focus.jest.js index e486707eaa..82f30f9ec3 100644 --- a/test/focus.jest.js +++ b/test/focus.jest.js @@ -14,7 +14,7 @@ * limitations under the License. */ -const {FFOX, CHROMIUM, LINUX, WEBKIT} = testOptions; +const {FFOX, CHROMIUM, LINUX, WEBKIT, MAC} = testOptions; describe('Page.focus', function() { it('should work', async function({page, server}) { @@ -44,7 +44,7 @@ describe('Page.focus', function() { expect(focused).toBe(true); expect(blurred).toBe(true); }); - it.fail(WEBKIT && !LINUX)('should traverse focus', async function({page, server}) { + it('should traverse focus', async function({page}) { await page.setContent(``); let focused = false; await page.exposeFunction('focusEvent', () => focused = true); @@ -59,4 +59,45 @@ describe('Page.focus', function() { expect(await page.$eval('#i1', e => e.value)).toBe('First'); expect(await page.$eval('#i2', e => e.value)).toBe('Last'); }); + it('should traverse focus in all directions', async function({page}) { + await page.setContent(``); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.value)).toBe('1'); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.value)).toBe('2'); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.value)).toBe('3'); + await page.keyboard.press('Shift+Tab'); + expect(await page.evaluate(() => document.activeElement.value)).toBe('2'); + await page.keyboard.press('Shift+Tab'); + expect(await page.evaluate(() => document.activeElement.value)).toBe('1'); + }); + // Chromium and WebKit both have settings for tab traversing all links, but + // it is only on by default in WebKit. + it.skip(!MAC || !WEBKIT)('should traverse only form elements', async function({page}) { + await page.setContent(` + + + link + + `); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input-1'); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input-2'); + await page.keyboard.press('Shift+Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input-1'); + await page.keyboard.press('Alt+Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('button'); + await page.keyboard.press('Alt+Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('link'); + await page.keyboard.press('Alt+Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input-2'); + await page.keyboard.press('Alt+Shift+Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('link'); + await page.keyboard.press('Alt+Shift+Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('button'); + await page.keyboard.press('Alt+Shift+Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input-1'); + }); }); diff --git a/test/keyboard.jest.js b/test/keyboard.jest.js index 600f06ac88..7f67b50545 100644 --- a/test/keyboard.jest.js +++ b/test/keyboard.jest.js @@ -290,7 +290,7 @@ describe('Keyboard', function() { await textarea.type('👹 Tokyo street Japan 🇯🇵'); expect(await frame.$eval('textarea', textarea => textarea.value)).toBe('👹 Tokyo street Japan 🇯🇵'); }); - it.skip(CHROMIUM && MAC)('should handle selectAll', async ({page, server}) => { + it('should handle selectAll', async ({page, server}) => { await page.goto(server.PREFIX + '/input/textarea.html'); const textarea = await page.$('textarea'); await textarea.type('some text'); @@ -318,6 +318,15 @@ describe('Keyboard', function() { await page.keyboard.press('Backspace'); expect(await page.$eval('textarea', textarea => textarea.value)).toBe('some tex'); }); + it.skip(!MAC)('should support MacOS shortcuts', async ({page, server}) => { + await page.goto(server.PREFIX + '/input/textarea.html'); + const textarea = await page.$('textarea'); + await textarea.type('some text'); + // select one word backwards + await page.keyboard.press('Shift+Control+Alt+KeyB'); + await page.keyboard.press('Backspace'); + expect(await page.$eval('textarea', textarea => textarea.value)).toBe('some '); + }); it('should press the meta key', async ({page}) => { const lastEvent = await captureLastKeydown(page); await page.keyboard.press('Meta');