import { mount } from "$core/binder";
import {
clearAllGlobalHooks,
clearGlobalHooks,
getElementBindings,
isElementMounted,
notifyElementMounted,
notifyElementUnmounted,
registerElementHook,
registerGlobalHook,
unregisterGlobalHook,
} from "$core/lifecycle";
import { registerPlugin } from "$core/plugin";
import { signal } from "$core/signal";
import type { PluginContext } from "$types/volt";
import { afterEach, describe, expect, it, vi } from "vitest";
describe("lifecycle hooks", () => {
afterEach(() => {
clearAllGlobalHooks();
});
describe("global lifecycle hooks", () => {
describe("beforeMount", () => {
it("executes before mount", () => {
const executionOrder: string[] = [];
const root = document.createElement("div");
root.innerHTML = "
";
registerGlobalHook("beforeMount", () => {
executionOrder.push("beforeMount");
});
const message = signal("test");
executionOrder.push("before mount call");
mount(root, { message });
executionOrder.push("after mount call");
expect(executionOrder).toEqual(["before mount call", "beforeMount", "after mount call"]);
});
it("receives root and scope", () => {
let receivedRoot: Element | undefined;
let receivedScope: Record | undefined;
const root = document.createElement("div");
const message = signal("test");
registerGlobalHook("beforeMount", (element: Element, scope: Record) => {
receivedRoot = element;
receivedScope = scope;
});
mount(root, { message });
expect(receivedRoot).toBeDefined();
expect(receivedRoot).toBe(root);
expect(receivedScope!.message).toBe(message);
expect(receivedScope!.$store).toBeDefined();
expect(receivedScope!.$arc).toBeDefined();
});
it("can register multiple hooks", () => {
const hooks: number[] = [];
const root = document.createElement("div");
registerGlobalHook("beforeMount", () => {
hooks.push(1);
});
registerGlobalHook("beforeMount", () => {
hooks.push(2);
});
mount(root, {});
expect(hooks).toEqual([1, 2]);
});
});
describe("afterMount", () => {
it("executes after mount completes", () => {
const executionOrder: string[] = [];
const root = document.createElement("div");
root.innerHTML = "";
registerGlobalHook("afterMount", () => {
executionOrder.push("afterMount");
});
const message = signal("test");
executionOrder.push("before mount");
mount(root, { message });
executionOrder.push("after mount");
expect(executionOrder).toEqual(["before mount", "afterMount", "after mount"]);
});
it("executes after mount completes", () => {
const root = document.createElement("div");
root.innerHTML = "";
let mountCompleted = false;
registerGlobalHook("afterMount", () => {
mountCompleted = true;
});
const message = signal("hello");
mount(root, { message });
expect(mountCompleted).toBe(true);
});
});
describe("beforeUnmount", () => {
it("executes before unmount", () => {
const executionOrder: string[] = [];
const root = document.createElement("div");
registerGlobalHook("beforeUnmount", () => {
executionOrder.push("beforeUnmount");
});
const cleanup = mount(root, {});
executionOrder.push("before cleanup");
cleanup();
executionOrder.push("after cleanup");
expect(executionOrder).toEqual(["before cleanup", "beforeUnmount", "after cleanup"]);
});
it("executes before bindings are destroyed", () => {
const root = document.createElement("div");
root.innerHTML = "";
let wasMounted = false;
registerGlobalHook("beforeUnmount", () => {
wasMounted = true;
});
const message = signal("hello");
const cleanup = mount(root, { message });
cleanup();
expect(wasMounted).toBe(true);
});
});
describe("afterUnmount", () => {
it("executes after unmount completes", () => {
const executionOrder: string[] = [];
const root = document.createElement("div");
registerGlobalHook("afterUnmount", () => {
executionOrder.push("afterUnmount");
});
const cleanup = mount(root, {});
executionOrder.push("before cleanup");
cleanup();
executionOrder.push("after cleanup");
expect(executionOrder).toEqual(["before cleanup", "afterUnmount", "after cleanup"]);
});
});
describe("hook registration management", () => {
it("can unregister hooks", () => {
const hook = vi.fn();
const root = document.createElement("div");
const unregister = registerGlobalHook("beforeMount", hook);
mount(root, {});
expect(hook).toHaveBeenCalledTimes(1);
unregister();
mount(root, {});
expect(hook).toHaveBeenCalledTimes(1);
});
it("unregisterGlobalHook removes hooks", () => {
const hook = vi.fn();
const root = document.createElement("div");
registerGlobalHook("beforeMount", hook);
mount(root, {});
expect(hook).toHaveBeenCalledTimes(1);
unregisterGlobalHook("beforeMount", hook);
mount(root, {});
expect(hook).toHaveBeenCalledTimes(1);
});
it("clearGlobalHooks removes all hooks for a lifecycle event", () => {
const hook1 = vi.fn();
const hook2 = vi.fn();
const root = document.createElement("div");
registerGlobalHook("beforeMount", hook1);
registerGlobalHook("beforeMount", hook2);
clearGlobalHooks("beforeMount");
mount(root, {});
expect(hook1).not.toHaveBeenCalled();
expect(hook2).not.toHaveBeenCalled();
});
it("clearAllGlobalHooks removes all hooks", () => {
const beforeMountHook = vi.fn();
const afterMountHook = vi.fn();
const root = document.createElement("div");
registerGlobalHook("beforeMount", beforeMountHook);
registerGlobalHook("afterMount", afterMountHook);
clearAllGlobalHooks();
mount(root, {});
expect(beforeMountHook).not.toHaveBeenCalled();
expect(afterMountHook).not.toHaveBeenCalled();
});
});
describe("error handling", () => {
it("catches and logs errors in beforeMount hooks", () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const root = document.createElement("div");
registerGlobalHook("beforeMount", () => {
throw new Error("beforeMount error");
});
expect(() => {
mount(root, {});
}).not.toThrow();
expect(consoleErrorSpy).toHaveBeenCalledTimes(3);
expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, expect.stringContaining("[lifecycle]"));
expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, "Caused by:", expect.any(Error));
expect(consoleErrorSpy).toHaveBeenNthCalledWith(3, "Element:", root);
consoleErrorSpy.mockRestore();
});
it("continues executing other hooks after error", () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const hook2 = vi.fn();
const root = document.createElement("div");
registerGlobalHook("beforeMount", () => {
throw new Error("Error");
});
registerGlobalHook("beforeMount", hook2);
mount(root, {});
expect(hook2).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
});
});
describe("element lifecycle", () => {
it("tracks element mounted state", () => {
const element = document.createElement("div");
expect(isElementMounted(element)).toBe(false);
notifyElementMounted(element);
expect(isElementMounted(element)).toBe(true);
notifyElementUnmounted(element);
expect(isElementMounted(element)).toBe(false);
});
it("executes onMount callbacks", () => {
const callback = vi.fn();
const element = document.createElement("div");
registerElementHook(element, "mount", callback);
notifyElementMounted(element);
expect(callback).toHaveBeenCalledTimes(1);
});
it("executes onUnmount callbacks", () => {
const callback = vi.fn();
const element = document.createElement("div");
registerElementHook(element, "unmount", callback);
notifyElementMounted(element);
notifyElementUnmounted(element);
expect(callback).toHaveBeenCalledTimes(1);
});
it("only executes onMount once", () => {
const callback = vi.fn();
const element = document.createElement("div");
registerElementHook(element, "mount", callback);
notifyElementMounted(element);
notifyElementMounted(element);
expect(callback).toHaveBeenCalledTimes(1);
});
it("only executes onUnmount if element was mounted", () => {
const callback = vi.fn();
const element = document.createElement("div");
registerElementHook(element, "unmount", callback);
notifyElementUnmounted(element);
expect(callback).not.toHaveBeenCalled();
});
it("catches and logs errors in element hooks", () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const element = document.createElement("div");
registerElementHook(element, "mount", () => {
throw new Error("Mount error");
});
notifyElementMounted(element);
expect(consoleErrorSpy).toHaveBeenCalledTimes(3);
expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, expect.stringContaining("[lifecycle]"));
expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, "Caused by:", expect.any(Error));
expect(consoleErrorSpy).toHaveBeenNthCalledWith(3, "Element:", element);
consoleErrorSpy.mockRestore();
});
});
describe("binding lifecycle", () => {
it("tracks mounted state for elements with bindings", () => {
const root = document.createElement("div");
root.dataset.voltText = "message";
const message = signal("test");
mount(root, { message });
expect(isElementMounted(root)).toBe(true);
});
it("returns empty array for elements with no tracked bindings", () => {
const element = document.createElement("div");
expect(getElementBindings(element)).toEqual([]);
});
});
describe("plugin lifecycle hooks", () => {
it("plugin can register mount hooks", () => {
const onMountSpy = vi.fn();
const root = document.createElement("div");
root.dataset.voltCustom = "value";
registerPlugin("custom", (context: PluginContext) => {
context.lifecycle.onMount(() => {
onMountSpy();
});
});
mount(root, {});
expect(onMountSpy).toHaveBeenCalled();
});
it("plugin can register unmount hooks", () => {
const onUnmountSpy = vi.fn();
const root = document.createElement("div");
root.dataset.voltCustom = "value";
registerPlugin("custom", (context: PluginContext) => {
context.lifecycle.onUnmount(() => {
onUnmountSpy();
});
});
const cleanup = mount(root, {});
cleanup();
expect(onUnmountSpy).toHaveBeenCalled();
});
it("plugin beforeBinding hooks execute before plugin handler", () => {
const executionOrder: string[] = [];
const root = document.createElement("div");
root.dataset.voltCustom = "value";
registerPlugin("custom", (context: PluginContext) => {
context.lifecycle.beforeBinding(() => {
executionOrder.push("beforeBinding");
});
executionOrder.push("handler");
});
mount(root, {});
expect(executionOrder).toEqual(["beforeBinding", "handler"]);
});
it("plugin afterBinding hooks execute after plugin handler", async () => {
const executionOrder: string[] = [];
const root = document.createElement("div");
root.dataset.voltCustom = "value";
registerPlugin("custom", (context: PluginContext) => {
executionOrder.push("handler");
context.lifecycle.afterBinding(() => {
executionOrder.push("afterBinding");
});
});
mount(root, {});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(executionOrder).toEqual(["handler", "afterBinding"]);
});
});
describe("hook execution order", () => {
it("executes hooks in correct order during mount", () => {
const executionOrder: string[] = [];
const root = document.createElement("div");
root.innerHTML = "";
registerGlobalHook("beforeMount", () => {
executionOrder.push("global:beforeMount");
});
registerGlobalHook("afterMount", () => {
executionOrder.push("global:afterMount");
});
const message = signal("test");
mount(root, { message });
expect(executionOrder).toEqual(["global:beforeMount", "global:afterMount"]);
});
it("executes hooks in correct order during unmount", () => {
const executionOrder: string[] = [];
const root = document.createElement("div");
registerGlobalHook("beforeUnmount", () => {
executionOrder.push("global:beforeUnmount");
});
registerGlobalHook("afterUnmount", () => {
executionOrder.push("global:afterUnmount");
});
const cleanup = mount(root, {});
cleanup();
expect(executionOrder).toEqual(["global:beforeUnmount", "global:afterUnmount"]);
});
it("executes mount and unmount in order", () => {
const executionOrder: string[] = [];
const root = document.createElement("div");
registerGlobalHook("beforeMount", () => {
executionOrder.push("beforeMount");
});
registerGlobalHook("afterMount", () => {
executionOrder.push("afterMount");
});
registerGlobalHook("beforeUnmount", () => {
executionOrder.push("beforeUnmount");
});
registerGlobalHook("afterUnmount", () => {
executionOrder.push("afterUnmount");
});
const cleanup = mount(root, {});
cleanup();
expect(executionOrder).toEqual(["beforeMount", "afterMount", "beforeUnmount", "afterUnmount"]);
});
});
});