profile
viewpoint

puppeteer/puppeteer 66027

Headless Chrome Node.js API

puppeteer/pptr.dev 38

puppeteer documentation website

johanbay/bbfetch 0

Access the Blackboard installation at Aarhus University from the command line

johanbay/enter 0

A tool for entering answers for the multiple choice sets described at http://users-cs.au.dk/mis/Multiple/

johanbay/puppeteer 0

Headless Chrome Node.js API

push eventpuppeteer/recorder

Johan Bay

commit sha e837ceab59c88b25754f82a3c959541a87d3b3b1

Address more comments

view details

push time in 6 hours

Pull request review commentpuppeteer/recorder

Move event handling out of the page context

 describe('Recorder', () => {     await expect(getScriptFromStream(output)).resolves.toMatchInlineSnapshot(`             "const {open, click, type, submit, expect, scrollToBottom} = require('@puppeteer/recorder');             open('[url]', {}, async (page) => {-              await click('aria/button[name=\\"Test Button\\"]');+              await click('aria/Test Button[role=\\"button\\"]');

It seems like the advantage of .toMatchInlineSnapshot is that it supports a golden-file like testing infrastructure where the expected outputs can be updated by running the test with --updateSnapshot. So we might want to leave the outputs as-is, even though they are quite painful too look at?

johanbay

comment created time in 6 hours

PullRequestReviewEvent

Pull request review commentpuppeteer/recorder

Move event handling out of the page context

 export default async (     const value = targetValue.result.value;     const escapedValue = value.replace(/'/g, "\\'");     const selector = await getSelector(client, targetId);-    addLineToPuppeteerScript(`await type('${selector}', '${escapedValue}');`);+    addLineToPuppeteerScript(`await type(${JSON.stringify(selector)}, ${JSON.stringify(escapedValue)});`);

Ah, yes that makes sense.

johanbay

comment created time in 6 hours

PullRequestReviewEvent

push eventpuppeteer/recorder

Johan Bay

commit sha 6e2659cac8d3a1b4e3c59acc879d7f370648e766

Fix breakpoint bug and address comments We sometimes tried set the event listener breakpoint before the breakpoints were added. We now wait for `domcontentloaded`.

view details

push time in 8 hours

Pull request review commentpuppeteer/recorder

Move event handling out of the page context

 describe('CSS Path', () => {   });    it('should return an id selector if the node has an id', () => {-    document.body.innerHTML = `<div id="test" data-id="test"></div>`;+    document.body.innerHTML = `<div id="test" data-id="test" />`;

yep it seems like the linter changed this. I might have had a wrong linting config at some point while working on this.

johanbay

comment created time in 9 hours

PullRequestReviewEvent

delete branch puppeteer/puppeteer

delete branch : v5.4.0-post

delete time in 12 hours

push eventpuppeteer/puppeteer

Johan Bay

commit sha d78786506621907cf92809ac479a07e75bdc20fb

chore: bump version to v5.4.0-post (#6544)

view details

push time in 12 hours

PR merged puppeteer/puppeteer

Reviewers
chore: bump version to v5.4.0-post cla: yes
+2 -2

0 comment

2 changed files

johanbay

pr closed time in 12 hours

PR opened puppeteer/recorder

Reviewers
Move event handling out of the page context

There's quite a lot in this PR, but I'm not sure if there is a reasonable way to cut it into smaller pieces. Puppeteer 5.4.0 is now released, so the aria handler is built in and queryAXTree CDP endpoint is available in the bundled Chromium.

+468 -563

0 comment

14 changed files

pr created time in 3 days

push eventpuppeteer/recorder

Johan Bay

commit sha 1e96f2cdfcd15cdad2308a2f787bb5a4a026d6ff

Improve typings

view details

push time in 3 days

PullRequestReviewEvent

PR opened puppeteer/puppeteer

Reviewers
chore: bump version to v5.4.0-post
+2 -2

0 comment

2 changed files

pr created time in 3 days

create barnchpuppeteer/puppeteer

branch : v5.4.0-post

created branch time in 3 days

created tagpuppeteer/puppeteer

tagv5.4.0

Headless Chrome Node.js API

created time in 3 days

release puppeteer/puppeteer

v5.4.0

released time in 3 days

delete branch puppeteer/puppeteer

delete branch : release-v5.4.0

delete time in 3 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 729cdfe98a254622cc3e6aa96b509b7882717357

chore: mark version v5.4.0 (#6542)

view details

push time in 3 days

PR merged puppeteer/puppeteer

Reviewers
chore: mark version v5.4.0 cla: yes

PTAL, but let's not merge until after https://github.com/puppeteer/puppeteer/pull/6536.

+79 -29

0 comment

7 changed files

johanbay

pr closed time in 3 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha d9a2951dab986065e76f477deedafa61d4d85bfd

chore: mark version v5.4.0

view details

push time in 3 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 8363c7d9398e93f820c06495e0e94c89d291e521

chore: mark version v5.4.0

view details

push time in 3 days

PR opened puppeteer/puppeteer

Reviewers
chore: mark version v5.4.0

PTAL, but let's not merge until after https://github.com/puppeteer/puppeteer/pull/6536.

+56 -17

0 comment

4 changed files

pr created time in 3 days

push eventpuppeteer/puppeteer

Jack Franklin

commit sha e6b8c77d94a54dd1e8de561cc33f571c26e7b05d

chore: fix travis config (#6537)

view details

Johan Bay

commit sha 5e5fed1deb6ba963a9e905711de5827696ccb248

fix: ignore spurious bindingCalled events (#6538)

view details

Johan Bay

commit sha 0347721e02164d58d4e118e43fccbaaaa0522b97

Merge branch 'main' of github.com:puppeteer/puppeteer into automate-publish

view details

push time in 3 days

create barnchpuppeteer/puppeteer

branch : release-v5.4.0

created branch time in 3 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 5e5fed1deb6ba963a9e905711de5827696ccb248

fix: ignore spurious bindingCalled events (#6538)

view details

push time in 3 days

push eventpuppeteer/recorder

Mathias Bynens

commit sha b1688cd2fc894cd9b64ec6d8ed84199ecca1e640

Correct and clarify HTML inputs E.g. `<div/>` is not valid in HTML documents, only in XHTML documents (which we’re not using here). It still appeared to work because the HTML parser is very forgiving, but this patch clarifies the intended result by explicitly including closing tags where applicable (even in cases where they are not strictly necessary for validity, like `<p>`).

view details

dependabot[bot]

commit sha 423bb099e331b2d4a2fa7d515cc79f7997d0e03b

Bump bl from 4.0.2 to 4.0.3 Bumps [bl](https://github.com/rvagg/bl) from 4.0.2 to 4.0.3. - [Release notes](https://github.com/rvagg/bl/releases) - [Commits](https://github.com/rvagg/bl/compare/v4.0.2...v4.0.3) Signed-off-by: dependabot[bot] <support@github.com>

view details

Peter Marshall

commit sha df291d7b13a2b180a421c98c4168743999975e8f

Merge pull request #26 from puppeteer/dependabot/npm_and_yarn/bl-4.0.3 Bump bl from 4.0.2 to 4.0.3

view details

Peter Marshall

commit sha 6266e64eb875554c47274a5253f1036a6cb4dcf1

Update the README

view details

Johan Bay

commit sha dfc0e40cf12eba08297f8e38cac12a67b7882500

add linting and prettier config from Puppeteer

view details

Johan Bay

commit sha 3b9c45b177e1112311032c18572bcc09732c70d6

use `ComputedAccessibilityInfo` for getSelector This is not testable in a jsdom environment so dom-helpers.spec.ts has been converted to a Puppeteer environment.

view details

Johan Bay

commit sha 278b985df9b141cc517a6fbb204ea59de04ac2ab

use Puppeteer's built-in aria handler

view details

Johan Bay

commit sha 8193d5abcaf5b9eb1bf0f636a304b4a65695250b

observe events through setEventListenerBreakpoint

view details

Johan Bay

commit sha 3d7bed1ee017f3611c46555174bc67e3586586ec

disable script injection

view details

Johan Bay

commit sha 81ddfd60751fef4c3f84fe7c1a17851094d1ced3

retrieve CSS selector through CDP

view details

Johan Bay

commit sha 9c56851ee87089f076f44c278938fdd672853594

Fix submit events and refactor a bit

view details

push time in 3 days

PR opened puppeteer/recorder

Reviewers
add linting and prettier config from Puppeteer

What do you all think about applying the linting and prettier config from Puppeteer here as well? This PR copies it mostly verbatim, although I downgraded some of the rules from error to warning.

+413 -7050

0 comment

19 changed files

pr created time in 4 days

create barnchpuppeteer/recorder

branch : linting

created branch time in 4 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha c9341aad99cfff1b7296a0e8e4af9cf324498002

fix: ignore spurious bindingCalled events

view details

push time in 4 days

push eventpuppeteer/puppeteer

Jack Franklin

commit sha e6b8c77d94a54dd1e8de561cc33f571c26e7b05d

chore: fix travis config (#6537)

view details

Johan Bay

commit sha 0d17e18a6ce400e05668b530ed015df13a848e4f

fix: ignore spurious bindingCalled events

view details

Johan Bay

commit sha 95984a833f68f3abc3f821fbc0f89c84fe098836

fix: apply suggestions from code review Co-authored-by: Mathias Bynens <mathias@qiwi.be>

view details

push time in 4 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha c21a3cca0fa059225e202cf5ac5ea74cbe0c95ff

fix: apply suggestions from code review Co-authored-by: Mathias Bynens <mathias@qiwi.be>

view details

push time in 4 days

Pull request review commentpuppeteer/puppeteer

fix: ignore spurious bindingCalled events

 export class DOMWorld {   private async _onBindingCalled(     event: Protocol.Runtime.BindingCalledEvent   ): Promise<void> {-    const { type, name, seq, args } = JSON.parse(event.payload);+    let payload: { type: string; name: string; seq: number; args: unknown[] };+    try {+      payload = JSON.parse(event.payload);+    } catch {+      // The binding was either called by something in the page or it was+      // called before our wrapper was initialized

Exactly. I can't make out how it happens, but very rarely we'll call the ariaQueryHandler binding before the pageBinding wrapper is set up.

johanbay

comment created time in 4 days

PullRequestReviewEvent

PR opened puppeteer/puppeteer

Reviewers
fix: ignore spurious bindingCalled events
+18 -2

0 comment

2 changed files

pr created time in 4 days

create barnchpuppeteer/puppeteer

branch : fix-onBindingCalled-flake

created branch time in 4 days

PullRequestReviewEvent

push eventpuppeteer/recorder

Johan Bay

commit sha b246edb8af686ac1df14df37af8e6ab6fe3f87fa

observe events through setEventListenerBreakpoint

view details

push time in 6 days

push eventpuppeteer/recorder

Johan Bay

commit sha e108e89161aef7f8ad035b0b2a14587625ed34c9

observe events through setEventListenerBreakpoint

view details

push time in 6 days

create barnchpuppeteer/recorder

branch : use-pptr-aria

created branch time in 6 days

delete branch puppeteer/recorder

delete branch : use-ComputedAccessibilityInfo

delete time in 13 days

PR closed puppeteer/recorder

Reviewers
use `ComputedAccessibilityInfo` for getSelector

This PR removes the use of the aria-api package from the recording phase and instead uses Element.computedName and Element.computedRole (https://bugs.chromium.org/p/chromium/issues/detail?id=442978). This is not testable in a jsdom environment so dom-helpers.spec.ts has been converted to a Puppeteer environment.

+144 -53

1 comment

9 changed files

johanbay

pr closed time in 13 days

pull request commentpuppeteer/recorder

use `ComputedAccessibilityInfo` for getSelector

After discussing this with Peter, we have decided to explore a CDP-based solution instead. The ComputedAccessibilityInfo-based solution is fine for the current nodejs driven Recorder, but it clashes with future plans to inject the recorder in already running Chromium instances.

johanbay

comment created time in 13 days

Pull request review commentpuppeteer/recorder

use `ComputedAccessibilityInfo` for getSelector

  * limitations under the License.  */ -import { isSubmitButton, getSelector } from '../src/injected/dom-helpers';+import { readFileSync } from "fs";

yeah I'm not sure what happened here. Thanks!

johanbay

comment created time in 13 days

PullRequestReviewEvent

push eventpuppeteer/puppeteer

Johan Bay

commit sha 8fabe328006b9741eba53fa4f7f319d3b3286fe2

feat(queryhandler): add built-in pierce handler (#6509) Adds a handler 'pierce' that pierces shadow roots while querying.

view details

push time in 13 days

delete branch puppeteer/puppeteer

delete branch : add-pierce-handler

delete time in 13 days

PR merged puppeteer/puppeteer

feat(queryhandler): add built-in pierce handler cla: yes

Adds a handler 'pierce' that pierces shadow roots while querying. Closes #6217.

+93 -1

0 comment

2 changed files

johanbay

pr closed time in 13 days

PR closed puppeteer/puppeteer

Proposal: fetch elements within shadow root with `shadow$` and `shadow$$` cla: yes

There are various issues on missing support for fetching elements within the shadow DOM of elements. Some related issues are:

  • https://github.com/puppeteer/puppeteer/issues/6217
  • https://github.com/puppeteer/puppeteer/issues/5405
  • https://github.com/puppeteer/puppeteer/issues/4171

Also for Puppeteer embedder projects like WebdriverIO:

  • https://github.com/webdriverio/webdriverio/issues/4484

This PR proposes shadow$ and shadow$$ to allow query an element through element shadow roots. Given the following DOM structure:

<html>
<body>
  <div id="elem">
    #shadow-root
    <customA></customA>
    <customA>
      #shadow-root
      <subCustomB>
        #shadow-root
        <div class="container"></div>
      </subCustomB>
    </customA>
  </div>
</body>
</html>

Given someone would fetch the root of an shadow element:

const elem = await page.$('#elem')

The following queries would give following results:

  • elem.shadow$(['customA', 'subCustomB', '.container'])
    
    would return null because we would always pick the first element being found
  • const customAs = await elem.shadow$$(['customA'])
    return customAs[1].shadow$(['subCustomB', '.container'])
    
    would return div.container

One idea could be to always use Element.querySelectorAll and iterate through all nodes. This could be potentially a very expensive operation though and would only make sense for shadow$$.

WDYAT?

+81 -0

10 comments

2 changed files

christian-bromann

pr closed time in 13 days

pull request commentpuppeteer/puppeteer

Proposal: fetch elements within shadow root with `shadow$` and `shadow$$`

Yes it sounds like we can close this one and merge https://github.com/puppeteer/puppeteer/pull/6509. Thanks!

christian-bromann

comment created time in 13 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha f768ca0d4c125a973eaff37bbf8db6e298ae6c81

feat(queryhandler): add built-in pierce handler Adds a handler 'pierce' that pierces shadow roots while querying.

view details

push time in 13 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 0111ea53f4a4c3ee7855fa9e8938dca2a1879441

fix lint Co-authored-by: Mathias Bynens <mathias@qiwi.be>

view details

push time in 13 days

pull request commentpuppeteer/puppeteer

Proposal: fetch elements within shadow root with `shadow$` and `shadow$$`

With the changes to the query handler infrastructure, we can do something like this https://github.com/puppeteer/puppeteer/pull/6509 to support querying elements inside shadow roots. So in the examples above one would do page.$('pierce/.container'). Does that work for you @LarsDenBakker and @christian-bromann?

christian-bromann

comment created time in 13 days

PR opened puppeteer/puppeteer

feat(queryhandler): add built-in pierce handler

Adds a handler 'pierce' that pierces shadow roots while querying.

+90 -1

0 comment

2 changed files

pr created time in 14 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 9bc5d047ffa89a394123d3af8cad13c5ce4a36a9

feat(queryhandler): add built-in pierce handler Adds a handler 'pierce' that pierces shadow roots while querying.

view details

push time in 14 days

create barnchpuppeteer/puppeteer

branch : add-pierce-handler

created branch time in 14 days

PR opened puppeteer/recorder

use `ComputedAccessibilityInfo` for getSelector

This is not testable in a jsdom environment so dom-helpers.spec.ts has been converted to a Puppeteer environment.

+144 -53

0 comment

9 changed files

pr created time in 14 days

push eventpuppeteer/recorder

Johan Bay

commit sha 6e4d072c56db26d5eb4a01fb0da9301c1ba0380e

use `ComputedAccessibilityInfo` for getSelector This is not testable in a jsdom environment so dom-helpers.spec.ts has been converted to a Puppeteer environment.

view details

push time in 17 days

delete branch puppeteer/puppeteer

delete branch : add-aria-handler-2

delete time in 19 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 3afe1935da5ee3b3a3ed8e910dd8dc280a0ae094

feat(a11y-query): extend aria handler with waitFor (#6472) This commit adds waitFor to the built-in aria handler (#6307).

view details

push time in 19 days

PR merged puppeteer/puppeteer

Reviewers
feat(a11y-query): extend aria handler with waitFor cla: yes

This PR adds waitFor to the built-in aria handler (#6307). This is part 3 of https://github.com/puppeteer/puppeteer/pull/6453.

+576 -125

0 comment

7 changed files

johanbay

pr closed time in 19 days

issue closedpuppeteer/puppeteer

Proposal: Query by accessible name

Motivation

Currently Puppeteer supports querying the DOM using CSS selectors. While this provides a versatile way to select DOM elements by for example id, tag, or class-attributes, these properties are often subject to change or even generated at build-time. Alternatively, we could support querying the accessibility tree for accessible names. This could provide the following benefits:

  • Make selectors in test scripts more resilient to source code changes.
  • Make test scripts more readable since the accessible names should carry semantic context.
  • Motivate good practices for assigning accessibility properties to elements.

Current state of affairs

Querying by accessible names is currently possible by implementing a custom query handler to traverse the DOM and check accessible names along the way. This is what puppeteer/recorder currently does. Since there is no direct way to get the computed accessible name and role from the browser-side, one has to compute those on demand which is not a trivial task. One can instead access and traverse the accessibility tree through Puppeteer where the computed accessible names and roles are available. The catch then is that Puppeteer only gives access to a static snapshot of the accessibility tree. To support interaction, we need a mapping to live DOM nodes.

Proposal

Extend Puppeteer to support querying the accessibility tree with support for interaction. The steps for achieving this could be:

  • [x] Implement CDP-level support for querying the accessibility tree. Landed in https://github.com/chromium/chromium/commit/d557b1cd92b3c47dae01aaaddd7c6be0ceb6cb94
  • [x] Restructure such that query handlers can query on the Puppeteer-level (as opposed to querying in the page context) Done in #6437
  • [ ] Implement atlernative query handler for a11y-based querying

closed time in 19 days

johanbay

push eventpuppeteer/puppeteer

Johan Bay

commit sha cc7f1fd063bf73dbb40cf08059283b78aaa28516

docs(queryhandler): add custom query handler docs (#6476)

view details

push time in 19 days

delete branch puppeteer/puppeteer

delete branch : custom-query-docs

delete time in 19 days

PR merged puppeteer/puppeteer

Reviewers
docs(queryhandler): add custom query handler docs cla: yes
+178 -5

0 comment

12 changed files

johanbay

pr closed time in 19 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 64e5702516f4f177c1bd5ab4d5d7828f490cc4b3

docs(queryhandler): add custom query handler docs

view details

push time in 19 days

delete branch puppeteer/puppeteer

delete branch : fix-hide-internal-handlers

delete time in 19 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 70ed87515868d3cfdbb91805d27b1b5924e0a25d

fix(queryhandler) only expose custom handlers (#6475) This commit changes the custom query handler API to only operate on user-defined query handlers.

view details

push time in 19 days

PR merged puppeteer/puppeteer

Reviewers
fix(queryhandler) only expose custom handlers cla: yes

This PR changes the custom query handler API to only operate on user-defined query handlers.

+7 -8

0 comment

1 changed file

johanbay

pr closed time in 19 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 71563a6700605683491c1635012e94ec7d76dc97

... -> … Co-authored-by: Mathias Bynens <mathias@qiwi.be>

view details

push time in 19 days

Pull request review commentpuppeteer/puppeteer

docs(queryhandler): add custom query handler docs

 export class Puppeteer {   }    /**-   * @internal+   * Registers a {@link CustomQueryHandler | custom query handler}. After+   * registration, the handler can be used everywhere where a selector is+   * expected by prepending the selection string with `<name>/`. The name is+   * only allowed to consist of lower- and upper case latin letters.+   * @example+   * ```+   * puppeteer.registerCustomQueryHandler('text', { ... });+   * const aHandle = await page.$('text/...');

Ah yes that sounds like a good idea. Thanks!

johanbay

comment created time in 19 days

PullRequestReviewEvent

PR opened puppeteer/puppeteer

Reviewers
docs(queryhandler): add custom query handler docs
+178 -5

0 comment

12 changed files

pr created time in 19 days

create barnchpuppeteer/puppeteer

branch : custom-query-docs

created branch time in 19 days

PR opened puppeteer/puppeteer

Reviewers
fix(queryhandler) only expose custom handlers

This PR changes the custom query handler API to only operate on user-defined query handlers.

+7 -8

0 comment

1 changed file

pr created time in 19 days

create barnchpuppeteer/puppeteer

branch : fix-hide-internal-handlers

created branch time in 20 days

push eventpuppeteer/puppeteer

Johan Bay

commit sha 24c5bba48acd4e453b75bce9bea62b697e09af9b

feat(a11y-query): extend aria handler with waitFor

view details

push time in 20 days

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 export class DOMWorld {     return queryHandler.waitFor(this, updatedSelector, options);   } +  // If multiple waitFor are set up asynchronously, we need to wait for the+  // first one to set up the binding in the page before running the others.+  private _settingUpBinding: Promise<void> | null = null;+  /**+   * @internal+   */+  async addBindingToContext(name: string) {+    // Previous operation added the binding so we are done.+    if (this._ctxBindings.has(name)) return;+    // Wait for other operation to finish+    if (this._settingUpBinding) {+      await this._settingUpBinding;+      return this.addBindingToContext(name);

No it shouldn't. The first check this._ctxBindings.has(name) should be true if the binding was added by the current operation, so it will return immediately. The idea behind the recursive call is that if multiple tasks are blocked on await this._settingUpBinding;, then one of them will get to do the recursive call and assign a new promise to _settingUpBinding. When the other tasks gets to do the recursive call they are therefore blocked here again.

johanbay

comment created time in 20 days

PullRequestReviewEvent

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 describeChromeOnly('AriaQueryHandler', () => {     });   }); +  describe('waitForSelector (aria)', function () {+    const addElement = (tag) =>+      document.body.appendChild(document.createElement(tag));++    it('should immediately resolve promise if node exists', async () => {+      const { page, server } = getTestState();+      await page.goto(server.EMPTY_PAGE);+      await page.evaluate(addElement, 'button');+      await page.waitForSelector('aria/[role="button"]');+    });++    it('should work independently of `exposeFunction`', async () => {+      const { page, server } = getTestState();+      await page.goto(server.EMPTY_PAGE);+      await page.exposeFunction('ariaQuerySelector', (a, b) => a + b);+      await page.evaluate(addElement, 'button');+      await page.waitForSelector('aria/[role="button"]');+      const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)');+      expect(result).toBe(10);+    });++    it('should work with removed MutationObserver', async () => {+      const { page } = getTestState();++      await page.evaluate(() => delete window.MutationObserver);+      const [handle] = await Promise.all([+        page.waitForSelector('aria/anything'),+        page.setContent(`<h1>anything</h1>`),+      ]);+      expect(+        await page.evaluate((x: HTMLElement) => x.textContent, handle)+      ).toBe('anything');+    });++    it('should resolve promise when node is added', async () => {+      const { page, server } = getTestState();++      await page.goto(server.EMPTY_PAGE);+      const frame = page.mainFrame();+      const watchdog = frame.waitForSelector('aria/[role="heading"]');+      await frame.evaluate(addElement, 'br');+      await frame.evaluate(addElement, 'h1');+      const elementHandle = await watchdog;+      const tagName = await elementHandle+        .getProperty('tagName')+        .then((element) => element.jsonValue());+      expect(tagName).toBe('H1');+    });++    it('should work when node is added through innerHTML', async () => {+      const { page, server } = getTestState();++      await page.goto(server.EMPTY_PAGE);+      const watchdog = page.waitForSelector('aria/name');+      await page.evaluate(addElement, 'span');+      await page.evaluate(+        () =>+          (document.querySelector('span').innerHTML =+            '<h3><div aria-label="name"></div></h3>')+      );+      await watchdog;+    });++    it('Page.waitForSelector is shortcut for main frame', async () => {+      const { page, server } = getTestState();++      await page.goto(server.EMPTY_PAGE);+      await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);+      const otherFrame = page.frames()[1];+      const watchdog = page.waitForSelector('aria/[role="button"]');+      await otherFrame.evaluate(addElement, 'button');+      await page.evaluate(addElement, 'button');+      const elementHandle = await watchdog;+      expect(elementHandle.executionContext().frame()).toBe(page.mainFrame());+    });++    it('should run in specified frame', async () => {+      const { page, server } = getTestState();++      await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);+      await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE);+      const frame1 = page.frames()[1];+      const frame2 = page.frames()[2];+      const waitForSelectorPromise = frame2.waitForSelector(+        'aria/[role="button"]'+      );+      await frame1.evaluate(addElement, 'button');+      await frame2.evaluate(addElement, 'button');+      const elementHandle = await waitForSelectorPromise;+      expect(elementHandle.executionContext().frame()).toBe(frame2);+    });++    it('should throw when frame is detached', async () => {+      const { page, server } = getTestState();++      await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);+      const frame = page.frames()[1];+      let waitError = null;+      const waitPromise = frame+        .waitForSelector('aria/does-not-exist')+        .catch((error) => (waitError = error));+      await utils.detachFrame(page, 'frame1');+      await waitPromise;+      expect(waitError).toBeTruthy();+      expect(waitError.message).toContain(+        'waitForFunction failed: frame got detached.'+      );+    });++    it('should survive cross-process navigation', async () => {+      const { page, server } = getTestState();++      let imgFound = false;+      const waitForSelector = page+        .waitForSelector('aria/[role="img"]')+        .then(() => (imgFound = true));+      await page.goto(server.EMPTY_PAGE);+      expect(imgFound).toBe(false);+      await page.reload();+      expect(imgFound).toBe(false);+      await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');+      await waitForSelector;+      expect(imgFound).toBe(true);+    });++    it('should wait for visible', async () => {+      const { page } = getTestState();++      let divFound = false;+      const waitForSelector = page+        .waitForSelector('aria/name', { visible: true })+        .then(() => (divFound = true));+      await page.setContent(+        `<div aria-label='name' style='display: none; visibility: hidden;'>1</div>`+      );+      expect(divFound).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.removeProperty('display')+      );+      expect(divFound).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.removeProperty('visibility')+      );+      expect(await waitForSelector).toBe(true);+      expect(divFound).toBe(true);+    });++    it('should wait for visible recursively', async () => {+      const { page } = getTestState();++      let divVisible = false;+      const waitForSelector = page+        .waitForSelector('aria/inner', { visible: true })+        .then(() => (divVisible = true));+      await page.setContent(+        `<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>`+      );+      expect(divVisible).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.removeProperty('display')+      );+      expect(divVisible).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.removeProperty('visibility')+      );+      expect(await waitForSelector).toBe(true);+      expect(divVisible).toBe(true);+    });++    it('hidden should wait for visibility: hidden', async () => {+      const { page } = getTestState();++      let divHidden = false;+      await page.setContent(+        `<div role='button' style='display: block;'></div>`+      );+      const waitForSelector = page+        .waitForSelector('aria/[role="button"]', { hidden: true })+        .then(() => (divHidden = true));+      await page.waitForSelector('aria/[role="button"]'); // do a round trip+      expect(divHidden).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.setProperty('visibility', 'hidden')+      );+      expect(await waitForSelector).toBe(true);+      expect(divHidden).toBe(true);+    });++    it('hidden should wait for display: none', async () => {+      const { page } = getTestState();++      let divHidden = false;+      await page.setContent(`<div role='main' style='display: block;'></div>`);+      const waitForSelector = page+        .waitForSelector('aria/[role="main"]', { hidden: true })+        .then(() => (divHidden = true));+      await page.waitForSelector('aria/[role="main"]'); // do a round trip+      expect(divHidden).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.setProperty('display', 'none')+      );+      expect(await waitForSelector).toBe(true);+      expect(divHidden).toBe(true);+    });++    it('hidden should wait for removal', async () => {+      const { page } = getTestState();++      await page.setContent(`<div role='main'></div>`);+      let divRemoved = false;+      const waitForSelector = page+        .waitForSelector('aria/[role="main"]', { hidden: true })+        .then(() => (divRemoved = true));+      await page.waitForSelector('aria/[role="main"]'); // do a round trip+      expect(divRemoved).toBe(false);+      await page.evaluate(() => document.querySelector('div').remove());+      expect(await waitForSelector).toBe(true);+      expect(divRemoved).toBe(true);+    });++    it('should return null if waiting to hide non-existing element', async () => {+      const { page } = getTestState();++      const handle = await page.waitForSelector('aria/non-existing', {+        hidden: true,+      });+      expect(handle).toBe(null);+    });++    it('should respect timeout', async () => {+      const { page, puppeteer } = getTestState();++      let error = null;+      await page+        .waitForSelector('aria/[role="button"]', { timeout: 10 })+        .catch((error_) => (error = error_));+      expect(error).toBeTruthy();+      expect(error.message).toContain(+        'waiting for selector `[role="button"]` failed: timeout'+      );+      expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);+    });++    it('should have an error message specifically for awaiting an element to be hidden', async () => {+      const { page } = getTestState();++      await page.setContent(`<div role='main'></div>`);+      let error = null;+      await page+        .waitForSelector('aria/[role="main"]', { hidden: true, timeout: 10 })+        .catch((error_) => (error = error_));+      expect(error).toBeTruthy();+      expect(error.message).toContain(+        'waiting for selector `[role="main"]` to be hidden failed: timeout'+      );+    });++    it('should respond to node attribute mutation', async () => {+      const { page } = getTestState();++      let divFound = false;+      const waitForSelector = page+        .waitForSelector('aria/zombo')+        .then(() => (divFound = true));+      await page.setContent(`<div aria-label='notZombo'></div>`);+      expect(divFound).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').setAttribute('aria-label', 'zombo')+      );+      expect(await waitForSelector).toBe(true);+    });++    it('should return the element handle', async () => {+      const { page } = getTestState();++      const waitForSelector = page.waitForSelector('aria/zombo');+      await page.setContent(`<div aria-label='zombo'>anything</div>`);+      expect(+        await page.evaluate(+          (x: HTMLElement) => x.textContent,+          await waitForSelector+        )+      ).toBe('anything');+    });++    // TODO: Figure out what the stack trace should look like for custom handlers

It seems like we can't do much better than this since waitFor is async.

johanbay

comment created time in 20 days

PullRequestReviewEvent

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 export class DOMWorld {     return queryHandler.waitFor(this, updatedSelector, options);   } +  /**+   * @internal+   */+  async addBindingToContext(name: string) {+    const expression = helper.pageBindingInitString('internal', name);+    try {+      const context = await this.executionContext();+      await context._client.send('Runtime.addBinding', {+        name,+        executionContextId: context._contextId,+      });+      await context.evaluate(expression);+    } catch (error) {+      // When the page is navigated, the promise is rejected.+      // We will try again in the new execution context.+      if (error.message.includes('Execution context was destroyed')) return;+      // We could have tried to evaluate in a context which was already+      // destroyed.+      if (error.message.includes('Cannot find context with specified id'))+        return;+      debugError(error);+    }+  }++  /**+   * @internal+   */+  async addBinding(name: string, puppeteerFunction: Function): Promise<void> {+    if (this._ctxBindings.has(name))+      throw new Error(+        `Failed to add page binding with name ${name}: window['${name}'] already exists!`+      );+    this._ctxBindings.set(name, puppeteerFunction);+    await this.addBindingToContext(name);+  }++  /**+   * @internal+   */+  hasBinding(name: string): boolean {+    return this._ctxBindings.has(name);+  }++  private async _onBindingCalled(+    event: Protocol.Runtime.BindingCalledEvent+  ): Promise<void> {+    let jsonPayload = null;+    try {+      jsonPayload = JSON.parse(event.payload);+    } catch {+      // Binding must have been called before initializing the wrapper.+      return;+    }+    const { type, name, seq, args } = jsonPayload;+    if (type !== 'internal' || !this._ctxBindings.has(name)) return;+    if (!this._hasContext()) return;+    const context = await this.executionContext();+    if (context._contextId !== event.executionContextId) return;+    try {+      const result = await this._ctxBindings.get(name)(...args);+      await context.evaluate(deliverResult, name, seq, result);+    } catch (error) {+      // The WaitTask may already have been resolved by timing out, or the+      // exection context may have been destroyed.+      // In both caes, the promises above are rejected with a protocol error.+      // We can safely ignores these, as the WaitTask is re-installed in+      // the next execution context if needed.+      if (error.message.includes('Protocol error')) return;+      debugError(error);+    }+    function deliverResult(name: string, seq: number, result: unknown): void {+      window[name].callbacks.get(seq).resolve(result);+      window[name].callbacks.delete(seq);

Done.

johanbay

comment created time in 20 days

PullRequestReviewEvent

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 const queryAllArray = async (   return jsHandle; }; +// If multiple waitFor are set up asynchronously, we need to wait for the first+// one to set up the binding in the page before running the others.+let settingUpBinding = null;

Moved to DOMWorld.ts to allow asyncronously setting up bindings in multiple worlds.

johanbay

comment created time in 20 days

PullRequestReviewEvent

push eventpuppeteer/puppeteer

Johan Bay

commit sha 7575c5ec8aaad1c6ed514c704fb08f85ed60148e

feat(a11y-query): extend aria handler with waitFor

view details

push time in 20 days

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 describeChromeOnly('AriaQueryHandler', () => {     });   }); +  describe('waitForSelector (aria)', function () {+    const addElement = (tag) =>+      document.body.appendChild(document.createElement(tag));++    it('should immediately resolve promise if node exists', async () => {+      const { page, server } = getTestState();+      await page.goto(server.EMPTY_PAGE);+      await page.evaluate(addElement, 'button');+      await page.waitForSelector('aria/[role="button"]');+    });++    it('should work independently of `exposeFunction`', async () => {+      const { page, server } = getTestState();+      await page.goto(server.EMPTY_PAGE);+      await page.exposeFunction('ariaQuerySelector', (a, b) => a + b);+      await page.evaluate(addElement, 'button');+      await page.waitForSelector('aria/[role="button"]');+      const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)');+      expect(result).toBe(10);+    });++    it('should work with removed MutationObserver', async () => {+      const { page } = getTestState();++      await page.evaluate(() => delete window.MutationObserver);+      const [handle] = await Promise.all([+        page.waitForSelector('aria/anything'),+        page.setContent(`<h1>anything</h1>`),+      ]);+      expect(+        await page.evaluate((x: HTMLElement) => x.textContent, handle)+      ).toBe('anything');+    });++    it('should resolve promise when node is added', async () => {+      const { page, server } = getTestState();++      await page.goto(server.EMPTY_PAGE);+      const frame = page.mainFrame();+      const watchdog = frame.waitForSelector('aria/[role="heading"]');+      await frame.evaluate(addElement, 'br');+      await frame.evaluate(addElement, 'h1');+      const elementHandle = await watchdog;+      const tagName = await elementHandle+        .getProperty('tagName')+        .then((element) => element.jsonValue());+      expect(tagName).toBe('H1');+    });++    it('should work when node is added through innerHTML', async () => {+      const { page, server } = getTestState();++      await page.goto(server.EMPTY_PAGE);+      const watchdog = page.waitForSelector('aria/name');+      await page.evaluate(addElement, 'span');+      await page.evaluate(+        () =>+          (document.querySelector('span').innerHTML =+            '<h3><div aria-label="name"></div></h3>')+      );+      await watchdog;+    });++    it('Page.waitForSelector is shortcut for main frame', async () => {+      const { page, server } = getTestState();++      await page.goto(server.EMPTY_PAGE);+      await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);+      const otherFrame = page.frames()[1];+      const watchdog = page.waitForSelector('aria/[role="button"]');+      await otherFrame.evaluate(addElement, 'button');+      await page.evaluate(addElement, 'button');+      const elementHandle = await watchdog;+      expect(elementHandle.executionContext().frame()).toBe(page.mainFrame());+    });++    it('should run in specified frame', async () => {+      const { page, server } = getTestState();++      await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);+      await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE);+      const frame1 = page.frames()[1];+      const frame2 = page.frames()[2];+      const waitForSelectorPromise = frame2.waitForSelector(+        'aria/[role="button"]'+      );+      await frame1.evaluate(addElement, 'button');+      await frame2.evaluate(addElement, 'button');+      const elementHandle = await waitForSelectorPromise;+      expect(elementHandle.executionContext().frame()).toBe(frame2);+    });++    it('should throw when frame is detached', async () => {+      const { page, server } = getTestState();++      await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);+      const frame = page.frames()[1];+      let waitError = null;+      const waitPromise = frame+        .waitForSelector('aria/does-not-exist')+        .catch((error) => (waitError = error));+      await utils.detachFrame(page, 'frame1');+      await waitPromise;+      expect(waitError).toBeTruthy();+      expect(waitError.message).toContain(+        'waitForFunction failed: frame got detached.'+      );+    });++    it('should survive cross-process navigation', async () => {+      const { page, server } = getTestState();++      let imgFound = false;+      const waitForSelector = page+        .waitForSelector('aria/[role="img"]')+        .then(() => (imgFound = true));+      await page.goto(server.EMPTY_PAGE);+      expect(imgFound).toBe(false);+      await page.reload();+      expect(imgFound).toBe(false);+      await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');+      await waitForSelector;+      expect(imgFound).toBe(true);+    });++    it('should wait for visible', async () => {+      const { page } = getTestState();++      let divFound = false;+      const waitForSelector = page+        .waitForSelector('aria/name', { visible: true })+        .then(() => (divFound = true));+      await page.setContent(+        `<div aria-label='name' style='display: none; visibility: hidden;'>1</div>`+      );+      expect(divFound).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.removeProperty('display')+      );+      expect(divFound).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.removeProperty('visibility')+      );+      expect(await waitForSelector).toBe(true);+      expect(divFound).toBe(true);+    });++    it('should wait for visible recursively', async () => {+      const { page } = getTestState();++      let divVisible = false;+      const waitForSelector = page+        .waitForSelector('aria/inner', { visible: true })+        .then(() => (divVisible = true));+      await page.setContent(+        `<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>`+      );+      expect(divVisible).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.removeProperty('display')+      );+      expect(divVisible).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.removeProperty('visibility')+      );+      expect(await waitForSelector).toBe(true);+      expect(divVisible).toBe(true);+    });++    it('hidden should wait for visibility: hidden', async () => {+      const { page } = getTestState();++      let divHidden = false;+      await page.setContent(+        `<div role='button' style='display: block;'></div>`+      );+      const waitForSelector = page+        .waitForSelector('aria/[role="button"]', { hidden: true })+        .then(() => (divHidden = true));+      await page.waitForSelector('aria/[role="button"]'); // do a round trip+      expect(divHidden).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.setProperty('visibility', 'hidden')+      );+      expect(await waitForSelector).toBe(true);+      expect(divHidden).toBe(true);+    });++    it('hidden should wait for display: none', async () => {+      const { page } = getTestState();++      let divHidden = false;+      await page.setContent(`<div role='main' style='display: block;'></div>`);+      const waitForSelector = page+        .waitForSelector('aria/[role="main"]', { hidden: true })+        .then(() => (divHidden = true));+      await page.waitForSelector('aria/[role="main"]'); // do a round trip+      expect(divHidden).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').style.setProperty('display', 'none')+      );+      expect(await waitForSelector).toBe(true);+      expect(divHidden).toBe(true);+    });++    it('hidden should wait for removal', async () => {+      const { page } = getTestState();++      await page.setContent(`<div role='main'></div>`);+      let divRemoved = false;+      const waitForSelector = page+        .waitForSelector('aria/[role="main"]', { hidden: true })+        .then(() => (divRemoved = true));+      await page.waitForSelector('aria/[role="main"]'); // do a round trip+      expect(divRemoved).toBe(false);+      await page.evaluate(() => document.querySelector('div').remove());+      expect(await waitForSelector).toBe(true);+      expect(divRemoved).toBe(true);+    });++    it('should return null if waiting to hide non-existing element', async () => {+      const { page } = getTestState();++      const handle = await page.waitForSelector('aria/non-existing', {+        hidden: true,+      });+      expect(handle).toBe(null);+    });++    it('should respect timeout', async () => {+      const { page, puppeteer } = getTestState();++      let error = null;+      await page+        .waitForSelector('aria/[role="button"]', { timeout: 10 })+        .catch((error_) => (error = error_));+      expect(error).toBeTruthy();+      expect(error.message).toContain(+        'waiting for selector `[role="button"]` failed: timeout'+      );+      expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);+    });++    it('should have an error message specifically for awaiting an element to be hidden', async () => {+      const { page } = getTestState();++      await page.setContent(`<div role='main'></div>`);+      let error = null;+      await page+        .waitForSelector('aria/[role="main"]', { hidden: true, timeout: 10 })+        .catch((error_) => (error = error_));+      expect(error).toBeTruthy();+      expect(error.message).toContain(+        'waiting for selector `[role="main"]` to be hidden failed: timeout'+      );+    });++    it('should respond to node attribute mutation', async () => {+      const { page } = getTestState();++      let divFound = false;+      const waitForSelector = page+        .waitForSelector('aria/zombo')+        .then(() => (divFound = true));+      await page.setContent(`<div aria-label='notZombo'></div>`);+      expect(divFound).toBe(false);+      await page.evaluate(() =>+        document.querySelector('div').setAttribute('aria-label', 'zombo')+      );+      expect(await waitForSelector).toBe(true);+    });++    it('should return the element handle', async () => {+      const { page } = getTestState();++      const waitForSelector = page.waitForSelector('aria/zombo');+      await page.setContent(`<div aria-label='zombo'>anything</div>`);+      expect(+        await page.evaluate(+          (x: HTMLElement) => x.textContent,+          await waitForSelector+        )+      ).toBe('anything');+    });++    // TODO: Figure out what the stack trace should look like for custom handlers

This is the current stack trace:

"TimeoutError: waiting for selector `zombo` failed: timeout 10ms exceeded
    at new WaitTask (.../puppeteer/lib/cjs/puppeteer/common/DOMWorld.js:494:34)
    at DOMWorld.waitForSelectorInPage (.../puppeteer/lib/cjs/puppeteer/common/DOMWorld.js:427:26)
    at Object.waitFor (.../puppeteer/lib/cjs/puppeteer/common/AriaQueryHandler.js:55:21)
    at process._tickCallback (internal/process/next_tick.js:68:7)"

It would be nice if ariaqueryhandler.spec.ts was part of the trace since that is the actual call site for waitForSelector. The fact that the selector is zombo instead of aria/zombo is maybe a bit unfortunate as well.

johanbay

comment created time in 20 days

PullRequestReviewEvent

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 const queryAllArray = async (   return jsHandle; }; +// If multiple waitFor are set up asynchronously, we need to wait for the first+// one to set up the binding in the page before running the others.+let settingUpBinding = null;++async function addHandlerToWorld(domWorld: DOMWorld) {+  if (settingUpBinding) {+    await settingUpBinding;+  }+  if (!domWorld.hasBinding('ariaQuerySelector')) {+    let done: () => void | null = null;+    settingUpBinding = new Promise((resolve) => {+      done = resolve;+    });+    await domWorld.addBinding('ariaQuerySelector', async (selector: string) => {

Done, but moved into DOMWorld instead.

johanbay

comment created time in 20 days

PullRequestReviewEvent

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 function evaluationString(fun: Function | string, ...args: unknown[]): string {   return `(${fun})(${args.map(serializeArgument).join(',')})`; } +function pageBindingInitString(type: string, name: string): string {+  return evaluationString(addPageBinding, type, name);

Done.

johanbay

comment created time in 20 days

PullRequestReviewEvent

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 export class DOMWorld {     return queryHandler.waitFor(this, updatedSelector, options);   } +  /**+   * @internal+   */+  async addBindingToContext(name: string) {+    const expression = helper.pageBindingInitString('internal', name);+    try {+      const context = await this.executionContext();+      await context._client.send('Runtime.addBinding', {+        name,+        executionContextId: context._contextId,+      });+      await context.evaluate(expression);+    } catch (error) {+      // When the page is navigated, the promise is rejected.+      // We will try again in the new execution context.+      if (error.message.includes('Execution context was destroyed')) return;+      // We could have tried to evaluate in a context which was already+      // destroyed.+      if (error.message.includes('Cannot find context with specified id'))+        return;+      debugError(error);+    }+  }++  /**+   * @internal+   */+  async addBinding(name: string, puppeteerFunction: Function): Promise<void> {+    if (this._ctxBindings.has(name))+      throw new Error(+        `Failed to add page binding with name ${name}: window['${name}'] already exists!`

Good idea, done.

johanbay

comment created time in 20 days

PullRequestReviewEvent

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 class WaitTask {       await success.dispose();       return;     }+    // When frame is detached the task should have been terminated by the DOMWorld.+    // This can fail if we were adding this task while the frame was detached,+    // so we terminate here instead.+    if (error) {+      if (+        error.message.includes(+          'Execution Context is not available in detached frame'

Done. Also updated the actual error.

johanbay

comment created time in 20 days

PullRequestReviewEvent

Pull request review commentpuppeteer/puppeteer

feat(a11y-query): extend aria handler with waitFor

 const queryAllArray = async (   return jsHandle; }; +// If multiple waitFor are set up asynchronously, we need to wait for the first+// one to set up the binding in the page before running the others.+let settingUpBinding = null;

Ah, good point! In practice, bindings are set up per execution context, but the world is responsible for re-binding when the execution context changes. It's fine to add bindings in two different worlds concurrently (it should happen when doing waitForSelector in two different frames), so maybe this code actually synchronizes a bit too much. I'll try pushing it into the worlds instead.

johanbay

comment created time in 20 days

PullRequestReviewEvent
more