Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "chore: add empty array repeat fixture and parent-to-child attribute passing",
"packageName": "@microsoft/fast-html",
"email": "7559015+janechu@users.noreply.github.com",
"dependentChangeType": "none"
}
6 changes: 3 additions & 3 deletions packages/fast-html/test/fixtures/nested-elements/entry.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<title>Nested Elements Hydration Tests</title>
</head>
<body>
<parent-element></parent-element>
<parent-element title="Empty List" :items="{{emptyItems}}"></parent-element>
<parent-element title="Single Item" :items="{{singleItem}}"></parent-element>
<parent-element category="{{category}}"></parent-element>
<parent-element title="Empty List" :items="{{emptyItems}}" category="{{category}}"></parent-element>
<parent-element title="Single Item" :items="{{singleItem}}" category="{{category}}"></parent-element>
<script type="module" src="./main.ts"></script>
</body>
</html>
27 changes: 20 additions & 7 deletions packages/fast-html/test/fixtures/nested-elements/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,61 @@
<title>Nested Elements Hydration Tests</title>
</head>
<body>
<parent-element><template shadowrootmode="open" shadowroot="open"><div class="list-container">
<parent-element category="General"><template shadowrootmode="open" shadowroot="open"><div class="list-container">
<h2><!--fe-b$$start$$0$$title-0$$fe-b-->My Items<!--fe-b$$end$$0$$title-0$$fe-b--></h2>
<div class="items">
<!--fe-b$$start$$1$$repeat-1$$fe-b--><!--fe-repeat$$start$$0$$fe-repeat-->
<child-element text="Item 1" idx="0" data-fe-c-0-2><template shadowrootmode="open" shadowroot="open"><div class="item">
<child-element text="Item 1" idx="0" category="General" data-fe-c-0-3><template shadowrootmode="open" shadowroot="open"><div class="item">
<span class="index"><!--fe-b$$start$$0$$idx-0$$fe-b-->0<!--fe-b$$end$$0$$idx-0$$fe-b--></span>
<span class="text"><!--fe-b$$start$$1$$text-1$$fe-b-->Item 1<!--fe-b$$end$$1$$text-1$$fe-b--></span>
<grand-child-element category="General" data-fe-c-2-1><template shadowrootmode="open" shadowroot="open"><span class="category"><!--fe-b$$start$$0$$category-0$$fe-b-->General<!--fe-b$$end$$0$$category-0$$fe-b--></span></template></grand-child-element>
</div></template></child-element>
<!--fe-repeat$$end$$0$$fe-repeat--><!--fe-repeat$$start$$1$$fe-repeat-->
<child-element text="Item 2" idx="1" data-fe-c-0-2><template shadowrootmode="open" shadowroot="open"><div class="item">
<child-element text="Item 2" idx="1" category="General" data-fe-c-0-3><template shadowrootmode="open" shadowroot="open"><div class="item">
<span class="index"><!--fe-b$$start$$0$$idx-0$$fe-b-->1<!--fe-b$$end$$0$$idx-0$$fe-b--></span>
<span class="text"><!--fe-b$$start$$1$$text-1$$fe-b-->Item 2<!--fe-b$$end$$1$$text-1$$fe-b--></span>
<grand-child-element category="General" data-fe-c-2-1><template shadowrootmode="open" shadowroot="open"><span class="category"><!--fe-b$$start$$0$$category-0$$fe-b-->General<!--fe-b$$end$$0$$category-0$$fe-b--></span></template></grand-child-element>
</div></template></child-element>
<!--fe-repeat$$end$$1$$fe-repeat--><!--fe-repeat$$start$$2$$fe-repeat-->
<child-element text="Item 3" idx="2" data-fe-c-0-2><template shadowrootmode="open" shadowroot="open"><div class="item">
<child-element text="Item 3" idx="2" category="General" data-fe-c-0-3><template shadowrootmode="open" shadowroot="open"><div class="item">
<span class="index"><!--fe-b$$start$$0$$idx-0$$fe-b-->2<!--fe-b$$end$$0$$idx-0$$fe-b--></span>
<span class="text"><!--fe-b$$start$$1$$text-1$$fe-b-->Item 3<!--fe-b$$end$$1$$text-1$$fe-b--></span>
<grand-child-element category="General" data-fe-c-2-1><template shadowrootmode="open" shadowroot="open"><span class="category"><!--fe-b$$start$$0$$category-0$$fe-b-->General<!--fe-b$$end$$0$$category-0$$fe-b--></span></template></grand-child-element>
</div></template></child-element>
<!--fe-repeat$$end$$2$$fe-repeat--><!--fe-b$$end$$1$$repeat-1$$fe-b-->
</div>
</div></template></parent-element>
<parent-element title="Empty List"><template shadowrootmode="open" shadowroot="open"><div class="list-container">
<parent-element title="Empty List" category="General"><template shadowrootmode="open" shadowroot="open"><div class="list-container">
<h2><!--fe-b$$start$$0$$title-0$$fe-b-->Empty List<!--fe-b$$end$$0$$title-0$$fe-b--></h2>
<div class="items">
<!--fe-b$$start$$1$$repeat-1$$fe-b--><!--fe-b$$end$$1$$repeat-1$$fe-b-->
</div>
</div></template></parent-element>
<parent-element title="Single Item"><template shadowrootmode="open" shadowroot="open"><div class="list-container">
<parent-element title="Single Item" category="General"><template shadowrootmode="open" shadowroot="open"><div class="list-container">
<h2><!--fe-b$$start$$0$$title-0$$fe-b-->Single Item<!--fe-b$$end$$0$$title-0$$fe-b--></h2>
<div class="items">
<!--fe-b$$start$$1$$repeat-1$$fe-b--><!--fe-repeat$$start$$0$$fe-repeat-->
<child-element text="Only Item" idx="0" data-fe-c-0-2><template shadowrootmode="open" shadowroot="open"><div class="item">
<child-element text="Only Item" idx="0" category="General" data-fe-c-0-3><template shadowrootmode="open" shadowroot="open"><div class="item">
<span class="index"><!--fe-b$$start$$0$$idx-0$$fe-b-->0<!--fe-b$$end$$0$$idx-0$$fe-b--></span>
<span class="text"><!--fe-b$$start$$1$$text-1$$fe-b-->Only Item<!--fe-b$$end$$1$$text-1$$fe-b--></span>
<grand-child-element category="General" data-fe-c-2-1><template shadowrootmode="open" shadowroot="open"><span class="category"><!--fe-b$$start$$0$$category-0$$fe-b-->General<!--fe-b$$end$$0$$category-0$$fe-b--></span></template></grand-child-element>
</div></template></child-element>
<!--fe-repeat$$end$$0$$fe-repeat--><!--fe-b$$end$$1$$repeat-1$$fe-b-->
</div>
</div></template></parent-element>
<f-template name="grand-child-element">
<template>
<span class="category">{{category}}</span>
</template>
</f-template>
<f-template name="child-element">
<template>
<div class="item">
<span class="index">{{idx}}</span>
<span class="text">{{text}}</span>
<grand-child-element
category="{{category}}"
></grand-child-element>
</div>
</template>
</f-template>
Expand All @@ -61,6 +73,7 @@ <h2>{{title}}</h2>
<child-element
text="{{item.text}}"
idx="{{$index}}"
category="{{category}}"
></child-element>
</f-repeat>
</div>
Expand Down
25 changes: 20 additions & 5 deletions packages/fast-html/test/fixtures/nested-elements/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FASTElement } from "@microsoft/fast-element";
import { attr, FASTElement } from "@microsoft/fast-element";
import { RenderableFASTElement, TemplateElement } from "@microsoft/fast-html";
import { deepMerge } from "@microsoft/fast-html/utilities.js";

