UNPKG

@badisi/wdio-harness

Version:

WebdriverIO support for Angular component test harnesses.

279 lines (278 loc) 10.7 kB
import { TestKey, ComponentHarness } from '@angular/cdk/testing'; import { browser } from '@wdio/globals'; import logger from '@wdio/logger'; import colors from '@colors/colors/safe.js'; const { magenta, green } = colors; var Button; (function (Button) { Button["LEFT"] = "left"; Button["MIDDLE"] = "middle"; Button["RIGHT"] = "right"; })(Button || (Button = {})); /** Registers the element logger. */ const log = logger('wdio-harness'); /** Maps the `TestKey` constants to WebdriverIO's `Key` constants. */ const keyMap = { [TestKey.BACKSPACE]: 'Backspace', [TestKey.TAB]: 'Tab', [TestKey.ENTER]: 'Enter', [TestKey.SHIFT]: 'Shift', [TestKey.CONTROL]: 'Control', [TestKey.ALT]: 'Alt', [TestKey.ESCAPE]: 'Escape', [TestKey.PAGE_UP]: 'PageUp', [TestKey.PAGE_DOWN]: 'PageDown', [TestKey.END]: 'End', [TestKey.HOME]: 'Home', [TestKey.LEFT_ARROW]: 'ArrowLeft', [TestKey.UP_ARROW]: 'ArrowUp', [TestKey.RIGHT_ARROW]: 'ArrowRight', [TestKey.DOWN_ARROW]: 'ArrowDown', [TestKey.INSERT]: 'Insert', [TestKey.DELETE]: 'Delete', [TestKey.F1]: 'F1', [TestKey.F2]: 'F2', [TestKey.F3]: 'F3', [TestKey.F4]: 'F4', [TestKey.F5]: 'F5', [TestKey.F6]: 'F6', [TestKey.F7]: 'F7', [TestKey.F8]: 'F8', [TestKey.F9]: 'F9', [TestKey.F10]: 'F10', [TestKey.F11]: 'F11', [TestKey.F12]: 'F12', [TestKey.META]: 'Meta', [TestKey.COMMA]: ',' }; ComponentHarness.prototype.element = function () { return this.locatorFactory.rootElement.element(); }; /** Converts a `ModifierKeys` object to a list of WebdriverIO `Key`s. */ const toWebdriverIOModifierKeys = (modifiers) => { const result = []; if (modifiers.control) { result.push(keyMap[TestKey.CONTROL]); } if (modifiers.alt) { result.push(keyMap[TestKey.ALT]); } if (modifiers.shift) { result.push(keyMap[TestKey.SHIFT]); } if (modifiers.meta) { result.push(keyMap[TestKey.META]); } return result; }; /** * A `TestElement` implementation for WebdriverIO. */ export class WebdriverIOTestElement { hostElement; constructor(hostElement) { this.hostElement = hostElement; } /** Return the host element. */ element() { return this.hostElement; } /** Blur the element. */ async blur() { this.logAction('BLUR'); return browser.executeScript('arguments[0].blur()', [this.hostElement]); } /** Clear the element's input (for input and textarea elements only). */ async clear() { this.logAction('CLEAR'); return this.hostElement.clearValue(); } async click(...args) { this.logAction('CLICK'); return this.dispatchClickEventSequence(args, Button.LEFT); } async rightClick(...args) { this.logAction('RIGHT_CLICK'); return this.dispatchClickEventSequence(args, Button.RIGHT); } /** Focus the element. */ async focus() { this.logAction('FOCUS'); return browser.executeScript('arguments[0].focus()', [this.hostElement]); } /** Get the computed value of the given CSS property for the element. */ async getCssValue(property) { this.logAction('GET_CSS_VALUE'); return (await this.hostElement.getCSSProperty(property)).value ?? ''; } /** Hovers the mouse over the element. */ async hover() { this.logAction('HOVER'); return this.hostElement.moveTo(); } /** Moves the mouse away from the element. */ async mouseAway() { this.logAction('MOUSE_AWAY'); return this.hostElement.moveTo({ xOffset: -1, yOffset: -1 }); } async sendKeys(...modifiersAndKeys) { let modifiers; let rest; const first = modifiersAndKeys[0]; if (first !== undefined && typeof first !== 'string' && typeof first !== 'number') { modifiers = first; rest = modifiersAndKeys.slice(1); } else { modifiers = {}; rest = modifiersAndKeys; } const KeyNULL = String.fromCharCode(57344); const modifierKeys = toWebdriverIOModifierKeys(modifiers); const keys = rest .map(k => (typeof k === 'string' ? k.split('') : [keyMap[k]])) .reduce((arr, k) => arr.concat(k), []) .reduce((arr, k) => { if (modifierKeys.length > 0) { return arr.concat(...modifierKeys, k, KeyNULL); } return arr.concat(k); }, []); this.logAction('SEND_KEYS', `[${keys.join(', ')}]`); if (keys.length !== 0) { await this.focus(); return browser.keys(keys); } } /** Gets the text from the element. */ async text(options) { this.logAction('TEXT', options?.exclude ? `{ exclude: ${options?.exclude} }` : undefined); if (options?.exclude) { return browser.executeScript(` const clone = arguments[0].cloneNode(true); const exclusions = clone.querySelectorAll(arguments[1]); for (let i = 0; i < exclusions.length; i++) { exclusions[i].remove(); } return (clone.textContent ?? '').trim(); `, [this.hostElement, options.exclude]); } // We don't go through WebdriverIO's `getText`, because it excludes text from hidden elements. return browser.executeScript(`return (arguments[0].textContent ?? '').trim()`, [this.hostElement]); } /** Gets the value for the given attribute from the element. */ async getAttribute(name) { this.logAction('GET_ATTRIBUTE', name); return this.hostElement.getAttribute(name); } /** Checks whether the element has the given class. */ async hasClass(name) { this.logAction('HAS_CLASS', name); const classes = (await this.getAttribute('class')) || ''; return new Set(classes.split(/\s+/).filter(c => c)).has(name); } /** Gets the dimensions of the element. */ async getDimensions() { this.logAction('GET_DIMENSIONS'); const { width, height } = await this.hostElement.getSize(); const { x: left, y: top } = await this.hostElement.getLocation(); return { width, height, left, top }; } /** Gets the value of a property of an element. */ async getProperty(name) { this.logAction('GET_PROPERTY', name); return this.hostElement.getProperty(name); } /** Checks whether this element matches the given selector. */ async matchesSelector(selector) { this.logAction('MATCHES_SELECTOR', selector); return browser.executeScript(` return (Element.prototype.matches ?? Element.prototype.msMatchesSelector).call(arguments[0], arguments[1]) `, [this.hostElement, selector]); } /** Checks whether the element is focused. */ async isFocused() { this.logAction('IS_FOCUSED'); return this.hostElement.isFocused(); } /** Sets the value of a property of an input. */ async setInputValue(value) { this.logAction('SET_INPUT_VALUE', value); return this.hostElement.setValue(value); } /** Selects the options at the specified indexes inside of a native `select` element. */ async selectOptions(...optionIndexes) { this.logAction('SELECT_OPTIONS', `[${optionIndexes.join(', ')}]`); const options = await this.hostElement.$$('option').getElements(); const indexes = new Set(optionIndexes); // Convert to a set to remove duplicates. if (options.length && indexes.size) { // Reset the value so all the selected states are cleared. We can // reuse the input-specific method since the logic is the same. await this.setInputValue(''); for (let i = 0; i < options.length; i++) { if (indexes.has(i)) { // We have to hold the control key while clicking on options so that multiple can be // selected in multi-selection mode. The key doesn't do anything for single selection. await this.keyDown(keyMap[TestKey.CONTROL]); await options[i].click(); await this.keyUp(keyMap[TestKey.CONTROL]); } } } } /** Dispatches an event with a particular name. */ async dispatchEvent(name, data) { this.logAction('DISPATCH_EVENT', name); return browser.executeScript(` const event = document.createEvent('Event'); event.initEvent(arguments[0]); if (arguments[2]) { Object.assign(event, arguments[2]); } arguments[1]['dispatchEvent'](event); `, [name, this.hostElement, data]); } /** Performs a key-down action. */ async keyDown(value) { this.logAction('KEY_DOWN:', value); return browser.performActions([{ id: 'keyboard', type: 'key', actions: [{ type: 'keyDown', value }], }]); } /** Performs a key-up action. */ async keyUp(value) { this.logAction('KEY_UP:', value); return browser.performActions([{ id: 'keyboard', type: 'key', actions: [{ type: 'keyUp', value }], }]); } // --- HELPER(s) --- /** Dispatches all the events that are part of a click event sequence. */ async dispatchClickEventSequence(args, button) { let modifiers = {}; if (args.length && typeof args[args.length - 1] === 'object') { modifiers = args.pop(); } const modifierKeys = toWebdriverIOModifierKeys(modifiers); // Omitting the offset argument to mouseMove results in clicking the center. // This is the default behavior we want, so we use an empty array of offsetArgs if // no args remain after popping the modifiers from the args passed to this function. const offsetArgs = (args.length === 2 ? { xOffset: Number(args[0]), yOffset: Number(args[1]) } : undefined); await this.hostElement.moveTo(offsetArgs); for (const modifierKey of modifierKeys) { this.keyDown(modifierKey); } await this.hostElement.click({ button }); for (const modifierKey of modifierKeys) { this.keyUp(modifierKey); } } /** Writes info to the console outputs. */ logAction(action, args) { log.info(`${magenta(action)} ${green(this.hostElement.selector.toString())} ${args ? args : ''}`); } }