feat: replace CJS require with static imports, add comprehensive layout system tests
This commit is contained in:
@@ -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 { componentRegistry } from "../../components/registry"
|
||||
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 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 }
|
||||
}
|
||||
|
||||
function buildLayoutForEntries(
|
||||
/** @internal Exported for testing only. */
|
||||
export function buildLayoutForEntries(
|
||||
entries: PluginJsonEntry[],
|
||||
layoutConfig: LayoutConfig,
|
||||
): Partial<FullPageLayout> {
|
||||
@@ -863,7 +868,8 @@ function buildLayoutForEntries(
|
||||
return result
|
||||
}
|
||||
|
||||
function resolveGroups(
|
||||
/** @internal Exported for testing only. */
|
||||
export function resolveGroups(
|
||||
items: {
|
||||
component: QuartzComponent
|
||||
priority: number
|
||||
@@ -924,9 +930,6 @@ function resolveGroups(
|
||||
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({
|
||||
components: flexComponents,
|
||||
direction: groupConfig.direction ?? "row",
|
||||
@@ -951,10 +954,8 @@ function applyDisplayWrapper(
|
||||
display: "mobile-only" | "desktop-only",
|
||||
): QuartzComponent {
|
||||
if (display === "mobile-only") {
|
||||
const MobileOnly = require("../../components/MobileOnly").default as Function
|
||||
return MobileOnly(component) as QuartzComponent
|
||||
} else {
|
||||
const DesktopOnly = require("../../components/DesktopOnly").default as Function
|
||||
return DesktopOnly(component) as QuartzComponent
|
||||
}
|
||||
}
|
||||
@@ -969,7 +970,6 @@ function applyConditionWrapper(component: QuartzComponent, conditionName: string
|
||||
return component
|
||||
}
|
||||
|
||||
const ConditionalRender = require("../../components/ConditionalRender").default as Function
|
||||
return ConditionalRender({
|
||||
component,
|
||||
condition: predicate,
|
||||
|
||||
Reference in New Issue
Block a user