import { describe, expect, it } from "vitest"; import { applyFilter, evaluateCondition, propertiesMap } from "./index.js"; import type { RecordRow, PropertyInfo, EvalContext } from "../values/types.js"; const props: PropertyInfo[] = [ { id: "p-title", name: "Title", type: "title", config: {} }, { id: "p-num", name: "Estimate", type: "number", config: {} }, { id: "p-date", name: "Due", type: "date", config: {} }, { id: "p-status", name: "Status", type: "select", config: { options: [{ id: "done", name: "Done", color: "green" }] }, }, { id: "p-multi", name: "Tags", type: "multi_select", config: { options: [ { id: "t1", name: "T1", color: "blue" }, { id: "t2", name: "T2", color: "red" }, ], }, }, { id: "p-check", name: "Done?", type: "checkbox", config: {} }, ]; const map = propertiesMap(props); const ctx: EvalContext = { now: Date.parse("2025-06-15T12:00:00Z"), actorUserId: "usr-me", userTimezone: "UTC", }; const row = (over: Partial> = {}): RecordRow => ({ id: "r1", pageId: "pg1", position: "a0", createdTime: 1, lastEditedTime: 1, values: { "p-title": "Write spec", "p-num": 10, "p-date": "2025-06-16", "p-status": "done", "p-multi": ["t1", "t2"], "p-check": true, ...over, }, }); describe("filter engine", () => { it("text operators (case-insensitive contains/starts/ends)", () => { const c = { propertyId: "p-title", operator: "contains" as const, value: "SPEC" }; expect(evaluateCondition(row(), props[0]!, c, ctx)).toBe(true); expect( evaluateCondition( row(), props[0]!, { propertyId: "p-title", operator: "starts_with", value: "write" }, ctx, ), ).toBe(true); expect( evaluateCondition( row(), props[0]!, { propertyId: "p-title", operator: "ends_with", value: "spec" }, ctx, ), ).toBe(true); }); it("number comparison + isBetween", () => { const n = (op: string, value: unknown) => ({ propertyId: "p-num", operator: op as never, value, }); expect(evaluateCondition(row(), props[1]!, n(">", 5), ctx)).toBe(true); expect(evaluateCondition(row(), props[1]!, n("<", 5), ctx)).toBe(false); expect(evaluateCondition(row(), props[1]!, n("isBetween", [5, 15]), ctx)).toBe(true); }); it("select is/isNot + empty semantics", () => { const s = (op: string, value?: unknown) => ({ propertyId: "p-status", operator: op as never, value, }); expect(evaluateCondition(row(), props[3]!, s("=", "done"), ctx)).toBe(true); expect(evaluateCondition(row(), props[3]!, s("!=", "done"), ctx)).toBe(false); expect(evaluateCondition(row({ "p-status": null }), props[3]!, s("is_empty"), ctx)).toBe(true); }); it("multi_select contains/doesNotContain", () => { const m = (op: string, value: unknown) => ({ propertyId: "p-multi", operator: op as never, value, }); expect(evaluateCondition(row(), props[4]!, m("contains", "t1"), ctx)).toBe(true); expect(evaluateCondition(row(), props[4]!, m("doesNotContain", "t1"), ctx)).toBe(false); expect(evaluateCondition(row(), props[4]!, m("=", ["t1", "t2"]), ctx)).toBe(true); }); it("checkbox isChecked/isUnchecked", () => { expect( evaluateCondition(row(), props[5]!, { propertyId: "p-check", operator: "isChecked" }, ctx), ).toBe(true); expect( evaluateCondition( row({ "p-check": false }), props[5]!, { propertyId: "p-check", operator: "isUnchecked" }, ctx, ), ).toBe(true); }); it("date: equality + relative windows (this week)", () => { const d = (op: string, value: unknown) => ({ propertyId: "p-date", operator: op as never, value, }); expect(evaluateCondition(row(), props[2]!, d("=", "2025-06-16"), ctx)).toBe(true); expect(evaluateCondition(row(), props[2]!, d("isAfter", "2025-06-15"), ctx)).toBe(true); // "this week"/"last week" are relative to ctx.now (ISO: Monday-start, // Sunday = last day of the week). The module ctx.now (2025-06-15) is a // SUNDAY, whose week is Mon 06-09 → Sun 06-15 — so the row date 06-16 // (a Monday) is NOT in it. To pin the intended "06-16 is in its week" // behavior, evaluate against a `now` that falls inside the 06-16 week. const weekCtx: EvalContext = { now: Date.parse("2025-06-17T12:00:00Z"), // Tue, inside Mon 06-16 → Sun 06-22 actorUserId: "usr-me", userTimezone: "UTC", }; expect(evaluateCondition(row(), props[2]!, d("=", "this week"), weekCtx)).toBe(true); expect(evaluateCondition(row(), props[2]!, d("=", "last week"), weekCtx)).toBe(false); }); it("tokens: me / today resolve against ctx", () => { const owner = { propertyId: "p-owner", operator: "=" as const, value: "me" }; const ownerProp: PropertyInfo = { id: "p-owner", name: "Owner", type: "person", config: {} }; expect(evaluateCondition(row({ "p-owner": ["usr-me"] }), ownerProp, owner, ctx)).toBe(true); expect( evaluateCondition( row(), props[2]!, { propertyId: "p-date", operator: "=", value: "today" }, { now: Date.parse("2025-06-16T00:00:00Z"), userTimezone: "UTC" }, ), ).toBe(true); }); it("nested and/or/not trees", () => { const tree = { op: "and" as const, conditions: [ { propertyId: "p-num", operator: ">" as const, value: 5 }, { op: "or" as const, conditions: [ { propertyId: "p-status", operator: "=" as const, value: "nope" }, { propertyId: "p-check", operator: "isChecked" as const }, ], }, { op: "not" as const, conditions: [{ propertyId: "p-num", operator: ">" as const, value: 100 }], }, ], }; expect(applyFilter([row()], tree, map, ctx)).toHaveLength(1); const failing = { op: "or" as const, conditions: [{ propertyId: "p-num", operator: ">" as const, value: 100 }], }; expect(applyFilter([row()], failing, map, ctx)).toHaveLength(0); }); });