From d72f248d91e15dccb30476d5ca7aa2d158b24b37 Mon Sep 17 00:00:00 2001 From: Andrey Belym Date: Tue, 18 Apr 2017 14:34:11 +0300 Subject: [PATCH] Fix remarks --- .eslintrc | 2 +- README.md | 33 +++++++++++++++-- package.json | 4 +- src/index.js | 71 +++++++++++++++++++++++++++++++----- test/fixtures/dialog-test.js | 6 +-- test/fixtures/menu-test.js | 10 ++--- 6 files changed, 100 insertions(+), 26 deletions(-) diff --git a/.eslintrc b/.eslintrc index 9788343..2f825fb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -24,7 +24,7 @@ "no-new-wrappers": "error", "no-octal-escape": "error", "no-proto": "error", - "no-return-assign": "error", + "no-return-assign": "off", "no-script-url": "error", "no-sequences": "error", "no-shadow": "error", diff --git a/README.md b/README.md index 82aaed6..36aaa8e 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,40 @@ testCafe ## Config Below is complete list of all configuration options that you can put into `.testcafe-electron-rc`. - - `mainWindowUrl` __(mandatory)__ - specifies URL used for the main window page of the appplication. + - `mainWindowUrl` __(required)__ - specifies URL used for the main window page of the appplication. If you use `file://` urls, you can also specify a relative (to the application directory) or an absolute path to the file of the page. - `appPath` __(optional)__ - alters path to the application. By default, application path is the part after `electron:` of the string used in TestCafe CLI or API. You can override it by specifying an absolute path, or append a relative path from 'appPath'. - `appArgs` __(optional)__ - overrides application commandline arguments with the values specified in this option. It should be an array or an object with numeric keys. - `disableNavigateEvents` __(optional)__ - if you use `did-navigate` ow `will-navigate` webContent events to prevent navigation, you should disable it by setting this option to `true`. - - `openDevTools` __(optional)__ - if `true`, DevTools will be opened just before tests start - . + - `openDevTools` __(optional)__ - if `true`, DevTools will be opened just before tests start. + +## Helpers +You can use some helper functions from provider in your test files. Use ES6 import statement to get them, like +```js +import { getMainMenu, clickOnMenuItem } from 'testcafe-browser-provider-electron'; +``` + - `async function getMenuItem (menuItemSelector)` - get a snapshot of the given menu item. `menuItemSelector` is a string that consists + of menu type and menu item labels, separated by the `>` sign, e.g. `Main Menu > File > Open` or `Context Menu > Undo`. + The `Main Menu` menu type can be skipped. If there are a number of the specified menu items with the same label on the same level, + you can specify a one-based index in square brackets, e.g. `Main Menu > Window > My Window [2]` selects the second menu item with + label `My Window` in the `Window` menu. Check properties available in the snapshot [here](https://github.com/electron/electron/blob/master/docs/api/menu-item.md). + + - `async function getMainMenu ()` - get a snapshot of application main menu. You can check properties available in the snapshot + [here](https://github.com/electron/electron/blob/master/docs/api/menu.md). + + - `async function getContextMenu ()` - get a snapshot of context menu. You can check properties available in the snapshot + [here](https://github.com/electron/electron/blob/master/docs/api/menu.md), + + - `async function clickOnMenuItem (menuItem, modifiers)` - perform a click on the given `menuItem`. It can be a string, + in this case it will be passed to the `getMenuItem` function and the returned value will be used; or a value retrieved + with `getMenuItem`, `getMainMenu`, `getContextMenu` functions. + Also you can pass state of control keys (`Ctrl`, `Alt`, `Meta` etc.) in the `modifiers` argument, e.g. the default is + `{ shift: false, ctrl: false, alt: false, meta: false}`. Examples: `clickOnMenuItem('Main Menu > File > Open')`, + `clickOnMenuItem('File > Open')`, `clickOnMenuItem((await getMainMenu()).items[0].submenu.items[0])`, + + - `async function setElectronDialogHandler (handler, dependencies)` - set a function `function handler (type, ...args)` that will handle native Electron dialogs. Specify global variables of the function in the `dependencies` argument. + Handler function must be synchronous and will be invoked with the dialog type `type`, and the arguments `args` from the original dialog function. + ## Author Developer Express Inc. (https://devexpress.com) diff --git a/package.json b/package.json index 06039be..25f9759 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "testcafe-browser-provider-electron", "version": "0.0.1", - "description": "electron TestCafe browser provider plugin.", + "description": "TestCafe browser provider plugin for testing applications built with Electron.", "repository": "https://github.com/DevExpress/testcafe-browser-provider-electron", "homepage": "https://github.com/DevExpress/testcafe-browser-provider-electron", "author": { @@ -51,7 +51,7 @@ "gulp-eslint": "^3.0.1", "node-version": "^1.0.0", "publish-please": "^2.1.4", - "testcafe": "alpha", + "testcafe": "*", "tmp": "0.0.28" } } diff --git a/src/index.js b/src/index.js index 262e98c..57ea0ff 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,17 @@ import { ClientFunction } from 'testcafe'; const exec = promisify(nodeExec, Promise); +const simplifyMenuItemLabel = label => label.replace(/\s/g, '').toLowerCase(); + +const MENU_ITEM_INDEX_RE = /\[(\d+)\]$/; + +const MODIFIERS_KEYS_MAP = { + 'shift': 'shiftKey', + 'ctrl': 'ctrlKey', + 'alt': 'altKey', + 'meta': 'metaKey' +}; + /* eslint-disable no-undef */ const getMainMenu = ClientFunction(() => { return require('electron').remote.Menu.getApplicationMenu(); @@ -23,11 +34,11 @@ const getContextMenu = ClientFunction(() => { return require('electron').remote.getGlobal(contextMenuGlobal); }, { dependencies: CONSTANTS }); -const doMenuClick = ClientFunction((menuSnapshot, modifiers) => { +const doClickOnMenuItem = ClientFunction((menuType, menuItemIndex, modifiers) => { var remote = require('electron').remote; - var menu = null; + var menu = null; - switch (menuSnapshot[typeProperty]) { + switch (menuType) { case mainMenuType: menu = remote.Menu.getApplicationMenu(); break; @@ -40,7 +51,7 @@ const doMenuClick = ClientFunction((menuSnapshot, modifiers) => { if (!menu) return; - var menuItem = menuSnapshot[indexProperty] + var menuItem = menuItemIndex .reduce((m, i) => m.items[i].submenu || m.items[i], menu); menuItem.click(menuItem, require('electron').remote.getCurrentWindow(), modifiers); @@ -80,10 +91,35 @@ function wrapMenu (type, menu, index = []) { if (item.submenu) wrapMenu(type, item.submenu, currentIndex); } + return menu; } -export default { +function findMenuItem (menu, menuItemPath) { + var menuItem = null; + + for (let i = 0; menu && i < menuItemPath.length; i++) { + const indexMatch = menuItemPath[i].match(MENU_ITEM_INDEX_RE); + const index = indexMatch ? Number(indexMatch[1]) - 1 : 0; + const label = indexMatch ? menuItemPath[i].replace(MENU_ITEM_INDEX_RE, '') : menuItemPath[i]; + + menuItem = menu.items.filter(item => simplifyMenuItemLabel(item.label) === label)[index]; + + menu = menuItem && menuItem.submenu || null; + } + + return menuItem || null; +} + +function ensureModifiers (srcModifiers = {}) { + var result = {}; + + Object.keys(MODIFIERS_KEYS_MAP).forEach(mod => result[MODIFIERS_KEYS_MAP[mod]] = !!srcModifiers[mod]); + + return result; +} + +const ElectronBrowserProvider = { isMultiBrowser: true, async openBrowser (id, pageUrl, mainPath) { @@ -120,16 +156,21 @@ export default { }, //Helpers - async mainMenu () { + async getMainMenu () { return wrapMenu(CONSTANTS.mainMenuType, await getMainMenu()); }, - async contextMenu () { + async getContextMenu () { return wrapMenu(CONSTANTS.contextMenuType, await getContextMenu()); }, - async menuClick (menuSnapshot, modifiers = {}) { - await doMenuClick(menuSnapshot, modifiers); + async clickOnMenuItem (menuItem, modifiers = {}) { + var menuItemSnapshot = typeof menuItem === 'string' ? await ElectronBrowserProvider.getMenuItem(menuItem) : menuItem; + + if (!menuItemSnapshot) + throw new Error('Invalid menu item argument'); + + await doClickOnMenuItem(menuItemSnapshot[CONSTANTS.typeProperty], menuItemSnapshot[CONSTANTS.indexProperty], ensureModifiers(modifiers)); }, async setElectronDialogHandler (fn, context) { @@ -137,5 +178,17 @@ export default { fn: fn.toString(), ctx: context }); + }, + + async getMenuItem (menuItemSelector) { + var menuItemPath = menuItemSelector.split(/\s*>\s*/).map(simplifyMenuItemLabel); + var menu = menuItemPath[0] === 'contextmenu' ? await ElectronBrowserProvider.getContextMenu() : await ElectronBrowserProvider.getMainMenu(); + + if (menuItemPath[0] === 'contextmenu' || menuItemPath[0] === 'mainmenu') + menuItemPath.shift(); + + return findMenuItem(menu, menuItemPath); } }; + +export { ElectronBrowserProvider as default }; diff --git a/test/fixtures/dialog-test.js b/test/fixtures/dialog-test.js index 1e6d436..30f0092 100644 --- a/test/fixtures/dialog-test.js +++ b/test/fixtures/dialog-test.js @@ -1,6 +1,6 @@ import { testPage } from '../config'; import { ClientFunction } from 'testcafe'; -import { mainMenu, setElectronDialogHandler, menuClick } from 'testcafe-browser-provider-electron'; +import { setElectronDialogHandler, clickOnMenuItem } from 'testcafe-browser-provider-electron'; fixture `Dialog` @@ -9,11 +9,9 @@ fixture `Dialog` const checkDialogHandled = ClientFunction(() => window.dialogResult); test('Should handle Open Dialog', async t => { - var menu = await mainMenu(); - await setElectronDialogHandler(type => type + ' handled'); - await menuClick(menu.items[0].submenu.items[1]); + await clickOnMenuItem('Test > Dialog'); await t.expect(checkDialogHandled()).eql('open-dialog handled'); }); diff --git a/test/fixtures/menu-test.js b/test/fixtures/menu-test.js index 71804ac..91f52ee 100644 --- a/test/fixtures/menu-test.js +++ b/test/fixtures/menu-test.js @@ -1,6 +1,6 @@ import { testPage } from '../config'; import { ClientFunction } from 'testcafe'; -import { mainMenu, contextMenu, menuClick } from 'testcafe-browser-provider-electron'; +import { clickOnMenuItem } from 'testcafe-browser-provider-electron'; fixture `Menu` @@ -10,9 +10,7 @@ const checkMainMenuClicked = ClientFunction(() => window.mainMenuClicked); const checkContextMenuClicked = ClientFunction(() => window.contextMenuClicked); test('Should click on main menu', async t => { - var menu = await mainMenu(); - - await menuClick(menu.items[0].submenu.items[0]); + await clickOnMenuItem('Main menu > Test > Click'); await t.expect(checkMainMenuClicked()).ok(); }); @@ -20,9 +18,7 @@ test('Should click on main menu', async t => { test('Should click on context menu', async t => { await t.rightClick('body'); - var menu = await contextMenu(); - - await menuClick(menu.items[0]); + await clickOnMenuItem('Context Menu > Test'); await t.expect(checkContextMenuClicked()).ok(); });