Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/selenium-webdriver/bidi/browsingContext.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
const { InvalidArgumentError, NoSuchFrameError } = require('../lib/error')
19
const { BrowsingContextInfo } = require('./browsingContextTypes')
20
const { SerializationOptions, ReferenceValue, RemoteValue } = require('./protocolValue')
21
const { WebElement } = require('../lib/webdriver')
22
const { CaptureScreenshotParameters } = require('./captureScreenshotParameters')
23
const { CreateContextParameters } = require('./createContextParameters')
24
25
/**
26
* Represents the locator to locate nodes in the browsing context.
27
* Described in https://w3c.github.io/webdriver-bidi/#type-browsingContext-Locator.
28
*/
29
class Locator {
30
static Type = Object.freeze({
31
CSS: 'css',
32
INNER_TEXT: 'innerText',
33
XPATH: 'xpath',
34
})
35
36
#type
37
#value
38
#ignoreCase
39
#matchType
40
#maxDepth
41
42
constructor(type, value, ignoreCase = undefined, matchType = undefined, maxDepth = undefined) {
43
this.#type = type
44
this.#value = value
45
this.#ignoreCase = ignoreCase
46
this.#matchType = matchType
47
this.#maxDepth = maxDepth
48
}
49
50
/**
51
* Creates a new Locator object with CSS selector type.
52
*
53
* @param {string} value - The CSS selector value.
54
* @returns {Locator} A new Locator object with CSS selector type.
55
*/
56
static css(value) {
57
return new Locator(Locator.Type.CSS, value)
58
}
59
60
/**
61
* Creates a new Locator object with the given XPath value.
62
*
63
* @param {string} value - The XPath value.
64
* @returns {Locator} A new Locator object.
65
*/
66
static xpath(value) {
67
return new Locator(Locator.Type.XPATH, value)
68
}
69
70
/**
71
* Creates a new Locator object with the specified inner text value.
72
*
73
* @param {string} value - The inner text value to locate.
74
* @param {boolean|undefined} [ignoreCase] - Whether to ignore the case when matching the inner text value.
75
* @param {string|undefined} [matchType] - The type of matching to perform (full or partial).
76
* @param {number|undefined} [maxDepth] - The maximum depth to search for the inner text value.
77
* @returns {Locator} A new Locator object with the specified inner text value.
78
*/
79
static innerText(value, ignoreCase = undefined, matchType = undefined, maxDepth = undefined) {
80
return new Locator(Locator.Type.INNER_TEXT, value, ignoreCase, matchType, maxDepth)
81
}
82
83
toMap() {
84
const map = new Map()
85
86
map.set('type', this.#type.toString())
87
map.set('value', this.#value)
88
map.set('ignoreCase', this.#ignoreCase)
89
map.set('matchType', this.#matchType)
90
map.set('maxDepth', this.#maxDepth)
91
92
return map
93
}
94
}
95
96
/**
97
* Represents the contains under BrowsingContext module commands.
98
* Described in https://w3c.github.io/webdriver-bidi/#module-browsingContext
99
* Each browsing context command requires a browsing context id.
100
* Hence, this class represent browsing context lifecycle.
101
*/
102
class BrowsingContext {
103
constructor(driver) {
104
this._driver = driver
105
}
106
107
/**
108
* @returns id
109
*/
110
get id() {
111
return this._id
112
}
113
114
async init({ browsingContextId = undefined, type = undefined, createParameters = undefined }) {
115
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
116
throw Error('WebDriver instance must support BiDi protocol')
117
}
118
119
if (browsingContextId === undefined && type === undefined && createParameters === undefined) {
120
throw Error('Either BrowsingContextId or Type or CreateParameters must be provided')
121
}
122
123
if (type === undefined && createParameters !== undefined) {
124
throw Error('Type must be provided with CreateParameters')
125
}
126
127
if (type !== undefined && !['window', 'tab'].includes(type)) {
128
throw Error(`Valid types are 'window' & 'tab'. Received: ${type}`)
129
}
130
131
this.bidi = await this._driver.getBidi()
132
this._id =
133
browsingContextId === undefined
134
? (await this.create(type, createParameters))['result']['context']
135
: browsingContextId
136
}
137
138
/**
139
* Creates a browsing context for the given type with the given parameters
140
*/
141
async create(type, createParameters = undefined) {
142
if (createParameters !== undefined && (!createParameters) instanceof CreateContextParameters) {
143
throw Error(`Pass in the instance of CreateContextParameters. Received: ${createParameters}`)
144
}
145
146
let parameters = new Map()
147
parameters.set('type', type)
148
149
if (createParameters !== undefined) {
150
createParameters.asMap().forEach((value, key) => {
151
parameters.set(key, value)
152
})
153
}
154
155
const params = {
156
method: 'browsingContext.create',
157
params: Object.fromEntries(parameters),
158
}
159
return await this.bidi.send(params)
160
}
161
162
/**
163
* @param url the url to navigate to
164
* @param readinessState type of readiness state: "none" / "interactive" / "complete"
165
* @returns NavigateResult object
166
*/
167
async navigate(url, readinessState = undefined) {
168
if (readinessState !== undefined && !['none', 'interactive', 'complete'].includes(readinessState)) {
169
throw Error(`Valid readiness states are 'none', 'interactive' & 'complete'. Received: ${readinessState}`)
170
}
171
172
const params = {
173
method: 'browsingContext.navigate',
174
params: {
175
context: this._id,
176
url: url,
177
wait: readinessState,
178
},
179
}
180
const navigateResult = (await this.bidi.send(params))['result']
181
182
return new NavigateResult(navigateResult['url'], navigateResult['navigation'])
183
}
184
185
/**
186
* @param maxDepth the max depth of the descendents of browsing context tree
187
* @returns BrowsingContextInfo object
188
*/
189
async getTree(maxDepth = undefined) {
190
const params = {
191
method: 'browsingContext.getTree',
192
params: {
193
root: this._id,
194
maxDepth: maxDepth,
195
},
196
}
197
198
let result = await this.bidi.send(params)
199
if ('error' in result) {
200
throw Error(result['error'])
201
}
202
203
result = result['result']['contexts'][0]
204
return new BrowsingContextInfo(result['context'], result['url'], result['children'], result['parent'])
205
}
206
207
/**
208
* @returns {Promise<Array<BrowsingContextInfo>>} A Promise that resolves to an array of BrowsingContextInfo objects representing the top-level browsing contexts.
209
*/
210
async getTopLevelContexts() {
211
const params = {
212
method: 'browsingContext.getTree',
213
params: {},
214
}
215
216
let result = await this.bidi.send(params)
217
if ('error' in result) {
218
throw Error(result['error'])
219
}
220
221
const contexts = result['result']['contexts']
222
const browsingContexts = contexts.map((context) => {
223
return new BrowsingContextInfo(context['id'], context['url'], context['children'], context['parent'])
224
})
225
return browsingContexts
226
}
227
228
/**
229
* Closes the browsing context
230
* @returns {Promise<void>}
231
*/
232
async close() {
233
const params = {
234
method: 'browsingContext.close',
235
params: {
236
context: this._id,
237
},
238
}
239
await this.bidi.send(params)
240
}
241
242
/**
243
* Prints PDF of the webpage
244
* @param options print options given by the user
245
* @returns PrintResult object
246
*/
247
async printPage(options = {}) {
248
let params = {
249
method: 'browsingContext.print',
250
// Setting default values for parameters
251
params: {
252
context: this._id,
253
background: false,
254
margin: {
255
bottom: 1.0,
256
left: 1.0,
257
right: 1.0,
258
top: 1.0,
259
},
260
orientation: 'portrait',
261
page: {
262
height: 27.94,
263
width: 21.59,
264
},
265
pageRanges: [],
266
scale: 1.0,
267
shrinkToFit: true,
268
},
269
}
270
271
// Updating parameter values based on the options passed
272
params.params = this._driver.validatePrintPageParams(options, params.params)
273
274
const response = await this.bidi.send(params)
275
return new PrintResult(response.result.data)
276
}
277
278
/**
279
* Captures a screenshot of the browsing context.
280
*
281
* @param {CaptureScreenshotParameters|undefined} [captureScreenshotParameters] - Optional parameters for capturing the screenshot.
282
* @returns {Promise<string>} - A promise that resolves to the base64-encoded string representation of the captured screenshot.
283
* @throws {InvalidArgumentError} - If the provided captureScreenshotParameters is not an instance of CaptureScreenshotParameters.
284
*/
285
async captureScreenshot(captureScreenshotParameters = undefined) {
286
if (
287
captureScreenshotParameters !== undefined &&
288
!(captureScreenshotParameters instanceof CaptureScreenshotParameters)
289
) {
290
throw new InvalidArgumentError(
291
`Pass in a CaptureScreenshotParameters object. Received: ${captureScreenshotParameters}`,
292
)
293
}
294
295
const screenshotParams = new Map()
296
screenshotParams.set('context', this._id)
297
if (captureScreenshotParameters !== undefined) {
298
captureScreenshotParameters.asMap().forEach((value, key) => {
299
screenshotParams.set(key, value)
300
})
301
}
302
303
let params = {
304
method: 'browsingContext.captureScreenshot',
305
params: Object.fromEntries(screenshotParams),
306
}
307
308
const response = await this.bidi.send(params)
309
this.checkErrorInScreenshot(response)
310
return response['result']['data']
311
}
312
313
async captureBoxScreenshot(x, y, width, height) {
314
let params = {
315
method: 'browsingContext.captureScreenshot',
316
params: {
317
context: this._id,
318
clip: {
319
type: 'box',
320
x: x,
321
y: y,
322
width: width,
323
height: height,
324
},
325
},
326
}
327
328
const response = await this.bidi.send(params)
329
this.checkErrorInScreenshot(response)
330
return response['result']['data']
331
}
332
333
/**
334
* Captures a screenshot of a specific element within the browsing context.
335
* @param {string} sharedId - The shared ID of the element to capture.
336
* @param {string} [handle] - The handle of the element to capture (optional).
337
* @returns {Promise<string>} A promise that resolves to the base64-encoded screenshot data.
338
*/
339
async captureElementScreenshot(sharedId, handle = undefined) {
340
let params = {
341
method: 'browsingContext.captureScreenshot',
342
params: {
343
context: this._id,
344
clip: {
345
type: 'element',
346
element: {
347
sharedId: sharedId,
348
handle: handle,
349
},
350
},
351
},
352
}
353
354
const response = await this.bidi.send(params)
355
this.checkErrorInScreenshot(response)
356
return response['result']['data']
357
}
358
359
checkErrorInScreenshot(response) {
360
if ('error' in response) {
361
const { error, msg } = response
362
363
switch (error) {
364
case 'invalid argument':
365
throw new InvalidArgumentError(msg)
366
367
case 'no such frame':
368
throw new NoSuchFrameError(msg)
369
}
370
}
371
}
372
373
/**
374
* Activates and focuses the top-level browsing context.
375
* @returns {Promise<void>} A promise that resolves when the browsing context is activated.
376
* @throws {Error} If there is an error while activating the browsing context.
377
*/
378
async activate() {
379
const params = {
380
method: 'browsingContext.activate',
381
params: {
382
context: this._id,
383
},
384
}
385
386
let result = await this.bidi.send(params)
387
if ('error' in result) {
388
throw Error(result['error'])
389
}
390
}
391
392
/**
393
* Handles a user prompt in the browsing context.
394
*
395
* @param {boolean} [accept] - Optional. Indicates whether to accept or dismiss the prompt.
396
* @param {string} [userText] - Optional. The text to enter.
397
* @throws {Error} If an error occurs while handling the user prompt.
398
*/
399
async handleUserPrompt(accept = undefined, userText = undefined) {
400
const params = {
401
method: 'browsingContext.handleUserPrompt',
402
params: {
403
context: this._id,
404
accept: accept,
405
userText: userText,
406
},
407
}
408
409
let result = await this.bidi.send(params)
410
if ('error' in result) {
411
throw Error(result['error'])
412
}
413
}
414
415
/**
416
* Reloads the current browsing context.
417
*
418
* @param {boolean} [ignoreCache] - Whether to ignore the cache when reloading.
419
* @param {string} [readinessState] - The readiness state to wait for before returning.
420
* Valid readiness states are 'none', 'interactive', and 'complete'.
421
* @returns {Promise<NavigateResult>} - A promise that resolves to the result of the reload operation.
422
* @throws {Error} - If an invalid readiness state is provided.
423
*/
424
async reload(ignoreCache = undefined, readinessState = undefined) {
425
if (readinessState !== undefined && !['none', 'interactive', 'complete'].includes(readinessState)) {
426
throw Error(`Valid readiness states are 'none', 'interactive' & 'complete'. Received: ${readinessState}`)
427
}
428
429
const params = {
430
method: 'browsingContext.reload',
431
params: {
432
context: this._id,
433
ignoreCache: ignoreCache,
434
wait: readinessState,
435
},
436
}
437
const navigateResult = (await this.bidi.send(params))['result']
438
439
return new NavigateResult(navigateResult['url'], navigateResult['navigation'])
440
}
441
442
/**
443
* Sets the viewport size and device pixel ratio for the browsing context.
444
* @param {number} width - The width of the viewport.
445
* @param {number} height - The height of the viewport.
446
* @param {number} [devicePixelRatio] - The device pixel ratio (optional)
447
* @throws {Error} If an error occurs while setting the viewport.
448
*/
449
async setViewport(width, height, devicePixelRatio = undefined) {
450
const params = {
451
method: 'browsingContext.setViewport',
452
params: {
453
context: this._id,
454
viewport: { width: width, height: height },
455
devicePixelRatio: devicePixelRatio,
456
},
457
}
458
let result = await this.bidi.send(params)
459
if ('error' in result) {
460
throw Error(result['error'])
461
}
462
}
463
464
/**
465
* Traverses the browsing context history by a given delta.
466
*
467
* @param {number} delta - The delta value to traverse the history. A positive value moves forward, while a negative value moves backward.
468
* @returns {Promise<void>} - A promise that resolves when the history traversal is complete.
469
*/
470
async traverseHistory(delta) {
471
const params = {
472
method: 'browsingContext.traverseHistory',
473
params: {
474
context: this._id,
475
delta: delta,
476
},
477
}
478
await this.bidi.send(params)
479
}
480
481
/**
482
* Moves the browsing context forward by one step in the history.
483
* @returns {Promise<void>} A promise that resolves when the browsing context has moved forward.
484
*/
485
async forward() {
486
await this.traverseHistory(1)
487
}
488
489
/**
490
* Navigates the browsing context to the previous page in the history.
491
* @returns {Promise<void>} A promise that resolves when the navigation is complete.
492
*/
493
async back() {
494
await this.traverseHistory(-1)
495
}
496
497
/**
498
* Locates nodes in the browsing context.
499
*
500
* @param {Locator} locator - The locator object used to locate the nodes.
501
* @param {number} [maxNodeCount] - The maximum number of nodes to locate (optional).
502
* @param {string} [sandbox] - The sandbox name for locating nodes (optional).
503
* @param {SerializationOptions} [serializationOptions] - The serialization options for locating nodes (optional).
504
* @param {ReferenceValue[]} [startNodes] - The array of start nodes for locating nodes (optional).
505
* @returns {Promise<RemoteValue[]>} - A promise that resolves to the arrays of located nodes.
506
* @throws {Error} - If the locator is not an instance of Locator.
507
* @throws {Error} - If the serializationOptions is provided but not an instance of SerializationOptions.
508
* @throws {Error} - If the startNodes is provided but not an array of ReferenceValue objects.
509
* @throws {Error} - If any of the startNodes is not an instance of ReferenceValue.
510
*/
511
async locateNodes(
512
locator,
513
maxNodeCount = undefined,
514
sandbox = undefined,
515
serializationOptions = undefined,
516
startNodes = undefined,
517
) {
518
if (!(locator instanceof Locator)) {
519
throw Error(`Pass in a Locator object. Received: ${locator}`)
520
}
521
522
if (serializationOptions !== undefined && !(serializationOptions instanceof SerializationOptions)) {
523
throw Error(`Pass in SerializationOptions object. Received: ${serializationOptions} `)
524
}
525
526
if (startNodes !== undefined && !Array.isArray(startNodes)) {
527
throw Error(`Pass in an array of ReferenceValue objects. Received: ${startNodes}`)
528
}
529
530
let startNodesSerialized = undefined
531
532
if (startNodes !== undefined && Array.isArray(startNodes)) {
533
startNodesSerialized = []
534
startNodes.forEach((node) => {
535
if (!(node instanceof ReferenceValue)) {
536
throw Error(`Pass in a ReferenceValue object. Received: ${node}`)
537
} else {
538
startNodesSerialized.push(node.asMap())
539
}
540
})
541
}
542
543
const params = {
544
method: 'browsingContext.locateNodes',
545
params: {
546
context: this._id,
547
locator: Object.fromEntries(locator.toMap()),
548
maxNodeCount: maxNodeCount,
549
sandbox: sandbox,
550
serializationOptions: serializationOptions,
551
startNodes: startNodesSerialized,
552
},
553
}
554
555
let response = await this.bidi.send(params)
556
if ('error' in response) {
557
throw Error(response['error'])
558
}
559
560
const nodes = response.result.nodes
561
const remoteValues = []
562
563
nodes.forEach((node) => {
564
remoteValues.push(new RemoteValue(node))
565
})
566
return remoteValues
567
}
568
569
/**
570
* Locates a single node in the browsing context.
571
*
572
* @param {Locator} locator - The locator used to find the node.
573
* @param {string} [sandbox] - The sandbox of the node (optional).
574
* @param {SerializationOptions} [serializationOptions] - The serialization options for the node (optional).
575
* @param {Array} [startNodes] - The starting nodes for the search (optional).
576
* @returns {Promise<RemoteValue>} - A promise that resolves to the located node.
577
*/
578
async locateNode(locator, sandbox = undefined, serializationOptions = undefined, startNodes = undefined) {
579
const elements = await this.locateNodes(locator, 1, sandbox, serializationOptions, startNodes)
580
return elements[0]
581
}
582
583
async locateElement(locator) {
584
const elements = await this.locateNodes(locator, 1)
585
return new WebElement(this._driver, elements[0].sharedId)
586
}
587
588
async locateElements(locator) {
589
const elements = await this.locateNodes(locator)
590
591
let webElements = []
592
elements.forEach((element) => {
593
webElements.push(new WebElement(this._driver, element.sharedId))
594
})
595
return webElements
596
}
597
}
598
599
/**
600
* Represents the result of a navigation operation.
601
*/
602
class NavigateResult {
603
constructor(url, navigationId) {
604
this._url = url
605
this._navigationId = navigationId
606
}
607
608
/**
609
* Gets the URL of the navigated page.
610
* @returns {string} The URL of the navigated page.
611
*/
612
get url() {
613
return this._url
614
}
615
616
/**
617
* Gets the ID of the navigation operation.
618
* @returns {number} The ID of the navigation operation.
619
*/
620
get navigationId() {
621
return this._navigationId
622
}
623
}
624
625
/**
626
* Represents a print result.
627
*/
628
class PrintResult {
629
constructor(data) {
630
this._data = data
631
}
632
633
/**
634
* Gets the data associated with the print result.
635
* @returns {any} The data associated with the print result.
636
*/
637
get data() {
638
return this._data
639
}
640
}
641
642
/**
643
* initiate browsing context instance and return
644
* @param driver
645
* @param browsingContextId The browsing context of current window/tab
646
* @param type "window" or "tab"
647
* @param createParameters The parameters for creating a new browsing context
648
* @returns {Promise<BrowsingContext>}
649
*/
650
async function getBrowsingContextInstance(
651
driver,
652
{ browsingContextId = undefined, type = undefined, createParameters = undefined },
653
) {
654
let instance = new BrowsingContext(driver)
655
await instance.init({ browsingContextId, type, createParameters })
656
return instance
657
}
658
659
module.exports = getBrowsingContextInstance
660
module.exports.Locator = Locator
661
662