Created
August 13, 2024 03:48
-
-
Save christian-bromann/335fbc83f5dcd5127aa1d99f963f3e26 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
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("<html />") | |
B-->C(<div />) | |
B-->D(<my-cmp-b />) | |
B-->E(<my-cmp-c />) | |
C-->F(<my-cmp-a />) | |
F-->G(<my-cmp-d />) | |
F-->H(<my-cmp-e />) | |
H-->I(<button>Click me!</button>) | |
E-->J(...) | |
J-->K(<my-cmp-e />) | |
K-->L(<button>Click me!</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("<html />"):::green | |
classDef green stroke:#0f0 | |
classDef red stroke:#f00 | |
B-->C(<div />):::green | |
B-->D(<my-cmp-b />):::green | |
B-->E(<my-cmp-c />):::green | |
C-->F(<my-cmp-a />):::green | |
F-->G(<my-cmp-d />):::red | |
F-->H(<my-cmp-e />):::red | |
H-->I(<button>Click me!</button>):::red | |
E-->J(...):::red | |
J-->K(<my-cmp-e />):::red | |
K-->L(<button>Click me!</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("<html />"):::green | |
classDef green stroke:#0f0 | |
classDef red stroke:#f00 | |
B-->C(<div />):::green | |
B-->D(<my-cmp-b />):::green | |
B-->E(<my-cmp-c />):::green | |
C-->F(<my-cmp-a />):::green | |
F-->G(<my-cmp-d />):::red | |
F-->H(<my-cmp-e />):::green | |
H-->I(<button>Click me!</button>):::green | |
E-->J(...):::red | |
J-->K(<my-cmp-e />):::red | |
K-->L(<button>Click me!</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("<html />"):::green | |
classDef green stroke:#0f0 | |
classDef red stroke:#f00 | |
B-->C(<div />):::green | |
B-->D(<my-cmp-b />):::green | |
B-->E(<my-cmp-c />):::green | |
C-->F(<my-cmp-a />):::green | |
F-->G(<my-cmp-d />):::red | |
F-->H(<my-cmp-e />):::green | |
H-->M(...):::green | |
M-->N(<my-cmp-f />):::green | |
N-->I(<button>Click me!</button>):::red | |
E-->J(...):::red | |
J-->K(<my-cmp-e />):::red | |
K-->L(<button>Click me!</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("<html />"):::green | |
classDef green stroke:#0f0 | |
classDef red stroke:#f00 | |
B-->C(<div />):::green | |
B-->D(<my-cmp-b />):::green | |
B-->E(<my-cmp-c />):::green | |
C-->F(<my-cmp-a />):::green | |
F-->G(<my-cmp-d />):::green | |
F-->H(<my-cmp-e />):::green | |
H-->M(...):::green | |
M-->N(<my-cmp-f />):::green | |
N-->I(<button>Click me!</button>):::green | |
E-->J(...):::green | |
J-->K(<my-cmp-e />):::green | |
K-->L(<button>Click me!</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