Skip to content

Commit bb10806

Browse files
committed
[test] Unit test createStateFromQueryOrJSONs
This was primarily motivated by the bug identified in <nextstrain/auspice.us#122> and fixed via a one-line patch in <nextstrain#1986>. I confirmed that the tests added here would have flagged this bug (via reverting ece625f). There's a lot of future directions we can take tests such as these, this is only the starting point.
1 parent c35c123 commit bb10806

2 files changed

Lines changed: 204 additions & 0 deletions

File tree

test/data/generic.json

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
{
2+
"version": "v2",
3+
"meta": {
4+
"title": "Generic (small) feature-rich dataset for testing purposes",
5+
"updated": "2023-03-28",
6+
"build_url": "https://github.com/nextstrain/auspice",
7+
"build_avatar": "https://avatars.githubusercontent.com/u/22159334?s=48&v=4",
8+
"description": "# Markdown description",
9+
"warning": "Achtung!",
10+
"maintainers": [{"name": "The nextstrain team", "url": "https://nextstrain.org"}],
11+
"genome_annotations": {"nuc": { "end": 100, "start": 1 }},
12+
"filters": ["subtree"],
13+
"panels": ["tree", "map", "entropy"],
14+
"extensions": "any data structure!",
15+
"geo_resolutions": [{"key": "subtree2", "title": "Subtree2 (demes)", "demes": {"red": {"latitude": 0, "longitude": -125}, "purple": {"latitude": -5, "longitude": -130}}}],
16+
"colorings": [
17+
{ "key": "num_date", "title": "Sampling date", "type": "temporal"},
18+
{ "key": "subtree", "title": "subtree (Colour)", "type": "categorical", "scale": [["blue", "#2c7fb8"], ["green", "#78c679"]]},
19+
{ "key": "subtree2", "title": "subtree 2 (Colour)", "type": "categorical", "scale": [["red", "#f03b20"], ["purple", "#8856a7"]]},
20+
{ "key": "ord", "title": "ordinal (string)", "type": "ordinal"},
21+
{ "key": "bool", "title": "Boolean / Binary", "type": "boolean"}
22+
],
23+
"display_defaults": {
24+
"map_triplicate": false,
25+
"geo_resolution": "subtree2",
26+
"color_by": "subtree2",
27+
"distance_measure": "div",
28+
"layout": "rect",
29+
"branch_label": "node_name",
30+
"label": "node_name:alpha",
31+
"tip_label": "subtree",
32+
"stream_label": "river",
33+
"transmission_lines": true,
34+
"language": "en",
35+
"sidebar": "open",
36+
"panels": ["tree", "map", "entropy"]
37+
},
38+
"stream_labels": ["river"],
39+
"data_provenance": [{"name": "test-data", "url": "https://github.com/nextstrain/auspice"}]
40+
},
41+
"tree": {
42+
"name": "romeo",
43+
"node_attrs": {"div": 0, "subtree": {"value": "green"}, "subtree2": {"value": "red"}, "ord": {"value": "A"}, "bool": {"value": "True"}, "num_date": {"value": 2000.00}},
44+
"branch_attrs": {"labels": {"node_name": "romeo"}, "mutations": {"nuc": ["T50A"]}},
45+
"children": [
46+
{
47+
"name": "alpha",
48+
"node_attrs": {"div": 1, "subtree": {"value": "green"}, "subtree2": {"value": "red"}, "ord": {"value": "A"}, "bool": {"value": "True"},
49+
"num_date": {"value": 2003.00}},
50+
"branch_attrs": {"labels": {"node_name": "alpha", "river": "Nile"}, "mutations": {"nuc": []}},
51+
"children": [
52+
{
53+
"name": "bravo",
54+
"node_attrs": {"div": 2, "subtree": {"value": "green"}, "subtree2": {"value": "red"}, "ord": {"value": "C"}, "bool": {"value": "True"}, "num_date": {"value": 2005.00}},
55+
"branch_attrs": {"labels": {"node_name": "bravo"}, "mutations": {"nuc": []}}
56+
},
57+
{
58+
"name": "charlie",
59+
"node_attrs": {"div": 2, "subtree": {"value": "green"}, "subtree2": {"value": "red"}, "num_date": {"value": 2012.00}},
60+
"branch_attrs": {"labels": {"node_name": "charlie"}, "mutations": {"nuc": []}},
61+
"children": [
62+
{
63+
"name": "delta",
64+
"node_attrs": {"div": 3, "subtree": {"value": "green"}, "subtree2": {"value": "purple"}, "ord": {"value": "B"}, "bool": {"value": "1"}, "num_date": {"value": 2015.00}},
65+
"branch_attrs": {"labels": {"node_name": "delta"}, "mutations": {"nuc": []}}
66+
},
67+
{
68+
"name": "echo",
69+
"node_attrs": {"div": 3, "subtree": {"value": "blue"}, "subtree2": {"value": "purple"}, "ord": {"value": "B"}, "bool": {"value": "True"}, "num_date": {"value": 2018.00}},
70+
"branch_attrs": {"labels": {"node_name": "echo", "river": "Lena"}, "mutations": {"nuc": ["A50T"]}},
71+
"children": [
72+
{
73+
"name": "foxtrot",
74+
"node_attrs": {"div": 4, "subtree": {"value": "blue"}, "subtree2": {"value": "purple"}, "ord": {"value": "B"}, "bool": {"value": 0}, "num_date": {"value": 2019.00}},
75+
"branch_attrs": {"labels": {"node_name": "foxtrot"}, "mutations": {"nuc": []}}
76+
},
77+
{
78+
"name": "golf",
79+
"node_attrs": {"div": 4, "subtree": {"value": "blue"}, "subtree2": {"value": "purple"}, "ord": {"value": "A"}, "bool": {"value": "False"}, "num_date": {"value": 2020.00}},
80+
"branch_attrs": {"labels": {"node_name": "golf"}, "mutations": {"nuc": []}}
81+
}
82+
]
83+
}
84+
]
85+
}
86+
]
87+
}
88+
]
89+
},
90+
"root_sequence": "ATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGCATGC"
91+
}

