feat: replace CJS require with static imports, add comprehensive layout system tests
This commit is contained in:
+528
-171
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -118,7 +118,7 @@
|
|||||||
"yargs": "^18.0.0"
|
"yargs": "^18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quartz-community/types": "^0.2.1",
|
"@quartz-community/types": "^0.3.0",
|
||||||
"@quartz-community/utils": "^0.1.0",
|
"@quartz-community/utils": "^0.1.0",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/node": "^25.0.10",
|
"@types/node": "^25.0.10",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import test, { describe } from "node:test"
|
||||||
|
import assert from "node:assert"
|
||||||
|
import { resolveFrame, frameRegistry } from "./index"
|
||||||
|
import { DefaultFrame } from "./DefaultFrame"
|
||||||
|
import { FullWidthFrame } from "./FullWidthFrame"
|
||||||
|
import type { PageFrame } from "./types"
|
||||||
|
|
||||||
|
const customFrame: PageFrame = {
|
||||||
|
name: "custom-test-frame",
|
||||||
|
render: () => null as any,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveFrame", () => {
|
||||||
|
test("returns DefaultFrame for undefined", () => {
|
||||||
|
assert.strictEqual(resolveFrame(undefined), DefaultFrame)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns DefaultFrame for 'default'", () => {
|
||||||
|
assert.strictEqual(resolveFrame("default"), DefaultFrame)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns named built-in frame", () => {
|
||||||
|
assert.strictEqual(resolveFrame("full-width"), FullWidthFrame)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns DefaultFrame for unknown frame name", () => {
|
||||||
|
assert.strictEqual(resolveFrame("nonexistent"), DefaultFrame)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("plugin-registered frame takes priority", () => {
|
||||||
|
frameRegistry.register("custom-test-frame", customFrame, "test-plugin")
|
||||||
|
const result = resolveFrame("custom-test-frame")
|
||||||
|
assert.strictEqual(result, customFrame)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns DefaultFrame for unknown name even with plugin frames registered", () => {
|
||||||
|
frameRegistry.register("custom-test-frame", customFrame, "test-plugin")
|
||||||
|
const result = resolveFrame("totally-unknown")
|
||||||
|
assert.strictEqual(result, DefaultFrame)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import test, { describe, afterEach } from "node:test"
|
||||||
|
import assert from "node:assert"
|
||||||
|
import { ComponentRegistry } from "./registry"
|
||||||
|
import { QuartzComponent, QuartzComponentConstructor } from "./types"
|
||||||
|
|
||||||
|
const StubA = (() => null) as unknown as QuartzComponent
|
||||||
|
const StubB = (() => null) as unknown as QuartzComponent
|
||||||
|
StubA.displayName = "StubA"
|
||||||
|
StubB.displayName = "StubB"
|
||||||
|
|
||||||
|
const StubConstructor = ((opts?: any) => {
|
||||||
|
const c = (() => null) as unknown as QuartzComponent
|
||||||
|
c.displayName = opts?.name ?? "stub"
|
||||||
|
return c
|
||||||
|
}) as unknown as QuartzComponentConstructor
|
||||||
|
|
||||||
|
let registry: ComponentRegistry | null = null
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
registry?.clear()
|
||||||
|
registry = null
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("register and get", () => {
|
||||||
|
test("registers a component and retrieves it by name", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
registry.register("foo", StubA, "source")
|
||||||
|
|
||||||
|
const result = registry.get("foo")
|
||||||
|
assert.strictEqual(result?.component, StubA)
|
||||||
|
assert.strictEqual(result?.source, "source")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns undefined for unregistered names", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
const result = registry.get("nonexistent")
|
||||||
|
assert.strictEqual(result, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("overwrites component from different source", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
registry.register("foo", StubA, "src1")
|
||||||
|
registry.register("foo", StubB, "src2")
|
||||||
|
|
||||||
|
const result = registry.get("foo")
|
||||||
|
assert.strictEqual(result?.component, StubB)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("instantiate", () => {
|
||||||
|
test("returns a component instance from a constructor", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
const instance = registry.instantiate(StubConstructor)
|
||||||
|
assert.ok(instance)
|
||||||
|
assert.strictEqual(typeof instance, "function")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("caches instances by constructor + options", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
const first = registry.instantiate(StubConstructor)
|
||||||
|
const second = registry.instantiate(StubConstructor)
|
||||||
|
assert.strictEqual(first, second)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("different options produce different instances", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
const first = registry.instantiate(StubConstructor, { a: 1 })
|
||||||
|
const second = registry.instantiate(StubConstructor, { a: 2 })
|
||||||
|
assert.notStrictEqual(first, second)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("undefined options and no-arg call produce same cache key", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
const first = registry.instantiate(StubConstructor)
|
||||||
|
const second = registry.instantiate(StubConstructor, undefined)
|
||||||
|
assert.strictEqual(first, second)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getAllComponents", () => {
|
||||||
|
test("deduplicates components registered under multiple names", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
registry.register("foo", StubConstructor, "source")
|
||||||
|
registry.register("bar", StubConstructor, "source")
|
||||||
|
|
||||||
|
const result = registry.getAllComponents()
|
||||||
|
assert.strictEqual(result.length, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("reuses cached instance from prior instantiate call", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
registry.register("x", StubConstructor, "source")
|
||||||
|
const instance = registry.instantiate(StubConstructor, { opt: 1 })
|
||||||
|
|
||||||
|
const result = registry.getAllComponents()
|
||||||
|
assert.strictEqual(result[0], instance)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips components that fail to instantiate", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
const ThrowingConstructor = (() => {
|
||||||
|
throw new Error("boom")
|
||||||
|
}) as unknown as QuartzComponentConstructor
|
||||||
|
|
||||||
|
registry.register("bad", ThrowingConstructor, "source")
|
||||||
|
const result = registry.getAllComponents()
|
||||||
|
assert.deepStrictEqual(result, [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("setOptionOverrides and cache invalidation", () => {
|
||||||
|
test("stores and retrieves option overrides", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
registry.setOptionOverrides("plugin", { key: "val" })
|
||||||
|
assert.deepStrictEqual(registry.getOptionOverrides("plugin"), { key: "val" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("merges with existing overrides", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
registry.setOptionOverrides("plugin", { a: 1 })
|
||||||
|
registry.setOptionOverrides("plugin", { b: 2 })
|
||||||
|
assert.deepStrictEqual(registry.getOptionOverrides("plugin"), { a: 1, b: 2 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("clears instance cache when overrides change", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
const first = registry.instantiate(StubConstructor, { name: "cached" })
|
||||||
|
registry.setOptionOverrides("anything", { trigger: true })
|
||||||
|
const second = registry.instantiate(StubConstructor, { name: "cached" })
|
||||||
|
assert.notStrictEqual(first, second)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores empty or undefined overrides", () => {
|
||||||
|
registry = new ComponentRegistry()
|
||||||
|
registry.setOptionOverrides("plugin", {})
|
||||||
|
assert.strictEqual(registry.getOptionOverrides("plugin"), undefined)
|
||||||
|
registry.setOptionOverrides("plugin", undefined)
|
||||||
|
assert.strictEqual(registry.getOptionOverrides("plugin"), undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -18,7 +18,8 @@ export interface RegisteredComponent {
|
|||||||
manifest?: ComponentManifest
|
manifest?: ComponentManifest
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComponentRegistry {
|
/** @internal Exported for testing only. */
|
||||||
|
export class ComponentRegistry {
|
||||||
private components = new Map<string, RegisteredComponent>()
|
private components = new Map<string, RegisteredComponent>()
|
||||||
private instanceCache = new Map<string, QuartzComponent>()
|
private instanceCache = new Map<string, QuartzComponent>()
|
||||||
private optionOverrides = new Map<string, Record<string, unknown>>()
|
private optionOverrides = new Map<string, Record<string, unknown>>()
|
||||||
@@ -111,6 +112,13 @@ class ComponentRegistry {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal For testing only — resets all registry state. */
|
||||||
|
clear(): void {
|
||||||
|
this.components.clear()
|
||||||
|
this.instanceCache.clear()
|
||||||
|
this.optionOverrides.clear()
|
||||||
|
}
|
||||||
|
|
||||||
private findCachedInstance(
|
private findCachedInstance(
|
||||||
constructor: QuartzComponentConstructor<any>,
|
constructor: QuartzComponentConstructor<any>,
|
||||||
): QuartzComponent | undefined {
|
): QuartzComponent | undefined {
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
import test, { describe, afterEach } from "node:test"
|
||||||
|
import assert from "node:assert"
|
||||||
|
import { buildLayoutForEntries, resolveGroups } from "./config-loader"
|
||||||
|
import { componentRegistry } from "../../components/registry"
|
||||||
|
import type { QuartzComponent, QuartzComponentConstructor } from "../../components/types"
|
||||||
|
import { PluginJsonEntry, LayoutPosition } from "./types"
|
||||||
|
|
||||||
|
const makeComponent = (name: string): QuartzComponent => {
|
||||||
|
const c = (() => null) as unknown as QuartzComponent
|
||||||
|
c.displayName = name
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeConstructor = (name: string): QuartzComponentConstructor => {
|
||||||
|
return () => makeComponent(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeEntry(
|
||||||
|
source: string,
|
||||||
|
layout?: { position: LayoutPosition; priority: number },
|
||||||
|
): PluginJsonEntry {
|
||||||
|
return {
|
||||||
|
source,
|
||||||
|
enabled: true,
|
||||||
|
options: {},
|
||||||
|
...(layout ? { layout: { position: layout.position, priority: layout.priority } } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
componentRegistry.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("position assignment", () => {
|
||||||
|
test("places component in correct position from layout.position", () => {
|
||||||
|
const component = makeComponent("MyPlugin")
|
||||||
|
componentRegistry.register("my-plugin", component, "test-source")
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries([makeEntry("my-plugin", { position: "left", priority: 10 })], {})
|
||||||
|
assert.deepStrictEqual(result.left, [component])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("places component in footer position", () => {
|
||||||
|
const component = makeComponent("FooterComp")
|
||||||
|
componentRegistry.register("footer-comp", component, "test-source")
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries(
|
||||||
|
[makeEntry("footer-comp", { position: "footer", priority: 20 })],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert.deepStrictEqual(result.footer, [component])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("places component in header position", () => {
|
||||||
|
const component = makeComponent("HeaderComp")
|
||||||
|
componentRegistry.register("header-comp", component, "test-source")
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries(
|
||||||
|
[makeEntry("header-comp", { position: "header", priority: 5 })],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert.deepStrictEqual(result.header, [component])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns empty arrays when no entries have layout", () => {
|
||||||
|
const component = makeComponent("NoLayout")
|
||||||
|
componentRegistry.register("no-layout", component, "test-source")
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries([makeEntry("no-layout")], {})
|
||||||
|
assert.deepStrictEqual(result.header, [])
|
||||||
|
assert.deepStrictEqual(result.left, [])
|
||||||
|
assert.deepStrictEqual(result.right, [])
|
||||||
|
assert.deepStrictEqual(result.beforeBody, [])
|
||||||
|
assert.deepStrictEqual(result.afterBody, [])
|
||||||
|
assert.deepStrictEqual(result.footer, [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("defaultPosition fallback", () => {
|
||||||
|
test("uses manifest defaultPosition when no explicit layout", () => {
|
||||||
|
const component = makeComponent("F")
|
||||||
|
componentRegistry.register("f", component, "test-source", {
|
||||||
|
name: "f",
|
||||||
|
displayName: "F",
|
||||||
|
description: "",
|
||||||
|
version: "1",
|
||||||
|
defaultPosition: "footer",
|
||||||
|
defaultPriority: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries([makeEntry("f")], {})
|
||||||
|
assert.deepStrictEqual(result.footer, [component])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("explicit layout takes precedence over defaultPosition", () => {
|
||||||
|
const component = makeComponent("P")
|
||||||
|
componentRegistry.register("p", component, "test-source", {
|
||||||
|
name: "p",
|
||||||
|
displayName: "P",
|
||||||
|
description: "",
|
||||||
|
version: "1",
|
||||||
|
defaultPosition: "right",
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries(
|
||||||
|
[makeEntry("p", { position: "left", priority: 10 })],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert.deepStrictEqual(result.left, [component])
|
||||||
|
assert.deepStrictEqual(result.right, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("silently skips invalid defaultPosition", () => {
|
||||||
|
const component = makeComponent("Bad")
|
||||||
|
componentRegistry.register("bad", component, "test-source", {
|
||||||
|
name: "bad",
|
||||||
|
displayName: "Bad",
|
||||||
|
description: "",
|
||||||
|
version: "1",
|
||||||
|
defaultPosition: "body",
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries([makeEntry("bad")], {})
|
||||||
|
assert.deepStrictEqual(result.header, [])
|
||||||
|
assert.deepStrictEqual(result.left, [])
|
||||||
|
assert.deepStrictEqual(result.right, [])
|
||||||
|
assert.deepStrictEqual(result.beforeBody, [])
|
||||||
|
assert.deepStrictEqual(result.afterBody, [])
|
||||||
|
assert.deepStrictEqual(result.footer, [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("priority sorting", () => {
|
||||||
|
test("sorts components within a position by priority", () => {
|
||||||
|
const compA = makeComponent("A")
|
||||||
|
const compB = makeComponent("B")
|
||||||
|
const compC = makeComponent("C")
|
||||||
|
componentRegistry.register("a", compA, "test-source")
|
||||||
|
componentRegistry.register("b", compB, "test-source")
|
||||||
|
componentRegistry.register("c", compC, "test-source")
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries(
|
||||||
|
[
|
||||||
|
makeEntry("a", { position: "left", priority: 30 }),
|
||||||
|
makeEntry("b", { position: "left", priority: 10 }),
|
||||||
|
makeEntry("c", { position: "left", priority: 20 }),
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
const names = result.left?.map((component) => component.displayName)
|
||||||
|
assert.deepStrictEqual(names, ["B", "C", "A"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("defaults to priority 50 for defaultPosition without defaultPriority", () => {
|
||||||
|
const explicit = makeComponent("Explicit")
|
||||||
|
const ctor = makeConstructor("Default")
|
||||||
|
const defaulted = ctor(undefined)
|
||||||
|
|
||||||
|
componentRegistry.register("explicit", explicit, "test-source")
|
||||||
|
componentRegistry.register("defaulted", defaulted, "test-source", {
|
||||||
|
name: "defaulted",
|
||||||
|
displayName: "Default",
|
||||||
|
description: "",
|
||||||
|
version: "1",
|
||||||
|
defaultPosition: "left",
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries(
|
||||||
|
[makeEntry("explicit", { position: "left", priority: 40 }), makeEntry("defaulted")],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
const names = result.left?.map((component) => component.displayName)
|
||||||
|
assert.deepStrictEqual(names, ["Explicit", "Default"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resolveGroups", () => {
|
||||||
|
test("returns ungrouped items in priority order", () => {
|
||||||
|
const a = makeComponent("A")
|
||||||
|
const b = makeComponent("B")
|
||||||
|
const c = makeComponent("C")
|
||||||
|
const items = [
|
||||||
|
{ component: a, priority: 30 },
|
||||||
|
{ component: b, priority: 10 },
|
||||||
|
{ component: c, priority: 20 },
|
||||||
|
]
|
||||||
|
const result = resolveGroups(items, {})
|
||||||
|
assert.strictEqual(result.length, 3)
|
||||||
|
assert.strictEqual(result[0], b)
|
||||||
|
assert.strictEqual(result[1], c)
|
||||||
|
assert.strictEqual(result[2], a)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns ungrouped items unchanged", () => {
|
||||||
|
const a = makeComponent("A")
|
||||||
|
const items = [{ component: a, priority: 10 }]
|
||||||
|
const result = resolveGroups(items, {})
|
||||||
|
assert.strictEqual(result.length, 1)
|
||||||
|
assert.strictEqual(result[0], a)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns empty array for empty input", () => {
|
||||||
|
const result = resolveGroups([], {})
|
||||||
|
assert.deepStrictEqual(result, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("wraps grouped items in a Flex component", () => {
|
||||||
|
const a = makeComponent("A")
|
||||||
|
const b = makeComponent("B")
|
||||||
|
const items = [
|
||||||
|
{ component: a, priority: 10, group: "toolbar" },
|
||||||
|
{ component: b, priority: 20, group: "toolbar" },
|
||||||
|
]
|
||||||
|
const result = resolveGroups(items, {})
|
||||||
|
assert.strictEqual(result.length, 1)
|
||||||
|
assert.notStrictEqual(result[0], a)
|
||||||
|
assert.notStrictEqual(result[0], b)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses explicit group priority from config", () => {
|
||||||
|
const grouped = makeComponent("Grouped")
|
||||||
|
const ungrouped = makeComponent("Ungrouped")
|
||||||
|
const items = [
|
||||||
|
{ component: grouped, priority: 50, group: "nav" },
|
||||||
|
{ component: ungrouped, priority: 10 },
|
||||||
|
]
|
||||||
|
const result = resolveGroups(items, { nav: { priority: 5 } })
|
||||||
|
assert.strictEqual(result.length, 2)
|
||||||
|
assert.strictEqual(result[1], ungrouped)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to first member priority when no group config", () => {
|
||||||
|
const a = makeComponent("A")
|
||||||
|
const b = makeComponent("B")
|
||||||
|
const solo = makeComponent("Solo")
|
||||||
|
const items = [
|
||||||
|
{ component: a, priority: 20, group: "nav" },
|
||||||
|
{ component: b, priority: 40, group: "nav" },
|
||||||
|
{ component: solo, priority: 30 },
|
||||||
|
]
|
||||||
|
const result = resolveGroups(items, {})
|
||||||
|
assert.strictEqual(result.length, 2)
|
||||||
|
assert.strictEqual(result[1], solo)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("single-member group still wraps in Flex", () => {
|
||||||
|
const a = makeComponent("A")
|
||||||
|
const items = [{ component: a, priority: 10, group: "solo" }]
|
||||||
|
const result = resolveGroups(items, {})
|
||||||
|
assert.strictEqual(result.length, 1)
|
||||||
|
assert.notStrictEqual(result[0], a)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("buildLayoutForEntries with display wrappers", () => {
|
||||||
|
test("applies display wrapper for mobile-only", () => {
|
||||||
|
const component = makeComponent("Wrapped")
|
||||||
|
componentRegistry.register("wrapped-plugin", component, "test-source")
|
||||||
|
|
||||||
|
const entry: PluginJsonEntry = {
|
||||||
|
source: "wrapped-plugin",
|
||||||
|
enabled: true,
|
||||||
|
options: {},
|
||||||
|
layout: { position: "left" as LayoutPosition, priority: 10, display: "mobile-only" },
|
||||||
|
}
|
||||||
|
const result = buildLayoutForEntries([entry], {})
|
||||||
|
assert.strictEqual(result.left?.length, 1)
|
||||||
|
assert.notStrictEqual(result.left?.[0], component)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("buildLayoutForEntries with constructors", () => {
|
||||||
|
test("instantiates constructor components via registry", () => {
|
||||||
|
const ctor = makeConstructor("Instantiated")
|
||||||
|
componentRegistry.register("ctor-plugin", ctor, "test-source")
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries(
|
||||||
|
[makeEntry("ctor-plugin", { position: "left", priority: 10 })],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert.strictEqual(result.left?.length, 1)
|
||||||
|
assert.strictEqual(result.left?.[0].displayName, "Instantiated")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("merges entry options with TS overrides for constructors", () => {
|
||||||
|
const ctor = makeConstructor("Merged")
|
||||||
|
componentRegistry.register("merge-plugin", ctor, "test-source")
|
||||||
|
componentRegistry.setOptionOverrides("merge-plugin", { extra: true })
|
||||||
|
|
||||||
|
const result = buildLayoutForEntries(
|
||||||
|
[{ source: "merge-plugin", enabled: true, options: { base: 1 }, layout: { position: "right" as LayoutPosition, priority: 10 } }],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert.strictEqual(result.right?.length, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -27,6 +27,10 @@ import { loadComponentsFromPackage } from "./componentLoader"
|
|||||||
import { loadFramesFromPackage } from "./frameLoader"
|
import { loadFramesFromPackage } from "./frameLoader"
|
||||||
import { componentRegistry } from "../../components/registry"
|
import { componentRegistry } from "../../components/registry"
|
||||||
import { getCondition } from "./conditions"
|
import { getCondition } from "./conditions"
|
||||||
|
import Flex from "../../components/Flex"
|
||||||
|
import MobileOnly from "../../components/MobileOnly"
|
||||||
|
import DesktopOnly from "../../components/DesktopOnly"
|
||||||
|
import ConditionalRender from "../../components/ConditionalRender"
|
||||||
|
|
||||||
const CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.yaml")
|
const CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.yaml")
|
||||||
const DEFAULT_CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.default.yaml")
|
const DEFAULT_CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.default.yaml")
|
||||||
@@ -717,7 +721,8 @@ export async function loadQuartzLayout(layoutOverrides?: {
|
|||||||
return { defaults: mergedDefaults, byPageType: mergedByPageType }
|
return { defaults: mergedDefaults, byPageType: mergedByPageType }
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLayoutForEntries(
|
/** @internal Exported for testing only. */
|
||||||
|
export function buildLayoutForEntries(
|
||||||
entries: PluginJsonEntry[],
|
entries: PluginJsonEntry[],
|
||||||
layoutConfig: LayoutConfig,
|
layoutConfig: LayoutConfig,
|
||||||
): Partial<FullPageLayout> {
|
): Partial<FullPageLayout> {
|
||||||
@@ -863,7 +868,8 @@ function buildLayoutForEntries(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveGroups(
|
/** @internal Exported for testing only. */
|
||||||
|
export function resolveGroups(
|
||||||
items: {
|
items: {
|
||||||
component: QuartzComponent
|
component: QuartzComponent
|
||||||
priority: number
|
priority: number
|
||||||
@@ -924,9 +930,6 @@ function resolveGroups(
|
|||||||
justify: m.groupOptions?.justify,
|
justify: m.groupOptions?.justify,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Dynamically import Flex to avoid circular dependencies
|
|
||||||
const FlexModule = require("../../components/Flex")
|
|
||||||
const Flex = FlexModule.default as Function
|
|
||||||
const flexComponent = Flex({
|
const flexComponent = Flex({
|
||||||
components: flexComponents,
|
components: flexComponents,
|
||||||
direction: groupConfig.direction ?? "row",
|
direction: groupConfig.direction ?? "row",
|
||||||
@@ -951,10 +954,8 @@ function applyDisplayWrapper(
|
|||||||
display: "mobile-only" | "desktop-only",
|
display: "mobile-only" | "desktop-only",
|
||||||
): QuartzComponent {
|
): QuartzComponent {
|
||||||
if (display === "mobile-only") {
|
if (display === "mobile-only") {
|
||||||
const MobileOnly = require("../../components/MobileOnly").default as Function
|
|
||||||
return MobileOnly(component) as QuartzComponent
|
return MobileOnly(component) as QuartzComponent
|
||||||
} else {
|
} else {
|
||||||
const DesktopOnly = require("../../components/DesktopOnly").default as Function
|
|
||||||
return DesktopOnly(component) as QuartzComponent
|
return DesktopOnly(component) as QuartzComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,7 +970,6 @@ function applyConditionWrapper(component: QuartzComponent, conditionName: string
|
|||||||
return component
|
return component
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConditionalRender = require("../../components/ConditionalRender").default as Function
|
|
||||||
return ConditionalRender({
|
return ConditionalRender({
|
||||||
component,
|
component,
|
||||||
condition: predicate,
|
condition: predicate,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import test, { describe } from "node:test"
|
import test, { describe } from "node:test"
|
||||||
import assert from "node:assert"
|
import assert from "node:assert"
|
||||||
import { resolveLayout } from "./dispatcher"
|
import { collectComponents, resolveLayout } from "./dispatcher"
|
||||||
import { QuartzPageTypePluginInstance } from "../types"
|
import { QuartzPageTypePluginInstance } from "../types"
|
||||||
import { QuartzComponent } from "../../components/types"
|
import { QuartzComponent } from "../../components/types"
|
||||||
|
|
||||||
@@ -62,6 +62,26 @@ describe("resolveLayout", () => {
|
|||||||
)
|
)
|
||||||
assert.deepStrictEqual(result.header, [])
|
assert.deepStrictEqual(result.header, [])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("all array slots default to [] when sharedDefaults only has head", () => {
|
||||||
|
const result = resolveLayout(makePageType(), { head: StubHead }, {})
|
||||||
|
assert.deepStrictEqual(result.header, [])
|
||||||
|
assert.deepStrictEqual(result.left, [])
|
||||||
|
assert.deepStrictEqual(result.right, [])
|
||||||
|
assert.deepStrictEqual(result.beforeBody, [])
|
||||||
|
assert.deepStrictEqual(result.afterBody, [])
|
||||||
|
assert.deepStrictEqual(result.footer, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves component references through override", () => {
|
||||||
|
const result = resolveLayout(
|
||||||
|
makePageType(),
|
||||||
|
{ head: StubHead, footer: [StubA, StubB] },
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert.strictEqual(result.footer[0], StubA)
|
||||||
|
assert.strictEqual(result.footer[1], StubB)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("resolveLayout frame resolution", () => {
|
describe("resolveLayout frame resolution", () => {
|
||||||
@@ -83,4 +103,50 @@ describe("resolveLayout frame resolution", () => {
|
|||||||
const result = resolveLayout(makePageType(), { head: StubHead }, {})
|
const result = resolveLayout(makePageType(), { head: StubHead }, {})
|
||||||
assert.strictEqual(result.frame, "default")
|
assert.strictEqual(result.frame, "default")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("defaults to 'default' when byPageType entry exists but has no frame", () => {
|
||||||
|
const result = resolveLayout(
|
||||||
|
makePageType(),
|
||||||
|
{ head: StubHead },
|
||||||
|
{ content: { left: [StubA] } },
|
||||||
|
)
|
||||||
|
assert.strictEqual(result.frame, "default")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("collectComponents", () => {
|
||||||
|
test("collects all unique components across page types", () => {
|
||||||
|
const pageTypes = [makePageType(), makePageType({ layout: "landing" })]
|
||||||
|
const sharedDefaults = { head: StubHead }
|
||||||
|
const byPageType = {
|
||||||
|
content: { footer: [StubA] },
|
||||||
|
landing: { footer: [StubB] },
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = collectComponents(pageTypes, sharedDefaults, byPageType)
|
||||||
|
assert.ok(result.includes(StubA))
|
||||||
|
assert.ok(result.includes(StubB))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("deduplicates shared components", () => {
|
||||||
|
const pageTypes = [makePageType(), makePageType({ layout: "landing" })]
|
||||||
|
const sharedDefaults = { head: StubHead }
|
||||||
|
const byPageType = {
|
||||||
|
content: { left: [StubA] },
|
||||||
|
landing: { left: [StubA] },
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = collectComponents(pageTypes, sharedDefaults, byPageType)
|
||||||
|
const matches = result.filter((component) => component === StubA)
|
||||||
|
assert.strictEqual(matches.length, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("handles empty footer and header arrays", () => {
|
||||||
|
const pageTypes = [makePageType({ layout: "empty" })]
|
||||||
|
const sharedDefaults = { head: StubHead }
|
||||||
|
const byPageType = { empty: { footer: [], header: [] } }
|
||||||
|
|
||||||
|
const result = collectComponents(pageTypes, sharedDefaults, byPageType)
|
||||||
|
assert.ok(result.every((component) => component))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export function resolveLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectComponents(
|
/** @internal Exported for testing only. */
|
||||||
|
export function collectComponents(
|
||||||
pageTypes: QuartzPageTypePluginInstance[],
|
pageTypes: QuartzPageTypePluginInstance[],
|
||||||
sharedDefaults: Partial<FullPageLayout>,
|
sharedDefaults: Partial<FullPageLayout>,
|
||||||
byPageType: Record<string, Partial<FullPageLayout>>,
|
byPageType: Record<string, Partial<FullPageLayout>>,
|
||||||
|
|||||||
Reference in New Issue
Block a user