Repository: stenciljs/core (TypeScript)

Issue:
HTMLSlotElement assignedElements / assignedNodes not available in spec tests (reopen)
Stencil version:

@stencil/core@2.3.0
I'm submitting a:

[x] bug report
[ ] feature request
[ ] support request => Please do not submit support requests here, use one of these channels: https://stencil-worldwide.herokuapp.com/ or https://forum.ionicframework.com/

Current behavior:
In a .spec.tsx test, The mock HTML element returned from shadowRoot.querySelector('slot[name="items"]') does not have the assignedElements or assignedNodes functions on it.

Expected behavior:
It would have assignedElements and assignedNodes available, as per the HTMLSlotElement API.

I am able to call assignedElements() when running the component in the browsers I support.

Steps to reproduce:

menu-list.tsx

```ts
import { Component, ComponentInterface, Host, h } from '@stencil/core';

@Component({
  tag: 'menu-list',
  styleUrl: 'menu-list.css',
  shadow: true,
})
export class MenuList implements ComponentInterface {

  render() {
    return (
      <Host>
        <slot name="items"></slot>
      </Host>
    );
  }
}
```
menu-list.spec.tsx

```ts
describe('menu-list', () => {
  it('renders', async () => {
    const page = await newSpecPage({
      components: [MenuList],
      html:
      `<menu-list>
        <div slot="items">One</div>
        <div slot="items">Two</div>
        <div slot="items">Three</div>
      </menu-list>`,
    });

    const slotElement:HTMLSlotElement = page.root.shadowRoot.querySelector(
      'slot[name="items"]'
    );

    expect(slotElement.assignedElements()).toBeTruthy();
  });
});
```

I'm reopening the issue: [2606](https://github.com/ionic-team/stencil/issues/2606)
Cos is not solved until now and I have no access to reopen there =/

However, I have the same problem to test the assignedElements, cos I'm using this way into the component:

```ts
this.element.shadowRoot.querySelector('slot').assignedElements()
```

This line cause this error:
![image](https://user-images.githubusercontent.com/10416598/108484070-bd846580-729b-11eb-9042-096ca69dc034.png)


My dependencies:
```json
    "@stencil/core": "2.3.0",
    "@stencil/sass": "1.4.1",
    "@types/jest": "25.1.4",
    "jest": "25.1.0",
    "jest-cli": "25.1.0",
    "yargs": "15.3.0"
```



Discussion:
Wouldn't be it possible to mock HtmlSlotElement and spy on assignedElements?
As far as i see, the MockHTMLElement is either not complete, or it's just not intended to implement all methods of childs of HTMLElement?

As i am new to this topic i'm having no idea on how to perform this mock/spy construction
Same thing happens to me in e2e tests
Any news from this issue?
same thing for me 
Is there movement on this? I'm having the same problem.
This is a problem for me too, in both Specs and E2E tests
Same problem here. We use `assignedNodes()` in `componentDidLoad` so I can't even initialize the test as it errors out before I can write any expectations.
I gave up and changed the implementation to not use `assignedNodes()` anymore =/

So, instead of:
```ts
this.element
        .shadowRoot
        .querySelector('slot')
        .assignedElements()
        .forEach((option: HTMLAbeOptionElement) => option.toggle());
```

I'm using `querySelectorAll()`:
```ts
@Element() element: HTMLAbeDropdownElement;

const options = this.element.querySelectorAll('abe-option');
options.forEach((option: HTMLAbeOptionElement) => option.toggle());
```

Well, this is it =)
Almost made anniversary =P 
I'm seeing the same issue.
experiencing this issue in our unit tests as well.  We use assignedElements in componentWillRender but the element is converted to a mockhtmlelement during the test run and results in the `assignedElements is not a function` error.
> I gave up and changed the implementation to not use assignedNodes() anymore =/

The problem with doing this is that it won't work in scenarios where one component wraps another one and the slotted element needs to pass from one to another.

For example, if you have `ComponentA` that uses `ComponentB` internally in its shadowRoot and you pass the a slotted element from `ComponentA` into a slot of `ComponentB`.

You won't be able to use `querySelector` because the slotted elements are not in the same rootNode.

