(devtools): removing devtools automation protocol (#12301)
* (devtools): bye bye * remove e2e tests * more cleanuops * fix unit tests * fix test command
This commit is contained in:
parent
e60161dc13
commit
28e64e439f
166 changed files with 526 additions and 9035 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -70,7 +70,6 @@ packages/**/cjs/
|
|||
/*.js
|
||||
/*.mjs
|
||||
/*.ts
|
||||
devtools.d.ts
|
||||
!.eslintrc.js
|
||||
!vitest.config.ts
|
||||
packages/**/package-lock.json
|
||||
|
|
|
|||
|
|
@ -253,10 +253,7 @@ When a PR gets submitted, WebdriverIO runs the following checks:
|
|||
$ pnpm run test:smoke
|
||||
```
|
||||
- *e2e Tests*
|
||||
Last but not least, we run actual e2e tests with a real browser to ensure that our WebDriver DevTools implementation is working as expected. These
|
||||
tests spin up headless Chrome, Firefox and Edge browsers to test various implementations like for example the commands implemented in the `devtools` package. Given that the WebDriver functionality
|
||||
is already tested with [WPT](https://github.com/web-platform-tests/wpt), we don't need to do it there.
|
||||
In order to run these tests, an installation of [Firefox Nightly](https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly), [Google Chrome](https://www.google.com/chrome/) and [Microsoft Edge](https://www.microsoft.com/en-us/edge/download)is required
|
||||
Last but not least, we run actual e2e tests with a real headless browser to ensure we can spin up tests in all support variants and that certain services from end to end.
|
||||
You can manually trigger this check by calling:
|
||||
```sh
|
||||
$ pnpm run test:e2e
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ With that, the project offers not only the ability of automating user agents usi
|
|||
|
||||
- automation of modern web browsers
|
||||
- automation of mobile devices (web as well as native applications)
|
||||
- automation of desktop applications (e.g. Electron or MacOS applications)
|
||||
- testing applications from end-to-end at scale (including parallelisation and reporting)
|
||||
- integration into 3rd party services and vendors (e.g. [Sauce Labs](https://saucelabs.com/) or [BrowserStack](https://browserstack.com/))
|
||||
- support of functional as well as non functional testing aspects (e.g. visual regression or frontend performance testing)
|
||||
- automated debugging in the browser (e.g. using the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/))
|
||||
- support of functional as well as non functional testing aspects (e.g. component testing, visual regression or frontend performance testing)
|
||||
- scraping of websites
|
||||
- providing technical content and best practices in form of [boilerplates](https://webdriver.io/) and [blog posts](https://webdriver.io/blog/)
|
||||
- providing technical help in form of a [community channel](https://discord.webdriver.io)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
***
|
||||
|
||||
WebdriverIO is a test automation framework, for e2e as well as unit and component testing in the browser, that allows you to run tests based on the [WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html), [WebDriver BiDi](https://github.com/w3c/webdriver-bidi) as well as [Chrome DevTools](https://chromedevtools.github.io/devtools-protocol/) protocol and [Appium](http://appium.io/) automation technology. It provides support for your favorite BDD/TDD test framework and will run your tests locally or in the cloud using Sauce Labs, BrowserStack, TestingBot or LambdaTest.
|
||||
WebdriverIO is a test automation framework, for e2e as well as unit and component testing in the browser, that allows you to run tests based on the [WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html) and [WebDriver BiDi](https://github.com/w3c/webdriver-bidi) as well as [Appium](http://appium.io/) automation technology. It provides support for your favorite BDD/TDD test framework and will run your tests locally or in the cloud using Sauce Labs, BrowserStack, TestingBot or LambdaTest.
|
||||
|
||||
## :woman_technologist: :man_technologist: Contributing
|
||||
Do you like WebdriverIO and want to help make it better? Awesome! Have a look into our [Contributor Documentation](CONTRIBUTING.md) to get started and find out what contributions can be and how to make them.
|
||||
|
|
@ -88,7 +88,6 @@ __Did you build a WebdriverIO service or reporter?__ That's awesome! Please add
|
|||
### Core
|
||||
|
||||
- [webdriver](https://github.com/webdriverio/webdriverio/tree/main/packages/webdriver) - A Node.js bindings implementation for the W3C WebDriver and Mobile JSONWire Protocol
|
||||
- [devtools](https://github.com/webdriverio/webdriverio/tree/main/packages/devtools) - A Chrome DevTools protocol binding that maps WebDriver commands into Chrome DevTools commands using Puppeteer
|
||||
- [webdriverio](https://github.com/webdriverio/webdriverio/blob/main/packages/webdriverio) - Next-gen browser and mobile automation test framework for Node.js
|
||||
- [@wdio/cli](https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-cli) - A WebdriverIO testrunner command line interface
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ import webdriverMonadOrig from '../../packages/wdio-utils/src/monad.js'
|
|||
import {
|
||||
isW3C as isW3cOrig,
|
||||
sessionEnvironmentDetector as sessionEnvDetector,
|
||||
capabilitiesEnvironmentDetector as capabilitiesEnvDetector,
|
||||
devtoolsEnvironmentDetector as devtoolsEnvDetector
|
||||
capabilitiesEnvironmentDetector as capabilitiesEnvDetector
|
||||
} from '../../packages/wdio-utils/src/envDetector.js'
|
||||
import { UNICODE_CHARACTERS as UNICODE_CHARACTERS_ORIG, HOOK_DEFINITION as HOOK_DEFINITION_ORIG } from '../../packages/wdio-utils/src/constants.js'
|
||||
|
||||
|
|
@ -106,7 +105,6 @@ export const sessionEnvironmentDetector = vi.fn().mockImplementation(
|
|||
// @ts-ignore
|
||||
(...args) => sessionEnvDetector(...args))
|
||||
export const capabilitiesEnvironmentDetector = capabilitiesEnvDetector
|
||||
export const devtoolsEnvironmentDetector = devtoolsEnvDetector
|
||||
export const transformCommandLogResult = vi.fn().mockImplementation((data) => data)
|
||||
export const sleep = vi.fn().mockImplementation(sleepOrig)
|
||||
export const UNICODE_CHARACTERS = UNICODE_CHARACTERS_ORIG
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const cdpSession = new CDPSessionMock()
|
|||
|
||||
class TargetMock {
|
||||
page = vi.fn().mockImplementation(() => page)
|
||||
asPage = vi.fn().mockImplementation(async () => page)
|
||||
createCDPSession = vi.fn().mockImplementation(() => cdpSession)
|
||||
}
|
||||
const target = new TargetMock()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ test('allow to attach to an existing session', async () => {
|
|||
const browser = await remote({
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'wdio:devtoolsOptions': { headless: true, dumpio: true }
|
||||
'goog:chromeOptions': {
|
||||
args: ['headless', 'disable-gpu']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -24,6 +26,9 @@ test('allow to attach to an existing session', async () => {
|
|||
|
||||
await otherBrowser.deleteSession()
|
||||
|
||||
/**
|
||||
* verify that browser session is deleted
|
||||
*/
|
||||
const error = await browser.status().catch((err) => err)
|
||||
expect(error.message).not.toBe('ChromeDriver ready for new sessions.')
|
||||
expect(error.message).toEqual(expect.stringContaining('fetch failed'))
|
||||
|
|
|
|||
|
|
@ -1,316 +0,0 @@
|
|||
import { beforeAll, beforeEach, afterAll, describe, it, expect } from 'vitest'
|
||||
import DevTools from '../packages/devtools/build/index.js'
|
||||
import { ELEMENT_KEY } from '../packages/webdriver/build/constants.js'
|
||||
import type { Client } from '../packages/devtools/build/index.js'
|
||||
|
||||
let browser: Client
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await DevTools.newSession({
|
||||
outputDir: __dirname,
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'wdio:devtoolsOptions': {
|
||||
headless: true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('elements', () => {
|
||||
beforeEach(async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
})
|
||||
|
||||
it('should be able to do a drag&drop', async () => {
|
||||
const drag = await browser.findElement('css selector', '.ui-draggable')
|
||||
const dragPosition = await browser.getElementRect(drag[ELEMENT_KEY])
|
||||
const drop = await browser.findElement('css selector', '.ui-droppable')
|
||||
const dropPosition = await browser.getElementRect(drop[ELEMENT_KEY])
|
||||
const dragStart = {
|
||||
x: dragPosition.x + (dragPosition.width / 2),
|
||||
y: dragPosition.y + (dragPosition.height / 2)
|
||||
}
|
||||
const dropEnd = {
|
||||
x: (dropPosition.x + (dropPosition.width / 2) - dragStart.x),
|
||||
y: (dropPosition.y + (dropPosition.height / 2) - dragStart.y)
|
||||
}
|
||||
|
||||
await browser.executeScript('window.scrollTo(0, 0)', [])
|
||||
await browser.performActions([{
|
||||
type: 'pointer',
|
||||
id: 'finger1',
|
||||
parameters: {
|
||||
pointerType: 'mouse'
|
||||
},
|
||||
actions: [{
|
||||
type: 'pointerMove',
|
||||
duration: 0,
|
||||
...dragStart
|
||||
}, {
|
||||
type: 'pointerDown',
|
||||
button: 0
|
||||
}, {
|
||||
type: 'pause',
|
||||
duration: 10
|
||||
}, {
|
||||
type: 'pointerMove',
|
||||
duration: 100,
|
||||
origin: 'pointer',
|
||||
...dropEnd
|
||||
}, {
|
||||
type: 'pointerUp',
|
||||
button: 0
|
||||
}]
|
||||
}])
|
||||
|
||||
const elem = await browser.findElement('css selector', '.searchinput')
|
||||
expect(await browser.getElementProperty(elem[ELEMENT_KEY], 'value'))
|
||||
.toBe('Dropped!')
|
||||
})
|
||||
|
||||
it('findElement with css selector', async () => {
|
||||
const yellowBox = await browser.findElement('css selector', '.yellow')
|
||||
expect(yellowBox).toEqual({ [ELEMENT_KEY]: 'ELEMENT-4' })
|
||||
const redBox = await browser.findElement('css selector', '.red')
|
||||
expect(redBox).toEqual({ [ELEMENT_KEY]: 'ELEMENT-5' })
|
||||
})
|
||||
|
||||
it('find nested element with css selector', async () => {
|
||||
const nested = await browser.findElement('css selector', '.nested')
|
||||
const header = await browser.findElementFromElement(nested[ELEMENT_KEY], 'css selector', '.findme')
|
||||
expect(header).toEqual({ [ELEMENT_KEY]: 'ELEMENT-7' })
|
||||
})
|
||||
|
||||
it('findElement with xPath', async () => {
|
||||
const xPathElement = await browser.findElement('xpath', '//*[@id="newWindow"]')
|
||||
expect(xPathElement).toEqual({ [ELEMENT_KEY]: 'ELEMENT-8' })
|
||||
})
|
||||
|
||||
it('findElements with css selector', async () => {
|
||||
const boxes = await browser.findElements('css selector', '.box')
|
||||
expect(boxes).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('find nested elements with css selector', async () => {
|
||||
const nested = await browser.findElement('css selector', '.nested')
|
||||
const spans = await browser.findElementsFromElement(nested[ELEMENT_KEY], 'css selector', 'span')
|
||||
expect(spans).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('find nested elements with xPath', async () => {
|
||||
const nested = await browser.findElement('css selector', '.nested')
|
||||
const spans = await browser.findElementsFromElement(nested[ELEMENT_KEY], 'xpath', './/div')
|
||||
expect(spans).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('can click and go back and forward', async () => {
|
||||
const link = await browser.findElement('css selector', '#secondPageLink')
|
||||
await browser.elementClick(link[ELEMENT_KEY])
|
||||
expect(await browser.getTitle()).toBe('two')
|
||||
await browser.back()
|
||||
expect(await browser.getTitle()).toBe('WebdriverJS Testpage')
|
||||
await browser.forward()
|
||||
expect(await browser.getTitle()).toBe('two')
|
||||
})
|
||||
|
||||
it('is able to select an option', async () => {
|
||||
const option = await browser.findElement('css selector', 'option[value="someValue5"]')
|
||||
await browser.elementClick(option[ELEMENT_KEY])
|
||||
const selectedValue = await browser.findElement('css selector', '#selectedValue')
|
||||
expect(await browser.getElementText(selectedValue[ELEMENT_KEY])).toBe('someValue5')
|
||||
})
|
||||
|
||||
it('element properties', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
const link = await browser.findElement('css selector', '#secondPageLink')
|
||||
expect(await browser.getElementText(link[ELEMENT_KEY])).toBe('two')
|
||||
expect(await browser.getElementAttribute(link[ELEMENT_KEY], 'href')).toBe('./two.html')
|
||||
expect(await browser.getElementProperty(link[ELEMENT_KEY], 'tagName')).toBe('A')
|
||||
expect(await browser.getElementTagName(link[ELEMENT_KEY])).toBe('a')
|
||||
expect(await browser.getElementCSSValue(link[ELEMENT_KEY], 'color')).toBe('rgb(0, 136, 204)')
|
||||
|
||||
const rect = await browser.getElementRect(link[ELEMENT_KEY])
|
||||
// can be 16 or 16.5 depending on the environment
|
||||
expect(Math.floor(rect.height)).toBe(16)
|
||||
expect(rect.width > 21 && rect.width < 24) // changes depending where it is run
|
||||
expect(rect.x).toBe(15)
|
||||
expect(Math.round(rect.y)).toBe(142)
|
||||
|
||||
const selectedCheckbox = await browser.findElement('css selector', '.checkbox_selected')
|
||||
expect(await browser.isElementSelected(selectedCheckbox[ELEMENT_KEY])).toBe(true)
|
||||
const notSelectedCheckbox = await browser.findElement('css selector', '.checkbox_notselected')
|
||||
expect(await browser.isElementSelected(notSelectedCheckbox[ELEMENT_KEY])).toBe(false)
|
||||
|
||||
const disabledInput = await browser.findElement('css selector', 'input[value="d"]')
|
||||
expect(await browser.isElementEnabled(disabledInput[ELEMENT_KEY])).toBe(false)
|
||||
const enabledInput = await browser.findElement('css selector', 'input[value="a"]')
|
||||
expect(await browser.isElementEnabled(enabledInput[ELEMENT_KEY])).toBe(true)
|
||||
})
|
||||
|
||||
it('getActiveElement', async () => {
|
||||
const textarea = await browser.findElement('css selector', 'textarea')
|
||||
await browser.elementClick(textarea[ELEMENT_KEY])
|
||||
|
||||
const activeElement = await browser.getActiveElement()
|
||||
expect(await browser.getElementTagName(activeElement[ELEMENT_KEY])).toBe('textarea')
|
||||
})
|
||||
|
||||
it('elementSendKeys', async () => {
|
||||
const textarea = await browser.findElement('css selector', 'textarea')
|
||||
await browser.elementSendKeys(textarea[ELEMENT_KEY], 'foobar')
|
||||
expect(await browser.getElementProperty(textarea[ELEMENT_KEY], 'value')).toBe('foobar')
|
||||
})
|
||||
|
||||
it('elementSendKeys for file-type input', async () => {
|
||||
const fileInput = await browser.findElement('css selector', '#upload-test')
|
||||
await browser.elementSendKeys(fileInput[ELEMENT_KEY], 'README.md')
|
||||
expect(await browser.getElementProperty(fileInput[ELEMENT_KEY], 'value')).toBe('C:\\fakepath\\README.md')
|
||||
})
|
||||
|
||||
it('elementSendKeys can use special characters', async () => {
|
||||
await browser.navigateTo('https://todomvc.com/examples/vue/dist/#/')
|
||||
const todoInput = await browser.findElement('css selector', '.new-todo')
|
||||
await browser.elementSendKeys(todoInput[ELEMENT_KEY], 'ToDo #1\uE007ToDo #2\uE007ToDo #3\uE007')
|
||||
const todoCountElem = await browser.findElement('css selector', '.todo-count strong')
|
||||
const todoCount = await browser.getElementText(todoCountElem[ELEMENT_KEY])
|
||||
expect(todoCount).toBe('3')
|
||||
})
|
||||
|
||||
it('elementSendKeys can send unicode backspace', async () => {
|
||||
const textarea = await browser.findElement('css selector', 'textarea')
|
||||
await browser.elementSendKeys(textarea[ELEMENT_KEY], 'foobar')
|
||||
expect(await browser.getElementProperty(textarea[ELEMENT_KEY], 'value')).toBe('foobar')
|
||||
await browser.elementSendKeys(textarea[ELEMENT_KEY], '\uE003')
|
||||
expect(await browser.getElementProperty(textarea[ELEMENT_KEY], 'value')).toBe('fooba')
|
||||
})
|
||||
|
||||
it('elementClear', async () => {
|
||||
const textarea = await browser.findElement('css selector', 'textarea')
|
||||
await browser.elementClear(textarea[ELEMENT_KEY])
|
||||
expect(await browser.getElementProperty(textarea[ELEMENT_KEY], 'value')).toBe('')
|
||||
})
|
||||
|
||||
it('link text', async () => {
|
||||
const link = await browser.findElement('link text', 'two')
|
||||
expect(await browser.getElementText(link[ELEMENT_KEY])).toBe('two')
|
||||
})
|
||||
|
||||
it('partial link text', async () => {
|
||||
const link = await browser.findElement('partial link text', 'new tab')
|
||||
expect(await browser.getElementText(link[ELEMENT_KEY])).toBe('open new tab')
|
||||
})
|
||||
|
||||
it('should be able to use keys command', async () => {
|
||||
const textarea = await browser.findElement('css selector', 'textarea')
|
||||
await browser.elementClick(textarea[ELEMENT_KEY])
|
||||
await browser.performActions([{
|
||||
type: 'key',
|
||||
id: 'keyboard',
|
||||
actions: [
|
||||
{ type: 'keyDown', value: 'f' },
|
||||
{ type: 'keyDown', value: 'o' },
|
||||
{ type: 'keyDown', value: 'o' },
|
||||
{ type: 'keyDown', value: 'b' },
|
||||
{ type: 'keyDown', value: 'a' },
|
||||
{ type: 'keyDown', value: 'r' },
|
||||
{ type: 'keyDown', value: '😉' },
|
||||
{ type: 'keyUp', value: 'f' },
|
||||
{ type: 'keyUp', value: 'o' },
|
||||
{ type: 'keyUp', value: 'o' },
|
||||
{ type: 'keyUp', value: 'b' },
|
||||
{ type: 'keyUp', value: 'a' },
|
||||
{ type: 'keyUp', value: 'r' },
|
||||
{ type: 'keyUp', value: '😉' }
|
||||
]
|
||||
}])
|
||||
expect(await browser.getElementProperty(textarea[ELEMENT_KEY], 'value'))
|
||||
.toBe('foobar😉')
|
||||
})
|
||||
|
||||
it('should allow to click relative to the center of an element', async () => {
|
||||
await browser.executeScript('window.scrollTo(0, 0)', [])
|
||||
const message = await browser.findElement('css selector', '.btn1_right_clicked')
|
||||
const btn2 = await browser.findElement('css selector', '.btn2')
|
||||
|
||||
expect(await browser.getElementCSSValue(message[ELEMENT_KEY], 'display'))
|
||||
.toBe('none')
|
||||
await browser.performActions([{
|
||||
type: 'pointer',
|
||||
id: 'pointer1',
|
||||
parameters: { pointerType: 'mouse' },
|
||||
actions: [{
|
||||
type: 'pointerMove',
|
||||
origin: {
|
||||
[ELEMENT_KEY]: btn2[ELEMENT_KEY]
|
||||
},
|
||||
x: -50,
|
||||
y: 0
|
||||
}, {
|
||||
type: 'pointerDown',
|
||||
button: 2
|
||||
}, {
|
||||
type: 'pointerUp',
|
||||
button: 2
|
||||
}]
|
||||
}])
|
||||
await new Promise((r) => setTimeout(r, 3000))
|
||||
expect(await browser.getElementCSSValue(message[ELEMENT_KEY], 'display'))
|
||||
.toBe('block')
|
||||
})
|
||||
|
||||
it('should allow to double click on an element', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io/')
|
||||
const elem = await browser.findElement('css selector', '.btn1')
|
||||
const notifier = await browser.findElement('css selector', '.btn1_dblclicked')
|
||||
|
||||
expect(await browser.getElementAttribute(notifier[ELEMENT_KEY], 'style'))
|
||||
.toBe(null)
|
||||
|
||||
await browser.performActions([{
|
||||
type: 'pointer',
|
||||
id: 'pointer1',
|
||||
parameters: { pointerType: 'mouse' },
|
||||
actions: [
|
||||
{ type: 'pointerMove', origin: elem, x: 0, y: 0 },
|
||||
{ type: 'pointerDown', button: 0 },
|
||||
{ type: 'pointerUp', button: 0 },
|
||||
{ type: 'pause', duration: 10 },
|
||||
{ type: 'pointerDown', button: 0 },
|
||||
{ type: 'pointerUp', button: 0 }
|
||||
]
|
||||
}])
|
||||
|
||||
expect(await browser.getElementAttribute(notifier[ELEMENT_KEY], 'style'))
|
||||
.toBe('display: block;')
|
||||
})
|
||||
|
||||
it('should support deep selectors', async () => {
|
||||
await browser.navigateTo('https://polymer-library.polymer-project.org/3.0/api/elements/array-selector')
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
const headerSlot = await browser.findElement('shadow', 'section[anchor-id="elementBase-properties"]')
|
||||
expect(
|
||||
(await browser.getElementText(headerSlot[ELEMENT_KEY])).trim()
|
||||
).toContain('Properties')
|
||||
})
|
||||
|
||||
it('can fetch shadow elements', async () => {
|
||||
await browser.navigateTo('https://polymer-library.polymer-project.org/3.0/api/elements/array-selector')
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
const element = await browser.findElement('tag name', 'pw-footer')
|
||||
const shadowRoot = await browser.getElementShadowRoot(
|
||||
element['element-6066-11e4-a52e-4f735466cecf']
|
||||
)
|
||||
const elementRef = await browser.findElementFromShadowRoot(
|
||||
shadowRoot['shadow-6066-11e4-a52e-4f735466cecf'],
|
||||
'css selector',
|
||||
'.copyright'
|
||||
)
|
||||
expect(await browser.getElementText(elementRef[ELEMENT_KEY]))
|
||||
.toContain('Brought to you by The Polymer Project')
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.deleteSession()
|
||||
})
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
const assert = require('node:assert')
|
||||
const Devtools = require('../../packages/devtools')
|
||||
|
||||
;(async () => {
|
||||
const client = await Devtools.newSession({
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': { args: ['headless', 'disable-gpu'] }
|
||||
}
|
||||
})
|
||||
|
||||
await client.navigateTo('https://www.google.com/ncr')
|
||||
assert.equal(await client.getTitle(), 'Google')
|
||||
await client.deleteSession()
|
||||
})().then(
|
||||
() => console.log('Devtools CJS Test Passed!') || process.exit(0),
|
||||
(err) => console.log('Devtools CJS Test Failed!', err) || process.exit(1)
|
||||
)
|
||||
|
|
@ -1,399 +0,0 @@
|
|||
import { beforeAll, afterAll, describe, it, expect, vi } from 'vitest'
|
||||
|
||||
import DevTools from '../packages/devtools/build/index.js'
|
||||
import { ELEMENT_KEY } from '../packages/webdriver/build/constants.js'
|
||||
|
||||
import type { Client } from '../packages/devtools/build/index.js'
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
let browser: Client
|
||||
|
||||
async function waitForElement (browser: Client, selectorStrategy: string, selector: string, retries = 10): Promise<string> {
|
||||
const elem = await browser.findElement(selectorStrategy, selector)
|
||||
if (elem[ELEMENT_KEY]) {
|
||||
return elem[ELEMENT_KEY]
|
||||
}
|
||||
|
||||
if (retries < 0) {
|
||||
throw new Error(`element with ${selectorStrategy} "${selector}" not found`)
|
||||
}
|
||||
|
||||
await sleep(200)
|
||||
return waitForElement(browser, selectorStrategy, selector, --retries)
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await DevTools.newSession({
|
||||
outputDir: __dirname,
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'wdio:devtoolsOptions': {
|
||||
headless: true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should navigate to a page and get page info', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
expect(await browser.getTitle()).toBe('WebdriverJS Testpage')
|
||||
expect(await browser.getUrl()).toContain('http://guinea-pig.webdriver.io')
|
||||
expect(await browser.getPageSource()).toContain('WebdriverJS Testpage')
|
||||
|
||||
const elem = await browser.findElement('css selector', '#t')
|
||||
expect(await browser.getElementText(elem[ELEMENT_KEY])).toBe('lions, tigers')
|
||||
})
|
||||
|
||||
it('should include the hash', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io#hash=hello')
|
||||
expect(await browser.getUrl()).toBe('http://guinea-pig.webdriver.io/#hash=hello')
|
||||
})
|
||||
|
||||
describe('timeouts', () => {
|
||||
beforeAll(async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
})
|
||||
|
||||
it('getTimeouts', async () => {
|
||||
expect(await browser.getTimeouts()).toEqual({
|
||||
implicit: 0,
|
||||
pageLoad: 300000,
|
||||
script: 30000
|
||||
})
|
||||
})
|
||||
|
||||
it('setTimeouts', async () => {
|
||||
await browser.setTimeouts(1, 2, 3)
|
||||
expect(await browser.getTimeouts()).toEqual({
|
||||
implicit: 1,
|
||||
pageLoad: 2,
|
||||
script: 3
|
||||
})
|
||||
})
|
||||
|
||||
it('test script timeout', async () => {
|
||||
expect.assertions(2)
|
||||
|
||||
const asyncCommand = () => browser.executeAsyncScript(
|
||||
'return setTimeout(() => arguments[2](document.title + \' \' + arguments[0] + arguments[1]), 100)',
|
||||
['Test', '!'])
|
||||
|
||||
try {
|
||||
await asyncCommand()
|
||||
} catch (err: any) {
|
||||
expect(err.message).toBe('Evaluation failed: script timeout')
|
||||
}
|
||||
|
||||
await browser.setTimeouts(0, 1000, 300)
|
||||
expect(await asyncCommand()).toBe('WebdriverJS Testpage Test!')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.setTimeouts(0, 300000, 300000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('alerts', () => {
|
||||
let jsAlertBtn: string, alertConfirmBtn: string, alertPromptBtn: string
|
||||
|
||||
beforeAll(async () => {
|
||||
await browser.navigateTo('https://the-internet.herokuapp.com/javascript_alerts')
|
||||
const buttons = await browser.findElements('css selector', '.example button')
|
||||
jsAlertBtn = buttons[0][ELEMENT_KEY]
|
||||
alertConfirmBtn = buttons[1][ELEMENT_KEY]
|
||||
alertPromptBtn = buttons[2][ELEMENT_KEY]
|
||||
})
|
||||
|
||||
it('getAlertText', async () => {
|
||||
await browser.elementClick(jsAlertBtn)
|
||||
expect(await browser.getAlertText()).toBe('I am a JS Alert')
|
||||
})
|
||||
|
||||
it('acceptAlert', async () => {
|
||||
expect.assertions(2)
|
||||
|
||||
await browser.acceptAlert()
|
||||
|
||||
const result = await browser.findElement('css selector', '#result')
|
||||
expect(await browser.getElementText(result[ELEMENT_KEY]))
|
||||
.toBe('You successfully clicked an alert')
|
||||
|
||||
try {
|
||||
await browser.getAlertText()
|
||||
} catch (err: any) {
|
||||
expect(err.message).toBe('no such alert')
|
||||
}
|
||||
})
|
||||
|
||||
it('dismissAlert', async () => {
|
||||
await browser.elementClick(alertConfirmBtn)
|
||||
expect(await browser.getAlertText()).toBe('I am a JS Confirm')
|
||||
|
||||
await browser.dismissAlert()
|
||||
const result = await browser.findElement('css selector', '#result')
|
||||
expect(await browser.getElementText(result[ELEMENT_KEY]))
|
||||
.toBe('You clicked: Cancel')
|
||||
|
||||
try {
|
||||
await browser.getAlertText()
|
||||
} catch (err: any) {
|
||||
expect(err.message).toBe('no such alert')
|
||||
}
|
||||
})
|
||||
|
||||
it('confirmAlert', async () => {
|
||||
await browser.elementClick(alertConfirmBtn)
|
||||
expect(await browser.getAlertText()).toBe('I am a JS Confirm')
|
||||
|
||||
await browser.acceptAlert()
|
||||
const result = await browser.findElement('css selector', '#result')
|
||||
expect(await browser.getElementText(result[ELEMENT_KEY]))
|
||||
.toBe('You clicked: Ok')
|
||||
})
|
||||
|
||||
it('sendAlertText', async () => {
|
||||
await browser.elementClick(alertPromptBtn)
|
||||
await browser.sendAlertText('foobar42')
|
||||
const result = await browser.findElement('css selector', '#result')
|
||||
expect(await browser.getElementText(result[ELEMENT_KEY]))
|
||||
.toBe('You entered: foobar42')
|
||||
|
||||
await browser.elementClick(alertPromptBtn)
|
||||
await browser.dismissAlert()
|
||||
const resultDismiss = await browser.findElement('css selector', '#result')
|
||||
expect(await browser.getElementText(resultDismiss[ELEMENT_KEY]))
|
||||
.toBe('You entered: null')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cookies', () => {
|
||||
beforeAll(async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
})
|
||||
|
||||
it('should have no cookies in the beginning', async () => {
|
||||
expect(await browser.getAllCookies()).toEqual([])
|
||||
})
|
||||
|
||||
it('should add a cookie', async () => {
|
||||
await browser.addCookie({
|
||||
name: 'foobar',
|
||||
value: 42
|
||||
})
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
expect(await browser.getAllCookies()).toHaveLength(1)
|
||||
|
||||
await browser.navigateTo('https://google.com')
|
||||
const cookies = await browser.getAllCookies()
|
||||
const ourCookie = cookies.find((cookie) => cookie.name ==='foobar' && cookie.value === '42')
|
||||
expect(ourCookie).toBe(undefined)
|
||||
})
|
||||
|
||||
it('deleteCookie', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
const cookie = await browser.getNamedCookie('foobar')
|
||||
expect(cookie.value).toBe('42')
|
||||
|
||||
await browser.deleteCookie('foobar')
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
expect(await browser.getAllCookies()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('window handling', () => {
|
||||
beforeAll(async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
})
|
||||
|
||||
it('should have a single window handle in the beginning', async () => {
|
||||
const handle = await browser.getWindowHandle()
|
||||
expect(typeof handle).toBe('string')
|
||||
|
||||
const handles = await browser.getWindowHandles()
|
||||
expect(handles).toEqual([handle])
|
||||
})
|
||||
|
||||
it('should switch to window created by DOM interaction', async () => {
|
||||
const newWindowLink = await browser.findElement('css selector', '#newWindow')
|
||||
await browser.elementClick(newWindowLink[ELEMENT_KEY])
|
||||
await sleep(1000)
|
||||
expect(await browser.getWindowHandles()).toHaveLength(2)
|
||||
|
||||
const handles = await browser.getWindowHandles()
|
||||
await browser.switchToWindow(handles[1])
|
||||
expect(await browser.getTitle()).toBe('two')
|
||||
|
||||
await browser.closeWindow()
|
||||
expect(await browser.getTitle()).toBe('WebdriverJS Testpage')
|
||||
expect(await browser.getWindowHandles()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('createWindow', async () => {
|
||||
expect(await browser.getTitle()).toBe('WebdriverJS Testpage')
|
||||
|
||||
await browser.createWindow('tab')
|
||||
const handles = await browser.getWindowHandles()
|
||||
expect(handles).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('switchToWindow', async () => {
|
||||
await browser.navigateTo('https://the-internet.herokuapp.com')
|
||||
expect(await browser.getTitle()).toBe('The Internet')
|
||||
|
||||
const handles = await browser.getWindowHandles()
|
||||
await browser.switchToWindow(handles[0])
|
||||
expect(await browser.getTitle()).toBe('WebdriverJS Testpage')
|
||||
})
|
||||
|
||||
it('closeWindow', async () => {
|
||||
await browser.closeWindow()
|
||||
expect(await browser.getTitle()).toBe('The Internet')
|
||||
})
|
||||
})
|
||||
|
||||
describe('frames', () => {
|
||||
beforeAll(async () => {
|
||||
await browser.navigateTo('https://the-internet.herokuapp.com/iframe')
|
||||
})
|
||||
|
||||
it('switchToFrame', async () => {
|
||||
const getDocumentText = () => browser.executeScript(
|
||||
'return document.documentElement.outerText',
|
||||
[]
|
||||
)
|
||||
|
||||
expect(await getDocumentText())
|
||||
.toContain('An iFrame containing the TinyMCE WYSIWYG Editor')
|
||||
const iframe = await browser.findElement('css selector', 'iframe')
|
||||
await browser.switchToFrame(iframe)
|
||||
|
||||
await sleep(1000)
|
||||
expect(await getDocumentText())
|
||||
.toContain('Your content goes here.')
|
||||
|
||||
const body = await browser.findElement('css selector', 'body')
|
||||
expect(await browser.getElementAttribute(body[ELEMENT_KEY], 'id')).toBe('tinymce')
|
||||
await browser.elementClick(body[ELEMENT_KEY])
|
||||
})
|
||||
|
||||
it('parentFrame', async () => {
|
||||
await browser.switchToParentFrame()
|
||||
expect(await browser.getTitle()).toBe('The Internet')
|
||||
})
|
||||
|
||||
it('switchToFrame(number)', async () => {
|
||||
const getDocumentText = () => browser.executeScript(
|
||||
'return document.documentElement.outerText',
|
||||
[]
|
||||
)
|
||||
|
||||
expect(await getDocumentText())
|
||||
.toContain('An iFrame containing the TinyMCE WYSIWYG Editor')
|
||||
await browser.switchToFrame(0)
|
||||
|
||||
expect(await getDocumentText())
|
||||
.toContain('Your content goes here.')
|
||||
|
||||
const body = await browser.findElement('css selector', 'body')
|
||||
expect(await browser.getElementAttribute(body[ELEMENT_KEY], 'id')).toBe('tinymce')
|
||||
await browser.elementClick(body[ELEMENT_KEY])
|
||||
})
|
||||
|
||||
it('switchToFrame(null)', async () => {
|
||||
await browser.switchToFrame(null)
|
||||
expect(await browser.getTitle()).toBe('The Internet')
|
||||
})
|
||||
|
||||
it('allows to switch to parent frame even if there isn\'t any', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io/two.html')
|
||||
expect(await browser.getPageSource()).toContain('<title>two</title>')
|
||||
await browser.switchToFrame(null)
|
||||
expect(await browser.getPageSource()).toContain('<title>two</title>')
|
||||
await browser.switchToFrame(null)
|
||||
expect(await browser.getPageSource()).toContain('<title>two</title>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('executeScript', () => {
|
||||
beforeAll(async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
})
|
||||
|
||||
it('sync', async () => {
|
||||
expect(await browser.executeScript(
|
||||
'return document.title + \' \' + arguments[0] + arguments[1]',
|
||||
['Test', '!'])
|
||||
).toBe('WebdriverJS Testpage Test!')
|
||||
})
|
||||
|
||||
it('sync error', async () => {
|
||||
const spy = vi.spyOn(global, 'clearTimeout')
|
||||
spy.mockClear()
|
||||
|
||||
await expect(async () => {
|
||||
await browser.executeScript(
|
||||
'throw new Error(\'sync error\')',
|
||||
[]
|
||||
)
|
||||
}).rejects.toThrow('sync error')
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(5)
|
||||
spy.mockClear()
|
||||
})
|
||||
|
||||
it('async', async () => {
|
||||
expect(await browser.executeAsyncScript(
|
||||
'return setTimeout(() => arguments[2](document.title + \' \' + arguments[0] + arguments[1]), 500)',
|
||||
['Test2', '!'])
|
||||
).toBe('WebdriverJS Testpage Test2!')
|
||||
})
|
||||
|
||||
it('respects promises', async () => {
|
||||
expect(await browser.executeScript(
|
||||
'return new Promise((resolve) => setTimeout(() => resolve(document.title + \' \' + arguments[0] + arguments[1]), 500))',
|
||||
['Test3', '!'])
|
||||
).toBe('WebdriverJS Testpage Test3!')
|
||||
})
|
||||
|
||||
it('refresh', async () => {
|
||||
await browser.executeScript('window.foobar = 42', [])
|
||||
expect(await browser.executeScript('return window.foobar', [])).toBe(42)
|
||||
await browser.refresh()
|
||||
expect(await browser.executeScript('return window.foobar', [])).toBe(undefined)
|
||||
})
|
||||
|
||||
it('serializes elements', async () => {
|
||||
const script = 'return (function () { return this.document.querySelector("body") }).apply(null, arguments)'
|
||||
const elem = await browser.executeScript(script, [])
|
||||
expect(typeof elem[ELEMENT_KEY]).toBe('string')
|
||||
})
|
||||
|
||||
it('can handle various of script formats', async () => {
|
||||
expect(await browser.executeScript('console.log("hello")', [])).toBe(undefined)
|
||||
expect(await browser.executeScript(' return "string";', [])).toBe('string')
|
||||
expect(await browser.executeScript('/* test */ console.log("test")', [])).toBe(undefined)
|
||||
expect(await browser.executeScript('return { foo: "bar" }', [])).toEqual({ foo: 'bar' })
|
||||
expect(await browser.executeScript('return ({ foo: "bar" })', [])).toEqual({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('handles windows', () => {
|
||||
it('should be able to work with popups', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io/')
|
||||
const parentButton = await browser.findElement('css selector', '#parentButton')
|
||||
await browser.elementClick(parentButton[ELEMENT_KEY])
|
||||
|
||||
const handles = await browser.getWindowHandles()
|
||||
await browser.switchToWindow(handles[handles.length - 1])
|
||||
|
||||
const closeButton = await waitForElement(browser, 'css selector', '#closeButton')
|
||||
await browser.elementClick(closeButton)
|
||||
|
||||
expect(await browser.getTitle()).toBe('WebdriverJS Testpage')
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.deleteSession()
|
||||
})
|
||||
BIN
e2e/screenshot.png
Normal file
BIN
e2e/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
|
|
@ -1,37 +0,0 @@
|
|||
import { beforeAll, describe, it, afterAll, expect } from 'vitest'
|
||||
import DevTools, { type Client } from '../../packages/devtools/src/index.js'
|
||||
|
||||
let browser: Client
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await DevTools.newSession({
|
||||
outputDir: __dirname,
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
'wdio:devtoolsOptions': {
|
||||
dumpio: true,
|
||||
headless: true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Chromium Edge', () => {
|
||||
it('can browser pages', async () => {
|
||||
await browser.navigateTo('http://json.org/')
|
||||
expect(await browser.getTitle()).toBe('JSON')
|
||||
})
|
||||
|
||||
it('has proper UA', async () => {
|
||||
const userAgent = await browser.executeScript('return navigator.userAgent', [])
|
||||
console.log('Detected user agent:', userAgent)
|
||||
expect(userAgent).toContain('Edg/')
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (!browser) {
|
||||
return
|
||||
}
|
||||
await browser.deleteSession()
|
||||
})
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { beforeAll, describe, it, afterAll, expect } from 'vitest'
|
||||
import DevTools, { type Client } from '../../packages/devtools/src/index.js'
|
||||
|
||||
let browser: Client
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await DevTools.newSession({
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
'wdio:devtoolsOptions': {
|
||||
dumpio: true,
|
||||
headless: true
|
||||
},
|
||||
...(process.env.FIREFOX_BINARY
|
||||
? { 'moz:firefoxOptions': { binary: process.env.FIREFOX_BINARY } }
|
||||
: {}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Firefox', () => {
|
||||
it('can browser pages', async () => {
|
||||
await browser.navigateTo('http://json.org/')
|
||||
expect(await browser.getTitle()).toBe('JSON')
|
||||
})
|
||||
|
||||
it('has proper UA', async () => {
|
||||
const userAgent = await browser.executeScript('return navigator.userAgent', [])
|
||||
console.log('Detected user agent:', userAgent)
|
||||
expect(userAgent).toContain('Firefox')
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (!browser) {
|
||||
return
|
||||
}
|
||||
await browser.deleteSession()
|
||||
})
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 1000 * 60,
|
||||
include: ['./**/*.e2e.ts'],
|
||||
hookTimeout: 60 * 1000,
|
||||
threads: false
|
||||
}
|
||||
})
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import { beforeAll, afterAll, test, expect } from 'vitest'
|
||||
import DevTools from '../packages/devtools/build/index.js'
|
||||
import { ELEMENT_KEY } from '../packages/webdriver/build/constants.js'
|
||||
import type { Client } from '../packages/devtools/build/index.js'
|
||||
|
||||
let browser: Client
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await DevTools.newSession({
|
||||
outputDir: __dirname,
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'wdio:devtoolsOptions': {
|
||||
headless: true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('can handle navigation through clicks', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
expect(await browser.getTitle()).toBe('WebdriverJS Testpage')
|
||||
|
||||
const link = await browser.findElement('link text', 'two')
|
||||
await browser.elementClick(link[ELEMENT_KEY])
|
||||
expect(await browser.getTitle()).toBe('two')
|
||||
|
||||
await browser.back()
|
||||
expect(await browser.getTitle()).toBe('WebdriverJS Testpage')
|
||||
|
||||
const githubLink = await browser.findElement('link text', 'GitHub Repo')
|
||||
await browser.elementClick(githubLink[ELEMENT_KEY])
|
||||
expect(await browser.getTitle()).toContain('GitHub')
|
||||
})
|
||||
|
||||
test('can handle navigation via JS', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
expect(await browser.getTitle()).toBe('WebdriverJS Testpage')
|
||||
|
||||
await browser.executeScript('window.location.href = "https://github.com"', [])
|
||||
expect(await browser.getTitle()).toContain('GitHub')
|
||||
})
|
||||
|
||||
test('can handle random page transitions', async () => {
|
||||
await browser.navigateTo('http://guinea-pig.webdriver.io')
|
||||
await browser.executeScript('return (() => setTimeout(() => { window.location.href = "https://github.com" }, 200))()', [])
|
||||
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < 1000) {
|
||||
const title = await browser.getTitle()
|
||||
expect(typeof title).toBe('string')
|
||||
|
||||
/**
|
||||
* stop loop if new page was reached
|
||||
*/
|
||||
if (title.includes('GitHub')) {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.deleteSession()
|
||||
})
|
||||
|
|
@ -8,5 +8,5 @@ describe('main suite 1', () => {
|
|||
: caps.browserName && caps.browserName.includes('chrome') ? 'chrome/' : caps.browserName!.toLowerCase()
|
||||
await browser.url('http://guinea-pig.webdriver.io/')
|
||||
await expect($('#useragent')).toHaveTextContaining(assertionValue, { ignoreCase: true })
|
||||
})
|
||||
}, { retry: 2 })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -46,11 +46,9 @@
|
|||
"test:component:run": "cd e2e && pnpm test:browser",
|
||||
"test:smoke": "node ./tests/smoke.runner.js",
|
||||
"test:e2e": "vitest --config e2e/vitest.config.ts --run",
|
||||
"test:e2e:edge": "vitest --config e2e/standalone/vitest.config.ts edge.e2e.ts",
|
||||
"test:e2e:firefox": "vitest --config e2e/standalone/vitest.config.ts firefox.e2e.ts",
|
||||
"test:e2e:webdriver": "tsx ./packages/wdio-cli/bin/wdio.js ./e2e/wdio/wdio.local.conf.ts",
|
||||
"test:e2e:cloud": "tsx ./packages/wdio-cli/bin/wdio.js ./e2e/wdio/wdio.sauce.conf.ts",
|
||||
"test:e2e:cjs": "node e2e/interop/webdriverio.e2e.js && node e2e/interop/webdriver.e2e.js && node e2e/interop/devtools.e2e.js && node e2e/interop/cucumber-framework.e2e.js && node e2e/interop/allure-reporter.e2e.js && node e2e/interop/shared-store.e2e.js && node e2e/interop/wdio-cli.e2e.js && node e2e/interop/wdio-reporter.e2e.js",
|
||||
"test:e2e:cjs": "node e2e/interop/webdriverio.e2e.js && node e2e/interop/webdriver.e2e.js && node e2e/interop/cucumber-framework.e2e.js && node e2e/interop/allure-reporter.e2e.js && node e2e/interop/shared-store.e2e.js && node e2e/interop/wdio-cli.e2e.js && node e2e/interop/wdio-reporter.e2e.js",
|
||||
"ts": "run-p ts:*",
|
||||
"ts:webdriver": "cd tests/typings/webdriver && npx tsc --skipLibCheck",
|
||||
"ts:devtools": "cd tests/typings/devtools && npx tsc --skipLibCheck",
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
src
|
||||
tests
|
||||
tsconfig.json
|
||||
tsconfig.prod.json
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
Copyright (c) OpenJS Foundation and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
> [!WARNING]
|
||||
> ***WebdriverIO is deprecating the use of Chrome Devtools as automation protocol through a WebDriver like interface. Instead, you can use the [`getPuppeteer`](/docs/api/browser/getPuppeteer) command to get ahold of a Puppeteer instance for Chrome Devtools introspection and automation.***
|
||||
|
||||
DevTools
|
||||
========
|
||||
|
||||
> A Chrome DevTools protocol binding that maps WebDriver commands into Chrome DevTools commands using [Puppeteer](https://www.npmjs.com/package/puppeteer)
|
||||
|
||||
This package provides a low level interface to run browser automation scripts based on the WebDriver protocol. If you are looking for a tool to automate Chrome or Firefox you should look up [Puppeteer](https://www.npmjs.com/package/puppeteer). This is suppose to be used by the [WebdriverIO](https://webdriver.io/) package in order to run its automation on the Chrome DevTools protocol.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm i webdriverio
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
The following example demonstrates how WebdriverIO can be used with the `devtools` package as automation binding using the [`automationProtocol`](https://webdriver.io/docs/options#automationProtocol) option:
|
||||
|
||||
```js
|
||||
import { remote } from 'webdriverio'
|
||||
|
||||
const browser = await remote({
|
||||
automationProtocol: 'devtools',
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'wdio:devtoolsOptions': {
|
||||
headless: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await browser.url('https://webdriver.io')
|
||||
|
||||
/**
|
||||
* run Puppeteer code
|
||||
*/
|
||||
const puppeteer = await browser.getPuppeteer()
|
||||
const page = (await puppeteer.pages())[0]
|
||||
await page.setRequestInterception(true)
|
||||
page.on('request', interceptedRequest => {
|
||||
if (interceptedRequest.url().endsWith('webdriverio.png')) {
|
||||
return interceptedRequest.continue({
|
||||
url: 'https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png'
|
||||
})
|
||||
}
|
||||
|
||||
interceptedRequest.continue()
|
||||
})
|
||||
|
||||
// continue with WebDriver commands
|
||||
await browser.url('https://v6.webdriver.io')
|
||||
await browser.pause(2000)
|
||||
|
||||
/**
|
||||
* now on the https://v6.webdriver.io page you see the Puppeteer logo
|
||||
* instead of the WebdriverIO one
|
||||
*/
|
||||
|
||||
await browser.deleteSession()
|
||||
```
|
||||
|
||||
## `wdio:devtoolsOptions` Capability
|
||||
|
||||
In order to set [Puppeteer specific configurations](https://pptr.dev/#?product=Puppeteer&version=v5.5.0&show=api-puppeteerlaunchoptions) you can use the `wdio:devtoolsOptions` capability which is a custom property (compliant to the WebDriver protocol), e.g.:
|
||||
|
||||
```js
|
||||
{
|
||||
browserName: 'chrome',
|
||||
'wdio:devtoolsOptions': {
|
||||
headless: true,
|
||||
defaultViewport: {
|
||||
width: 800,
|
||||
height: 600,
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
The following commands are already supported:
|
||||
|
||||
- [x] [newSession](https://w3c.github.io/webdriver/#new-session)
|
||||
- [x] [deleteSession](https://w3c.github.io/webdriver/#delete-session)
|
||||
- [x] [status](https://w3c.github.io/webdriver/#status)
|
||||
- [x] [getTimeouts](https://w3c.github.io/webdriver/#get-timeouts)
|
||||
- [x] [setTimeouts](https://w3c.github.io/webdriver/#set-timeouts)
|
||||
- [x] [getUrl](https://w3c.github.io/webdriver/#get-current-url)
|
||||
- [x] [navigateTo](https://w3c.github.io/webdriver/#navigate-to)
|
||||
- [x] [back](https://w3c.github.io/webdriver/#back)
|
||||
- [x] [forward](https://w3c.github.io/webdriver/#forward)
|
||||
- [x] [refresh](https://w3c.github.io/webdriver/#refresh)
|
||||
- [x] [getTitle](https://w3c.github.io/webdriver/#get-title)
|
||||
- [x] [getWindowHandle](https://w3c.github.io/webdriver/#get-window-handle)
|
||||
- [x] [closeWindow](https://w3c.github.io/webdriver/#close-window)
|
||||
- [x] [switchToWindow](https://w3c.github.io/webdriver/#switch-to-window)
|
||||
- [x] [createWindow](https://w3c.github.io/webdriver/#new-window)
|
||||
- [x] [getWindowHandles](https://w3c.github.io/webdriver/#get-window-handles)
|
||||
- [x] [switchToFrame](https://w3c.github.io/webdriver/#switch-to-frame)
|
||||
- [x] [switchToParentFrame](https://w3c.github.io/webdriver/#switch-to-parent-frame)
|
||||
- [x] [getWindowRect](https://w3c.github.io/webdriver/#get-window-rect)
|
||||
- [x] [setWindowRect](https://w3c.github.io/webdriver/#set-window-rect)
|
||||
- [ ] [maximizeWindow](https://w3c.github.io/webdriver/#maximize-window) (not possible with Puppeteer)
|
||||
- [ ] [minimizeWindow](https://w3c.github.io/webdriver/#minimize-window) (not possible with Puppeteer)
|
||||
- [ ] [fullscreenWindow](https://w3c.github.io/webdriver/#fullscreen-window) (not possible with Puppeteer)
|
||||
- [x] [findElement](https://w3c.github.io/webdriver/#find-element)
|
||||
- [x] [findElements](https://w3c.github.io/webdriver/#find-elements)
|
||||
- [x] [findElementFromElement](https://w3c.github.io/webdriver/#find-element-from-element)
|
||||
- [x] [findElementsFromElement](https://w3c.github.io/webdriver/#find-elements-from-element)
|
||||
- [x] [getActiveElement](https://w3c.github.io/webdriver/#get-active-element)
|
||||
- [x] [isElementSelected](https://w3c.github.io/webdriver/#is-element-selected)
|
||||
- [x] [isElementDisplayed](https://w3c.github.io/webdriver/#element-displayedness)
|
||||
- [x] [getElementAttribute](https://w3c.github.io/webdriver/#get-element-attribute)
|
||||
- [x] [getElementProperty](https://w3c.github.io/webdriver/#get-element-property)
|
||||
- [x] [getElementCSSValue](https://w3c.github.io/webdriver/#get-element-css-value)
|
||||
- [x] [getElementText](https://w3c.github.io/webdriver/#get-element-text)
|
||||
- [x] [getElementTagName](https://w3c.github.io/webdriver/#get-element-tag-name)
|
||||
- [x] [getElementRect](https://w3c.github.io/webdriver/#get-element-rect)
|
||||
- [x] [isElementEnabled](https://w3c.github.io/webdriver/#is-element-enabled)
|
||||
- [x] [elementClick](https://w3c.github.io/webdriver/#element-click)
|
||||
- [x] [elementClear](https://w3c.github.io/webdriver/#element-clear)
|
||||
- [x] [elementSendKeys](https://w3c.github.io/webdriver/#element-send-keys)
|
||||
- [x] [getPageSource](https://w3c.github.io/webdriver/#get-page-source)
|
||||
- [x] [executeScript](https://w3c.github.io/webdriver/#execute-script)
|
||||
- [x] [executeAsyncScript](https://w3c.github.io/webdriver/#execute-async-script)
|
||||
- [x] [getAllCookies](https://w3c.github.io/webdriver/#get-all-cookies)
|
||||
- [x] [getNamedCookie](https://w3c.github.io/webdriver/#get-named-cookie)
|
||||
- [x] [addCookie](https://w3c.github.io/webdriver/#add-cookie)
|
||||
- [x] [deleteAllCookies](https://w3c.github.io/webdriver/#delete-all-cookies)
|
||||
- [x] [deleteCookie](https://w3c.github.io/webdriver/#delete-cookie)
|
||||
- [x] [performActions](https://w3c.github.io/webdriver/#perform-actions)
|
||||
- [x] [releaseActions](https://w3c.github.io/webdriver/#release-actions)
|
||||
- [x] [dismissAlert](https://w3c.github.io/webdriver/#dismiss-alert)
|
||||
- [x] [acceptAlert](https://w3c.github.io/webdriver/#accept-alert)
|
||||
- [x] [getAlertText](https://w3c.github.io/webdriver/#get-alert-text)
|
||||
- [x] [sendAlertText](https://w3c.github.io/webdriver/#send-alert-text)
|
||||
- [x] [takeScreenshot](https://w3c.github.io/webdriver/#take-screenshot)
|
||||
- [x] [takeElementScreenshot](https://w3c.github.io/webdriver/#take-element-screenshot)
|
||||
|
||||
### Selector Strategies
|
||||
|
||||
- [x] [CSS Selector](https://w3c.github.io/webdriver/#css-selectors)
|
||||
- [x] [Link Text](https://w3c.github.io/webdriver/#partial-link-text)
|
||||
- [x] [Partial Link Text](https://w3c.github.io/webdriver/#partial-link-text)
|
||||
- [x] [Tag Name](https://w3c.github.io/webdriver/#tag-name)
|
||||
- [x] [XPath](https://w3c.github.io/webdriver/#xpath)
|
||||
|
||||
### Browser
|
||||
|
||||
- [x] Chrome
|
||||
- [x] Firefox (nightly only)
|
||||
- [x] Edge
|
||||
- [ ] Safari
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
{
|
||||
"name": "devtools",
|
||||
"version": "8.32.1",
|
||||
"description": "A Chrome DevTools protocol binding that maps WebDriver commands into Chrome DevTools commands using Puppeteer",
|
||||
"author": "Christian Bromann <mail@bromann.dev>",
|
||||
"homepage": "https://github.com/webdriverio/webdriverio/tree/main/packages/devtools",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/webdriverio/webdriverio.git",
|
||||
"directory": "packages/devtools"
|
||||
},
|
||||
"keywords": [
|
||||
"devtoolsdriver"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/webdriverio/webdriverio/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./build/cjs/index.js",
|
||||
"module": "./build/index.js",
|
||||
"types": "./build/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/cjs/index.js"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typeScriptVersion": "3.8.3",
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.0",
|
||||
"@wdio/config": "8.31.1",
|
||||
"@wdio/logger": "8.28.0",
|
||||
"@wdio/protocols": "8.32.0",
|
||||
"@wdio/types": "8.31.1",
|
||||
"@wdio/utils": "8.31.1",
|
||||
"chrome-launcher": "^1.0.0",
|
||||
"edge-paths": "^3.0.5",
|
||||
"import-meta-resolve": "^4.0.0",
|
||||
"puppeteer-core": "22.3.0",
|
||||
"query-selector-shadow-dom": "^1.0.0",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uuid": "^9.0.0",
|
||||
"which": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ua-parser-js": "^0.7.33",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/which": "^3.0.3",
|
||||
"@wdio/globals": "8.32.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
class Devtools {
|
||||
static async newSession (
|
||||
options: any,
|
||||
modifier?: (...args: any[]) => any,
|
||||
userPrototype = {},
|
||||
customCommandWrapper?: (...args: any[]) => any
|
||||
): Promise<any> {
|
||||
const WebDriver = (await import('../index.js')).default
|
||||
return WebDriver.newSession(options, modifier, userPrototype, customCommandWrapper)
|
||||
}
|
||||
|
||||
/**
|
||||
* allows user to attach to existing sessions
|
||||
*/
|
||||
static async attachToSession (
|
||||
options: any,
|
||||
modifier?: (...args: any[]) => any,
|
||||
userPrototype = {},
|
||||
commandWrapper?: (...args: any[]) => any
|
||||
): Promise<any> {
|
||||
const Devtools = (await import('../index.js')).default
|
||||
return Devtools.attachToSession(options, modifier, userPrototype, commandWrapper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes The instance session id and browser capabilities for the new session
|
||||
* directly into the passed in browser object
|
||||
*
|
||||
* @param {Object} instance the object we get from a new browser session.
|
||||
* @returns {string} the new session id of the browser
|
||||
*/
|
||||
static async reloadSession (instance: any, newCapabilities: any) {
|
||||
const Devtools = (await import('../index.js')).default
|
||||
return Devtools.reloadSession(instance, newCapabilities)
|
||||
}
|
||||
|
||||
static get Devtools () {
|
||||
return Devtools
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Devtools
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import acceptAlert from './commands/acceptAlert.js'
|
||||
import addCookie from './commands/addCookie.js'
|
||||
import back from './commands/back.js'
|
||||
import closeWindow from './commands/closeWindow.js'
|
||||
import createWindow from './commands/createWindow.js'
|
||||
import deleteAllCookies from './commands/deleteAllCookies.js'
|
||||
import deleteCookie from './commands/deleteCookie.js'
|
||||
import deleteSession from './commands/deleteSession.js'
|
||||
import dismissAlert from './commands/dismissAlert.js'
|
||||
import elementClear from './commands/elementClear.js'
|
||||
import elementClick from './commands/elementClick.js'
|
||||
import elementSendKeys from './commands/elementSendKeys.js'
|
||||
import executeAsyncScript from './commands/executeAsyncScript.js'
|
||||
import executeScript from './commands/executeScript.js'
|
||||
import findElement from './commands/findElement.js'
|
||||
import findElementFromElement from './commands/findElementFromElement.js'
|
||||
import findElements from './commands/findElements.js'
|
||||
import findElementsFromElement from './commands/findElementsFromElement.js'
|
||||
import getElementShadowRoot from './commands/getElementShadowRoot.js'
|
||||
import findElementFromShadowRoot from './commands/findElementFromShadowRoot.js'
|
||||
import findElementsFromShadowRoot from './commands/findElementsFromShadowRoot.js'
|
||||
import forward from './commands/forward.js'
|
||||
import getActiveElement from './commands/getActiveElement.js'
|
||||
import getAlertText from './commands/getAlertText.js'
|
||||
import getAllCookies from './commands/getAllCookies.js'
|
||||
import getElementAttribute from './commands/getElementAttribute.js'
|
||||
import getElementCSSValue from './commands/getElementCSSValue.js'
|
||||
import getElementComputedLabel from './commands/getElementComputedLabel.js'
|
||||
import getElementComputedRole from './commands/getElementComputedRole.js'
|
||||
import getElementProperty from './commands/getElementProperty.js'
|
||||
import getElementRect from './commands/getElementRect.js'
|
||||
import getElementTagName from './commands/getElementTagName.js'
|
||||
import getElementText from './commands/getElementText.js'
|
||||
import getNamedCookie from './commands/getNamedCookie.js'
|
||||
import getPageSource from './commands/getPageSource.js'
|
||||
import getTimeouts from './commands/getTimeouts.js'
|
||||
import getTitle from './commands/getTitle.js'
|
||||
import getUrl from './commands/getUrl.js'
|
||||
import getWindowHandle from './commands/getWindowHandle.js'
|
||||
import getWindowHandles from './commands/getWindowHandles.js'
|
||||
import getWindowRect from './commands/getWindowRect.js'
|
||||
import isElementEnabled from './commands/isElementEnabled.js'
|
||||
import isElementSelected from './commands/isElementSelected.js'
|
||||
import navigateTo from './commands/navigateTo.js'
|
||||
import newSession from './commands/newSession.js'
|
||||
import performActions from './commands/performActions.js'
|
||||
import refresh from './commands/refresh.js'
|
||||
import releaseActions from './commands/releaseActions.js'
|
||||
import sendAlertText from './commands/sendAlertText.js'
|
||||
import setTimeouts from './commands/setTimeouts.js'
|
||||
import setWindowRect from './commands/setWindowRect.js'
|
||||
import status from './commands/status.js'
|
||||
import switchToFrame from './commands/switchToFrame.js'
|
||||
import switchToParentFrame from './commands/switchToParentFrame.js'
|
||||
import switchToWindow from './commands/switchToWindow.js'
|
||||
import takeElementScreenshot from './commands/takeElementScreenshot.js'
|
||||
import takeScreenshot from './commands/takeScreenshot.js'
|
||||
|
||||
export {
|
||||
acceptAlert, addCookie, back, closeWindow, createWindow, deleteAllCookies, deleteCookie, deleteSession,
|
||||
dismissAlert, elementClear, elementClick, elementSendKeys, executeAsyncScript, executeScript, findElement,
|
||||
findElementFromElement, findElements, findElementsFromElement, forward, getActiveElement, getAlertText,
|
||||
getAllCookies, getElementAttribute, getElementCSSValue, getElementComputedLabel, getElementComputedRole,
|
||||
getElementProperty, getElementRect, getElementTagName, getElementText, getNamedCookie, getPageSource,
|
||||
getTimeouts, getTitle, getUrl, getWindowHandle, getWindowHandles, getWindowRect, isElementEnabled,
|
||||
isElementSelected, navigateTo, newSession, performActions, refresh, releaseActions, sendAlertText,
|
||||
setTimeouts, setWindowRect, status, switchToFrame, switchToParentFrame, switchToWindow, takeElementScreenshot,
|
||||
takeScreenshot, getElementShadowRoot, findElementFromShadowRoot, findElementsFromShadowRoot
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Accept Alert command accepts a simple dialog if present, otherwise error.
|
||||
*
|
||||
* @alias browser.acceptAlert
|
||||
* @see https://w3c.github.io/webdriver/#dfn-accept-alert
|
||||
*/
|
||||
export default async function acceptAlert(this: DevToolsDriver) {
|
||||
if (!this.activeDialog) {
|
||||
throw new Error('no such alert')
|
||||
}
|
||||
|
||||
await this.activeDialog.accept(this.activeDialog.defaultValue())
|
||||
delete this.activeDialog
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import type { Cookie } from '@wdio/protocols'
|
||||
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Add Cookie command adds a single cookie to the cookie store
|
||||
* associated with the active document's address.
|
||||
*
|
||||
* @alias browser.addCookie
|
||||
* @see https://w3c.github.io/webdriver/#dfn-adding-a-cookie
|
||||
* @param {object} cookie A JSON object representing a cookie. It must have at least the name and value fields and could have more, including expiry-time and so on
|
||||
*/
|
||||
export default async function addCookie(
|
||||
this: DevToolsDriver,
|
||||
{ cookie }: { cookie: Cookie }
|
||||
) {
|
||||
const page = this.getPageHandle()
|
||||
|
||||
const cookieProps = Object.keys(cookie)
|
||||
if (!cookieProps.includes('name') || !cookieProps.includes('value')) {
|
||||
throw new Error(
|
||||
'Provided cookie object is missing either "name" or "value" property'
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof cookie.value !== 'string') {
|
||||
cookie.value = (cookie.value as any).toString()
|
||||
}
|
||||
|
||||
await page.setCookie(cookie)
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Back command causes the browser to traverse one step backward
|
||||
* in the joint session history of the current top-level browsing context.
|
||||
* This is equivalent to pressing the back button in the browser chrome or calling `window.history.back`.
|
||||
*
|
||||
* @alias browser.back
|
||||
* @see https://w3c.github.io/webdriver/#dfn-back
|
||||
*/
|
||||
export default async function back (this: DevToolsDriver) {
|
||||
delete this.currentFrame
|
||||
const page = this.getPageHandle()
|
||||
|
||||
await page.goBack()
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { sleep } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Close Window command closes the current top-level browsing context.
|
||||
* Once done, if there are no more top-level browsing contexts open,
|
||||
* the WebDriver session itself is closed.
|
||||
*
|
||||
* @alias browser.closeWindow
|
||||
* @see https://w3c.github.io/webdriver/#dfn-close-window
|
||||
*/
|
||||
export default async function closeWindow (this: DevToolsDriver) {
|
||||
delete this.currentFrame
|
||||
|
||||
const page = this.getPageHandle()
|
||||
await page.close()
|
||||
await sleep(100)
|
||||
|
||||
const handles = [...this.windows.keys()]
|
||||
this.currentWindowHandle = handles[handles.length - 1]
|
||||
|
||||
if (!this.currentWindowHandle) {
|
||||
const page = await this.browser.newPage()
|
||||
const newWindowHandle = uuidv4()
|
||||
this.windows.set(newWindowHandle, page)
|
||||
this.currentWindowHandle = newWindowHandle
|
||||
}
|
||||
|
||||
const newPage = this.getPageHandle()
|
||||
await newPage.bringToFront()
|
||||
return this.currentWindowHandle
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import command from '../scripts/createWindow.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
const WINDOW_FEATURES = 'menubar=1,toolbar=1,location=1,resizable=1,scrollbars=1'
|
||||
const NEW_PAGE_URL = 'about:blank'
|
||||
const DEFAULT_WINDOW_TYPE = 'tab'
|
||||
|
||||
/**
|
||||
* Create a new top-level browsing context.
|
||||
*
|
||||
* @alias browser.createWindow
|
||||
* @see https://w3c.github.io/webdriver/#new-window
|
||||
* @param {string} type Set to 'tab' if the newly created window shares an OS-level window with the current browsing context, or 'window' otherwise.
|
||||
* @return {object} New window object containing 'handle' with the value of the handle and 'type' with the value of the created window type
|
||||
*/
|
||||
export default async function createWindow (
|
||||
this: DevToolsDriver,
|
||||
{ type }: { type: 'window' | 'tab' }
|
||||
) {
|
||||
type = type || DEFAULT_WINDOW_TYPE
|
||||
let newPage
|
||||
|
||||
if (type === 'window') {
|
||||
const page = this.getPageHandle()
|
||||
|
||||
await page.evaluate(command, NEW_PAGE_URL, WINDOW_FEATURES)
|
||||
const newWindowTarget = await this.browser.waitForTarget(
|
||||
(target) => target.url() === NEW_PAGE_URL)
|
||||
|
||||
newPage = await newWindowTarget.page()
|
||||
if (!newPage) {
|
||||
throw new Error('Couldn\'t find page to switch to')
|
||||
}
|
||||
} else {
|
||||
newPage = await this.browser.newPage()
|
||||
}
|
||||
|
||||
await newPage.bringToFront()
|
||||
return {
|
||||
handle: this.currentWindowHandle,
|
||||
type
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Delete All Cookies command allows deletion of all cookies
|
||||
* associated with the active document's address.
|
||||
*
|
||||
* @alias browser.deleteAllCookies
|
||||
* @see https://w3c.github.io/webdriver/#dfn-delete-all-cookies
|
||||
*/
|
||||
export default async function deleteAllCookies (this: DevToolsDriver) {
|
||||
const page = this.getPageHandle()
|
||||
const cookies = await page.cookies()
|
||||
|
||||
for (const cookie of cookies) {
|
||||
await page.deleteCookie(cookie)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Delete Cookie command allows you to delete either a single cookie by parameter name,
|
||||
* or all the cookies associated with the active document's address if name is undefined.
|
||||
*
|
||||
* @alias browser.deleteCookie
|
||||
* @see https://w3c.github.io/webdriver/#dfn-delete-cookie
|
||||
* @param {string} name name of the cookie to delete
|
||||
*/
|
||||
export default async function deleteCookie (
|
||||
this: DevToolsDriver,
|
||||
{ name }: { name: string }
|
||||
) {
|
||||
const page = this.getPageHandle()
|
||||
const cookies = await page.cookies()
|
||||
const cookieToDelete = cookies.find((cookie) => cookie.name === name)
|
||||
|
||||
if (cookieToDelete) {
|
||||
await page.deleteCookie(cookieToDelete)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Delete Session command closes any top-level browsing contexts associated
|
||||
* with the current session, terminates the connection, and finally closes the current session.
|
||||
*
|
||||
* @alias browser.deleteSession
|
||||
* @see https://w3c.github.io/webdriver/#dfn-delete-session
|
||||
*/
|
||||
export default async function deleteSession (this: DevToolsDriver) {
|
||||
await this.browser.close()
|
||||
this.windows.clear()
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Dismiss Alert command dismisses a simple dialog if present, otherwise error.
|
||||
* A request to dismiss an alert user prompt, which may not necessarily have a dismiss button,
|
||||
* has the same effect as accepting it.
|
||||
*
|
||||
* @alias browser.dismissAlert
|
||||
* @see https://w3c.github.io/webdriver/#dfn-dismiss-alert
|
||||
*/
|
||||
export default async function dismissAlert (this: DevToolsDriver) {
|
||||
if (!this.activeDialog) {
|
||||
throw new Error('no such alert')
|
||||
}
|
||||
|
||||
await this.activeDialog.dismiss()
|
||||
delete this.activeDialog
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import command from '../scripts/elementClear.js'
|
||||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Element Clear command scrolls into view an editable or resettable element and then attempts
|
||||
* to clear its selected files or text content.
|
||||
*
|
||||
* @alias browser.elementClear
|
||||
* @see https://w3c.github.io/webdriver/#dfn-element-clear
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
*/
|
||||
export default async function elementClear (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
const page = this.getPageHandle(true)
|
||||
await page.$eval('html', command, elementHandle)
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import getElementTagName from './getElementTagName.js'
|
||||
import selectOptionScript from '../scripts/selectOption.js'
|
||||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Element Click command scrolls into view the element if it is not already pointer-interactable,
|
||||
* and clicks its in-view center point. If the element's center point is obscured by another element,
|
||||
* an element click intercepted error is returned.
|
||||
*
|
||||
* If the element is outside the viewport, an element not interactable error is returned.
|
||||
*
|
||||
* @alias browser.elementClick
|
||||
* @see https://w3c.github.io/webdriver/#dfn-element-click
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
*/
|
||||
export default async function elementClick (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const page = this.getPageHandle()
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
/**
|
||||
* in order to allow clicking on option elements (as WebDriver does)
|
||||
* we need to check if the element is such a an element and select
|
||||
* it instead of actually executing the click
|
||||
*/
|
||||
const tagName = await getElementTagName.call(this, { elementId })
|
||||
if (tagName === 'option') {
|
||||
return page.$eval('html', selectOptionScript, elementHandle)
|
||||
}
|
||||
|
||||
/**
|
||||
* ensure to fulfill the click promise if the click has triggered an alert
|
||||
*/
|
||||
return new Promise((resolve, reject) => {
|
||||
/**
|
||||
* listen on possible modal dialogs that might pop up due to the
|
||||
* click action, just continue in this case
|
||||
*/
|
||||
const dialogHandler = () => resolve(null)
|
||||
page.once('dialog', dialogHandler)
|
||||
return elementHandle.click().then(() => {
|
||||
/**
|
||||
* no modals popped up, so clean up the listener
|
||||
*/
|
||||
page.off('dialog', dialogHandler)
|
||||
resolve(null)
|
||||
}).catch(reject)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import path from 'node:path'
|
||||
import { UNICODE_CHARACTERS } from '@wdio/utils'
|
||||
import type { ElementHandle, KeyInput } from 'puppeteer-core'
|
||||
|
||||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Element Send Keys command scrolls into view the form control element and then sends
|
||||
* the provided keys to the element. In case the element is not keyboard-interactable,
|
||||
* an element not interactable error is returned. The key input state used for input
|
||||
* may be cleared mid-way through "typing" by sending the null key, which is U+E000 (NULL)
|
||||
*
|
||||
* @alias browser.elementSendKeys
|
||||
* @see https://w3c.github.io/webdriver/#dfn-element-send-keys
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @param {string} text string to send as keystrokes to the element
|
||||
*/
|
||||
export default async function elementSendKeys (
|
||||
this: DevToolsDriver,
|
||||
{ elementId, text }: { elementId: string, text: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId) as any as ElementHandle<HTMLInputElement>
|
||||
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
await elementHandle.focus()
|
||||
const page = this.getPageHandle()
|
||||
const propertyHandles = {
|
||||
tagName: await elementHandle.getProperty('tagName'),
|
||||
type: await elementHandle.getProperty('type')
|
||||
}
|
||||
|
||||
const tagName = await propertyHandles.tagName?.jsonValue() as unknown as string
|
||||
const type = await propertyHandles.type?.jsonValue() as unknown as string
|
||||
let typeInput: string[] = [text]
|
||||
|
||||
for (const [key, value] of Object.entries(UNICODE_CHARACTERS)) {
|
||||
typeInput = typeInput.reduce((input, val) => [
|
||||
...input,
|
||||
...val.split(value).flatMap(
|
||||
(value: string, index: number, array: string[]) =>
|
||||
array.length - 1 !== index // check for the last item
|
||||
? [value, key]
|
||||
: value,
|
||||
)
|
||||
], [] as string[])
|
||||
}
|
||||
|
||||
if (tagName === 'INPUT' && type === 'file') {
|
||||
const paths = (text || '').split('\n').map(p => path.resolve(p))
|
||||
await elementHandle.uploadFile(...paths)
|
||||
} else {
|
||||
for (const input of typeInput) {
|
||||
UNICODE_CHARACTERS[input as keyof typeof UNICODE_CHARACTERS]
|
||||
? await page.keyboard.press(input as KeyInput)
|
||||
: await page.keyboard.type(input)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import command from '../scripts/executeAsyncScript.js'
|
||||
import { transformExecuteArgs, transformExecuteResult } from '../utils.js'
|
||||
import { SERIALIZE_PROPERTY, SERIALIZE_FLAG } from '../constants.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Execute Async Script command causes JavaScript to execute as an anonymous function.
|
||||
* Unlike the Execute Script command, the result of the function is ignored.
|
||||
* Instead an additional argument is provided as the final argument to the function.
|
||||
* This is a function that, when called, returns its first argument as the response.
|
||||
*
|
||||
* @alias browser.executeScript
|
||||
* @see https://w3c.github.io/webdriver/#dfn-execute-async-script
|
||||
* @param {string} script a string, the Javascript function body you want executed
|
||||
* @param {*[]} args an array of JSON values which will be deserialized and passed as arguments to your function
|
||||
* @return * Either the return value of your script, the fulfillment of the Promise returned by your script, or the error which was the reason for your script's returned Promise's rejection.
|
||||
*/
|
||||
export default async function executeAsyncScript (
|
||||
this: DevToolsDriver,
|
||||
{ script, args }: { script: string, args: any[] }
|
||||
) {
|
||||
const page = this.getPageHandle(true)
|
||||
const scriptTimeout = this.timeouts.get('script') || 0
|
||||
script = script.trim()
|
||||
|
||||
if (script.startsWith('return (')) {
|
||||
script = script.slice(7)
|
||||
}
|
||||
|
||||
if (script.startsWith('return')) {
|
||||
script = `(function () { ${script} }).apply(null, arguments)`
|
||||
}
|
||||
|
||||
const result = await page.$eval(
|
||||
'html',
|
||||
command,
|
||||
script,
|
||||
scriptTimeout,
|
||||
SERIALIZE_PROPERTY,
|
||||
SERIALIZE_FLAG,
|
||||
...(await transformExecuteArgs.call(this, args))
|
||||
)
|
||||
|
||||
return transformExecuteResult.call(this, page, result)
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import command from '../scripts/executeScript.js'
|
||||
import { transformExecuteArgs, transformExecuteResult } from '../utils.js'
|
||||
import { SERIALIZE_PROPERTY, SERIALIZE_FLAG } from '../constants.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Execute Script command executes a JavaScript function in the context of the
|
||||
* current browsing context and returns the return value of the function.
|
||||
*
|
||||
* @alias browser.executeScript
|
||||
* @see https://w3c.github.io/webdriver/#dfn-execute-script
|
||||
* @param {string} script a string, the Javascript function body you want executed
|
||||
* @param {*[]} args an array of JSON values which will be deserialized and passed as arguments to your function
|
||||
* @return * Either the return value of your script, the fulfillment of the Promise returned by your script, or the error which was the reason for your script's returned Promise's rejection.
|
||||
*/
|
||||
export default async function executeScript (
|
||||
this: DevToolsDriver,
|
||||
{ script, args }: { script: string, args: any[] }
|
||||
) {
|
||||
const page = this.getPageHandle(true)
|
||||
const scriptTimeout = this.timeouts.get('script')
|
||||
script = script.trim()
|
||||
|
||||
if (script.startsWith('return (')) {
|
||||
script = script.slice(7)
|
||||
}
|
||||
|
||||
if (script.startsWith('return')) {
|
||||
script = `(function () { ${script} }).apply(null, arguments)`
|
||||
}
|
||||
|
||||
const executePromise = page.$eval(
|
||||
'html',
|
||||
command,
|
||||
script,
|
||||
SERIALIZE_PROPERTY,
|
||||
SERIALIZE_FLAG,
|
||||
...(await transformExecuteArgs.call(this, args))
|
||||
)
|
||||
|
||||
let executeTimeout: undefined | ReturnType<typeof setTimeout>
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
executeTimeout = setTimeout(() => {
|
||||
const timeoutError = `script timeout${
|
||||
this.activeDialog
|
||||
? ' reason: a browser dialog has opened as result of a executeScript call'
|
||||
: ''
|
||||
}`
|
||||
|
||||
return reject(new Error(timeoutError))
|
||||
}, scriptTimeout)
|
||||
})
|
||||
|
||||
const result = await Promise.race([executePromise, timeoutPromise]).finally(() => {
|
||||
clearTimeout(executeTimeout)
|
||||
})
|
||||
|
||||
return transformExecuteResult.call(this, page, result)
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { SUPPORTED_SELECTOR_STRATEGIES } from '../constants.js'
|
||||
import { findElement as findElementUtil } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Find Element command is used to find an element in the current browsing context
|
||||
* that can be used for future commands.
|
||||
*
|
||||
* @alias browser.findElement
|
||||
* @see https://w3c.github.io/webdriver/#dfn-find-element
|
||||
* @param {string} using a valid element location strategy
|
||||
* @param {string} value the actual selector that will be used to find an element
|
||||
* @return {Object} A JSON representation of an element object.
|
||||
*/
|
||||
export default function findElement (
|
||||
this: DevToolsDriver,
|
||||
{ using, value }: { using: string, value: string }
|
||||
) {
|
||||
if (!SUPPORTED_SELECTOR_STRATEGIES.includes(using)) {
|
||||
throw new Error(`selector strategy "${using}" is not yet supported`)
|
||||
}
|
||||
|
||||
if (using === 'link text') {
|
||||
using = 'xpath'
|
||||
value = `//a[normalize-space() = "${value}"]`
|
||||
} else if (using === 'partial link text') {
|
||||
using = 'xpath'
|
||||
value = `//a[contains(., "${value}")]`
|
||||
} else if (using === 'shadow') {
|
||||
/**
|
||||
* `shadow/<selector>` is the way query-selector-shadow-dom
|
||||
* understands to query for shadow elements
|
||||
*/
|
||||
using = 'css'
|
||||
value = `shadow/${value}`
|
||||
}
|
||||
|
||||
const page = this.getPageHandle(true)
|
||||
return findElementUtil.call(this, page, using, value)
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { SUPPORTED_SELECTOR_STRATEGIES } from '../constants.js'
|
||||
import { findElement, getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Find Element From Element command is used to find an element from a web element
|
||||
* in the current browsing context that can be used for future commands.
|
||||
*
|
||||
* @alias browser.findElementFromElement
|
||||
* @see https://w3c.github.io/webdriver/#dfn-find-element-from-element
|
||||
* @param {string} using a valid element location strategy
|
||||
* @param {string} value the actual selector that will be used to find an element
|
||||
* @return {Object} A JSON representation of an element object.
|
||||
*/
|
||||
export default async function findElementFromElement (
|
||||
this: DevToolsDriver,
|
||||
{ elementId, using, value }: { elementId: string, using: string, value: string }
|
||||
) {
|
||||
if (!SUPPORTED_SELECTOR_STRATEGIES.includes(using)) {
|
||||
throw new Error(`selector strategy "${using}" is not yet supported`)
|
||||
}
|
||||
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
if (using === 'link text') {
|
||||
using = 'xpath'
|
||||
value = `.//a[normalize-space() = "${value}"]`
|
||||
} else if (using === 'partial link text') {
|
||||
using = 'xpath'
|
||||
value = `.//a[contains(., "${value}")]`
|
||||
} else if (using === 'shadow') {
|
||||
/**
|
||||
* `shadow/<selector>` is the way query-selector-shadow-dom
|
||||
* understands to query for shadow elements
|
||||
*/
|
||||
using = 'css'
|
||||
value = `shadow/${value}`
|
||||
}
|
||||
|
||||
return findElement.call(this, elementHandle, using, value)
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import findElementFromElement from './findElementFromElement.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Find Element From Shadow Root command is used to find an element
|
||||
* within the shadow root of an element that can be used for future commands.
|
||||
* This command returns JSON representation of the element that can be passed
|
||||
* to $ command to transform the reference to an extended WebdriverIO element.
|
||||
*
|
||||
* @alias browser.findElementFromShadowRoot
|
||||
* @see https://w3c.github.io/webdriver/#find-element-from-shadow-root
|
||||
* @param {string} using a valid element location strategy
|
||||
* @param {string} value the actual selector that will be used to find an element
|
||||
* @return {Object} A JSON representation of an element shadow object, e.g. `{ 'element-6066-11e4-a52e-4f735466cecf': 'ELEMENT_1' }`.
|
||||
*/
|
||||
export default async function findElementFromShadowRoot (
|
||||
this: DevToolsDriver,
|
||||
{ shadowId, using, value }: { shadowId: string, using: string, value: string }
|
||||
) {
|
||||
if (using !== 'css selector') {
|
||||
throw new Error('Fetching elements from a shadow element using something other than "css selector" is currently not supported.')
|
||||
}
|
||||
|
||||
return findElementFromElement.call(this, {
|
||||
elementId: shadowId,
|
||||
using: 'shadow',
|
||||
value
|
||||
})
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { SUPPORTED_SELECTOR_STRATEGIES } from '../constants.js'
|
||||
import { findElements as findElementsUtil } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Find Elements command is used to find elements
|
||||
* in the current browsing context that can be used for future commands.
|
||||
*
|
||||
* @alias browser.findElements
|
||||
* @see https://w3c.github.io/webdriver/#dfn-find-elements
|
||||
* @param {string} using a valid element location strategy
|
||||
* @param {string} value the actual selector that will be used to find an element
|
||||
* @return {object[]} A (possibly empty) JSON list of representations of an element object.
|
||||
*/
|
||||
export default async function findElements (
|
||||
this: DevToolsDriver,
|
||||
{ using, value }: { using: string, value: string }
|
||||
) {
|
||||
if (!SUPPORTED_SELECTOR_STRATEGIES.includes(using)) {
|
||||
throw new Error(`selector strategy "${using}" is not yet supported`)
|
||||
}
|
||||
|
||||
if (using === 'link text') {
|
||||
using = 'xpath'
|
||||
value = `//a[normalize-space() = "${value}"]`
|
||||
} else if (using === 'partial link text') {
|
||||
using = 'xpath'
|
||||
value = `//a[contains(., "${value}")]`
|
||||
} else if (using === 'shadow') {
|
||||
/**
|
||||
* `shadow/<selector>` is the way query-selector-shadow-dom
|
||||
* understands to query for shadow elements
|
||||
*/
|
||||
using = 'css'
|
||||
value = `shadow/${value}`
|
||||
}
|
||||
|
||||
const page = this.getPageHandle(true)
|
||||
return findElementsUtil.call(this, page, using, value)
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { SUPPORTED_SELECTOR_STRATEGIES } from '../constants.js'
|
||||
import { findElements, getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Find Elements From Element command is used to find elements from a web element
|
||||
* in the current browsing context that can be used for future commands.
|
||||
*
|
||||
* @alias browser.findElementFromElements
|
||||
* @see https://w3c.github.io/webdriver/#dfn-find-elements-from-element
|
||||
* @param {string} using a valid element location strategy
|
||||
* @param {string} value the actual selector that will be used to find an element
|
||||
* @return {object[]} A (possibly empty) JSON list of representations of an element object.
|
||||
*/
|
||||
export default async function findElementFromElements (
|
||||
this: DevToolsDriver,
|
||||
{ elementId, using, value }: { elementId: string, using: string, value: string }
|
||||
) {
|
||||
if (!SUPPORTED_SELECTOR_STRATEGIES.includes(using)) {
|
||||
throw new Error(`selector strategy "${using}" is not yet supported`)
|
||||
}
|
||||
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
if (using === 'link text') {
|
||||
using = 'xpath'
|
||||
value = `.//a[normalize-space() = "${value}"]`
|
||||
} else if (using === 'partial link text') {
|
||||
using = 'xpath'
|
||||
value = `.//a[contains(., "${value}")]`
|
||||
} else if (using === 'shadow') {
|
||||
/**
|
||||
* `shadow/<selector>` is the way query-selector-shadow-dom
|
||||
* understands to query for shadow elements
|
||||
*/
|
||||
using = 'css'
|
||||
value = `shadow/${value}`
|
||||
}
|
||||
|
||||
return findElements.call(this, elementHandle, using, value)
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import findElementsFromElement from './findElementsFromElement.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Find Elements command is used to find elements within the shadow root of an
|
||||
* element that can be used for future commands. This command returns array of JSON
|
||||
* representation of the elements that can be passed to $ command to transform the
|
||||
* reference to an extended WebdriverIO element (See findElement).
|
||||
*
|
||||
* @alias browser.findElementsFromShadowRoot
|
||||
* @see https://w3c.github.io/webdriver/#find-elements-from-shadow-root
|
||||
* @param {string} using a valid element location strategy
|
||||
* @param {string} value the actual selector that will be used to find an element
|
||||
* @return {object[]} A (possibly empty) JSON list of representations of an element object, e.g. `{ 'element-6066-11e4-a52e-4f735466cecf': 'ELEMENT_1' }`.
|
||||
*/
|
||||
export default async function findElementsFromShadowRoot (
|
||||
this: DevToolsDriver,
|
||||
{ shadowId, using, value }: { shadowId: string, using: string, value: string }
|
||||
) {
|
||||
if (using !== 'css selector') {
|
||||
throw new Error('Fetching elements from a shadow element using something other than "css selector" is currently not supported.')
|
||||
}
|
||||
|
||||
return findElementsFromElement.call(this, {
|
||||
elementId: shadowId,
|
||||
using: 'shadow',
|
||||
value
|
||||
})
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Forward command causes the browser to traverse one step forwards
|
||||
* in the joint session history of the current top-level browsing context.
|
||||
*
|
||||
* @alias browser.forward
|
||||
* @see https://w3c.github.io/webdriver/#dfn-forward
|
||||
*/
|
||||
export default async function forward (this: DevToolsDriver) {
|
||||
delete this.currentFrame
|
||||
const page = this.getPageHandle()
|
||||
await page.goForward()
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import findElement from './findElement.js'
|
||||
import command from '../scripts/getActiveElement.js'
|
||||
import cleanUp from '../scripts/cleanUpSerializationSelector.js'
|
||||
import { SERIALIZE_PROPERTY } from '../constants.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* Get Active Element returns the active element of the current browsing context’s document element.
|
||||
*
|
||||
* @param browser.getActiveElement
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-active-element
|
||||
* @return {Object} A JSON representation of an element object.
|
||||
*/
|
||||
export default async function getActiveElement (
|
||||
this: DevToolsDriver
|
||||
) {
|
||||
const page = this.getPageHandle(true)
|
||||
const selector = `[${SERIALIZE_PROPERTY}]`
|
||||
|
||||
/**
|
||||
* set data property to active element to allow to query for it
|
||||
*/
|
||||
const hasElem = await page.$eval('html', command, SERIALIZE_PROPERTY)
|
||||
|
||||
if (!hasElem) {
|
||||
throw new Error('no element active')
|
||||
}
|
||||
|
||||
/**
|
||||
* query for element
|
||||
*/
|
||||
const activeElement = await findElement.call(this, {
|
||||
using: 'css selector',
|
||||
value: selector
|
||||
})
|
||||
|
||||
/**
|
||||
* clean up data property
|
||||
*/
|
||||
await page.$eval(selector, cleanUp, SERIALIZE_PROPERTY)
|
||||
|
||||
return activeElement
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Alert Text command returns the message of the current user prompt.
|
||||
* If there is no current user prompt, it returns an error.
|
||||
*
|
||||
* @alias browser.getAlertText
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-alert-text
|
||||
* @return {string} The message of the user prompt.
|
||||
*/
|
||||
export default function getAlertText (this: DevToolsDriver) {
|
||||
if (!this.activeDialog) {
|
||||
throw new Error('no such alert')
|
||||
}
|
||||
|
||||
return this.activeDialog.message()
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get All Cookies command returns all cookies associated with the address
|
||||
* of the current browsing context’s active document.
|
||||
*
|
||||
* @alias browser.getAllCookies
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-all-cookies
|
||||
* @return {Object[]} A list of serialized cookies. Each serialized cookie has a number of optional fields which may or may not be returned in addition to `name` and `value`.
|
||||
*/
|
||||
export default async function getAllCookies (this: DevToolsDriver) {
|
||||
const page = this.getPageHandle()
|
||||
return page.cookies()
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import command from '../scripts/getElementAttribute.js'
|
||||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Element Attribute command will return the attribute of a web element.
|
||||
*
|
||||
* @alias browser.getElementAttribute
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-element-attribute
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @param {string} name name of the attribute value to retrieve
|
||||
* @return {string} The named attribute of the element.
|
||||
*/
|
||||
export default async function getElementAttribute (
|
||||
this: DevToolsDriver,
|
||||
{ elementId, name }: { elementId: string, name: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
const page = this.getPageHandle(true)
|
||||
return page.$eval('html', command, elementHandle, name)
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import command from '../scripts/getElementCSSValue.js'
|
||||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Element CSS Value command retrieves the computed value
|
||||
* of the given CSS property of the given web element.
|
||||
*
|
||||
* @alias browser.getElementCSSValue
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-element-css-value
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @param {string} propertyName name of the CSS property to retrieve
|
||||
* @return {string} The computed value of the parameter corresponding to property name from the element's style declarations (unless the document type is xml, in which case the return value is simply the empty string).
|
||||
*/
|
||||
export default async function getElementCSSValue (
|
||||
this: DevToolsDriver,
|
||||
{ elementId, propertyName }: { elementId: string, propertyName: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
const page = this.getPageHandle(true)
|
||||
return page.$eval('html', command, elementHandle, propertyName)
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* Get the computed WAI-ARIA label of an element.
|
||||
*
|
||||
* @alias browser.getElementComputedLabel
|
||||
* @see https://w3c.github.io/webdriver/#get-computed-label
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @return {string} The result of computing the WAI-ARIA label of element.
|
||||
*/
|
||||
export default async function getElementComputedLabel (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const page = this.getPageHandle(true)
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
const snapshot = await page.accessibility.snapshot({
|
||||
root: elementHandle
|
||||
})
|
||||
|
||||
if (!snapshot) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return snapshot.name
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* Get the computed WAI-ARIA role of an element.
|
||||
*
|
||||
* @alias browser.getElementComputedRole
|
||||
* @see https://w3c.github.io/webdriver/#get-computed-role
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @return {string} The result of computing the WAI-ARIA role of element.
|
||||
*/
|
||||
export default async function getElementComputedRole (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const page = this.getPageHandle(true)
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
const snapshot = await page.accessibility.snapshot({
|
||||
root: elementHandle
|
||||
})
|
||||
|
||||
if (!snapshot) {
|
||||
return 'Ignored'
|
||||
}
|
||||
|
||||
return snapshot.role
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Element Property command will return the result of getting a property of an element.
|
||||
*
|
||||
* @alias browser.getElementProperty
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-element-property
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @param {string} name name of the attribute property to retrieve
|
||||
* @return {string} The named property of the element, accessed by calling GetOwnProperty on the element object.
|
||||
*/
|
||||
export default async function getElementProperty (
|
||||
this: DevToolsDriver,
|
||||
{ elementId, name }: { elementId: string, name: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
const jsHandle = await elementHandle.getProperty(name as any)
|
||||
if (!jsHandle) {
|
||||
return null
|
||||
}
|
||||
|
||||
return jsHandle.jsonValue()
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import command from '../scripts/getElementRect.js'
|
||||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Element Rect command returns the dimensions and coordinates of the given web element.
|
||||
*
|
||||
* @alias browser.getElementRect
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-element-rect
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @return {string} A JSON object representing the position and bounding rect of the element.
|
||||
*/
|
||||
export default async function getElementRect (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
const page = this.getPageHandle(true)
|
||||
return page.$eval('html', command, elementHandle)
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { getStaleElementError } from '../utils.js'
|
||||
import { SHADOW_ELEMENT_KEY } from '../constants.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* Get the shadow root object of an element. The result object can be used to fetch
|
||||
* elements within this shadow root using e.g. `findElementFromShadowRoots` or `findElementsFromShadowRoots`.
|
||||
*
|
||||
* @alias browser.getElementShadowRoot
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-element-rect
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @return {string} A JSON representation of an element shadow root, e.g. `{ 'shadow-6066-11e4-a52e-4f735466cecf': 'ELEMENT_1' }`.
|
||||
*/
|
||||
export default async function getElementShadowRoot (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
/**
|
||||
* given that puppeteer doesn't allow to get an element handle from a shadow root
|
||||
* we just return the given element and use the shadow selector in `findElementFromShadowRoots`
|
||||
* and `findElementsFromShadowRoots` commands
|
||||
*/
|
||||
return { [SHADOW_ELEMENT_KEY]: elementId }
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import command from '../scripts/getElementTagName.js'
|
||||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Element Tag Name command returns the qualified element name of the given web element.
|
||||
*
|
||||
* @alias browser.getElementTagName
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-element-tag-name
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @return {string} The tagName attribute of the element.
|
||||
*/
|
||||
export default async function getElementTagName (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
const page = this.getPageHandle(true)
|
||||
const result = await page.$eval('html', command, elementHandle)
|
||||
return (result || '').toLowerCase()
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import command from '../scripts/getElementText.js'
|
||||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Element Text command intends to return an element’s text \"as rendered\".
|
||||
* An element's rendered text is also used for locating a elements
|
||||
* by their link text and partial link text.
|
||||
*
|
||||
* @alias browser.getElementText
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-element-text
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @return {string} The visible text of the element (including child elements).
|
||||
*/
|
||||
export default async function getElementText (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
const page = this.getPageHandle(true)
|
||||
return page.$eval('html', command, elementHandle)
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Named Cookie command returns the cookie with the requested name from the
|
||||
* associated cookies in the cookie store of the current browsing context's active document.
|
||||
* If no cookie is found, a no such cookie error is returned.
|
||||
*
|
||||
* @alias browser.getNamedCookie
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-named-cookie
|
||||
* @param {string} name name of the cookie to retrieve
|
||||
* @return {object} A serialized cookie, with name and value fields. There are a number of optional fields like `path`, `domain`, and `expiry-time` which may also be present.
|
||||
*/
|
||||
export default async function getNamedCookie (
|
||||
this: DevToolsDriver,
|
||||
{ name }: { name: string }
|
||||
) {
|
||||
const page = this.getPageHandle()
|
||||
const cookies = await page.cookies()
|
||||
const cookie = cookies.find((cookie) => cookie.name === name)
|
||||
|
||||
if (!cookie) {
|
||||
throw new Error(`No cookie with name ${name}`)
|
||||
}
|
||||
|
||||
return cookie
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Page Source command returns a string serialization of the DOM
|
||||
* of the current browsing context active document.
|
||||
*
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-page-source
|
||||
* @alias browser.getPageSource
|
||||
* @return {string} the DOM of the current browsing context active document
|
||||
*/
|
||||
export default function getPageSource (this: DevToolsDriver) {
|
||||
const page = this.getPageHandle(true)
|
||||
return page.content()
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Timeouts command gets timeout durations associated with the current session.
|
||||
*
|
||||
* @alias browser.getTimeouts
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-timeouts
|
||||
* @return {Object} Object containing timeout durations for `script`, `pageLoad` and `implicit` timeouts.
|
||||
*/
|
||||
export default function getTimeouts (this: DevToolsDriver) {
|
||||
return {
|
||||
implicit: this.timeouts.get('implicit'),
|
||||
pageLoad: this.timeouts.get('pageLoad'),
|
||||
script: this.timeouts.get('script')
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Title command returns the document title of the current top-level
|
||||
* browsing context, equivalent to calling `document.title`.
|
||||
*
|
||||
* @alias browser.getTitle
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-title
|
||||
* @return {string} Returns a string which is the same as `document.title` of the current top-level browsing context.
|
||||
*/
|
||||
export default async function getTitle (this: DevToolsDriver) {
|
||||
const page = this.getPageHandle(true)
|
||||
return page.title()
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import command from '../scripts/getUrl.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Current URL command returns the URL of the current top-level browsing context
|
||||
*
|
||||
* @alias browser.getUrl
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-current-url
|
||||
* @returns {String} current document URL of the top-level browsing context.
|
||||
*/
|
||||
export default async function getUrl (this: DevToolsDriver) {
|
||||
const page = this.getPageHandle(true)
|
||||
return page.$eval('html', command)
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Window Handle command returns the window handle for the current top-level browsing context.
|
||||
* It can be used as an argument to Switch To Window.
|
||||
*
|
||||
* @alias browser.getWindowHandle
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-window-handle
|
||||
* @return {string} Returns a string which is the window handle for the current top-level browsing context.
|
||||
*/
|
||||
export default async function getWindowHandle (this: DevToolsDriver) {
|
||||
return this.currentWindowHandle
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Window Handles command returns a list of window handles
|
||||
* for every open top-level browsing context.
|
||||
* The order in which the window handles are returned is arbitrary.
|
||||
*
|
||||
* @alias browser.getWindowHandles
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-window-handles
|
||||
* @return {string[]} An array which is a list of window handles.
|
||||
*/
|
||||
export default async function getWindowHandles(this: DevToolsDriver) {
|
||||
return Array.from(this.windows.keys())
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { DEFAULT_WIDTH, DEFAULT_HEIGHT } from '../constants.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Get Window Rect command returns the size and position on the screen of the operating system window
|
||||
* corresponding to the current top-level browsing context.
|
||||
*
|
||||
* @alias browser.getWindowRect
|
||||
* @see https://w3c.github.io/webdriver/#dfn-get-window-rect
|
||||
* @return {object} A JSON representation of a "window rect" object. This has 4 properties: `x`, `y`, `width` and `height`.
|
||||
*/
|
||||
export default async function getWindowRect (this: DevToolsDriver) {
|
||||
const page = this.getPageHandle()
|
||||
const viewport = await page.viewport() || {}
|
||||
return Object.assign(
|
||||
{ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, x: 0, y: 0 },
|
||||
viewport
|
||||
)
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import acceptAlert from './acceptAlert.js'
|
||||
import addCookie from './addCookie.js'
|
||||
import back from './back.js'
|
||||
import closeWindow from './closeWindow.js'
|
||||
import createWindow from './createWindow.js'
|
||||
import deleteAllCookies from './deleteAllCookies.js'
|
||||
import deleteCookie from './deleteCookie.js'
|
||||
import deleteSession from './deleteSession.js'
|
||||
import dismissAlert from './dismissAlert.js'
|
||||
import elementClear from './elementClear.js'
|
||||
import elementClick from './elementClick.js'
|
||||
import elementSendKeys from './elementSendKeys.js'
|
||||
import executeAsyncScript from './executeAsyncScript.js'
|
||||
import executeScript from './executeScript.js'
|
||||
import findElementFromElement from './findElementFromElement.js'
|
||||
import findElementFromShadowRoot from './findElementFromShadowRoot.js'
|
||||
import findElementsFromElement from './findElementsFromElement.js'
|
||||
import findElementsFromShadowRoot from './findElementsFromShadowRoot.js'
|
||||
import findElements from './findElements.js'
|
||||
import findElement from './findElement.js'
|
||||
import forward from './forward.js'
|
||||
import getActiveElement from './getActiveElement.js'
|
||||
import getAlertText from './getAlertText.js'
|
||||
import getAllCookies from './getAllCookies.js'
|
||||
import getElementAttribute from './getElementAttribute.js'
|
||||
import getElementComputedLabel from './getElementComputedLabel.js'
|
||||
import getElementComputedRole from './getElementComputedRole.js'
|
||||
import getElementCSSValue from './getElementCSSValue.js'
|
||||
import getElementProperty from './getElementProperty.js'
|
||||
import getElementRect from './getElementRect.js'
|
||||
import getElementShadowRoot from './getElementShadowRoot.js'
|
||||
import getElementTagName from './getElementTagName.js'
|
||||
import getElementText from './getElementText.js'
|
||||
import getNamedCookie from './getNamedCookie.js'
|
||||
import getPageSource from './getPageSource.js'
|
||||
import getTimeouts from './getTimeouts.js'
|
||||
import getTitle from './getTitle.js'
|
||||
import getUrl from './getUrl.js'
|
||||
import getWindowHandles from './getWindowHandles.js'
|
||||
import getWindowHandle from './getWindowHandle.js'
|
||||
import getWindowRect from './getWindowRect.js'
|
||||
import isElementEnabled from './isElementEnabled.js'
|
||||
import isElementSelected from './isElementSelected.js'
|
||||
import navigateTo from './navigateTo.js'
|
||||
import newSession from './newSession.js'
|
||||
import performActions from './performActions.js'
|
||||
import refresh from './refresh.js'
|
||||
import releaseActions from './releaseActions.js'
|
||||
import sendAlertText from './sendAlertText.js'
|
||||
import setTimeouts from './setTimeouts.js'
|
||||
import setWindowRect from './setWindowRect.js'
|
||||
import status from './status.js'
|
||||
import switchToFrame from './switchToFrame.js'
|
||||
import switchToParentFrame from './switchToParentFrame.js'
|
||||
import switchToWindow from './switchToWindow.js'
|
||||
import takeElementScreenshot from './takeElementScreenshot.js'
|
||||
import takeScreenshot from './takeScreenshot.js'
|
||||
|
||||
export {
|
||||
acceptAlert, addCookie, back, closeWindow, createWindow, deleteAllCookies, deleteCookie, deleteSession,
|
||||
dismissAlert, elementClear, elementClick, elementSendKeys, executeAsyncScript, executeScript,
|
||||
findElementFromElement, findElementFromShadowRoot, findElementsFromElement, findElementsFromShadowRoot,
|
||||
findElements, findElement, forward, getActiveElement, getAlertText, getAllCookies, getElementAttribute,
|
||||
getElementComputedLabel, getElementComputedRole, getElementCSSValue, getElementProperty, getElementRect,
|
||||
getElementShadowRoot, getElementTagName, getElementText, getNamedCookie, getPageSource, getTimeouts,
|
||||
getTitle, getUrl, getWindowHandles, getWindowHandle, getWindowRect, isElementEnabled, isElementSelected,
|
||||
navigateTo, newSession, performActions, refresh, releaseActions, sendAlertText, setTimeouts, setWindowRect,
|
||||
status, switchToFrame, switchToParentFrame, switchToWindow, takeElementScreenshot, takeScreenshot
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import getElementAttribute from './getElementAttribute.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* Is Element Enabled determines if the referenced element is enabled or not.
|
||||
* This operation only makes sense on form controls.
|
||||
*
|
||||
* @alias browser.isElementEnabled
|
||||
* @see https://w3c.github.io/webdriver/#dfn-is-element-enabled
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @return {string} If the element is in an xml document, or is a disabled form control: `false`, otherwise, `true`.
|
||||
*/
|
||||
export default async function isElementEnabled (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const result = await getElementAttribute.call(this, { elementId, name: 'disabled' })
|
||||
return result === null
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import getElementProperty from './getElementProperty.js'
|
||||
import getElementTagName from './getElementTagName.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* Is Element Selected determines if the referenced element is selected or not.
|
||||
* This operation only makes sense on input elements of the Checkbox- and Radio Button states,
|
||||
* or option elements.
|
||||
*
|
||||
* @param browser.isElementSelected
|
||||
* @see https://w3c.github.io/webdriver/#dfn-is-element-selected
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @return {boolean} `true` or `false` based on the selected state.
|
||||
*/
|
||||
export default async function isElementSelected (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const tagName = await getElementTagName.call(this, { elementId })
|
||||
const name = tagName === 'option' ? 'selected' : 'checked'
|
||||
const isSelected = await getElementProperty.call(this, { elementId, name })
|
||||
return Boolean(isSelected)
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The navigateTo (go) command is used to cause the user agent to navigate the current
|
||||
* top-level browsing context a new location.
|
||||
*
|
||||
* @alias browser.navigateTo
|
||||
* @see https://w3c.github.io/webdriver/#dfn-navigate-to
|
||||
* @param {string} url current top-level browsing context’s active document’s document URL
|
||||
* @return {string} current document URL of the top-level browsing context.
|
||||
*/
|
||||
export default async function navigateTo (
|
||||
this: DevToolsDriver,
|
||||
{ url }: { url: string }
|
||||
) {
|
||||
/**
|
||||
* when navigating to a new url get out of frame scope
|
||||
*/
|
||||
delete this.currentFrame
|
||||
|
||||
const page = this.getPageHandle()
|
||||
await page.goto(url)
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import os from 'node:os'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import launch from '../launcher.js'
|
||||
import { sessionMap } from '../index.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The New Session command creates a new WebDriver session with the endpoint node.
|
||||
* If the creation fails, a session not created error is returned.
|
||||
*
|
||||
* @alias browser.newSession
|
||||
* @see https://w3c.github.io/webdriver/#dfn-new-sessions
|
||||
* @param {Object} capabilities An object describing the set of capabilities for the capability processing algorithm
|
||||
* @return {Object} Object containing sessionId and capabilities of created WebDriver session.
|
||||
*/
|
||||
export default async function newSession (
|
||||
this: DevToolsDriver,
|
||||
{ capabilities }: { capabilities: WebdriverIO.Capabilities }
|
||||
) {
|
||||
const browser = await launch(capabilities)
|
||||
const sessionId = uuidv4()
|
||||
const [browserName, browserVersion] = (await browser.version()).split('/')
|
||||
|
||||
sessionMap.set(sessionId, browser)
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
capabilities: {
|
||||
browserName,
|
||||
browserVersion,
|
||||
platformName: os.platform(),
|
||||
platformVersion: os.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
import type { KeyInput } from 'puppeteer-core/lib/esm/puppeteer/common/USKeyboardLayout.js'
|
||||
import { _keyDefinitions } from 'puppeteer-core/lib/esm/puppeteer/common/USKeyboardLayout.js'
|
||||
import type { Keyboard, Mouse } from 'puppeteer-core/lib/esm/puppeteer/api/Input.js'
|
||||
|
||||
import getElementRect from './getElementRect.js'
|
||||
import getWindowRect from './getWindowRect.js'
|
||||
import { ELEMENT_KEY, UNICODE_CHARACTERS } from '../constants.js'
|
||||
import { sleep } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
const KEY = 'key'
|
||||
const POINTER = 'pointer'
|
||||
|
||||
interface Action {
|
||||
duration?: number
|
||||
type: string
|
||||
value?: string
|
||||
x?: number
|
||||
y?: number
|
||||
deltaX?: number
|
||||
deltaY?: number
|
||||
button?: number
|
||||
origin?: any
|
||||
}
|
||||
|
||||
interface ActionsParameter {
|
||||
type?: string
|
||||
actions: Action[]
|
||||
parameters?: {
|
||||
pointerType?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Perform Actions command is used to execute complex user actions.
|
||||
* See [spec](https://github.com/jlipps/simple-wd-spec#perform-actions) for more details.
|
||||
*
|
||||
* @alias browser.performActions
|
||||
* @see https://w3c.github.io/webdriver/#dfn-perform-actions
|
||||
* @param {object[]} actions A list of objects, each of which represents an input source and its associated actions.
|
||||
*/
|
||||
export default async function performActions(
|
||||
this: DevToolsDriver,
|
||||
{ actions }: { actions: ActionsParameter[] }
|
||||
) {
|
||||
const page = this.getPageHandle()
|
||||
const lastPointer: {
|
||||
x?: number,
|
||||
y?: number
|
||||
} = {}
|
||||
|
||||
/**
|
||||
* see https://github.com/jlipps/simple-wd-spec#input-sources-and-corresponding-actions
|
||||
* for details on the `actions` format
|
||||
*/
|
||||
for (const action of actions) {
|
||||
if (action.type === null || action.type === 'null') {
|
||||
for (const singleAction of action.actions) {
|
||||
await sleep(singleAction.duration)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (action.type === 'key') {
|
||||
const skipChars = []
|
||||
for (const singleAction of action.actions) {
|
||||
if (singleAction.type === 'pause') {
|
||||
await sleep(singleAction.duration)
|
||||
continue
|
||||
}
|
||||
|
||||
const cmd = singleAction.type.slice(KEY.length).toLowerCase() as keyof Keyboard
|
||||
const keyboardFn = (page.keyboard[cmd] as Function).bind(page.keyboard)
|
||||
|
||||
/**
|
||||
* skip up event as we had to use sendCharacter for non unicode
|
||||
* characters which includes the up event already
|
||||
*/
|
||||
if (cmd === 'up' && skipChars[0] === singleAction.value) {
|
||||
skipChars.shift()
|
||||
continue
|
||||
}
|
||||
|
||||
/**
|
||||
* for special characters like emojis 😉 we need to
|
||||
* send in the value as text because it is not unicode
|
||||
*/
|
||||
const [key] = (Object.entries(UNICODE_CHARACTERS)
|
||||
.find(([, charValue]) => charValue === singleAction.value) || []) as [KeyInput | undefined, never]
|
||||
const pptrKey = key && _keyDefinitions[key]
|
||||
? key
|
||||
: _keyDefinitions[singleAction.value as unknown as KeyInput]
|
||||
? singleAction.value as KeyInput
|
||||
: undefined
|
||||
if (!pptrKey) {
|
||||
await page.keyboard.sendCharacter(singleAction.value as unknown as KeyInput)
|
||||
skipChars.push(singleAction.value)
|
||||
continue
|
||||
}
|
||||
|
||||
await keyboardFn(pptrKey)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (action.type === 'pointer') {
|
||||
if (action.parameters && action.parameters.pointerType && action.parameters.pointerType !== 'mouse') {
|
||||
throw new Error('Currently only "mouse" is supported as pointer type')
|
||||
}
|
||||
|
||||
/**
|
||||
* detect double click
|
||||
*/
|
||||
if (
|
||||
action.actions.length === 6 &&
|
||||
action.actions[0].type === 'pointerMove' &&
|
||||
action.actions[1].type === 'pointerDown' &&
|
||||
action.actions[2].type === 'pointerUp' &&
|
||||
action.actions[3].type === 'pause' &&
|
||||
action.actions[4].type === 'pointerDown' &&
|
||||
action.actions[5].type === 'pointerUp'
|
||||
) {
|
||||
let x = action.actions[0].x || 0
|
||||
let y = action.actions[0].y || 0
|
||||
if (action.actions[0].origin) {
|
||||
const location = await getElementRect.call(this, { elementId: action.actions[0].origin[ELEMENT_KEY] })
|
||||
x += location.x + (location.width / 2)
|
||||
y += location.y + (location.height / 2)
|
||||
}
|
||||
|
||||
await page.mouse.click(x, y, { clickCount: 2 })
|
||||
continue
|
||||
}
|
||||
|
||||
for (const singleAction of action.actions) {
|
||||
if (singleAction.type === 'pause') {
|
||||
await sleep(singleAction.duration)
|
||||
continue
|
||||
}
|
||||
|
||||
const cmd = singleAction.type.slice(POINTER.length).toLowerCase()
|
||||
const keyboardFn = (page.mouse[cmd as keyof Mouse] as Function).bind(page.mouse)
|
||||
const { duration, button, origin } = singleAction
|
||||
|
||||
let { x, y } = singleAction
|
||||
if (cmd === 'move') {
|
||||
/**
|
||||
* set location relative from last position if origin is set to pointer
|
||||
*/
|
||||
if (
|
||||
typeof x === 'number' &&
|
||||
typeof y === 'number' &&
|
||||
origin === 'pointer' &&
|
||||
lastPointer.x && lastPointer.y
|
||||
) {
|
||||
x += lastPointer.x
|
||||
y += lastPointer.y
|
||||
}
|
||||
|
||||
/**
|
||||
* set location relative from an element
|
||||
*/
|
||||
if (origin && typeof origin[ELEMENT_KEY] === 'string' && typeof x === 'number' && typeof y === 'number') {
|
||||
const elemRect = await getElementRect.call(this, { elementId: origin[ELEMENT_KEY] })
|
||||
x += elemRect.x + (elemRect.width / 2)
|
||||
y += elemRect.y + (elemRect.height / 2)
|
||||
}
|
||||
|
||||
lastPointer.x = x
|
||||
lastPointer.y = y
|
||||
await keyboardFn(x, y, { steps: 10 })
|
||||
continue
|
||||
} else {
|
||||
/**
|
||||
* "left" is default button
|
||||
* "1": middle, "2": right
|
||||
*/
|
||||
const pptrButton = (
|
||||
button === 1 ? 'middle' : (
|
||||
button === 2 ? 'right' : 'left'
|
||||
)
|
||||
)
|
||||
await keyboardFn({ button: pptrButton })
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
await sleep(duration)
|
||||
}
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (action.type === 'wheel') {
|
||||
for (const singleAction of action.actions) {
|
||||
const deltaX = singleAction.deltaX || 0
|
||||
const deltaY = singleAction.deltaY || 0
|
||||
|
||||
if (singleAction.origin) {
|
||||
const windowSize = await getWindowRect.call(this)
|
||||
const location = await getElementRect.call(this, { elementId: singleAction.origin[ELEMENT_KEY] })
|
||||
await page.mouse.wheel({
|
||||
deltaX: location.x + deltaX,
|
||||
deltaY: location.y - windowSize.height + deltaY
|
||||
})
|
||||
} else if (singleAction.x || singleAction.y) {
|
||||
await page.mouse.wheel({
|
||||
deltaX: (singleAction.x || 0) + deltaX,
|
||||
deltaY: (singleAction.y || 0) + deltaY
|
||||
})
|
||||
} else {
|
||||
await page.mouse.wheel({ deltaX, deltaY })
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
throw new Error(`Unknown action type ("${action.type}"), allowed are only: null, key and pointer`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Refresh command causes the browser to reload the page in current top-level browsing context.
|
||||
*
|
||||
* @alias browser.refresh
|
||||
* @see https://w3c.github.io/webdriver/#dfn-refresh
|
||||
*/
|
||||
export default async function refresh (this: DevToolsDriver) {
|
||||
delete this.currentFrame
|
||||
|
||||
const page = this.getPageHandle()
|
||||
await page.reload()
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
/**
|
||||
* The Release Actions command is used to release all the keys and pointer buttons that are currently depressed.
|
||||
* This causes events to be fired as if the state was released by an explicit series of actions.
|
||||
* It also clears all the internal state of the virtual devices.
|
||||
*
|
||||
* There is no reference implementation for it in Puppeteer so this command will be a NOOP.
|
||||
*
|
||||
* @alias browser.performActions
|
||||
* @see https://w3c.github.io/webdriver/#dfn-release-actions
|
||||
*/
|
||||
export default async function performActions () {}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Send Alert Text command sets the text field of a window.prompt user prompt to the given value.
|
||||
*
|
||||
* @alias browser.sendAlertText
|
||||
* @see https://w3c.github.io/webdriver/#dfn-send-alert-text
|
||||
* @param {string} text string to set the prompt to
|
||||
*/
|
||||
export default async function sendAlertText (
|
||||
this: DevToolsDriver,
|
||||
{ text }: { text: string }) {
|
||||
if (!this.activeDialog) {
|
||||
throw new Error('no such alert')
|
||||
}
|
||||
|
||||
await this.activeDialog.accept(text)
|
||||
delete this.activeDialog
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Set Timeouts command sets timeout durations associated with the current session.
|
||||
* The timeouts that can be controlled are listed in the table of session timeouts below.
|
||||
*
|
||||
* @alias browser.setTimeouts
|
||||
* @see https://w3c.github.io/webdriver/#dfn-set-timeouts
|
||||
* @param {number} implicit integer in ms for session implicit wait timeout
|
||||
* @param {number} pageLoad integer in ms for session page load timeout
|
||||
* @param {number} script integer in ms for session script timeout
|
||||
*/
|
||||
export default async function setTimeouts (
|
||||
this: DevToolsDriver,
|
||||
{ implicit, pageLoad, script }: { implicit: number, pageLoad: number, script: number }
|
||||
) {
|
||||
await this.setTimeouts(implicit, pageLoad, script)
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Set Window Rect command alters the size and the position of the operating system window
|
||||
* corresponding to the current top-level browsing context.
|
||||
*
|
||||
* @alias browser.setWindowRect
|
||||
* @see https://w3c.github.io/webdriver/#dfn-set-window-rect
|
||||
* @param {number} x the screenX attribute of the window object
|
||||
* @param {number} y the screenY attribute of the window object
|
||||
* @param {number} width the width of the outer dimensions of the top-level browsing context, including browser chrome etc...
|
||||
* @param {number} height the height of the outer dimensions of the top-level browsing context, including browser chrome etc...
|
||||
* @return {object} A JSON representation of a "window rect" object based on the new window state.
|
||||
*/
|
||||
export default async function setWindowRect (
|
||||
this: DevToolsDriver,
|
||||
params: { width: number, height: number }
|
||||
) {
|
||||
const page = this.getPageHandle()
|
||||
await page.setViewport(params)
|
||||
return { width: params.width, height: params.height }
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import url from 'node:url'
|
||||
import path from 'node:path'
|
||||
import { resolve } from 'import-meta-resolve'
|
||||
|
||||
let puppeteerVersion: string
|
||||
|
||||
/**
|
||||
* The Status command returns information about whether a remote end is in a state
|
||||
* in which it can create new sessions and can additionally include arbitrary meta information
|
||||
* that is specific to the implementation.
|
||||
*
|
||||
* @alias browser.status
|
||||
* @see https://w3c.github.io/webdriver/#dfn-status
|
||||
* @return {Object} returning an object with the Puppeteer version being used
|
||||
*/
|
||||
export default async function status () {
|
||||
if (!puppeteerVersion) {
|
||||
const puppeteerPath = await resolve('puppeteer-core', import.meta.url)
|
||||
try {
|
||||
const pkgJsonPath = path.resolve(url.fileURLToPath(puppeteerPath), '..', '..', '..', '..', 'package.json')
|
||||
const pkgJson = JSON.parse((await fs.readFile(pkgJsonPath, 'utf-8')).toString())
|
||||
puppeteerVersion = pkgJson.version
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: '',
|
||||
ready: true,
|
||||
puppeteerVersion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import type { Page } from 'puppeteer-core/lib/esm/puppeteer/api/Page.js'
|
||||
import type { Frame } from 'puppeteer-core/lib/esm/puppeteer/api/Frame.js'
|
||||
import type { ElementReference } from '@wdio/protocols'
|
||||
|
||||
import { ELEMENT_KEY } from '../constants.js'
|
||||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
type FrameIdParameter = { id: null | number | ElementReference }
|
||||
|
||||
/**
|
||||
* The Switch To Frame command is used to select the current top-level browsing context
|
||||
* or a child browsing context of the current browsing context to use as the current
|
||||
* browsing context for subsequent commands.
|
||||
*
|
||||
* @alias browser.switchToFrame
|
||||
* @see https://w3c.github.io/webdriver/#dfn-switch-to-frame
|
||||
* @param {string|object|null} id one of three possible types: null: this represents the top-level browsing context (i.e., not an iframe), a Number, representing the index of the window object corresponding to a frame, an Element object received using `findElement`.
|
||||
*/
|
||||
export default async function switchToFrame (
|
||||
this: DevToolsDriver,
|
||||
{ id }: FrameIdParameter
|
||||
) {
|
||||
const page = this.getPageHandle(true) as unknown as Frame
|
||||
|
||||
/**
|
||||
* switch to parent frame
|
||||
*/
|
||||
if (id === null) {
|
||||
/**
|
||||
* if we are already in the parent frame, don't do anything
|
||||
*/
|
||||
if (typeof page.parentFrame !== 'function') {
|
||||
return { id: null }
|
||||
}
|
||||
|
||||
let parentFrame = await page.parentFrame()
|
||||
while (parentFrame) {
|
||||
parentFrame = await parentFrame.parentFrame()
|
||||
}
|
||||
this.currentFrame = parentFrame as unknown as Page
|
||||
return { id: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* switch frame by element ID
|
||||
*/
|
||||
if (typeof id === 'object' && typeof (id as ElementReference)[ELEMENT_KEY] === 'string') {
|
||||
const elementHandle = await this.elementStore.get(id[ELEMENT_KEY])
|
||||
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(id[ELEMENT_KEY])
|
||||
}
|
||||
|
||||
const contentFrame = await elementHandle.contentFrame()
|
||||
|
||||
if (!contentFrame) {
|
||||
throw new Error('no such frame')
|
||||
}
|
||||
|
||||
this.currentFrame = contentFrame as unknown as Page
|
||||
return { id: id[ELEMENT_KEY] }
|
||||
}
|
||||
|
||||
/**
|
||||
* switch frame by number
|
||||
*/
|
||||
if (typeof id === 'number') {
|
||||
/**
|
||||
* `page` has `frames` method while `frame` has `childFrames` method
|
||||
*/
|
||||
const getFrames = (page as unknown as Page).frames || page.childFrames
|
||||
const childFrames = await getFrames.apply(page)
|
||||
const childFrame = childFrames[id]
|
||||
|
||||
if (!childFrame) {
|
||||
throw new Error('no such frame')
|
||||
}
|
||||
|
||||
this.currentFrame = childFrame as unknown as Page
|
||||
return { id: childFrame._id }
|
||||
}
|
||||
|
||||
throw new Error(`Could not switch frame, unknown id: ${id}`)
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import type { Page } from 'puppeteer-core/lib/esm/puppeteer/api/Page.js'
|
||||
import type { Frame } from 'puppeteer-core/lib/esm/puppeteer/api/Frame.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Switch to Parent Frame command sets the current browsing context for future commands
|
||||
* to the parent of the current browsing context.
|
||||
*
|
||||
* @alias browser.switchToParentFrame
|
||||
* @see https://w3c.github.io/webdriver/#dfn-switch-to-parent-frame
|
||||
*/
|
||||
export default async function switchToParentFrame (this: DevToolsDriver) {
|
||||
const page = this.getPageHandle(true) as unknown as Frame
|
||||
|
||||
/**
|
||||
* check if we can access child frames, if now we are already in the
|
||||
* parent browsing context
|
||||
*/
|
||||
if (typeof page.parentFrame !== 'function') {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* ToDo(Christian): investigate why we interchangeably use Page and Frames here
|
||||
*/
|
||||
this.currentFrame = await page.parentFrame() as unknown as Page
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Switch To Window command is used to select the current top-level browsing context
|
||||
* for the current session, i.e. the one that will be used for processing commands.
|
||||
*
|
||||
* @alias browser.switchToWindow
|
||||
* @see https://w3c.github.io/webdriver/#dfn-switch-to-window
|
||||
* @param {string} handle representing a window handle, should be one of the strings that was returned in a call to getWindowHandles
|
||||
*/
|
||||
export default async function switchToWindow (
|
||||
this: DevToolsDriver,
|
||||
{ handle }: { handle: string }
|
||||
) {
|
||||
if (!this.windows.has(handle)) {
|
||||
throw new Error(`window with handle ${handle} not found`)
|
||||
}
|
||||
|
||||
delete this.currentFrame
|
||||
this.currentWindowHandle = handle
|
||||
const page = this.getPageHandle()
|
||||
page.on('dialog', this.dialogHandler.bind(this))
|
||||
await page.bringToFront()
|
||||
|
||||
return handle
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { getStaleElementError } from '../utils.js'
|
||||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Take Element Screenshot command takes a screenshot of the visible region
|
||||
* encompassed by the bounding rectangle of an element.
|
||||
*
|
||||
* @alias browser.takeElementScreenshot
|
||||
* @see https://w3c.github.io/webdriver/#dfn-take-element-screenshot
|
||||
* @param {string} elementId the id of an element returned in a previous call to Find Element(s)
|
||||
* @return {string} The base64-encoded PNG image data comprising the screenshot of the visible region of an element’s bounding rectangle after it has been scrolled into view.
|
||||
*/
|
||||
export default async function takeElementScreenshot (
|
||||
this: DevToolsDriver,
|
||||
{ elementId }: { elementId: string }
|
||||
) {
|
||||
const elementHandle = await this.elementStore.get(elementId)
|
||||
|
||||
if (!elementHandle) {
|
||||
throw getStaleElementError(elementId)
|
||||
}
|
||||
|
||||
return elementHandle.screenshot({
|
||||
captureBeyondViewport: false,
|
||||
encoding: 'base64',
|
||||
type: 'png',
|
||||
})
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import type DevToolsDriver from '../devtoolsdriver.js'
|
||||
|
||||
/**
|
||||
* The Take Screenshot command takes a screenshot of the top-level browsing context's viewport.
|
||||
*
|
||||
* @alias browser.takeScreenshot
|
||||
* @see https://w3c.github.io/webdriver/#dfn-take-screenshot
|
||||
* @return {string} The base64-encoded PNG image data comprising the screenshot of the initial viewport.
|
||||
*/
|
||||
export default async function takeScreenshot (this: DevToolsDriver) {
|
||||
const page = this.getPageHandle()
|
||||
return page.screenshot({
|
||||
captureBeyondViewport: false,
|
||||
encoding: 'base64',
|
||||
fullPage: false, // limit to viewport
|
||||
type: 'png'
|
||||
})
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
import type { Options } from '@wdio/types'
|
||||
|
||||
export const DEFAULT_WIDTH = 1200
|
||||
export const DEFAULT_HEIGHT = 900
|
||||
export const DEFAULT_X_POSITION = 0
|
||||
export const DEFAULT_Y_POSITION = 0
|
||||
|
||||
export const ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf'
|
||||
export const SHADOW_ELEMENT_KEY = 'shadow-6066-11e4-a52e-4f735466cecf'
|
||||
|
||||
// https://github.com/puppeteer/puppeteer/blob/083ea41e943e2a20014279fcfceb23c98a1e4491/src/node/Launcher.ts#L168
|
||||
export const DEFAULT_FLAGS = [
|
||||
// suppresses Save Password prompt window
|
||||
'--enable-automation',
|
||||
// do not block popups
|
||||
'--disable-popup-blocking',
|
||||
// Disable all chrome extensions entirely
|
||||
'--disable-extensions',
|
||||
// Disable various background network services, including extension updating,
|
||||
// safe browsing service, upgrade detector, translate, UMA
|
||||
'--disable-background-networking',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
// Disable syncing to a Google account
|
||||
'--disable-sync',
|
||||
// Disable reporting to UMA, but allows for collection
|
||||
'--metrics-recording-only',
|
||||
// Disable installation of default apps on first run
|
||||
'--disable-default-apps',
|
||||
// Mute any audio
|
||||
'--mute-audio',
|
||||
// Skip first run wizards
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
// chromedriver flags
|
||||
'--disable-hang-monitor',
|
||||
'--disable-prompt-on-repost',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
'--disable-breakpad',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-ipc-flooding-protection',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--force-fieldtrials=*BackgroundTracing/default/',
|
||||
'--enable-features=NetworkService,NetworkServiceInProcess',
|
||||
/**
|
||||
* `site-per-process` affects `page.frames()`, see #4471
|
||||
* `TranslateUI` disables built-in Google Translate service
|
||||
*/
|
||||
'--disable-features=site-per-process,TranslateUI,BlinkGenPropertyTrees'
|
||||
]
|
||||
|
||||
export const CHROME_NAMES = ['chrome', 'googlechrome', 'headlesschrome', 'google-chrome', 'chromium']
|
||||
export const FIREFOX_NAMES = ['firefox', 'ff', 'mozilla', 'mozillafirefox', 'headless firefox', 'headlessfirefox']
|
||||
export const EDGE_NAMES = ['edge', 'msedge', 'microsoft-edge', 'microsoftedge']
|
||||
export const SUPPORTED_BROWSER = [...CHROME_NAMES, ...FIREFOX_NAMES, ...EDGE_NAMES]
|
||||
|
||||
export const BROWSER_TYPE: {
|
||||
chrome: 'chrome',
|
||||
firefox: 'firefox',
|
||||
edge: 'edge'
|
||||
} = {
|
||||
chrome: 'chrome',
|
||||
firefox: 'firefox',
|
||||
edge: 'edge'
|
||||
}
|
||||
|
||||
export const DEFAULTS: Options.Definition<Options.WebDriver> = {
|
||||
capabilities: {
|
||||
type: 'object',
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* Level of logging verbosity
|
||||
*/
|
||||
logLevel: {
|
||||
type: 'string',
|
||||
default: 'info',
|
||||
match: /(trace|debug|info|warn|error|silent)/
|
||||
},
|
||||
/**
|
||||
* directory for log files
|
||||
*/
|
||||
outputDir: {
|
||||
type: 'string'
|
||||
},
|
||||
/**
|
||||
* maxConnectionRetries in chrome-launcher
|
||||
*/
|
||||
connectionRetryCount: {
|
||||
type: 'number',
|
||||
default: 50
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_IMPLICIT_TIMEOUT = 0
|
||||
export const DEFAULT_PAGELOAD_TIMEOUT = 5 * 60 * 1000 // 5 min
|
||||
export const DEFAULT_SCRIPT_TIMEOUT = 30 * 1000 // 30s
|
||||
|
||||
export const SUPPORTED_SELECTOR_STRATEGIES = ['css selector', 'tag name', 'xpath', 'link text', 'partial link text', 'shadow']
|
||||
export const SERIALIZE_PROPERTY = 'data-devtoolsdriver-fetchedElement'
|
||||
export const SERIALIZE_FLAG = '__executeElement'
|
||||
|
||||
export const PPTR_LOG_PREFIX = 'puppeteer:protocol'
|
||||
|
||||
export const ERROR_MESSAGES = {
|
||||
staleElement: {
|
||||
name: 'stale element reference',
|
||||
message: 'stale element reference: The element reference is stale; either the element is no longer attached to the DOM, it is not in the current frame context, or the document has been refreshed'
|
||||
}
|
||||
}
|
||||
|
||||
export const VENDOR_PREFIX: {
|
||||
chrome: 'goog:chromeOptions',
|
||||
'chrome headless': 'goog:chromeOptions',
|
||||
firefox: 'moz:firefoxOptions',
|
||||
edge: 'ms:edgeOptions'
|
||||
} = {
|
||||
chrome: 'goog:chromeOptions',
|
||||
'chrome headless': 'goog:chromeOptions',
|
||||
firefox: 'moz:firefoxOptions',
|
||||
edge: 'ms:edgeOptions'
|
||||
}
|
||||
|
||||
/**
|
||||
* unicode chars mapped to Puppeteer key map
|
||||
*/
|
||||
export const UNICODE_CHARACTERS = {
|
||||
'Cancel': '\uE001',
|
||||
'Help': '\uE002',
|
||||
'Backspace': '\uE003',
|
||||
'Tab': '\uE004',
|
||||
'Clear': '\uE005',
|
||||
'Enter': '\uE006',
|
||||
'NumpadEnter': '\uE007',
|
||||
'ShiftLeft': '\uE008',
|
||||
'ControlLeft': '\uE009',
|
||||
'ControlRight': '\uE051',
|
||||
'AltLeft': '\uE00A',
|
||||
'Pause': '\uE00B',
|
||||
'Escape': '\uE00C',
|
||||
'Space': '\uE00D',
|
||||
'PageUp': '\uE00E',
|
||||
'PageDown': '\uE00F',
|
||||
'End': '\uE010',
|
||||
'Home': '\uE011',
|
||||
'ArrowLeft': '\uE012',
|
||||
'ArrowUp': '\uE013',
|
||||
'ArrowRight': '\uE014',
|
||||
'ArrowDown': '\uE015',
|
||||
'Insert': '\uE016',
|
||||
'Delete': '\uE017',
|
||||
'Semicolon': '\uE018',
|
||||
'=': '\uE019',
|
||||
'Numpad0': '\uE01A',
|
||||
'Numpad1': '\uE01B',
|
||||
'Numpad2': '\uE01C',
|
||||
'Numpad3': '\uE01D',
|
||||
'Numpad4': '\uE01E',
|
||||
'Numpad5': '\uE01F',
|
||||
'Numpad6': '\uE020',
|
||||
'Numpad7': '\uE021',
|
||||
'Numpad8': '\uE022',
|
||||
'Numpad9': '\uE023',
|
||||
'NumpadMultiply': '\uE024',
|
||||
'NumpadAdd': '\uE025',
|
||||
'Separator': '\uE026',
|
||||
'NumpadSubtract': '\uE027',
|
||||
'NumpadDecimal': '\uE028',
|
||||
'NumpadDivide': '\uE029',
|
||||
'F1': '\uE031',
|
||||
'F2': '\uE032',
|
||||
'F3': '\uE033',
|
||||
'F4': '\uE034',
|
||||
'F5': '\uE035',
|
||||
'F6': '\uE036',
|
||||
'F7': '\uE037',
|
||||
'F8': '\uE038',
|
||||
'F9': '\uE039',
|
||||
'F10': '\uE03A',
|
||||
'F11': '\uE03B',
|
||||
'F12': '\uE03C',
|
||||
'MetaLeft': '\uE03D'
|
||||
}
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
import fs from 'node:fs'
|
||||
import url from 'node:url'
|
||||
import path from 'node:path'
|
||||
import type { EventEmitter } from 'node:events'
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import logger from '@wdio/logger'
|
||||
import type { CommandEndpoint } from '@wdio/protocols'
|
||||
|
||||
import type { Browser } from 'puppeteer-core/lib/esm/puppeteer/api/Browser.js'
|
||||
import type { Dialog } from 'puppeteer-core/lib/esm/puppeteer/api/Dialog.js'
|
||||
import type { Page } from 'puppeteer-core/lib/esm/puppeteer/api/Page.js'
|
||||
import type { Target } from 'puppeteer-core/lib/esm/puppeteer/api/Target.js'
|
||||
import type { Frame } from 'puppeteer-core/lib/esm/puppeteer/api/Frame.js'
|
||||
|
||||
import * as commands from './commands/index.js'
|
||||
import ElementStore from './elementstore.js'
|
||||
import { validate, sanitizeError } from './utils.js'
|
||||
import { DEFAULT_IMPLICIT_TIMEOUT, DEFAULT_PAGELOAD_TIMEOUT, DEFAULT_SCRIPT_TIMEOUT } from './constants.js'
|
||||
import type { ActiveListener } from './types.js'
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
|
||||
const log = logger('devtools')
|
||||
|
||||
export default class DevToolsDriver {
|
||||
commands: Record<string, Function> = {}
|
||||
elementStore = new ElementStore()
|
||||
windows: Map<string, Page> = new Map()
|
||||
timeouts: Map<string, number> = new Map()
|
||||
activeDialog?: Dialog = undefined
|
||||
|
||||
browser: Browser
|
||||
currentFrame?: Page
|
||||
currentWindowHandle?: string
|
||||
currentFrameUrl?: string
|
||||
activeListeners: ActiveListener[] = []
|
||||
constructor(browser: Browser, pages: Page[]) {
|
||||
this.browser = browser
|
||||
|
||||
const dir = path.resolve(__dirname, 'commands')
|
||||
const files = fs.readdirSync(dir).filter(
|
||||
(file) => (
|
||||
file.endsWith('.js') ||
|
||||
(
|
||||
file.endsWith('.ts') &&
|
||||
!file.endsWith('.d.ts')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for (const filename of files) {
|
||||
const commandName = path.basename(filename, path.extname(filename))
|
||||
|
||||
if (!commandName) {
|
||||
throw new Error('Couldn\'t determine command name')
|
||||
}
|
||||
|
||||
this.commands[commandName] = commands[commandName as keyof typeof commands]
|
||||
}
|
||||
|
||||
this.initBrowser(browser, pages)
|
||||
}
|
||||
|
||||
private _createWindowHandle (page: Page) {
|
||||
const pageId = uuidv4()
|
||||
this.windows.set(pageId, page)
|
||||
this.currentFrame = page
|
||||
this.currentWindowHandle = pageId
|
||||
}
|
||||
|
||||
private async _targetCreatedHandler (target: Target) {
|
||||
const page = await target.page()
|
||||
|
||||
if (!page) {
|
||||
return
|
||||
}
|
||||
|
||||
this._createWindowHandle(page)
|
||||
}
|
||||
|
||||
private async _targetDestroyedHandler (target: Target) {
|
||||
const page = await target.page()
|
||||
for (const [pageId, p] of this.windows.entries()) {
|
||||
if (page !== p) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.trace(`Target destroyed, removing window handle ${pageId}`)
|
||||
this.windows.delete(pageId)
|
||||
break
|
||||
}
|
||||
|
||||
const pageIds = [...this.windows.keys()]
|
||||
this.currentFrame = this.windows.get(pageIds[0])
|
||||
this.currentWindowHandle = pageIds[0]
|
||||
log.trace(`Switching to window handle with id ${pageIds[0]}`)
|
||||
}
|
||||
|
||||
private addListener(emitter: ActiveListener['emitter'], eventName: string, handler: any) {
|
||||
const boundHandler = handler.bind(this)
|
||||
emitter.on(eventName, boundHandler)
|
||||
this.activeListeners.push({ emitter, eventName, boundHandler })
|
||||
}
|
||||
|
||||
private cleanupListeners() {
|
||||
this.activeListeners.forEach(({ emitter, eventName, boundHandler }) => {
|
||||
emitter.off(eventName, boundHandler)
|
||||
})
|
||||
this.activeListeners = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Inits browser listeners and sets initial handlers for given pages.
|
||||
* Function is also intended to be used while reloading DevTools session.
|
||||
* @param browser Puppeteer Browser
|
||||
* @param pages Puppeteer page array
|
||||
*/
|
||||
initBrowser(browser: Browser, pages: Page[]) {
|
||||
this.cleanupListeners()
|
||||
this.elementStore = new ElementStore()
|
||||
this.windows = new Map()
|
||||
this.activeDialog = undefined
|
||||
this.browser = browser
|
||||
|
||||
this.addListener(this.browser, 'targetcreated', this._targetCreatedHandler)
|
||||
this.addListener(this.browser, 'targetdestroyed', this._targetDestroyedHandler)
|
||||
|
||||
for (const page of pages) {
|
||||
this._createWindowHandle(page)
|
||||
}
|
||||
|
||||
/**
|
||||
* set default timeouts
|
||||
*/
|
||||
this.setTimeouts(DEFAULT_IMPLICIT_TIMEOUT, DEFAULT_PAGELOAD_TIMEOUT, DEFAULT_SCRIPT_TIMEOUT)
|
||||
|
||||
const page = this.getPageHandle()
|
||||
if (page) {
|
||||
this.addListener(page, 'dialog', this.dialogHandler)
|
||||
this.addListener(page, 'framenavigated', this.framenavigatedHandler)
|
||||
}
|
||||
}
|
||||
|
||||
register(commandInfo: CommandEndpoint) {
|
||||
const self = this
|
||||
const { command, ref, parameters, variables = [] } = commandInfo
|
||||
|
||||
/**
|
||||
* check if command is implemented
|
||||
*/
|
||||
if (typeof this.commands[command] !== 'function') {
|
||||
return () => {
|
||||
throw new Error(
|
||||
`Command "${command}" is not supported using the Devtools protocol. ` +
|
||||
'You might want to use WebDriver as automation engine. For more ' +
|
||||
'information check out https://webdriver.io/docs/automationProtocols!'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* within here you find the webdriver scope
|
||||
*/
|
||||
let retries = 0
|
||||
const wrappedCommand = async function (this: EventEmitter, ...args: any[]): Promise<any> {
|
||||
await self.checkPendingNavigations()
|
||||
const params = validate(command, parameters, variables as any, ref, args)
|
||||
let result
|
||||
|
||||
try {
|
||||
this.emit('command', { command, params, retries })
|
||||
result = await self.commands[command].call(self, params)
|
||||
} catch (err: any) {
|
||||
/**
|
||||
* if though we check for an execution context before executing a command we
|
||||
* can technically still run into the situation (especially if the command
|
||||
* contains multiple interaction with the page and is long) where the execution
|
||||
* context gets destroyed. For these cases handle page transitions gracefully
|
||||
* by repeating the command.
|
||||
*/
|
||||
if (err.message.includes('most likely because of a navigation')) {
|
||||
log.debug('Command failed due to unfinished page transition, retrying...')
|
||||
const page = self.getPageHandle()
|
||||
|
||||
++retries
|
||||
|
||||
// In some cases, the page load event never arrives
|
||||
// Wait for a page load event only after trying to execute the command a few times
|
||||
if (retries >= 3) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const pageloadTimeout = setTimeout(() => reject(new Error('page load timeout')), self.timeouts.get('pageLoad'))
|
||||
page.once('load', () => {
|
||||
clearTimeout(pageloadTimeout)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return wrappedCommand.apply(this, args)
|
||||
}
|
||||
|
||||
throw sanitizeError(err)
|
||||
}
|
||||
|
||||
this.emit('result', { command, params, retries, result: { value: result } })
|
||||
if (typeof result !== 'undefined') {
|
||||
const isScreenshot = (
|
||||
command.toLowerCase().includes('screenshot') &&
|
||||
typeof result === 'string' &&
|
||||
result.length > 64
|
||||
)
|
||||
log.info('RESULT', isScreenshot ? `${result.substr(0, 61)}...` : result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return wrappedCommand
|
||||
}
|
||||
|
||||
dialogHandler(dialog: Dialog) {
|
||||
this.activeDialog = dialog
|
||||
}
|
||||
|
||||
framenavigatedHandler(frame: Frame) {
|
||||
this.currentFrameUrl = frame.url()
|
||||
this.elementStore.clear(frame.parentFrame() ? frame : undefined)
|
||||
}
|
||||
|
||||
setTimeouts(implicit?: number, pageLoad?: number, script?: number) {
|
||||
if (typeof implicit === 'number') {
|
||||
this.timeouts.set('implicit', implicit)
|
||||
}
|
||||
if (typeof pageLoad === 'number') {
|
||||
this.timeouts.set('pageLoad', pageLoad)
|
||||
}
|
||||
if (typeof script === 'number') {
|
||||
this.timeouts.set('script', script)
|
||||
}
|
||||
|
||||
const page = this.getPageHandle()
|
||||
const pageloadTimeout = this.timeouts.get('pageLoad')
|
||||
if (page && pageloadTimeout) {
|
||||
page.setDefaultTimeout(pageloadTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
getPageHandle (isInFrame = false) {
|
||||
if (isInFrame && this.currentFrame) {
|
||||
return this.currentFrame
|
||||
}
|
||||
|
||||
if (!this.currentWindowHandle) {
|
||||
throw new Error('no current window handle registered')
|
||||
}
|
||||
|
||||
const pageHandle = this.windows.get(this.currentWindowHandle)
|
||||
|
||||
if (!pageHandle) {
|
||||
throw new Error('Couldn\'t find page handle')
|
||||
}
|
||||
|
||||
return pageHandle
|
||||
}
|
||||
|
||||
async checkPendingNavigations (pendingNavigationStart = Date.now()) {
|
||||
/**
|
||||
* ensure there is no page transition happening and an execution context
|
||||
* is available
|
||||
*/
|
||||
let page = this.getPageHandle()
|
||||
|
||||
/**
|
||||
* ignore pending navigation check if dialog is open
|
||||
* or there are no pages
|
||||
*/
|
||||
if (this.activeDialog || !page) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* if current page is a frame we have to get the page from the browser
|
||||
* that has this frame listed
|
||||
*/
|
||||
if (!page.mainFrame) {
|
||||
const pages = await this.browser.pages()
|
||||
const mainFrame = pages.find((browserPage) => (
|
||||
browserPage.frames().find((frame: unknown) => page === frame)
|
||||
))
|
||||
|
||||
if (mainFrame) {
|
||||
page = mainFrame
|
||||
}
|
||||
}
|
||||
|
||||
const pageloadTimeout = this.timeouts.get('pageLoad')
|
||||
const pageloadTimeoutReached = typeof pageloadTimeout !== 'undefined'
|
||||
? Date.now() - pendingNavigationStart > pageloadTimeout
|
||||
: false
|
||||
|
||||
try {
|
||||
const executionContext = await page.mainFrame()
|
||||
await executionContext.evaluate('1')
|
||||
|
||||
/**
|
||||
* if we have an execution context, also check for the ready state
|
||||
*/
|
||||
const readyState = await executionContext.evaluate('document.readyState')
|
||||
if (readyState === 'complete' || pageloadTimeoutReached) {
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
/**
|
||||
* throw original error if a context could not be established
|
||||
*/
|
||||
if (pageloadTimeoutReached) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* Avoid looping so quickly we run out of memory before the timeout.
|
||||
*/
|
||||
await new Promise(resolve => setTimeout(resolve, Math.min(100, typeof pageloadTimeout === 'number' ? pageloadTimeout / 10 : 100)))
|
||||
await this.checkPendingNavigations(pendingNavigationStart)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import type { ElementHandle } from 'puppeteer-core/lib/esm/puppeteer/api/ElementHandle.js'
|
||||
import type { Frame } from 'puppeteer-core/lib/esm/puppeteer/api/Frame.js'
|
||||
|
||||
export default class ElementStore {
|
||||
private _index = 0
|
||||
private _elementMap: Map<string, ElementHandle> = new Map()
|
||||
private _frameMap: Map<Frame, Set<string>> = new Map()
|
||||
|
||||
set (elementHandle: ElementHandle) {
|
||||
const index = `ELEMENT-${++this._index}`
|
||||
this._elementMap.set(index, elementHandle)
|
||||
const frame = elementHandle.frame
|
||||
if (frame) {
|
||||
let elementIndexes = this._frameMap.get(frame)
|
||||
if (!elementIndexes) {
|
||||
elementIndexes = new Set()
|
||||
this._frameMap.set(frame, elementIndexes)
|
||||
}
|
||||
elementIndexes.add(index)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
async get (index: string) {
|
||||
const elementHandle = this._elementMap.get(index)
|
||||
|
||||
if (!elementHandle) {
|
||||
return elementHandle
|
||||
}
|
||||
|
||||
const isElementAttachedToDOM = await elementHandle.evaluate((el: HTMLElement) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected
|
||||
return el.isConnected
|
||||
})
|
||||
|
||||
return isElementAttachedToDOM ? elementHandle : undefined
|
||||
}
|
||||
|
||||
clear (frame?: Frame) {
|
||||
if (!frame) {
|
||||
this._elementMap.clear()
|
||||
this._frameMap.clear()
|
||||
return
|
||||
}
|
||||
|
||||
const elementIndexes = this._frameMap.get(frame)
|
||||
if (elementIndexes) {
|
||||
elementIndexes.forEach((elementIndex) => this._elementMap.delete(elementIndex))
|
||||
this._frameMap.delete(frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
/* istanbul ignore file */
|
||||
|
||||
/**
|
||||
* @license Copyright 2016 Google Inc. All Rights Reserved.
|
||||
* 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 os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { execSync } from 'node:child_process'
|
||||
import { getEdgePath } from 'edge-paths'
|
||||
import { canAccess } from '@wdio/utils/node'
|
||||
|
||||
import { sort, findByWhich } from '../utils.js'
|
||||
import { darwinGetAppPaths, darwinGetInstallations } from './finder.js'
|
||||
|
||||
const newLineRegex = /\r?\n/
|
||||
const EDGE_BINARY_NAMES = ['edge', 'msedge', 'microsoft-edge', 'microsoft-edge-dev']
|
||||
const EDGE_REGEX = /((ms|microsoft))?edge/g
|
||||
|
||||
function darwin() {
|
||||
const suffixes = [
|
||||
'/Contents/MacOS/Microsoft Edge'
|
||||
]
|
||||
|
||||
const appName = 'Microsoft Edge'
|
||||
const defaultPath = `/Applications/${appName}.app${suffixes[0]}`
|
||||
|
||||
let installations
|
||||
if (canAccess(defaultPath)) {
|
||||
installations = [defaultPath]
|
||||
} else {
|
||||
const appPaths = darwinGetAppPaths(appName)
|
||||
installations = darwinGetInstallations(appPaths, suffixes)
|
||||
}
|
||||
|
||||
// Retains one per line to maintain readability.
|
||||
// clang-format off
|
||||
const priorities = [
|
||||
{ regex: new RegExp(`^${process.env.HOME}/Applications/.*Microsoft Edge.app`), weight: 50 },
|
||||
{ regex: /^\/Applications\/.*Microsoft Edge.app/, weight: 100 },
|
||||
{ regex: /^\/Volumes\/.*Microsoft Edge.app/, weight: -2 }
|
||||
]
|
||||
|
||||
const whichFinds = findByWhich(
|
||||
EDGE_BINARY_NAMES,
|
||||
[{ regex: EDGE_REGEX, weight: 51 }]
|
||||
)
|
||||
const installFinds = sort(installations, priorities)
|
||||
return [...installFinds, ...whichFinds]
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for linux executables in 3 ways
|
||||
* 1. Look into the directories where .desktop are saved on gnome based distros
|
||||
* 2. Look for edge by using the which command
|
||||
*/
|
||||
function linux() {
|
||||
let installations: string[] = []
|
||||
|
||||
// 1. Look into the directories where .desktop are saved on gnome based distros
|
||||
const desktopInstallationFolders = [
|
||||
path.join(os.homedir(), '.local/share/applications/'),
|
||||
'/usr/share/applications/',
|
||||
]
|
||||
desktopInstallationFolders.forEach(folder => {
|
||||
installations = installations.concat(findEdgeExecutables(folder))
|
||||
})
|
||||
|
||||
return findByWhich(
|
||||
EDGE_BINARY_NAMES,
|
||||
[{ regex: EDGE_REGEX, weight: 51 }]
|
||||
)
|
||||
}
|
||||
|
||||
function win32 () {
|
||||
const installations: string[] = []
|
||||
const suffixes = [
|
||||
`${path.sep}Microsoft${path.sep}Edge${path.sep}Application${path.sep}edge.exe`,
|
||||
`${path.sep}Microsoft${path.sep}Edge${path.sep}Application${path.sep}msedge.exe`,
|
||||
`${path.sep}Microsoft${path.sep}Edge Dev${path.sep}Application${path.sep}msedge.exe`
|
||||
]
|
||||
|
||||
const prefixes = [
|
||||
process.env.LOCALAPPDATA || '', process.env.PROGRAMFILES || '', process.env['PROGRAMFILES(X86)'] || ''
|
||||
].filter(Boolean)
|
||||
|
||||
const checkedPath: string[] = []
|
||||
prefixes.forEach(prefix => suffixes.forEach(suffix => {
|
||||
const edgePath = path.join(prefix, suffix)
|
||||
checkedPath.push(edgePath)
|
||||
if (canAccess(edgePath)) {
|
||||
installations.push(edgePath)
|
||||
}
|
||||
}))
|
||||
|
||||
/**
|
||||
* fallback using edge-path
|
||||
*/
|
||||
if (installations.length === 0) {
|
||||
const edgePath = getEdgePath()
|
||||
if (canAccess(edgePath)) {
|
||||
installations.push(edgePath)
|
||||
}
|
||||
}
|
||||
|
||||
return installations
|
||||
}
|
||||
|
||||
function findEdgeExecutables(folder: string) {
|
||||
const argumentsRegex = /(^[^ ]+).*/ // Take everything up to the first space
|
||||
const edgeExecRegex = '^Exec=/.*/(edge)-.*'
|
||||
|
||||
const installations: string[] = []
|
||||
if (canAccess(folder)) {
|
||||
let execPaths
|
||||
|
||||
// Some systems do not support grep -R so fallback to -r.
|
||||
// See https://github.com/GoogleChrome/chrome-launcher/issues/46 for more context.
|
||||
try {
|
||||
execPaths = execSync(
|
||||
`grep -ER "${edgeExecRegex}" ${folder} | awk -F '=' '{print $2}'`, { stdio: 'pipe' })
|
||||
} catch (err: any) {
|
||||
execPaths = execSync(
|
||||
`grep -Er "${edgeExecRegex}" ${folder} | awk -F '=' '{print $2}'`, { stdio: 'pipe' })
|
||||
}
|
||||
|
||||
execPaths = execPaths.toString().split(newLineRegex).map(
|
||||
(execPath) => execPath.replace(argumentsRegex, '$1'))
|
||||
|
||||
execPaths.forEach((execPath) => canAccess(execPath) && installations.push(execPath))
|
||||
}
|
||||
|
||||
return installations
|
||||
}
|
||||
|
||||
export default {
|
||||
darwin,
|
||||
linux,
|
||||
win32
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import path from 'node:path'
|
||||
import { execSync } from 'node:child_process'
|
||||
|
||||
import { canAccess } from '@wdio/utils/node'
|
||||
|
||||
const DARWIN_LIST_APPS = 'system_profiler SPApplicationsDataType -json'
|
||||
|
||||
interface ApplicationDataType {
|
||||
SPApplicationsDataType: {
|
||||
info: string
|
||||
path: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export const darwinGetAppPaths = (app: string) => {
|
||||
const apps: ApplicationDataType = JSON.parse(execSync(DARWIN_LIST_APPS).toString())
|
||||
const appPaths = apps.SPApplicationsDataType
|
||||
.filter(inst => inst.info && inst.info.startsWith(app))
|
||||
.map(inst => inst.path)
|
||||
|
||||
return appPaths
|
||||
}
|
||||
|
||||
export const darwinGetInstallations = (appPaths: string[], suffixes: string[]) => {
|
||||
const installations: string[] = []
|
||||
appPaths.forEach((inst) => {
|
||||
suffixes.forEach(suffix => {
|
||||
const execPath = path.join(inst.substring(0, inst.indexOf('.app') + 4).trim(), suffix)
|
||||
if (canAccess(execPath) && installations.indexOf(execPath) === -1) {
|
||||
installations.push(execPath)
|
||||
}
|
||||
})
|
||||
})
|
||||
return installations
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
/* istanbul ignore file */
|
||||
|
||||
/**
|
||||
* @license Copyright 2016 Google Inc. All Rights Reserved.
|
||||
* 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 os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { execSync } from 'node:child_process'
|
||||
import { canAccess } from '@wdio/utils/node'
|
||||
|
||||
import { sort, findByWhich } from '../utils.js'
|
||||
import { darwinGetAppPaths, darwinGetInstallations } from './finder.js'
|
||||
|
||||
const newLineRegex = /\r?\n/
|
||||
|
||||
export interface Priorities {
|
||||
regex: RegExp
|
||||
weight: number
|
||||
}
|
||||
|
||||
function darwin() {
|
||||
const suffixes = [
|
||||
'/Contents/MacOS/firefox-bin'
|
||||
]
|
||||
|
||||
const appName = 'Firefox Nightly'
|
||||
const defaultPath = `/Applications/${appName}.app${suffixes[0]}`
|
||||
|
||||
let installations
|
||||
if (canAccess(defaultPath)) {
|
||||
installations = [defaultPath]
|
||||
} else {
|
||||
const appPaths = darwinGetAppPaths(appName)
|
||||
installations = darwinGetInstallations(appPaths, suffixes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retains one per line to maintain readability.
|
||||
*/
|
||||
const priorities: Priorities[] = [
|
||||
{ regex: new RegExp(`^${process.env.HOME}/Applications/.*Firefox.app`), weight: 50 },
|
||||
{ regex: /^\/Applications\/.*Firefox.app/, weight: 100 },
|
||||
{ regex: /^\/Volumes\/.*Firefox.app/, weight: -2 }
|
||||
]
|
||||
|
||||
const whichFinds = findByWhich(
|
||||
['firefox-nightly', 'firefox-trunk'],
|
||||
[{ regex: /firefox-nightly/, weight: 51 }]
|
||||
)
|
||||
const installFinds = sort(installations, priorities)
|
||||
return [...installFinds, ...whichFinds]
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for linux executables in 3 ways
|
||||
* 1. Look into the directories where .desktop are saved on gnome based distros
|
||||
* 2. Look for edge by using the which command
|
||||
*/
|
||||
function linux() {
|
||||
let installations: string[] = []
|
||||
|
||||
// 1. Look into the directories where .desktop are saved on gnome based distros
|
||||
const desktopInstallationFolders = [
|
||||
path.join(os.homedir(), '.local/share/applications/'),
|
||||
'/usr/share/applications/',
|
||||
]
|
||||
desktopInstallationFolders.forEach(folder => {
|
||||
installations = installations.concat(findFirefoxExecutables(folder))
|
||||
})
|
||||
|
||||
const whichFinds = findByWhich(
|
||||
['firefox-nightly', 'firefox-trunk', 'firefox'],
|
||||
[{ regex: /firefox/, weight: 51 }]
|
||||
)
|
||||
return [...installations, ...whichFinds]
|
||||
}
|
||||
|
||||
function win32() {
|
||||
const installations: string[] = []
|
||||
const suffixes = [
|
||||
`${path.sep}Firefox Nightly${path.sep}Application${path.sep}firefox.exe`
|
||||
]
|
||||
|
||||
const prefixes = [
|
||||
process.env.LOCALAPPDATA || '', process.env.PROGRAMFILES || '', process.env['PROGRAMFILES(X86)'] || ''
|
||||
].filter(Boolean)
|
||||
|
||||
prefixes.forEach(prefix => suffixes.forEach(suffix => {
|
||||
const firefoxPath = path.join(prefix, suffix)
|
||||
if (canAccess(firefoxPath)) {
|
||||
installations.push(firefoxPath)
|
||||
}
|
||||
}))
|
||||
|
||||
return installations
|
||||
}
|
||||
|
||||
function findFirefoxExecutables(folder: string) {
|
||||
const argumentsRegex = /(^[^ ]+).*/ // Take everything up to the first space
|
||||
const edgeExecRegex = '^Exec=/.*/(firefox)-.*'
|
||||
|
||||
const installations: string[] = []
|
||||
if (canAccess(folder)) {
|
||||
let execPaths
|
||||
|
||||
// Some systems do not support grep -R so fallback to -r.
|
||||
// See https://github.com/GoogleChrome/chrome-launcher/issues/46 for more context.
|
||||
try {
|
||||
execPaths = execSync(
|
||||
`grep -ER "${edgeExecRegex}" ${folder} | awk -F '=' '{print $2}'`, { stdio: 'pipe' })
|
||||
} catch (err: any) {
|
||||
execPaths = execSync(
|
||||
`grep -Er "${edgeExecRegex}" ${folder} | awk -F '=' '{print $2}'`, { stdio: 'pipe' })
|
||||
}
|
||||
|
||||
execPaths = execPaths.toString().split(newLineRegex).map(
|
||||
(execPath) => execPath.replace(argumentsRegex, '$1'))
|
||||
|
||||
execPaths.forEach((execPath) => canAccess(execPath) && installations.push(execPath))
|
||||
}
|
||||
|
||||
return installations
|
||||
}
|
||||
|
||||
export default {
|
||||
darwin,
|
||||
linux,
|
||||
win32
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import edgeFinder from './edge.js'
|
||||
import firefoxFinder from './firefox.js'
|
||||
|
||||
export default (browserName: 'firefox' | 'edge', platform: NodeJS.Platform) => {
|
||||
const finder = {
|
||||
firefox: firefoxFinder,
|
||||
edge: edgeFinder
|
||||
}[browserName]
|
||||
|
||||
const supportedPlatforms = Object.keys(finder) as NodeJS.Platform[]
|
||||
if (!supportedPlatforms.includes(platform)) {
|
||||
throw new Error(`Operating system ("${process.platform}") is not supported`)
|
||||
}
|
||||
|
||||
return finder[platform as keyof typeof finder]
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import UAParser from 'ua-parser-js'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import logger from '@wdio/logger'
|
||||
import { webdriverMonad, devtoolsEnvironmentDetector } from '@wdio/utils'
|
||||
import { validateConfig } from '@wdio/config'
|
||||
import type { CommandEndpoint } from '@wdio/protocols'
|
||||
import type { Options, Capabilities } from '@wdio/types'
|
||||
import type { Browser } from 'puppeteer-core/lib/esm/puppeteer/api/Browser.js'
|
||||
|
||||
import DevToolsDriver from './devtoolsdriver.js'
|
||||
import launch from './launcher.js'
|
||||
import { DEFAULTS, SUPPORTED_BROWSER, VENDOR_PREFIX } from './constants.js'
|
||||
import { getPrototype, patchDebug } from './utils.js'
|
||||
import type { Client, AttachOptions, DevToolsOptions as WDIODevtoolsOptionsExtension } from './types.js'
|
||||
|
||||
const log = logger('devtools:puppeteer')
|
||||
|
||||
/**
|
||||
* patch debug package to log Puppeteer CDP messages
|
||||
*/
|
||||
await patchDebug(log)
|
||||
|
||||
export const sessionMap = new Map()
|
||||
|
||||
export default class DevTools {
|
||||
static async newSession (
|
||||
options: Options.WebDriver,
|
||||
modifier?: Function,
|
||||
userPrototype = {},
|
||||
customCommandWrapper?: Function
|
||||
): Promise<Client> {
|
||||
const envLogLevel = process.env.WDIO_LOG_LEVEL as Options.WebDriverLogTypes | undefined
|
||||
options.logLevel = envLogLevel ?? options.logLevel
|
||||
const params = validateConfig(DEFAULTS, options)
|
||||
|
||||
if (params.logLevel && (!options.logLevels || !(options.logLevels as any).devtools)) {
|
||||
logger.setLevel('devtools', params.logLevel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store all log events in a file
|
||||
*/
|
||||
if (params.outputDir && !process.env.WDIO_LOG_PATH) {
|
||||
process.env.WDIO_LOG_PATH = path.join(params.outputDir, 'wdio.log')
|
||||
}
|
||||
|
||||
log.info('Initiate new session using the DevTools protocol')
|
||||
|
||||
const requestedCapabilities = { ...params.capabilities }
|
||||
const browser = await launch(params.capabilities as WebdriverIO.Capabilities)
|
||||
const pages = await browser.pages()
|
||||
const driver = new DevToolsDriver(browser, pages)
|
||||
const sessionId = uuidv4()
|
||||
const uaParser = new UAParser(await browser.userAgent())
|
||||
const userAgent = uaParser.getResult()
|
||||
|
||||
/**
|
||||
* find vendor key in capabilities
|
||||
*/
|
||||
type ValueOf<T> = T[keyof T]
|
||||
const availableVendorPrefixes = Object.values(VENDOR_PREFIX)
|
||||
const vendorCapPrefix = Object.keys(params.capabilities as WebdriverIO.Capabilities)
|
||||
.find(
|
||||
(capKey: ValueOf<typeof VENDOR_PREFIX>) => availableVendorPrefixes.includes(capKey)
|
||||
) as keyof WebdriverIO.Capabilities
|
||||
||
|
||||
VENDOR_PREFIX[userAgent.browser.name?.toLocaleLowerCase() as keyof typeof VENDOR_PREFIX]
|
||||
|
||||
const { browserName } = (requestedCapabilities as Capabilities.W3CCapabilities).alwaysMatch || requestedCapabilities
|
||||
params.capabilities = {
|
||||
browserName: (userAgent.browser.name || browserName || 'unknown').split(' ').shift()?.toLowerCase(),
|
||||
browserVersion: userAgent.browser.version,
|
||||
platformName: os.platform(),
|
||||
platform: os.release()
|
||||
}
|
||||
|
||||
if (vendorCapPrefix) {
|
||||
Object.assign(params.capabilities, {
|
||||
[vendorCapPrefix]: Object.assign(
|
||||
{ debuggerAddress: browser.wsEndpoint().split('/')[2] },
|
||||
params.capabilities[vendorCapPrefix]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
sessionMap.set(sessionId, { browser, session: driver })
|
||||
const environmentPrototype: Record<string, { value: any }> = {}
|
||||
Object.entries(devtoolsEnvironmentDetector({
|
||||
browserName: userAgent?.browser?.name?.toLowerCase()
|
||||
})).forEach(([name, value]) => {
|
||||
environmentPrototype[name] = { value }
|
||||
})
|
||||
const commandWrapper = (
|
||||
method: string,
|
||||
endpoint: string,
|
||||
commandInfo: CommandEndpoint
|
||||
) => driver.register(commandInfo)
|
||||
const protocolCommands = getPrototype(commandWrapper)
|
||||
const prototype = {
|
||||
...protocolCommands,
|
||||
...userPrototype,
|
||||
...environmentPrototype
|
||||
}
|
||||
|
||||
const monad = webdriverMonad(
|
||||
{ ...params, requestedCapabilities },
|
||||
modifier,
|
||||
prototype
|
||||
)
|
||||
return monad(sessionId, customCommandWrapper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes The instance session id and browser capabilities for the new session
|
||||
* directly into the passed in browser object
|
||||
*
|
||||
* @param {object} instance the object we get from a new browser session.
|
||||
* @returns {string} the new session id of the browser
|
||||
*/
|
||||
static async reloadSession (instance: Client, newCapabilities: WebdriverIO.Capabilities): Promise<string> {
|
||||
const { session } = sessionMap.get(instance.sessionId)
|
||||
const browser = await launch({
|
||||
...instance.requestedCapabilities as WebdriverIO.Capabilities,
|
||||
...(newCapabilities || {})
|
||||
})
|
||||
const pages = await browser.pages()
|
||||
session.initBrowser.call(session, browser, pages)
|
||||
instance.puppeteer = browser
|
||||
sessionMap.set(instance.sessionId, { browser, session })
|
||||
return instance.sessionId
|
||||
}
|
||||
|
||||
/**
|
||||
* allows user to attach to existing sessions
|
||||
*/
|
||||
static async attachToSession (
|
||||
options: AttachOptions,
|
||||
modifier?: Function,
|
||||
userPrototype = {},
|
||||
customCommandWrapper?: Function
|
||||
): Promise<Client> {
|
||||
const browser = await launch(options.capabilities)
|
||||
const pages = await browser.pages()
|
||||
const driver = new DevToolsDriver(browser, pages)
|
||||
const sessionId = uuidv4()
|
||||
const uaParser = new UAParser(await browser.userAgent())
|
||||
const userAgent = uaParser.getResult()
|
||||
|
||||
const environmentPrototype: Record<string, { value: Browser | boolean }> = { puppeteer: { value: browser } }
|
||||
Object.entries(devtoolsEnvironmentDetector({
|
||||
browserName: userAgent?.browser?.name?.toLowerCase()
|
||||
})).forEach(([name, value]) => {
|
||||
environmentPrototype[name] = { value }
|
||||
})
|
||||
const commandWrapper = (
|
||||
method: string,
|
||||
endpoint: string,
|
||||
commandInfo: CommandEndpoint
|
||||
) => driver.register(commandInfo)
|
||||
const protocolCommands = getPrototype(commandWrapper)
|
||||
const prototype = {
|
||||
...protocolCommands,
|
||||
...userPrototype,
|
||||
...environmentPrototype
|
||||
}
|
||||
|
||||
const monad = webdriverMonad(
|
||||
options,
|
||||
modifier,
|
||||
prototype
|
||||
)
|
||||
return monad(sessionId, customCommandWrapper)
|
||||
}
|
||||
}
|
||||
|
||||
export { SUPPORTED_BROWSER }
|
||||
export * from './types.js'
|
||||
|
||||
declare global {
|
||||
namespace WebdriverIO {
|
||||
interface WDIODevtoolsOptions extends WDIODevtoolsOptionsExtension {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
import type { Options } from 'chrome-launcher'
|
||||
import { launch as launchChromeBrowser } from 'chrome-launcher'
|
||||
import type { PuppeteerLaunchOptions, ConnectOptions } from 'puppeteer-core'
|
||||
import puppeteer, { KnownDevices, Puppeteer } from 'puppeteer-core'
|
||||
import logger from '@wdio/logger'
|
||||
import type { Browser } from 'puppeteer-core/lib/esm/puppeteer/api/Browser.js'
|
||||
import type { Capabilities } from '@wdio/types'
|
||||
import { QueryHandler } from 'query-selector-shadow-dom/plugins/puppeteer/index.js'
|
||||
|
||||
import browserFinder from './finder/index.js'
|
||||
import { getPages, launchChromeUsingWhich } from './utils.js'
|
||||
import {
|
||||
CHROME_NAMES,
|
||||
FIREFOX_NAMES,
|
||||
EDGE_NAMES,
|
||||
BROWSER_TYPE,
|
||||
DEFAULT_FLAGS,
|
||||
DEFAULT_WIDTH,
|
||||
DEFAULT_HEIGHT,
|
||||
DEFAULT_X_POSITION,
|
||||
DEFAULT_Y_POSITION,
|
||||
VENDOR_PREFIX
|
||||
} from './constants.js'
|
||||
|
||||
const log = logger('devtools')
|
||||
|
||||
const DEVICE_NAMES = Object.keys(KnownDevices)
|
||||
|
||||
/**
|
||||
* launches Chrome and returns a Puppeteer browser instance
|
||||
* @param {object} capabilities session capabilities
|
||||
* @return {object} puppeteer browser instance
|
||||
*/
|
||||
async function launchChrome (capabilities: WebdriverIO.Capabilities) {
|
||||
const chromeOptions: Capabilities.ChromeOptions = capabilities[VENDOR_PREFIX.chrome] || {}
|
||||
const mobileEmulation = chromeOptions.mobileEmulation || {}
|
||||
const devtoolsOptions = capabilities['wdio:devtoolsOptions'] || {}
|
||||
const chromeOptionsArgs = (chromeOptions.args || []).map((arg) => (
|
||||
arg.startsWith('--') ? arg : `--${arg}`
|
||||
))
|
||||
|
||||
/**
|
||||
* `ignoreDefaultArgs` and `headless` are currently expected to be part of the capabilities
|
||||
* but we should move them into a custom capability object, e.g. `wdio:devtoolsOptions`.
|
||||
* This should be cleaned up for v7 release
|
||||
* ToDo(Christian): v7 cleanup
|
||||
*/
|
||||
const ignoreDefaultArgs = (capabilities as any).ignoreDefaultArgs || devtoolsOptions.ignoreDefaultArgs
|
||||
const headless = (chromeOptions as any).headless || devtoolsOptions.headless
|
||||
|
||||
if (typeof mobileEmulation.deviceName === 'string') {
|
||||
const deviceProperties = KnownDevices[mobileEmulation.deviceName as keyof typeof KnownDevices]
|
||||
|
||||
if (!deviceProperties) {
|
||||
throw new Error(`Unknown device name "${mobileEmulation.deviceName}", available: ${DEVICE_NAMES.join(', ')}`)
|
||||
}
|
||||
|
||||
mobileEmulation.userAgent = deviceProperties.userAgent
|
||||
mobileEmulation.deviceMetrics = {
|
||||
width: deviceProperties.viewport.width,
|
||||
height: deviceProperties.viewport.height,
|
||||
pixelRatio: deviceProperties.viewport.deviceScaleFactor
|
||||
}
|
||||
}
|
||||
|
||||
let userDataDir: string | boolean | undefined
|
||||
const userDataDirIndex = chromeOptionsArgs.findIndex((arg) => arg.includes('user-data-dir'))
|
||||
if (userDataDirIndex > -1) {
|
||||
userDataDir = chromeOptionsArgs[userDataDirIndex].split('=').pop() as string
|
||||
chromeOptionsArgs.splice(userDataDirIndex, 1)
|
||||
}
|
||||
|
||||
const defaultFlags = Array.isArray(ignoreDefaultArgs) ? DEFAULT_FLAGS.filter(flag => !ignoreDefaultArgs.includes(flag)) : (!ignoreDefaultArgs) ? DEFAULT_FLAGS : []
|
||||
const deviceMetrics = mobileEmulation.deviceMetrics || (devtoolsOptions.defaultViewport && {
|
||||
width: devtoolsOptions.defaultViewport.width,
|
||||
height: devtoolsOptions.defaultViewport.height,
|
||||
pixelRatio: devtoolsOptions.defaultViewport.deviceScaleFactor,
|
||||
touch: devtoolsOptions.defaultViewport.isMobile
|
||||
}) || {}
|
||||
const windowFlags = devtoolsOptions.defaultViewport !== null ?
|
||||
[`--window-position=${DEFAULT_X_POSITION},${DEFAULT_Y_POSITION}`, `--window-size=${deviceMetrics?.width || DEFAULT_WIDTH},${deviceMetrics?.height || DEFAULT_HEIGHT}`] : []
|
||||
const chromeFlags = [
|
||||
...defaultFlags,
|
||||
...windowFlags,
|
||||
...(headless ? [
|
||||
'--headless',
|
||||
'--no-sandbox'
|
||||
] : []),
|
||||
...chromeOptionsArgs
|
||||
]
|
||||
|
||||
if (typeof deviceMetrics.pixelRatio === 'number') {
|
||||
chromeFlags.push(`--device-scale-factor=${deviceMetrics.pixelRatio}`)
|
||||
}
|
||||
|
||||
if (typeof mobileEmulation.userAgent === 'string') {
|
||||
chromeFlags.push(`--user-agent=${mobileEmulation.userAgent}`)
|
||||
}
|
||||
|
||||
if (deviceMetrics?.touch) {
|
||||
chromeFlags.push(
|
||||
'--enable-touch-drag-drop',
|
||||
'--touch-events',
|
||||
'--enable-viewport'
|
||||
)
|
||||
}
|
||||
|
||||
// Honor both ignoreHTTPSErrors and acceptInsecureCerts
|
||||
// Only for WebKit/Blink engines, Firefox uses a different option
|
||||
if (capabilities.acceptInsecureCerts || devtoolsOptions.ignoreHTTPSErrors) {
|
||||
chromeFlags.push('--ignore-certificate-errors')
|
||||
}
|
||||
|
||||
log.info(`Launch Google Chrome (${chromeOptions.binary}) with flags: ${chromeFlags.join(' ')}`)
|
||||
const launchOptions: Options = {
|
||||
prefs: chromeOptions.prefs,
|
||||
chromePath: chromeOptions.binary,
|
||||
ignoreDefaultFlags: true,
|
||||
chromeFlags,
|
||||
userDataDir,
|
||||
envVars: devtoolsOptions.env,
|
||||
...(devtoolsOptions.customPort ? { port: devtoolsOptions.customPort } : {})
|
||||
}
|
||||
const chrome = await launchChromeBrowser(launchOptions).catch(
|
||||
(err: Error) => launchChromeUsingWhich(err, launchOptions))
|
||||
|
||||
log.info(`Connect Puppeteer with browser on port ${chrome.port}`)
|
||||
const browser = await puppeteer.connect({
|
||||
...chromeOptions,
|
||||
...devtoolsOptions,
|
||||
defaultViewport: null,
|
||||
browserURL: `http://127.0.0.1:${chrome.port}`
|
||||
}) as unknown as Browser // casting from @types/puppeteer to built in type
|
||||
|
||||
/**
|
||||
* when using Chrome Launcher we have to close a tab as Puppeteer
|
||||
* creates automatically a new one
|
||||
*/
|
||||
const pages = await getPages(browser)
|
||||
for (const page of pages.slice(0, -1)) {
|
||||
if (page.url() === 'about:blank') {
|
||||
await page.close()
|
||||
}
|
||||
}
|
||||
|
||||
return browser
|
||||
}
|
||||
|
||||
function launchBrowser (capabilities: WebdriverIO.Capabilities, browserType: 'edge' | 'firefox') {
|
||||
const product = browserType === BROWSER_TYPE.firefox ? BROWSER_TYPE.firefox : BROWSER_TYPE.chrome
|
||||
const vendorCapKey = VENDOR_PREFIX[browserType]
|
||||
const devtoolsOptions = capabilities['wdio:devtoolsOptions'] || {}
|
||||
|
||||
/**
|
||||
* `ignoreDefaultArgs` and `headless` are currently expected to be part of the capabilities
|
||||
* but we should move them into a custom capability object, e.g. `wdio:devtoolsOptions`.
|
||||
* This should be cleaned up for v7 release
|
||||
* ToDo(Christian): v7 cleanup
|
||||
*/
|
||||
const ignoreDefaultArgs = (capabilities as any).ignoreDefaultArgs || devtoolsOptions.ignoreDefaultArgs
|
||||
const headless = (capabilities as any).headless || devtoolsOptions.headless
|
||||
|
||||
// Set devtoolsOptions to honor both ignoreHTTPSErrors and acceptInsecureCerts
|
||||
// Only necessary for Firefox, not for WebKit/Blink engines
|
||||
devtoolsOptions.ignoreHTTPSErrors = browserType === 'firefox'
|
||||
? Boolean(devtoolsOptions.ignoreHTTPSErrors || capabilities.acceptInsecureCerts)
|
||||
: devtoolsOptions.ignoreHTTPSErrors ?? false
|
||||
|
||||
if (!capabilities[vendorCapKey]) {
|
||||
capabilities[vendorCapKey] = {}
|
||||
}
|
||||
|
||||
const browserFinderMethod = browserFinder(browserType, process.platform)
|
||||
const executablePath = (
|
||||
capabilities[vendorCapKey]?.binary ||
|
||||
browserFinderMethod()[0]
|
||||
)
|
||||
|
||||
const puppeteerOptions: PuppeteerLaunchOptions = Object.assign(<PuppeteerLaunchOptions>{
|
||||
product,
|
||||
executablePath,
|
||||
ignoreDefaultArgs,
|
||||
headless: Boolean(headless),
|
||||
defaultViewport: {
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT
|
||||
},
|
||||
prefs: capabilities[vendorCapKey]?.prefs,
|
||||
args: [
|
||||
// Set args to honor both ignoreHTTPSErrors and acceptInsecureCerts
|
||||
// Only for WebKit/Blink engines, Firefox uses a different option
|
||||
...['--ignore-certificate-errors']
|
||||
.filter(() =>
|
||||
browserType === 'edge'
|
||||
&& (devtoolsOptions.ignoreHTTPSErrors || capabilities.acceptInsecureCerts) )
|
||||
]
|
||||
}, capabilities[vendorCapKey] || {}, devtoolsOptions || {})
|
||||
|
||||
if (!executablePath) {
|
||||
throw new Error('Couldn\'t find executable for browser')
|
||||
}
|
||||
|
||||
log.info(`Launch ${executablePath} with config: ${JSON.stringify(puppeteerOptions)}`)
|
||||
return puppeteer.launch(puppeteerOptions) as unknown as Promise<Browser>
|
||||
}
|
||||
|
||||
function connectBrowser (connectionUrl: string, capabilities: WebdriverIO.Capabilities) {
|
||||
const connectionProp = connectionUrl.startsWith('http') ? 'browserURL' : 'browserWSEndpoint'
|
||||
const devtoolsOptions = capabilities['wdio:devtoolsOptions']
|
||||
const options: ConnectOptions = {
|
||||
[connectionProp]: connectionUrl,
|
||||
...devtoolsOptions
|
||||
}
|
||||
return puppeteer.connect(options) as unknown as Promise<Browser>
|
||||
}
|
||||
|
||||
export default async function launch (capabilities: WebdriverIO.Capabilities) {
|
||||
try {
|
||||
Puppeteer.unregisterCustomQueryHandler('shadow')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// ToDo(Christian): fix types (https://github.com/Georgegriff/query-selector-shadow-dom/issues/77)
|
||||
Puppeteer.registerCustomQueryHandler('shadow', QueryHandler as any)
|
||||
const browserName = capabilities.browserName?.toLowerCase()
|
||||
|
||||
/**
|
||||
* check if capabilities already contains connection details and connect
|
||||
* to that rather than starting a new browser
|
||||
*/
|
||||
const browserOptions = capabilities['goog:chromeOptions'] || capabilities['ms:edgeOptions']
|
||||
const devtoolsOptions = capabilities['wdio:devtoolsOptions'] || {}
|
||||
const connectionUrl = (
|
||||
(browserOptions?.debuggerAddress && `http://${browserOptions?.debuggerAddress}`) ||
|
||||
devtoolsOptions.browserURL ||
|
||||
devtoolsOptions.browserWSEndpoint
|
||||
)
|
||||
if (connectionUrl) {
|
||||
return connectBrowser(connectionUrl, capabilities)
|
||||
}
|
||||
|
||||
/**
|
||||
* This fixes running e2e tests on Windows. For some reason within a Vitest environment
|
||||
* capitalization matters for environment variables.
|
||||
*/
|
||||
if (!process.env.PROGRAMFILES && process.env.ProgramFiles) {
|
||||
process.env.PROGRAMFILES = process.env.ProgramFiles
|
||||
}
|
||||
const programFiles86 = process.env['ProgramFiles(X86)'] || process.env['ProgramFiles(x86)']
|
||||
if (!process.env['PROGRAMFILES(X86)'] && programFiles86) {
|
||||
process.env['PROGRAMFILES(X86)'] = programFiles86
|
||||
}
|
||||
|
||||
if (browserName && CHROME_NAMES.includes(browserName)) {
|
||||
return launchChrome(capabilities)
|
||||
}
|
||||
|
||||
if (browserName && FIREFOX_NAMES.includes(browserName)) {
|
||||
return launchBrowser(capabilities, BROWSER_TYPE.firefox)
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (browserName && EDGE_NAMES.includes(browserName)) {
|
||||
return launchBrowser(capabilities, BROWSER_TYPE.edge)
|
||||
}
|
||||
|
||||
throw new Error(`Couldn't identify browserName "${browserName}"`)
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* cleans up temporary attribute that is used to query element
|
||||
* with puppeteer
|
||||
*/
|
||||
export default function cleanUp (elem: Element | Element[], dataProperty: string) {
|
||||
const elems = Array.isArray(elem) ? elem : [elem]
|
||||
for (const el of elems) {
|
||||
el.removeAttribute(dataProperty)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export default function createWindow (url: string, features: string) {
|
||||
return window.open(url, '_blank', features)
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
/**
|
||||
* clear input value of element
|
||||
*/
|
||||
export default function elementClear (html: HTMLElement, elem: HTMLInputElement) {
|
||||
elem.value = ''
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/**
|
||||
* injector script for executeScript command
|
||||
* According to the WebDriver spec we need to ensure to serialize all elements
|
||||
* being returned from the script. In order to do so we attach a data attribute
|
||||
* to them and fetch them using Puppeteer after the script execution. Instead
|
||||
* of the element we return the dataFlag string instead to signalize that this
|
||||
* is suppose to be an element handle
|
||||
*
|
||||
* @param {HTMLElement} _ $eval fetched element
|
||||
* @param {string} script user script
|
||||
* @param {string} dataProperty property name for elements being returned
|
||||
* @param {string} dataFlag flag for element
|
||||
* @param {Object[]} args user arguments for custom script
|
||||
* @return {Object} result of custom script
|
||||
*/
|
||||
export default (
|
||||
_: Element,
|
||||
script: string,
|
||||
scriptTimeout: number,
|
||||
dataProperty: string,
|
||||
dataFlag: string,
|
||||
...commandArgs: any[]
|
||||
): Promise<any> => {
|
||||
return new Promise((_resolve, _reject) => {
|
||||
setTimeout(
|
||||
() => _reject(new Error('Evaluation failed: script timeout')),
|
||||
scriptTimeout
|
||||
)
|
||||
|
||||
;(window as any).arguments = [...commandArgs, (result: NodeList | any) => {
|
||||
let tmpResult: (Node | any)[] = result instanceof NodeList ? Array.from(result) : result
|
||||
const isResultArray = Array.isArray(tmpResult)
|
||||
tmpResult = isResultArray ? tmpResult : [tmpResult as unknown as Node]
|
||||
|
||||
if (tmpResult.find((r) => r instanceof HTMLElement)) {
|
||||
tmpResult = tmpResult.map((r, i) => {
|
||||
if (r instanceof HTMLElement) {
|
||||
const dataPropertyValue = `${dataFlag}_${i}`
|
||||
r.setAttribute(dataProperty, dataPropertyValue)
|
||||
return dataPropertyValue
|
||||
}
|
||||
|
||||
return r
|
||||
})
|
||||
}
|
||||
|
||||
return _resolve(isResultArray ? tmpResult : tmpResult[0])
|
||||
}]
|
||||
|
||||
return eval(script)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/**
|
||||
* injector script for executeScript command
|
||||
* According to the WebDriver spec we need to ensure to serialize all elements
|
||||
* being returned from the script. In order to do so we attach a data attribute
|
||||
* to them and fetch them using Puppeteer after the script execution. Instead
|
||||
* of the element we return the dataFlag string instead to signalize that this
|
||||
* is suppose to be an element handle
|
||||
*
|
||||
* @param {HTMLElement} _ $eval fetched element
|
||||
* @param {string} script user script
|
||||
* @param {string} dataProperty property name for elements being returned
|
||||
* @param {string} dataFlag flag for element
|
||||
* @param {Object[]} args user arguments for custom script
|
||||
* @return {Object} result of custom script
|
||||
*/
|
||||
export default (_: HTMLElement, script: string, dataProperty: string, dataFlag: string, ...args: any[]) => {
|
||||
(window as any).arguments = args
|
||||
const result = eval(script)
|
||||
let tmpResult = result instanceof NodeList ? Array.from(result) : result
|
||||
const isResultArray = Array.isArray(tmpResult)
|
||||
tmpResult = isResultArray ? tmpResult : [tmpResult]
|
||||
|
||||
if (tmpResult.find((r: any) => r instanceof HTMLElement)) {
|
||||
tmpResult = tmpResult.map((r: any, i: number) => {
|
||||
if (r instanceof HTMLElement) {
|
||||
const dataPropertyValue = `${dataFlag}_${i}`
|
||||
r.setAttribute(dataProperty, dataPropertyValue)
|
||||
return dataPropertyValue
|
||||
}
|
||||
|
||||
return r
|
||||
})
|
||||
}
|
||||
|
||||
return isResultArray ? tmpResult : tmpResult[0]
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
/**
|
||||
* returns active element of document
|
||||
*/
|
||||
export default function getActiveElement (_: Element, dataProperty: string) {
|
||||
if (!document.activeElement) {
|
||||
return false
|
||||
}
|
||||
|
||||
document.activeElement.setAttribute(dataProperty, 'true')
|
||||
return true
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export default function getElementAttribute (_: HTMLElement, elem: HTMLElement, attr: string) {
|
||||
return elem.getAttribute(attr)
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export default function getElementCSSValue (_: HTMLElement, elem: HTMLElement, propertyName: string): string {
|
||||
/**
|
||||
* Have to cast to any due to https://github.com/Microsoft/TypeScript/issues/17827
|
||||
*/
|
||||
return (window.getComputedStyle(elem) as any)[propertyName]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue