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 7038b66316..bfab779390 100644 --- a/release.txt +++ b/release.txt @@ -1,6 +1,2 @@ Features -- Expanded integration tests for TermTypeSearch. New unit tests available for the recently added Single-Cell Cell Type and Single-Cell Gene Expression handlers as well as previously implemented Gene Expression, SNP, ssGSEA, and Term Collection handlers. - -Fixes -- Simplified logic for custom termType2terms vocabulary. Resolved issue with elements from other handlers lingering on tab change and the pills for the singleCellCellType not appearing after clicking away from the Single-Cell Cell Type tab. - \ No newline at end of file +- New unit tests for recently created termwrappers, singleCellCellType and singleCellGeneExpression.