```tsx
// ComponentA: uses ComponentB internally
render() {
    return (
        <ComponentB>
          <slot />
       </ComponentB>
    );
  }

// ComponentB: needs to get assigned elements of the slot. 
// querySelector won't work in here if ComponentA is used 
// since the slotted elements are in light DOM and not in the
// shadowRoot. It would be much simpler to just use 
// assignedElements to get the elements here.
render() {
    return (
        <slot />
    );
 }
```
Re: the example from the post above, I had a similar scenario. 

Child component: `this.template.querySelector('slot[name="footer"]').assignedElements({flatten: true});`
This retrieved the elements from the slot for me... basically the same thing result alvarocjunq ended up using.

Yep, querying for the slot and getting its assignedElements works perfectly. The only issue is that it won't work in e2e tests so we can't test the functionality properly.
Solved this by changing `.assignedNodes()` to `.assignedNodes?.()` in the code.
I'm experiencing the same problem in stencil 3.0.0: `assignedNodes()` and `assignedElements()` are both `undefined` on slot elements.
A workaround I found that helps preventing `assignedElements is not a function` and similar errors, even when they are used in early lifecycle methods, is the following:

```js
import { MockHTMLElement } from '@stencil/core/mock-doc';

// ...

beforeAll(() => {
  Object.defineProperty(MockHTMLElement.prototype, 'assignedElements', {
    value: () => [], // `jest.fn()` works as well, but will result in just one shared function for all elements
    configurable: true,
    writable: true,
  });
  Object.defineProperty(MockHTMLElement.prototype, 'assignedNodes', {
    value: () => [],
    configurable: true,
    writable: true,
  });
});
```

Importing `MockHTMLElement` in the jest setup file didn't work for me, sadly. For usage in later lifecycle methods, when you need to define the return value of those methods for proper testing, you can do as follows:

```js
const slotElement = page.root.shadowRoot.querySelector<HTMLSlotElement>('slot');
const childElements = Array.from(page.root.children);  // or similar

slotElement.assignedElements = jest.fn().mockReturnValue(childElements);
```

Hope this helps. If anyone has an idea how to make the mocked function return the actually assigned elements/nodes and how to mock those methods for all tests at once (instead of only for the current test file), please let me know. Didn't invest any further.
One way to test if slotted tags are reflected into the DOM could be to listen for the "slotchange" event like this:

`
  it('Test if tag can reflected into DOM', async () => {
    const page = await newE2EPage();
    await page.setContent('<web-component></web-component>');

    let slotTag = await page.find('web-component >>> slot');
    const spy = await slotTag.spyOnEvent('slotchange');

    await page.$eval('web-component', (mainTag: HTMLElement) => {
      let mySlot = mainTag.shadowRoot.querySelector('slot');

      mySlot.addEventListener('slotchange', () => console.log('slot has changed'));
      let slottedTag = document.createElement('select');
      mainTag.appendChild(slottedTag);
    });

    await page.waitForChanges();
    expect(spy).toHaveReceivedEvent();
  });
`
    
    
This tests if slots are added into the DOM, but doesn't test if slotted tags are inserted into the correct place in the DOM. However, this is something that can be used.



Patch:
diff --git a/src/mock-doc/element.ts b/src/mock-doc/element.ts
index 5a57f15c115..ea1f0e5f792 100644
--- a/src/mock-doc/element.ts
+++ b/src/mock-doc/element.ts
@@ -1,8 +1,9 @@
 import { cloneAttributes } from './attribute';
+import { NODE_TYPES } from './constants';
 import { getStyleElementText, MockCSSStyleSheet, setStyleElementText } from './css-style-sheet';
 import { createCustomElement } from './custom-element-registry';
 import { MockDocumentFragment } from './document-fragment';
-import { MockElement, MockHTMLElement } from './node';
+import { MockElement, MockHTMLElement, MockNode } from './node';
 
 export function createElement(ownerDocument: any, tagName: string): any {
   if (typeof tagName !== 'string' || tagName === '' || !/^[a-z0-9-_:]+$/i.test(tagName)) {
@@ -41,6 +42,12 @@ export function createElement(ownerDocument: any, tagName: string): any {
     case 'script':
       return new MockScriptElement(ownerDocument);
 
+    case 'slot':
+      return new MockSlotElement(ownerDocument);
+
+    case 'slot-fb':
+      return new MockHTMLElement(ownerDocument, tagName);
+
     case 'style':
       return new MockStyleElement(ownerDocument);
 
@@ -52,9 +59,6 @@ export function createElement(ownerDocument: any, tagName: string): any {
 
     case 'ul':
       return new MockUListElement(ownerDocument);
-
-    case 'slot-fb':
-      return new MockHTMLElement(ownerDocument, tagName);
   }
 
   if (ownerDocument != null && tagName.includes('-')) {
@@ -514,6 +518,91 @@ export class MockUListElement extends MockHTMLElement {
   }
 }
 
+export class MockSlotElement extends MockHTMLElement {
+  constructor(ownerDocument: any) {
+    super(ownerDocument, 'slot');
+  }
+
+  assignedNodes(opts?: { flatten: boolean }): (MockNode | Node)[] {
+    let nodesToReturn: (MockNode | Node)[] = [];
+
+    const ownerHost = (this.getRootNode() as any).host as MockElement;
+    if (!ownerHost) return nodesToReturn;
+
+    if (ownerHost.childNodes.length) {
+      // try to find lightDOM nodes matching this slot's name (or lack of)
+      if ((this as any).name) {
+        nodesToReturn = ownerHost.childNodes.filter(
+          (n) =>
+            n.nodeType === NODE_TYPES.ELEMENT_NODE && (n as MockElement).getAttribute('slot') === (this as any).name,
+        );
+      } else {
+        // find elements that do not have a slot attribute or
+        // any other type of node
+        nodesToReturn = ownerHost.childNodes.filter(
+          (n) =>
+            (n.nodeType === NODE_TYPES.ELEMENT_NODE && !(n as MockElement).getAttribute('slot')) ||
+            n.nodeType !== NODE_TYPES.ELEMENT_NODE,
+        );
+      }
+      if (nodesToReturn.length) return nodesToReturn;
+    }
+
+    // no flatten option? Return whatever's in this slot (without nested slots)
+    if (!opts?.flatten) return this.childNodes.filter((n) => !(n instanceof MockSlotElement));
+
+    // flatten option? Return all nodes in this slot (including anything within nested slots)
+    return this.childNodes.reduce(
+      (acc, node) => {
+        if (node instanceof MockSlotElement) {
+          acc.push(...node.assignedNodes(opts));
+        } else {
+          acc.push(node);
+        }
+        return acc;
+      },
+      [] as (MockNode | Node)[],
+    );
+  }
+
+  assignedElements(opts?: { flatten: boolean }): (Element | MockHTMLElement)[] {
+    let elesToReturn: (Element | MockHTMLElement)[] = [];
+
+    const ownerHost = (this.getRootNode() as any).host as MockElement;
+    if (!ownerHost) return elesToReturn;
+
+    if (ownerHost.children.length) {
+      // try to find lightDOM elements matching this slot's name (or lack of)
+      if ((this as any).name) {
+        elesToReturn = ownerHost.children.filter((n) => (n as MockElement).getAttribute('slot') == (this as any).name);
+      } else {
+        elesToReturn = ownerHost.children.filter((n) => !(n as MockElement).getAttribute('slot'));
+      }
+      if (elesToReturn.length) return elesToReturn;
+    }
+
+    // no flatten option? Return whatever elements are in this slot (without nested slots)
+    if (!opts?.flatten) return this.children.filter((n) => !(n instanceof MockSlotElement));
+
+    // flatten option? Return all elements in this slot (including anything within nested slots)
+    return this.children.reduce(
+      (acc, node) => {
+        if (node instanceof MockSlotElement) {
+          acc.push(...node.assignedElements(opts));
+        } else {
+          acc.push(node);
+        }
+        return acc;
+      },
+      [] as (MockElement | Element)[],
+    );
+  }
+}
+
+patchPropAttributes(MockSlotElement.prototype, {
+  name: String,
+});
+
 type CanvasContext = '2d' | 'webgl' | 'webgl2' | 'bitmaprenderer';
 export class CanvasRenderingContext {
   context: CanvasContext;


Test patch:
diff --git a/src/mock-doc/test/element.spec.ts b/src/mock-doc/test/element.spec.ts
index 5297f3e7eb8..642fe2ebe42 100644
--- a/src/mock-doc/test/element.spec.ts
+++ b/src/mock-doc/test/element.spec.ts
@@ -521,4 +521,88 @@ describe('element', () => {
     expect(ctx).toBeDefined();
     expect(ctx.toDataURL()).toBe('data:,');
   });
+
+  describe('slot', () => {
+    it('returns no nodes via `assignedNodes` / `assignedElements` when not within a custom element', () => {
+      const slot = doc.createElement('slot');
+      slot.innerHTML = 'invisible <div>Something</div> <!-- not here -->';
+      expect(slot.assignedNodes().length).toEqual(0);
+      expect(slot.assignedElements().length).toEqual(0);
+    });
+
+    it('returns correct nodes with `assignedNodes`', () => {
+      const elm = doc.createElement('cmp-a');
+      const shadowRoot = elm.attachShadow({ mode: 'open' });
+      const slot = doc.createElement('slot');
+      shadowRoot.appendChild(slot);
+      slot.innerHTML = '<slot name="nested-slot">Fallback content</slot>';
+
+      elm.innerHTML = `
+        text node 
+        <div>light dom</div>
+        <!-- comment node -->
+        <div slot="nested-slot">nested slot</div>
+      `;
+
+      expect(slot.assignedNodes().length).toEqual(5);
+      expect(slot.assignedNodes()[0].textContent.trim()).toEqual('text node');
+      expect(slot.assignedNodes()[1].textContent.trim()).toEqual('light dom');
+      expect(slot.assignedNodes()[3].textContent.trim()).toEqual('comment node');
+      expect(slot.assignedNodes()[4].textContent.trim()).toEqual('');
+      expect(slot.assignedNodes({ flatten: true }).length).toEqual(5);
+
+      elm.innerHTML = '<div slot="nested-slot">nested slot</div>';
+
+      expect(slot.assignedNodes().length).toEqual(0);
+      expect(slot.assignedNodes({ flatten: true }).length).toEqual(1);
+      expect(slot.assignedNodes({ flatten: true })[0].textContent.trim()).toEqual('nested slot');
+
+      elm.innerHTML = 'text node <div slot="nested-slot">nested slot</div>';
+
+      expect(slot.assignedNodes().length).toEqual(1);
+      expect(slot.assignedNodes({ flatten: true }).length).toEqual(1);
+      expect(slot.assignedNodes()[0].textContent.trim()).toEqual('text node');
+
+      elm.innerHTML = '';
+      expect(slot.assignedNodes().length).toEqual(0);
+      expect(slot.assignedNodes({ flatten: true }).length).toEqual(1);
+      expect(slot.assignedNodes({ flatten: true })[0].textContent.trim()).toEqual('Fallback content');
+    });
+
+    it('returns correct elements with `assignedElements`', () => {
+      const elm = doc.createElement('cmp-a');
+      const shadowRoot = elm.attachShadow({ mode: 'open' });
+      const slot = doc.createElement('slot');
+      shadowRoot.appendChild(slot);
+      slot.innerHTML = '<slot name="nested-slot"><div>Fallback content</div></slot>';
+
+      elm.innerHTML = `
+        text node 
+        <div>light dom</div>
+        <!-- comment node -->
+        <div slot="nested-slot">nested slot</div>
+      `;
+
+      expect(slot.assignedElements().length).toEqual(1);
+      expect(slot.assignedElements()[0].textContent.trim()).toEqual('light dom');
+      expect(slot.assignedElements({ flatten: true }).length).toEqual(1);
+
+      elm.innerHTML = '<div slot="nested-slot">nested slot</div>';
+
+      expect(slot.assignedElements().length).toEqual(0);
+      expect(slot.assignedElements({ flatten: true }).length).toEqual(1);
+      expect(slot.assignedElements({ flatten: true })[0].textContent.trim()).toEqual('nested slot');
+
+      elm.innerHTML = 'text node <div slot="nested-slot">nested slot</div>';
+
+      expect(slot.assignedElements().length).toEqual(0);
+      expect(slot.assignedElements({ flatten: true }).length).toEqual(1);
+      expect(slot.assignedElements({ flatten: true })[0].textContent.trim()).toEqual('nested slot');
+
+      elm.innerHTML = '';
+      expect(slot.assignedElements().length).toEqual(0);
+      expect(slot.assignedElements({ flatten: true }).length).toEqual(1);
+      expect(slot.assignedElements({ flatten: true })[0].textContent.trim()).toEqual('Fallback content');
+    });
+  });
 });