Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error when encountering DocumentFragment #1243

Closed
2 tasks done
ankur22 opened this issue Mar 13, 2024 · 2 comments
Closed
2 tasks done

Error when encountering DocumentFragment #1243

ankur22 opened this issue Mar 13, 2024 · 2 comments
Assignees
Labels
bug Something isn't working user request Requested by the community

Comments

@ankur22
Copy link
Collaborator

ankur22 commented Mar 13, 2024

Brief summary

When a test tries to interact with a website that contains DocumentFragments, the browser module will error out or timeout. The error that is returned is usually something like:

ERRO[0000] Uncaught (in promise) GoError: waiting for selector "//p[@id=\"inDocFrag\"]": DOMException: Failed to execute 'evaluate' on 'Document': The node provided is '#document-fragment', which is not a valid context node type.
    at XPathQueryEngine.queryAll (__xk6_browser_evaluation_script__:131:25)
    at InjectedScript._queryEngineAll (__xk6_browser_evaluation_script__:158:42)
    at InjectedScript._querySelectorRecursively (__xk6_browser_evaluation_script__:214:20)
    at InjectedScript._exploreShadowDOM (__xk6_browser_evaluation_script__:243:38)
    at InjectedScript._exploreShadowDOM (__xk6_browser_evaluation_script__:256:35)
    at InjectedScript._exploreShadowDOM (__xk6_browser_evaluation_script__:256:35)
    at InjectedScript._exploreShadowDOM (__xk6_browser_evaluation_script__:256:35)
    at InjectedScript._querySelectorRecursively (__xk6_browser_evaluation_script__:228:34)
    at InjectedScript.querySelectorAll (__xk6_browser_evaluation_script__:651:25)
    at predicate (__xk6_browser_evaluation_script__:921:29)
        at github.com/grafana/xk6-browser/browser.mapPage.func16 (native)
        at file:///Users/ankuragarwal/go/src/github.com/grafana/xk6-browser/e2e/shadow-and-doc-frags.js:25:23(20)  executor=shared-iterations scenario=browser

I've seen this occur with waitFor and waitForSelector APIs. Other APIs that work with the same underlying injected script will also suffer from the same issue, i.e. any API that needs to run an XPath query against the DOM.

xk6-browser version

v1.4.1

OS

NA

Chrome version

NA

Docker version and image (if applicable)

NA

Steps to reproduce the problem

Use the following html file:

HTML file
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DocumentFragment and ShadowRoot Test page</title>
</head>
<body>
    <h2>DocumentFragment and ShadowRoot Test page</h2>
    <div id="docFrag"></div>

    <!-- Element that will host the Shadow DOM -->
    <div id="shadowHost"></div>

    <script>
        function addDocFrag() {
            const container = document.getElementById('docFrag');
            const fragment = document.createDocumentFragment();

            // Add some additional text in a paragraph
            const paragraph = document.createElement('p');
            paragraph.id = 'inDocFrag'; // Set the id of the div
            paragraph.textContent = 'This text is added via a document fragment!';
            fragment.appendChild(paragraph);

            // Append the fragment to the container
            container.appendChild(fragment);
        }

        function addShadowDom() {
            const shadowHost = document.getElementById('shadowHost');
            // When mode is set to closed, we cannot access internals with JS.
            // We will need to create a custom element that exposes these
            // internals with getters and setters.
            const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

            // Create a DocumentFragment to add to the Shadow DOM
            const fragment = document.createDocumentFragment();

            // Add some styled content to the fragment
            const styleElement = document.createElement('style');
            styleElement.textContent = `
                p {
                    color: blue;
                    font-weight: bold;
                }
            `;
            fragment.appendChild(styleElement);

            const paragraphElement = document.createElement('p');
            paragraphElement.id = 'inShadowRootDocFrag';
            paragraphElement.textContent = 'This is inside Shadow DOM, added via a DocumentFragment!';
            fragment.appendChild(paragraphElement);

            // Append the DocumentFragment to the Shadow DOM.
            shadowRoot.appendChild(fragment);
        }

        function done() {
            // Create a new div element which will reside in the original Document.
            const doneDiv = document.createElement('div');
            doneDiv.id = 'done';
            doneDiv.textContent = "All additions to page completed (i'm in the original document)";

            // Append it to the original Document.
            document.body.appendChild(doneDiv);
        }

        addDocFrag();
        addShadowDom();
        done();
    </script>
