remote-context-helper.js (27044B)
1 'use strict'; 2 3 // Requires: 4 // - /common/dispatcher/dispatcher.js 5 // - /common/utils.js 6 // - /common/get-host-info.sub.js if automagic conversion of origin names to 7 // URLs is used. 8 9 /** 10 * This provides a more friendly interface to remote contexts in dispatches.js. 11 * The goal is to make it easy to write multi-window/-frame/-worker tests where 12 * the logic is entirely in 1 test file and there is no need to check in any 13 * other file (although it is often helpful to check in files of JS helper 14 * functions that are shared across remote context). 15 * 16 * So for example, to test that history traversal works, we create a new window, 17 * navigate it to a new document, go back and then go forward. 18 * 19 * @example 20 * promise_test(async t => { 21 * const rcHelper = new RemoteContextHelper(); 22 * const rc1 = await rcHelper.addWindow(); 23 * const rc2 = await rc1.navigateToNew(); 24 * assert_equals(await rc2.executeScript(() => 'here'), 'here', 'rc2 is live'); 25 * rc2.historyBack(); 26 * assert_equals(await rc1.executeScript(() => 'here'), 'here', 'rc1 is live'); 27 * rc1.historyForward(); 28 * assert_equals(await rc2.executeScript(() => 'here'), 'here', 'rc2 is live'); 29 * }); 30 * 31 * Note on the correspondence between remote contexts and 32 * `RemoteContextWrapper`s. A remote context is entirely determined by its URL. 33 * So navigating away from one and then back again will result in a remote 34 * context that can be controlled by the same `RemoteContextWrapper` instance 35 * before and after navigation. Messages sent to a remote context while it is 36 * destroyed or in BFCache will be queued and processed if that that URL is 37 * navigated back to. 38 * 39 * Navigation: 40 * This framework does not keep track of the history of the frame tree and so it 41 * is up to the test script to keep track of what remote contexts are currently 42 * active and to keep references to the corresponding `RemoteContextWrapper`s. 43 * 44 * Any action that leads to navigation in the remote context must be executed 45 * using 46 * @see RemoteContextWrapper.navigate. 47 */ 48 49 { 50 const RESOURCES_PATH = 51 '/html/browsers/browsing-the-web/remote-context-helper/resources'; 52 const WINDOW_EXECUTOR_PATH = `${RESOURCES_PATH}/executor-window.py`; 53 const WORKER_EXECUTOR_PATH = `${RESOURCES_PATH}/executor-worker.js`; 54 55 /** 56 * Turns a string into an origin. If `origin` is null this will return the 57 * current document's origin. If `origin` contains not '/', this will attempt 58 * to use it as an index in `get_host_info()`. Otherwise returns the input 59 * origin. 60 * @private 61 * @param {string|null} origin The input origin. 62 * @return {string|null} The output origin. 63 * @throws {RangeError} is `origin` cannot be found in 64 * `get_host_info()`. 65 */ 66 function finalizeOrigin(origin) { 67 if (!origin) { 68 return location.origin; 69 } 70 if (!origin.includes('/')) { 71 const origins = get_host_info(); 72 if (origin in origins) { 73 return origins[origin]; 74 } else { 75 throw new RangeError( 76 `${origin} is not a key in the get_host_info() object`); 77 } 78 } 79 return origin; 80 } 81 82 /** 83 * @private 84 * @param {string} url 85 * @returns {string} Absolute url using `location` as the base. 86 */ 87 function makeAbsolute(url) { 88 return new URL(url, location).toString(); 89 } 90 91 async function fetchText(url) { 92 return fetch(url).then(r => r.text()); 93 } 94 95 /** 96 * Represents a configuration for a remote context executor. 97 */ 98 class RemoteContextConfig { 99 /** 100 * @param {Object} [options] 101 * @param {string} [options.origin] A URL or a key in `get_host_info()`. 102 * @see finalizeOrigin for how origins are handled. 103 * @param {string[]} [options.scripts] A list of script URLs. The current 104 * document will be used as the base for relative URLs. 105 * @param {[string, string][]} [options.headers] A list of pairs of name 106 * and value. The executor will be served with these headers set. 107 * @param {string} [options.startOn] If supplied, the executor will start 108 * when this event occurs, e.g. "pageshow", 109 * (@see window.addEventListener). This only makes sense for 110 * window-based executors, not worker-based. 111 * @param {string} [options.status] If supplied, the executor will pass 112 * this value in the "status" parameter to the executor. The default 113 * executor will default to a status code of 200, if the parameter is 114 * not supplied. 115 * @param {string} [options.urlType] Determines what kind of URL is used. Options: 116 * 'origin', the URL will be based on the origin; 117 * 'data' or 'blob', the URL will contains the document content in 118 * a 'data:' or 'blob:' URL; 'blank', the URL will be blank and the 119 * document content will be written to the initial empty document using 120 * `document.open()`, `document.write()`, and `document.close()`. If not 121 * supplied, the default is 'origin'. 122 */ 123 constructor( 124 {origin, scripts = [], headers = [], startOn, status, urlType} = {}) { 125 this.origin = origin; 126 this.scripts = scripts; 127 this.headers = headers; 128 this.startOn = startOn; 129 this.status = status; 130 this.urlType = urlType; 131 } 132 133 /** 134 * If `config` is not already a `RemoteContextConfig`, one is constructed 135 * using `config`. 136 * @private 137 * @param {object} [config] 138 * @returns 139 */ 140 static ensure(config) { 141 if (!config) { 142 return DEFAULT_CONTEXT_CONFIG; 143 } 144 return new RemoteContextConfig(config); 145 } 146 147 /** 148 * Merges `this` with another `RemoteContextConfig` and to give a new 149 * `RemoteContextConfig`. `origin` is replaced by the other if present, 150 * `headers` and `scripts` are concatenated with `this`'s coming first. 151 * @param {RemoteContextConfig} extraConfig 152 * @returns {RemoteContextConfig} 153 */ 154 merged(extraConfig) { 155 let origin = this.origin; 156 if (extraConfig.origin) { 157 origin = extraConfig.origin; 158 } 159 let startOn = this.startOn; 160 if (extraConfig.startOn) { 161 startOn = extraConfig.startOn; 162 } 163 let status = this.status; 164 if (extraConfig.status) { 165 status = extraConfig.status; 166 } 167 let urlType = this.urlType; 168 if (extraConfig.urlType) { 169 urlType = extraConfig.urlType; 170 } 171 const headers = this.headers.concat(extraConfig.headers); 172 const scripts = this.scripts.concat(extraConfig.scripts); 173 return new RemoteContextConfig( 174 {origin, headers, scripts, startOn, status, urlType}); 175 } 176 177 /** 178 * Creates a URL for an executor based on this config. 179 * @param {string} uuid The unique ID of the executor. 180 * @param {boolean} isWorker If true, the executor will be Worker. If false, 181 * it will be a HTML document. 182 * @returns {string|Blob|undefined} 183 */ 184 async createExecutorUrl(uuid, isWorker) { 185 const origin = finalizeOrigin(this.origin); 186 const url = new URL( 187 isWorker ? WORKER_EXECUTOR_PATH : WINDOW_EXECUTOR_PATH, origin); 188 189 // UUID is needed for executor. 190 url.searchParams.append('uuid', uuid); 191 192 if (this.headers) { 193 addHeaders(url, this.headers); 194 } 195 for (const script of this.scripts) { 196 url.searchParams.append('script', makeAbsolute(script)); 197 } 198 199 if (this.startOn) { 200 url.searchParams.append('startOn', this.startOn); 201 } 202 203 if (this.status) { 204 url.searchParams.append('status', this.status); 205 } 206 207 const urlType = this.urlType || 'origin'; 208 switch (urlType) { 209 case 'origin': 210 case 'blank': 211 return url.href; 212 case 'data': 213 return `data:text/html;base64,${btoa(await fetchText(url.href))}`; 214 case 'blob': 215 return URL.createObjectURL( 216 new Blob([await fetchText(url.href)], {type: 'text/html'})); 217 default: 218 throw TypeError(`Invalid urlType: ${urlType}`); 219 }; 220 } 221 } 222 223 /** 224 * The default `RemoteContextConfig` to use if none is supplied. It has no 225 * origin, headers or scripts. 226 * @constant {RemoteContextConfig} 227 */ 228 const DEFAULT_CONTEXT_CONFIG = new RemoteContextConfig(); 229 230 /** 231 * Attaches header to the URL. See 232 * https://web-platform-tests.org/writing-tests/server-pipes.html#headers 233 * @param {string} url the URL to which headers should be attached. 234 * @param {[[string, string]]} headers a list of pairs of head-name, 235 * header-value. 236 */ 237 function addHeaders(url, headers) { 238 function escape(s) { 239 return s.replace('(', '\\(').replace(')', '\\)').replace(',', '\\,'); 240 } 241 const formattedHeaders = headers.map((header) => { 242 return `header(${escape(header[0])}, ${escape(header[1])})`; 243 }); 244 url.searchParams.append('pipe', formattedHeaders.join('|')); 245 } 246 247 function windowExecutorCreator( 248 { target = '_blank', features } = {}, remoteContextWrapper) { 249 let openWindow = (url, target, features, documentContent) => { 250 const w = window.open(url, target, features); 251 if (documentContent) { 252 w.document.open(); 253 w.document.write(documentContent); 254 w.document.close(); 255 } 256 }; 257 258 return (url, documentContent) => { 259 if (url && url.substring(0, 5) == 'data:') { 260 throw new TypeError('Windows cannot use data: URLs.'); 261 } 262 263 if (remoteContextWrapper) { 264 return remoteContextWrapper.executeScript( 265 openWindow, [url, target, features, documentContent]); 266 } else { 267 openWindow(url, target, features, documentContent); 268 } 269 }; 270 } 271 272 function elementExecutorCreator( 273 remoteContextWrapper, elementName, attributes) { 274 return (url, documentContent) => { 275 return remoteContextWrapper.executeScript( 276 (url, elementName, attributes, documentContent) => { 277 const el = document.createElement(elementName); 278 for (const attribute in attributes) { 279 el.setAttribute(attribute, attributes[attribute]); 280 } 281 if (url) { 282 if (elementName == 'object') { 283 el.data = url; 284 } else { 285 el.src = url; 286 } 287 } 288 const parent = 289 elementName == 'frame' ? findOrCreateFrameset() : document.body; 290 parent.appendChild(el); 291 if (documentContent) { 292 el.contentDocument.open(); 293 el.contentDocument.write(documentContent); 294 el.contentDocument.close(); 295 } 296 }, 297 [url, elementName, attributes, documentContent]); 298 }; 299 } 300 301 function iframeSrcdocExecutorCreator(remoteContextWrapper, attributes) { 302 return async (url) => { 303 // `url` points to the content needed to run an `Executor` in the frame. 304 // So we download the content and pass it via the `srcdoc` attribute, 305 // setting the iframe's `src` to `undefined`. 306 attributes['srcdoc'] = await fetchText(url); 307 308 elementExecutorCreator( 309 remoteContextWrapper, 'iframe', attributes)(undefined); 310 }; 311 } 312 313 function workerExecutorCreator(remoteContextWrapper, globalVariable) { 314 return url => { 315 return remoteContextWrapper.executeScript((url, globalVariable) => { 316 const worker = new Worker(url); 317 if (globalVariable) { 318 window[globalVariable] = worker; 319 } 320 }, [url, globalVariable]); 321 }; 322 } 323 324 function navigateExecutorCreator(remoteContextWrapper) { 325 return url => { 326 return remoteContextWrapper.navigate((url) => { 327 window.location = url; 328 }, [url]); 329 }; 330 } 331 332 /** 333 * This class represents a remote context running an executor (a 334 * window/frame/worker that can receive commands). It is the interface for 335 * scripts to control remote contexts. 336 * 337 * Instances are returned when new remote contexts are created (e.g. 338 * `addFrame` or `navigateToNew`). 339 */ 340 class RemoteContextWrapper { 341 /** 342 * This should only be constructed by `RemoteContextHelper`. 343 * @private 344 */ 345 constructor(context, helper, url) { 346 this.context = context; 347 this.helper = helper; 348 this.url = url; 349 } 350 351 /** 352 * Executes a script in the remote context. 353 * @param {function} fn The script to execute. 354 * @param {any[]} args An array of arguments to pass to the script. 355 * @returns {Promise<any>} The return value of the script (after 356 * being serialized and deserialized). 357 */ 358 async executeScript(fn, args) { 359 return this.context.execute_script(fn, args); 360 } 361 362 /** 363 * Adds a string of HTML to the executor's document. 364 * @param {string} html 365 * @returns {Promise<undefined>} 366 */ 367 async addHTML(html) { 368 return this.executeScript((htmlSource) => { 369 document.body.insertAdjacentHTML('beforebegin', htmlSource); 370 }, [html]); 371 } 372 373 /** 374 * Adds scripts to the executor's document. 375 * @param {string[]} urls A list of URLs. URLs are relative to the current 376 * document. 377 * @returns {Promise<undefined>} 378 */ 379 async addScripts(urls) { 380 if (!urls) { 381 return []; 382 } 383 return this.executeScript(urls => { 384 return addScripts(urls); 385 }, [urls.map(makeAbsolute)]); 386 } 387 388 /** 389 * Adds an `iframe` with `src` attribute to the current document. 390 * @param {RemoteContextConfig} [extraConfig] 391 * @param {[string, string][]} [attributes] A list of pairs of strings 392 * of attribute name and value these will be set on the iframe element 393 * when added to the document. 394 * @returns {Promise<RemoteContextWrapper>} The remote context. 395 */ 396 addIframe(extraConfig, attributes = {}) { 397 return this.helper.createContext({ 398 executorCreator: elementExecutorCreator(this, 'iframe', attributes), 399 extraConfig, 400 }); 401 } 402 403 /** 404 * Adds a `frame` with `src` attribute to the current document's first 405 * `frameset` element. 406 * @param {RemoteContextConfig} [extraConfig] 407 * @param {[string, string][]} [attributes] A list of pairs of strings 408 * of attribute name and value these will be set on the frame element 409 * when added to the document. 410 * @returns {Promise<RemoteContextWrapper>} The remote context. 411 */ 412 addFrame(extraConfig, attributes = {}) { 413 return this.helper.createContext({ 414 executorCreator: elementExecutorCreator(this, 'frame', attributes), 415 extraConfig, 416 }); 417 } 418 419 /** 420 * Adds an `embed` with `src` attribute to the current document. 421 * @param {RemoteContextConfig} [extraConfig] 422 * @param {[string, string][]} [attributes] A list of pairs of strings 423 * of attribute name and value these will be set on the embed element 424 * when added to the document. 425 * @returns {Promise<RemoteContextWrapper>} The remote context. 426 */ 427 addEmbed(extraConfig, attributes = {}) { 428 return this.helper.createContext({ 429 executorCreator: elementExecutorCreator(this, 'embed', attributes), 430 extraConfig, 431 }); 432 } 433 434 /** 435 * Adds an `object` with `data` attribute to the current document. 436 * @param {RemoteContextConfig} [extraConfig] 437 * @param {[string, string][]} [attributes] A list of pairs of strings 438 * of attribute name and value these will be set on the object element 439 * when added to the document. 440 * @returns {Promise<RemoteContextWrapper>} The remote context. 441 */ 442 addObject(extraConfig, attributes = {}) { 443 return this.helper.createContext({ 444 executorCreator: elementExecutorCreator(this, 'object', attributes), 445 extraConfig, 446 }); 447 } 448 449 /** 450 * Adds an iframe with `srcdoc` attribute to the current document 451 * @param {RemoteContextConfig} [extraConfig] 452 * @param {[string, string][]} [attributes] A list of pairs of strings 453 * of attribute name and value these will be set on the iframe element 454 * when added to the document. 455 * @returns {Promise<RemoteContextWrapper>} The remote context. 456 */ 457 addIframeSrcdoc(extraConfig, attributes = {}) { 458 return this.helper.createContext({ 459 executorCreator: iframeSrcdocExecutorCreator(this, attributes), 460 extraConfig, 461 }); 462 } 463 464 /** 465 * Opens a window from the remote context. @see createContext for 466 * @param {RemoteContextConfig|object} [extraConfig] 467 * @param {Object} [options] 468 * @param {string} [options.target] Passed to `window.open` as the 469 * 2nd argument 470 * @param {string} [options.features] Passed to `window.open` as the 471 * 3rd argument 472 * @returns {Promise<RemoteContextWrapper>} The remote context. 473 */ 474 addWindow(extraConfig, options) { 475 return this.helper.createContext({ 476 executorCreator: windowExecutorCreator(options, this), 477 extraConfig, 478 }); 479 } 480 481 /** 482 * Adds a dedicated worker to the current document. 483 * @param {string|null} [globalVariable] The name of the global variable to 484 * which to assign the `Worker` object after construction. If `null`, 485 * then no assignment will take place. 486 * @param {RemoteContextConfig} [extraConfig] 487 * @returns {Promise<RemoteContextWrapper>} The remote context. 488 */ 489 addWorker(globalVariable, extraConfig) { 490 return this.helper.createContext({ 491 executorCreator: workerExecutorCreator(this, globalVariable), 492 extraConfig, 493 isWorker: true, 494 }); 495 } 496 497 /** 498 * Gets a `Headers` object containing the request headers that were used 499 * when the browser requested this document. 500 * 501 * Currently, this only works for `RemoteContextHelper`s representing 502 * windows, not workers. 503 * @returns {Promise<Headers>} 504 */ 505 async getRequestHeaders() { 506 // This only works in window environments for now. We could make it work 507 // for workers too; if you have a need, just share or duplicate the code 508 // that's in executor-window.py. Anyway, we explicitly use `window` in 509 // the script so that we get a clear error if you try using it on a 510 // worker. 511 512 // We need to serialize and deserialize the `Headers` object manually. 513 const asNestedArrays = await this.executeScript(() => [...window.__requestHeaders]); 514 return new Headers(asNestedArrays); 515 } 516 517 /** 518 * Executes a script in the remote context that will perform a navigation. 519 * To do this safely, we must suspend the executor and wait for that to 520 * complete before executing. This ensures that all outstanding requests are 521 * completed and no more can start. It also ensures that the executor will 522 * restart if the page goes into BFCache or it was a same-document 523 * navigation. It does not return a value. 524 * 525 * NOTE: We cannot monitor whether and what navigations are happening. The 526 * logic has been made as robust as possible but is not fool-proof. 527 * 528 * Foolproof rule: 529 * - The script must perform exactly one navigation. 530 * - If that navigation is a same-document history traversal, you must 531 * `await` the result of `waitUntilLocationIs`. (Same-document non-traversal 532 * navigations do not need this extra step.) 533 * 534 * More complex rules: 535 * - The script must perform a navigation. If it performs no navigation, 536 * the remote context will be left in the suspended state. 537 * - If the script performs a direct same-document navigation, it is not 538 * necessary to use this function but it will work as long as it is the only 539 * navigation performed. 540 * - If the script performs a same-document history navigation, you must 541 * `await` the result of `waitUntilLocationIs`. 542 * 543 * @param {function} fn The script to execute. 544 * @param {any[]} args An array of arguments to pass to the script. 545 * @returns {Promise<undefined>} 546 */ 547 navigate(fn, args) { 548 return this.executeScript((fnText, args) => { 549 executeScriptToNavigate(fnText, args); 550 }, [fn.toString(), args]); 551 } 552 553 /** 554 * Navigates to the given URL, by executing a script in the remote 555 * context that will perform navigation with the `location.href` 556 * setter. 557 * 558 * Be aware that performing a cross-document navigation using this 559 * method will cause this `RemoteContextWrapper` to become dormant, 560 * since the remote context it points to is no longer active and 561 * able to receive messages. You also won't be able to reliably 562 * tell when the navigation finishes; the returned promise will 563 * fulfill when the script finishes running, not when the navigation 564 * is done. As such, this is most useful for testing things like 565 * unload behavior (where it doesn't matter) or prerendering (where 566 * there is already a `RemoteContextWrapper` for the destination). 567 * For other cases, using `navigateToNew()` will likely be better. 568 * 569 * @param {string|URL} url The URL to navigate to. 570 * @returns {Promise<undefined>} 571 */ 572 navigateTo(url) { 573 return this.navigate(url => { 574 location.href = url; 575 }, [url.toString()]); 576 } 577 578 /** 579 * Navigates the context to a new document running an executor. 580 * @param {RemoteContextConfig} [extraConfig] 581 * @returns {Promise<RemoteContextWrapper>} The remote context. 582 */ 583 async navigateToNew(extraConfig) { 584 return this.helper.createContext({ 585 executorCreator: navigateExecutorCreator(this), 586 extraConfig, 587 }); 588 } 589 590 ////////////////////////////////////// 591 // Navigation Helpers. 592 // 593 // It is up to the test script to know which remote context will be 594 // navigated to and which `RemoteContextWrapper` should be used after 595 // navigation. 596 // 597 // NOTE: For a same-document history navigation, the caller use `await` a 598 // call to `waitUntilLocationIs` in order to know that the navigation has 599 // completed. For convenience the method below can return the promise to 600 // wait on, if passed the expected location. 601 602 async waitUntilLocationIs(expectedLocation) { 603 return this.executeScript(async (expectedLocation) => { 604 if (location.href === expectedLocation) { 605 return; 606 } 607 608 // Wait until the location updates to the expected one. 609 await new Promise(resolve => { 610 const listener = addEventListener('hashchange', (event) => { 611 if (event.newURL === expectedLocation) { 612 removeEventListener(listener); 613 resolve(); 614 } 615 }); 616 }); 617 }, [expectedLocation]); 618 } 619 620 /** 621 * Performs a history traversal. 622 * @param {integer} n How many steps to traverse. @see history.go 623 * @param {string} [expectedLocation] If supplied will be passed to @see waitUntilLocationIs. 624 * @returns {Promise<undefined>} 625 */ 626 async historyGo(n, expectedLocation) { 627 await this.navigate((n) => { 628 history.go(n); 629 }, [n]); 630 if (expectedLocation) { 631 await this.waitUntilLocationIs(expectedLocation); 632 } 633 } 634 635 /** 636 * Performs a history traversal back. 637 * @param {string} [expectedLocation] If supplied will be passed to @see waitUntilLocationIs. 638 * @returns {Promise<undefined>} 639 */ 640 async historyBack(expectedLocation) { 641 await this.navigate(() => { 642 history.back(); 643 }); 644 if (expectedLocation) { 645 await this.waitUntilLocationIs(expectedLocation); 646 } 647 } 648 649 /** 650 * Performs a history traversal back. 651 * @param {string} [expectedLocation] If supplied will be passed to @see waitUntilLocationIs. 652 * @returns {Promise<undefined>} 653 */ 654 async historyForward(expectedLocation) { 655 await this.navigate(() => { 656 history.forward(); 657 }); 658 if (expectedLocation) { 659 await this.waitUntilLocationIs(expectedLocation); 660 } 661 } 662 } 663 664 665 /** 666 * This class represents a configuration for creating remote contexts. This is 667 * the entry-point 668 * for creating remote contexts, providing @see addWindow . 669 */ 670 class RemoteContextHelper { 671 /** 672 * The constructor to use when creating new remote context wrappers. 673 * Can be overridden by subclasses. 674 */ 675 static RemoteContextWrapper = RemoteContextWrapper; 676 677 /** 678 * @param {RemoteContextConfig|object} config The configuration 679 * for this remote context. 680 */ 681 constructor(config) { 682 this.config = RemoteContextConfig.ensure(config); 683 } 684 685 /** 686 * Creates a new remote context and returns a `RemoteContextWrapper` giving 687 * access to it. 688 * @private 689 * @param {Object} options 690 * @param {(url: string) => Promise<undefined>} [options.executorCreator] A 691 * function that takes a URL and causes the browser to navigate some 692 * window to that URL, e.g. via an iframe or a new window. If this is 693 * not supplied, then the returned RemoteContextWrapper won't actually 694 * be communicating with something yet, and something will need to 695 * navigate to it using its `url` property, before communication can be 696 * established. 697 * @param {RemoteContextConfig|object} [options.extraConfig] If supplied, 698 * extra configuration for this remote context to be merged with 699 * `this`'s existing config. If it's not a `RemoteContextConfig`, it 700 * will be used to construct a new one. 701 * @returns {Promise<RemoteContextWrapper>} 702 */ 703 async createContext({ 704 executorCreator, 705 extraConfig, 706 isWorker = false, 707 }) { 708 const config = 709 this.config.merged(RemoteContextConfig.ensure(extraConfig)); 710 711 // UUID is needed for executor. 712 const uuid = token(); 713 const url = await config.createExecutorUrl(uuid, isWorker); 714 715 if (executorCreator) { 716 if (config.urlType == 'blank') { 717 await executorCreator(undefined, await fetchText(url)); 718 } else { 719 await executorCreator(url, undefined); 720 } 721 } 722 723 return new this.constructor.RemoteContextWrapper(new RemoteContext(uuid), this, url); 724 } 725 726 /** 727 * Creates a window with a remote context. @see createContext for 728 * @param {RemoteContextConfig|object} [extraConfig] Will be 729 * merged with `this`'s config. 730 * @param {Object} [options] 731 * @param {string} [options.target] Passed to `window.open` as the 732 * 2nd argument 733 * @param {string} [options.features] Passed to `window.open` as the 734 * 3rd argument 735 * @returns {Promise<RemoteContextWrapper>} 736 */ 737 addWindow(extraConfig, options) { 738 return this.createContext({ 739 executorCreator: windowExecutorCreator(options), 740 extraConfig, 741 }); 742 } 743 } 744 // Export this class. 745 self.RemoteContextHelper = RemoteContextHelper; 746 }