Expand Down Expand Up @@ -47,6 +47,9 @@ export class ItemList extends RenderableElement {

public title!: string;

@attr
public category!: string;

// Track which list instance this is (1st, 2nd, or 3rd on the page)
private static prepareCallCount = 0;
private static instanceMap = new WeakMap<ItemList, number>();
Expand Down Expand Up @@ -79,14 +82,13 @@ export class Item extends RenderableElement {

public idx!: number;

@attr
public category!: string;

// Track which item instance this is globally
private static prepareCallCount = 0;
private static instanceMap = new WeakMap<Item, number>();

constructor() {
super();
}

async prepare() {
// Assign instance number on first prepare() call for this instance
if (!Item.instanceMap.has(this)) {
Expand All @@ -105,6 +107,11 @@ export class Item extends RenderableElement {
}
}

export class GrandChildItem extends RenderableElement {
@attr
public category!: string;
}

RenderableFASTElement(ItemList).defineAsync({
name: "parent-element",
templateOptions: "defer-and-hydrate",
Expand All @@ -115,6 +122,11 @@ RenderableFASTElement(Item).defineAsync({
templateOptions: "defer-and-hydrate",
});

RenderableFASTElement(GrandChildItem).defineAsync({
name: "grand-child-element",
templateOptions: "defer-and-hydrate",
});

(window as any).messages = [];

TemplateElement.options({
Expand All @@ -124,6 +136,9 @@ TemplateElement.options({
"child-element": {
observerMap: "all",
},
"grand-child-element": {
observerMap: "all",
},
})
.config({
elementDidDefine(name: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { test, expect } from "@playwright/test";
import { expect, test } from "@playwright/test";
import type { ItemList } from "./main.js";

test.describe("Nested Elements Hydration", () => {
test("should hydrate parent elements before child elements", async ({ page }) => {
Expand All @@ -7,21 +8,21 @@ test.describe("Nested Elements Hydration", () => {
const messages = (await page.evaluate("window.messages")) as string[];

const parentDefinitionIndex = messages.findIndex(message =>
message.startsWith("Element did define: parent-element")
message.startsWith("Element did define: parent-element"),
);
const firstChildHydrationIndex = messages.findIndex(message =>
message.startsWith("Element will hydrate: child-element")
message.startsWith("Element will hydrate: child-element"),
);

expect(parentDefinitionIndex).toBeGreaterThan(-1);
expect(firstChildHydrationIndex).toBeGreaterThan(-1);
expect(parentDefinitionIndex).toBeLessThan(firstChildHydrationIndex);

const childHydrationStarts = messages.filter(message =>
message.startsWith("Element will hydrate: child-element")
message.startsWith("Element will hydrate: child-element"),
);
const childHydrationCompletes = messages.filter(message =>
message.startsWith("Element did hydrate: child-element")
message.startsWith("Element did hydrate: child-element"),
);

// Non-zero proves the fixture actually exercised nested hydration.
Expand All @@ -30,4 +31,42 @@ test.describe("Nested Elements Hydration", () => {
// Equal counts mean every child that started hydration also finished.
expect(childHydrationStarts.length).toBe(childHydrationCompletes.length);
});

test("should pass parent attribute to child elements", async ({ page }) => {
await page.goto("/fixtures/nested-elements/");

const parentElements = page.locator("parent-element");
const firstParent = parentElements.nth(0);
const childElements = firstParent.locator("child-element");

const childCount = await childElements.count();
expect(childCount).toBeGreaterThan(0);

// Each child receives the parent's category as an attribute
for (let i = 0; i < childCount; i++) {
await expect(childElements.nth(i)).toHaveAttribute("category", "General");
}

// Grand-child-element renders the category passed from parent β†’ child β†’ grand-child
const grandChildren = firstParent.locator("grand-child-element");
for (let i = 0; i < childCount; i++) {
const categoryText = grandChildren.nth(i).locator(".category");
await expect(categoryText).toHaveText("General");
}

await firstParent.evaluate((node: ItemList) => {
node.category = "Updated";
});

// Child attributes propagate the updated value
for (let i = 0; i < childCount; i++) {
await expect(childElements.nth(i)).toHaveAttribute("category", "Updated");
}

// Grand-child-element renders the updated category
for (let i = 0; i < childCount; i++) {
const categoryText = grandChildren.nth(i).locator(".category");
await expect(categoryText).toHaveText("Updated");
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"title": "My Items",
"items": [{ "text": "Item 1" }, { "text": "Item 2" }, { "text": "Item 3" }],
"emptyItems": [],
"singleItem": [{ "text": "Only Item" }]
"singleItem": [{ "text": "Only Item" }],
"category": "General"
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
<f-template name="grand-child-element">
<template>
<span class="category">{{category}}</span>
</template>
</f-template>
<f-template name="child-element">
<template>
<div class="item">
<span class="index">{{idx}}</span>
<span class="text">{{text}}</span>
<grand-child-element
category="{{category}}"
></grand-child-element>
</div>
</template>
</f-template>
Expand All @@ -15,6 +23,7 @@ <h2>{{title}}</h2>
<child-element
text="{{item.text}}"
idx="{{$index}}"
category="{{category}}"
></child-element>
</f-repeat>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/fast-html/test/fixtures/repeat/entry.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<test-element-no-item-repeat-binding
list="{{emptyList}}"
></test-element-no-item-repeat-binding>
<test-element-empty-array :list="{{emptyList}}"></test-element-empty-array>
<test-element-event :list="{{eventList}}"></test-element-event>
<script type="module" src="./main.ts"></script>
</body>
Expand Down
12 changes: 12 additions & 0 deletions packages/fast-html/test/fixtures/repeat/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
<test-element-no-item-repeat-binding><template shadowrootmode="open" shadowroot="open"><ul>
<!--fe-b$$start$$0$$repeat-0$$fe-b--><!--fe-b$$end$$0$$repeat-0$$fe-b-->
</ul></template></test-element-no-item-repeat-binding>
<test-element-empty-array><template shadowrootmode="open" shadowroot="open"><ul>
<!--fe-b$$start$$0$$repeat-0$$fe-b--><!--fe-b$$end$$0$$repeat-0$$fe-b-->
</ul></template></test-element-empty-array>
<test-element-event><template shadowrootmode="open" shadowroot="open"><!--fe-b$$start$$0$$repeat-0$$fe-b--><!--fe-repeat$$start$$0$$fe-repeat-->
<button data-fe-c-0-1>Click</button>
<!--fe-repeat$$end$$0$$fe-repeat--><!--fe-b$$end$$0$$repeat-0$$fe-b--></template></test-element-event>
Expand Down Expand Up @@ -94,6 +97,15 @@
</f-repeat>
</template>
</f-template>
<f-template name="test-element-empty-array">
<template>
<ul>
<f-repeat value="{{item in list}}">
<li>{{item}}</li>
</f-repeat>
</ul>
</template>
</f-template>
<f-template name="test-element-interval-updates">
<template>
<ul>
Expand Down
13 changes: 12 additions & 1 deletion packages/fast-html/test/fixtures/repeat/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { FASTElement, observable } from "@microsoft/fast-element";
import { attr, FASTElement, observable } from "@microsoft/fast-element";
import { RenderableFASTElement, TemplateElement } from "@microsoft/fast-html";
import { deepMerge } from "@microsoft/fast-html/utilities.js";

export class TestElement extends FASTElement {
@observable
list: Array<string> = ["Foo", "Bar"];

@attr
item_parent: string = "Bat";
}
RenderableFASTElement(TestElement).defineAsync({
Expand Down Expand Up @@ -56,6 +57,15 @@ RenderableFASTElement(TestElementNoItemRepeatBinding).defineAsync({
templateOptions: "defer-and-hydrate",
});

export class TestElementEmptyArray extends FASTElement {
@observable
list: Array<string> = [];
}
RenderableFASTElement(TestElementEmptyArray).defineAsync({
name: "test-element-empty-array",
templateOptions: "defer-and-hydrate",
});

export class TestElementEvent extends FASTElement {
@observable
list: Array<string> = ["A"];
Expand All @@ -75,6 +85,7 @@ RenderableFASTElement(TestElementEvent).defineAsync({
export class TestElementWithObserverMap extends FASTElement {
list: Array<string> = ["Foo", "Bar"];

@attr
item_parent: string = "Bat";
}
RenderableFASTElement(TestElementWithObserverMap).defineAsync({
Expand Down
25 changes: 25 additions & 0 deletions packages/fast-html/test/fixtures/repeat/repeat.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, test } from "@playwright/test";
import type {
TestElement,
TestElementEmptyArray,
TestElementEvent,
TestElementIntervalUpdates,
} from "./main.js";
Expand Down Expand Up @@ -187,4 +188,28 @@ test.describe("f-template", async () => {
await buttons.nth(1).click();
await expect(element).toHaveJSProperty("clickCount", 2);
});

test("repeat directive with an empty array should render no items", async ({
page,
}) => {
await page.goto("/fixtures/repeat/");

const element = page.locator("test-element-empty-array");
const listItems = element.locator("li");

await expect(listItems).toHaveCount(0);

await element.evaluate((node: TestElementEmptyArray) => {
node.list = ["A", "B", "C"];
});

await expect(listItems).toHaveCount(3);
await expect(listItems).toContainText(["A", "B", "C"]);

await element.evaluate((node: TestElementEmptyArray) => {
node.list = [];
});

await expect(listItems).toHaveCount(0);
});
});
9 changes: 9 additions & 0 deletions packages/fast-html/test/fixtures/repeat/templates.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@
</f-repeat>
</template>
</f-template>
<f-template name="test-element-empty-array">
<template>
<ul>
<f-repeat value="{{item in list}}">
<li>{{item}}</li>
</f-repeat>
</ul>
</template>
</f-template>
<f-template name="test-element-interval-updates">
<template>
<ul>
Expand Down
Loading