Skip to content

Commit f2a5416

Browse files
committed
test(roam): use tilde imports in utils tests
1 parent 1bcfa9a commit f2a5416

4 files changed

Lines changed: 347 additions & 2 deletions

File tree

apps/roam/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"lint": "eslint .",
1010
"lint:fix": "eslint . --fix",
1111
"publish": "tsx scripts/publish.ts",
12-
"check-types": "tsc --noEmit --skipLibCheck"
12+
"check-types": "tsc --noEmit --skipLibCheck",
13+
"test": "vitest run src/utils/__tests__",
14+
"test:watch": "vitest src/utils/__tests__"
1315
},
1416
"license": "Apache-2.0",
1517
"devDependencies": {
@@ -26,7 +28,8 @@
2628
"dotenv": "^16.0.3",
2729
"esbuild": "0.17.14",
2830
"tailwindcss": "^3.4.17",
29-
"tsx": "^4.19.2"
31+
"tsx": "^4.19.2",
32+
"vitest": "^4.0.0"
3033
},
3134
"//": "axios dep temporary - need to fix the dep in underlying libraries",
3235
"tags": [
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { describe, expect, it } from "vitest";
2+
import compileDatalog, { toVar } from "~/utils/compileDatalog";
3+
import gatherDatalogVariablesFromClause from "~/utils/gatherDatalogVariablesFromClause";
4+
import replaceDatalogVariables from "~/utils/replaceDatalogVariables";
5+
6+
describe("compileDatalog", () => {
7+
it("sanitizes variable names", () => {
8+
expect(toVar('a b"(c)')).toBe("abc");
9+
});
10+
11+
it("compiles nested and-clauses", () => {
12+
const query = compileDatalog({
13+
type: "and-clause",
14+
clauses: [
15+
{
16+
type: "data-pattern",
17+
arguments: [
18+
{ type: "variable", value: "node" },
19+
{ type: "constant", value: ":node/title" },
20+
{ type: "constant", value: '"Hello"' },
21+
],
22+
},
23+
{
24+
type: "pred-expr",
25+
pred: "=",
26+
arguments: [
27+
{ type: "variable", value: "node" },
28+
{ type: "variable", value: "match" },
29+
],
30+
},
31+
],
32+
});
33+
34+
expect(query).toContain("(and");
35+
expect(query).toContain('[?node :node/title "Hello"]');
36+
expect(query).toContain("[(= ?node ?match)]");
37+
});
38+
});
39+
40+
describe("gatherDatalogVariablesFromClause", () => {
41+
it("collects variables from nested clauses", () => {
42+
const variables = gatherDatalogVariablesFromClause({
43+
type: "and-clause",
44+
clauses: [
45+
{
46+
type: "data-pattern",
47+
arguments: [
48+
{ type: "variable", value: "a" },
49+
{ type: "constant", value: ":rel" },
50+
{ type: "variable", value: "b" },
51+
],
52+
},
53+
{
54+
type: "or-join-clause",
55+
variables: [
56+
{ type: "variable", value: "c" },
57+
{ type: "variable", value: "d" },
58+
],
59+
clauses: [],
60+
},
61+
],
62+
});
63+
64+
expect(Array.from(variables).sort()).toEqual(["a", "b", "c", "d"]);
65+
});
66+
});
67+
68+
describe("replaceDatalogVariables", () => {
69+
it("replaces explicit variable names and function bindings", () => {
70+
const [clause] = replaceDatalogVariables(
71+
[{ from: "node", to: "page" }],
72+
[
73+
{
74+
type: "fn-expr",
75+
fn: "identity",
76+
arguments: [{ type: "variable", value: "node" }],
77+
binding: {
78+
type: "bind-scalar",
79+
variable: { type: "variable", value: "node" },
80+
},
81+
},
82+
],
83+
);
84+
85+
expect(clause.type).toBe("fn-expr");
86+
if (clause.type !== "fn-expr") return;
87+
expect(clause.arguments[0]).toMatchObject({ value: "page" });
88+
expect(clause.binding).toMatchObject({
89+
variable: { value: "page" },
90+
});
91+
});
92+
93+
it("supports transform replacement for all variables", () => {
94+
const [clause] = replaceDatalogVariables(
95+
[{ from: true, to: (v) => `${v}-v2` }],
96+
[
97+
{
98+
type: "not-join-clause",
99+
variables: [{ type: "variable", value: "a" }],
100+
clauses: [
101+
{
102+
type: "data-pattern",
103+
arguments: [
104+
{ type: "variable", value: "a" },
105+
{ type: "constant", value: ":x" },
106+
{ type: "variable", value: "b" },
107+
],
108+
},
109+
],
110+
},
111+
],
112+
);
113+
114+
expect(clause.type).toBe("not-join-clause");
115+
if (clause.type !== "not-join-clause") return;
116+
expect(clause.variables[0].value).toBe("a-v2");
117+
expect(clause.clauses[0]).toMatchObject({
118+
arguments: [{ value: "a-v2" }, { value: ":x" }, { value: "b-v2" }],
119+
});
120+
});
121+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("~/utils/conditionToDatalog", () => ({
4+
default: vi.fn(({ source, target }) => [
5+
{
6+
type: "data-pattern",
7+
arguments: [
8+
{ type: "variable", value: source },
9+
{ type: "constant", value: ":rel" },
10+
/^:in /.test(target)
11+
? { type: "variable", value: target.substring(4) }
12+
: { type: "constant", value: '"value"' },
13+
],
14+
},
15+
]),
16+
}));
17+
vi.mock("~/utils/predefinedSelections", () => ({
18+
default: [
19+
{
20+
test: /^created$/,
21+
pull: () => "(pull ?node [:create/time])",
22+
mapper: (r: Record<string, string>) => r[":create/time"] || "",
23+
},
24+
],
25+
}));
26+
vi.mock("roamjs-components/util/env", () => ({ getNodeEnv: () => "test" }));
27+
28+
import fireQuery, { fireQuerySync, getDatalogQuery } from "~/utils/fireQuery";
29+
30+
describe("getDatalogQuery", () => {
31+
it("includes :in variables and de-duplicates expected inputs", async () => {
32+
const built = getDatalogQuery({
33+
conditions: [
34+
{
35+
type: "clause",
36+
relation: "r",
37+
source: "node",
38+
target: ":in title",
39+
uid: "1",
40+
not: false,
41+
},
42+
{
43+
type: "clause",
44+
relation: "r",
45+
source: "node",
46+
target: ":in title",
47+
uid: "2",
48+
not: false,
49+
},
50+
],
51+
selections: [{ uid: "s1", text: "created", label: "Created" }],
52+
inputs: { title: "Graph" },
53+
});
54+
55+
expect(built.query).toContain(":in $ ?title");
56+
expect(built.inputs).toEqual(["Graph"]);
57+
const formatted = await built.formatResult([
58+
{ ":node/title": "A", ":block/uid": "u1" },
59+
{ ":block/uid": "u1" },
60+
{ ":create/time": "123" },
61+
]);
62+
expect(formatted).toMatchObject({ text: "A", uid: "u1", Created: "123" });
63+
});
64+
});
65+
66+
describe("fireQuery", () => {
67+
beforeEach(() => {
68+
(globalThis as { window: unknown }).window = {
69+
roamAlphaAPI: {
70+
data: {
71+
async: {
72+
fast: {
73+
q: vi
74+
.fn()
75+
.mockResolvedValue([
76+
[{ ":node/title": "Local", ":block/uid": "l1" }],
77+
]),
78+
},
79+
},
80+
backend: {
81+
q: vi
82+
.fn()
83+
.mockResolvedValue([
84+
[{ ":node/title": "Remote", ":block/uid": "r1" }],
85+
]),
86+
},
87+
fast: {
88+
q: vi
89+
.fn()
90+
.mockReturnValue([
91+
[{ ":node/title": "Sync", ":block/uid": "s1" }],
92+
]),
93+
},
94+
},
95+
},
96+
};
97+
});
98+
99+
it("uses backend queries by default and maps output", async () => {
100+
const results = await fireQuery({ conditions: [], selections: [] });
101+
expect(results[0]).toMatchObject({ text: "Remote", uid: "r1" });
102+
});
103+
104+
it("uses async fast query when local=true", async () => {
105+
const results = await fireQuery({
106+
conditions: [],
107+
selections: [],
108+
local: true,
109+
});
110+
expect(results[0]).toMatchObject({ text: "Local", uid: "l1" });
111+
});
112+
113+
it("returns sync mapped results", () => {
114+
const results = fireQuerySync({ conditions: [], selections: [] });
115+
expect(results).toEqual([{ text: "Sync", uid: "s1" }]);
116+
});
117+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("roamjs-components/util/getSubTree", () => ({
4+
default: vi.fn(),
5+
}));
6+
vi.mock("roamjs-components/util/getSettingValueFromTree", () => ({
7+
default: vi.fn(
8+
({ tree, key }) =>
9+
tree.find((t: { text: string }) => t.text === key)?.children?.[0]?.text ||
10+
"",
11+
),
12+
}));
13+
vi.mock("roamjs-components/writes/createBlock", () => ({
14+
default: vi.fn(),
15+
}));
16+
17+
import getSubTree from "roamjs-components/util/getSubTree";
18+
import createBlock from "roamjs-components/writes/createBlock";
19+
import parseQuery from "~/utils/parseQuery";
20+
import { getTitleDatalog } from "~/utils/conditionToDatalog";
21+
22+
const mockedGetSubTree = vi.mocked(getSubTree);
23+
const mockedCreateBlock = vi.mocked(createBlock);
24+
25+
describe("parseQuery", () => {
26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
(globalThis as { window: unknown }).window = {
29+
roamAlphaAPI: {
30+
util: { generateUID: () => "new-uid" },
31+
},
32+
};
33+
});
34+
35+
it("parses query nodes and builds default return column", () => {
36+
const queryTree = {
37+
uid: "parent",
38+
children: [
39+
{ text: "conditions" },
40+
{ text: "selections" },
41+
{ text: "custom" },
42+
],
43+
};
44+
mockedGetSubTree.mockImplementation(({ key }) => {
45+
if (key === "conditions") return { uid: "conditions-uid", children: [] };
46+
if (key === "selections") {
47+
return {
48+
uid: "selections-uid",
49+
children: [
50+
{ uid: "sel-node", text: "node", children: [{ text: "Title" }] },
51+
{
52+
uid: "sel-created",
53+
text: "created",
54+
children: [{ text: "Created" }],
55+
},
56+
],
57+
};
58+
}
59+
return {
60+
uid: "custom-uid",
61+
children: [{ text: "[:find ?x]" }, { text: "enabled" }],
62+
};
63+
});
64+
65+
const parsed = parseQuery(queryTree as never);
66+
67+
expect(parsed.columns).toEqual([
68+
{ key: "Title", uid: "returnuid", selection: "node" },
69+
{ key: "Created", uid: "sel-created", selection: "created" },
70+
]);
71+
expect(parsed.isCustomEnabled).toBe(true);
72+
expect(parsed.customNode).toBe("[:find ?x]");
73+
});
74+
75+
it("creates missing subtree blocks", () => {
76+
const queryTree = { uid: "parent", children: [] };
77+
mockedGetSubTree.mockReturnValue({ uid: "", children: [] } as never);
78+
79+
parseQuery(queryTree as never);
80+
81+
expect(mockedCreateBlock).toHaveBeenCalledTimes(3);
82+
});
83+
});
84+
85+
describe("getTitleDatalog", () => {
86+
it("maps :in input target to variable binding", () => {
87+
const clauses = getTitleDatalog({ source: "node", target: ":in title" });
88+
expect(clauses[0]).toMatchObject({
89+
type: "data-pattern",
90+
arguments: [
91+
{ type: "variable", value: "node" },
92+
{ type: "constant", value: ":node/title" },
93+
{ type: "variable", value: "title" },
94+
],
95+
});
96+
});
97+
98+
it("maps regex target to re-find expression", () => {
99+
const clauses = getTitleDatalog({ source: "node", target: "/hello/i" });
100+
expect(clauses).toHaveLength(3);
101+
expect(clauses[1]).toMatchObject({ type: "fn-expr", fn: "re-pattern" });
102+
expect(clauses[2]).toMatchObject({ type: "pred-expr", pred: "re-find" });
103+
});
104+
});

0 commit comments

Comments
 (0)