Skip to content

Commit 10929ec

Browse files
committed
adding tests
1 parent 709819f commit 10929ec

File tree

4 files changed

+424
-4
lines changed

4 files changed

+424
-4
lines changed

frontend/src/pages/node/associationColumns.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ export interface ColumnContext {
1717
getCategoryLabel: (idOrDefault: string) => string;
1818
}
1919

20-
/**
21-
* Build the AppTable columns for an association category. Mirrors the existing
22-
* in-component logic, but as a pure function.
23-
*/
20+
/** Build the AppTable columns for an association category. . */
2421
export function buildAssociationCols(ctx: ColumnContext): Cols<Datum> {
2522
const { categoryId, nodeCategory, isDirect, items, getCategoryLabel } = ctx;
2623

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { DirectionalAssociation } from "@/api/model";
3+
import { buildAssociationCols } from "@/pages/node/associationColumns";
4+
5+
type Ctx = Parameters<typeof buildAssociationCols>[0];
6+
7+
const makeRow = (patch: Record<string, any> = {}): DirectionalAssociation =>
8+
({
9+
direction: "outgoing",
10+
id: "S1-O1",
11+
subject: "S1",
12+
subject_label: "Subject",
13+
subject_category: "biolink:Gene",
14+
object: "O1",
15+
object_label: "Object",
16+
object_category: "biolink:Disease",
17+
predicate: "biolink:related_to",
18+
evidence_count: 1,
19+
primary_knowledge_source: "SourceX",
20+
original_subject: "S1",
21+
...patch,
22+
}) as any;
23+
24+
const makeCtx = (patch: Partial<Ctx> = {}): Ctx => ({
25+
categoryId: "biolink:SomeAssociation",
26+
nodeCategory: "biolink:Gene",
27+
isDirect: true,
28+
items: [makeRow()],
29+
getCategoryLabel: (id) =>
30+
id === "biolink:Gene" ? "Gene" : id === "biolink:Disease" ? "Disease" : id,
31+
...patch,
32+
});
33+
34+
const keys = (cols: Array<{ key?: string; slot?: string }>) =>
35+
cols.map((c) => c.key ?? c.slot);
36+
37+
describe("buildAssociationCols", () => {
38+
it("builds base columns with headings derived from first item categories", () => {
39+
const cols = buildAssociationCols(makeCtx());
40+
expect(keys(cols).slice(0, 4)).toEqual([
41+
"subject_label",
42+
"predicate",
43+
"object_label",
44+
"evidence_count",
45+
]);
46+
// headings reflect getCategoryLabel on subject/object categories
47+
expect(cols[0].heading).toBe("Gene");
48+
expect(cols[2].heading).toBe("Disease");
49+
});
50+
51+
it("adds extra 'Taxon' column for Interaction categories (with divider)", () => {
52+
const cols = buildAssociationCols(
53+
makeCtx({ categoryId: "biolink:ProteinProteinInteraction" }),
54+
);
55+
// last two entries should be divider then taxon
56+
const end = cols.slice(-2);
57+
expect(end[0].slot).toBe("divider");
58+
expect(end[1].slot).toBe("taxon");
59+
});
60+
61+
it("CausalGeneToDiseaseAssociation (Disease, Direct): removes object & predicate; inserts Source before Details", () => {
62+
const cols = buildAssociationCols(
63+
makeCtx({
64+
nodeCategory: "biolink:Disease",
65+
isDirect: true,
66+
categoryId: "biolink:CausalGeneToDiseaseAssociation",
67+
}),
68+
);
69+
const k = keys(cols);
70+
expect(k).not.toContain("object_label");
71+
expect(k).not.toContain("predicate");
72+
const iSource = k.indexOf("primary_knowledge_source");
73+
const iDetails = k.indexOf("evidence_count");
74+
expect(iSource).toBeGreaterThan(-1);
75+
expect(iSource).toBeLessThan(iDetails);
76+
});
77+
78+
it("VariantToDiseaseAssociation: Direct removes object; always adds Source before Details", () => {
79+
const direct = buildAssociationCols(
80+
makeCtx({
81+
nodeCategory: "biolink:Disease",
82+
isDirect: true,
83+
categoryId: "biolink:VariantToDiseaseAssociation",
84+
}),
85+
);
86+
const kd = keys(direct);
87+
expect(kd).not.toContain("object_label");
88+
const iSourceD = kd.indexOf("primary_knowledge_source");
89+
const iDetailsD = kd.indexOf("evidence_count");
90+
expect(iSourceD).toBeGreaterThan(-1);
91+
expect(iSourceD).toBeLessThan(iDetailsD);
92+
93+
const all = buildAssociationCols(
94+
makeCtx({
95+
nodeCategory: "biolink:Disease",
96+
isDirect: false,
97+
categoryId: "biolink:VariantToDiseaseAssociation",
98+
}),
99+
);
100+
const ka = keys(all);
101+
expect(ka).toContain("object_label"); // not removed on All
102+
const iSourceA = ka.indexOf("primary_knowledge_source");
103+
const iDetailsA = ka.indexOf("evidence_count");
104+
expect(iSourceA).toBeGreaterThan(-1);
105+
expect(iSourceA).toBeLessThan(iDetailsA);
106+
});
107+
108+
it("Disease + Direct + other categories: removes subject_label and predicate", () => {
109+
const cols = buildAssociationCols(
110+
makeCtx({
111+
nodeCategory: "biolink:Disease",
112+
isDirect: true,
113+
categoryId: "biolink:OtherDiseaseAssociation",
114+
}),
115+
);
116+
const k = keys(cols);
117+
expect(k).not.toContain("subject_label");
118+
expect(k).not.toContain("predicate");
119+
expect(k).toContain("object_label");
120+
});
121+
122+
it("GenotypeToDiseaseAssociation: removes predicate, ensures Taxon before Subject; adds Source before Details on Direct", () => {
123+
const cols = buildAssociationCols(
124+
makeCtx({
125+
nodeCategory: "biolink:Disease",
126+
isDirect: true,
127+
categoryId: "biolink:GenotypeToDiseaseAssociation",
128+
}),
129+
);
130+
const k = keys(cols);
131+
expect(k).not.toContain("predicate");
132+
const iTaxon = k.indexOf("taxon");
133+
const iSubject = k.indexOf("subject_label");
134+
expect(iTaxon).toBeGreaterThan(-1);
135+
expect(iTaxon).toBeLessThan(iSubject);
136+
const iSource = k.indexOf("primary_knowledge_source");
137+
const iDetails = k.indexOf("evidence_count");
138+
expect(iSource).toBeGreaterThan(-1);
139+
expect(iSource).toBeLessThan(iDetails);
140+
});
141+
142+
it("DiseaseToPhenotypicFeatureAssociation (Disease, All): swaps subject and object", () => {
143+
const cols = buildAssociationCols(
144+
makeCtx({
145+
nodeCategory: "biolink:Disease",
146+
isDirect: false,
147+
categoryId: "biolink:DiseaseToPhenotypicFeatureAssociation",
148+
}),
149+
);
150+
const k = keys(cols);
151+
const iSub = k.indexOf("subject_label");
152+
const iObj = k.indexOf("object_label");
153+
expect(iSub).toBeGreaterThan(-1);
154+
expect(iObj).toBeGreaterThan(-1);
155+
expect(iObj).toBeLessThan(iSub);
156+
});
157+
158+
it("GeneToPhenotypicFeatureAssociation (Disease, All): drops predicate and adds Disease Context if any row has it", () => {
159+
const cols = buildAssociationCols(
160+
makeCtx({
161+
nodeCategory: "biolink:Disease",
162+
isDirect: false,
163+
categoryId: "biolink:GeneToPhenotypicFeatureAssociation",
164+
items: [makeRow({ disease_context_qualifier: "X" })],
165+
}),
166+
);
167+
const k = keys(cols);
168+
expect(k).not.toContain("predicate");
169+
expect(k).toContain("disease_context_qualifier");
170+
const iCtx = k.indexOf("disease_context_qualifier");
171+
const iSub = k.indexOf("subject_label");
172+
expect(iCtx).toBeLessThan(iSub);
173+
});
174+
175+
it("PhenotypicFeature categories add Frequency and Onset columns (plus divider)", () => {
176+
const cols = buildAssociationCols(
177+
makeCtx({
178+
categoryId: "biolink:GeneToPhenotypicFeatureAssociation",
179+
nodeCategory: "biolink:Gene",
180+
isDirect: true,
181+
}),
182+
);
183+
const k = keys(cols);
184+
expect(k).toContain("divider");
185+
expect(k).toContain("frequency_qualifier");
186+
expect(k).toContain("onset_qualifier_label");
187+
});
188+
189+
it("DiseaseToPhenotypicFeature adds 'original_subject' as Source in extra columns", () => {
190+
const cols = buildAssociationCols(
191+
makeCtx({
192+
categoryId: "biolink:DiseaseToPhenotypicFeatureAssociation",
193+
}),
194+
);
195+
expect(keys(cols)).toContain("original_subject");
196+
});
197+
198+
it("Header renames for G2P when viewing a Disease node", () => {
199+
const cols = buildAssociationCols(
200+
makeCtx({
201+
nodeCategory: "biolink:Disease",
202+
categoryId: "biolink:GeneToPhenotypicFeatureAssociation",
203+
}),
204+
);
205+
const byKey = Object.fromEntries(cols.map((c) => [c.key ?? c.slot, c]));
206+
expect(byKey["subject_label"].heading).toBe("Causal Genes");
207+
expect(byKey["object_label"].heading).toBe("Causal Gene Phenotypes");
208+
});
209+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, expect, it } from "vitest";
2+
import { useAssociationCategories } from "@/composables/use-association-categories";
3+
4+
type AssocCount = { category?: string; label: string; count: number };
5+
type TestNode = { association_counts?: AssocCount[] };
6+
7+
const hidden =
8+
"biolink:ChemicalOrDrugOrTreatmentToDiseaseOrPhenotypicFeatureAssociation";
9+
const causal = "biolink:CausalGeneToDiseaseAssociation";
10+
const genePh = "biolink:GeneToPhenotypicFeatureAssociation";
11+
12+
describe("useAssociationCategories", () => {
13+
it("returns [] when association_counts is missing", () => {
14+
const node: TestNode = {};
15+
const { options } = useAssociationCategories(node as any);
16+
expect(options.value).toEqual([]);
17+
});
18+
19+
it("maps to {id,label,count} and startCases the label", () => {
20+
const node: TestNode = {
21+
association_counts: [{ category: "X", label: "hello world", count: 3 }],
22+
};
23+
const { options } = useAssociationCategories(node as any);
24+
expect(options.value).toEqual([
25+
{ id: "X", label: "Hello World", count: 3 },
26+
]);
27+
});
28+
29+
it("filters out hidden categories", () => {
30+
const node: TestNode = {
31+
association_counts: [
32+
{ category: hidden, label: "should hide", count: 1 },
33+
{ category: "Y", label: "keep me", count: 2 },
34+
],
35+
};
36+
const { options } = useAssociationCategories(node as any);
37+
expect(options.value.map((o) => o.id)).toEqual(["Y"]);
38+
});
39+
40+
it("keeps special order: causal before gene→phenotype", () => {
41+
// causal appears after genePh initially → should be moved before
42+
const node: TestNode = {
43+
association_counts: [
44+
{
45+
category: genePh,
46+
label: "gene to phenotypic feature association",
47+
count: 5,
48+
},
49+
{
50+
category: causal,
51+
label: "causal gene to disease association",
52+
count: 2,
53+
},
54+
],
55+
};
56+
const { options } = useAssociationCategories(node as any);
57+
expect(options.value.map((o) => o.id)).toEqual([causal, genePh]);
58+
});
59+
60+
it("does not reorder if one of the two categories is missing", () => {
61+
const node: TestNode = {
62+
association_counts: [
63+
{
64+
category: genePh,
65+
label: "gene to phenotypic feature association",
66+
count: 5,
67+
},
68+
{ category: "Z", label: "other", count: 1 },
69+
],
70+
};
71+
const { options } = useAssociationCategories(node as any);
72+
expect(options.value.map((o) => o.id)).toEqual([genePh, "Z"]);
73+
});
74+
75+
it("handles missing category by substituting empty id", () => {
76+
const node: TestNode = {
77+
association_counts: [{ category: undefined, label: "no id", count: 1 }],
78+
};
79+
const { options } = useAssociationCategories(node as any);
80+
expect(options.value[0]).toMatchObject({ id: "", count: 1 });
81+
});
82+
});

0 commit comments

Comments
 (0)