|
| 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 | +}); |
0 commit comments