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

selector cannot select within an open shadowDOM #109

Open
sukima opened this issue Oct 5, 2023 · 10 comments
Open

selector cannot select within an open shadowDOM #109

sukima opened this issue Oct 5, 2023 · 10 comments

Comments

@sukima
Copy link
Contributor

sukima commented Oct 5, 2023

The current querySelector/querySelectorAll doesn’t have a syntax to select within a shadowDOM.

Some solutions:

  1. Create a new helper called shadowSelector that knows to use the element.shadowRoot.querySelector(). But we would need to allow the helper to take a second query for scoping.
  2. Create the above helper but force consumers to nest their selectors thus internally using this.element.shadowRoot.querySelector.
  3. Introduce a custom selector like :shadow to allow diving into from the selector helper like: myButton = selector(‘foo-bar::shadow button’); But that means parsing the string before passing it to querySelector.
@sukima
Copy link
Contributor Author

sukima commented Oct 5, 2023

1 could look like

class FooBar extends PageObject {
  myButton = shadowSelector(‘custom-element’, ‘button’);
}

2 might look like

class FooBar extends PageObject {
  shadow = shadowSelector(‘custom-element’, class extends PageObject {
    theButton = selector(‘button’);
  });

  get myButton() {
    return this.shadow.theButton;
  }
}

3 might look like:

class FooBar extends PageObject {
  myButton = selector(‘custom-element::shadow button’);
}

@sukima
Copy link
Contributor Author

sukima commented Oct 5, 2023

It is actually not possible to wrap a ShadowRoot with a PageObject at the moment making this a blocker for using fractal for custom elements.

class FooBar extends PageObject {
  get shadow() {
    return new PageObject(this.element.shadowRoot);
  }
}

Demo with QUnit

@bendemboski
Copy link
Owner

Thanks for the issue! In my mind, I've broken this down into two separate issues:

  1. Allowing PageObjects to wrap DocumentFragments (this covers ShadowRoots because they are DocumentFragments)
  2. Providing a nice declarative API for accessing the shadow DOM

1 is implemented in #110 (which is nice because now PageObjects can be used on DocumentFragments in any other context as well). I haven't touched 2 yet, because I think it requires more thought and maybe experimentation, but I think what I've implemented allows you to implement API-compatible forms of your three options as helpers utilities in your app. If not, let me know!

@bendemboski
Copy link
Owner

Oh, and have a look at #110 and tell me what you think -- as long as you don't see any glaring issues, I'll merge it and cut a release so you can try it out.

@sukima
Copy link
Contributor Author

sukima commented Oct 5, 2023

Looks like #110 would satisfy the feature.

If we were to explore options for a more declarative API what are your thoughts on that interface? How do you feel about introducing an unofficial ::shadow selector?

@bendemboski
Copy link
Owner

I'd definitely like to explore other options first. That would require doing our own parsing of selectors, which is something I'd very much like to avoid.

fractal-page-object kinda tries to wrap itself pretty tightly around native APIs, and CSS selectors, and as far as I know the ::shadow pseudo-selector was an abortive experiment in the ecosystem, so I don't think it's a good idea to introduce it into fractal-page-object. Currently "getting into" the shadow DOM in Javascript requires more than just a single CSS selector/query however you do it, so I think I want to just treat that as a limitation and see what declarative APIs we can build that embrace that.

@bendemboski
Copy link
Owner

v0.5.0 released with low-level shadow DOM support

@sukima
Copy link
Contributor Author

sukima commented Feb 1, 2024

Just out of curiosity would an API like this work?

class FooBar extends PageObject {
  somethingWhichHasAShadowRoot = shadowSelector('.something', class extends PageObject {
    somethingWithinTheShadowRoot = selector('.something-inside');
  });
}

Then it would look like page.somethingWhichHasAShadowRoot.somethingWithinTheShadowRoot.element but under the hood shadowSelector performs an extra step to assert this.element and return new PageObject(this.element.shadowRoot)?

Or if that doesn't support lazy evaluation maybe a different ShadowPageObject to extend from that knows to look for this.element.shadowRoot on demand?

class FooBar extends PageObject {
  somethingWhichHasAShadowRoot = selector('.something', class extends ShadowPageObject {
    somethingWithinTheShadowRoot = selector('.something-inside');
  });
}

@bendemboski
Copy link
Owner

@sukima I've put some thought into this, and need to put some more in, but I think making the shadow DOM vs regular DOM distinction on the page object that is the root of the shadow DOM may not be the way to go. If I want to be able to interact with both the element's shadow DOM and its children/descendants in the regular DOM, I'd have to create two separate page objects, e.g.

class FooBar extends PageObject {
  somethingWhichHasAShadowRoot = shadowSelector('.something', class extends PageObject {
    somethingInTheShadowDOM = selector('.something-shadowy');
  });
  somethingWhichHasAShadowRoot2 = selector('.something', class extends PageObject {
    somethingInTheRegularDOM = selector('.something-not-shadowy');
  });
}

and that seems counter to how we like to model our DOMs with page objects. I think what we probably want it to end up looking like is something more like

class FooBar extends PageObject {
  somethingWhichHasAShadowRoot = selector('.something', class extends PageObject {
    somethingInTheShadowDOM = shadowSelector('.something-shadowy');
    somethingInTheRegularDOM = selector('.something-not-shadowy');
  });
}

where shadowSelector() can be used to select something in the shadow DOM attached to its parent, the same as selector() can be used to select something in the regular DOM "attached to" (i.e. descendants of) its parent.

This forces you to define a page object for the element with the shadow root, which maybe isn't a huge problem, but also might not be ideal. So maybe it would make sense to overload shadowSelector() to either accept a single selector as its argument, or two selectors, and in the two selector case, the first one selects the element with the shadow root and the second one selects within the shadow DOM. So with

class FooBar extends PageObject {
  somethingWhichHasAShadowRoot = selector('.something', class extends PageObject {
    somethingInTheShadowDOM = shadowSelector('.something-shadowy');
  });

  somethingInTheShadowDOM2 = shadowSelector('.something', '.something-shadowy');
}

new FooBar().somethingWhichHasAShadowRoot.somethingInTheShadowDOM.element would be the same as new FooBar().somethingInTheShadowDOM2.element.

I'll have to think some more about the feasibility of implementing an API like this, but hopefully it's doable.

But just from an API design standpoint, what do you think?

@sukima
Copy link
Contributor Author

sukima commented Mar 25, 2024

@bendemboski Apologies, this dropped below the fold. I think we are on the same page. Some thoughts:

  1. I can't speak for everyone but I'm so used to making sub-PageObjects it is kind of second nature to me. So much so that I tend to name them instead of defining them anonymously inline. I'm OK with this suggestion.
  2. The second option to provide two selectors would also work. Though slightly different from the selector interface since it is a different function the API can change slightly without breaking the mental model. I'm OK with this suggestion.
  3. If it helps, for me, I tend to have the mental model that a shadow DOM is a child of the parent anyway. Least that is how the Web Dev Tools renders them. Thus the idea that shadowSelector drills into the parent's shadow DOM makes sense to me. I understand it is slightly different then the mental model of multiple selectors separated by spaces that nested selector() implies. (foo bar versus foo > bar). I don't know how best to reconcile this with shadow DOMs and perhaps maybe we shouldn't try.

The suggestions you gave would work for my purposes and if you are still comfortable with that proposed interface I would not mind looking deeper into the current source code―no guarantees I'll get something done though 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants