selenium-webdriver.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import * as webdriver from 'selenium-webdriver';
  2. import { TestKey, getNoKeysSpecifiedError, _getTextWithExcludedElements, HarnessEnvironment } from '../testing.mjs';
  3. import 'rxjs';
  4. /**
  5. * Maps the `TestKey` constants to WebDriver's `webdriver.Key` constants.
  6. * See https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/webdriver/key.js#L29
  7. */
  8. const seleniumWebDriverKeyMap = {
  9. [TestKey.BACKSPACE]: webdriver.Key.BACK_SPACE,
  10. [TestKey.TAB]: webdriver.Key.TAB,
  11. [TestKey.ENTER]: webdriver.Key.ENTER,
  12. [TestKey.SHIFT]: webdriver.Key.SHIFT,
  13. [TestKey.CONTROL]: webdriver.Key.CONTROL,
  14. [TestKey.ALT]: webdriver.Key.ALT,
  15. [TestKey.ESCAPE]: webdriver.Key.ESCAPE,
  16. [TestKey.PAGE_UP]: webdriver.Key.PAGE_UP,
  17. [TestKey.PAGE_DOWN]: webdriver.Key.PAGE_DOWN,
  18. [TestKey.END]: webdriver.Key.END,
  19. [TestKey.HOME]: webdriver.Key.HOME,
  20. [TestKey.LEFT_ARROW]: webdriver.Key.ARROW_LEFT,
  21. [TestKey.UP_ARROW]: webdriver.Key.ARROW_UP,
  22. [TestKey.RIGHT_ARROW]: webdriver.Key.ARROW_RIGHT,
  23. [TestKey.DOWN_ARROW]: webdriver.Key.ARROW_DOWN,
  24. [TestKey.INSERT]: webdriver.Key.INSERT,
  25. [TestKey.DELETE]: webdriver.Key.DELETE,
  26. [TestKey.F1]: webdriver.Key.F1,
  27. [TestKey.F2]: webdriver.Key.F2,
  28. [TestKey.F3]: webdriver.Key.F3,
  29. [TestKey.F4]: webdriver.Key.F4,
  30. [TestKey.F5]: webdriver.Key.F5,
  31. [TestKey.F6]: webdriver.Key.F6,
  32. [TestKey.F7]: webdriver.Key.F7,
  33. [TestKey.F8]: webdriver.Key.F8,
  34. [TestKey.F9]: webdriver.Key.F9,
  35. [TestKey.F10]: webdriver.Key.F10,
  36. [TestKey.F11]: webdriver.Key.F11,
  37. [TestKey.F12]: webdriver.Key.F12,
  38. [TestKey.META]: webdriver.Key.META,
  39. [TestKey.COMMA]: ',',
  40. };
  41. /** Gets a list of WebDriver `Key`s for the given `ModifierKeys`. */
  42. function getSeleniumWebDriverModifierKeys(modifiers) {
  43. const result = [];
  44. if (modifiers.control) {
  45. result.push(webdriver.Key.CONTROL);
  46. }
  47. if (modifiers.alt) {
  48. result.push(webdriver.Key.ALT);
  49. }
  50. if (modifiers.shift) {
  51. result.push(webdriver.Key.SHIFT);
  52. }
  53. if (modifiers.meta) {
  54. result.push(webdriver.Key.META);
  55. }
  56. return result;
  57. }
  58. /** A `TestElement` implementation for WebDriver. */
  59. class SeleniumWebDriverElement {
  60. element;
  61. _stabilize;
  62. constructor(element, _stabilize) {
  63. this.element = element;
  64. this._stabilize = _stabilize;
  65. }
  66. /** Blur the element. */
  67. async blur() {
  68. await this._executeScript((element) => element.blur(), this.element());
  69. await this._stabilize();
  70. }
  71. /** Clear the element's input (for input and textarea elements only). */
  72. async clear() {
  73. await this.element().clear();
  74. await this._stabilize();
  75. }
  76. async click(...args) {
  77. await this._dispatchClickEventSequence(args, webdriver.Button.LEFT);
  78. await this._stabilize();
  79. }
  80. async rightClick(...args) {
  81. await this._dispatchClickEventSequence(args, webdriver.Button.RIGHT);
  82. await this._stabilize();
  83. }
  84. /** Focus the element. */
  85. async focus() {
  86. await this._executeScript((element) => element.focus(), this.element());
  87. await this._stabilize();
  88. }
  89. /** Get the computed value of the given CSS property for the element. */
  90. async getCssValue(property) {
  91. await this._stabilize();
  92. return this.element().getCssValue(property);
  93. }
  94. /** Hovers the mouse over the element. */
  95. async hover() {
  96. await this._actions().mouseMove(this.element()).perform();
  97. await this._stabilize();
  98. }
  99. /** Moves the mouse away from the element. */
  100. async mouseAway() {
  101. await this._actions().mouseMove(this.element(), { x: -1, y: -1 }).perform();
  102. await this._stabilize();
  103. }
  104. async sendKeys(...modifiersAndKeys) {
  105. const first = modifiersAndKeys[0];
  106. let modifiers;
  107. let rest;
  108. if (first !== undefined && typeof first !== 'string' && typeof first !== 'number') {
  109. modifiers = first;
  110. rest = modifiersAndKeys.slice(1);
  111. }
  112. else {
  113. modifiers = {};
  114. rest = modifiersAndKeys;
  115. }
  116. const modifierKeys = getSeleniumWebDriverModifierKeys(modifiers);
  117. const keys = rest
  118. .map(k => (typeof k === 'string' ? k.split('') : [seleniumWebDriverKeyMap[k]]))
  119. .reduce((arr, k) => arr.concat(k), [])
  120. // webdriver.Key.chord doesn't work well with geckodriver (mozilla/geckodriver#1502),
  121. // so avoid it if no modifier keys are required.
  122. .map(k => (modifierKeys.length > 0 ? webdriver.Key.chord(...modifierKeys, k) : k));
  123. // Throw an error if no keys have been specified. Calling this function with no
  124. // keys should not result in a focus event being dispatched unexpectedly.
  125. if (keys.length === 0) {
  126. throw getNoKeysSpecifiedError();
  127. }
  128. await this.element().sendKeys(...keys);
  129. await this._stabilize();
  130. }
  131. /**
  132. * Gets the text from the element.
  133. * @param options Options that affect what text is included.
  134. */
  135. async text(options) {
  136. await this._stabilize();
  137. if (options?.exclude) {
  138. return this._executeScript(_getTextWithExcludedElements, this.element(), options.exclude);
  139. }
  140. // We don't go through the WebDriver `getText`, because it excludes text from hidden elements.
  141. return this._executeScript((element) => (element.textContent || '').trim(), this.element());
  142. }
  143. /**
  144. * Sets the value of a `contenteditable` element.
  145. * @param value Value to be set on the element.
  146. */
  147. async setContenteditableValue(value) {
  148. const contenteditableAttr = await this.getAttribute('contenteditable');
  149. if (contenteditableAttr !== '' &&
  150. contenteditableAttr !== 'true' &&
  151. contenteditableAttr !== 'plaintext-only') {
  152. throw new Error('setContenteditableValue can only be called on a `contenteditable` element.');
  153. }
  154. await this._stabilize();
  155. return this._executeScript((element, valueToSet) => (element.textContent = valueToSet), this.element(), value);
  156. }
  157. /** Gets the value for the given attribute from the element. */
  158. async getAttribute(name) {
  159. await this._stabilize();
  160. return this._executeScript((element, attribute) => element.getAttribute(attribute), this.element(), name);
  161. }
  162. /** Checks whether the element has the given class. */
  163. async hasClass(name) {
  164. await this._stabilize();
  165. const classes = (await this.getAttribute('class')) || '';
  166. return new Set(classes.split(/\s+/).filter(c => c)).has(name);
  167. }
  168. /** Gets the dimensions of the element. */
  169. async getDimensions() {
  170. await this._stabilize();
  171. const { width, height } = await this.element().getSize();
  172. const { x: left, y: top } = await this.element().getLocation();
  173. return { width, height, left, top };
  174. }
  175. /** Gets the value of a property of an element. */
  176. async getProperty(name) {
  177. await this._stabilize();
  178. return this._executeScript((element, property) => element[property], this.element(), name);
  179. }
  180. /** Sets the value of a property of an input. */
  181. async setInputValue(newValue) {
  182. await this._executeScript((element, value) => (element.value = value), this.element(), newValue);
  183. await this._stabilize();
  184. }
  185. /** Selects the options at the specified indexes inside of a native `select` element. */
  186. async selectOptions(...optionIndexes) {
  187. await this._stabilize();
  188. const options = await this.element().findElements(webdriver.By.css('option'));
  189. const indexes = new Set(optionIndexes); // Convert to a set to remove duplicates.
  190. if (options.length && indexes.size) {
  191. // Reset the value so all the selected states are cleared. We can
  192. // reuse the input-specific method since the logic is the same.
  193. await this.setInputValue('');
  194. for (let i = 0; i < options.length; i++) {
  195. if (indexes.has(i)) {
  196. // We have to hold the control key while clicking on options so that multiple can be
  197. // selected in multi-selection mode. The key doesn't do anything for single selection.
  198. await this._actions().keyDown(webdriver.Key.CONTROL).perform();
  199. await options[i].click();
  200. await this._actions().keyUp(webdriver.Key.CONTROL).perform();
  201. }
  202. }
  203. await this._stabilize();
  204. }
  205. }
  206. /** Checks whether this element matches the given selector. */
  207. async matchesSelector(selector) {
  208. await this._stabilize();
  209. return this._executeScript((element, s) => (Element.prototype.matches || Element.prototype.msMatchesSelector).call(element, s), this.element(), selector);
  210. }
  211. /** Checks whether the element is focused. */
  212. async isFocused() {
  213. await this._stabilize();
  214. return webdriver.WebElement.equals(this.element(), this.element().getDriver().switchTo().activeElement());
  215. }
  216. /**
  217. * Dispatches an event with a particular name.
  218. * @param name Name of the event to be dispatched.
  219. */
  220. async dispatchEvent(name, data) {
  221. await this._executeScript(dispatchEvent, name, this.element(), data);
  222. await this._stabilize();
  223. }
  224. /** Gets the webdriver action sequence. */
  225. _actions() {
  226. return this.element().getDriver().actions();
  227. }
  228. /** Executes a function in the browser. */
  229. async _executeScript(script, ...var_args) {
  230. return this.element()
  231. .getDriver()
  232. .executeScript(script, ...var_args);
  233. }
  234. /** Dispatches all the events that are part of a click event sequence. */
  235. async _dispatchClickEventSequence(args, button) {
  236. let modifiers = {};
  237. if (args.length && typeof args[args.length - 1] === 'object') {
  238. modifiers = args.pop();
  239. }
  240. const modifierKeys = getSeleniumWebDriverModifierKeys(modifiers);
  241. // Omitting the offset argument to mouseMove results in clicking the center.
  242. // This is the default behavior we want, so we use an empty array of offsetArgs if
  243. // no args remain after popping the modifiers from the args passed to this function.
  244. const offsetArgs = (args.length === 2 ? [{ x: args[0], y: args[1] }] : []);
  245. let actions = this._actions().mouseMove(this.element(), ...offsetArgs);
  246. for (const modifierKey of modifierKeys) {
  247. actions = actions.keyDown(modifierKey);
  248. }
  249. actions = actions.click(button);
  250. for (const modifierKey of modifierKeys) {
  251. actions = actions.keyUp(modifierKey);
  252. }
  253. await actions.perform();
  254. }
  255. }
  256. /**
  257. * Dispatches an event with a particular name and data to an element. Note that this needs to be a
  258. * pure function, because it gets stringified by WebDriver and is executed inside the browser.
  259. */
  260. function dispatchEvent(name, element, data) {
  261. const event = document.createEvent('Event');
  262. event.initEvent(name);
  263. // tslint:disable-next-line:ban Have to use `Object.assign` to preserve the original object.
  264. Object.assign(event, data || {});
  265. element.dispatchEvent(event);
  266. }
  267. /** The default environment options. */
  268. const defaultEnvironmentOptions = {
  269. queryFn: async (selector, root) => root().findElements(webdriver.By.css(selector)),
  270. };
  271. /**
  272. * This function is meant to be executed in the browser. It taps into the hooks exposed by Angular
  273. * and invokes the specified `callback` when the application is stable (no more pending tasks).
  274. */
  275. function whenStable(callback) {
  276. Promise.all(window.frameworkStabilizers.map(stabilizer => new Promise(stabilizer))).then(callback);
  277. }
  278. /**
  279. * This function is meant to be executed in the browser. It checks whether the Angular framework has
  280. * bootstrapped yet.
  281. */
  282. function isBootstrapped() {
  283. return !!window.frameworkStabilizers;
  284. }
  285. /** Waits for angular to be ready after the page load. */
  286. async function waitForAngularReady(wd) {
  287. await wd.wait(() => wd.executeScript(isBootstrapped));
  288. await wd.executeAsyncScript(whenStable);
  289. }
  290. /** A `HarnessEnvironment` implementation for WebDriver. */
  291. class SeleniumWebDriverHarnessEnvironment extends HarnessEnvironment {
  292. /** The options for this environment. */
  293. _options;
  294. /** Environment stabilization callback passed to the created test elements. */
  295. _stabilizeCallback;
  296. constructor(rawRootElement, options) {
  297. super(rawRootElement);
  298. this._options = { ...defaultEnvironmentOptions, ...options };
  299. this._stabilizeCallback = () => this.forceStabilize();
  300. }
  301. /** Gets the ElementFinder corresponding to the given TestElement. */
  302. static getNativeElement(el) {
  303. if (el instanceof SeleniumWebDriverElement) {
  304. return el.element();
  305. }
  306. throw Error('This TestElement was not created by the WebDriverHarnessEnvironment');
  307. }
  308. /** Creates a `HarnessLoader` rooted at the document root. */
  309. static loader(driver, options) {
  310. return new SeleniumWebDriverHarnessEnvironment(() => driver.findElement(webdriver.By.css('body')), options);
  311. }
  312. /**
  313. * Flushes change detection and async tasks captured in the Angular zone.
  314. * In most cases it should not be necessary to call this manually. However, there may be some edge
  315. * cases where it is needed to fully flush animation events.
  316. */
  317. async forceStabilize() {
  318. await this.rawRootElement().getDriver().executeAsyncScript(whenStable);
  319. }
  320. /** @docs-private */
  321. async waitForTasksOutsideAngular() {
  322. // TODO: figure out how we can do this for the webdriver environment.
  323. // https://github.com/angular/components/issues/17412
  324. }
  325. /** Gets the root element for the document. */
  326. getDocumentRoot() {
  327. return () => this.rawRootElement().getDriver().findElement(webdriver.By.css('body'));
  328. }
  329. /** Creates a `TestElement` from a raw element. */
  330. createTestElement(element) {
  331. return new SeleniumWebDriverElement(element, this._stabilizeCallback);
  332. }
  333. /** Creates a `HarnessLoader` rooted at the given raw element. */
  334. createEnvironment(element) {
  335. return new SeleniumWebDriverHarnessEnvironment(element, this._options);
  336. }
  337. // Note: This seems to be working, though we may need to re-evaluate if we encounter issues with
  338. // stale element references. `() => Promise<webdriver.WebElement[]>` seems like a more correct
  339. // return type, though supporting it would require changes to the public harness API.
  340. /**
  341. * Gets a list of all elements matching the given selector under this environment's root element.
  342. */
  343. async getAllRawElements(selector) {
  344. const els = await this._options.queryFn(selector, this.rawRootElement);
  345. return els.map((x) => () => x);
  346. }
  347. }
  348. export { SeleniumWebDriverElement, SeleniumWebDriverHarnessEnvironment, waitForAngularReady };
  349. //# sourceMappingURL=selenium-webdriver.mjs.map