diff --git a/change/@microsoft-fast-html-when-00615379-81a9-4dd0-86af-bd600a8ab155.json b/change/@microsoft-fast-html-when-00615379-81a9-4dd0-86af-bd600a8ab155.json new file mode 100644 index 00000000000..a1e216b54fc --- /dev/null +++ b/change/@microsoft-fast-html-when-00615379-81a9-4dd0-86af-bd600a8ab155.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore(fast-html): use @microsoft/fast-build to build when fixture", + "packageName": "@microsoft/fast-html", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/crates/microsoft-fast-build/DESIGN.md b/crates/microsoft-fast-build/DESIGN.md index b32b852dded..14e72d49449 100644 --- a/crates/microsoft-fast-build/DESIGN.md +++ b/crates/microsoft-fast-build/DESIGN.md @@ -66,7 +66,7 @@ fn render_node(template, root, loop_vars, locator, hydration: Option<&mut Hydrat The `is_entry` flag distinguishes two rendering contexts: -- **`is_entry: true`** — the template is the top-level entry HTML. Custom elements found at this level (root custom elements) receive the **full root state** as their child rendering context. +- **`is_entry: true`** — the template is the top-level entry HTML. Custom elements found at this level (root custom elements) receive the **full root state merged with their own attribute-derived state** as their child rendering context. Root state provides app-level context; per-element attributes overlay on top. - **`is_entry: false`** — the template is a shadow template, a directive body, or a repeat item. Custom elements found here receive **attribute-based child state** as usual. `renderer::render_entry_with_locator` sets `is_entry: true`; `renderer::render_with_locator` sets `is_entry: false`. All recursive calls from `render_when`, `render_repeat_items`, and `render_custom_element` (for shadow templates) always use `is_entry: false`. @@ -217,8 +217,8 @@ A custom element is any opening tag whose name contains a hyphen, excluding `f-w 2. **Detect self-closing** — if the character before `>` (ignoring whitespace) is `/`, the element is self-closing. The output always uses non-self-closing form. 3. **Parse attributes** — `parse_element_attributes` walks the opening tag string and extracts `(name, Option)` pairs. 4. **Build child state**: - - **Root custom elements** (`is_entry: true`) — the child state is the **complete root state** (`root.clone()`). This mirrors the runtime behaviour: root elements receive application state rather than per-instance attribute state. All top-level state keys are available directly in their templates. - - **Nested custom elements** (`is_entry: false`) — the child state is built from the element's HTML attributes: + - **Root custom elements** (`is_entry: true`) — the child state starts with the **complete root state** as a base, then each HTML attribute on the element is resolved and overlaid on top (attribute-derived values take precedence). This ensures app-level context keys (e.g. `error`, `showProgress`) are available alongside per-element attribute state (e.g. `planet="earth"`, `vara="3"`, boolean `show`). + - **Nested custom elements** (`is_entry: false`) — the child state is built entirely from the element's HTML attributes: - Attributes starting with `@` (event handlers) or named `f-ref`, `f-slotted`, `f-children` (attribute directives) are **skipped** — all are resolved entirely by the FAST client runtime and have no meaning in server-side rendering state. - Attributes starting with `:` (property bindings) are **stripped from rendered HTML** but their resolved value **is added to the child state** under the lowercased property name (without the `:` prefix). This lets structured data (arrays, objects) be passed to the SSR template without appearing as a visible HTML attribute. - **HTML attribute keys are lowercased** — HTML attribute names are case-insensitive and browsers always store them lowercase. `isEnabled` becomes `isenabled`; hyphens are preserved: `selected-user-id` stays `selected-user-id`. @@ -246,7 +246,7 @@ A custom element is any opening tag whose name contains a hyphen, excluding `f-w ``` When a nested element has attribute bindings (`{{expr}}` or `{expr}` values) and is being rendered inside another element's shadow (i.e., `parent_hydration` is `Some`), those bindings are counted, `data-fe-c-{start}-{count}` is added to the element's opening tag, and the binding indices are allocated from the parent scope. -Note: `is_entry` controls both child state and opening-tag attribute handling. Root elements (`is_entry: true`) resolve primitive `{{binding}}` attributes to their values and strip non-primitive ones. +Note: `is_entry` controls both child state and opening-tag attribute handling. Root elements (`is_entry: true`) merge root state with attribute-derived state for their template, and resolve primitive `{{binding}}` attributes to their values (stripping non-primitive ones) in the rendered HTML. If a custom element has no matching template, it is left in place by `next_directive` (which only returns `CustomElement` for tags in the locator). diff --git a/crates/microsoft-fast-build/README.md b/crates/microsoft-fast-build/README.md index ccb731cdd7b..fa724f45dfe 100644 --- a/crates/microsoft-fast-build/README.md +++ b/crates/microsoft-fast-build/README.md @@ -273,34 +273,40 @@ let html = render_template_with_locator( )?; ``` -### Rendering entry HTML (root custom elements receive full state) +### Rendering entry HTML (root custom elements receive merged state) -When rendering the top-level **entry HTML** of a page, use `render_entry_with_locator` or `render_entry_template_with_locator`. Custom elements found at this level are treated as **root elements** and receive the complete root state rather than building their child state from HTML attributes. +When rendering the top-level **entry HTML** of a page, use `render_entry_with_locator` or `render_entry_template_with_locator`. Custom elements found at this level are treated as **root elements** and receive the complete root state **merged with their own HTML attribute-derived state**. Attribute-derived values take precedence over root state for overlapping keys. -This mirrors the FAST runtime behaviour: root-level components access application state (e.g. via `$fastController.context`) rather than per-instance attribute state. +This gives root elements access to both app-level context (from the shared root state) and their own per-element attribute values (e.g. `planet="earth"`, `vara="3"`, boolean `show`), without requiring each attribute to be present in the state JSON. ```rust use microsoft_fast_build::{render_entry_with_locator, render_entry_template_with_locator, Locator}; let locator = Locator::from_patterns(&["./components/**/*.html"])?; -// Root custom elements (my-header, my-app) receive the full root state. -// Their templates can reference any key in the state JSON directly. +// Root custom elements (my-header, my-app) receive the full root state merged with +// any per-element attributes. Their templates can reference any key in the state JSON directly. let html = render_entry_template_with_locator( - r#"{{heading}}"#, + r#"{{heading}}"#, r#"{"heading": "Hello", "user": "Alice", "items": [{"name": "Item 1"}]}"#, &locator, )?; // my-header's template can use {{user}}, {{heading}}, etc. directly. -// my-app's template can use {{items}}, etc. directly. +// my-app's template can use {{items}}, {{planet}}, etc. directly. ``` #### Attribute handling on root elements -Because root custom elements receive the full root state, `{{binding}}` attributes on root elements are **resolved** rather than forwarded via child state as they would be for nested elements: +Root custom elements receive a **merged child state**: the full root state as a base, with the element's own HTML attributes overlaid on top. This means: + +- Per-element attributes (e.g. `planet="earth"`, `vara="3"`, boolean `show`) are available in the template alongside root state keys. +- `{{binding}}` attributes resolve their value from root state and add it to the child state under the (lowercased) attribute name. +- Attribute-derived values take precedence over root state keys when the same key appears in both. + +`{{binding}}` attributes on root elements are also **resolved in the rendered HTML output** rather than forwarded: - **Primitive bindings** (`string`, `number`, `boolean`) — resolved and rendered with the resolved value. e.g. `text="{{message}}"` → `text="Hello world"`. -- **Non-primitive bindings** (`array`, `object`, `null`) — stripped. These cannot be represented as HTML attribute values; the state is available directly in the element's template. +- **Non-primitive bindings** (`array`, `object`, `null`) — stripped from the HTML output (the state is still available in the element's template via the merged child state). - **Static attributes** (no binding syntax) — passed through unchanged. ```html @@ -317,15 +323,15 @@ Nested custom elements inside shadow templates continue to use attribute-based c | Context | Child state source | `{{binding}}` attrs in rendered HTML | |---|---|---| -| Root element in entry HTML (via `render_entry_*`) | Full root state | Resolved — primitives kept, non-primitives stripped | +| Root element in entry HTML (via `render_entry_*`) | Root state merged with element's own attrs | Resolved — primitives kept, non-primitives stripped | | Nested element inside a shadow template | Attributes on the element tag | Rendered (resolved) | | Element inside `f-repeat` or `f-when` (at any level) | Attributes on the element tag | Rendered (resolved) | ```html - - + + - + ``` diff --git a/crates/microsoft-fast-build/src/directive.rs b/crates/microsoft-fast-build/src/directive.rs index b477519b451..c05be58fb77 100644 --- a/crates/microsoft-fast-build/src/directive.rs +++ b/crates/microsoft-fast-build/src/directive.rs @@ -97,7 +97,7 @@ pub fn render_when( let start = hy.start_marker(idx, &name); let end = hy.end_marker(idx, &name); let inner_content = if evaluate(&expr, root, loop_vars) { - let mut child_scope = hy.child(); + let mut child_scope = hy.child(idx); render_node(&inner, root, loop_vars, locator, Some(&mut child_scope), false)? } else { String::new() @@ -251,36 +251,32 @@ pub fn render_custom_element( open_tag_content[..open_tag_content.len() - 1].to_string() }; - // Build child state. + // Build the child state used to render this element's shadow template. // - // **Entry custom elements** — those marked with `is_entry` — receive the **full root - // state** as their child rendering state. This mirrors the runtime behaviour: entry - // elements receive state from the application (e.g. via `$fastController.context`) - // rather than from HTML attributes, so all top-level state keys are available directly - // in their templates. + // **Entry custom elements** (`is_entry == true`) receive the full root state merged + // with their own HTML attribute-derived state. The root state provides app-level + // context (e.g. `error`, `showProgress`), while per-element attributes (e.g. + // `planet="earth"`, `vara="3"`, boolean `show`) overlay on top so templates that + // compare against per-element values resolve correctly. Attribute-derived values take + // precedence over root state for overlapping keys. // - // All other custom elements (`is_entry == false`) receive state built from their HTML - // attributes as usual (`:prop` forwarded typed, regular attrs lowercased, `data-*` - // grouped). This includes elements rendered inside `f-when`/`f-repeat` bodies, even - // when they are not under a parent hydration scope. - // - // We avoid cloning `root` for the entry case by using an `Option` that is `None` for - // entry elements (falling back to `root` via `unwrap_or`) and `Some(owned)` for - // attribute-derived nested state. - let nested_child_state = if is_entry { - None - } else { - // Nested element: build child state from the element's HTML attributes. - // `data-*` attributes are stored using the full dot-notation path returned by - // `data_attr_to_dataset_key` (e.g. `"dataset.dateOfBirth"`), split on the first - // `.` to build a nested state object so `{{dataset.X}}` bindings resolve correctly. + // **Nested custom elements** (`is_entry == false`) receive state built solely from + // their HTML attributes (`:prop` forwarded typed, regular attrs lowercased, `data-*` + // grouped). This includes elements rendered inside `f-when`/`f-repeat` bodies. + fn build_attr_state( + open_tag_content: &str, + root: &JsonValue, + loop_vars: &[(String, JsonValue)], + base: Option>, + ) -> JsonValue { let attrs = parse_element_attributes(open_tag_content); - let mut state_map = std::collections::HashMap::new(); + let mut state_map = base.unwrap_or_default(); for (attr_name, value) in &attrs { // Skip @event handlers — they are client-side only and have no meaning in SSR state. - // Also skip f-ref, f-slotted, and f-children attribute directives — all are resolved - // entirely by the FAST client runtime. + // Also skip f-ref, f-slotted, f-children, and ?boolean-binding directives — all are + // resolved entirely by the FAST client runtime. if attr_name.starts_with('@') + || attr_name.starts_with('?') || attr_name.eq_ignore_ascii_case("f-ref") || attr_name.eq_ignore_ascii_case("f-slotted") || attr_name.eq_ignore_ascii_case("f-children") @@ -315,7 +311,19 @@ pub fn render_custom_element( state_map.insert(key, json_val); } } - Some(JsonValue::Object(state_map)) + JsonValue::Object(state_map) + } + + let nested_child_state = if is_entry { + // Start with the full root state, then overlay per-element attribute values. + let base = if let JsonValue::Object(ref root_map) = root { + root_map.clone() + } else { + std::collections::HashMap::new() + }; + Some(build_attr_state(open_tag_content, root, loop_vars, Some(base))) + } else { + Some(build_attr_state(open_tag_content, root, loop_vars, None)) }; let child_root = nested_child_state.as_ref().unwrap_or(root); diff --git a/crates/microsoft-fast-build/src/hydration.rs b/crates/microsoft-fast-build/src/hydration.rs index 979b7692ace..bc9c0b7354f 100644 --- a/crates/microsoft-fast-build/src/hydration.rs +++ b/crates/microsoft-fast-build/src/hydration.rs @@ -1,17 +1,34 @@ /// Hydration state for one template scope. /// Each custom-element shadow, f-when body, and f-repeat item template gets its own scope. +/// +/// `scope_prefix` is prepended to marker names so that nested f-when bodies produce names +/// that are unique within the full shadow DOM. For example, a f-when body whose parent +/// allocated index 1 gets `scope_prefix = "1-"`, yielding inner markers like +/// `` instead of ``. +/// This prevents the FAST client runtime from confusing an inner end-marker with the +/// outer end-marker when both would otherwise share the same `id` field. pub struct HydrationScope { pub binding_idx: usize, + scope_prefix: String, } impl HydrationScope { pub fn new() -> Self { - Self { binding_idx: 0 } + Self { + binding_idx: 0, + scope_prefix: String::new(), + } } - /// Create a child scope with a fresh binding counter. - pub fn child(&self) -> Self { - Self { binding_idx: 0 } + /// Create a child scope for the body of an f-when directive. + /// `parent_idx` is the index allocated by the parent scope for the f-when itself; + /// it is used as a path segment in the scope prefix to guarantee globally-unique + /// marker names across all nesting levels within a single shadow DOM. + pub fn child(&self, parent_idx: usize) -> Self { + Self { + binding_idx: 0, + scope_prefix: format!("{}{}-", self.scope_prefix, parent_idx), + } } pub fn next_binding(&mut self) -> usize { @@ -24,11 +41,14 @@ impl HydrationScope { /// - Content bindings: `name` = the expression (e.g. `"text"` for `{{text}}`) /// - f-when: `name` = `"when-{idx}"` /// - f-repeat: `name` = `"repeat-{idx}"` + /// + /// The `scope_prefix` is prepended to `name` to produce a globally-unique marker id + /// when directives are nested (e.g. f-when inside f-when). pub fn start_marker(&self, idx: usize, name: &str) -> String { - format!("", idx, name) + format!("", idx, self.scope_prefix, name) } pub fn end_marker(&self, idx: usize, name: &str) -> String { - format!("", idx, name) + format!("", idx, self.scope_prefix, name) } } diff --git a/crates/microsoft-fast-build/tests/custom_elements.rs b/crates/microsoft-fast-build/tests/custom_elements.rs index 11d35b746c4..a6c0df08c06 100644 --- a/crates/microsoft-fast-build/tests/custom_elements.rs +++ b/crates/microsoft-fast-build/tests/custom_elements.rs @@ -494,3 +494,74 @@ fn test_custom_element_object_from_colon_binding() { assert!(!result.contains(":config"), "colon attr not in HTML: {result}"); assert!(result.contains("Hello"), "rendered: {result}"); } + +// ── entry-level custom element merged state ──────────────────────────────────── + +#[test] +fn test_root_element_per_element_attr_available_in_template() { + // A per-element static attribute (e.g. planet="earth") should be available as a + // template binding key in the entry element's shadow template, even if that key + // is not present in the shared root state.json. + let locator = make_locator(&[("planet-el", r#"{{planet}}"#)]); + let result = render_entry_template_with_locator( + r#""#, + r#"{"shared":"yes"}"#, + &locator, + ).unwrap(); + assert!(result.contains("earth"), "per-element attr available in template: {result}"); +} + +#[test] +fn test_root_element_attr_overrides_root_state_key() { + // When an entry element carries a static attribute whose key matches a root state + // key, the attribute-derived value should take precedence in the child template. + let locator = make_locator(&[("my-el", r#"{{color}}"#)]); + let result = render_entry_template_with_locator( + r#""#, + r#"{"color":"red"}"#, + &locator, + ).unwrap(); + assert!(result.contains("blue"), "attr value overrides root state: {result}"); + assert!(!result.contains("red"), "root state key not used when attr present: {result}"); +} + +#[test] +fn test_root_element_root_state_available_alongside_attr() { + // Root state keys that are NOT shadowed by an attribute should still be accessible + // in the entry element's template alongside per-element attr values. + let locator = make_locator(&[("my-el", r#"{{planet}} {{shared}}"#)]); + let result = render_entry_template_with_locator( + r#""#, + r#"{"shared":"context"}"#, + &locator, + ).unwrap(); + assert!(result.contains("mars"), "per-element attr rendered: {result}"); + assert!(result.contains("context"), "root state key still accessible: {result}"); +} + +#[test] +fn test_root_element_boolean_attr_available_in_template() { + // A boolean (no-value) attribute on an entry element should be available as + // true in the child template. + let locator = make_locator(&[("my-el", r#"visible"#)]); + let result = render_entry_template_with_locator( + r#""#, + r#"{}"#, + &locator, + ).unwrap(); + assert!(result.contains("visible"), "boolean attr resolves to true in template: {result}"); +} + +#[test] +fn test_root_elements_with_different_per_element_attrs() { + // Multiple root elements in the same entry HTML each have their own attribute + // values available in their respective templates. + let locator = make_locator(&[("planet-el", r#"{{planet}}"#)]); + let result = render_entry_template_with_locator( + r#""#, + r#"{"shared":"yes"}"#, + &locator, + ).unwrap(); + assert!(result.contains("earth"), "first element attr: {result}"); + assert!(result.contains("mars"), "second element attr: {result}"); +} diff --git a/crates/microsoft-fast-build/tests/hydration.rs b/crates/microsoft-fast-build/tests/hydration.rs index 483426c7439..63fca1af77f 100644 --- a/crates/microsoft-fast-build/tests/hydration.rs +++ b/crates/microsoft-fast-build/tests/hydration.rs @@ -332,8 +332,8 @@ fn test_hydration_f_repeat_with_inner_when() { assert!(result.contains(""), "item start: {result}"); // Item scope: f-when is binding 0, name = "when-0" assert!(result.contains(""), "when start: {result}"); - // When body scope: {{item.text}} is binding 0, name = "item.text-0" - assert!(result.contains("Foo"), + // When body scope: {{item.text}} is binding 0 inside when-0, name = "0-item.text-0" + assert!(result.contains("Foo"), "text binding: {result}"); } diff --git a/packages/fast-html/scripts/build-fixtures.js b/packages/fast-html/scripts/build-fixtures.js index 412d2103160..dcfd8ec9186 100644 --- a/packages/fast-html/scripts/build-fixtures.js +++ b/packages/fast-html/scripts/build-fixtures.js @@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url"; // Builds test fixtures using @microsoft/fast-build. Add fixture names here // incrementally as each one is verified to work with the fast-build CLI. -const fixtures = ["attribute", "binding", "event", "ref", "slotted"]; +const fixtures = ["attribute", "binding", "event", "ref", "slotted", "when"]; const __dirname = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); diff --git a/packages/fast-html/test/fixtures/when/entry.html b/packages/fast-html/test/fixtures/when/entry.html new file mode 100644 index 00000000000..0340a4430e9 --- /dev/null +++ b/packages/fast-html/test/fixtures/when/entry.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/fast-html/test/fixtures/when/index.html b/packages/fast-html/test/fixtures/when/index.html index 7c4a26d631a..4323e9cd2b4 100644 --- a/packages/fast-html/test/fixtures/when/index.html +++ b/packages/fast-html/test/fixtures/when/index.html @@ -1,160 +1,130 @@ - + - - - - - - - - - - - - - + + + - - - - - - - - - + + - - - - - - - - - + + - - - - - - - - - + + - - - - - - - - - + + - - - - - - - - - + + - - - - - - - - - + + - - - - - - - - - + + - - - - - - - - - + + - - - - - - - - - + + - - - - - - - - - - + + - - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/fast-html/test/fixtures/when/main.ts b/packages/fast-html/test/fixtures/when/main.ts index ba3f1414004..56b187634de 100644 --- a/packages/fast-html/test/fixtures/when/main.ts +++ b/packages/fast-html/test/fixtures/when/main.ts @@ -29,8 +29,8 @@ RenderableFASTElement(TestElementNot).defineAsync({ }); class TestElementEquals extends FASTElement { - @attr({ attribute: "var-a" }) - varA: number = 0; + @attr({ attribute: "vara" }) + vara: number = 0; } RenderableFASTElement(TestElementEquals).defineAsync({ name: "test-element-equals", @@ -38,8 +38,8 @@ RenderableFASTElement(TestElementEquals).defineAsync({ }); class TestElementNotEquals extends FASTElement { - @attr({ attribute: "var-a" }) - varA: number = 0; + @attr({ attribute: "vara" }) + vara: number = 0; } RenderableFASTElement(TestElementNotEquals).defineAsync({ name: "test-element-not-equals", @@ -47,8 +47,8 @@ RenderableFASTElement(TestElementNotEquals).defineAsync({ }); class TestElementGe extends FASTElement { - @attr({ attribute: "var-a" }) - varA: number = 0; + @attr({ attribute: "vara" }) + vara: number = 0; } RenderableFASTElement(TestElementGe).defineAsync({ name: "test-element-ge", @@ -56,8 +56,8 @@ RenderableFASTElement(TestElementGe).defineAsync({ }); class TestElementGt extends FASTElement { - @attr({ attribute: "var-a" }) - varA: number = 0; + @attr({ attribute: "vara" }) + vara: number = 0; } RenderableFASTElement(TestElementGt).defineAsync({ name: "test-element-gt", @@ -65,8 +65,8 @@ RenderableFASTElement(TestElementGt).defineAsync({ }); class TestElementLe extends FASTElement { - @attr({ attribute: "var-a" }) - varA: number = 0; + @attr({ attribute: "vara" }) + vara: number = 0; } RenderableFASTElement(TestElementLe).defineAsync({ name: "test-element-le", @@ -74,8 +74,8 @@ RenderableFASTElement(TestElementLe).defineAsync({ }); class TestElementLt extends FASTElement { - @attr({ attribute: "var-a" }) - varA: number = 0; + @attr({ attribute: "vara" }) + vara: number = 0; } RenderableFASTElement(TestElementLt).defineAsync({ name: "test-element-lt", @@ -83,11 +83,11 @@ RenderableFASTElement(TestElementLt).defineAsync({ }); class TestElementOr extends FASTElement { - @attr({ attribute: "this-var", mode: "boolean" }) - thisVar: boolean = false; + @attr({ attribute: "thisvar", mode: "boolean" }) + thisvar: boolean = false; - @attr({ attribute: "that-var", mode: "boolean" }) - thatVar: boolean = false; + @attr({ attribute: "thatvar", mode: "boolean" }) + thatvar: boolean = false; } RenderableFASTElement(TestElementOr).defineAsync({ name: "test-element-or", @@ -95,11 +95,11 @@ RenderableFASTElement(TestElementOr).defineAsync({ }); class TestElementAnd extends FASTElement { - @attr({ attribute: "this-var", mode: "boolean" }) - thisVar: boolean = false; + @attr({ attribute: "thisvar", mode: "boolean" }) + thisvar: boolean = false; - @attr({ attribute: "that-var", mode: "boolean" }) - thatVar: boolean = false; + @attr({ attribute: "thatvar", mode: "boolean" }) + thatvar: boolean = false; } RenderableFASTElement(TestElementAnd).defineAsync({ name: "test-element-and", diff --git a/packages/fast-html/test/fixtures/when/state.json b/packages/fast-html/test/fixtures/when/state.json new file mode 100644 index 00000000000..5fa17480a7c --- /dev/null +++ b/packages/fast-html/test/fixtures/when/state.json @@ -0,0 +1,10 @@ +{ + "error": false, + "showProgress": true, + "enableContinue": false, + "strings": { + "errorMessage": "Error occurred", + "continueButtonText": "Continue", + "retryButtonText": "Retry" + } +} diff --git a/packages/fast-html/test/fixtures/when/templates.html b/packages/fast-html/test/fixtures/when/templates.html new file mode 100644 index 00000000000..7623b2f92fc --- /dev/null +++ b/packages/fast-html/test/fixtures/when/templates.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +