-
-
Notifications
You must be signed in to change notification settings - Fork 59
feat: Alpha Support for Multi-remote Browser #1985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
dprevost-LMI
wants to merge
30
commits into
webdriverio:main
Choose a base branch
from
dprevost-LMI:support-multiremote
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,028
−117
Draft
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
608b0af
Draft first working multi-remote case
dprevost-LMI cf8088b
useful command to check everything (to revert)
dprevost-LMI 6fd69aa
Working multiple targets and failures
dprevost-LMI e70bd6b
Working with multiple provided values
dprevost-LMI 2b5b5e5
Add comment on future plan
dprevost-LMI 24e7578
Working case of optimize waitUntil running per instance + failure msg
dprevost-LMI 2846f34
working consolidation of waitUntil + formatFailureMessage
dprevost-LMI dbce283
Add doc for supported case for alpha version + some future plan
dprevost-LMI 938f87c
code review
dprevost-LMI 665109c
Add multi-remote alternative doc with Parametrized Tests
dprevost-LMI 60d8aa5
Add Direct Instance Access alternative + fix english grammar
dprevost-LMI 617a0aa
Code review
dprevost-LMI ea00149
Add multi-remote typing support
dprevost-LMI 8fd88dd
Add missing unit tests for waitUntilResult
dprevost-LMI fc617b8
fix linting
dprevost-LMI ce71bd9
add unit test for formatMessage
dprevost-LMI e4e69ab
Add UT for multiRemoteUtil
dprevost-LMI 8da4813
Code review
dprevost-LMI 82c7caf
fix rebranching
dprevost-LMI d0bb3ad
Revert undesired formating changes
dprevost-LMI 27c1dcd
revert undesired changes
dprevost-LMI bf8e852
Code review
dprevost-LMI 594ad71
revert more undesired changes
dprevost-LMI 870b248
Final code review
dprevost-LMI 3fa95d5
Fix rebase
dprevost-LMI 1fd463b
Add missing typing test for toBeDisplayed
dprevost-LMI 8f91bf3
Add doc and future task for multi-remote
dprevost-LMI a848e2d
Apply suggestions from code review
dprevost-LMI 3fcc76d
fix: backport isNot ambiguous failure message from PR #1987
dprevost-LMI 20eefac
fix: Stop altering arg variable + more robust and clear concatenation
dprevost-LMI File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| # MultiRemote Support (Alpha) | ||
|
|
||
| Multi-remote support is in active development. | ||
|
|
||
| ## Usage | ||
|
|
||
| By default, multi-remote matchers fetch data (e.g., `getTitle`) from all remotes, simplifying tests where browsers share the same behavior. | ||
|
|
||
| Use the typed global constants: | ||
| ```ts | ||
| import { multiremotebrowser as multiRemoteBrowser } from '@wdio/globals' | ||
| ... | ||
| await expect(multiRemoteBrowser).toHaveTitle('...') | ||
| ``` | ||
| Note: `multiRemoteBrowser` is used in examples pending a planned rename. | ||
|
|
||
|
|
||
| Assuming the following WebdriverIO multi-remote configuration: | ||
| ```ts | ||
| export const config: WebdriverIO.MultiremoteConfig = { | ||
| ... | ||
| capabilities: { | ||
| myChromeBrowser: { | ||
| capabilities: { | ||
| browserName: 'chrome', | ||
| 'goog:chromeOptions': { args: ['--headless'] } | ||
| } | ||
| }, | ||
| myFirefoxBrowser: { | ||
| capabilities: { | ||
| browserName: 'firefox', | ||
| 'moz:firefoxOptions': { args: ['-headless'] } | ||
| } | ||
| } | ||
| }, | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| And an `it` test like: | ||
| ```ts | ||
| import { multiremotebrowser as multiRemoteBrowser } from '@wdio/globals' | ||
|
|
||
| it('should have title "My Site Title"', async function () { | ||
| await multiRemoteBrowser.url('https://mysite.com') | ||
|
|
||
| // ... assertions | ||
| }) | ||
| ``` | ||
|
|
||
|
|
||
| ## Single Expected Value | ||
| To test all remotes against the same value, pass a single expected value. | ||
| ```ts | ||
| await expect(multiRemoteBrowser).toHaveTitle('My Site Title') | ||
| ``` | ||
|
|
||
| ## Multiple Expected Values | ||
| For differing remotes, pass an array of expected values. | ||
| - Note: Values must match the configuration order. | ||
| ```ts | ||
| await expect(multiRemoteBrowser).toHaveTitle(['My Chrome Site Title', 'My Firefox Site Title']) | ||
| ``` | ||
|
|
||
| ## **NOT IMPLEMENTED** Per Remote Expected Value | ||
| To test specific remotes, map instance names to expected values. | ||
|
|
||
| ```ts | ||
| // Test both defined remotes with specific values | ||
| await expect(multiRemoteBrowser).toHaveTitle({ | ||
| 'myChromeBrowser' : 'My Chrome Site Title', | ||
| 'myFirefoxBrowser' : 'My Firefox Site Title' | ||
| }) | ||
| ``` | ||
|
|
||
| To assert a single remote and skip others: | ||
| ```ts | ||
| await expect(multiRemoteBrowser).toHaveTitle({ | ||
| 'myFirefoxBrowser' : 'My Firefox Site Title' | ||
| }) | ||
| ``` | ||
|
|
||
| To assert all remotes with a default value, overriding specific ones: | ||
| ```ts | ||
| await expect(multiRemoteBrowser).toHaveTitle({ | ||
| default : 'My Default Site Title', | ||
| 'myFirefoxBrowser' : 'My Firefox Site Title' | ||
| }) | ||
| ``` | ||
|
|
||
| ## Limitations | ||
| - Options (e.g., `StringOptions`) apply globally. | ||
| - Alpha support is limited to the `toHaveTitle` browser matcher. | ||
| - Element matchers are planned. | ||
| - Assertions currently throw on the first error. Future updates will report thrown errors as failures, and will only throw if all remotes fail. | ||
| - SoftAssertions, snapshot services and network matchers might come after. | ||
|
|
||
| ## Alternatives | ||
|
|
||
| Since multi-remote instances are standard browsers, you can also assert by iterating over the instance list. | ||
|
|
||
| ### Parameterized Tests | ||
| Using the parameterized feature of your test framework, you can iterate over the multi-remote instances. | ||
|
|
||
| Mocha Parameterized Example | ||
| ```ts | ||
| describe('Multiremote test', async () => { | ||
| multiRemoteBrowser.instances.forEach(function (instance) { | ||
| describe(`Test ${instance}`, function () { | ||
| it('should have title "My Site Title"', async function () { | ||
| const browser = multiRemoteBrowser.getInstance(instance) | ||
| await browser.url('https://mysite.com') | ||
|
|
||
| await expect(browser).toHaveTitle("My Site Title") | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
| ``` | ||
| ### Direct Instance Access (TypeScript) | ||
| By extending the WebdriverIO `namespace` in TypeScript (see [documentation](https://webdriver.io/docs/multiremote/#extending-typescript-types)), you can directly access each instance and use `expect` on them. | ||
|
|
||
| ```ts | ||
| it('should have title per browsers', async () => { | ||
| await multiRemoteBrowser.url('https://mysite.com') | ||
|
|
||
| await expect(multiRemoteBrowser.myChromeBrowser).toHaveTitle('My Chrome Site Title') | ||
| await expect(multiRemoteBrowser.myFirefoxBrowser).toHaveTitle('My Firefox Site Title') | ||
| }) | ||
| ``` | ||
|
|
||
| Required configuration: | ||
|
|
||
| File `type.d.ts` | ||
| ```ts | ||
| declare namespace WebdriverIO { | ||
| interface MultiRemoteBrowser { | ||
| myChromeBrowser: WebdriverIO.Browser | ||
| myFirefoxBrowser: WebdriverIO.Browser | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| In `tsconfig.json` | ||
| ```json | ||
| { | ||
| "compilerOptions": { | ||
| ... | ||
| }, | ||
| "include": [ | ||
| ... | ||
| "type.d.ts" | ||
| ] | ||
| } | ||
| ``` | ||
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,39 +1,66 @@ | ||
| import { waitUntil, enhanceError, compareText } from '../../utils.js' | ||
| import { compareText, waitUntilResultSucceed } from '../../utils.js' | ||
| import { DEFAULT_OPTIONS } from '../../constants.js' | ||
| import type { MaybeArray } from '../../util/multiRemoteUtil.js' | ||
| import { mapExpectedValueWithInstances } from '../../util/multiRemoteUtil.js' | ||
| import { formatFailureMessage } from '../../util/formatMessage.js' | ||
|
|
||
| type ExpectedValueType = string | RegExp | WdioAsymmetricMatcher<string> | ||
|
|
||
| export async function toHaveTitle( | ||
| this: ExpectWebdriverIO.MatcherContext, | ||
| browsers: WebdriverIO.MultiRemoteBrowser, | ||
| expectedValues: MaybeArray<ExpectedValueType>, | ||
| options?: ExpectWebdriverIO.StringOptions, | ||
| ): Promise<ExpectWebdriverIO.AssertionResult> | ||
| export async function toHaveTitle( | ||
| this: ExpectWebdriverIO.MatcherContext, | ||
| browser: WebdriverIO.Browser, | ||
| expectedValue: string | RegExp | WdioAsymmetricMatcher<string>, | ||
| options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS | ||
| expectedValue: ExpectedValueType, | ||
| options?: ExpectWebdriverIO.StringOptions, | ||
| ): Promise<ExpectWebdriverIO.AssertionResult> | ||
| export async function toHaveTitle( | ||
| this: ExpectWebdriverIO.MatcherContext, | ||
| browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, | ||
| expectedValue: MaybeArray<ExpectedValueType>, | ||
| options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS, | ||
| ) { | ||
| const isNot = this.isNot | ||
| const { expectation = 'title', verb = 'have' } = this | ||
| const { expectation = 'title', verb = 'have', isNot } = this | ||
| const context = { expectation, verb, isNot, isMultiRemote: browser.isMultiremote } | ||
|
|
||
| await options.beforeAssertion?.({ | ||
| matcherName: 'toHaveTitle', | ||
| expectedValue, | ||
| options, | ||
| }) | ||
|
|
||
| let actual | ||
| const pass = await waitUntil(async () => { | ||
| actual = await browser.getTitle() | ||
| const browsersWithExpected = mapExpectedValueWithInstances(browser, expectedValue) | ||
|
|
||
| const conditions = Object.entries(browsersWithExpected).map(([instanceName, { browser, expectedValue: expected }]) => async () => { | ||
| const actual = await browser.getTitle() | ||
|
|
||
| return compareText(actual, expectedValue, options).result | ||
| }, isNot, options) | ||
| const result = compareText(actual, expected, options) | ||
| result.instance = instanceName | ||
| return result | ||
| }) | ||
|
|
||
| const conditionsResults = await waitUntilResultSucceed( | ||
| conditions, | ||
| isNot, | ||
| options, | ||
| ) | ||
|
|
||
| const message = enhanceError('window', expectedValue, actual, this, verb, expectation, '', options) | ||
| const result: ExpectWebdriverIO.AssertionResult = { | ||
| pass, | ||
| message: () => message | ||
| const message = formatFailureMessage('window', conditionsResults.results, context, '', options) | ||
| const assertionResult: ExpectWebdriverIO.AssertionResult = { | ||
| pass: conditionsResults.pass, | ||
| message: () => message, | ||
| } | ||
|
|
||
| await options.afterAssertion?.({ | ||
| matcherName: 'toHaveTitle', | ||
| expectedValue, | ||
| options, | ||
| result | ||
| result: assertionResult, | ||
| }) | ||
|
|
||
| return result | ||
| return assertionResult | ||
| } |
This file contains hidden or 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
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import type { Browser } from 'webdriverio' | ||
|
|
||
| export const toArray = <T>(value: T | T[] | MaybeArray<T>): T[] => (Array.isArray(value) ? value : [value]) | ||
|
|
||
| export type MaybeArray<T> = T | T[] | ||
|
|
||
| export const isMultiRemote = (browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser): browser is WebdriverIO.MultiRemoteBrowser => { | ||
| return (browser as WebdriverIO.MultiRemoteBrowser).isMultiremote === true | ||
| } | ||
|
|
||
| type BrowserWithExpected<T> = Record<string, { | ||
| browser: Browser; | ||
| expectedValue: T; | ||
| }> | ||
|
|
||
| export const mapExpectedValueWithInstances = <T>(browsers: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, expectedValues: T | MaybeArray<T>): BrowserWithExpected<T> => { | ||
| if (isMultiRemote(browsers)) { | ||
| if (Array.isArray(expectedValues)) { | ||
| if (expectedValues.length !== browsers.instances.length) { | ||
| throw new Error(`Expected values length (${expectedValues.length}) does not match number of browser instances (${browsers.instances.length}) in multi-remote setup.`) | ||
| } | ||
| } | ||
| // TODO multi-remote support: add support for object like { default: 'title', browserA: 'titleA', browserB: 'titleB' } later | ||
|
|
||
| const browsersWithExpected = browsers.instances.reduce((acc: BrowserWithExpected<T>, instance, index) => { | ||
| const browser = browsers.getInstance(instance) | ||
| const expectedValue: T = Array.isArray(expectedValues) ? expectedValues[index] : expectedValues | ||
| acc[instance] = { browser, expectedValue } | ||
| return acc | ||
| }, {}) | ||
| return browsersWithExpected | ||
| } | ||
|
|
||
| // TODO multi-remote support: using default could clash if someone use name default, to review later | ||
| return { default: { browser: browsers, expectedValue: expectedValues as T } } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does
defaultmean here?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would provide a default value to assert on all remotes. So instead of enumerating them all when you only have one exception, you can still offer one value for all and another for the exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if someone calls their browser
default? Also are there many occurrences where you have to provide an exception for a single browser? How about we settle on:And see what the community responds.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that was a concern I had, still thinking about it.
Excellent question. The idea was to provide "flexibility"
I like it, baby steps and more incremental features, I agree.
However, does your proposal also scope out the array approach?