Skip to content

Commit 8ab84cd

Browse files
DavertMikclaude
andcommitted
feat: blockResources option for all web helpers
blockResources aborts requests of the listed resource types (image, media, font, stylesheet) before they reach the network. Unknown types fail on helper construction. - Playwright: context route with resourceType, registered with a predicate so stopMockingRoute cannot remove it - Puppeteer: cooperative request interception on every page; mockRoute, stopMockingRoute and traffic recording continue requests cooperatively so the blocker and mocks work together. stopMockingRoute no longer throws on RegExp urls - CDPBrowser: Fetch.enable with resourceType patterns and Fetch.failRequest BlockedByClient - WebDriver: BiDi network intercept matching the request destination - Appium: no-op Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 352466d commit 8ab84cd

15 files changed

Lines changed: 178 additions & 17 deletions

File tree

‎docs/configuration.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,28 @@ webServer: [
126126

127127
The server is started as part of bootstrap, right before the `bootstrap` hook (or `bootstrapAll` for `run-workers` and `run-multiple`), and stopped after `teardown` (`teardownAll`). It starts once in the main process; workers and child processes reuse it. The interactive `shell` starts it too, `dry-run` and `check` only with `--bootstrap`. CodeceptJS stops the command and every process it spawned, including when the run is interrupted with Ctrl+C.
128128

129+
## Blocking Resources
130+
131+
Web helpers can skip loading resources a test doesn't check. Set `blockResources` in the helper config:
132+
133+
```js
134+
helpers: {
135+
Playwright: {
136+
url: 'http://localhost:3000',
137+
blockResources: ['image', 'font', 'media'],
138+
},
139+
}
140+
```
141+
142+
Allowed values are `image`, `media`, `font` and `stylesheet`. Any other value fails on startup. Matching requests are aborted before they reach the network, so tests no longer exercise those resources.
143+
144+
| Helper | How it blocks | Notes |
145+
| --- | --- | --- |
146+
| Playwright | context route on every page, tab and session | Routing disables Playwright's HTTP cache. A `mockRoute` for a blocked URL still serves the mock. |
147+
| Puppeteer | request interception on every page | Request interception disables Puppeteer's page cache. A `mockRoute` for a blocked URL still serves the mock. |
148+
| CDPBrowser | CDP `Fetch` domain | Works in Chrome. Lightpanda doesn't load images in the first place. Obscura doesn't block: it ignores `Fetch.failRequest`. |
149+
| WebDriver | WebDriver BiDi network interception, matching the request destination | Needs BiDi, which is on by default. Every request makes a round trip to the test process. Ignored when BiDi is off. Appium ignores the option. |
150+
129151
## Dynamic configuration
130152

131153
A JS/TS config file is plain code, so you can read environment variables and build the config at runtime:

‎docs/helpers/Kitesurf.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ Type: [object][4]
8585
* `pollInterval` **[number][8]?** interval between checks while waiting, in milliseconds.
8686
* `getPageTimeout` **[number][8]?** maximum time to wait for a page to load, in seconds.
8787
* `waitForNavigation` **[string][3]?** when a navigation is considered finished: `load`, `domcontentloaded`, or `networkidle`.
88+
* `blockResources` **[Array][15]<[string][3]>?** resource types to skip loading: `'image'`, `'media'`, `'font'`, `'stylesheet'`. Matching requests are aborted before they reach the network.
8889

8990

9091

‎docs/helpers/Lightpanda.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Type: [object][4]
102102
* `pollInterval` **[number][8]?** interval between checks while waiting, in milliseconds.
103103
* `getPageTimeout` **[number][8]?** maximum time to wait for a page to load, in seconds.
104104
* `waitForNavigation` **[string][3]?** when a navigation is considered finished: `load`, `domcontentloaded`, or `networkidle`.
105+
* `blockResources` **[Array][15]<[string][3]>?** resource types to skip loading: `'image'`, `'media'`, `'font'`, `'stylesheet'`. Matching requests are aborted before they reach the network.
105106

106107

107108

‎docs/helpers/Moli.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Type: [object][4]
9191
* `pollInterval` **[number][8]?** interval between checks while waiting, in milliseconds.
9292
* `getPageTimeout` **[number][8]?** maximum time to wait for a page to load, in seconds.
9393
* `waitForNavigation` **[string][3]?** when a navigation is considered finished: `load`, `domcontentloaded`, or `networkidle`.
94+
* `blockResources` **[Array][15]<[string][3]>?** resource types to skip loading: `'image'`, `'media'`, `'font'`, `'stylesheet'`. Matching requests are aborted before they reach the network.
9495

9596

9697

‎docs/helpers/Obscura.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Type: [object][4]
118118
* `pollInterval` **[number][8]?** interval between checks while waiting, in milliseconds.
119119
* `getPageTimeout` **[number][8]?** maximum time to wait for a page to load, in seconds.
120120
* `waitForNavigation` **[string][3]?** when a navigation is considered finished: `load`, `domcontentloaded`, or `networkidle`.
121+
* `blockResources` **[Array][15]<[string][3]>?** resource types to skip loading: `'image'`, `'media'`, `'font'`, `'stylesheet'`. Matching requests are aborted before they reach the network.
121122

122123

123124

‎docs/helpers/Playwright.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Type: [object][8]
7979
* `bypassCSP` **[boolean][26]?** bypass Content Security Policy or CSP
8080
* `highlightElement` **[boolean][26]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
8181
* `visibleLocator` **[boolean][26]?** append [`visible()`][49] to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to steps that must reach hidden elements: `grab*` methods, `scrollTo`, `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found.
82+
* `blockResources` **[Array][9]<[string][7]>?** resource types to skip loading: `'image'`, `'media'`, `'font'`, `'stylesheet'`. Matching requests are aborted before they reach the network.
8283
* `recordHar` **[object][8]?** record HAR and will be saved to `output/har`. See more of [HAR options][3].
8384
* `testIdAttribute` **[string][7]?** locate elements based on the testIdAttribute. See more of [locate by test id][50].
8485
* `storageState` **([string][7] | [object][8])?** Playwright storage state (path to JSON file or object)

‎docs/helpers/Puppeteer.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Type: [object][6]
5959
* `browser` **[string][5]?** can be changed to `firefox` when using [puppeteer-firefox][2].
6060
* `chrome` **[object][6]?** pass additional [Puppeteer run options][31].
6161
* `highlightElement` **[boolean][25]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
62+
* `blockResources` **[Array][18]<[string][5]>?** resource types to skip loading: `'image'`, `'media'`, `'font'`, `'stylesheet'`. Matching requests are aborted before they reach the network.
6263

6364
## findElement
6465

‎docs/helpers/WebDriver.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Type: [object][19]
3232
* `url` **[string][18]** base url of website to be tested.
3333
* `browser` **[string][18]** Browser in which to perform testing.
3434
* `bidiProtocol` **[boolean][34]?** WebDriver Bidi Protocol. Default: false. More info: [https://webdriver.io/docs/api/webdriverBidi/][38]
35+
* `blockResources` **[Array][30]<[string][18]>?** resource types to skip loading: `'image'`, `'media'`, `'font'`, `'stylesheet'`. Matching requests are aborted before they reach the network.
3536
* `basicAuth` **[string][18]?** (optional) the basic authentication to pass to base url. Example: {username: 'username', password: 'password'}
3637
* `host` **[string][18]?** WebDriver host to connect.
3738
* `port` **[number][23]?** WebDriver port to connect.

‎lib/helper/Appium.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ class Appium extends Webdriver {
314314
]
315315
}
316316

317+
async _blockResources() {}
318+
317319
async _startBrowser() {
318320
if (this.appiumV2 === true) {
319321
this.options.capabilities = this._convertAppiumV2Caps(this.options.capabilities)

‎lib/helper/CDPBrowser.js‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ariaSnapshotFromAXNodes } from '../aria.js'
1212
import Locator from '../locator.js'
1313
import output from '../output.js'
1414
import store from '../store.js'
15-
import { xpathLocator, normalizePath, resolveUrl, toCamelCase, convertCssPropertiesToCamelCase, normalizeSpacesInString, fileExists, base64EncodeFile, getMimeType, isFile, isWindows, screenshotOutputFolder } from '../utils.js'
15+
import { ucfirst, xpathLocator, normalizePath, resolveUrl, toCamelCase, convertCssPropertiesToCamelCase, normalizeSpacesInString, fileExists, base64EncodeFile, getMimeType, isFile, isWindows, screenshotOutputFolder } from '../utils.js'
1616
import ElementNotFound from './errors/ElementNotFound.js'
1717
import MultipleElementsFound from './errors/MultipleElementsFound.js'
1818
import { includes as stringIncludes } from '../assert/include.js'
@@ -24,7 +24,7 @@ import WebElement from '../element/WebElement.js'
2424
import CDPElementHandle from './extras/CDPElementHandle.js'
2525
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
2626
import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript, clipboardExpression } from './extras/clipboard.js'
27-
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, flushNetworkTraffics } from './network/actions.js'
27+
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, flushNetworkTraffics, validateBlockResources } from './network/actions.js'
2828
import { assembleApng, isPng } from './extras/apngAssembler.js'
2929

3030
/**
@@ -45,6 +45,7 @@ import { assembleApng, isPng } from './extras/apngAssembler.js'
4545
* @prop {number} [pollInterval=25] - interval between checks while waiting, in milliseconds.
4646
* @prop {number} [getPageTimeout=30] - maximum time to wait for a page to load, in seconds.
4747
* @prop {string} [waitForNavigation=load] - when a navigation is considered finished: `load`, `domcontentloaded`, or `networkidle`.
48+
* @prop {string[]} [blockResources] - resource types to skip loading: `'image'`, `'media'`, `'font'`, `'stylesheet'`. Matching requests are aborted before they reach the network.
4849
*/
4950
const config = {}
5051

@@ -127,8 +128,10 @@ class CDPBrowser extends Helper {
127128
pollInterval: 25,
128129
getPageTimeout: 30,
129130
waitForNavigation: 'load',
131+
blockResources: [],
130132
...config,
131133
}
134+
validateBlockResources(this.options.blockResources)
132135
this.cdp = null
133136
this.sessionId = null
134137
this.targetId = null
@@ -359,6 +362,13 @@ class CDPBrowser extends Helper {
359362
const endpoint = await this._resolveEndpoint()
360363
this.cdp = new CDPConnection(endpoint, { headers: this.options.headers, timeout: this.options.getPageTimeout * 1000 })
361364
await this.cdp.connect()
365+
this.cdp.on('Fetch.requestPaused', (params, sessionId) => {
366+
if (this.options.blockResources.includes(params.resourceType.toLowerCase())) {
367+
this.cdp.send('Fetch.failRequest', { requestId: params.requestId, errorReason: 'BlockedByClient' }, sessionId).catch(err => this.debugSection('blockResources', err.message))
368+
return
369+
}
370+
this.cdp.send('Fetch.continueRequest', { requestId: params.requestId }, sessionId).catch(err => this.debugSection('blockResources', err.message))
371+
})
362372
}
363373

364374
_beforeStep() {
@@ -384,6 +394,9 @@ class CDPBrowser extends Helper {
384394
await this.cdp.send('Page.enable', {}, this.sessionId).catch(() => null)
385395
await this.cdp.send('Runtime.enable', {}, this.sessionId).catch(() => null)
386396
await this.cdp.send('Page.setLifecycleEventsEnabled', { enabled: true }, this.sessionId).catch(() => null)
397+
if (this.options.blockResources.length) {
398+
await this.cdp.send('Fetch.enable', { patterns: this.options.blockResources.map(type => ({ urlPattern: '*', resourceType: ucfirst(type) })) }, this.sessionId)
399+
}
387400
await this._probeCapabilities()
388401
}
389402

0 commit comments

Comments
 (0)