test/recomputeReduxState.test.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
/* NOTE: We use the jsdom environment since `createStateFromQueryOrJSONs` relies on `window` */
5+
6+
import { createStateFromQueryOrJSONs } from "../src/actions/recomputeReduxState";
7+
import genericMainJson from './data/generic.json';
8+
9+
/**
10+
* The `createStateFromQueryOrJSONs` function is a large and complex function used at dataset load time
11+
* as well as to effect narrative slide changes. The aim here is to test it on a number of datasets and
12+
* different ways the function can be called. Simply testing that the function returns an object (i.e.
13+
* doesn't throw) is enough to start with, as there's only a single return point in the function.
14+
*
15+
* Future directions:
16+
* - assert dispatch is called in certain circumstances
17+
* - check console logs (console.error's are expected in many circumstances)
18+
* - create a similar test for sidecar files
19+
* - consider using these datasets to smoke test via playright
20+
* - create state then call `createStateFromQueryOrJSONs` again
21+
*/
22+
23+
24+
/**
25+
* Define a list of dataset permutations, each element being an array with
26+
* a short description (string) and a function which takes in the mainDataset (object)
27+
* and returns a modified version.
28+
*/
29+
const datasetPermutationSpecifications = [
30+
["generic dataset JSON", (dataset) => dataset],
31+
..._modifyJsonToRemoveOptionalMetaKeys(),
32+
..._modifyJsonToRemoveOptionalDisplayDefaultKeys(),
33+
]
34+
35+
function* datasetPermutations() {
36+
for (const [summary, modify] of datasetPermutationSpecifications) {
37+
yield [summary, modify(clone(genericMainJson))]
38+
}
39+
}
40+
41+
42+
/**
43+
* Run through each permutation of the dataset and turn it into react state
44+
* via the simplest possible call to `createStateFromQueryOrJSONs`
45+
*/
46+
for (const [summary, mainDataset] of datasetPermutations()) {
47+
it(`Basic redux state creation for: ${summary}`, () => {
48+
const state = createStateFromQueryOrJSONs({
49+
json: mainDataset,
50+
query: {},
51+
dispatch,
52+
})
53+
expect(state).toBeInstanceOf(Object)
54+
})
55+
}
56+
57+
/**
58+
* Test two-trees
59+
*/
60+
for (const [summary, modifiedDataset] of datasetPermutations()) {
61+
it(`Basic redux state creation for two trees: generic + ${summary}`, () => {
62+
const state = createStateFromQueryOrJSONs({
63+
json: modifiedDataset,
64+
query: {},
65+
dispatch,
66+
})
67+
expect(state).toBeInstanceOf(Object)
68+
})
69+
}
70+
71+
72+
function _modifyJsonToRemoveOptionalMetaKeys() {
73+
return Object.keys(genericMainJson['meta']) // all are optional in the schema
74+
.filter(() => true) // NOTE: the schema requires "updated", "panels", but auspice can handle cases where these are missing
75+
.map((key) => [
76+
`missing .meta key "${key}"`,
77+
(dataset) => {
78+
delete dataset['meta'][key];
79+
return dataset
80+
}
81+
])
82+
}
83+
84+
85+
86+
function _modifyJsonToRemoveOptionalDisplayDefaultKeys() {
87+
// Since the dataset works without the `display_defaults` object at all, this seems a little unnecessary,
88+
// but I guess there's no harm?
89+
return Object.keys(genericMainJson['meta']['display_defaults']) // all are optional in the schema
90+
.filter(() => true) // all are optional in the schema
91+
.map((key) => [
92+
`missing .meta.display_default key "${key}"`,
93+
(dataset) => {
94+
delete dataset['meta']['display_defaults'][key];
95+
return dataset
96+
}
97+
])
98+
}
99+
100+
101+
function dispatch() {
102+
/* no-op */
103+
}
104+
105+
/**
106+
* Clone a (JSON) dataset. We can't use the native `window.structuredClone`
107+
* because we're in a jsdom test environment, and we can't polyfill it without
108+
* updating our version of core-js (structuredClone added in v3.20.0). Since the
109+
* dataset is originally JSON it's fine to use the classic stringify-parse approach.
110+
*/
111+
function clone(dataset) {
112+
return JSON.parse(JSON.stringify(dataset))
113+
}

0 commit comments

Comments
 (0)