Skip to content

Commit 6f3a22c

Browse files
authored
ENG-1525 Add Vitest and unit tests for roam utils; update package.json (#1140)
* test(roam): use tilde imports in utils tests * test(roam): run vitest in ci * chore: catalog vitest dependency
1 parent 89afafd commit 6f3a22c

10 files changed

Lines changed: 486 additions & 91 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ jobs:
3434
- name: Check TypeScript Types
3535
run: npx turbo check-types
3636

37+
- name: Run Tests
38+
run: npx turbo run test --filter=roam --ui stream
39+
3740
lint-changed-files:
3841
if: github.event_name == 'pull_request'
3942
runs-on: ubuntu-latest

apps/roam/package.json

Lines changed: 6 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 --config vitest.config.mts",
14+
"test:watch": "vitest --config vitest.config.mts"
1315
},
1416
"license": "Apache-2.0",
1517
"devDependencies": {
@@ -19,14 +21,16 @@
1921
"@repo/typescript-config": "workspace:*",
2022
"@types/file-saver": "2.0.5",
2123
"@types/nanoid": "2.0.0",
24+
"@types/node": "^20",
2225
"@types/react": "catalog:roam",
2326
"@types/react-dom": "catalog:roam",
2427
"@types/react-vertical-timeline-component": "^3.3.3",
2528
"axios": "^0.27.2",
2629
"dotenv": "^16.0.3",
2730
"esbuild": "0.17.14",
2831
"tailwindcss": "^3.4.17",
29-
"tsx": "^4.19.2"
32+
"tsx": "^4.19.2",
33+
"vitest": "catalog:"
3034
},
3135
"//": "axios dep temporary - need to fix the dep in underlying libraries",
3236
"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: "get",
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: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
type ConditionToDatalogArgs = {
4+
source: string;
5+
target: string;
6+
};
7+
8+
vi.mock("~/utils/conditionToDatalog", () => ({
9+
default: vi.fn(({ source, target }: ConditionToDatalogArgs) => [
10+
{
11+
type: "data-pattern",
12+
arguments: [
13+
{ type: "variable", value: source },
14+
{ type: "constant", value: ":rel" },
15+
/^:in /.test(target)
16+
? { type: "variable", value: target.substring(4) }
17+
: { type: "constant", value: '"value"' },
18+
],
19+
},
20+
]),
21+
}));
22+
vi.mock("~/utils/predefinedSelections", () => ({
23+
default: [
24+
{
25+
test: /^created$/,
26+
pull: () => "(pull ?node [:create/time])",
27+
mapper: (r: Record<string, string>) => r[":create/time"] || "",
28+
},
29+
],
30+
}));
31+
vi.mock("roamjs-components/util/env", () => ({ getNodeEnv: () => "test" }));
32+
33+
import fireQuery, { fireQuerySync, getDatalogQuery } from "~/utils/fireQuery";
34+
35+
describe("getDatalogQuery", () => {
36+
it("includes :in variables and de-duplicates expected inputs", async () => {
37+
const built = getDatalogQuery({
38+
conditions: [
39+
{
40+
type: "clause",
41+
relation: "r",
42+
source: "node",
43+
target: ":in title",
44+
uid: "1",
45+
not: false,
46+
},
47+
{
48+
type: "clause",
49+
relation: "r",
50+
source: "node",
51+
target: ":in title",
52+
uid: "2",
53+
not: false,
54+
},
55+
],
56+
selections: [{ uid: "s1", text: "created", label: "Created" }],
57+
inputs: { title: "Graph" },
58+
});
59+
60+
expect(built.query).toContain(":in $ ?title");
61+
expect(built.inputs).toEqual(["Graph"]);
62+
const formatted = await built.formatResult([
63+
{ ":node/title": "A", ":block/uid": "u1" },
64+
{ ":block/uid": "u1" },
65+
{ ":create/time": "123" },
66+
]);
67+
expect(formatted).toMatchObject({ text: "A", uid: "u1", Created: "123" });
68+
});
69+
});
70+
71+
describe("fireQuery", () => {
72+
beforeEach(() => {
73+
(globalThis as { window: unknown }).window = {
74+
roamAlphaAPI: {
75+
data: {
76+
async: {
77+
fast: {
78+
q: vi
79+
.fn()
80+
.mockResolvedValue([
81+
[
82+
{ ":node/title": "Local", ":block/uid": "l1" },
83+
{ ":block/uid": "l1" },
84+
],
85+
]),
86+
},
87+
},
88+
backend: {
89+
q: vi
90+
.fn()
91+
.mockResolvedValue([
92+
[
93+
{ ":node/title": "Remote", ":block/uid": "r1" },
94+
{ ":block/uid": "r1" },
95+
],
96+
]),
97+
},
98+
fast: {
99+
q: vi
100+
.fn()
101+
.mockReturnValue([
102+
[{ ":node/title": "Sync", ":block/uid": "s1" }],
103+
]),
104+
},
105+
},
106+
},
107+
};
108+
});
109+
110+
it("uses backend queries by default and maps output", async () => {
111+
const results = await fireQuery({ conditions: [], selections: [] });
112+
expect(results[0]).toMatchObject({ text: "Remote", uid: "r1" });
113+
});
114+
115+
it("uses async fast query when local=true", async () => {
116+
const results = await fireQuery({
117+
conditions: [],
118+
selections: [],
119+
local: true,
120+
});
121+
expect(results[0]).toMatchObject({ text: "Local", uid: "l1" });
122+
});
123+
124+
it("returns sync mapped results", () => {
125+
const results = fireQuerySync({ conditions: [], selections: [] });
126+
expect(results).toEqual([{ text: "Sync", uid: "s1" }]);
127+
});
128+
});

0 commit comments

Comments
 (0)