@microsoft/fast-html 1.0.0-alpha.29 → 1.0.0-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,6 +73,150 @@ RenderableFASTElement(MyCustomElement).defineAsync({
73
73
  });
74
74
  ```
75
75
 
76
+ #### Lifecycle Callbacks
77
+
78
+ FAST HTML provides lifecycle callbacks that allow you to hook into various stages of template processing and element hydration. These callbacks are useful for tracking the rendering lifecycle, gathering analytics, or coordinating complex initialization sequences.
79
+
80
+ ##### Available Callbacks
81
+
82
+ **Template Lifecycle Callbacks:**
83
+ - `elementDidRegister(name: string)` - Called after the JavaScript class definition has been registered
84
+ - `templateWillUpdate(name: string)` - Called before the template has been evaluated and assigned
85
+ - `templateDidUpdate(name: string)` - Called after the template has been assigned to the definition
86
+ - `elementDidDefine(name: string)` - Called after the custom element has been defined
87
+
88
+ **Hydration Lifecycle Callbacks:**
89
+ - `elementWillHydrate(name: string)` - Called before an element begins hydration
90
+ - `elementDidHydrate(name: string)` - Called after an element completes hydration
91
+ - `hydrationComplete()` - Called after all elements have completed hydration
92
+
93
+ ##### Configuring Callbacks
94
+
95
+ Configure lifecycle callbacks using `TemplateElement.config()`:
96
+
97
+ ```typescript
98
+ import { TemplateElement, type HydrationLifecycleCallbacks } from "@microsoft/fast-html";
99
+
100
+ // You can configure all callbacks at once
101
+ const callbacks: HydrationLifecycleCallbacks = {
102
+ elementDidRegister(name: string) {
103
+ console.log(`Element registered: ${name}`);
104
+ },
105
+ templateWillUpdate(name: string) {
106
+ console.log(`Template updating: ${name}`);
107
+ },
108
+ templateDidUpdate(name: string) {
109
+ console.log(`Template updated: ${name}`);
110
+ },
111
+ elementDidDefine(name: string) {
112
+ console.log(`Element defined: ${name}`);
113
+ },
114
+ elementWillHydrate(name: string) {
115
+ console.log(`Element will hydrate: ${name}`);
116
+ },
117
+ elementDidHydrate(name: string) {
118
+ console.log(`Element hydrated: ${name}`);
119
+ },
120
+ hydrationComplete() {
121
+ console.log('All elements hydrated');
122
+ }
123
+ };
124
+
125
+ TemplateElement.config(callbacks);
126
+
127
+ // Or configure only the callbacks you need
128
+ TemplateElement.config({
129
+ elementDidHydrate(name: string) {
130
+ console.log(`${name} is ready`);
131
+ },
132
+ hydrationComplete() {
133
+ console.log('Page is interactive');
134
+ }
135
+ });
136
+ ```
137
+
138
+ ##### Lifecycle Order
139
+
140
+ The lifecycle callbacks occur in the following general sequence:
141
+
142
+ 1. **Registration Phase**: `elementDidRegister` is called when the element class is registered
143
+ 2. **Template Phase**: `templateWillUpdate` → (template processing) → `templateDidUpdate` → `elementDidDefine`
144
+ 3. **Hydration Phase**: `elementWillHydrate` → (hydration) → `elementDidHydrate`
145
+ 4. **Completion**: `hydrationComplete` is called after all elements finish hydrating
146
+
147
+ **Note:** Template processing is asynchronous and happens independently for each element. The template and hydration phases can be interleaved when multiple elements are being processed simultaneously.
148
+
149
+ ##### Use Cases
150
+
151
+ **Performance Monitoring:**
152
+ ```typescript
153
+ TemplateElement.config({
154
+ elementWillHydrate(name: string) {
155
+ performance.mark(`${name}-hydration-start`);
156
+ },
157
+ elementDidHydrate(name: string) {
158
+ performance.mark(`${name}-hydration-end`);
159
+ performance.measure(
160
+ `${name}-hydration`,
161
+ `${name}-hydration-start`,
162
+ `${name}-hydration-end`
163
+ );
164
+ },
165
+ hydrationComplete() {
166
+ // Report to analytics
167
+ const entries = performance.getEntriesByType('measure');
168
+ console.log('Hydration metrics:', entries);
169
+ }
170
+ });
171
+ ```
172
+
173
+ **Loading State Management:**
174
+ ```typescript
175
+ TemplateElement.config({
176
+ elementWillHydrate(name: string) {
177
+ // Show loading indicator
178
+ document.body.classList.add('hydrating');
179
+ },
180
+ hydrationComplete() {
181
+ // Hide loading indicator once all elements are ready
182
+ document.body.classList.remove('hydrating');
183
+ document.body.classList.add('hydrated');
184
+ }
185
+ });
186
+ ```
187
+
188
+ **Debugging and Development:**
189
+ ```typescript
190
+ if (process.env.NODE_ENV === 'development') {
191
+ const events: Array<{callback: string; name?: string; timestamp: number}> = [];
192
+
193
+ TemplateElement.config({
194
+ elementDidRegister(name) {
195
+ events.push({ callback: 'elementDidRegister', name, timestamp: Date.now() });
196
+ },
197
+ templateWillUpdate(name) {
198
+ events.push({ callback: 'templateWillUpdate', name, timestamp: Date.now() });
199
+ },
200
+ templateDidUpdate(name) {
201
+ events.push({ callback: 'templateDidUpdate', name, timestamp: Date.now() });
202
+ },
203
+ elementDidDefine(name) {
204
+ events.push({ callback: 'elementDidDefine', name, timestamp: Date.now() });
205
+ },
206
+ elementWillHydrate(name) {
207
+ events.push({ callback: 'elementWillHydrate', name, timestamp: Date.now() });
208
+ },
209
+ elementDidHydrate(name) {
210
+ events.push({ callback: 'elementDidHydrate', name, timestamp: Date.now() });
211
+ },
212
+ hydrationComplete() {
213
+ events.push({ callback: 'hydrationComplete', timestamp: Date.now() });
214
+ console.table(events);
215
+ }
216
+ });
217
+ }
218
+ ```
219
+
76
220
  ### Syntax
77
221
 
78
222
  All bindings use a handlebars-like syntax.
@@ -1,3 +1,3 @@
1
- export { TemplateElement } from "./template.js";
2
1
  export { RenderableFASTElement } from "./element.js";
3
2
  export { ObserverMap } from "./observer-map.js";
3
+ export { ObserverMapOption, TemplateElement, type ElementOptions, type ElementOptionsDictionary, type HydrationLifecycleCallbacks, } from "./template.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A ponyfill for requestIdleCallback that falls back to setTimeout.
3
+ * Uses the native requestIdleCallback when available.
4
+ *
5
+ * @param callback - The function to call when the browser is idle.
6
+ * @param options - Options object that may contain a timeout property.
7
+ * @returns An ID that can be used to cancel the callback.
8
+ * @public
9
+ */
10
+ export declare function requestIdleCallback(callback: (deadline: IdleDeadline) => void, options?: {
11
+ timeout: number;
12
+ }): ReturnType<typeof globalThis.requestIdleCallback | typeof setTimeout>;
13
+ /**
14
+ * A ponyfill for cancelIdleCallback that falls back to clearTimeout.
15
+ * Uses the native cancelIdleCallback when available.
16
+ *
17
+ * @param id - The ID of the callback to cancel.
18
+ * @public
19
+ */
20
+ export declare function cancelIdleCallback(id: ReturnType<typeof globalThis.requestIdleCallback | typeof setTimeout>): void;
21
+ /**
22
+ * Waits for all custom element descendants of a target element to be connected.
23
+ *
24
+ * @param target - The target element to observe.
25
+ * @param callback - The function to call when all custom element descendants are connected.
26
+ * @param options - Options object that may contain a timeout property for the idle callback.
27
+ *
28
+ * @remarks
29
+ * This function uses requestIdleCallback to periodically check if all custom element
30
+ * descendants (elements with a hyphen in their tag name) are connected to the DOM.
31
+ * Once all such elements are connected, the provided callback is invoked.
32
+ *
33
+ * The `timeout` option specifies the maximum time to wait for each idle callback before
34
+ * rechecking, defaulting to 50 milliseconds.
35
+ *
36
+ * @public
37
+ */
38
+ export declare function waitForConnectedDescendants(target: HTMLElement, callback: () => void, options?: {
39
+ shallow?: boolean;
40
+ timeout?: number;
41
+ }): void;
@@ -1,8 +1,20 @@
1
- import { FASTElement } from "@microsoft/fast-element";
1
+ import { FASTElement, HydrationControllerCallbacks, TemplateLifecycleCallbacks } from "@microsoft/fast-element";
2
2
  import "@microsoft/fast-element/install-hydratable-view-templates.js";
3
- export type ObserverMapOption = "all";
3
+ /**
4
+ * Values for the observerMap element option.
5
+ */
6
+ export declare const ObserverMapOption: {
7
+ readonly all: "all";
8
+ };
9
+ /**
10
+ * Type for the observerMap element option.
11
+ */
12
+ export type ObserverMapOption = (typeof ObserverMapOption)[keyof typeof ObserverMapOption];
13
+ /**
14
+ * Element options the TemplateElement will use to update the registered element
15
+ */
4
16
  export interface ElementOptions {
5
- observerMap?: ObserverMapOption | undefined;
17
+ observerMap?: ObserverMapOption;
6
18
  }
7
19
  /**
8
20
  * A dictionary of element options the TemplateElement will use to update the registered element
@@ -10,6 +22,20 @@ export interface ElementOptions {
10
22
  export interface ElementOptionsDictionary<ElementOptionsType = ElementOptions> {
11
23
  [key: string]: ElementOptionsType;
12
24
  }
25
+ /**
26
+ * Lifecycle callbacks for template and hydration events.
27
+ * Combines template lifecycle callbacks with hydration callbacks and adds template-processing events.
28
+ */
29
+ export interface HydrationLifecycleCallbacks extends HydrationControllerCallbacks, TemplateLifecycleCallbacks {
30
+ /**
31
+ * Called after the JS class definition has been registered
32
+ */
33
+ elementDidRegister?(name: string): void;
34
+ /**
35
+ * Called before the template has been evaluated and assigned
36
+ */
37
+ templateWillUpdate?(name: string): void;
38
+ }
13
39
  /**
14
40
  * The <f-template> custom element that will provide view logic to the element
15
41
  */
@@ -34,6 +60,23 @@ declare class TemplateElement extends FASTElement {
34
60
  * Metadata containing JSON schema for properties on a custom eleemnt
35
61
  */
36
62
  private schema?;
63
+ /**
64
+ * Lifecycle callbacks for hydration events
65
+ */
66
+ private static lifecycleCallbacks;
67
+ /**
68
+ * Configure lifecycle callbacks for hydration events.
69
+ *
70
+ * @param callbacks - Lifecycle callbacks to configure.
71
+ * @returns The {@link TemplateElement} class.
72
+ */
73
+ static config(callbacks: HydrationLifecycleCallbacks): typeof TemplateElement;
74
+ /**
75
+ * Set options for custom elements.
76
+ *
77
+ * @param elementOptions - A dictionary of custom element options
78
+ * @returns The TemplateElement class.
79
+ */
37
80
  static options(elementOptions?: ElementOptionsDictionary): typeof TemplateElement;
38
81
  constructor();
39
82
  /**
@@ -0,0 +1,5 @@
1
+ export declare const lifecycleEvents: Array<{
2
+ callback: string;
3
+ name?: string;
4
+ }>;
5
+ export declare let hydrationCompleteEmitted: boolean;
@@ -10,16 +10,12 @@ import { attr } from "@microsoft/fast-element";
10
10
  export function RenderableFASTElement(BaseCtor) {
11
11
  const C = class extends BaseCtor {
12
12
  constructor(...args) {
13
+ var _a, _b;
13
14
  super(...args);
14
15
  this.deferHydration = true;
15
- if (this.prepare) {
16
- this.prepare().then(() => {
17
- this.deferHydration = false;
18
- });
19
- }
20
- else {
16
+ ((_b = (_a = this.prepare) === null || _a === void 0 ? void 0 : _a.call(this)) !== null && _b !== void 0 ? _b : Promise.resolve()).then(() => {
21
17
  this.deferHydration = false;
22
- }
18
+ });
23
19
  }
24
20
  };
25
21
  attr({ mode: "boolean", attribute: "defer-hydration" })(C.prototype, "deferHydration");
@@ -1,3 +1,3 @@
1
- export { TemplateElement } from "./template.js";
2
1
  export { RenderableFASTElement } from "./element.js";
3
2
  export { ObserverMap } from "./observer-map.js";
3
+ export { ObserverMapOption, TemplateElement, } from "./template.js";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * A ponyfill for requestIdleCallback that falls back to setTimeout.
3
+ * Uses the native requestIdleCallback when available.
4
+ *
5
+ * @param callback - The function to call when the browser is idle.
6
+ * @param options - Options object that may contain a timeout property.
7
+ * @returns An ID that can be used to cancel the callback.
8
+ * @public
9
+ */
10
+ export function requestIdleCallback(callback, options) {
11
+ if ("requestIdleCallback" in globalThis) {
12
+ return globalThis.requestIdleCallback(callback, options);
13
+ }
14
+ const start = Date.now();
15
+ return setTimeout(() => {
16
+ callback({
17
+ didTimeout: (options === null || options === void 0 ? void 0 : options.timeout) ? Date.now() - start >= options.timeout : false,
18
+ timeRemaining: () => 0,
19
+ });
20
+ }, 1);
21
+ }
22
+ /**
23
+ * A ponyfill for cancelIdleCallback that falls back to clearTimeout.
24
+ * Uses the native cancelIdleCallback when available.
25
+ *
26
+ * @param id - The ID of the callback to cancel.
27
+ * @public
28
+ */
29
+ export function cancelIdleCallback(id) {
30
+ if ("cancelIdleCallback" in globalThis) {
31
+ globalThis.cancelIdleCallback(id);
32
+ }
33
+ else {
34
+ clearTimeout(id);
35
+ }
36
+ }
37
+ /**
38
+ * Waits for all custom element descendants of a target element to be connected.
39
+ *
40
+ * @param target - The target element to observe.
41
+ * @param callback - The function to call when all custom element descendants are connected.
42
+ * @param options - Options object that may contain a timeout property for the idle callback.
43
+ *
44
+ * @remarks
45
+ * This function uses requestIdleCallback to periodically check if all custom element
46
+ * descendants (elements with a hyphen in their tag name) are connected to the DOM.
47
+ * Once all such elements are connected, the provided callback is invoked.
48
+ *
49
+ * The `timeout` option specifies the maximum time to wait for each idle callback before
50
+ * rechecking, defaulting to 50 milliseconds.
51
+ *
52
+ * @public
53
+ */
54
+ export function waitForConnectedDescendants(target, callback, options) {
55
+ var _a, _b;
56
+ let idleCallbackId;
57
+ const shallow = (_a = options === null || options === void 0 ? void 0 : options.shallow) !== null && _a !== void 0 ? _a : false;
58
+ const query = `${shallow ? ":scope > " : ""}:not(:defined)`;
59
+ const timeout = (_b = options === null || options === void 0 ? void 0 : options.timeout) !== null && _b !== void 0 ? _b : 50;
60
+ const scheduleCheck = (deadline) => {
61
+ if (idleCallbackId) {
62
+ cancelIdleCallback(idleCallbackId);
63
+ idleCallbackId = undefined;
64
+ }
65
+ if (!target.querySelector(query) || (deadline && deadline.timeRemaining() <= 0)) {
66
+ callback();
67
+ return;
68
+ }
69
+ idleCallbackId = requestIdleCallback(scheduleCheck, { timeout });
70
+ };
71
+ scheduleCheck();
72
+ }
@@ -1,13 +1,37 @@
1
1
  import { __awaiter, __decorate, __metadata } from "tslib";
2
2
  import { attr, children, elements, FAST, FASTElement, FASTElementDefinition, fastElementRegistry, HydratableElementController, ref, repeat, slotted, ViewTemplate, when, } from "@microsoft/fast-element";
3
3
  import "@microsoft/fast-element/install-hydratable-view-templates.js";
4
- import { bindingResolver, getExpressionChain, getNextBehavior, getRootPropertyName, resolveWhen, transformInnerHTML, } from "./utilities.js";
5
4
  import { ObserverMap } from "./observer-map.js";
6
5
  import { Schema } from "./schema.js";
6
+ import { bindingResolver, getExpressionChain, getNextBehavior, getRootPropertyName, resolveWhen, transformInnerHTML, } from "./utilities.js";
7
+ /**
8
+ * Values for the observerMap element option.
9
+ */
10
+ export const ObserverMapOption = {
11
+ all: "all",
12
+ };
7
13
  /**
8
14
  * The <f-template> custom element that will provide view logic to the element
9
15
  */
10
16
  class TemplateElement extends FASTElement {
17
+ /**
18
+ * Configure lifecycle callbacks for hydration events.
19
+ *
20
+ * @param callbacks - Lifecycle callbacks to configure.
21
+ * @returns The {@link TemplateElement} class.
22
+ */
23
+ static config(callbacks) {
24
+ TemplateElement.lifecycleCallbacks = callbacks;
25
+ // Pass the hydration-specific callbacks to HydratableElementController
26
+ HydratableElementController.config(Object.assign({}, callbacks));
27
+ return this;
28
+ }
29
+ /**
30
+ * Set options for custom elements.
31
+ *
32
+ * @param elementOptions - A dictionary of custom element options
33
+ * @returns The TemplateElement class.
34
+ */
11
35
  static options(elementOptions = {}) {
12
36
  const result = {};
13
37
  for (const key in elementOptions) {
@@ -39,35 +63,46 @@ class TemplateElement extends FASTElement {
39
63
  }
40
64
  connectedCallback() {
41
65
  super.connectedCallback();
42
- if (typeof this.name === "string") {
43
- this.schema = new Schema(this.name);
44
- FASTElementDefinition.registerAsync(this.name).then((value) => __awaiter(this, void 0, void 0, function* () {
45
- var _a, _b, _c;
46
- if (!((_a = TemplateElement.elementOptions) === null || _a === void 0 ? void 0 : _a[this.name])) {
47
- TemplateElement.setOptions(this.name);
48
- }
49
- if (((_b = TemplateElement.elementOptions[this.name]) === null || _b === void 0 ? void 0 : _b.observerMap) ===
50
- "all") {
51
- this.observerMap = new ObserverMap(value.prototype, this.schema);
52
- }
53
- const registeredFastElement = fastElementRegistry.getByType(value);
54
- const template = this.getElementsByTagName("template").item(0);
55
- if (template) {
56
- const innerHTML = yield transformInnerHTML(this.innerHTML);
57
- // Cache paths during template processing (pass undefined if observerMap is not available)
58
- const { strings, values } = yield this.resolveStringsAndValues(null, innerHTML, false, null, 0, this.schema, this.observerMap);
59
- // Define the root properties cached in the observer map as observable (only if observerMap exists)
60
- (_c = this.observerMap) === null || _c === void 0 ? void 0 : _c.defineProperties();
61
- if (registeredFastElement) {
62
- // all new elements will get the updated template
63
- registeredFastElement.template = this.resolveTemplateOrBehavior(strings, values);
64
- }
65
- }
66
- else {
67
- throw FAST.error(2000 /* Message.noTemplateProvided */, { name: this.name });
68
- }
69
- }));
66
+ const name = this.name;
67
+ if (typeof name !== "string") {
68
+ return;
70
69
  }
70
+ this.schema = new Schema(name);
71
+ FASTElementDefinition.registerAsync(name).then((value) => __awaiter(this, void 0, void 0, function* () {
72
+ var _a, _b, _c, _d, _e, _f, _g;
73
+ (_b = (_a = TemplateElement.lifecycleCallbacks).elementDidRegister) === null || _b === void 0 ? void 0 : _b.call(_a, name);
74
+ if (!((_c = TemplateElement.elementOptions) === null || _c === void 0 ? void 0 : _c[name])) {
75
+ TemplateElement.setOptions(name);
76
+ }
77
+ if (((_d = TemplateElement.elementOptions[name]) === null || _d === void 0 ? void 0 : _d.observerMap) === "all") {
78
+ this.observerMap = new ObserverMap(value.prototype, this.schema);
79
+ }
80
+ const registeredFastElement = fastElementRegistry.getByType(value);
81
+ const template = this.getElementsByTagName("template").item(0);
82
+ if (template) {
83
+ // Callback: Before template has been evaluated and assigned
84
+ (_f = (_e = TemplateElement.lifecycleCallbacks).templateWillUpdate) === null || _f === void 0 ? void 0 : _f.call(_e, name);
85
+ const innerHTML = transformInnerHTML(this.innerHTML);
86
+ // Cache paths during template processing (pass undefined if observerMap is not available)
87
+ const { strings, values } = yield this.resolveStringsAndValues(null, innerHTML, false, null, 0, this.schema, this.observerMap);
88
+ // Define the root properties cached in the observer map as observable (only if observerMap exists)
89
+ (_g = this.observerMap) === null || _g === void 0 ? void 0 : _g.defineProperties();
90
+ if (registeredFastElement) {
91
+ // Attach lifecycle callbacks to the definition before assigning template
92
+ // This allows the Observable notification to trigger the callbacks
93
+ registeredFastElement.lifecycleCallbacks = {
94
+ templateDidUpdate: TemplateElement.lifecycleCallbacks.templateDidUpdate,
95
+ elementDidDefine: TemplateElement.lifecycleCallbacks.elementDidDefine,
96
+ };
97
+ // All new elements will get the updated template
98
+ // This assignment triggers the Observable notification → callbacks fire
99
+ registeredFastElement.template = this.resolveTemplateOrBehavior(strings, values);
100
+ }
101
+ }
102
+ else {
103
+ throw FAST.error(2000 /* Message.noTemplateProvided */, { name: this.name });
104
+ }
105
+ }));
71
106
  }
72
107
  /**
73
108
  * Resolve strings and values from an innerHTML string
@@ -265,6 +300,10 @@ TemplateElement.elementOptions = {};
265
300
  * Default element options
266
301
  */
267
302
  TemplateElement.defaultElementOptions = {};
303
+ /**
304
+ * Lifecycle callbacks for hydration events
305
+ */
306
+ TemplateElement.lifecycleCallbacks = {};
268
307
  __decorate([
269
308
  attr,
270
309
  __metadata("design:type", String)
@@ -0,0 +1,166 @@
1
+ import { __awaiter } from "tslib";
2
+ import { expect, test } from "@playwright/test";
3
+ test.describe("Lifecycle Callbacks", () => __awaiter(void 0, void 0, void 0, function* () {
4
+ test("should invoke all lifecycle callbacks in correct order for simple element", ({ page, }) => __awaiter(void 0, void 0, void 0, function* () {
5
+ yield page.goto("/lifecycle-callbacks");
6
+ // Wait for hydration to complete
7
+ yield page.waitForFunction(() => window.getHydrationCompleteStatus());
8
+ const events = yield page.evaluate(() => window.lifecycleEvents);
9
+ // Verify callbacks for simple-element were called
10
+ const simpleElementEvents = events.filter((e) => e.name === "simple-element");
11
+ expect(simpleElementEvents.length).toBeGreaterThan(0);
12
+ // Check that callbacks were called in the correct order
13
+ const callbackOrder = simpleElementEvents.map((e) => e.callback);
14
+ expect(callbackOrder).toContain("elementDidRegister");
15
+ expect(callbackOrder).toContain("templateWillUpdate");
16
+ expect(callbackOrder).toContain("templateDidUpdate");
17
+ expect(callbackOrder).toContain("elementDidDefine");
18
+ expect(callbackOrder).toContain("elementWillHydrate");
19
+ expect(callbackOrder).toContain("elementDidHydrate");
20
+ // Verify the order is correct
21
+ const registerIndex = callbackOrder.indexOf("elementDidRegister");
22
+ const willUpdateIndex = callbackOrder.indexOf("templateWillUpdate");
23
+ const didUpdateIndex = callbackOrder.indexOf("templateDidUpdate");
24
+ const defineIndex = callbackOrder.indexOf("elementDidDefine");
25
+ const willHydrateIndex = callbackOrder.indexOf("elementWillHydrate");
26
+ const didHydrateIndex = callbackOrder.indexOf("elementDidHydrate");
27
+ expect(registerIndex).toBeLessThan(willUpdateIndex);
28
+ expect(willUpdateIndex).toBeLessThan(didUpdateIndex);
29
+ expect(didUpdateIndex).toBeLessThan(defineIndex);
30
+ expect(willHydrateIndex).toBeGreaterThan(-1);
31
+ expect(didHydrateIndex).toBeGreaterThan(willHydrateIndex);
32
+ }));
33
+ test("should invoke callbacks for multiple elements", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
34
+ yield page.goto("/lifecycle-callbacks");
35
+ yield page.waitForFunction(() => window.getHydrationCompleteStatus());
36
+ const events = yield page.evaluate(() => window.lifecycleEvents);
37
+ // Check that callbacks were invoked for all elements
38
+ const elementNames = new Set(events.map((e) => e.name));
39
+ expect(elementNames.has("simple-element")).toBe(true);
40
+ expect(elementNames.has("complex-element")).toBe(true);
41
+ expect(elementNames.has("nested-element")).toBe(true);
42
+ }));
43
+ test("should invoke elementWillHydrate before hydration", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
44
+ yield page.goto("/lifecycle-callbacks");
45
+ yield page.waitForFunction(() => window.getHydrationCompleteStatus());
46
+ const events = yield page.evaluate(() => window.lifecycleEvents);
47
+ const willHydrateEvents = events.filter((e) => e.callback === "elementWillHydrate");
48
+ const didHydrateEvents = events.filter((e) => e.callback === "elementDidHydrate");
49
+ // Every elementWillHydrate should be followed by elementDidHydrate for the same element
50
+ willHydrateEvents.forEach((willEvent) => {
51
+ const correspondingDidEvent = didHydrateEvents.find((e) => e.name === willEvent.name);
52
+ expect(correspondingDidEvent).toBeDefined();
53
+ });
54
+ }));
55
+ test("should invoke hydrationComplete callback after all elements hydrate", ({ page, }) => __awaiter(void 0, void 0, void 0, function* () {
56
+ yield page.goto("/lifecycle-callbacks");
57
+ const hydrationComplete = yield page.waitForFunction(() => window.getHydrationCompleteStatus(), { timeout: 5000 });
58
+ expect(hydrationComplete).toBeTruthy();
59
+ const events = yield page.evaluate(() => window.lifecycleEvents);
60
+ // Verify hydrationComplete callback was invoked
61
+ const hydrationCompleteEvents = events.filter((e) => e.callback === "hydrationComplete");
62
+ expect(hydrationCompleteEvents.length).toBe(1);
63
+ // Verify all elements are hydrated
64
+ const simpleElement = yield page.locator("simple-element");
65
+ const complexElement = yield page.locator("complex-element");
66
+ const nestedElement = yield page.locator("nested-element");
67
+ yield expect(simpleElement).not.toHaveAttribute("needs-hydration");
68
+ yield expect(complexElement).not.toHaveAttribute("needs-hydration");
69
+ yield expect(nestedElement).not.toHaveAttribute("needs-hydration");
70
+ // Verify hydrationComplete was called AFTER all individual element hydrations
71
+ const lastElementDidHydrateIndex = events.reduce((maxIndex, e, index) => {
72
+ return e.callback === "elementDidHydrate" ? index : maxIndex;
73
+ }, -1);
74
+ const hydrationCompleteIndex = events.findIndex((e) => e.callback === "hydrationComplete");
75
+ expect(hydrationCompleteIndex).toBeGreaterThan(lastElementDidHydrateIndex);
76
+ }));
77
+ test("should handle complex element with observer map", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
78
+ yield page.goto("/lifecycle-callbacks");
79
+ yield page.waitForFunction(() => window.getHydrationCompleteStatus());
80
+ const complexElement = yield page.locator("complex-element");
81
+ // Verify the element has hydrated correctly
82
+ yield expect(complexElement).toHaveText(/Complex/);
83
+ yield expect(complexElement).not.toHaveAttribute("needs-hydration");
84
+ const events = yield page.evaluate(() => window.lifecycleEvents);
85
+ const complexEvents = events.filter((e) => e.name === "complex-element");
86
+ // Complex element should have all lifecycle callbacks
87
+ expect(complexEvents.length).toBeGreaterThan(0);
88
+ expect(complexEvents.some((e) => e.callback === "elementDidHydrate")).toBe(true);
89
+ }));
90
+ test("should handle nested elements correctly", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
91
+ yield page.goto("/lifecycle-callbacks");
92
+ yield page.waitForFunction(() => window.getHydrationCompleteStatus());
93
+ const events = yield page.evaluate(() => window.lifecycleEvents);
94
+ // Both parent complex-element and nested nested-element should have callbacks
95
+ const complexEvents = events.filter((e) => e.name === "complex-element");
96
+ const nestedEvents = events.filter((e) => e.name === "nested-element");
97
+ expect(complexEvents.length).toBeGreaterThan(0);
98
+ expect(nestedEvents.length).toBeGreaterThan(0);
99
+ // Both should have hydrated
100
+ expect(complexEvents.some((e) => e.callback === "elementDidHydrate")).toBe(true);
101
+ expect(nestedEvents.some((e) => e.callback === "elementDidHydrate")).toBe(true);
102
+ }));
103
+ test("should handle deferred hydration element", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
104
+ yield page.goto("/lifecycle-callbacks");
105
+ // The deferred element has a prepare() method that delays hydration
106
+ yield page.waitForFunction(() => window.getHydrationCompleteStatus(), {
107
+ timeout: 5000,
108
+ });
109
+ const events = yield page.evaluate(() => window.lifecycleEvents);
110
+ const deferredEvents = events.filter((e) => e.name === "deferred-element");
111
+ // Should have hydration callbacks
112
+ expect(deferredEvents.some((e) => e.callback === "elementWillHydrate")).toBe(true);
113
+ expect(deferredEvents.some((e) => e.callback === "elementDidHydrate")).toBe(true);
114
+ const deferredElement = yield page.locator("deferred-element");
115
+ yield expect(deferredElement).not.toHaveAttribute("needs-hydration");
116
+ yield expect(deferredElement).not.toHaveAttribute("defer-hydration");
117
+ }));
118
+ test("should verify template and hydration callbacks are invoked", ({ page, }) => __awaiter(void 0, void 0, void 0, function* () {
119
+ yield page.goto("/lifecycle-callbacks");
120
+ yield page.waitForFunction(() => window.getHydrationCompleteStatus());
121
+ const events = yield page.evaluate(() => window.lifecycleEvents);
122
+ // For each element, verify both template and hydration callbacks are invoked
123
+ // Note: These callbacks can be interleaved as template processing is async
124
+ const elementNames = ["simple-element", "complex-element", "nested-element"];
125
+ elementNames.forEach(name => {
126
+ const elementEvents = events.filter((e) => e.name === name);
127
+ const callbacks = elementEvents.map((e) => e.callback);
128
+ // Verify all expected callbacks were invoked
129
+ expect(callbacks).toContain("elementDidRegister");
130
+ expect(callbacks).toContain("templateWillUpdate");
131
+ expect(callbacks).toContain("templateDidUpdate");
132
+ expect(callbacks).toContain("elementDidDefine");
133
+ expect(callbacks).toContain("elementWillHydrate");
134
+ expect(callbacks).toContain("elementDidHydrate");
135
+ // Verify hydration callbacks are in order
136
+ const willHydrateIndex = callbacks.indexOf("elementWillHydrate");
137
+ const didHydrateIndex = callbacks.indexOf("elementDidHydrate");
138
+ expect(willHydrateIndex).toBeLessThan(didHydrateIndex);
139
+ // Verify template callbacks are in order
140
+ const registerIndex = callbacks.indexOf("elementDidRegister");
141
+ const willUpdateIndex = callbacks.indexOf("templateWillUpdate");
142
+ const didUpdateIndex = callbacks.indexOf("templateDidUpdate");
143
+ const defineIndex = callbacks.indexOf("elementDidDefine");
144
+ expect(registerIndex).toBeLessThan(willUpdateIndex);
145
+ expect(willUpdateIndex).toBeLessThan(didUpdateIndex);
146
+ expect(didUpdateIndex).toBeLessThan(defineIndex);
147
+ });
148
+ }));
149
+ test("should properly hydrate elements and maintain functionality", ({ page, }) => __awaiter(void 0, void 0, void 0, function* () {
150
+ yield page.goto("/lifecycle-callbacks");
151
+ yield page.waitForFunction(() => window.getHydrationCompleteStatus());
152
+ const simpleElement = yield page.locator("simple-element");
153
+ const complexElement = yield page.locator("complex-element");
154
+ // Verify simple element displays correctly
155
+ yield expect(simpleElement).toHaveText("Hello World");
156
+ // Verify complex element displays correctly
157
+ yield expect(complexElement).toHaveText(/Complex/);
158
+ // Verify elements are interactive after hydration
159
+ const currentCount = yield complexElement.evaluate((el) => el.count);
160
+ expect(currentCount).toBe(0);
161
+ // Interact with the element
162
+ yield complexElement.evaluate((el) => el.increment());
163
+ const newCount = yield complexElement.evaluate((el) => el.count);
164
+ expect(newCount).toBe(1);
165
+ }));
166
+ }));