Skip to content

Commit e7d8bd5

Browse files
authored
Merge pull request #592 from input-output-hk/djo/fix_explorer_state_reload
Fix explorer state reload & add unit tests using jest to explorer
2 parents 6bfb144 + 1ad530e commit e7d8bd5

File tree

12 files changed

+2777
-107
lines changed

12 files changed

+2777
-107
lines changed

.github/workflows/docs.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
path: |
5858
docs/build/*
5959
60-
build-explorer:
60+
build-test-explorer:
6161
runs-on: ubuntu-22.04
6262
steps:
6363
- name: Checkout sources
@@ -70,6 +70,15 @@ jobs:
7070
cache: 'yarn'
7171
cache-dependency-path: mithril-explorer/yarn.lock
7272

73+
- name: Install dependencies
74+
working-directory: mithril-explorer
75+
run: yarn install --frozen-lockfile
76+
77+
- name: Test explorer
78+
working-directory: mithril-explorer
79+
run: |
80+
make test
81+
7382
- name: Build Explorer
7483
working-directory: mithril-explorer
7584
run: |
@@ -109,7 +118,7 @@ jobs:
109118
needs:
110119
- cargo-doc
111120
- build-docusaurus
112-
- build-explorer
121+
- build-test-explorer
113122
- build-open-api-ui
114123
steps:
115124
- name: Download mithril-rust-doc artifact

docs/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: clean install
1+
.PHONY: clean install build serve dev upgrade
22

33
yarn.lock:
44
yarn install

mithril-explorer/Makefile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: clean install
1+
.PHONY: clean install build dev test watch-test lint upgrade
22

33
yarn.lock:
44
yarn install
@@ -15,6 +15,12 @@ dev:
1515
@echo "Serving dev build at: http://localhost:3000/explorer"
1616
yarn run dev
1717

18+
test: yarn.lock
19+
yarn run test:ci
20+
21+
watch-test: yarn.lock
22+
yarn run test
23+
1824
lint:
1925
yarn run lint
2026

@@ -26,4 +32,5 @@ clean:
2632
upgrade: clean install
2733
yarn upgrade next@latest react@latest react-bootstrap@latest react-dom@latest bootstrap@latest \
2834
bootstrap-icons@latest eslint@latest eslint-config-next@latest @reduxjs/toolkit@latest \
29-
next-redux-wrapper@latest react-redux@latest
35+
next-redux-wrapper@latest react-redux@latest @popperjs/core@latest \
36+
jest@latest jest-environment-jsdom@latest @testing-library/react@latest @testing-library/jest-dom@latest
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { render, screen } from '@testing-library/react'
2+
import '@testing-library/jest-dom'
3+
import AggregatorSetter from "../components/AggregatorSetter";
4+
import {initStore} from "./helpers";
5+
import {Provider} from "react-redux";
6+
import default_available_aggregators from "../aggregators-list";
7+
import {settingsSlice} from "../store/settingsSlice";
8+
9+
function renderAggregatorSetter(default_state = undefined) {
10+
const store = initStore(default_state);
11+
return [
12+
render(
13+
<Provider store={store}>
14+
<AggregatorSetter/>
15+
</Provider>
16+
),
17+
store
18+
];
19+
}
20+
21+
describe('AggregatorSetter', () => {
22+
it ('Load with data from the store', () => {
23+
const [_, store] = renderAggregatorSetter();
24+
const settingsState = store.getState().settings;
25+
26+
expect(screen.getByRole('option', { name: settingsState.selectedAggregator }).selected).toBe(true);
27+
expect(screen.getAllByRole('option').map(o => o.value)).toEqual(settingsState.availableAggregators);
28+
});
29+
30+
it ('Load custom aggregators', () => {
31+
const customAggregator = "http://aggregator.test";
32+
renderAggregatorSetter({
33+
settings: {
34+
...settingsSlice.getInitialState(),
35+
selectedAggregator: customAggregator,
36+
availableAggregators: [...default_available_aggregators, customAggregator],
37+
}
38+
});
39+
40+
expect(screen.getByRole('option', { name: customAggregator }).selected).toBe(true);
41+
expect(screen.getAllByRole('option').map(o => o.value)).toContain(customAggregator);
42+
});
43+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {saveToLocalStorage, storeBuilder} from "../store/store";
2+
3+
const baseLocation = 'http://localhost';
4+
5+
function initStore(default_state = undefined) {
6+
if (default_state) {
7+
saveToLocalStorage(default_state);
8+
}
9+
return storeBuilder();
10+
}
11+
12+
/**
13+
* Reset the windows location api to `http://localhost`
14+
*/
15+
function resetLocation() {
16+
setLocation(new URL(baseLocation));
17+
}
18+
19+
/**
20+
* Set the window.location to the given url
21+
*
22+
* If you use it define a beforeEach with resetLocation else the new location will persist between tests.
23+
* @param url The new location
24+
*/
25+
function setLocation(url) {
26+
Object.defineProperty(window, 'location', {
27+
set(v) {
28+
this._href = v;
29+
},
30+
get() {
31+
return this._href;
32+
}
33+
})
34+
35+
window.location = url;
36+
}
37+
38+
/**
39+
* Set the window.location search/query aggregator param to the given aggregator
40+
*
41+
* If you use it define a beforeEach with resetLocation else the new location will persist between tests.
42+
* @param aggregatorUrl The target aggregator
43+
*/
44+
function setLocationToAggregator(aggregatorUrl) {
45+
setLocation(new URL(`?aggregator=${aggregatorUrl}`, baseLocation));
46+
}
47+
48+
module.exports = {
49+
initStore,
50+
setLocation,
51+
setLocationToAggregator,
52+
resetLocation,
53+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {saveToLocalStorage, storeBuilder} from "../store/store";
2+
import {
3+
removeSelectedAggregator,
4+
selectAggregator,
5+
settingsSlice,
6+
setUpdateInterval,
7+
toggleAutoUpdate
8+
} from "../store/settingsSlice";
9+
import default_available_aggregators from "../aggregators-list";
10+
import {initStore, resetLocation, setLocationToAggregator} from "./helpers";
11+
12+
describe('Store Initialization', () => {
13+
beforeEach(() => {
14+
resetLocation();
15+
});
16+
17+
it('init with settings initialState without local storage', () => {
18+
const store = initStore();
19+
20+
expect(store.getState().settings).toEqual(settingsSlice.getInitialState());
21+
});
22+
23+
it('init with local storage saved state', () => {
24+
let aggregators = [...default_available_aggregators, "https://aggregator.test"];
25+
let expected = {
26+
settings: {
27+
...settingsSlice.getInitialState(),
28+
selectedAggregator: aggregators.at(aggregators.length - 1),
29+
availableAggregators: aggregators,
30+
updateInterval: 12345,
31+
}
32+
};
33+
saveToLocalStorage(expected);
34+
const store = storeBuilder();
35+
36+
expect(store.getState()).toEqual(expected);
37+
});
38+
39+
it('init with local storage and default aggregator in url', () => {
40+
const aggregatorInUrl = default_available_aggregators.at(1);
41+
setLocationToAggregator(aggregatorInUrl);
42+
let aggregators = [...default_available_aggregators, "https://aggregator.test"];
43+
let expected = {
44+
settings: {
45+
...settingsSlice.getInitialState(),
46+
selectedAggregator: aggregators.at(aggregators.length - 1),
47+
availableAggregators: aggregators,
48+
updateInterval: 12345,
49+
}
50+
};
51+
saveToLocalStorage(expected);
52+
expected.settings.selectedAggregator = aggregatorInUrl;
53+
const store = storeBuilder();
54+
55+
expect(store.getState()).toEqual(expected);
56+
});
57+
58+
it('Can toggle autoUpdate', () => {
59+
const store = initStore();
60+
61+
store.dispatch(toggleAutoUpdate());
62+
expect(store.getState().settings.autoUpdate).toEqual(false);
63+
64+
store.dispatch(toggleAutoUpdate());
65+
expect(store.getState().settings.autoUpdate).toEqual(true);
66+
});
67+
68+
it('Can change updateInterval', () => {
69+
const store = initStore();
70+
const expected = 124325;
71+
72+
store.dispatch(setUpdateInterval(expected));
73+
expect(store.getState().settings.updateInterval).toEqual(expected);
74+
});
75+
76+
it('Can change selectedAggregator', () => {
77+
const store = initStore();
78+
const expected = default_available_aggregators[2];
79+
80+
store.dispatch(selectAggregator(expected));
81+
expect(store.getState().settings.selectedAggregator).toEqual(expected);
82+
});
83+
84+
it('Add a custom aggregator when selectAggregator is called with an unknown aggregator', () => {
85+
const store = initStore();
86+
const expected = "http://aggregator.test";
87+
88+
store.dispatch(selectAggregator(expected));
89+
expect(store.getState().settings.selectedAggregator).toEqual(expected);
90+
expect(store.getState().settings.availableAggregators).toContain(expected);
91+
});
92+
93+
it('Can\'t remove a default aggregator', () => {
94+
const store = initStore();
95+
96+
store.dispatch(removeSelectedAggregator());
97+
expect(store.getState().settings.availableAggregators).toContain(default_available_aggregators[0]);
98+
});
99+
100+
it('Can remove a custom aggregator', () => {
101+
const customAggregator = "http://aggregator.test";
102+
const store = initStore({
103+
settings: {
104+
...settingsSlice.getInitialState(),
105+
selectedAggregator: customAggregator,
106+
availableAggregators: [...default_available_aggregators, customAggregator],
107+
}
108+
});
109+
110+
store.dispatch(removeSelectedAggregator());
111+
expect(store.getState().settings.availableAggregators).not.toContain(customAggregator);
112+
});
113+
});

mithril-explorer/components/AggregatorSetter/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {useState} from 'react';
22
import {Button, Col, Form, InputGroup, OverlayTrigger, Tooltip} from "react-bootstrap";
33
import {useDispatch, useSelector} from "react-redux";
4-
import {removeCustomAggregator, selectAggregator} from "../../store/settingsSlice";
4+
import {removeSelectedAggregator, selectAggregator} from "../../store/settingsSlice";
55
import AddAggregatorModal from "./AddAggregatorModal";
66

77
export default function AggregatorSetter(props) {
@@ -32,7 +32,7 @@ export default function AggregatorSetter(props) {
3232
</Button>
3333
{canRemoveSelected &&
3434
<>
35-
<Button variant="outline-danger" onClick={() => dispatch(removeCustomAggregator())}>
35+
<Button variant="outline-danger" onClick={() => dispatch(removeSelectedAggregator())}>
3636
<i className="bi bi-dash-circle"></i>
3737
</Button>
3838
<OverlayTrigger overlay={<Tooltip>Unofficial Aggregator</Tooltip>}>

mithril-explorer/jest.config.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// jest.config.js
2+
const nextJest = require('next/jest')
3+
4+
const createJestConfig = nextJest({
5+
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
6+
dir: './',
7+
})
8+
9+
// Add any custom config to be passed to Jest
10+
/** @type {import('jest').Config} */
11+
const customJestConfig = {
12+
// Add more setup options before each test is run
13+
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
14+
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
15+
moduleDirectories: ['node_modules', '<rootDir>/'],
16+
testEnvironment: 'jest-environment-jsdom',
17+
testPathIgnorePatterns: ['__tests__/helpers.*'],
18+
}
19+
20+
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
21+
module.exports = createJestConfig(customJestConfig)

mithril-explorer/package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
"dev": "next dev",
77
"build": "next build && next export",
88
"start": "next start",
9-
"lint": "next lint"
9+
"lint": "next lint",
10+
"test": "jest --watch",
11+
"test:ci": "jest --ci"
1012
},
1113
"dependencies": {
14+
"@popperjs/core": "^2.11.6",
1215
"@reduxjs/toolkit": "^1.9.0",
1316
"bootstrap": "^5.2.2",
1417
"bootstrap-icons": "^1.9.1",
@@ -20,7 +23,11 @@
2023
"react-redux": "^8.0.5"
2124
},
2225
"devDependencies": {
26+
"@testing-library/jest-dom": "^5.16.5",
27+
"@testing-library/react": "^13.4.0",
2328
"eslint": "^8.27.0",
24-
"eslint-config-next": "^13.0.2"
29+
"eslint-config-next": "^13.0.2",
30+
"jest": "^29.3.1",
31+
"jest-environment-jsdom": "^29.3.1"
2532
}
2633
}

mithril-explorer/store/settingsSlice.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const settingsSlice = createSlice({
3737
canRemoveSelected: !default_available_aggregators.includes(action.payload),
3838
}
3939
},
40-
removeCustomAggregator: (state) => {
40+
removeSelectedAggregator: (state) => {
4141
if (default_available_aggregators.includes(state.selectedAggregator)) {
4242
return state;
4343
}
@@ -56,7 +56,7 @@ export const {
5656
setUpdateInterval,
5757
toggleAutoUpdate,
5858
selectAggregator,
59-
removeCustomAggregator
59+
removeSelectedAggregator
6060
} = settingsSlice.actions;
6161

6262
export default settingsSlice.reducer;

0 commit comments

Comments
 (0)