Skip to content
Open
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": "prerelease",
"comment": "fix(fast-html): add errors fixture for f-template error cases",
"packageName": "@microsoft/fast-html",
"email": "7559015+janechu@users.noreply.github.com",
"dependentChangeType": "none"
}
8 changes: 6 additions & 2 deletions packages/fast-html/src/components/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@ class TemplateElement extends FASTElement {

const registeredFastElement: FASTElementDefinition | undefined =
fastElementRegistry.getByType(value);
const template = this.getElementsByTagName("template").item(0);
const templates = this.getElementsByTagName("template");

if (template) {
if (templates.length === 1) {
// Callback: Before template has been evaluated and assigned
TemplateElement.lifecycleCallbacks.templateWillUpdate?.(name);

Expand Down Expand Up @@ -248,6 +248,10 @@ class TemplateElement extends FASTElement {
values,
);
}
} else if (templates.length > 1) {
throw FAST.error(Message.moreThanOneTemplateProvided, {
name: this.name,
});
} else {
throw FAST.error(Message.noTemplateProvided, { name: this.name });
}
Expand Down
1 change: 1 addition & 0 deletions packages/fast-html/src/debug.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const debugMessages = {
[2000 /* noTemplateProvided */]: `The first child of the <f-template> must be a <template>, this is missing from ${name}.`,
[2001 /* moreThanOneTemplateProvided */]: `There can only be one <template> inside the <f-template>, you must add one for ${name}.`,
Comment on lines 2 to +3
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debugMessages entries are defined using template literals, so ${name} is being interpolated by JavaScript at module evaluation time (e.g. to window.name in browsers) instead of being left as a placeholder for FAST.error(code, { name }) to format. These messages should be plain strings containing the literal ${name} placeholder so formatMessage() can substitute runtime values.

Suggested change
[2000 /* noTemplateProvided */]: `The first child of the <f-template> must be a <template>, this is missing from ${name}.`,
[2001 /* moreThanOneTemplateProvided */]: `There can only be one <template> inside the <f-template>, you must add one for ${name}.`,
[2000 /* noTemplateProvided */]: "The first child of the <f-template> must be a <template>, this is missing from ${name}.",
[2001 /* moreThanOneTemplateProvided */]: "There can only be one <template> inside the <f-template>, you must add one for ${name}.",

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new moreThanOneTemplateProvided message text is misleading: it says "you must add one" even though this error is thrown when multiple <template> elements are present. Update the wording to instruct users to remove extra <template> elements and keep exactly one.

Suggested change
[2001 /* moreThanOneTemplateProvided */]: `There can only be one <template> inside the <f-template>, you must add one for ${name}.`,
[2001 /* moreThanOneTemplateProvided */]: `There can only be one <template> inside the <f-template>; remove any extra <template> elements and keep exactly one for ${name}.`,

Copilot uses AI. Check for mistakes.
};
1 change: 1 addition & 0 deletions packages/fast-html/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
*/
export const enum Message {
noTemplateProvided = 2000,
moreThanOneTemplateProvided = 2001,
}
49 changes: 49 additions & 0 deletions packages/fast-html/test/fixtures/errors/errors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expect, test } from "@playwright/test";

test.describe("f-template errors", () => {
test("throws an error when no template element is present", async ({ page }) => {
await page.addInitScript(() => {
(window as any).__errors = [];
window.addEventListener("unhandledrejection", event => {
(window as any).__errors.push(
event.reason?.message ?? String(event.reason),
);
});
Comment on lines +7 to +11
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the unhandledrejection handler, the event isn't preventDefault()'d. Since these tests intentionally trigger unhandled rejections, calling event.preventDefault() will suppress default reporting/noise and reduce the risk of future harness/config changes treating the rejection as a test failure.

Copilot uses AI. Check for mistakes.
});
Comment on lines +4 to +12
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The page.addInitScript setup for capturing unhandledrejection is duplicated in both tests. Consider moving this into a test.beforeEach (or a small helper) so the listener/storage initialization is defined once and stays consistent if it changes.

Copilot uses AI. Check for mistakes.

await page.goto("/fixtures/errors/");

await expect
.poll(async () => {
const errors: string[] = await page.evaluate(
() => (window as any).__errors,
);
return errors.some(msg => msg.includes("must be a <template>"));
})
.toBe(true);
});

test("throws an error when multiple template elements are present", async ({
page,
}) => {
await page.addInitScript(() => {
(window as any).__errors = [];
window.addEventListener("unhandledrejection", event => {
(window as any).__errors.push(
event.reason?.message ?? String(event.reason),
);
});
});

await page.goto("/fixtures/errors/");

await expect
.poll(async () => {
const errors: string[] = await page.evaluate(
() => (window as any).__errors,
);
return errors.some(msg => msg.includes("only be one <template>"));
})
.toBe(true);
});
});
19 changes: 19 additions & 0 deletions packages/fast-html/test/fixtures/errors/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<title>Errors</title>
</head>
<body>
<test-element-no-template></test-element-no-template>
<test-element-multiple-templates></test-element-multiple-templates>
<f-template name="test-element-no-template">
<span>No template element provided</span>
</f-template>
<f-template name="test-element-multiple-templates">
<template><span>First template</span></template>
<template><span>Second template</span></template>
</f-template>
<script type="module" src="./main.ts"></script>
</body>
</html>
14 changes: 14 additions & 0 deletions packages/fast-html/test/fixtures/errors/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FASTElement } from "@microsoft/fast-element";
import { TemplateElement } from "@microsoft/fast-html";

class TestElementNoTemplate extends FASTElement {}
FASTElement.define(TestElementNoTemplate, { name: "test-element-no-template" });

class TestElementMultipleTemplates extends FASTElement {}
FASTElement.define(TestElementMultipleTemplates, {
name: "test-element-multiple-templates",
});

TemplateElement.define({
name: "f-template",
});
Loading