Microsoft/fast-dna

View on GitHub
packages/web-components/fast-ssr/src/element-renderer/elemenent-renderer.spec.ts

Summary

Maintainability
F
5 days
Test Coverage
import "../install-dom-shim.js";
import { FASTElement, customElement, css, html, attr, observable, when } from "@microsoft/fast-element";
import { expect, test } from '@playwright/test';
import { SyncFASTElementRenderer } from "./fast-element-renderer.js";
import fastSSR from "../exports.js";
import { consolidate, consolidateAsync } from "../test-utilities/consolidate.js";
import { uniqueElementName } from "@microsoft/fast-element/testing.js";
import { PendingTaskEvent } from "@microsoft/fast-element/pending-task.js";

@customElement({
    name: "bare-element",
})
export class BareElement extends FASTElement {}
@customElement({
    name: "styled-element",
    styles: css`:host { display: block; }${css`:host { color: red; }`}
    `
})
export class StyledElement extends FASTElement {}
@customElement({
    name: "host-binding-element",
    template: html`
        <template attr="attr" ?bool-attr="${() => true}"></template>
    `
})
export class HostBindingElement extends FASTElement {}


test.describe("FASTElementRenderer", () => {
    test.describe("should have a 'matchesClass' method", () => {
        test("that returns true when invoked with a class that extends FASTElement ",  () => {
            class MyElement extends FASTElement {}
            expect(SyncFASTElementRenderer.matchesClass(MyElement, "", new Map())).toBe(true);
        });
        test("that returns false when invoked with a class that does not extend FASTElement ", () => {
            class MyElement extends HTMLElement {}
            expect(SyncFASTElementRenderer.matchesClass(MyElement, "", new Map())).toBe(false);
        });

        test("should return false when the provided class has been disabled", () => {
            class MyElement extends FASTElement {}
            const { ElementRenderer } = fastSSR();

            ElementRenderer.disable(MyElement);

            expect(ElementRenderer.matchesClass(MyElement, "", new Map())).toBe(false)
        });
        test("should return false when the provided tag-name has been disabled", () => {
            const name = uniqueElementName();
            class MyElement extends FASTElement {}
            const { ElementRenderer } = fastSSR();

            ElementRenderer.disable(name);

            expect(ElementRenderer.matchesClass(MyElement, name, new Map())).toBe(false)
        });
        test("should return false when the provided element definition has been disabled", () => {
            const name = uniqueElementName();
            class MyElement extends FASTElement {}
            const definition = MyElement.compose(name);
            const { ElementRenderer } = fastSSR();

            ElementRenderer.disable(definition);

            expect(ElementRenderer.matchesClass(MyElement, name, new Map())).toBe(false)
        });
    });

    test.describe("rendering stylesheets", () => {
        test(`should render stylesheets as 'style' elements by default`, () => {
            const { templateRenderer } = fastSSR();
            const result = consolidate(templateRenderer.render(html`<styled-element></styled-element>`));
            expect(result).toBe("<styled-element><template shadowrootmode=\"open\"><style>:host { display: block; }</style><style>:host { color: red; }</style></template></styled-element>");
        });
        test.skip(`should render stylesheets as 'fast-style' elements when configured`, () => {
            const { templateRenderer } = fastSSR(/* Replace w/ configuration when fast-style work is complete{useFASTStyle: true}*/);
            const result = consolidate(templateRenderer.render(html`<styled-element></styled-element>`));
            expect(result).toBe(`<styled-element><template shadowrootmode=\"open\"><fast-style style-id="fast-style-0" css=":host { display: block; }\"></fast-style><fast-style style-id=\"fast-style-1\" css=\":host { color: red; }"></fast-style></template></styled-element>`);
        });
    });

    test("should render attributes on the root of a template element to the host element", () => {
        const { templateRenderer } = fastSSR();
        const result = consolidate(templateRenderer.render(html`
            <host-binding-element></host-binding-element>
        `));
        expect(result).toBe(`
            <host-binding-element attr="attr" bool-attr><template shadowrootmode=\"open\"></template></host-binding-element>
        `);
    });

    test.describe("rendering an element with attributes", () => {
        test("should not render the attribute when binding evaluates null", () => {
            const { templateRenderer } = fastSSR();
            const result = consolidate(templateRenderer.render(html`
                <bare-element attr="${x => null}"></bare-element>
            `));
            expect(result).toBe(`
                <bare-element ><template shadowrootmode=\"open\"></template></bare-element>
            `);
        });
        test("should not render the attribute when the binding evaluates undefined", () => {
            const { templateRenderer } = fastSSR();
            const result = consolidate(templateRenderer.render(html`
                <bare-element attr="${x => undefined}"></bare-element>
            `));
            expect(result).toBe(`
                <bare-element ><template shadowrootmode=\"open\"></template></bare-element>
            `);
        });

        test("should render a boolean attribute with the values of true or false", () => {
            const { templateRenderer } = fastSSR();
            const result = consolidate(templateRenderer.render(html`
                <bare-element ?attr="${x => true}"></bare-element>
                <bare-element ?attr="${x => false}"></bare-element>
            `));
            expect(result).toBe(`
                <bare-element  attr><template shadowrootmode=\"open\"></template></bare-element>
                <bare-element ><template shadowrootmode=\"open\"></template></bare-element>
            `);
        });

        test("should render a non-boolean attribute with the values of true or false", () => {
            const { templateRenderer } = fastSSR();
            const result = consolidate(templateRenderer.render(html`
                <bare-element aria-expanded="${x => true}"></bare-element>
                <bare-element aria-expanded="${x => false}"></bare-element>
            `));
            expect(result).toBe(`
                <bare-element  aria-expanded="true"><template shadowrootmode=\"open\"></template></bare-element>
                <bare-element  aria-expanded="false"><template shadowrootmode=\"open\"></template></bare-element>
            `);
        });

        test("should render an attribute with a string value", () => {
            const { templateRenderer } = fastSSR();
            const result = consolidate(templateRenderer.render(html`
                <bare-element attr="${x => 'my-str-value'}"></bare-element>
            `));
            expect(result).toBe(`
                <bare-element  attr="my-str-value"><template shadowrootmode=\"open\"></template></bare-element>
            `);
        });

        test("should throw error when rendering an attribute with an object value", () => {
            const { templateRenderer } = fastSSR();
            try {
                consolidate(templateRenderer.render(html`<bare-element attr="${x => ({ key: 'my-value' })}"></bare-element>`));
            } catch (error) {
                expect(error).toEqual(new Error("Cannot assign attribute 'attr' for element bare-element."));
            }
        });
    });

    test.describe("emitting events", () => {

    @customElement("test-event-dispatch")
    class TestEventDispatch extends FASTElement {
        @attr({attribute: "event-detail"})
        eventDetail: string = "";

        @attr({ attribute: "listen-self", mode: "boolean"})
        listenSelf: boolean = false;

        @attr({ attribute: "stop-immediate-prop", mode: "boolean"})
        stopImmediateProp: boolean = false;

        @attr({ attribute: "stop-prop", mode: "boolean"})
        stopProp: boolean = false;

        connectedCallback(): void {
            super.connectedCallback();
            const e = new CustomEvent<{ data: string }>("test-event", {bubbles: true, detail: { data: ""}})

            if (this.listenSelf) {
                this.addEventListener("test-event", (e: any) => {
                    e.detail.data = "listen-self-success";
                    e.stopPropagation();
                });
            }

            if (this.stopProp) {
                this.addEventListener("test-event", (e: any) => {
                    e.detail.data = "stop-prop-success";
                    e.stopPropagation();
                });
            }
            if (this.stopImmediateProp) {
                this.addEventListener("test-event", (e: any) => {
                    e.detail.data = "stop-immediate-prop-success";
                    e.stopImmediatePropagation();
                });
                this.addEventListener("test-event", (e: any) => {
                    e.detail.data = "stop-immediate-prop-failure";
                });
            }

            this.dispatchEvent(e);

            this.eventDetail = e.detail.data || "";
        }
    }

    @customElement("test-event-listener")
    class TestEventListener extends FASTElement {
        @attr
        data: string = "default"

        connectedCallback() {
            super.connectedCallback();
            this.addEventListener("test-event", (e: Event) => {
                ( e as CustomEvent<{ data: string }> ).detail.data = this.data;
            })
        }
    }
        test("An element dispatching an event should get it's own handler fired", () => {
            const { templateRenderer } = fastSSR();
            const result = consolidate(templateRenderer.render(html`<test-event-dispatch listen-self></test-event-dispatch>` ));
            expect(result).toBe(`<test-event-dispatch  event-detail=\"listen-self-success\" listen-self><template shadowrootmode="open"></template></test-event-dispatch>`)
        });
        test("An ancestor with a handler should get it's handler invoked if the event bubbles", () => {
            const { templateRenderer } = fastSSR();

            const result = consolidate(templateRenderer.render(html`<test-event-listener data="bubble-success"><test-event-dispatch></test-event-dispatch></test-event-listener>`));
            expect(result).toBe("<test-event-listener  data=\"bubble-success\"><template shadowrootmode=\"open\"></template><test-event-dispatch event-detail=\"bubble-success\"><template shadowrootmode=\"open\"></template></test-event-dispatch></test-event-listener>")
        });
        test("Should bubble events to the document", () => {
            document.addEventListener("test-event", (e) => {
                (e as any).detail.data = "document-success";
            });
            const { templateRenderer } = fastSSR();

            const result = consolidate(templateRenderer.render(html`<test-event-dispatch></test-event-dispatch>`));

            expect(result).toBe(`<test-event-dispatch event-detail=\"document-success\"><template shadowrootmode=\"open\"></template></test-event-dispatch>`);
        });
        test("Should bubble events to the window", () => {
            window.addEventListener("test-event", (e) => {
                (e as any).detail.data = "window-success";
            });
            const { templateRenderer } = fastSSR();

            const result = consolidate(templateRenderer.render(html`<test-event-dispatch></test-event-dispatch>`));
            expect(result).toBe(`<test-event-dispatch event-detail=\"window-success\"><template shadowrootmode=\"open\"></template></test-event-dispatch>`);
        });
        test("Should not bubble an event that invokes event.stopImmediatePropagation()", () => {
            const { templateRenderer } = fastSSR();

            const result = consolidate(templateRenderer.render(html`<test-event-listener data="stop-immediate-propagation-failure"><test-event-dispatch stop-immediate-prop></test-event-dispatch></test-event-listener>`));
            expect(result).toBe(`<test-event-listener  data=\"stop-immediate-propagation-failure\"><template shadowrootmode=\"open\"></template><test-event-dispatch  event-detail=\"stop-immediate-prop-success\" stop-immediate-prop><template shadowrootmode=\"open\"></template></test-event-dispatch></test-event-listener>`)
        });
        test("Should not bubble an event that invokes event.stopPropagation()", () => {
            const { templateRenderer } = fastSSR();

            const result = consolidate(templateRenderer.render(html`<test-event-listener data="stop-propagation-failure"><test-event-dispatch stop-prop></test-event-dispatch></test-event-listener>`));
            expect(result).toBe(`<test-event-listener  data=\"stop-propagation-failure\"><template shadowrootmode=\"open\"></template><test-event-dispatch  event-detail=\"stop-prop-success\" stop-prop><template shadowrootmode=\"open\"></template></test-event-dispatch></test-event-listener>`)
        });
    });

   test.describe("rendering asynchronously", () => {
        test("should support attribute mutation for the element as a result of PendingTask events", async () => {
            const name = uniqueElementName();
            @customElement({
                name,
            })
            class MyElement extends FASTElement {
                connectedCallback(): void {
                    super.connectedCallback();
                    this.dispatchEvent(new PendingTaskEvent(new Promise((resolve) => {
                        window.setTimeout(() => {
                            this.setAttribute("async-resolved", "");
                            resolve();
                        }, 20);
                    })));
                }
            }

            const template = html`<${html.partial(name)}></${html.partial(name)}>`;
            const { templateRenderer } = fastSSR({renderMode: "async"});

            expect(await consolidateAsync(templateRenderer.render(template))).toBe(`<${name} async-resolved><template shadowrootmode="open"></template></${name}>`)
        });


        test("should render elements that have rejected PendingTaskEvents", async () => {
            const name = uniqueElementName();
            @customElement({
                name,
            })
            class MyElement extends FASTElement {
                connectedCallback(): void {
                    super.connectedCallback();
                    this.dispatchEvent(new PendingTaskEvent(new Promise((resolve, reject) => {
                        window.setTimeout(() => {
                            this.setAttribute("async-reject", "");
                            reject();
                        }, 20);
                    })));
                }
            }

            const template = html`<${html.partial(name)}></${html.partial(name)}>`;
            const { templateRenderer } = fastSSR({renderMode: "async"});

            expect(await consolidateAsync(templateRenderer.render(template))).toBe(`<${name} async-reject><template shadowrootmode="open"></template></${name}>`)
        });
        test("should await multiple PendingTaskEvents", async () => {
            const name = uniqueElementName();
            @customElement({
                name,
            })
            class MyElement extends FASTElement {
                connectedCallback(): void {
                    super.connectedCallback();
                    this.dispatchEvent(new PendingTaskEvent(new Promise((resolve) => {
                        window.setTimeout(() => {
                            this.setAttribute("async-resolved-one", "");
                            resolve();
                        }, 20);
                    })));
                    this.dispatchEvent(new PendingTaskEvent(new Promise((resolve) => {
                        window.setTimeout(() => {
                            this.setAttribute("async-resolved-two", "");
                            resolve();
                        }, 30);
                    })));
                }
            }

            const template = html`<${html.partial(name)}></${html.partial(name)}>`;
            const { templateRenderer } = fastSSR({renderMode: "async"});

            expect(await consolidateAsync(templateRenderer.render(template))).toBe(`<${name} async-resolved-one async-resolved-two><template shadowrootmode="open"></template></${name}>`)
        });
        test("should render template content only displayed after PendingTaskEvent is resolved", async () => {
            const name = uniqueElementName();
            @customElement({
                name,
                template: html`${when(x => x.renderContent, html`<h1>Async content success</h1>`)}`
            })
            class MyElement extends FASTElement {
                @observable
                renderContent: boolean = false;
                connectedCallback(): void {
                    super.connectedCallback();
                    this.dispatchEvent(new PendingTaskEvent(new Promise((resolve) => {
                        window.setTimeout(() => {
                            this.renderContent = true;
                            resolve();
                        }, 20);
                    })));
                }
            }

            const template = html`<${html.partial(name)}></${html.partial(name)}>`;
            const { templateRenderer } = fastSSR({renderMode: "async"});

            expect(await consolidateAsync(templateRenderer.render(template))).toBe(`<${name}><template shadowrootmode="open"><h1>Async content success</h1></template></${name}>`)
        });
        test("should support nested async rendering scenarios", async () => {
            const name = uniqueElementName();
            @customElement({
                name,
                template: html`<slot></slot>`
            })
            class MyElement extends FASTElement {
                @observable
                renderContent: boolean = false;
                connectedCallback(): void {
                    super.connectedCallback();
                    this.dispatchEvent(new PendingTaskEvent(new Promise((resolve) => {
                        window.setTimeout(() => {
                            this.setAttribute("async-resolved", "")
                            resolve();
                        }, 20);
                    })));
                }
            }

            const template = html`<${html.partial(name)}><${html.partial(name)}></${html.partial(name)}></${html.partial(name)}>`;
            const { templateRenderer } = fastSSR({renderMode: "async"});

            expect(await consolidateAsync(templateRenderer.render(template))).toBe(`<${name} async-resolved><template shadowrootmode="open"><slot></slot></template><${name} async-resolved><template shadowrootmode="open"><slot></slot></template></${name}></${name}>`)
        });
    })
});