Microsoft/fast-dna

View on GitHub
packages/web-components/fast-element/src/templating/view.spec.ts

Summary

Maintainability
F
4 days
Test Coverage
import { expect } from "chai";
import { Message } from "../interfaces.js";
import { ExecutionContext } from "../observation/observable.js";
import { FAST } from "../platform.js";
import { AddViewBehaviorFactory, HTMLDirective, ViewBehavior, ViewBehaviorFactory, ViewController } from "./html-directive.js";
import { Markup } from "./markup.js";
import { html } from "./template.js";
import { HTMLView } from "./view.js";

function startCapturingWarnings() {
    const currentWarn = FAST.warn;
    const list: { code: number, values?: Record<string, any> }[] = [];

    FAST.warn = function(code, values) {
        list.push({ code, values });
    }

    return {
        list,
        dispose() {
            FAST.warn = currentWarn;
        }
    };
}

describe(`The HTMLView`, () => {
    context("when binding hosts", () => {
        it("gracefully handles empty template elements", () => {
            const template = html`
                <template></template>
            `;

            const view = template.create();
            view.bind({});

            expect(view.firstChild).not.to.be.null;
            expect(view.lastChild).not.to.be.null;
        });
        it("gracefully handles empty template literals", () => {
            const template = html``;

            const view = template.create();
            view.bind({});

            expect(view.firstChild).not.to.be.null;
            expect(view.lastChild).not.to.be.null;
        });
        it("warns on class bindings when host not present", () => {
            const template = html`
                <template class="foo"></template>
            `;

            const warnings = startCapturingWarnings();
            const view = template.create();
            view.bind({});
            warnings.dispose();

            expect(warnings.list.length).equal(1);
            expect(warnings.list[0].code).equal(Message.hostBindingWithoutHost);
            expect(warnings.list[0].values!.name).equal("setAttribute");
        });

        it("warns on style bindings when host not present", () => {
            const template = html`
                <template style="color: red"></template>
            `;

            const warnings = startCapturingWarnings();
            const view = template.create();
            view.bind({});
            warnings.dispose();

            expect(warnings.list.length).equal(1);
            expect(warnings.list[0].code).equal(Message.hostBindingWithoutHost);
            expect(warnings.list[0].values!.name).equal("setAttribute");
        });

        it("warns on boolean bindings when host not present", () => {
            const template = html`
                <template ?disabled="${() => false}"></template>
            `;

            const warnings = startCapturingWarnings();
            const view = template.create();
            view.bind({});
            warnings.dispose();

            expect(warnings.list.length).equal(1);
            expect(warnings.list[0].code).equal(Message.hostBindingWithoutHost);
            expect(warnings.list[0].values!.name).equal("removeAttribute");
        });

        it("warns on property bindings when host not present", () => {
            const template = html`
                <template :myProperty="${() => false}"></template>
            `;

            const warnings = startCapturingWarnings();
            const view = template.create();
            view.bind({});
            warnings.dispose();

            expect(warnings.list.length).equal(1);
            expect(warnings.list[0].code).equal(Message.hostBindingWithoutHost);
            expect(warnings.list[0].values!.name).equal("myProperty");
        });

        it("warns on className bindings when host not present", () => {
            const template = html`
                <template :className="${() => "test"}"></template>
            `;

            const warnings = startCapturingWarnings();
            const view = template.create();
            view.bind({});
            warnings.dispose();

            expect(warnings.list.length).equal(1);
            expect(warnings.list[0].code).equal(Message.hostBindingWithoutHost);
            expect(warnings.list[0].values!.name).equal("className");
        });

        it("warns on event bindings when host not present", () => {
            const template = html`
                <template @click="${() => void 0}"></template>
            `;

            const warnings = startCapturingWarnings();
            const view = template.create();
            view.bind({});
            warnings.dispose();

            expect(warnings.list.length).equal(1);
            expect(warnings.list[0].code).equal(Message.hostBindingWithoutHost);
            expect(warnings.list[0].values!.name).equal("addEventListener");
        });
    });

    context("when rebinding", () => {
        it("properly unbinds the old source before binding the new source", () => {
            const sources: any[] = [];
            const boundStates: boolean[] = [];

            class SourceCaptureDirective
                implements HTMLDirective, ViewBehaviorFactory, ViewBehavior {
                id: string;
                nodeId: string;

                createHTML(add: AddViewBehaviorFactory): string {
                    return Markup.attribute(add(this));
                }

                createBehavior(): ViewBehavior<any, any> {
                    return this;
                }

                bind(controller: ViewController<any, any>): void {
                    sources.push(controller.source);
                    boundStates.push(controller.isBound);
                }
            }

            HTMLDirective.define(SourceCaptureDirective);

            const template = html`
                <div ${new SourceCaptureDirective()}></div>
            `;

            const view = template.create();
            const firstSource = {};
            view.bind(firstSource);

            const secondSource = {};
            view.bind(secondSource);

            expect(sources[0]).to.equal(firstSource);
            expect(boundStates[0]).to.be.false;

            expect(sources[1]).to.equal(secondSource);
            expect(boundStates[1]).to.be.false;
        });
    });

    context("execution context", () => {
        function createEvent() {
            const detail = { hello: "world" };
            const event = new CustomEvent('my-event', { detail });

            return { event, detail };
        }

        function createView() {
            return new HTMLView(
                document.createDocumentFragment(),
                [],
                {}
            );
        }

        it("can get the current event", () => {
            const { event } = createEvent();
            const view = createView();
            const context = view.context;

            ExecutionContext.setEvent(event);

            expect(context.event).equals(event);

            ExecutionContext.setEvent(null);
        });

        it("can get the current event detail", () => {
            const { event, detail } = createEvent();
            const view = createView();
            const context = view.context;

            ExecutionContext.setEvent(event);

            expect(context.eventDetail()).equals(detail);
            expect(context.eventDetail<typeof detail>().hello).equals(detail.hello);

            ExecutionContext.setEvent(null);
        });

        it("can connect a child context to a parent source", () => {
            const parentSource = {};
            const parentView = createView();
            const parentContext = parentView.context;
            const childView = createView();
            const childContext = childView.context;

            childContext.parent = parentSource;
            childContext.parentContext = parentContext;

            expect(childContext.parent).equals(parentSource);
            expect(childContext.parentContext).equals(parentContext);
        });

        it("can create an item context from a child context", () => {
            const parentSource = {};
            const parentView = createView();
            const parentContext = parentView.context;
            const itemView = createView();
            const itemContext = itemView.context;

            itemContext.parent = parentSource;
            itemContext.parentContext = parentContext;
            itemContext.index = 7;
            itemContext.length = 42;

            expect(itemContext.parent).equals(parentSource);
            expect(itemContext.parentContext).equals(parentContext);
            expect(itemContext.index).equals(7);
            expect(itemContext.length).equals(42);
        });

        context("item context", () => {
            const scenarios = [
                {
                    name: "even is first",
                    index: 0,
                    length: 42,
                    isEven: true,
                    isOdd: false,
                    isFirst: true,
                    isMiddle: false,
                    isLast: false
                },
                {
                    name: "odd in middle",
                    index: 7,
                    length: 42,
                    isEven: false,
                    isOdd: true,
                    isFirst: false,
                    isMiddle: true,
                    isLast: false
                },
                {
                    name: "even in middle",
                    index: 8,
                    length: 42,
                    isEven: true,
                    isOdd: false,
                    isFirst: false,
                    isMiddle: true,
                    isLast: false
                },
                {
                    name: "odd at end",
                    index: 41,
                    length: 42,
                    isEven: false,
                    isOdd: true,
                    isFirst: false,
                    isMiddle: false,
                    isLast: true
                },
                {
                    name: "even at end",
                    index: 40,
                    length: 41,
                    isEven: true,
                    isOdd: false,
                    isFirst: false,
                    isMiddle: false,
                    isLast: true
                }
            ];

            function assert(itemContext: ExecutionContext, scenario: typeof scenarios[0]) {
                expect(itemContext.index).equals(scenario.index);
                expect(itemContext.length).equals(scenario.length);
                expect(itemContext.isEven).equals(scenario.isEven);
                expect(itemContext.isOdd).equals(scenario.isOdd);
                expect(itemContext.isFirst).equals(scenario.isFirst);
                expect(itemContext.isInMiddle).equals(scenario.isMiddle);
                expect(itemContext.isLast).equals(scenario.isLast);
            }

            for (const scenario of scenarios) {
                it(`has correct position when ${scenario.name}`, () => {
                    const parentSource = {};
                    const parentView = createView();
                    const parentContext = parentView.context;
                    const itemView = createView();
                    const itemContext = itemView.context;

                    itemContext.parent = parentSource;
                    itemContext.parentContext = parentContext;
                    itemContext.index = scenario.index;
                    itemContext.length = scenario.length;

                    assert(itemContext, scenario);
                });
            }

            it ("can update its index and length", () => {
                const scenario1 = scenarios[0];
                const scenario2 = scenarios[1];

                const parentSource = {};
                const parentView = createView();
                const parentContext = parentView.context;
                const itemView = createView();
                const itemContext = itemView.context;

                itemContext.parent = parentSource;
                itemContext.parentContext = parentContext;
                itemContext.index = scenario1.index;
                itemContext.length = scenario1.length;

                assert(itemContext, scenario1);

                itemContext.index = scenario2.index;
                itemContext.length = scenario2.length;

                assert(itemContext, scenario2);
            });
        });
    });
});