Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save christian-bromann/335fbc83f5dcd5127aa1d99f963f3e26 to your computer and use it in GitHub Desktop.
Save christian-bromann/335fbc83f5dcd5127aa1d99f963f3e26 to your computer and use it in GitHub Desktop.
---
layout: iframe-left
url: https://w3c.github.io/webdriver-bidi/
---
# WebDriver Bidi Protocol
Some interesting new capabilities worth looking into:
<section v-click>
- The Log Module
</section>
<section v-click>
- The <pre class="inline">script.addPreloadScript</pre> Command
</section>
<section v-click>
- The <pre class="inline">script.SerializationOptions</pre> Type
</section>
<section v-click>
- The <pre class="inline">browsingContext.Locator</pre> Type
</section>
<section v-click>
- The <pre class="inline">network.addIntercept</pre> Command
</section>
---
layout: iframe-right
url: /bubbles.html
transition: none
class: overflow-inherit!
---
# Enable WebDriver Bidi
Can I use WebDriver Bidi today?
Except Safari, all browser have shipped with WebDriver Bidi primitives to be used today.
```ts {all|6|all} twoslash
/// <reference types="@wdio/globals" />
export const config: WebdriverIO.Config = {
capabilities: [{
browserName: 'chrome',
webSocketUrl: true
}]
}
```
Even cloud vendors such as BrowserStack and Sauce Labs have support for it.
---
layout: iframe-right
url: /bubbles.html
transition: none
class: overflow-inherit!
---
# The Log Module
Capturing logs at test runtime.
```ts {all|2-8|9-14|15-20|all} twoslash
import { browser } from '@wdio/globals'
/**
* enable listening on log events
*/
await browser.sessionSubscribe({
events: ['log.entryAdded']
})
/**
* listen on log events
*/
browser.on('log.entryAdded', (logEntry) => (
console.log(logEntry)
))
/**
* trigger a log event
*/
await browser.execute(() => (
console.log('Hello Bidi')
))
```
<section v-click class="h-[92%] absolute top-[2%] shadow-[0_35px_60px_-15px_rgba(0,0,0,1)] left-[105%] z-100 scale-[.8]">
```json
{
"args": [
{
"type": "string",
"value": "Hello Bidi"
}
],
"level": "info",
"method": "log",
"source": {
"context": "2903590F70C1E1C01EE293776DAA82E2",
"realm": "5127648937924364220.-3622626986935915699"
},
"stackTrace": {
"callFrames": [
{
"columnNumber": 44,
"functionName": "eval",
"lineNumber": 2,
"url": ""
},
// ...
]
},
"text": "Hello Bidi",
"timestamp": 1711597940186,
"type": "console"
}
```
</section>
---
layout: iframe-right
url: /bubbles.html
transition: none
class: overflow-inherit!
---
<h2 class="font-bold">The <pre class="inline">script.addPreloadScript</pre> Command</h2>
<p class="opacity-50">Configure the execution runtime before the application is loaded.</p>
<section :class="$clicks > 0 ? 'hidden' : ''">
#### Example: Emulate Geolocation
```ts
import { browser } from '@wdio/globals'
const patchedFn = options instanceof Error
? `cbError(new Error(${JSON.stringify(options.message)}))`
: `cbSuccess({
coords: ${JSON.stringify(options)},
timestamp: Date.now()
})`
await browser.scriptAddPreloadScript({
functionDeclaration: /*js*/`() => {
Object.defineProperty(navigator.geolocation, 'getCurrentPosition', {
value: (cbSuccess, cbError) => ${patchedFn}
})
}`
})
```
</section>
<section v-click>
#### Example: Emulate User Agent
```ts
import { browser } from '@wdio/globals'
await this.scriptAddPreloadScript({
functionDeclaration: /*js*/`() => {
Object.defineProperty(navigator, 'userAgent', {
value: ${JSON.stringify(options)}
})
}`
})
```
</section>
---
transition: none
---
# Testing Web Components
Solving the problem of piercing into the Shadow Root.
<div grid="~ cols-2 gap-8" m="t-2">
```mermaid
flowchart TD
B("&lt;html />")
B-->C(&lt;div />)
B-->D(&lt;my-cmp-b />)
B-->E(&lt;my-cmp-c />)
C-->F(&lt;my-cmp-a />)
F-->G(&lt;my-cmp-d />)
F-->H(&lt;my-cmp-e />)
H-->I(&lt;button>Click me!&lt;/button>)
E-->J(...)
J-->K(&lt;my-cmp-e />)
K-->L(&lt;button>Click me!&lt;/button>)
```
<div>
```ts
const buttons = await $$('aria/Click Me!')
console.log(buttons.length) // outputs: `0`
```
<div class="text-6xl mt-8 text-center">
🤔
</div>
</div>
</div>
---
transition: none
---
# Testing Web Components
Solving the problem of piercing into the Shadow Root.
<div grid="~ cols-2 gap-8" m="t-2">
```mermaid
flowchart TD
B("&lt;html />"):::green
classDef green stroke:#0f0
classDef red stroke:#f00
B-->C(&lt;div />):::green
B-->D(&lt;my-cmp-b />):::green
B-->E(&lt;my-cmp-c />):::green
C-->F(&lt;my-cmp-a />):::green
F-->G(&lt;my-cmp-d />):::red
F-->H(&lt;my-cmp-e />):::red
H-->I(&lt;button>Click me!&lt;/button>):::red
E-->J(...):::red
J-->K(&lt;my-cmp-e />):::red
K-->L(&lt;button>Click me!&lt;/button>):::red
```
<div>
```ts
const buttons = await $$('aria/Click Me!')
console.log(buttons.length) // outputs: `0`
```
<div class="text-6xl mt-8 text-center">
🤔
</div>
</div>
</div>
---
transition: none
---
# Testing Web Components
Solving the problem of piercing into the Shadow Root.
<div grid="~ cols-2 gap-8" m="t-2">
```mermaid
flowchart TD
B("&lt;html />"):::green
classDef green stroke:#0f0
classDef red stroke:#f00
B-->C(&lt;div />):::green
B-->D(&lt;my-cmp-b />):::green
B-->E(&lt;my-cmp-c />):::green
C-->F(&lt;my-cmp-a />):::green
F-->G(&lt;my-cmp-d />):::red
F-->H(&lt;my-cmp-e />):::green
H-->I(&lt;button>Click me!&lt;/button>):::green
E-->J(...):::red
J-->K(&lt;my-cmp-e />):::red
K-->L(&lt;button>Click me!&lt;/button>):::red
```
<div>
```ts
const button = await $('my-cmp-a')
.shadow$('my-cmp-e')
.shadow$('aria/Click Me!')
```
<div class="text-6xl mt-8 text-center">
🤔
</div>
</div>
</div>
---
transition: none
---
# Testing Web Components
Solving the problem of piercing into the Shadow Root.
<div grid="~ cols-2 gap-8" m="t-2">
```mermaid
flowchart TD
B("&lt;html />"):::green
classDef green stroke:#0f0
classDef red stroke:#f00
B-->C(&lt;div />):::green
B-->D(&lt;my-cmp-b />):::green
B-->E(&lt;my-cmp-c />):::green
C-->F(&lt;my-cmp-a />):::green
F-->G(&lt;my-cmp-d />):::red
F-->H(&lt;my-cmp-e />):::green
H-->M(...):::green
M-->N(&lt;my-cmp-f />):::green
N-->I(&lt;button>Click me!&lt;/button>):::red
E-->J(...):::red
J-->K(&lt;my-cmp-e />):::red
K-->L(&lt;button>Click me!&lt;/button>):::red
```
<div>
```ts
const button = await $('my-cmp-a')
.shadow$('my-cmp-e')
.shadow$('aria/Click Me!')
```
<div class="text-[#f00] text-8xl absolute top-[110px] right-[350px]">x</div>
<div class="text-6xl mt-8 text-center">
🤔
</div>
</div>
</div>
---
transition: none
---
# Testing Web Components
Solving the problem of piercing into the Shadow Root.
`script.addPreloadScript` to the rescue!
```ts
export default function customElementWrapper () {
const origFn = customElements.define.bind(customElements)
customElements.define = function(name: string, Constructor: CustomElementConstructor, options?: ElementDefinitionOptions) {
class WdioWrapperElement extends Constructor implements HTMLElement {
connectedCallback() {
super.connectedCallback && super.connectedCallback()
console.debug('[WDIO]', 'newShadowRoot', this.shadowRoot, this)
}
disconnectedCallback() {
super.disconnectedCallback && super.disconnectedCallback()
console.debug('[WDIO]', 'removeShadowRoot', this.shadowRoot)
}
}
return origFn(name, WdioWrapperElement, options)
}
}
```
---
transition: none
---
# Testing Web Components
Solving the problem of piercing into the Shadow Root.
<div grid="~ cols-2 gap-8" m="t-2">
```mermaid
flowchart TD
B("&lt;html />"):::green
classDef green stroke:#0f0
classDef red stroke:#f00
B-->C(&lt;div />):::green
B-->D(&lt;my-cmp-b />):::green
B-->E(&lt;my-cmp-c />):::green
C-->F(&lt;my-cmp-a />):::green
F-->G(&lt;my-cmp-d />):::green
F-->H(&lt;my-cmp-e />):::green
H-->M(...):::green
M-->N(&lt;my-cmp-f />):::green
N-->I(&lt;button>Click me!&lt;/button>):::green
E-->J(...):::green
J-->K(&lt;my-cmp-e />):::green
K-->L(&lt;button>Click me!&lt;/button>):::green
```
<div>
```ts
const buttons = await $$('aria/Click me!')
console.log(buttons.length) // outputs: `2` ✅
```
<div class="text-6xl mt-8 text-center">
🙌
</div>
</div>
</div>
---
transition: none
layout: image
image: /images/but.gif
backgroundSize: contain
---
<img v-click src="/images/attachShadow.png" />
---
transition: none
---
# Testing Web Components
Solving the problem of piercing into the Shadow Root.
Going back to the drawing board.
````md magic-move
```ts {all|7|all}
export default function customElementWrapper () {
const origFn = customElements.define.bind(customElements)
customElements.define = function(name: string, Constructor: CustomElementConstructor, options?: ElementDefinitionOptions) {
class WdioWrapperElement extends Constructor implements HTMLElement {
connectedCallback() {
super.connectedCallback && super.connectedCallback()
console.debug('[WDIO]', 'newShadowRoot', this.shadowRoot, this)
}
disconnectedCallback() {
super.disconnectedCallback && super.disconnectedCallback()
console.debug('[WDIO]', 'removeShadowRoot', this.shadowRoot)
}
}
return origFn(name, WdioWrapperElement, options)
}
}
```
```ts
export default function customElementWrapper () {
const origFn = customElements.define.bind(customElements)
customElements.define = function(name: string, Constructor: CustomElementConstructor, options?: ElementDefinitionOptions) {
class WdioWrapperElement extends Constructor implements HTMLElement {
disconnectedCallback() {
super.disconnectedCallback && super.disconnectedCallback()
console.debug('[WDIO]', 'removeShadowRoot', this.shadowRoot)
}
}
return origFn(name, WdioWrapperElement, options)
}
const attachShadowOrig = Element.prototype.attachShadow
Element.prototype.attachShadow = function (init: ShadowRootInit) {
const shadowRoot = attachShadowOrig.call(this, init)
console.debug('[WDIO]', 'newShadowRoot', shadowRoot, this)
return shadowRoot
}
}
```
````
<span v-click class="text-8xl absolute right-[15%] bottom-[20%]">🙌</span>
---
transition: none
---
# Testing Web Components
What about getting a snapshot of a Web Component?
<div grid="~ cols-2 gap-8" m="t-2">
```html {0-1,6-7}
<my-btn></my-btn>
```
```ts
// using WebdriverIO v8 and lower
const button = $('my-btn')
// get DOM snapshot
console.log(await button.getHTML())
// outputs: ""
// or:
await expect(button).toMatchInlineSnapshot(`""`)
```
</div>
<div class="text-6xl mt-8 text-center">
🤔
</div>
---
transition: none
---
# Testing Web Components
What about getting a snapshot of a Web Component?
<div grid="~ cols-2 gap-8" m="t-2">
```html
<my-btn></my-btn>
```
```ts
// using WebdriverIO v9 and up
const button = $('my-btn')
await expect(button).toMatchInlineSnapshot(`
<my-btn>
<template shadowrootmode="open">
<style>button { font-weight: bold; }</style>
<button>Click Me!</button>
</template>
</my-btn>
`)
```
</div>
<div class="text-6xl mt-8 text-center">
🙌
</div>
---
transition: none
---
# Testing Web Components
Do we still need this with `script.SerializationOptions`?
<style>
.smaller pre {
font-size: 0.6em !important;
line-height: 0.8em !important;
}
</style>
<div grid="~ cols-2" m="t-2">
<div class="smaller">
Executing scripts in WebDriver Bidi may become very powerful!
```ts
script.CallFunction = (
method: "script.callFunction",
params: script.CallFunctionParameters
)
script.CallFunctionParameters = {
functionDeclaration: text,
awaitPromise: bool,
target: script.Target,
? arguments: [*script.LocalValue],
? resultOwnership: script.ResultOwnership,
? serializationOptions: script.SerializationOptions,
? this: script.LocalValue,
? userActivation: bool .default false,
}
script.SerializationOptions = {
? maxDomDepth: (js-uint / null) .default 0,
? maxObjectDepth: (js-uint / null) .default null,
? includeShadowTree: ("none" / "open" / "all") .default "none",
}
```
</div>
<div class="flex items-center justify-center text-8xl">
👀
</div>
</div>
---
transition: none
---
# Element Selection
New locator capabilities via `browsingContext.Locator`!
<style>
.smaller pre {
font-size: 0.6em !important;
line-height: 0.8em !important;
}
</style>
<div grid="~ cols-2 gap-16" m="t-2">
<div class="smaller">
```ts
browsingContext.Locator = (
browsingContext.CssLocator /
browsingContext.InnerTextLocator /
browsingContext.XPathLocator
)
browsingContext.CssLocator = {
type: "css",
value: text
}
browsingContext.InnerTextLocator = {
type: "innerText",
value: text,
? ignoreCase: bool
? matchType: "full" / "partial",
? maxDepth: js-uint,
}
browsingContext.XPathLocator = {
type: "xpath",
value: text
}
```
</div>
<div>
Big update to WebDriver "Classic" locators:
- ~~tag name~~: <span class="text-[#f00]">removed</span>, use `css selector` instead
- ~~link text~~: <span class="text-[#f00]">removed</span>
- ~~partial link text~~: <span class="text-[#f00]">removed</span>
- <b class="text-[#0f0]">New:</b> inner text locator
- <b class="text-[#0f0]">New:</b> a11y locator
<div class="flex items-center justify-center text-8xl block mt-[30px]">
👀
</div>
</div>
</div>
---
transition: none
---
# Network Interception
Mock any type of requests within the browser!
<style>
.smaller pre {
font-size: 0.6em !important;
line-height: 0.8em !important;
}
</style>
<div grid="~ cols-2 gap-16" m="t-2">
<div class="smaller">
```ts
network.AddIntercept = (
method: "network.addIntercept",
params: network.AddInterceptParameters
)
network.AddInterceptParameters = {
phases: [+network.InterceptPhase],
? contexts: [+browsingContext.BrowsingContext],
? urlPatterns: [*network.UrlPattern],
}
network.InterceptPhase = (
"beforeRequestSent" /
"responseStarted" /
"authRequired"
)
```
</div>
<div>
The power of WebDriver Bidi network interception capabilities:
- modify outgoing requests
- attach a cookie or header to requests going to an API test-server
- redirect requests to a different url
- modify incoming responses
- update response body
- change cookie and header of the response
- overcome auth prompt when credentials are required
</div>
</div>
---
transition: none
---
# Network Interception
Mock any type of requests within the browser!
<div class="smaller">
````md magic-move
```ts {all|0-3|4-8|9|10-12|13-20|all}
await browser.sessionSubscribe({
events: ['network.responseStarted']
})
const pattern = 'http://localhost:8080/api/users'
await browser.networkAddIntercept({
phases: ['responseStarted'],
urlPatterns: [{ type: 'string', pattern }]
})
browser.on('network.responseStarted', (request) => {
if (!request.isBlocked) {
return
}
return browser.networkProvideResponse({
request: request.request.request,
statusCode: 400,
body: {
type: 'string',
value: JSON.stringify({ error: 'auth required' })
}
})
})
```
```ts
import { browser } from '@wdio/globals'
const mock = await browser.mock('*/api/user')
mock.respond({ error: 'auth required' }, {
statusCode: 400
})
```
```ts
import { browser } from '@wdio/globals'
const mock = await browser.mock('*/api/user')
mock.respondOnce({ error: 'not found' }, {
statusCode: 404
})
mock.respondOnce({ error: 'not found' }, {
statusCode: 404
})
mock.respond({ user: { ... } })
```
````
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment