From 2f61b248d2a931fcd71798dd7c915054a98cec9b Mon Sep 17 00:00:00 2001 From: Colleen Reilly Date: Wed, 11 Mar 2026 17:17:18 -0500 Subject: [PATCH] New unit tests for singleCellCellType & singleCellGeneExpression tws --- client/tw/singleCellCellType.ts | 3 +- client/tw/singleCellGeneExpression.ts | 5 +- .../tw/test/singleCellCellType.unit.spec.ts | 102 ++++++++++++ .../singleCellGeneExpression.unit.spec.ts | 153 ++++++++++++++++++ release.txt | 3 +- 5 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 client/tw/test/singleCellCellType.unit.spec.ts create mode 100644 client/tw/test/singleCellGeneExpression.unit.spec.ts diff --git a/client/tw/singleCellCellType.ts b/client/tw/singleCellCellType.ts index 5934c49ac3..5218437a89 100644 --- a/client/tw/singleCellCellType.ts +++ b/client/tw/singleCellCellType.ts @@ -1,6 +1,7 @@ import type { RawSingleCellCellTypeTerm, SingleCellCellTypeTerm, TermGroupSetting, TermValues } from '#types' +import { TermTypes } from '#shared/terms.js' -const termType = 'singleCellCellType' +const termType = TermTypes.SINGLECELL_CELL_TYPE export class SingleCellCellTypeBase { type = termType diff --git a/client/tw/singleCellGeneExpression.ts b/client/tw/singleCellGeneExpression.ts index 32673058cb..605643bf32 100644 --- a/client/tw/singleCellGeneExpression.ts +++ b/client/tw/singleCellGeneExpression.ts @@ -1,7 +1,8 @@ import type { RawSingleCellGeneExpTerm, SingleCellGeneExpressionTerm } from '#types' import type { TwOpts } from './TwBase.ts' +import { TermTypes } from '#shared/terms.js' -const termType = 'singleCellGeneExpression' +const termType = TermTypes.SINGLECELL_GENE_EXPRESSION export class SingleCellGeneExpressionBase { type = termType @@ -19,7 +20,7 @@ export class SingleCellGeneExpressionBase { } static validate(term: RawSingleCellGeneExpTerm) { - if (typeof term !== 'object') throw new Error('term is not an object') + if (!term || typeof term !== 'object') throw new Error('term is not an object') if (term.type != termType) throw new Error(`incorrect term.type='${term?.type}', expecting '${termType}'`) if (!term.gene && !term.name) throw new Error('no gene or name present') if (!term.gene || typeof term.gene != 'string') throw new Error(`${termType} term.gene must be non-empty string`) diff --git a/client/tw/test/singleCellCellType.unit.spec.ts b/client/tw/test/singleCellCellType.unit.spec.ts new file mode 100644 index 0000000000..45507f40e0 --- /dev/null +++ b/client/tw/test/singleCellCellType.unit.spec.ts @@ -0,0 +1,102 @@ +import tape from 'tape' +import { SingleCellCellTypeBase } from '../singleCellCellType.ts' +import { TermTypes } from '#shared/terms.js' + +/************************* + reusable helper functions +**************************/ + +function getValidRawTerm(overrides: any = {}) { + return { + type: TermTypes.SINGLECELL_CELL_TYPE, + sample: { sID: 'S1', eID: 'E1' }, + plot: 'My plot', + ...overrides + } +} + +/************** + test sections +***************/ + +tape('\n', function (test) { + test.comment('-***- tw/singleCellCellType -***-') + test.end() +}) + +tape('validate() should throw on invalid terms', test => { + test.throws( + () => SingleCellCellTypeBase.validate(null as any), + /term is not an object/, + 'Should throw when term is not an object' + ) + + test.throws( + () => SingleCellCellTypeBase.validate({ type: 'categorical' } as any), + /incorrect term.type='categorical'/, + 'Should throw when term.type is incorrect' + ) + + test.doesNotThrow( + () => SingleCellCellTypeBase.validate(getValidRawTerm() as any), + 'Should accept valid singleCellCellType term' + ) + + test.end() +}) + +tape('fill() should add default groupsetting and values when missing', test => { + const term = getValidRawTerm({ groupsetting: undefined, values: undefined }) + SingleCellCellTypeBase.fill(term as any) + + test.deepEqual(term.groupsetting, { disabled: false }, 'Should set default groupsetting') + test.deepEqual(term.values, {}, 'Should set default values object') + test.end() +}) + +tape('fill() should preserve existing groupsetting and values', test => { + const term = getValidRawTerm({ + groupsetting: { disabled: true, lst: [{ name: 'g1', groups: [] }] }, + values: { A: { key: 'A', label: 'A' } } + }) + SingleCellCellTypeBase.fill(term as any) + + test.equal(term.groupsetting.disabled, true, 'Should preserve existing groupsetting.disabled') + test.deepEqual(term.values, { A: { key: 'A', label: 'A' } }, 'Should preserve existing values') + test.end() +}) + +tape('fill() should no-op for class instances', test => { + const instance = new SingleCellCellTypeBase(getValidRawTerm() as any) + test.doesNotThrow( + () => SingleCellCellTypeBase.fill(instance as any), + 'Should not throw when fill is called on instance' + ) + test.end() +}) + +tape('constructor should set explicit term fields', test => { + const term = getValidRawTerm({ + groupsetting: { disabled: true }, + values: { B: { key: 'B', label: 'B' } } + }) + const x = new SingleCellCellTypeBase(term as any) + + test.equal(x.type, TermTypes.SINGLECELL_CELL_TYPE, 'Should set type') + test.deepEqual(x.sample, { sID: 'S1', eID: 'E1' }, 'Should set sample') + test.equal(x.plot, 'My plot', 'Should set plot') + test.deepEqual(x.groupsetting, { disabled: true }, 'Should preserve provided groupsetting') + test.deepEqual(x.values, { B: { key: 'B', label: 'B' } }, 'Should preserve provided values') + test.end() +}) + +tape('constructor should fallback to default fields when optional fields missing', test => { + const term = getValidRawTerm({ sample: undefined, plot: undefined, groupsetting: undefined, values: undefined }) + const x = new SingleCellCellTypeBase(term as any) + + test.deepEqual(x.sample, {}, 'Should fallback sample to empty object') + test.equal(x.plot, '', 'Should fallback plot to empty string') + test.deepEqual(x.groupsetting, { disabled: false }, 'Should fallback groupsetting') + test.deepEqual(x.values, {}, 'Should fallback values') + test.end() +}) diff --git a/client/tw/test/singleCellGeneExpression.unit.spec.ts b/client/tw/test/singleCellGeneExpression.unit.spec.ts new file mode 100644 index 0000000000..275fae1806 --- /dev/null +++ b/client/tw/test/singleCellGeneExpression.unit.spec.ts @@ -0,0 +1,153 @@ +import tape from 'tape' +import { SingleCellGeneExpressionBase, getSCGEunit } from '../singleCellGeneExpression.ts' +import { TermTypes } from '#shared/terms.js' + +/************************* + reusable helper functions +**************************/ + +const mockVocabApi = { + termdbConfig: { + queries: { + singleCell: { + geneExpression: { unit: 'log2 CPM' } + } + } + } +} + +const mockVocabApiNoUnit = { + termdbConfig: { + queries: { + singleCell: { + geneExpression: {} + } + } + } +} + +function getValidRawTerm(overrides: any = {}) { + return { + type: TermTypes.SINGLECELL_GENE_EXPRESSION, + gene: 'TP53', + sample: 'Tumor cells', + ...overrides + } +} + +/************** + test sections +***************/ + +tape('\n', function (test) { + test.comment('-***- tw/singleCellGeneExpression -***-') + test.end() +}) + +tape('getSCGEunit() should return configured unit and fallback default unit', test => { + test.equal(getSCGEunit(mockVocabApi as any), 'log2 CPM', 'Should return configured unit from termdbConfig') + test.equal(getSCGEunit(mockVocabApiNoUnit as any), 'Gene Expression', 'Should fallback to default unit') + test.end() +}) + +tape('validate() should throw on invalid terms', test => { + test.throws( + () => SingleCellGeneExpressionBase.validate(null as any), + /term is not an object/, + 'Should throw when term is not an object' + ) + + test.throws( + () => SingleCellGeneExpressionBase.validate({ type: TermTypes.GENE_EXPRESSION } as any), + /incorrect term.type='geneExpression'/, + 'Should throw when term.type is incorrect' + ) + + test.throws( + () => + SingleCellGeneExpressionBase.validate({ + type: TermTypes.SINGLECELL_GENE_EXPRESSION, + sample: 'Tumor cells' + } as any), + /no gene or name present/, + 'Should throw when both gene and name are missing' + ) + + test.throws( + () => + SingleCellGeneExpressionBase.validate({ + type: TermTypes.SINGLECELL_GENE_EXPRESSION, + gene: 123, + name: 'Bad gene', + sample: 'Tumor cells' + } as any), + /singleCellGeneExpression term.gene must be non-empty string/, + 'Should throw when gene is not a non-empty string' + ) + + test.throws( + () => + SingleCellGeneExpressionBase.validate({ + type: TermTypes.SINGLECELL_GENE_EXPRESSION, + gene: 'TP53' + } as any), + /missing sample name/, + 'Should throw when sample is missing' + ) + + test.end() +}) + +tape('fill() should populate missing name and unit', test => { + const term = getValidRawTerm({ name: undefined, unit: undefined }) + SingleCellGeneExpressionBase.fill(term as any, { vocabApi: mockVocabApi as any } as any) + + test.equal(term.unit, 'log2 CPM', 'Should set unit from vocabApi') + test.equal(term.name, 'TP53 log2 CPM', 'Should set generated name from gene and unit') + test.end() +}) + +tape('fill() should not overwrite existing name', test => { + const term = getValidRawTerm({ name: 'Custom label', unit: undefined }) + SingleCellGeneExpressionBase.fill(term as any, { vocabApi: mockVocabApi as any } as any) + + test.equal(term.name, 'Custom label', 'Should preserve existing name') + test.equal(term.unit, undefined, 'Should not force unit when name already exists') + test.end() +}) + +tape('fill() should no-op for class instances', test => { + const instance = new SingleCellGeneExpressionBase(getValidRawTerm(), { vocabApi: mockVocabApi as any } as any) + test.doesNotThrow( + () => SingleCellGeneExpressionBase.fill(instance as any, { vocabApi: mockVocabApi as any } as any), + 'Should not throw when fill is called on instance' + ) + test.end() +}) + +tape('constructor should set fields and use configured unit', test => { + const term = getValidRawTerm({ unit: undefined }) + const x = new SingleCellGeneExpressionBase(term as any, { vocabApi: mockVocabApi as any } as any) + + test.equal(x.type, TermTypes.SINGLECELL_GENE_EXPRESSION, 'Should set type') + test.equal(x.gene, 'TP53', 'Should set gene') + test.equal(x.sample, 'Tumor cells', 'Should set sample') + test.equal(x.unit, 'log2 CPM', 'Should set configured unit when term.unit is missing') + test.end() +}) + +tape('constructor should use default unit when config unit is missing', test => { + const term = getValidRawTerm({ unit: undefined }) + const x = new SingleCellGeneExpressionBase(term as any, { vocabApi: mockVocabApiNoUnit as any } as any) + + test.equal(x.unit, 'Gene Expression', 'Should fallback to default unit') + test.end() +}) + +tape('constructor should preserve explicit term.unit', test => { + const term = getValidRawTerm({ unit: 'Custom Unit' }) + const x = new SingleCellGeneExpressionBase(term as any, { vocabApi: mockVocabApi as any } as any) + + test.equal(x.unit, 'Custom Unit', 'Should preserve explicit term.unit') + test.end() +}) diff --git a/release.txt b/release.txt index 8b13789179..bfab779390 100644 --- a/release.txt +++ b/release.txt @@ -1 +1,2 @@ - +Features +- New unit tests for recently created termwrappers, singleCellCellType and singleCellGeneExpression.