</body>
</html>

Run this test with ./k6 run test.js:

Test file
import { browser } from 'k6/experimental/browser';
import { check } from 'k6';

export const options = {
  scenarios: {
    browser: {
      executor: 'shared-iterations',
      options: {
        browser: {
          type: 'chromium',
        },
      },
    },
  },
  thresholds: {
    checks: ['rate==1.0'],
  },
};

export default async function () {
  const page = browser.newPage();

  await page.goto('http://localhost:81/static/doc-frag.html');

  page.waitForSelector('//p[@id="inDocFrag"]', {
    timeout: 10000,
    state: 'attached',
  });

  check(page, {
    'inDocFrag': p => p.locator('//p[@id="inDocFrag"]').innerText() == 'This text is added via a document fragment!',
  });

  page.waitForSelector('//p[@id="inShadowRootDocFrag"]', {
    timeout: 10000,
    state: 'attached',
  });

  check(page, {
    'inShadowRootDocFrag': p => p.locator('//p[@id="inShadowRootDocFrag"]').innerText() == 'This is inside Shadow DOM, added via a DocumentFragment!',
  });

  page.waitForSelector('//div[@id="done"]', {
    timeout: 10000,
    state: 'attached',
  });

  check(page, {
    'done': p => p.locator('//div[@id="done"]').innerText() == "All additions to page completed (i'm in the original document)",
  });

  page.close();
}

Which will result in the error specified in the summary.

Expected behaviour

The test passes and all checks pass:

     ✓ inDocFrag
     ✓ inShadowRootDocFrag
     ✓ done

Actual behaviour

Errors on the first waitForSelector with the error in the summary.

Tasks

@ankur22 ankur22 added the bug Something isn't working label Mar 13, 2024
@ankur22
Copy link
Collaborator Author

ankur22 commented Mar 13, 2024

The stack trace points here in the injected_script.js file.

After close inspection of the error it turns out that the root in question when this error occurs is a DocumentFragment. Looking at what DocumentFragment implements its clear that it:

  1. Doesn't implement evaluate which is what XPathQueryEngine works with;
  2. therefore doesn't work with XPath selectors.

The possible ways forward were:

  1. Can we convert the XPath selector to a CSS selector? This likely requires an external dependency.
  2. Can we convert the DocumentFragment into a Document?

After some exploration it turns out that we can convert DocumentFragment into a Document without adding any external dependencies.

@ankur22
Copy link
Collaborator Author

ankur22 commented Mar 13, 2024

Another issue that was discovered was when working with a user supplied test script:

Test script
import { browser } from 'k6/experimental/browser';
import { sleep } from 'k6';

export const options = {
  scenarios: {
    browser: {
      executor: 'shared-iterations',
      options: {
        browser: {
          type: 'chromium',
        },
      },
    },
  },
  thresholds: {
    checks: ['rate==1.0'],
  },
};

export default async function () {
  const page = browser.newPage();

  try {
    await page.goto(url);

    const splashScreen = page.locator('//div[@id="splash-screen"][@class="animate finished"]');
    splashScreen.waitFor({ state: 'attached', timeout: 10000 });

  } finally {
    page.close();
  }
}

This resulted in the error:

ERRO[0004] Uncaught (in promise) GoError: waiting for "//div[@id=\"splash-screen\"][@class=\"animate finished\"]": Promise was collected
running at github.com/grafana/xk6-browser/common.(*Locator).WaitFor-fm (native)

So a promise isn't being handled and was lost.

Debugging the injected_script.js file, it would seem that we're not handling errors here, and so it is being swallowed which prevents us from understanding the cause of the error. We should try/catch in the polling function and reject when an error occurs.

@ankur22 ankur22 self-assigned this Mar 13, 2024
@ankur22 ankur22 added the user request Requested by the community label Mar 13, 2024
@ankur22 ankur22 closed this as completed Mar 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working user request Requested by the community
Projects
None yet
Development

No branches or pull requests

1 participant