// @vitest-environment jsdom import { flushPromises, mount } from "@vue/test-utils"; import { describe, expect, it, vi } from "vitest"; import BuefyTree from "@/components/buefy/tree/BuefyTree.vue"; vi.mock("buefy", () => ({ BCheckbox: { name: "BCheckbox", props: { modelValue: Boolean, indeterminate: Boolean, disabled: Boolean, }, emits: ["update:modelValue"], template: ` `, }, })); describe("BuefyTree checkbox selection", () => { it("removes the icon wrapper only for nodes that explicitly opt out", () => { const wrapper = mount(BuefyTree, { props: { data: [ { id: "default-icon", label: "Default icon", isLeaf: true }, { id: "compact", label: "Compact", isLeaf: true, hideIcon: true, hideToggle: true }, ], }, }); expect(wrapper.find('[data-node-key="default-icon"] > .b-tree-node-content .b-tree-node-icon').exists()).toBe(true); expect(wrapper.find('[data-node-key="compact"] > .b-tree-node-content .b-tree-node-icon').exists()).toBe(false); expect(wrapper.find('[data-node-key="default-icon"] > .b-tree-node-content .b-tree-node-toggle').exists()).toBe( true ); expect(wrapper.find('[data-node-key="compact"] > .b-tree-node-content .b-tree-node-toggle').exists()).toBe(false); }); it("uses non-selectable checkable categories as lazy select-all branch controllers", async () => { const load = vi.fn(async () => [ { id: "order:70128", label: "Ordre #70128", selectable: true, isLeaf: true }, { id: "order:70129", label: "Ordre #70129", selectable: true, isLeaf: true }, ]); const wrapper = mount(BuefyTree, { props: { data: [ { id: "category:orders", label: "Orders", selectable: false, checkable: true, isLeaf: false, }, ], selectionMode: "checkbox", lazy: true, load, }, }); await wrapper.find(".b-checkbox-stub").trigger("click"); await flushPromises(); expect(load).toHaveBeenCalledTimes(1); expect(wrapper.emitted("update:checkedKeys")?.at(-1)?.[0]).toEqual(["order:70128", "order:70129"]); await wrapper.find(".b-checkbox-stub").trigger("click"); await flushPromises(); expect(load).toHaveBeenCalledTimes(1); expect(wrapper.emitted("update:checkedKeys")?.at(-1)?.[0]).toEqual([]); }); it("renders progressive batches while selecting every logical child", async () => { const children = Array.from({ length: 1000 }, (_, index) => ({ id: `order:${index + 1}`, label: `Order ${index + 1}`, selectable: true, isLeaf: true, })); const wrapper = mount(BuefyTree, { props: { data: [{ id: "category:orders", label: "Orders", selectable: false, checkable: true, isLeaf: false }], selectionMode: "checkbox", lazy: true, progressiveBatchSize: 50, loadMoreLabel: "Show {count} more", load: vi.fn(async () => children), }, }); await wrapper.find(".b-tree-node-toggle").trigger("click"); await flushPromises(); expect(wrapper.findAll('[data-node-key^="order:"]')).toHaveLength(50); expect(wrapper.get(".b-tree-load-more__button").text()).toContain("Show 50 more"); await wrapper.get(".b-tree-load-more__button").trigger("click"); expect(wrapper.findAll('[data-node-key^="order:"]')).toHaveLength(100); await wrapper.find(".b-checkbox-stub").trigger("click"); await flushPromises(); expect(wrapper.emitted("update:checkedKeys")?.at(-1)?.[0]).toHaveLength(1000); expect(wrapper.findAll('[data-node-key^="order:"]')).toHaveLength(100); }); it("recursively expands lazy branches, reveals every child, and reuses the cache after collapse", async () => { let activeLoads = 0; let maxActiveLoads = 0; const load = vi.fn(async (node) => { activeLoads += 1; maxActiveLoads = Math.max(maxActiveLoads, activeLoads); await Promise.resolve(); activeLoads -= 1; if (node.id === "root") { return [ { id: "branch:a", label: "Branch A", isLeaf: false }, { id: "branch:b", label: "Branch B", isLeaf: false }, ]; } if (node.id === "branch:a") { return [ { id: "leaf:a1", label: "Leaf A1", isLeaf: true }, { id: "leaf:a2", label: "Leaf A2", isLeaf: true }, ]; } if (node.id === "branch:b") { return [{ id: "branch:b1", label: "Branch B1", isLeaf: false }]; } if (node.id === "branch:b1") { return [{ id: "leaf:b1", label: "Leaf B1", isLeaf: true }]; } return []; }); const wrapper = mount(BuefyTree, { props: { data: [{ id: "root", label: "Root", isLeaf: false }], lazy: true, load, progressiveBatchSize: 1, }, }); const [firstResult, duplicateResult] = await Promise.all([wrapper.vm.expandAll(), wrapper.vm.expandAll()]); await flushPromises(); expect(firstResult).toEqual(duplicateResult); expect(firstResult.cancelled).toBe(false); expect(firstResult.failedKeys).toEqual([]); expect(firstResult.expandedKeys).toEqual(["root", "branch:a", "branch:b", "branch:b1"]); expect(load).toHaveBeenCalledTimes(4); expect(maxActiveLoads).toBeGreaterThan(1); expect(maxActiveLoads).toBeLessThanOrEqual(4); expect(wrapper.findAll('[data-node-key^="branch:"]')).toHaveLength(3); expect(wrapper.findAll('[data-node-key^="leaf:"]')).toHaveLength(3); wrapper.vm.collapseAll(); await flushPromises(); expect(wrapper.findAll('[data-node-key^="branch:"]')).toHaveLength(0); await wrapper.vm.expandAll(); await flushPromises(); expect(load).toHaveBeenCalledTimes(4); expect(wrapper.findAll('[data-node-key^="leaf:"]')).toHaveLength(3); }); it("continues expanding sibling branches when one lazy branch fails", async () => { const load = vi.fn(async (node) => { if (node.id === "root") { return [ { id: "branch:good", label: "Good", isLeaf: false }, { id: "branch:bad", label: "Bad", isLeaf: false }, ]; } if (node.id === "branch:bad") { throw new Error("Branch failed"); } return [{ id: "leaf:good", label: "Good leaf", isLeaf: true }]; }); const wrapper = mount(BuefyTree, { props: { data: [{ id: "root", label: "Root", isLeaf: false }], lazy: true, load, }, }); const result = await wrapper.vm.expandAll(); await flushPromises(); expect(result.cancelled).toBe(false); expect(result.failedKeys).toEqual(["branch:bad"]); expect(result.expandedKeys).toEqual(["root", "branch:good", "branch:bad"]); expect(wrapper.emitted("load-error")?.[0]?.[0]).toEqual(new Error("Branch failed")); expect(wrapper.get('[data-node-key="leaf:good"]').text()).toContain("Good leaf"); expect(wrapper.get('[data-node-key="branch:bad"]').classes()).toContain("has-load-error"); }); });