89 | You need 10 or more correct answers to earn a badge. On this
90 | test you got 9 answer(s) correct.
91 |
92 | `;
93 |
--------------------------------------------------------------------------------
/src/learn/home.jsx:
--------------------------------------------------------------------------------
1 | const FloatingActionButton = require('@material-ui/core/Fab').default;
2 | const { Link } = require('react-router-dom');
3 | const Button = require('@material-ui/core/Button').default;
4 | const React = require('react');
5 | const { Share, BugReport } = require('@material-ui/icons');
6 | const Tooltip = require('@material-ui/core/Tooltip').default;
7 |
8 | const style = {
9 | marginTop: '20px',
10 | };
11 |
12 | const githubIconStyle = {
13 | textAlign: 'right',
14 | marginTop: '20px',
15 | };
16 |
17 | function share() {
18 | navigator.share({
19 | title: 'Math Drill',
20 | text: 'Math Exercises for Kids',
21 | url: 'https://learn.guyellisrocks.com',
22 | })
23 | // eslint-disable-next-line no-console
24 | .then(() => console.log('Successful share'))
25 | // eslint-disable-next-line no-console
26 | .catch((error) => console.log('Error sharing', error));
27 | }
28 |
29 | function shareComponent() {
30 | if (!navigator.share) {
31 | return null;
32 | }
33 | const shareStyle = {
34 | paddingLeft: '20px',
35 | };
36 | return (
37 | ",
3 | "bugs": {
4 | "url": "https://github.com/guyellis/learn/issues"
5 | },
6 | "dependencies": {
7 | "@material-ui/core": "4.3.3",
8 | "@material-ui/icons": "4.2.1",
9 | "autoprefixer": "10.4.16",
10 | "fs-extra": "8.1.0",
11 | "html-webpack-plugin": "5.5.0",
12 | "lodash": "4.17.21",
13 | "material-ui": "0.20.2",
14 | "mini-css-extract-plugin": "0.8.0",
15 | "moment": "2.29.4",
16 | "node-sass": "9.0.0",
17 | "prop-types": "15.7.2",
18 | "react": "16.9.0",
19 | "react-dom": "16.9.0",
20 | "react-markdown": "8.0.4",
21 | "react-router-dom": "5.1.2",
22 | "sass-loader": "7.3.1",
23 | "webpack": "5.94.0",
24 | "webpack-cleanup-plugin": "0.5.1",
25 | "webpack-cli": "3.3.12"
26 | },
27 | "description": "Experimental learning exercises for my kids",
28 | "devDependencies": {
29 | "awesome-typescript-loader": "5.2.1",
30 | "babel-eslint": "10.0.3",
31 | "css-loader": "6.8.1",
32 | "eslint": "6.7.2",
33 | "eslint-config-airbnb": "18.0.1",
34 | "eslint-config-airbnb-base": "14.0.0",
35 | "eslint-plugin-import": "2.18.2",
36 | "eslint-plugin-jest": "23.1.1",
37 | "eslint-plugin-jsx-a11y": "6.2.3",
38 | "eslint-plugin-react": "7.14.3",
39 | "file-loader": "5.0.2",
40 | "jest": "29.3.1",
41 | "postcss-loader": "7.3.3",
42 | "pre-commit": "1.2.2",
43 | "react-hot-loader": "4.12.18",
44 | "react-test-renderer": "16.8.6",
45 | "source-map-loader": "0.2.4",
46 | "style-loader": "1.0.1",
47 | "ts-jest": "29.0.3",
48 | "typescript": "3.5.3",
49 | "url-loader": "3.0.0",
50 | "webpack-dev-server": "5.2.1"
51 | },
52 | "engines": {
53 | "node": ">=10",
54 | "npm": ">=6"
55 | },
56 | "homepage": "https://github.com/guyellis/learn#readme",
57 | "license": "MIT",
58 | "main": "index.js",
59 | "name": "learn",
60 | "pre-commit": {
61 | "colors": true,
62 | "run": [
63 | "test"
64 | ],
65 | "silent": false
66 | },
67 | "repository": {
68 | "type": "git",
69 | "url": "git+https://github.com/guyellis/learn.git"
70 | },
71 | "scripts": {
72 | "build": "webpack --config webpack.production.config.js --progress --profile --colors",
73 | "coverage:view": "google-chrome coverage/lcov-report/index.html",
74 | "coverage": "tsc && jest --coverage",
75 | "deploy": "npm run build && npm run asset-copy && npm run upload",
76 | "lint": "eslint --ext js --ext jsx .",
77 | "lintfix": "eslint --ext js --ext jsx . --fix",
78 | "start": "webpack-dev-server --hot --progress --profile --colors",
79 | "test": "npm run lint && npm run lintfix && tsc && jest --coverage && npm run build",
80 | "asset-copy": "cp -R assets/* public/",
81 | "upload": "aws s3 cp public s3://learn.guyellisrocks.com/ --recursive"
82 | },
83 | "version": "0.0.2"
84 | }
85 |
--------------------------------------------------------------------------------
/test/learn/math/drill/score-bar.test.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const renderer = require('react-test-renderer');
3 | const ScoreBar = require('../../../../src/learn/math/drill/score-bar');
4 |
5 | function mockDates(times) {
6 | Date.now = jest.fn();
7 | times.forEach((time) => {
8 | const mockDate = time.date + time.dateMockDelta;
9 | Date.now.mockImplementationOnce(() => mockDate);
10 | });
11 | }
12 |
13 | describe('ScoreBar', () => {
14 | const nowFn = Date.now;
15 | afterAll(() => {
16 | Date.now = nowFn;
17 | });
18 |
19 | test('should not show if showScoreBar is false', () => {
20 | const times = [];
21 |
22 | const component = renderer.create( );
26 |
27 | const finishedBadge = component.toJSON();
28 | expect(finishedBadge).toMatchSnapshot();
29 | });
30 |
31 | test('should not show if times is empty', () => {
32 | const times = [];
33 |
34 | const component = renderer.create( );
38 |
39 | const finishedBadge = component.toJSON();
40 | expect(finishedBadge).toMatchSnapshot();
41 | });
42 |
43 | test('should show 1 column with Gold Badge', () => {
44 | const times = [{
45 | date: 1506395542848, // 9/25/2017 8:12pm PST
46 | timePerQuestion: 1.5,
47 | dateMockDelta: 1000 * 60 * 4,
48 | }];
49 |
50 | mockDates(times);
51 |
52 | const component = renderer.create( );
56 |
57 | const finishedBadge = component.toJSON();
58 | expect(finishedBadge).toMatchSnapshot();
59 | });
60 |
61 | test('should show 1 column with below Blue Badge', () => {
62 | const times = [{
63 | date: 1506395542848, // 9/25/2017 8:12pm PST
64 | timePerQuestion: 15.2,
65 | dateMockDelta: 1000 * 60 * 4,
66 | }];
67 |
68 | mockDates(times);
69 |
70 | const component = renderer.create( );
74 |
75 | const finishedBadge = component.toJSON();
76 | expect(finishedBadge).toMatchSnapshot();
77 | });
78 |
79 | test('should show 5 columns with mixed Badges', () => {
80 | const times = [{
81 | date: 1506395542848, // 9/25/2017 8:12pm PST
82 | timePerQuestion: 15.2,
83 | dateMockDelta: 1000 * 60 * 4,
84 | }, {
85 | date: 1506395642848, // A bit later
86 | timePerQuestion: 10.2,
87 | dateMockDelta: 1000 * 60 * 2,
88 | }, {
89 | date: 1506395742848, // And later
90 | timePerQuestion: 5.2,
91 | dateMockDelta: 1000 * 3,
92 | }];
93 |
94 | mockDates(times);
95 |
96 | const component = renderer.create( );
100 |
101 | const finishedBadge = component.toJSON();
102 | expect(finishedBadge).toMatchSnapshot();
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/test/learn/math/drill/__snapshots__/badge-totals.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Badge totals should be rendered 1`] = `
4 |
5 |
6 |
29 |
32 | 1
33 |
34 |
35 |
36 | Gold Badge(s) - 2 seconds or less (per question)
37 |
38 |
39 |
40 |
63 |
66 | 2
67 |
68 |
69 |
70 | Silver Badge(s) - between 2 and 3 seconds (per question)
71 |
72 |
73 |
74 |
97 |
100 | 3
101 |
102 |
103 |
104 | Bronze Badge(s) - between 3 and 4 seconds (per question)
105 |
106 |
107 |
108 | `;
109 |
--------------------------------------------------------------------------------
/src/learn/math/drill/scoreboard.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const TableHead = require('@material-ui/core/TableHead').default;
3 | const TableRow = require('@material-ui/core/TableRow').default;
4 | const TableBody = require('@material-ui/core/TableBody').default;
5 | const TableCell = require('@material-ui/core/TableCell').default;
6 | const Table = require('@material-ui/core/Table').default;
7 | const Paper = require('@material-ui/core/Paper').default;
8 |
9 | const Badge = require('./badge');
10 | const BadgeTotals = require('./badge-totals');
11 | const constants = require('../../common/constants');
12 | const helper = require('./helper');
13 |
14 | const {
15 | ALPHABET: alphabet,
16 | OPERATION_NAMES: operationNames,
17 | } = constants;
18 |
19 |
20 | function badge(badges) {
21 | if (!badges) {
22 | return null;
23 | }
24 | return (
25 |
26 | {
27 | badges.map((amount, color) => {
28 | if (!amount) {
29 | return null;
30 | }
31 | const key = `${amount}${color}`;
32 |
33 | return (
34 |
39 | );
40 | })
41 | }
42 |
43 | );
44 | }
45 |
46 | function scoreboard() {
47 | const { ops: operations, totals, levels } = helper.getScoreboard();
48 | const ops = [...operations];
49 | const opNames = ops.map((op) => {
50 | const operators = op.split('');
51 | const names = operators.map((operator) => operationNames[parseInt(operator, 10)]);
52 | return names.join(' / ');
53 | });
54 | /*
55 | {
56 | ops, // A Set of strings representing operators
57 | totals, // A 4 element array of badge totals
58 | levels, // An sparse array of up to 26 elements of objects with keys matching values in ops Set
59 | }
60 | */
61 |
62 | return (
63 |
64 |
65 | Scoreboard
66 |
67 |
68 |
69 |
70 |
71 |
72 | Level
73 | {opNames.map((opName) => (
74 |
75 | {opName}
76 |
77 | ))}
78 |
79 |
80 |
81 | {levels.map((level, index) => (level ? (
82 |
83 |
84 | {alphabet[index]}
85 |
86 | {ops.map((op) => (
87 | {badge(level[op])}
88 | ))}
89 |
90 | ) : ('')
91 | ))}
92 |
93 |
94 |
95 |
96 | );
97 | }
98 |
99 | module.exports = scoreboard;
100 |
--------------------------------------------------------------------------------
/src/learn/math/drill/running-results.jsx:
--------------------------------------------------------------------------------
1 |
2 | const React = require('react');
3 | const PropTypes = require('prop-types');
4 | const constants = require('../../common/constants');
5 | const Badge = require('./badge');
6 | const helper = require('./helper');
7 |
8 | const { OPERATIONS } = constants;
9 | const spanStyle = { paddingLeft: 20 };
10 | const inABox = {
11 | borderWidth: 1,
12 | borderStyle: 'solid',
13 | marginLeft: 20,
14 | padding: 3,
15 | };
16 | const check = '\u2714';
17 | const xmark = '\u2717';
18 | const lineStyle = { lineHeight: '35px' };
19 | const correctStyle = { color: 'green', ...lineStyle };
20 | const incorrectStyle = { color: 'red', ...lineStyle };
21 | const spanStyleIncorrect = { color: 'red', ...spanStyle };
22 |
23 | function runningResults(props) {
24 | const { previousResults, showIndex } = props;
25 | const previousResultRows = previousResults.map(({
26 | task, actuals, timeTaken, id,
27 | }) => {
28 | const [left, right, opIndex, answer] = task;
29 | const [actual] = actuals; // most recent answer is first in array
30 | const incorrects = actuals.slice(1).reverse();
31 | const operation = OPERATIONS[opIndex];
32 | const correct = answer === actual;
33 | const style = correct ? correctStyle : incorrectStyle;
34 | const colorIndex = helper.getBadgeColorIndex(timeTaken);
35 | return (
36 |
37 | {
38 | showIndex
39 | && (
40 |
41 | {`${id + 1})`}
42 |
43 | )
44 | }
45 |
46 |
47 | {left}
48 |
49 |
50 | {operation}
51 |
52 |
53 | {right}
54 |
55 |
56 | =
57 |
58 |
59 | {actual}
60 |
61 |
62 | {correct ? check : xmark}
63 |
64 |
65 | {`${timeTaken} seconds`}
66 |
67 | {
68 | !!incorrects.length
69 | && (
70 |
71 | {`Incorrect answer(s): ${incorrects.join()}`}
72 |
73 | )
74 | }
75 |
76 | );
77 | });
78 | return (
79 |
80 | {previousResultRows}
81 |
82 | );
83 | }
84 |
85 | runningResults.propTypes = {
86 | previousResults: PropTypes.arrayOf(PropTypes.shape({
87 | actuals: PropTypes.arrayOf(PropTypes.number).isRequired,
88 | id: PropTypes.number.isRequired,
89 | task: PropTypes.array.isRequired, // left, right, opIndex, answer
90 | timeTaken: PropTypes.number.isRequired,
91 | })).isRequired,
92 | showIndex: PropTypes.bool,
93 | };
94 |
95 | runningResults.defaultProps = {
96 | showIndex: false,
97 | };
98 |
99 | module.exports = runningResults;
100 |
--------------------------------------------------------------------------------
/src/learn/math/drill/running.jsx:
--------------------------------------------------------------------------------
1 | const PropTypes = require('prop-types');
2 | const React = require('react');
3 | const constants = require('../../common/constants');
4 | const QuizLine = require('./quiz-line');
5 | const RunningResults = require('./running-results');
6 |
7 | const {
8 | ALPHABET: alphabet,
9 | OPERATIONS: operations,
10 | } = constants;
11 |
12 | function running(props) {
13 | const {
14 | checkAnswer,
15 | currentTask,
16 | largeKeyboard,
17 | levelIndex,
18 | newRecord,
19 | onscreenKeyboard,
20 | opIndexes,
21 | previousResults,
22 | questionsRemaining,
23 | timeLeft,
24 | } = props;
25 |
26 | if (!currentTask) {
27 | // eslint-disable-next-line no-console
28 | console.warn('No currentTask defined in renderRunning');
29 | return null;
30 | }
31 |
32 | const spanStyle = {
33 | paddingLeft: 20,
34 | };
35 |
36 | const lastTen = previousResults.slice(Math.max(0, previousResults.length - 10)).reverse();
37 | const lastResult = lastTen[0];
38 |
39 | return (
40 |
41 |
42 |
43 | {`Level: ${alphabet[levelIndex]}`}
44 |
45 |
46 | {`Operation(s): ${opIndexes.map((i) => operations[i]).join()}`}
47 |
48 |
49 |
50 |
51 | {`Time Left: ${timeLeft} seconds`}
52 |
53 |
54 | {`Questions Remaining: ${questionsRemaining}`}
55 |
56 |
57 |
58 |
66 |
70 |
71 |
72 | );
73 | }
74 |
75 | running.propTypes = {
76 | checkAnswer: PropTypes.func.isRequired,
77 | currentTask: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
78 | largeKeyboard: PropTypes.bool.isRequired,
79 | levelIndex: PropTypes.number.isRequired,
80 | newRecord: PropTypes.shape({
81 | isNewRecord: PropTypes.bool.isRequired,
82 | currentTimePerQuestion: PropTypes.number.isRequired,
83 | existingRecordTimePerQuestion: PropTypes.number.isRequired,
84 | }).isRequired,
85 | onscreenKeyboard: PropTypes.bool.isRequired,
86 | opIndexes: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired,
87 | previousResults: PropTypes.arrayOf(PropTypes.shape({
88 | task: PropTypes.array.isRequired, // left, right, opIndex, answer
89 | actuals: PropTypes.arrayOf(PropTypes.number).isRequired,
90 | timeTaken: PropTypes.number.isRequired,
91 | id: PropTypes.number.isRequired,
92 | })).isRequired,
93 | questionsRemaining: PropTypes.number.isRequired,
94 | timeLeft: PropTypes.number.isRequired,
95 | };
96 |
97 | running.defaultProps = {
98 | // currentRecord: null,
99 | };
100 |
101 | module.exports = running;
102 |
--------------------------------------------------------------------------------
/test/learn/__snapshots__/menu.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Menu should render a Menu component 1`] = `
4 |
120 | `;
121 |
--------------------------------------------------------------------------------
/src/learn/math/drill/keyboard.jsx:
--------------------------------------------------------------------------------
1 | const FloatingActionButton = require('@material-ui/core/Fab').default;
2 | const PropTypes = require('prop-types');
3 | const React = require('react');
4 | const { Backspace, KeyboardReturn } = require('@material-ui/icons');
5 | const Tooltip = require('@material-ui/core/Tooltip').default;
6 |
7 | const normalButtonStyle = {
8 | margin: '5px',
9 | fontSize: '2em',
10 | };
11 |
12 | const largeButtonStyle = {
13 | height: '80px',
14 | width: '80px',
15 | margin: '5px',
16 | fontSize: '2em',
17 | };
18 |
19 | let buttonStyle = {};
20 |
21 | // eslint-disable-next-line react/prefer-stateless-function
22 | class Keyboard extends React.Component {
23 | constructor(props) {
24 | super(props);
25 | const buttons = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'back', 'enter', 'nothing'];
26 | this.click = buttons.reduce((acc, item) => {
27 | acc[item] = this.onClick.bind(this, item);
28 | return acc;
29 | }, {});
30 |
31 | if (props.largeKeyboard) {
32 | buttonStyle = { ...largeButtonStyle };
33 | } else {
34 | buttonStyle = { ...normalButtonStyle };
35 | }
36 | }
37 |
38 | onClick(value) {
39 | if (value === 'nothing') {
40 | return;
41 | }
42 | const { keyPress } = this.props;
43 | keyPress(value);
44 | }
45 |
46 | render() {
47 | const {
48 | onscreenKeyboard,
49 | } = this.props;
50 |
51 | if (!onscreenKeyboard) {
52 | return null;
53 | }
54 |
55 | const layout = [
56 | [
57 | '1', // key
58 | ['1', 'One', 1, '1'],
59 | ['2', 'Two', 2, '2'],
60 | ['3', 'Three', 3, '3'],
61 | [ , 'Backspace', 'back', '4'],
62 | ],
63 | [
64 | '2', // key
65 | ['4', 'Four', 4, '5'],
66 | ['5', 'Five', 5, '6'],
67 | ['6', 'Six', 6, '7'],
68 | [' ', ' ', 'Nothing1', '8'],
69 | ],
70 | [
71 | '3', // key
72 | ['7', 'Seven', 7, '9'],
73 | ['8', 'Eight', 8, '10'],
74 | ['9', 'Nine', 9, '11'],
75 | [' ', ' ', 'Nothing2', '12'],
76 | ],
77 | [
78 | '4', // key
79 | [' ', ' ', 'Nothing3', '13'],
80 | ['0', 'Zero', 0, '14'],
81 | [' ', ' ', 'Nothing4', '15'],
82 | [ , 'Enter', 'enter', '16'],
83 | ],
84 | ];
85 |
86 | return (
87 |
88 | {
89 | layout.map((lay) => (
90 |
91 | {
92 | lay.slice(1).map((item) => {
93 | const [content, title, click, key] = item;
94 | return (
95 |
96 | this.onClick(click)}
99 | style={buttonStyle}
100 | >
101 | {content}
102 |
103 |
104 | );
105 | })
106 | }
107 |
108 | ))
109 | }
110 |
111 | );
112 | }
113 | }
114 |
115 | Keyboard.propTypes = {
116 | keyPress: PropTypes.func.isRequired,
117 | onscreenKeyboard: PropTypes.bool.isRequired,
118 | largeKeyboard: PropTypes.bool.isRequired,
119 | };
120 |
121 | module.exports = Keyboard;
122 |
--------------------------------------------------------------------------------
/test/learn/math/drill/__snapshots__/quiz-line.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Badge totals should be rendered 1`] = `
4 |
5 |
13 |
21 | 1
22 |
23 |
31 | +
32 |
33 |
41 | 1
42 |
43 |
51 | =
52 |
53 |
82 |
105 |
108 |
115 |
118 |
119 | Check Answer
120 |
121 |
122 |
123 |
136 | Ready for your first answer...
137 |
138 |
139 |
149 | Current Speed 1.1 and Record 1
150 |
151 |
152 |
153 |
154 | `;
155 |
--------------------------------------------------------------------------------
/src/learn/db/index.js:
--------------------------------------------------------------------------------
1 | const constants = require('../common/constants');
2 |
3 | const {
4 | DEFAULT_OPTIONS,
5 | MATH_DRILL_OPTIONS,
6 | MATH_DRILL_SCORES,
7 | } = constants;
8 |
9 | class DB {
10 | static setItem(key, value) {
11 | localStorage.setItem(key, JSON.stringify(value));
12 | }
13 |
14 | static getItem(key) {
15 | const item = localStorage.getItem(key);
16 | return item ? JSON.parse(item) : item;
17 | }
18 |
19 | static saveOptions(data) {
20 | DB.setItem(MATH_DRILL_OPTIONS, data);
21 | }
22 |
23 | static getOptions() {
24 | let options = DB.getItem(MATH_DRILL_OPTIONS);
25 | if (options) {
26 | if (typeof options.userName !== 'string') {
27 | options.userName = '';
28 | DB.saveOptions(options);
29 | }
30 | if (typeof options.largeKeyboard !== 'boolean') {
31 | options.largeKeyboard = false;
32 | DB.saveOptions(options);
33 | }
34 | if (typeof options.opIndex === 'number') {
35 | options.opIndexes = [options.opIndex];
36 | delete options.opIndex;
37 | DB.saveOptions(options);
38 | }
39 | } else {
40 | options = DEFAULT_OPTIONS;
41 | DB.saveOptions(options);
42 | }
43 | return options;
44 | }
45 |
46 | static saveScores(data) {
47 | DB.setItem(MATH_DRILL_SCORES, data);
48 | }
49 |
50 | static appendScore(score) {
51 | const scores = DB.getScores() || [];
52 | scores.push(score);
53 | DB.saveScores({
54 | version: 3,
55 | scores,
56 | });
57 | }
58 |
59 |
60 | /**
61 | * Converts the scores in localStorage from Version 1 to 2
62 | * @static
63 | * @param {array} scores - Version 1 array of scores
64 | * @returns {object} - new scores shape
65 | * @memberof DB
66 | */
67 | static convertVersion1(scores) {
68 | // Need to change it to an object and the previousResults array needs to have the
69 | // actual property changed to an actuals array.
70 | scores.forEach((score) => {
71 | if (score.previousResults) {
72 | score.previousResults.forEach((previousResult) => {
73 | if (previousResult.actual) {
74 | // eslint-disable-next-line no-param-reassign
75 | previousResult.actuals = [previousResult.actual];
76 | // eslint-disable-next-line no-param-reassign
77 | delete previousResult.actual;
78 | }
79 | });
80 | }
81 | });
82 |
83 | return {
84 | version: 2,
85 | scores,
86 | };
87 | }
88 |
89 |
90 | /**
91 | * Converts the scores in localStorage from Version 2 to 3
92 | * @static
93 | * @param {object} scores - version 2 scores
94 | * @memberof DB
95 | */
96 | static convertVersion2(scoresObj) {
97 | const { version, scores } = scoresObj;
98 | if (version !== 2) {
99 | throw new Error(`version must be 2 and not ${version} in convertVersion2`);
100 | }
101 | // Fix bug - "key" in scores objects should have had a delimiter between the level
102 | // and the operators.
103 | scores.forEach((score) => {
104 | const { levelIndex, opIndexes } = score;
105 | // eslint-disable-next-line no-param-reassign
106 | score.key = `${levelIndex}-${opIndexes.sort().join('')}`;
107 | });
108 |
109 | return {
110 | version: 3,
111 | scores,
112 | };
113 | }
114 |
115 | static getScores() {
116 | const scores = DB.getItem(MATH_DRILL_SCORES);
117 |
118 | if (Array.isArray(scores)) {
119 | const convertedScores = DB.convertVersion1(scores);
120 | DB.saveScores(convertedScores);
121 | return DB.getScores();
122 | }
123 |
124 | if (scores && scores.version === 2) {
125 | const convertedScores = DB.convertVersion2(scores);
126 | DB.saveScores(convertedScores);
127 | return DB.getScores();
128 | }
129 |
130 | return scores && scores.scores;
131 | }
132 | }
133 |
134 | module.exports = DB;
135 |
--------------------------------------------------------------------------------
/tools/problems.txt:
--------------------------------------------------------------------------------
1 | 10 / 10 =
2 | 96 / 8 =
3 | 8 x 10 =
4 | 6 x 9 =
5 | 11 x 9 =
6 | 9 x 7 =
7 | 50 / 5 =
8 | 12 / 6 =
9 | 24 / 4 =
10 | 7 x 11 =
11 | 9 x 9 =
12 | 2 x 4 =
13 | 108 / 12 =
14 | 2 x 11 =
15 | 77 / 7 =
16 | 32 / 4 =
17 | 2 / 1 =
18 | 20 / 4 =
19 | 10 x 6 =
20 | 18 / 6 =
21 | 6 x 5 =
22 | 10 x 8 =
23 | 12 / 1 =
24 | 30 / 3 =
25 | 7 x 4 =
26 | 72 / 6 =
27 | 42 / 7 =
28 | 10 x 4 =
29 | 96 / 12 =
30 | 30 / 5 =
31 | 121 / 11 =
32 | 9 x 11 =
33 | 108 / 9 =
34 | 50 / 10 =
35 | 99 / 11 =
36 | 10 / 2 =
37 | 22 / 2 =
38 | 11 x 10 =
39 | 84 / 12 =
40 | 18 / 3 =
41 | 12 x 12 =
42 | 10 x 12 =
43 | 8 x 4 =
44 | 4 / 4 =
45 | 5 x 5 =
46 | 11 x 2 =
47 | 7 x 8 =
48 | 2 x 1 =
49 | 44 / 11 =
50 | 7 x 3 =
51 | 64 / 8 =
52 | 9 x 1 =
53 | 16 / 2 =
54 | 56 / 8 =
55 | 10 x 1 =
56 | 12 x 7 =
57 | 24 / 12 =
58 | 18 / 2 =
59 | 10 / 1 =
60 | 6 x 2 =
61 | 15 / 5 =
62 | 7 x 2 =
63 | 9 x 10 =
64 | 27 / 3 =
65 | 10 x 5 =
66 | 40 / 4 =
67 | 3 / 3 =
68 | 9 x 8 =
69 | 3 x 3 =
70 | 2 x 6 =
71 | 8 / 2 =
72 | 7 / 7 =
73 | 24 / 2 =
74 | 9 / 9 =
75 | 1 x 5 =
76 | 6 x 7 =
77 | 12 x 11 =
78 | 48 / 12 =
79 | 5 x 3 =
80 | 7 x 12 =
81 | 9 x 4 =
82 | 33 / 11 =
83 | 1 x 4 =
84 | 11 x 6 =
85 | 12 x 4 =
86 | 4 / 2 =
87 | 35 / 5 =
88 | 3 x 11 =
89 | 6 x 12 =
90 | 8 x 1 =
91 | 8 / 4 =
92 | 60 / 12 =
93 | 12 x 2 =
94 | 88 / 11 =
95 | 60 / 6 =
96 | 32 / 8 =
97 | 5 x 6 =
98 | 72 / 12 =
99 | 2 x 10 =
100 | 45 / 5 =
101 | 11 x 3 =
102 | 12 x 1 =
103 | 60 / 10 =
104 | 1 / 1 =
105 | 36 / 6 =
106 | 21 / 7 =
107 | 8 x 7 =
108 | 12 / 2 =
109 | 35 / 7 =
110 | 4 / 1 =
111 | 7 x 9 =
112 | 48 / 6 =
113 | 11 / 1 =
114 | 2 x 8 =
115 | 8 x 9 =
116 | 16 / 8 =
117 | 144 / 12 =
118 | 18 / 9 =
119 | 8 x 11 =
120 | 11 x 5 =
121 | 3 x 10 =
122 | 1 x 9 =
123 | 10 x 3 =
124 | 4 x 7 =
125 | 1 x 2 =
126 | 24 / 8 =
127 | 14 / 7 =
128 | 2 / 2 =
129 | 100 / 10 =
130 | 63 / 9 =
131 | 56 / 7 =
132 | 9 x 3 =
133 | 5 x 9 =
134 | 4 x 10 =
135 | 49 / 7 =
136 | 20 / 5 =
137 | 7 x 5 =
138 | 10 x 11 =
139 | 9 x 2 =
140 | 9 / 3 =
141 | 12 x 10 =
142 | 4 x 9 =
143 | 5 x 7 =
144 | 33 / 3 =
145 | 110 / 10 =
146 | 6 / 3 =
147 | 9 x 6 =
148 | 4 x 12 =
149 | 40 / 10 =
150 | 3 x 2 =
151 | 6 x 8 =
152 | 77 / 11 =
153 | 8 x 12 =
154 | 11 x 11 =
155 | 8 / 8 =
156 | 6 x 10 =
157 | 4 x 6 =
158 | 72 / 9 =
159 | 24 / 6 =
160 | 120 / 10 =
161 | 40 / 5 =
162 | 3 x 4 =
163 | 11 / 11 =
164 | 8 x 2 =
165 | 11 x 7 =
166 | 30 / 6 =
167 | 6 x 4 =
168 | 54 / 9 =
169 | 66 / 6 =
170 | 12 x 9 =
171 | 9 / 1 =
172 | 8 x 8 =
173 | 5 / 5 =
174 | 4 x 3 =
175 | 9 x 5 =
176 | 4 x 5 =
177 | 8 x 3 =
178 | 6 x 1 =
179 | 7 x 10 =
180 | 12 / 4 =
181 | 6 x 3 =
182 | 6 / 1 =
183 | 3 x 9 =
184 | 5 / 1 =
185 | 36 / 4 =
186 | 5 x 2 =
187 | 44 / 4 =
188 | 4 x 1 =
189 | 11 x 4 =
190 | 20 / 2 =
191 | 99 / 9 =
192 | 11 x 1 =
193 | 48 / 4 =
194 | 3 x 1 =
195 | 10 x 7 =
196 | 3 x 8 =
197 | 2 x 7 =
198 | 14 / 2 =
199 | 12 x 3 =
200 | 5 x 11 =
201 | 4 x 11 =
202 | 70 / 10 =
203 | 132 / 12 =
204 | 72 / 8 =
205 | 120 / 12 =
206 | 1 x 10 =
207 | 45 / 9 =
208 | 1 x 12 =
209 | 5 x 12 =
210 | 10 x 2 =
211 | 42 / 6 =
212 | 54 / 6 =
213 | 36 / 12 =
214 | 5 x 1 =
215 | 1 x 6 =
216 | 25 / 5 =
217 | 12 x 5 =
218 | 11 x 12 =
219 | 8 x 6 =
220 | 132 / 11 =
221 | 2 x 2 =
222 | 4 x 4 =
223 | 81 / 9 =
224 | 7 / 1 =
225 | 70 / 7 =
226 | 1 x 8 =
227 | 28 / 7 =
228 | 4 x 8 =
229 | 7 x 7 =
230 | 90 / 10 =
231 | 55 / 5 =
232 | 28 / 4 =
233 | 2 x 12 =
234 | 21 / 3 =
235 | 8 / 1 =
236 | 5 x 4 =
237 | 7 x 6 =
238 | 36 / 3 =
239 | 12 / 12 =
240 | 4 x 2 =
241 | 6 / 6 =
242 | 7 x 1 =
243 | 110 / 11 =
244 | 20 / 10 =
245 | 10 x 10 =
246 | 88 / 8 =
247 | 10 x 9 =
248 | 27 / 9 =
249 | 1 x 11 =
250 | 10 / 5 =
251 | 9 x 12 =
252 | 3 x 12 =
253 | 3 x 5 =
254 | 24 / 3 =
255 | 48 / 8 =
256 | 12 x 6 =
257 | 90 / 9 =
258 | 15 / 3 =
259 | 36 / 9 =
260 | 80 / 10 =
261 | 80 / 8 =
262 | 5 x 10 =
263 | 6 x 11 =
264 | 2 x 5 =
265 | 55 / 11 =
266 | 3 x 7 =
267 | 1 x 1 =
268 | 5 x 8 =
269 | 1 x 7 =
270 | 3 x 6 =
271 | 1 x 3 =
272 | 60 / 5 =
273 | 30 / 10 =
274 | 8 x 5 =
275 | 66 / 11 =
276 | 12 x 8 =
277 | 84 / 7 =
278 | 6 x 6 =
279 | 2 x 3 =
280 | 6 / 2 =
281 | 12 / 3 =
282 | 3 / 1 =
283 | 22 / 11 =
284 | 11 x 8 =
285 | 2 x 9 =
286 | 40 / 8 =
287 | 63 / 7 =
288 | 16 / 4 =
--------------------------------------------------------------------------------
/test/learn/math/drill/finished.test.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const renderer = require('react-test-renderer');
3 | const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default;
4 | const getMuiTheme = require('material-ui/styles/getMuiTheme').default;
5 | const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default;
6 | const Finished = require('../../../../src/learn/math/drill/finished');
7 | const constants = require('../../../../src/learn/common/constants');
8 | const db = require('../../../../src/learn/db');
9 |
10 | const {
11 | RECORD_NEW,
12 | // RECORD_EQUAL,
13 | // RECORD_MISS,
14 | // RECORD_NOT_EXIST,
15 | } = constants;
16 | const muiTheme = getMuiTheme(lightBaseTheme);
17 |
18 | describe('Finished', () => {
19 | beforeEach(() => {
20 | db.getScores = jest.fn();
21 | });
22 |
23 | afterEach(() => {
24 | db.getScores.mockClear();
25 | });
26 |
27 | test('should render when there is a single incorrect result', () => {
28 | const previousResults = [{
29 | actuals: [5],
30 | id: 1,
31 | task: [5, 5, 0, 10],
32 | timeTaken: 2.2,
33 | }];
34 | const resultInfo = {
35 | text: 'Result Info Text',
36 | newRecordInfo: RECORD_NEW,
37 | };
38 |
39 | const component = renderer.create(
40 |
41 |
50 | );
51 |
52 | const finishedBadge = component.toJSON();
53 | expect(finishedBadge).toMatchSnapshot();
54 | });
55 |
56 | test('should render when there is a single correct result', () => {
57 | const previousResults = [{
58 | actuals: [10],
59 | id: 1,
60 | task: [5, 5, 0, 10],
61 | timeTaken: 2.2,
62 | }];
63 | const resultInfo = {
64 | text: 'Result Info Text',
65 | newRecordInfo: RECORD_NEW,
66 | };
67 |
68 | const component = renderer.create(
69 |
70 |
79 | );
80 |
81 | const finishedBadge = component.toJSON();
82 | expect(finishedBadge).toMatchSnapshot();
83 | });
84 |
85 | test('should render when there is a mix of correct and incorrect results', () => {
86 | const previousResults = [{
87 | actuals: [5], // single incorrect
88 | id: 1,
89 | task: [5, 5, 0, 10],
90 | timeTaken: 2.2,
91 | }, {
92 | actuals: [10], // single correct
93 | id: 2,
94 | task: [5, 5, 0, 10],
95 | timeTaken: 2.2,
96 | }, {
97 | actuals: [10, 5, 4, 3], // correct with some wrong
98 | id: 3,
99 | task: [5, 5, 0, 10],
100 | timeTaken: 2.2,
101 | }];
102 | const resultInfo = {
103 | text: 'Result Info Text',
104 | newRecordInfo: RECORD_NEW,
105 | };
106 | db.getScores.mockReturnValueOnce([{
107 | key: '0-01',
108 | timePerQuestion: 2,
109 | }, {
110 | key: '0-02',
111 | timePerQuestion: 0.5,
112 | }, {
113 | key: '0-01',
114 | timePerQuestion: 1,
115 | }]);
116 |
117 | const component = renderer.create(
118 |
119 |
128 | );
129 |
130 | const finishedBadge = component.toJSON();
131 | expect(finishedBadge).toMatchSnapshot();
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/src/learn/help/index.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const ReactMarkdown = require('react-markdown');
3 |
4 | const input = `
5 | ### Help with Math Drill
6 |
7 | #### Options
8 |
9 | Configure the options for the test. These options get saved in your browser \
10 | so if you visit this page again with the same browser then the settings \
11 | will be as you last left them.
12 |
13 | The scores and badges are also saved in the browser. There is no server or \
14 | database for this app. Everything happens on your device and in the browser \
15 | and that's where all the data lives as well. If you use a different browser \
16 | you will start from fresh. If you share your browser with somebody they'll have \
17 | your settings and scores.
18 |
19 | ##### Level
20 |
21 | There are 26 levels from A to Z. If you select a different level the previous \
22 | one will be deselected. All levels are cumulative meaning that, for example \
23 | on level E you will also get questions from levels A through D. The questions \
24 | are randomly weighted towards the higher level you select. That means that \
25 | if you select level E there will probably be more questions from this level \
26 | than level D and there will be more from level D than from C etc.
27 |
28 | ##### Operation
29 |
30 | The four operators Addition, Subtraction, Multiplication and Division can be \
31 | selected in any combination. At least one of them needs to be selected and all \
32 | four can be selected as well.
33 |
34 | Because all Operations can be selected and because the Levels are cumulative you \
35 | can get all four operations with all levels by selecting all the Operators and \
36 | Level Z.
37 |
38 | ##### Time
39 |
40 | The time is expressed in minutes and will stop the test after this time period \
41 | has elapsed. Fractions of a minute can be used to refine the time. Time is not \
42 | really important if you are collecting badges (see below) because average time \
43 | becomes the deciding factor so setting this to a high value is okay.
44 |
45 | ##### Total Questions
46 |
47 | This is the total number of questions that will be asked. The test will end \
48 | once this number of questions have been correctly answered. If a question is not \
49 | correctly answered then the test will remain on that question until it is \
50 | complete.
51 |
52 | If you want to collect badges for completed tests you must answer at least ten \
53 | questions in each test.
54 |
55 | ##### Keyboard
56 |
57 | If you're doing this on a touch device like a smart phone or tablet the the \
58 | onscreen keyboard is the easiest to use. It's a simple keyboard that has all
59 | the functionality you need.
60 |
61 | If you doing this from a desktop computer or laptop then switching this off \
62 | is probably going to be easier.
63 |
64 | Try them both and see which one works best for you.
65 |
66 | ##### Large Keyboard
67 |
68 | This will only show up if you have selected "onscreen keyboard." Selecting \
69 | this option will increase the size of the onscreen keyboard. It seems to \
70 | make it easier for kids to use a larger keyboard sometimes.
71 |
72 | ##### Your Name
73 |
74 | This is entirely optional.
75 |
76 | ### Running
77 |
78 | Once you start running the test you will see the Level, Operation(s) \
79 | Time Left and Questions Remaining at the top of the screen.
80 |
81 | The current question will be presented right below that with a ? in a green \
82 | box where the answer will go.
83 |
84 | Answering a question correctly will advance to the next question until they \
85 | are all completed or until time runs out.
86 |
87 | After you have started answering questions two more pieces of information \
88 | will be presented under the question. The result of the previous question \
89 | you answered and your current speed versus the record (if there is one).
90 |
91 | ### Finished
92 |
93 | Once you have completed answering all your questions you'll see the results \
94 | on the Finished page.
95 |
96 | If you have earned a new badge you will see that here as well.
97 |
98 | There is also a summary of the question you answered quickest, slowest and \
99 | all your results if you want to review them.
100 |
101 | # Scoreboard
102 |
103 | The scoreboard is fairly obvious.
104 |
105 | There is a summary at the top showing all the badges you have earned.
106 |
107 | Below that is a table showing badges by level and operation.
108 |
109 | For now badges are only awarded for single-operation tests.
110 | `;
111 |
112 | function help() {
113 | return (
114 |
115 | );
116 | }
117 |
118 | module.exports = help;
119 |
--------------------------------------------------------------------------------
/test/learn/help/__snapshots__/index.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Help should render the Help component 1`] = `
4 | Array [
5 |
6 | Help with Math Drill
7 | ,
8 |
9 | Options
10 | ,
11 |
12 | Configure the options for the test. These options get saved in your browser so if you visit this page again with the same browser then the settings will be as you last left them.
13 |
,
14 |
15 | The scores and badges are also saved in the browser. There is no server or database for this app. Everything happens on your device and in the browser and that's where all the data lives as well. If you use a different browser you will start from fresh. If you share your browser with somebody they'll have your settings and scores.
16 |
,
17 |
18 | Level
19 | ,
20 |
21 | There are 26 levels from A to Z. If you select a different level the previous one will be deselected. All levels are cumulative meaning that, for example on level E you will also get questions from levels A through D. The questions are randomly weighted towards the higher level you select. That means that if you select level E there will probably be more questions from this level than level D and there will be more from level D than from C etc.
22 |
,
23 |
24 | Operation
25 | ,
26 |
27 | The four operators Addition, Subtraction, Multiplication and Division can be selected in any combination. At least one of them needs to be selected and all four can be selected as well.
28 |
,
29 |
30 | Because all Operations can be selected and because the Levels are cumulative you can get all four operations with all levels by selecting all the Operators and Level Z.
31 |
,
32 |
33 | Time
34 | ,
35 |
36 | The time is expressed in minutes and will stop the test after this time period has elapsed. Fractions of a minute can be used to refine the time. Time is not really important if you are collecting badges (see below) because average time becomes the deciding factor so setting this to a high value is okay.
37 |
,
38 |
39 | Total Questions
40 | ,
41 |
42 | This is the total number of questions that will be asked. The test will end once this number of questions have been correctly answered. If a question is not correctly answered then the test will remain on that question until it is complete.
43 |
,
44 |
45 | If you want to collect badges for completed tests you must answer at least ten questions in each test.
46 |
,
47 |
48 | Keyboard
49 | ,
50 |
51 | If you're doing this on a touch device like a smart phone or tablet the the onscreen keyboard is the easiest to use. It's a simple keyboard that has all
52 | the functionality you need.
53 |
,
54 |
55 | If you doing this from a desktop computer or laptop then switching this off is probably going to be easier.
56 |
,
57 |
58 | Try them both and see which one works best for you.
59 |
,
60 |
61 | Large Keyboard
62 | ,
63 |
64 | This will only show up if you have selected "onscreen keyboard." Selecting this option will increase the size of the onscreen keyboard. It seems to make it easier for kids to use a larger keyboard sometimes.
65 |
,
66 |
67 | Your Name
68 | ,
69 |
70 | This is entirely optional.
71 |
,
72 |
73 | Running
74 | ,
75 |
76 | Once you start running the test you will see the Level, Operation(s) Time Left and Questions Remaining at the top of the screen.
77 |
,
78 |
79 | The current question will be presented right below that with a ? in a green box where the answer will go.
80 |
,
81 |
82 | Answering a question correctly will advance to the next question until they are all completed or until time runs out.
83 |
,
84 |
85 | After you have started answering questions two more pieces of information will be presented under the question. The result of the previous question you answered and your current speed versus the record (if there is one).
86 |
,
87 |
88 | Finished
89 | ,
90 |
91 | Once you have completed answering all your questions you'll see the results on the Finished page.
92 |
,
93 |
94 | If you have earned a new badge you will see that here as well.
95 |
,
96 |
97 | There is also a summary of the question you answered quickest, slowest and all your results if you want to review them.
98 |
,
99 |
100 | Scoreboard
101 | ,
102 |
103 | The scoreboard is fairly obvious.
104 |
,
105 |
106 | There is a summary at the top showing all the badges you have earned.
107 |
,
108 |
109 | Below that is a table showing badges by level and operation.
110 |
,
111 |
112 | For now badges are only awarded for single-operation tests.
113 |
,
114 | ]
115 | `;
116 |
--------------------------------------------------------------------------------
/src/learn/math/drill/score-bar.jsx:
--------------------------------------------------------------------------------
1 | const PropTypes = require('prop-types');
2 | const React = require('react');
3 | const moment = require('moment');
4 | const constants = require('../../common/constants');
5 | const helper = require('./helper');
6 |
7 | const {
8 | BADGE_BOUNDARIES: badgeBoundaries,
9 | COLOR_HTML: htmlColors,
10 | COLOR_TEXT: colorText,
11 | } = constants;
12 |
13 | const sbStyleBase = {
14 |
15 | };
16 |
17 | const sbHeadStyleBase = {
18 | width: '100%',
19 | border: '1px solid black',
20 | display: 'flex',
21 | textAlign: 'left',
22 | };
23 |
24 | const sbHeadItemStyleBase = {
25 | flexGrow: '1',
26 | };
27 |
28 | const sbBodyStyleBase = {
29 | width: '100%',
30 | border: '1px solid black',
31 | display: 'flex',
32 | };
33 |
34 | const sbBodyItemStyleBase = {
35 | border: '1px solid black',
36 | display: 'flex',
37 | flexDirection: 'column',
38 | fontSize: 'large',
39 | height: '100%',
40 | flexGrow: '1',
41 | textAlign: 'center',
42 | };
43 |
44 | function renderColors(times, maxVal) {
45 | const sbBodyItemStyle = { ...sbBodyItemStyleBase, height: `${maxVal * 50}px` };
46 |
47 | const extra = badgeBoundaries.concat(maxVal);
48 | const widths = extra.reduce((acc, boundary, index) => {
49 | if (index !== badgeBoundaries.length) {
50 | acc.push(Math.round((100 * (extra[index + 1] - boundary)) / maxVal));
51 | }
52 | return acc;
53 | }, []);
54 |
55 | return (
56 |
57 | {
58 | colorText.map((color, index) => {
59 | const style = {
60 | backgroundColor: htmlColors[index],
61 | height: `${widths[index]}%`,
62 | };
63 | return (
64 |
68 | {color}
69 |
70 | );
71 | })
72 | }
73 |
74 | );
75 | }
76 |
77 | function renderUserScores(times, maxVal) {
78 | return times.map((time) => {
79 | const { timePerQuestion, date } = time;
80 | const sbBodyItemStyle = { ...sbBodyItemStyleBase, height: `${maxVal * 50}px` };
81 | const timePerQuestionLimit = Math.min(maxVal, timePerQuestion);
82 |
83 | const one = Math.min(95, ((timePerQuestionLimit * 100) / maxVal) - 5).toFixed(1);
84 | const three = Math.max(0, (((maxVal - timePerQuestionLimit) * 100) / maxVal) - 5).toFixed(1);
85 | const userTimes = [
86 | one,
87 | 10,
88 | three,
89 | ];
90 |
91 | const rightArrow = '\u21FE';
92 | const downArrow = '\u2193';
93 | const arrow = timePerQuestion > timePerQuestionLimit
94 | ? downArrow : rightArrow;
95 |
96 | const userTimeText = ['', `YOU${arrow}`, ''];
97 |
98 | const userColor = helper.getBadgeHtmlColor(timePerQuestion);
99 |
100 | return (
101 |
102 | {
103 | userTimes.map((userTime, index) => {
104 | const key = `userTimes-${index}`;
105 | const style = {
106 | height: `${userTime}%`,
107 | };
108 | if (index === 1) {
109 | style.backgroundColor = userColor;
110 | }
111 | return (
112 |
113 | {userTimeText[index]}
114 |
115 | );
116 | })
117 | }
118 |
119 | );
120 | });
121 | }
122 |
123 | function renderTitles(times) {
124 | const titles = times.map(({ date }) => ({
125 | title: moment(date).fromNow(),
126 | date,
127 | })).concat({ title: 'Badges', date: 0 });
128 |
129 | return (
130 |
131 | {
132 | titles.map(({ title, date }) => (
133 |
134 | {title}
135 |
136 | ))
137 | }
138 |
139 | );
140 | }
141 |
142 | function scoreBar({ times, showScoreBar }) {
143 | if (!showScoreBar || !times.length) {
144 | return null;
145 | }
146 |
147 | const boundaryMax = Math.max(...badgeBoundaries);
148 | const maxTimePerQuestion = Math.max(...times.map((time) => time.timePerQuestion));
149 | const maxVal = maxTimePerQuestion > (boundaryMax * 1.5)
150 | ? boundaryMax * 2
151 | : boundaryMax * 1.5;
152 |
153 | const sbBodyStyle = { ...sbBodyStyleBase, height: `${maxVal * 50}px` };
154 |
155 | return (
156 |
157 |
158 | {renderTitles(times)}
159 |
160 |
161 | {renderUserScores(times, maxVal)}
162 | {renderColors(times, maxVal)}
163 |
164 |
165 | );
166 | }
167 |
168 | scoreBar.propTypes = {
169 | times: PropTypes.arrayOf(PropTypes.shape({
170 | date: PropTypes.number.isRequired,
171 | timePerQuestion: PropTypes.number.isRequired,
172 | }).isRequired).isRequired,
173 | showScoreBar: PropTypes.bool.isRequired,
174 | };
175 |
176 | module.exports = scoreBar;
177 |
--------------------------------------------------------------------------------
/src/learn/math/drill/finished.jsx:
--------------------------------------------------------------------------------
1 | const PropTypes = require('prop-types');
2 | const React = require('react');
3 | const RunningResults = require('./running-results');
4 | const constants = require('../../common/constants');
5 | const FinishedBadge = require('./finished-badge');
6 | const ScoreBar = require('./score-bar');
7 | const PreviousResults = require('./previous-results');
8 | const Types = require('./types');
9 | const helper = require('./helper');
10 |
11 | const {
12 | RECORD_NEW,
13 | RECORD_EQUAL,
14 | RECORD_MISS,
15 | RECORD_NOT_EXIST,
16 | } = constants;
17 |
18 | const baseRecordStyle = {
19 | color: 'black',
20 | backgroundColor: '#8bc78b',
21 | padding: '20px',
22 | borderRadius: '5px',
23 | marginBottom: '10px',
24 | };
25 |
26 | const newRecordStyle = baseRecordStyle;
27 |
28 | const missRecordStyle = { ...baseRecordStyle, backgroundColor: '#ecc9e0' };
29 |
30 | const notExistRecordStyle = {
31 | ...baseRecordStyle,
32 | color: 'white',
33 | backgroundColor: '#8583dc',
34 | };
35 |
36 | function processResultInfo(resultInfo) {
37 | const { newRecordInfo, text } = resultInfo;
38 | let style;
39 | switch (newRecordInfo) {
40 | case RECORD_NEW:
41 | case RECORD_EQUAL:
42 | style = newRecordStyle;
43 | break;
44 | case RECORD_MISS:
45 | style = missRecordStyle;
46 | break;
47 | case RECORD_NOT_EXIST:
48 | style = notExistRecordStyle;
49 | break;
50 | default:
51 | return (
52 |
53 | {`Unknown newRecordInfo ${newRecordInfo} in switch case`}
54 |
55 | );
56 | }
57 | return (
58 |
59 | {text}
60 |
61 | );
62 | }
63 |
64 | function finished(props) {
65 | const { levelIndex: propsLevelIndex, opIndexes: propsOpIndexes } = props;
66 | const {
67 | levelIndex,
68 | opIndexes,
69 | previousResults,
70 | resultInfo,
71 | scoreBarTimes = helper.getScoreBarTimes(propsLevelIndex, propsOpIndexes),
72 | timeAllowed,
73 | timeLeft,
74 | totalProblems,
75 | } = props;
76 |
77 | const crunchedResults = PreviousResults.getStats(previousResults);
78 |
79 | const {
80 | incorrects,
81 | correctCount,
82 | longestTime: long,
83 | shortestTime: short,
84 | totalTime,
85 | } = crunchedResults;
86 |
87 | const longestTime = long ? [long] : [];
88 | const shortestTime = short ? [short] : [];
89 | const timePerQuestion = parseFloat((totalTime / correctCount).toFixed(1));
90 |
91 | const slowSort = PreviousResults.sortBySlowest(previousResults);
92 |
93 | if (!longestTime.length) {
94 | return (
95 |
96 |
97 | Finished
98 |
99 |
100 | {'Looks like you didn\'t get a chance to do anything that round!'}
101 |
102 |
103 | );
104 | }
105 |
106 | return (
107 |
108 |
109 | Finished
110 |
111 | {processResultInfo(resultInfo)}
112 |
118 |
9}
121 | />
122 |
123 | {`You had ${timeLeft} seconds left out of the ${timeAllowed} seconds allowed.`}
124 |
125 |
126 | {`You correctly answered ${correctCount} of the ${totalProblems} problems.`}
127 |
128 |
129 | The problem that took you the longest to answer
130 |
131 |
132 |
133 | The problem you answered the fastest
134 |
135 |
136 | {!!incorrects.length
137 | && (
138 |
139 |
140 | The ones that you got wrong
141 |
142 |
143 |
144 | )}
145 |
146 | How you did (sorted by slowest)
147 |
148 |
151 |
152 | );
153 | }
154 |
155 | finished.propTypes = {
156 | levelIndex: PropTypes.number.isRequired,
157 | opIndexes: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired,
158 | previousResults: PropTypes.arrayOf(Types.previousResults).isRequired,
159 | resultInfo: PropTypes.shape({
160 | text: PropTypes.string.isRequired,
161 | newRecordInfo: PropTypes.string.isRequired,
162 | }).isRequired,
163 | scoreBarTimes: PropTypes.arrayOf(PropTypes.shape({
164 | date: PropTypes.number.isRequired,
165 | timePerQuestion: PropTypes.number.isRequired,
166 | })),
167 | timeAllowed: PropTypes.number.isRequired,
168 | timeLeft: PropTypes.number.isRequired,
169 | totalProblems: PropTypes.number.isRequired,
170 | };
171 |
172 | finished.defaultProps = {
173 | scoreBarTimes: undefined,
174 | };
175 |
176 | module.exports = finished;
177 |
--------------------------------------------------------------------------------
/test/learn/__snapshots__/home.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Home should render a Home component 1`] = `
4 |
5 |
61 |
94 |
133 |
172 |
173 | `;
174 |
--------------------------------------------------------------------------------
/src/learn/math/drill/options.jsx:
--------------------------------------------------------------------------------
1 | const FloatingActionButton = require('@material-ui/core/Fab').default;
2 | const PropTypes = require('prop-types');
3 | const Button = require('@material-ui/core/Button').default;
4 | const React = require('react');
5 | const TextField = require('@material-ui/core/TextField').default;
6 | const Switch = require('@material-ui/core/Switch').default;
7 | const FormGroup = require('@material-ui/core/FormGroup').default;
8 | const FormControlLabel = require('@material-ui/core/FormControlLabel').default;
9 | const { Send } = require('@material-ui/icons');
10 | const Tooltip = require('@material-ui/core/Tooltip').default;
11 | const constants = require('../../common/constants');
12 |
13 | const {
14 | ALPHABET: alphabet,
15 | OPERATIONS: operations,
16 | } = constants;
17 |
18 | const buttonStyle = {
19 | margin: '5px',
20 | fontSize: '2em',
21 | };
22 |
23 | const sectionStyle = {
24 | marginTop: '30px',
25 | };
26 |
27 | function options(props) {
28 | const {
29 | largeKeyboard,
30 | levelIndex,
31 | minutes,
32 | onChange,
33 | onscreenKeyboard,
34 | onStart,
35 | opIndexes,
36 | setParentState,
37 | totalProblems,
38 | userName,
39 | } = props;
40 |
41 | function toggleOpIndex(opIndex) {
42 | const position = props.opIndexes.indexOf(opIndex);
43 | let operatIndexes;
44 | if (position >= 0) {
45 | operatIndexes = props.opIndexes;
46 | if (props.opIndexes.length > 1) {
47 | // Don't allow count to fall under 1
48 | operatIndexes.splice(position, 1);
49 | }
50 | } else {
51 | operatIndexes = props.opIndexes.concat(opIndex);
52 | }
53 |
54 | props.setParentState({ opIndexes: operatIndexes });
55 | }
56 |
57 | return (
58 |
59 |
60 |
61 | Level
62 |
63 |
64 | {
65 | alphabet.map((letter, index) => (
66 |
67 | setParentState({ levelIndex: index })}
70 | color={index === levelIndex ? 'secondary' : 'primary'}
71 | style={buttonStyle}
72 | >
73 | {letter}
74 |
75 |
76 | ))
77 | }
78 |
79 |
80 |
81 |
82 | Operation
83 |
84 |
85 | {
86 | operations.map((operation, index) => (
87 |
88 | toggleOpIndex(index)}
91 | color={opIndexes.includes(index) ? 'secondary' : 'primary'}
92 | style={buttonStyle}
93 | >
94 | {operation}
95 |
96 |
97 | ))
98 | }
99 |
100 |
101 |
102 |
103 | Time
104 |
105 |
115 |
116 |
117 |
118 | Total Questions (you only get badges for 10 or more correct questions)
119 |
120 |
130 |
131 |
132 |
133 | Keyboard
134 |
135 |
136 |
144 | )}
145 | label="Use onscreen keyboard"
146 | />
147 |
148 |
149 | {onscreenKeyboard
150 | && (
151 |
152 |
153 | Large Keyboard
154 |
155 |
156 |
164 | )}
165 | label="Large Keyboard"
166 | />
167 |
168 |
169 | )}
170 |
171 |
172 | Your Name (Optional)
173 |
174 |
184 |
185 |
186 |
187 | Start
188 |
189 |
190 |
191 |
192 |
193 | );
194 | }
195 |
196 |
197 | options.propTypes = {
198 | largeKeyboard: PropTypes.bool.isRequired,
199 | levelIndex: PropTypes.number.isRequired,
200 | minutes: PropTypes.string.isRequired,
201 | onChange: PropTypes.func.isRequired,
202 | onscreenKeyboard: PropTypes.bool.isRequired,
203 | onStart: PropTypes.func.isRequired,
204 | opIndexes: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired,
205 | setParentState: PropTypes.func.isRequired,
206 | totalProblems: PropTypes.string.isRequired,
207 | userName: PropTypes.string.isRequired,
208 | };
209 |
210 | module.exports = options;
211 |
--------------------------------------------------------------------------------
/src/learn/math/drill/helper-problems.js:
--------------------------------------------------------------------------------
1 | // Define the problems for math drill
2 |
3 | const special1 = [...Array(9).keys()].map((a) => [0, a + 1]);
4 | const special2 = [...Array(9).keys()].map((a) => [a + 1, 0]);
5 | const special3 = [...Array(10).keys()].map((a) => [a + 1, 0]);
6 | const special4 = [...Array(18).keys()].map((a) => [a + 1, a + 1]);
7 | const special5 = [...Array(9).keys()].map((a) => [1, a + 1]);
8 | const special6 = [...Array(9).keys()].map((a) => [a + 1, 1]); // [1...9, 1]
9 | const special7 = [...Array(9).keys()].map((a) => [a + 1, a + 1]); // [[1,1],...[9,9]]
10 |
11 | const special8 = [...Array(8).keys()]
12 | .map((a) => a + 2) // [2...9]
13 | .reduce((acc, denominator) => {
14 | let numerator = denominator - 1;
15 | while (numerator > 0) {
16 | acc.push([numerator, denominator]);
17 | numerator -= 1;
18 | }
19 | return acc;
20 | }, []); // [[1,2],[1,3],[2,3]...]
21 |
22 | // Extra for multiplication
23 | const special9 = [...Array(11).keys()].map((a) => [10, a]);
24 | const special10 = [...Array(11).keys()].map((a) => [a, 10]);
25 | const special11 = [...Array(12).keys()].map((a) => [11, a]);
26 | const special12 = [...Array(12).keys()].map((a) => [a, 11]);
27 | const special13 = [...Array(13).keys()].map((a) => [12, a]);
28 | const special14 = [...Array(13).keys()].map((a) => [a, 12]);
29 |
30 | // Extra for division
31 | const special15 = [...Array(12).keys()].map((a) => [(a + 1) * 10, 10]);
32 | const special16 = [...Array(12).keys()].map((a) => [(a + 1) * 11, 11]);
33 | const special17 = [...Array(12).keys()].map((a) => [(a + 1) * 12, 12]);
34 |
35 | const levelOps = [
36 | // Addition +
37 | [
38 | [[1, 2], [2, 1], [1, 3], [3, 1]], // A
39 | [[1, 4], [4, 1], [1, 1]], // B
40 | [[1, 5], [5, 1], [2, 2]], // C
41 | [[1, 6], [6, 1], [3, 3]], // D
42 | [[1, 7], [7, 1], [4, 4]], // E
43 | [[1, 8], [8, 1], [5, 5]], // F
44 | // #1 [0, 1..9]
45 | // #2 [1..9, 0]
46 | [[1, 9], [9, 1], ...special1, ...special2], // G
47 | [[2, 3], [3, 2], [6, 6]], // H
48 | [[4, 2], [2, 4], [7, 7]], // I
49 | [[5, 2], [2, 5], [8, 8]], // J
50 | [[6, 2], [2, 6], [9, 9]], // K
51 | [[7, 2], [2, 7], [4, 7], [7, 4]], // L
52 | [[8, 2], [2, 8], [8, 6], [6, 8]], // M
53 | [[9, 2], [2, 9], [9, 6], [6, 9]], // N
54 | [[4, 3], [3, 4], [6, 7], [7, 6]], // O
55 | [[5, 3], [3, 5], [7, 8], [8, 7]], // P
56 | [[5, 8], [8, 5], [7, 9], [9, 7]], // Q
57 | [[6, 3], [3, 6], [5, 9], [9, 5]], // R
58 | [[7, 3], [3, 7], [8, 9], [9, 8]], // S
59 | [[8, 3], [3, 8], [4, 9], [9, 4]], // T
60 | [[9, 3], [3, 9], [5, 7], [7, 5]], // U
61 | [[4, 5], [5, 4], [4, 8], [8, 4]], // V
62 | [[4, 6], [6, 4], [5, 6], [6, 5]], // W
63 | [], // X
64 | [], // Y
65 | [], // Z
66 | ],
67 | // Subtraction -
68 | [
69 | [[3, 2], [3, 1], [4, 3], [4, 1]], // A
70 | [[5, 4], [5, 1], [2, 1]], // B
71 | [[6, 1], [6, 5], [4, 2]], // C
72 | [[7, 1], [7, 6], [6, 3]], // D
73 | [[8, 1], [8, 7], [8, 4]], // E
74 | [[9, 1], [9, 8], [10, 5]], // F
75 | [[10, 1], [10, 9], ...special3], // G # [1..10, 0]
76 | [[5, 3], [5, 2], [12, 6]], // H
77 | [[6, 2], [6, 4], [14, 7]], // I
78 | [[7, 2], [7, 5], [16, 8]], // J
79 | [[8, 2], [8, 6], [18, 9]], // K
80 | [[9, 2], [9, 7], [11, 7], [11, 4]], // L
81 | special4, // M #4 [1..18, 1..18]
82 | [[10, 2], [10, 8], [14, 8], [14, 6]], // N
83 | [[11, 2], [11, 9], [15, 9], [15, 6]], // O
84 | [[7, 3], [7, 4], [13, 7], [13, 6]], // P
85 | [[8, 3], [8, 5], [15, 8], [15, 7]], // Q
86 | [[13, 8], [13, 5], [16, 9], [16, 7]], // R
87 | [[9, 3], [9, 6], [14, 9], [14, 5]], // S
88 | [[10, 3], [10, 7], [17, 9], [17, 8]], // T
89 | [[11, 3], [11, 8], [13, 9], [13, 4]], // U
90 | [[12, 3], [12, 9], [12, 7], [12, 5]], // V
91 | [[9, 5], [9, 4], [12, 8], [12, 4]], // W
92 | [[10, 6], [10, 4], [11, 6], [11, 5]], // X
93 | [], // Y
94 | [], // Z
95 | ],
96 | // Multiplication x
97 | [
98 | // #5 [1, 1..9]
99 | // #6 [1..9, 1]
100 | [...special5, ...special6], // A
101 | // #1 [0, 1..9]
102 | // #2 [1..9, 0]
103 | [...special1, ...special2], // B
104 | [[2, 3], [3, 2], [2, 2]], // C
105 | [[2, 4], [4, 2], [2, 5], [5, 2]], // D
106 | [[6, 2], [2, 6], [7, 2], [2, 7]], // E
107 | [[8, 2], [2, 8], [9, 2], [2, 9]], // F
108 | [[9, 3], [3, 9], [9, 4], [4, 9]], // G
109 | [[9, 5], [5, 9], [3, 3]], // H
110 | [[9, 6], [6, 9], [4, 4]], // I
111 | [[9, 7], [7, 9], [5, 5]], // J
112 | [[9, 8], [8, 9], [6, 6]], // K
113 | [[3, 4], [4, 3], [7, 7]], // L
114 | [[3, 5], [5, 3], [8, 8]], // M
115 | [[3, 6], [6, 3], [9, 9]], // N
116 | [[3, 7], [7, 3], [3, 8], [8, 3]], // O
117 | [[7, 8], [8, 7], [6, 8], [8, 6]], // P
118 | [[5, 8], [8, 5], [4, 8], [8, 4]], // Q
119 | [[7, 6], [6, 7], [7, 5], [5, 7]], // R
120 | [[7, 4], [4, 7], [6, 5], [5, 6]], // S
121 | [[5, 4], [4, 5], [4, 6], [6, 4]], // T
122 | [...special9], // U #9 [10, 0..10]
123 | [...special10], // V #10 [0..10, 10]
124 | [...special11], // W #11 [11, 0..11]
125 | [...special12], // X #11 [0..11, 11]
126 | [...special13], // Y #12 [12, 0..12]
127 | [...special14], // Z #12 [0..12, 12]
128 | ],
129 | // Division /
130 | [
131 | special6, // A
132 | special7, // B
133 | [[6, 2], [6, 3], [4, 2]], // C
134 | [[8, 2], [8, 4], [10, 2], [10, 5]], // D
135 | [[12, 2], [12, 6], [14, 2], [14, 7]], // E
136 | [[16, 2], [16, 8], [18, 2], [18, 9]], // F
137 | special8, // G
138 | [[27, 9], [27, 3], [36, 9], [36, 4]], // H
139 | [[45, 9], [45, 5], [9, 3]], // I
140 | [[54, 9], [54, 6], [16, 4]], // J
141 | [[63, 9], [63, 7], [25, 5]], // K
142 | [[72, 9], [72, 8], [36, 6]], // L
143 | [[12, 3], [12, 4], [49, 7]], // M
144 | [[15, 3], [15, 5], [64, 8]], // N
145 | [[18, 3], [18, 6], [81, 9]], // O
146 | [[21, 3], [21, 7], [24, 3], [24, 8]], // P
147 | [[56, 8], [56, 7], [48, 8], [48, 6]], // Q
148 | [[40, 8], [40, 5], [32, 8], [32, 4]], // R
149 | [[42, 7], [42, 6], [35, 7], [35, 5]], // S
150 | [[28, 7], [28, 4], [30, 6], [30, 5]], // T
151 | [[20, 4], [20, 5], [24, 6], [24, 4]], // U
152 | [[90, 9], [99, 9], [108, 9]], // V
153 | [[64, 8], [72, 8], [80, 8], [88, 8], [96, 8]], // W
154 | [...special15], // X #15 [10, 20, 30 ... 10]
155 | [...special16], // Y #16 [11, 22, 33 ... 11]
156 | [...special17], // Z #17 [12, 24, 36 ... 12]
157 | ],
158 |
159 | ];
160 |
161 | module.exports = {
162 | levelOps,
163 | };
164 |
--------------------------------------------------------------------------------
/test/learn/db/index.test.js:
--------------------------------------------------------------------------------
1 | const db = require('../../../src/learn/db');
2 | const constants = require('../../../src/learn/common/constants');
3 |
4 | const {
5 | MATH_DRILL_OPTIONS,
6 | MATH_DRILL_SCORES,
7 | } = constants;
8 |
9 | describe('DB', () => {
10 | // beforeAll(() => {
11 | // });
12 | let getItemMock;
13 | let setItemMock;
14 | beforeEach(() => {
15 | // eslint-disable-next-line no-proto
16 | setItemMock = jest.spyOn(window.localStorage.__proto__, 'setItem');
17 | // eslint-disable-next-line no-proto
18 | getItemMock = jest.spyOn(window.localStorage.__proto__, 'getItem');
19 | });
20 | afterEach(() => {
21 | getItemMock.mockRestore();
22 | setItemMock.mockRestore();
23 | });
24 | test('should set an item', () => {
25 | const value = { prop: 'one' };
26 |
27 | // eslint-disable-next-line no-proto
28 | jest.spyOn(window.localStorage.__proto__, 'setItem');
29 |
30 | db.setItem('key', value);
31 | expect(localStorage.setItem).toHaveBeenCalledWith('key', JSON.stringify(value));
32 | });
33 |
34 | test('should get an item', () => {
35 | const value = { prop: 'one' };
36 |
37 | localStorage.getItem.mockReturnValueOnce(JSON.stringify(value));
38 |
39 | const actual = db.getItem('key');
40 | expect(actual).toEqual(value);
41 | });
42 |
43 | test('should get a falsy item', () => {
44 | localStorage.getItem.mockReturnValueOnce(null);
45 | const actual = db.getItem('key');
46 | expect(actual).toBeNull();
47 | });
48 |
49 | test('should save options', () => {
50 | const value = { prop: 'one' };
51 | db.saveOptions(value);
52 | expect(localStorage.setItem).toHaveBeenCalledWith(MATH_DRILL_OPTIONS, JSON.stringify(value));
53 | });
54 |
55 | test('should save scores', () => {
56 | const value = { prop: 'one' };
57 | db.saveScores(value);
58 | expect(localStorage.setItem).toHaveBeenCalledWith(MATH_DRILL_SCORES, JSON.stringify(value));
59 | });
60 |
61 | test('should append scores', () => {
62 | localStorage.getItem.mockReturnValueOnce(JSON.stringify({
63 | version: 3,
64 | scores: [1, 2],
65 | }));
66 | db.appendScore(3);
67 | expect(localStorage.setItem).toHaveBeenCalledWith(MATH_DRILL_SCORES, JSON.stringify({
68 | version: 3,
69 | scores: [1, 2, 3],
70 | }));
71 | });
72 |
73 | test('should append scores when score collection is empty', () => {
74 | localStorage.getItem.mockReturnValueOnce(null);
75 | db.appendScore(3);
76 | expect(localStorage.setItem).toHaveBeenCalledWith(MATH_DRILL_SCORES, JSON.stringify({
77 | version: 3,
78 | scores: [3],
79 | }));
80 | });
81 |
82 | test('should get default options when none have previously been saved', () => {
83 | localStorage.getItem.mockReturnValueOnce(null);
84 | const actual = db.getOptions();
85 | const defaultOptions = {
86 | largeKeyboard: false,
87 | levelIndex: 0, // A
88 | minutes: '10',
89 | onscreenKeyboard: false,
90 | opIndexes: [0], // +
91 | totalProblems: '20',
92 | userName: '',
93 | };
94 | expect(localStorage.setItem).toHaveBeenCalledWith(
95 | MATH_DRILL_OPTIONS, JSON.stringify(defaultOptions));
96 | expect(actual).toMatchObject(defaultOptions);
97 | });
98 |
99 | test('should get and convert options', () => {
100 | const savedOptions = {
101 | levelIndex: 0, // A
102 | minutes: '1',
103 | onscreenKeyboard: true,
104 | opIndex: 0, // +
105 | totalProblems: '20',
106 | };
107 | localStorage.getItem.mockReturnValueOnce(JSON.stringify(savedOptions));
108 |
109 | const actual = db.getOptions();
110 |
111 | expect(localStorage.setItem).toHaveBeenCalledTimes(3);
112 |
113 | savedOptions.userName = '';
114 | savedOptions.largeKeyboard = false;
115 | savedOptions.opIndexes = [0];
116 | delete savedOptions.opIndex;
117 | expect(localStorage.setItem).toHaveBeenLastCalledWith(
118 | MATH_DRILL_OPTIONS, JSON.stringify(savedOptions));
119 |
120 | expect(actual).toEqual(savedOptions);
121 | });
122 |
123 | test('should get options that do not need converting', () => {
124 | const savedOptions = {
125 | largeKeyboard: true,
126 | levelIndex: 0, // A
127 | minutes: '1',
128 | onscreenKeyboard: true,
129 | opIndexes: [0], // A
130 | totalProblems: '20',
131 | userName: 'my name',
132 | };
133 | localStorage.getItem.mockReturnValueOnce(JSON.stringify(savedOptions));
134 |
135 | const actual = db.getOptions();
136 |
137 | expect(localStorage.setItem).toHaveBeenCalledTimes(0);
138 |
139 | expect(actual).toEqual(savedOptions);
140 | });
141 |
142 | test('should update scores and set version to current', () => {
143 | const scoresVersion1 = [{
144 | levelIndex: 0,
145 | opIndexes: [0],
146 | key: '00',
147 | previousResults: [{
148 | actual: 1,
149 | }, {
150 | actuals: [2],
151 | }],
152 | }, {
153 | levelIndex: 0,
154 | opIndexes: [0],
155 | key: '00',
156 | dummy: 1, // to test "else" in target code
157 | }];
158 |
159 | const scoresVersion2 = {
160 | version: 2,
161 | scores: [{
162 | levelIndex: 0,
163 | opIndexes: [0],
164 | key: '00',
165 | previousResults: [{
166 | actuals: [1],
167 | }, {
168 | actuals: [2],
169 | }],
170 | }, {
171 | levelIndex: 0,
172 | opIndexes: [0],
173 | key: '00',
174 | dummy: 1,
175 | }],
176 | };
177 |
178 | const scoresVersion3 = {
179 | version: 3,
180 | scores: [{
181 | levelIndex: 0,
182 | opIndexes: [0],
183 | key: '0-0',
184 | previousResults: [{
185 | actuals: [1],
186 | }, {
187 | actuals: [2],
188 | }],
189 | }, {
190 | levelIndex: 0,
191 | opIndexes: [0],
192 | key: '0-0',
193 | dummy: 1,
194 | }],
195 | };
196 |
197 | localStorage.getItem.mockReturnValueOnce(
198 | JSON.stringify(scoresVersion1));
199 | localStorage.getItem.mockReturnValueOnce(
200 | JSON.stringify(scoresVersion2));
201 | localStorage.getItem.mockReturnValueOnce(
202 | JSON.stringify(scoresVersion3));
203 |
204 | const actual = db.getScores();
205 |
206 | expect(localStorage.setItem).toHaveBeenCalledTimes(2);
207 | expect(localStorage.setItem).toHaveBeenCalledWith(
208 | MATH_DRILL_SCORES, JSON.stringify(scoresVersion2));
209 | expect(localStorage.setItem).toHaveBeenLastCalledWith(
210 | MATH_DRILL_SCORES, JSON.stringify(scoresVersion3));
211 |
212 | expect(actual).toEqual(scoresVersion3.scores);
213 | });
214 | });
215 |
--------------------------------------------------------------------------------
/test/learn/math/drill/__snapshots__/scoreboard.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ScoreBoard should render showScoreBoard 1`] = `
4 |
5 |
6 | Scoreboard
7 |
8 |
9 |
10 |
33 |
36 | 0
37 |
38 |
39 |
40 | Gold Badge(s) - 2 seconds or less (per question)
41 |
42 |
43 |
44 |
67 |
70 | 0
71 |
72 |
73 |
74 | Silver Badge(s) - between 2 and 3 seconds (per question)
75 |
76 |
77 |
78 |
101 |
104 | 0
105 |
106 |
107 |
108 | Bronze Badge(s) - between 3 and 4 seconds (per question)
109 |
110 |
111 |
112 |
135 |
138 | 1
139 |
140 |
141 |
142 | Blue Badge(s) - more than 4 seconds (per question)
143 |
144 |
145 |
146 |
154 |
157 |
160 |
163 |
168 | Level
169 |
170 |
175 | Addition
176 |
177 |
178 |
179 |
182 |
185 |
190 | A
191 |
192 |
196 |
197 |
220 |
223 | 1
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | `;
234 |
--------------------------------------------------------------------------------
/src/learn/math/drill/quiz-line.jsx:
--------------------------------------------------------------------------------
1 | const FloatingActionButton = require('@material-ui/core/Fab').default;
2 | const PropTypes = require('prop-types');
3 | const React = require('react');
4 | const TextField = require('@material-ui/core/TextField').default;
5 | const { Done } = require('@material-ui/icons');
6 | const Keyboard = require('./keyboard');
7 | const constants = require('../../common/constants');
8 | const RunningResults = require('./running-results');
9 |
10 | const { OPERATIONS: operations } = constants;
11 |
12 | const numberStyle = {
13 | fontSize: 'xx-large',
14 | margin: '10px',
15 | };
16 |
17 | const checkStyle = {
18 | margin: '10px',
19 | };
20 |
21 | const inputStyle = {
22 | textAlign: 'center',
23 | };
24 |
25 | const answerStyle = {
26 |
27 | paddingLeft: '15px',
28 | paddingRight: '15px',
29 | fontSize: 'xx-large',
30 | backgroundColor: 'green',
31 | color: 'white',
32 | };
33 |
34 | const lastResultCorrectStyle = {
35 | borderRadius: '5px',
36 | border: 'medium solid green',
37 | display: 'inline-block',
38 | paddingRight: '20px',
39 | };
40 |
41 | const lastResultIncorrectStyle = { ...lastResultCorrectStyle, border: 'medium solid red' };
42 |
43 | const quizLineStyle = {
44 | marginTop: '10px',
45 | marginBottom: '10px',
46 | };
47 |
48 | class QuizLine extends React.Component {
49 | constructor() {
50 | super();
51 | this.onChange = this.onChange.bind(this);
52 | this.handleKeyPress = this.handleKeyPress.bind(this);
53 | this.checkAnswer = this.checkAnswer.bind(this);
54 | this.keyPress = this.keyPress.bind(this);
55 | // TODO: Fix and remove this ESLint disable
56 | // eslint-disable-next-line react/state-in-constructor
57 | this.state = { answer: '' };
58 | }
59 |
60 | onChange(e) {
61 | const { name, value } = e.target;
62 | this.setState({
63 | [name]: value,
64 | });
65 | }
66 |
67 | checkAnswer() {
68 | const { checkAnswer } = this.props;
69 | const { answer } = this.state;
70 | checkAnswer(answer);
71 | this.setState({
72 | answer: '',
73 | });
74 | }
75 |
76 | handleKeyPress(event) {
77 | if (event.key === 'Enter') {
78 | this.checkAnswer();
79 | }
80 | }
81 |
82 | keyPress(key) {
83 | const { answer } = this.state;
84 | if (!isNaN(key)) {
85 | this.setState({
86 | answer: `${answer}${key}`,
87 | });
88 | } else {
89 | switch (key) {
90 | case 'back':
91 | if (answer.length) {
92 | this.setState({
93 | answer: answer.substr(0, answer.length - 1),
94 | });
95 | }
96 | break;
97 | case 'enter':
98 | this.checkAnswer();
99 | break;
100 | default:
101 | // eslint-disable-next-line no-console
102 | console.warn(`Unknown key in keyPress ${key}`);
103 | break;
104 | }
105 | }
106 | }
107 |
108 | renderNewRecord() {
109 | const {
110 | newRecord: {
111 | isNewRecord,
112 | currentTimePerQuestion,
113 | existingRecordTimePerQuestion,
114 | },
115 | } = this.props;
116 |
117 | const current = isNaN(currentTimePerQuestion) ? '[none]' : currentTimePerQuestion;
118 | const existing = existingRecordTimePerQuestion || '[none]';
119 |
120 | const recordText = `Current Speed ${current} and Record ${existing}${isNewRecord ? ' NEW RECORD!' : ''}`;
121 |
122 | return (
123 |
124 |
125 | {recordText}
126 |
127 |
128 | );
129 | }
130 |
131 | renderLastResult() {
132 | const { lastResult } = this.props;
133 | if (!lastResult) {
134 | const divStyle = {
135 |
136 | ...lastResultCorrectStyle,
137 | paddingBottom: '5px',
138 | paddingLeft: '10px',
139 | paddingRight: '10px',
140 | paddingTop: '5px',
141 | display: 'block',
142 | };
143 |
144 | return (
145 |
146 | {'Ready for your first answer...'}
147 |
148 | );
149 | }
150 | const { actuals, task } = lastResult;
151 | const [actual] = actuals;
152 | const [,,, answer] = task;
153 |
154 | const borderStyle = answer === actual
155 | ? lastResultCorrectStyle
156 | : lastResultIncorrectStyle;
157 |
158 | return (
159 |
160 |
161 |
162 | );
163 | }
164 |
165 | render() {
166 | const { answer } = this.state;
167 | const { onscreenKeyboard, problem, largeKeyboard } = this.props;
168 | const [left, right, opIndex] = problem;
169 | const operator = operations[opIndex];
170 | return (
171 |
172 |
173 |
174 | {left}
175 |
176 |
177 | {operator}
178 |
179 |
180 | {right}
181 |
182 |
183 | =
184 |
185 | {
186 | onscreenKeyboard
187 | ? (
188 |
189 | {answer || '?'}
190 |
191 | )
192 | : (
193 |
203 | )
204 | }
205 | {
206 | !onscreenKeyboard
207 | && (
208 |
209 |
210 | Check Answer
211 |
212 | )
213 | }
214 |
215 | {this.renderLastResult()}
216 | {this.renderNewRecord()}
217 |
218 |
223 |
224 |
225 | );
226 | }
227 | }
228 |
229 | QuizLine.propTypes = {
230 | checkAnswer: PropTypes.func.isRequired,
231 | lastResult: PropTypes.shape({
232 | actuals: PropTypes.arrayOf(PropTypes.number).isRequired,
233 | id: PropTypes.number.isRequired,
234 | task: PropTypes.array.isRequired, // left, right, opIndex, answer
235 | timeTaken: PropTypes.number.isRequired,
236 | }),
237 | newRecord: PropTypes.shape({
238 | isNewRecord: PropTypes.bool.isRequired,
239 | currentTimePerQuestion: PropTypes.number.isRequired,
240 | existingRecordTimePerQuestion: PropTypes.number.isRequired,
241 | }).isRequired,
242 | onscreenKeyboard: PropTypes.bool.isRequired,
243 | largeKeyboard: PropTypes.bool.isRequired,
244 | problem: PropTypes.arrayOf(PropTypes.number).isRequired,
245 | };
246 |
247 | QuizLine.defaultProps = {
248 | lastResult: null,
249 | };
250 |
251 | module.exports = QuizLine;
252 |
--------------------------------------------------------------------------------
/test/__snapshots__/app.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`App should render an App component 1`] = `
4 |
5 |
133 |
134 |
190 |
223 |
262 |
301 |
302 |
303 | `;
304 |
--------------------------------------------------------------------------------
/src/learn/math/drill/index.jsx:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 | const React = require('react');
3 | const db = require('../../db');
4 | const Finished = require('./finished');
5 | const helper = require('./helper');
6 | const Options = require('./options');
7 | const Running = require('./running');
8 |
9 | class MathDrill extends React.Component {
10 | static save(keyValuePair) {
11 | const options = db.getOptions();
12 | if (!options || !keyValuePair || !keyValuePair.hasOwnProperty) {
13 | return;
14 | }
15 |
16 | Object.keys(options).forEach((key) => {
17 | if (Object.prototype.hasOwnProperty.call(keyValuePair, key)) {
18 | options[key] = keyValuePair[key];
19 | }
20 | });
21 |
22 | db.saveOptions(options);
23 | }
24 |
25 | constructor() {
26 | super();
27 |
28 | const options = db.getOptions();
29 |
30 | // TODO: Fix and remove this ESLint disable
31 | // eslint-disable-next-line react/state-in-constructor
32 | this.state = {
33 | largeKeyboard: options.largeKeyboard,
34 | currentTask: [],
35 | levelIndex: options.levelIndex,
36 | minutes: options.minutes,
37 | onscreenKeyboard: options.onscreenKeyboard,
38 | opIndexes: options.opIndexes,
39 | previousResults: [], // previousResults results of quiz
40 | totalProblems: options.totalProblems,
41 | userName: options.userName,
42 | };
43 |
44 | this.checkAnswer = this.checkAnswer.bind(this);
45 | this.onChange = this.onChange.bind(this);
46 | this.onStart = this.onStart.bind(this);
47 | this.setNextTask = this.setNextTask.bind(this);
48 | this.onInterval = this.onInterval.bind(this);
49 | this.setParentState = this.setParentState.bind(this);
50 | }
51 |
52 | onInterval() {
53 | const {
54 | endTime,
55 | } = this.state;
56 | const timeLeft = Math.round(endTime.diff(moment()) / 1000);
57 | if (timeLeft <= 0) {
58 | this.endQuiz({ timeLeft });
59 | } else {
60 | this.setState({
61 | timeLeft,
62 | });
63 | }
64 | }
65 |
66 | onStart() {
67 | this.setNextTask();
68 | const {
69 | levelIndex,
70 | minutes = '1',
71 | opIndexes,
72 | totalProblems,
73 | } = this.state;
74 | const seconds = parseFloat(minutes, 10) * 60;
75 | const startTime = moment();
76 | const endTime = moment().add(seconds, 'seconds');
77 | const timerId = setInterval(this.onInterval, 1000);
78 | const currentRecord = helper.getCurrentRecord(levelIndex, opIndexes);
79 | this.setState({
80 | currentAction: 'running',
81 | currentRecord,
82 | endTime,
83 | questionsRemaining: parseInt(totalProblems, 10),
84 | seconds,
85 | startTime,
86 | timeLeft: seconds,
87 | timerId,
88 | });
89 | }
90 |
91 | onChange(e) {
92 | const { name } = e.target;
93 | const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
94 | const kvp = { [name]: value };
95 | this.setState(kvp);
96 | MathDrill.save(kvp);
97 | }
98 |
99 | setParentState(state) {
100 | this.setState(state);
101 | MathDrill.save(state);
102 | }
103 |
104 | setNextTask() {
105 | const { levelIndex, opIndexes, currentTask } = this.state;
106 | const nextTask = helper.getLowerUpper(levelIndex, opIndexes);
107 |
108 | if (nextTask.every((item, index) => currentTask[index] === item)) {
109 | this.setNextTask();
110 | } else {
111 | this.setState({
112 | currentTask: nextTask,
113 | });
114 | }
115 | }
116 |
117 | endQuiz(otherState = {}) {
118 | const { timerId } = this.state;
119 | clearInterval(timerId);
120 | // Need to do this to get latest results. Merging in otherState
121 | // with state will provide the final results. We can't rely on
122 | // a call to this.setState() in a previous method because it batches
123 | // its calls and may not have updated state.
124 | const state = {
125 |
126 | ...this.state,
127 | ...otherState,
128 | };
129 | const resultInfo = helper.appendScore(state);
130 | this.setState({
131 | currentAction: 'finished',
132 | timerId: null,
133 | ...otherState,
134 | resultInfo,
135 | });
136 | }
137 |
138 | // eslint-disable-next-line react/sort-comp
139 | static currentMatchesLast(task, previousResults) {
140 | if (!previousResults.length) {
141 | return false;
142 | }
143 | const lastResult = previousResults[previousResults.length - 1];
144 | const { task: lastTask } = lastResult;
145 | return task.length === 4 && task.every((t, i) => t === lastTask[i]);
146 | }
147 |
148 | checkAnswer(answer) {
149 | const actual = parseInt(answer, 10);
150 | let { totalProblems } = this.state;
151 | const { startTime } = this.state;
152 | totalProblems = parseInt(totalProblems, 10);
153 | if (!isNaN(actual)) {
154 | const { currentTask: task, previousResults = [] } = this.state;
155 | let { correctCount = 0, totalCount } = this.state;
156 | totalCount += 1;
157 | let { previousTime = startTime } = this.state;
158 | const timeTaken = parseFloat((moment().diff(previousTime) / 1000).toFixed(1));
159 |
160 | const [,,, expected] = task;
161 | const correct = actual === expected;
162 | if (correct) {
163 | correctCount += 1;
164 | previousTime = moment();
165 | }
166 | // If first time wrong then push onto previousResults
167 | // otherwise replace last element in previousResults
168 | // If correct and was previously wrong then replace otherwise push.
169 | // If problem exists at end of array then replace otherwise push.
170 | // A task is an array of: left, right, opIndex, expectedAnswer
171 | // and problems are not repeated so a first time can be checked using this.
172 | if (MathDrill.currentMatchesLast(task, previousResults)) {
173 | const current = previousResults[previousResults.length - 1];
174 | current.actuals.unshift(actual);
175 | current.timeTaken = timeTaken;
176 | } else {
177 | const actuals = [actual];
178 | previousResults.push({
179 | task, actuals, timeTaken, id: previousResults.length,
180 | });
181 | }
182 |
183 | const otherState = {
184 | correct,
185 | correctCount,
186 | previousTime,
187 | result: `${actual} is ${correct ? 'correct' : 'wrong'}`,
188 | previousResults,
189 | totalCount,
190 | questionsRemaining: totalProblems - correctCount,
191 | };
192 |
193 | if (correctCount === totalProblems) {
194 | this.endQuiz(otherState);
195 | } else {
196 | this.setState(otherState);
197 | if (correct) {
198 | this.setNextTask();
199 | }
200 | }
201 | }
202 | }
203 |
204 | renderOptions() {
205 | const {
206 | largeKeyboard,
207 | levelIndex,
208 | minutes,
209 | onscreenKeyboard,
210 | opIndexes,
211 | totalProblems,
212 | userName,
213 | } = this.state || {};
214 |
215 | return (
216 |
228 | );
229 | }
230 |
231 | renderRunning() {
232 | const {
233 | currentRecord,
234 | currentTask,
235 | largeKeyboard,
236 | levelIndex,
237 | onscreenKeyboard,
238 | opIndexes,
239 | previousResults,
240 | questionsRemaining,
241 | startTime,
242 | timeLeft,
243 | } = this.state;
244 |
245 | return (
246 |
258 | );
259 | }
260 |
261 | renderFinished() {
262 | const {
263 | previousResults,
264 | resultInfo,
265 | seconds,
266 | timeLeft,
267 | totalProblems,
268 | levelIndex,
269 | opIndexes,
270 | } = this.state;
271 | return (
272 |
281 | );
282 | }
283 |
284 | render() {
285 | const {
286 | currentAction = 'start',
287 | } = this.state || {};
288 | switch (currentAction) {
289 | case 'start':
290 | return this.renderOptions();
291 | case 'running':
292 | return this.renderRunning();
293 | case 'finished':
294 | return this.renderFinished();
295 | default:
296 | // eslint-disable-next-line no-console
297 | console.error(`Unknown currentAction ${currentAction}`);
298 | return null;
299 | }
300 | }
301 | }
302 |
303 | module.exports = MathDrill;
304 |
--------------------------------------------------------------------------------
/src/learn/math/drill/helper.js:
--------------------------------------------------------------------------------
1 | // A collection of helper functions
2 | const moment = require('moment');
3 | const db = require('../../db');
4 | const constants = require('../../common/constants');
5 | const { levelOps } = require('./helper-problems');
6 |
7 | const {
8 | RECORD_NEW,
9 | RECORD_EQUAL,
10 | RECORD_MISS,
11 | RECORD_NOT_EXIST,
12 | BADGE_BOUNDARIES: badgeBoundaries,
13 | COLOR_HTML,
14 | } = constants;
15 |
16 |
17 | function calculateAnswer([left, right], opIndex) {
18 | switch (opIndex) {
19 | case 0:
20 | return left + right;
21 | case 1:
22 | return left - right;
23 | case 2:
24 | return left * right;
25 | case 3:
26 | // When numerator is greater than denominator we're going to treat
27 | // it as a zero
28 | return Math.floor(left / right);
29 | default:
30 | return 0;
31 | }
32 | }
33 |
34 | /**
35 | *
36 | * @param {number} levelIndex - 0 through 25 - each letter of the alphabet
37 | * @param {number[]} opIndexes - An array of 0 through 3 - operators +, -, *, /
38 | */
39 | function getLowerUpper(levelIndexParam, opIndexes) {
40 | // Pick a random value from the opIndexes array
41 | const opIndex = opIndexes[Math.floor(Math.random() * opIndexes.length)];
42 |
43 | // Pick a collection of levels based on the opIndex
44 | const levels = levelOps[opIndex].filter((level) => !!level.length);
45 | // Some of the last few levels are intentionally empty because it's just a repeat of
46 | // the final level that has values.
47 | const levelIndex = Math.min(levels.length, levelIndexParam);
48 | // Put all the levels up to levelIndex into an array except put the same number of each
49 | // level as the ordinal number of the level. This will gradually weight the higher levels
50 | // so pairs from those levels will come up more often.
51 | // e.g.
52 | // level = 0, [0]
53 | // level = 1: [0, 1, 1]
54 | // level = 2: [0, 1, 1, 2, 2, 2]
55 | const weightedLevels = [...Array(levelIndex + 1).keys()]
56 | .reduce((acc, level) => acc.concat([...Array(level + 1).keys()]
57 | .map(() => level)), []);
58 |
59 | // Pick a random value from the array
60 | const levelElement = weightedLevels[Math.floor(Math.random() * weightedLevels.length)];
61 | // Get the array of pairs for that level
62 | const level = levels[Math.min(levelElement, levels.length - 1)];
63 | // Pick a random pair from the array
64 | const pair = level[Math.floor(Math.random() * level.length)];
65 |
66 | return [...pair, opIndex, calculateAnswer(pair, opIndex)];
67 | }
68 |
69 | // The key identifies a problem set by level and operators. This allows us
70 | // to search for previous scores that used that combination to see if this
71 | // is a new record.
72 | function createKey(levelIndex, opIndexes) {
73 | return `${levelIndex.toString()}-${opIndexes.join('')}`;
74 | }
75 |
76 | function getCurrentRecord(levelIndex, opIndexes) {
77 | const existingScores = db.getScores() || [];
78 | const key = createKey(levelIndex, opIndexes);
79 |
80 | const matchingProblems = existingScores.filter((score) => score.key === key)
81 | .sort((a, b) => a.timePerQuestion - b.timePerQuestion);
82 | return matchingProblems[0];
83 | }
84 |
85 | function appendScore(results) {
86 | const {
87 | correctCount,
88 | levelIndex,
89 | minutes,
90 | opIndexes: operatorIndexes,
91 | previousResults,
92 | questionsRemaining,
93 | timeLeft,
94 | totalProblems,
95 | } = results;
96 | const opIndexes = operatorIndexes.sort();
97 |
98 | const completed = questionsRemaining === 0;
99 | const timeTaken = (minutes * 60) - timeLeft;
100 |
101 | const timePerQuestion = correctCount === 0
102 | ? NaN
103 | : parseFloat((timeTaken / correctCount).toFixed(1));
104 | const incorrectCount = previousResults.length - correctCount;
105 | const date = Date.now();
106 |
107 | const resultInfo = {};
108 | const currentRecord = getCurrentRecord(levelIndex, opIndexes);
109 |
110 | // 5 scenarios
111 | // 1. No matching problems - first time this test has been done.
112 | // 2. Equals previous high score
113 | // 3. Beats previous high score
114 | // 4. Worse than previous high score
115 | // 5. Zero questions answered
116 | if (isNaN(timePerQuestion)) {
117 | resultInfo.text = `You didn't answer any questions correctly so we are not going to use this score \
118 | as part of your high scores. If you are struggling with these problems then try an \
119 | easier level or an ${'easier'} operator.`;
120 | resultInfo.newRecordInfo = RECORD_MISS;
121 | } else if (currentRecord) {
122 | const { timePerQuestion: bestTimePerQuestion } = currentRecord;
123 | if (timePerQuestion === bestTimePerQuestion) {
124 | resultInfo.text = `Your time is equal to your best score of ${timePerQuestion} seconds per question. Great job!`;
125 | resultInfo.newRecordInfo = RECORD_EQUAL;
126 | } else if (timePerQuestion < bestTimePerQuestion) {
127 | resultInfo.text = `NEW RECORD! Awesome work! Your new best score is ${timePerQuestion} seconds per question.`;
128 | resultInfo.newRecordInfo = RECORD_NEW;
129 | } else {
130 | resultInfo.text = `You answered the questions at a rate of ${timePerQuestion} seconds \
131 | per question. (Your best score is ${bestTimePerQuestion} seconds per question.)`;
132 | resultInfo.newRecordInfo = RECORD_MISS;
133 | }
134 | } else {
135 | resultInfo.text = `This is the first time you've done this problem. You took \
136 | ${timePerQuestion} seconds per question. Do this test again to see if you can \
137 | beat this score. Good luck!`;
138 | resultInfo.newRecordInfo = RECORD_NOT_EXIST;
139 | }
140 |
141 | if (!isNaN(timePerQuestion)) {
142 | const record = {
143 | completed,
144 | correctCount,
145 | date,
146 | incorrectCount,
147 | key: createKey(levelIndex, opIndexes),
148 | levelIndex,
149 | minutes,
150 | opIndexes,
151 | previousResults,
152 | questionsRemaining,
153 | timeLeft,
154 | timePerQuestion,
155 | timeTaken,
156 | totalProblems,
157 | };
158 |
159 | db.appendScore(record);
160 | }
161 |
162 | return resultInfo;
163 | }
164 |
165 | function getBadgeColorIndex(timePerQuestion) {
166 | return badgeBoundaries
167 | .reduce((badgeColor, boundary, index) => (timePerQuestion > boundary ? index : badgeColor), 0);
168 | }
169 |
170 | function getBadgeHtmlColor(timePerQuestion) {
171 | const index = getBadgeColorIndex(timePerQuestion);
172 | return COLOR_HTML[index];
173 | }
174 |
175 | function getScoreBarTimes(levelIndex, opIndexes, scoreParams) {
176 | const thisKey = createKey(levelIndex, opIndexes);
177 | const scores = scoreParams || db.getScores() || [];
178 | const filteredScores = scores
179 | .filter(({ key, correctCount }) => key === thisKey && correctCount > 9)
180 | .sort((a, b) => a.date - b.date);
181 | if (!filteredScores.length) {
182 | return [];
183 | }
184 |
185 | return filteredScores
186 | .slice(-5)
187 | .map(({ date, timePerQuestion }) => ({
188 | date,
189 | timePerQuestion,
190 | }));
191 | }
192 |
193 |
194 | function getScoreboard() {
195 | const scores = db.getScores() || [];
196 | // totals are for badges irrespective of operators so there will always be
197 | // 4 of these for Gold, Silver, Bronze and Blue
198 | const totals = [0, 0, 0, 0];
199 |
200 | // levelOperators has keys (props) that map to both the level and operator(s) that were used
201 | // and aggregate the scores based on this.
202 | // Example keys:
203 | // '00' - Level A Addition
204 | // '313' - Level D Mixed Subtraction/Division
205 | // The values are arrays of 4 elements that match to badge colors and the number of
206 | // badges at that level/operator(s)
207 | // Example value:
208 | // [0, 5, 2, 1] - 0 Gold, 5 Silver, 2 Bronze, 1 Blue
209 | const levelOperators = scores.reduce((acc, score) => {
210 | const {
211 | levelIndex,
212 | correctCount,
213 | opIndexes,
214 | timePerQuestion,
215 | } = score;
216 |
217 | // Filter down the scores to just those that have
218 | // at least 10 correct answers
219 | if (correctCount > 9) {
220 | const key = createKey(levelIndex, opIndexes);
221 | if (!acc[key]) {
222 | acc[key] = [0, 0, 0, 0];
223 | }
224 | const levelOp = acc[key];
225 |
226 | const badgeColorIndex = getBadgeColorIndex(timePerQuestion);
227 | totals[badgeColorIndex] += 1;
228 | levelOp[badgeColorIndex] += 1;
229 | }
230 |
231 | return acc;
232 | }, {});
233 |
234 | // ops is a collection of strings that are keys into the operators
235 | // used in the test. The operators are +, -, *, / and each of those
236 | // maps to an index value from 0 through 3.
237 | // For example: '0' is + and '13' is mixed - and /
238 | // Each new operator key is appended to this Set.
239 | const ops = new Set();
240 |
241 | // levelOperators is now an object with both the level and the operators
242 | // in the keys. In order to make this easy for the component to show this
243 | // in a table form we are now going to change it into a two dimensional
244 | // array. The rows will map to levels and then each row will have a collection
245 | // of arrays that show the number of badges for that operator(s).
246 | // We will need another array to map the operator(s) into the columns.
247 | const levels = Object.keys(levelOperators).reduce((acc, levelOperator) => {
248 | const [levelIndexSplit, operatorIndexes] = levelOperator.split('-');
249 | const levelIndex = parseInt(levelIndexSplit, 10);
250 | ops.add(operatorIndexes);
251 | if (!acc[levelIndex]) {
252 | acc[levelIndex] = {};
253 | }
254 | acc[levelIndex][operatorIndexes] = levelOperators[levelOperator];
255 | return acc;
256 | }, []);
257 |
258 | // levels now looks something like this:
259 | // [
260 | // { '0': [0,5,3,1], '01': [0,0,0,2]},
261 | // undefined,
262 | // { '0': [0,5,3,1], '02': [0,0,1,2]},
263 | // ]
264 | // and ops is a set with:
265 | // '0', '01', '02'
266 |
267 |
268 | return {
269 | ops, // A Set of strings representing operators
270 | totals, // A 4 element array of badge totals
271 | levels, // An sparse array of up to 26 elements of objects with keys matching values in ops Set
272 | };
273 | }
274 |
275 | function currentTimePerQuestion({ previousResults, startTime }) {
276 | const timeElapsed = moment().diff(startTime) / 1000;
277 | const correctQuestions = previousResults.reduce((acc, result) => {
278 | // result: {"task":[1,3,0,4],"actuals":[4],"timeTaken":2.5,"id":0}
279 | const { task, actuals } = result;
280 | const [actual] = actuals;
281 | const [,,, answer] = task;
282 | return acc + Number(actual === answer);
283 | }, 0);
284 | return correctQuestions
285 | ? parseFloat((timeElapsed / correctQuestions).toFixed(1))
286 | : NaN;
287 | }
288 |
289 | function newRecord({ currentRecord, previousResults, startTime }) {
290 | const currentTime = currentTimePerQuestion({ previousResults, startTime });
291 | if (!currentRecord) {
292 | return {
293 | isNewRecord: false,
294 | currentTimePerQuestion: currentTime,
295 | existingRecordTimePerQuestion: NaN,
296 | };
297 | }
298 | const { timePerQuestion } = currentRecord;
299 | return {
300 | isNewRecord: isNaN(currentTime) ? false : timePerQuestion > currentTime,
301 | currentTimePerQuestion: currentTime,
302 | existingRecordTimePerQuestion: timePerQuestion || NaN,
303 | };
304 | }
305 |
306 | module.exports = {
307 | appendScore,
308 | calculateAnswer,
309 | getBadgeColorIndex,
310 | getBadgeHtmlColor,
311 | getCurrentRecord,
312 | getLowerUpper,
313 | getScoreBarTimes,
314 | getScoreboard,
315 | newRecord,
316 | };
317 |
--------------------------------------------------------------------------------
/test/learn/math/drill/__snapshots__/score-bar.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ScoreBar should not show if showScoreBar is false 1`] = `null`;
4 |
5 | exports[`ScoreBar should not show if times is empty 1`] = `null`;
6 |
7 | exports[`ScoreBar should show 1 column with Gold Badge 1`] = `
8 |
11 |
21 |
31 |
38 | 4 minutes ago
39 |
40 |
47 | Badges
48 |
49 |
50 |
51 |
61 |
74 |
81 |
82 |
83 |
91 | YOU⇾
92 |
93 |
100 |
101 |
102 |
103 |
116 |
124 | Gold
125 |
126 |
134 | Silver
135 |
136 |
144 | Bronze
145 |
146 |
154 | Blue
155 |
156 |
157 |
158 |
159 | `;
160 |
161 | exports[`ScoreBar should show 1 column with below Blue Badge 1`] = `
162 |
165 |
175 |
185 |
192 | 4 minutes ago
193 |
194 |
201 | Badges
202 |
203 |
204 |
205 |
215 |
228 |
235 |
236 |
237 |
245 | YOU↓
246 |
247 |
254 |
255 |
256 |
257 |
270 |
278 | Gold
279 |
280 |
288 | Silver
289 |
290 |
298 | Bronze
299 |
300 |
308 | Blue
309 |
310 |
311 |
312 |
313 | `;
314 |
315 | exports[`ScoreBar should show 5 columns with mixed Badges 1`] = `
316 |
319 |
329 |
339 |
346 | 4 minutes ago
347 |
348 |
355 | 2 minutes ago
356 |
357 |
364 | a few seconds ago
365 |
366 |
373 | Badges
374 |
375 |
376 |
377 |
387 |
400 |
407 |
408 |
409 |
417 | YOU↓
418 |
419 |
426 |
427 |
428 |
429 |
442 |
449 |
450 |
451 |
459 | YOU↓
460 |
461 |
468 |
469 |
470 |
471 |
484 |
491 |
492 |
493 |
501 | YOU⇾
502 |
503 |
510 |
511 |
512 |
513 |
526 |
534 | Gold
535 |
536 |
544 | Silver
545 |
546 |
554 | Bronze
555 |
556 |
564 | Blue
565 |
566 |
567 |
568 |
569 | `;
570 |
--------------------------------------------------------------------------------
/test/learn/math/drill/helper.test.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment');
2 | const db = require('../../../../src/learn/db');
3 | const helper = require('../../../../src/learn/math/drill/helper');
4 | const constants = require('../../../../src/learn/common/constants');
5 | const util = require('../../../../src/learn/common/util');
6 |
7 | const { getScoreboard } = helper;
8 |
9 | const { fillArray } = util;
10 |
11 | const {
12 | RECORD_NEW,
13 | RECORD_EQUAL,
14 | RECORD_MISS,
15 | RECORD_NOT_EXIST,
16 | // BADGE_BOUNDARIES: badgeBoundaries,
17 | // COLOR_HTML,
18 | } = constants;
19 |
20 | describe('Helper', () => {
21 | beforeEach(() => {
22 | db.getScores = jest.fn();
23 | db.appendScore = jest.fn();
24 | });
25 |
26 | afterEach(() => {
27 | db.getScores.mockClear();
28 | db.appendScore.mockClear();
29 | });
30 |
31 | test('should get a pair of numbers from level #1 plus', () => {
32 | const pair = helper.getLowerUpper(0, [0]);
33 | expect(pair[0] === 1 || pair[1] === 1).toBe(true);
34 | });
35 |
36 | test('should get a random pair of numbers', () => {
37 | const pair = helper.getLowerUpper(4, [0]);
38 | expect(pair[0]).toBeLessThan(8);
39 | expect(pair[1]).toBeLessThan(8);
40 | });
41 |
42 | test('should default to [] when scores null in getScoreBarTimes()', () => {
43 | db.getScores.mockReturnValueOnce(null);
44 |
45 | const actual = helper.getScoreBarTimes(0, [0]);
46 |
47 | const expected = [];
48 |
49 | expect(actual).toEqual(expected);
50 | });
51 |
52 | test('should getScoreBarTimes() for zero items', () => {
53 | const scores = [];
54 |
55 | const actual = helper.getScoreBarTimes(0, [0], scores);
56 |
57 | const expected = [];
58 |
59 | expect(actual).toEqual(expected);
60 | });
61 |
62 | test('should getScoreBarTimes() for single item', () => {
63 | const scores = [{
64 | key: '0-0',
65 | correctCount: 10,
66 | date: 1,
67 | timePerQuestion: 1.3,
68 | }];
69 |
70 | const actual = helper.getScoreBarTimes(0, [0], scores);
71 |
72 | const expected = [{
73 | date: 1,
74 | timePerQuestion: 1.3,
75 | }];
76 |
77 | expect(actual).toEqual(expected);
78 | });
79 |
80 | test('should get the expected number of results', () => {
81 | const scores = [];
82 | const expected = [];
83 | for (let i = 0; i < 5; i += 1) {
84 | scores.push({
85 | key: '0-0',
86 | correctCount: 10,
87 | date: 1,
88 | timePerQuestion: 2,
89 | });
90 |
91 | const actual = helper.getScoreBarTimes(0, [0], scores);
92 |
93 | expected.push({
94 | date: 1,
95 | timePerQuestion: 2,
96 | });
97 |
98 | expect(actual).toEqual(expected);
99 | }
100 | });
101 |
102 | test('should sort getScoreBarTimes() for two items', () => {
103 | const scores = [{
104 | key: '0-0',
105 | correctCount: 22,
106 | date: 2,
107 | timePerQuestion: 2.2,
108 | }, {
109 | key: '0-0',
110 | correctCount: 11,
111 | date: 1,
112 | timePerQuestion: 1.1,
113 | }];
114 |
115 | const actual = helper.getScoreBarTimes(0, [0], scores);
116 |
117 | const expected = [{
118 | date: 1,
119 | timePerQuestion: 1.1,
120 | }, {
121 | date: 2,
122 | timePerQuestion: 2.2,
123 | }];
124 |
125 | expect(actual).toEqual(expected);
126 | });
127 |
128 | test(
129 | 'should not return more than 5 items getScoreBarTimes() for six items',
130 | () => {
131 | const scores = [{
132 | key: '0-0',
133 | correctCount: 66,
134 | date: 6,
135 | timePerQuestion: 6.6,
136 | }, {
137 | key: '0-0',
138 | correctCount: 55,
139 | date: 5,
140 | timePerQuestion: 5.5,
141 | }, {
142 | key: '0-0',
143 | correctCount: 44,
144 | date: 4,
145 | timePerQuestion: 4.4,
146 | }, {
147 | key: '0-0',
148 | correctCount: 33,
149 | date: 3,
150 | timePerQuestion: 3.3,
151 | }, {
152 | key: '0-0',
153 | correctCount: 22,
154 | date: 2,
155 | timePerQuestion: 2.2,
156 | }, {
157 | key: '0-0',
158 | correctCount: 11,
159 | date: 1,
160 | timePerQuestion: 1.1,
161 | }];
162 |
163 | const actual = helper.getScoreBarTimes(0, [0], scores);
164 |
165 | const expected = [{
166 | date: 2,
167 | timePerQuestion: 2.2,
168 | }, {
169 | date: 3,
170 | timePerQuestion: 3.3,
171 | }, {
172 | date: 4,
173 | timePerQuestion: 4.4,
174 | }, {
175 | date: 5,
176 | timePerQuestion: 5.5,
177 | }, {
178 | date: 6,
179 | timePerQuestion: 6.6,
180 | }];
181 |
182 | expect(actual).toEqual(expected);
183 | },
184 | );
185 |
186 | test('should calculate all Answer operators', () => {
187 | const { calculateAnswer } = helper;
188 | const inputs = [
189 | [[6, 3], 0, 9], // 6 + 3 = 9
190 | [[6, 3], 1, 3], // 6 - 3 = 3
191 | [[6, 3], 2, 18], // 6 x 3 = 18
192 | [[6, 3], 3, 2], // 6 / 3 = 2
193 | [[6, 3], 4, 0], // Unknown operator returns 0
194 | ];
195 |
196 | inputs.forEach((input) => {
197 | const [pair, operator, expected] = input;
198 | const actual = calculateAnswer(pair, operator);
199 | expect(actual).toBe(expected);
200 | });
201 | });
202 |
203 | test('should get current record if data exists', () => {
204 | db.getScores.mockReturnValueOnce([{
205 | key: '0-01',
206 | timePerQuestion: 2,
207 | }, {
208 | key: '0-02',
209 | timePerQuestion: 0.5,
210 | }, {
211 | key: '0-01',
212 | timePerQuestion: 1,
213 | }]);
214 | const { getCurrentRecord } = helper;
215 |
216 | const actual = getCurrentRecord(0, [0, 1]);
217 | expect(actual).toEqual({
218 | key: '0-01',
219 | timePerQuestion: 1,
220 | });
221 | });
222 |
223 | test('should get undefined for current record if no data exists', () => {
224 | db.getScores.mockReturnValueOnce(undefined);
225 | const { getCurrentRecord } = helper;
226 |
227 | const actual = getCurrentRecord(0, [0, 1]);
228 | expect(actual).toBeUndefined();
229 | });
230 |
231 | test('should append score with no existing record', () => {
232 | db.getScores.mockReturnValueOnce(undefined);
233 | const { appendScore } = helper;
234 | const results = {
235 | correctCount: 10,
236 | levelIndex: 0,
237 | minutes: 10, // in minutes
238 | opIndexes: [0],
239 | previousResults: [], // TODO: A test where this is undefined
240 | questionsRemaining: 0,
241 | timeLeft: 540, // in seconds
242 | totalProblems: 10,
243 | };
244 |
245 | const actual = appendScore(results);
246 | expect(actual).toEqual({
247 | // eslint-disable-next-line no-multi-str
248 | text: 'This is the first time you\'ve done this problem. You took \
249 | 6 seconds per question. Do this test again to see if you can \
250 | beat this score. Good luck!',
251 | newRecordInfo: RECORD_NOT_EXIST,
252 | });
253 | expect(db.getScores).toHaveBeenCalled();
254 | expect(db.appendScore).toHaveBeenCalled();
255 | });
256 |
257 | test('should not append score if no correct answers', () => {
258 | db.getScores.mockReturnValueOnce(undefined);
259 | const { appendScore } = helper;
260 | const results = {
261 | correctCount: 0,
262 | levelIndex: 0,
263 | minutes: 10, // in minutes
264 | opIndexes: [0],
265 | previousResults: [], // TODO: A test where this is undefined
266 | questionsRemaining: 0,
267 | timeLeft: 540, // in seconds
268 | totalProblems: 10,
269 | };
270 |
271 | const actual = appendScore(results);
272 | expect(actual).toEqual({
273 | // eslint-disable-next-line no-multi-str
274 | text: 'You didn\'t answer any questions correctly so we are not going to use this score \
275 | as part of your high scores. If you are struggling with these problems then try an \
276 | easier level or an easier operator.',
277 | newRecordInfo: RECORD_MISS,
278 | });
279 | expect(db.getScores).toHaveBeenCalled();
280 | expect(db.appendScore).not.toHaveBeenCalled();
281 | });
282 |
283 | test('should report an equal record', () => {
284 | db.getScores.mockReturnValueOnce([{
285 | timePerQuestion: 6,
286 | key: '0-0',
287 | }]);
288 | const { appendScore } = helper;
289 | const results = {
290 | correctCount: 10,
291 | levelIndex: 0,
292 | minutes: 10, // in minutes
293 | opIndexes: [0],
294 | previousResults: [], // TODO: A test where this is undefined
295 | questionsRemaining: 0,
296 | timeLeft: 540, // in seconds
297 | totalProblems: 10,
298 | };
299 |
300 | const actual = appendScore(results);
301 | expect(actual).toEqual({
302 | // eslint-disable-next-line no-multi-str
303 | text: 'Your time is equal to your best score of 6 seconds per question. Great job!',
304 | newRecordInfo: RECORD_EQUAL,
305 | });
306 | expect(db.getScores).toHaveBeenCalled();
307 | expect(db.appendScore).toHaveBeenCalled();
308 | });
309 |
310 | test('should report a new record', () => {
311 | db.getScores.mockReturnValueOnce([{
312 | timePerQuestion: 7,
313 | key: '0-0',
314 | }]);
315 | const { appendScore } = helper;
316 | const results = {
317 | correctCount: 10,
318 | levelIndex: 0,
319 | minutes: 10, // in minutes
320 | opIndexes: [0],
321 | previousResults: [], // TODO: A test where this is undefined
322 | questionsRemaining: 0,
323 | timeLeft: 540, // in seconds
324 | totalProblems: 10,
325 | };
326 |
327 | const actual = appendScore(results);
328 | expect(actual).toEqual({
329 | // eslint-disable-next-line no-multi-str
330 | text: 'NEW RECORD! Awesome work! Your new best score is 6 seconds per question.',
331 | newRecordInfo: RECORD_NEW,
332 | });
333 | expect(db.getScores).toHaveBeenCalled();
334 | expect(db.appendScore).toHaveBeenCalled();
335 | });
336 |
337 | test('should report a slower result', () => {
338 | db.getScores.mockReturnValueOnce([{
339 | timePerQuestion: 5,
340 | key: '0-0',
341 | }]);
342 | const { appendScore } = helper;
343 | const results = {
344 | correctCount: 10,
345 | levelIndex: 0,
346 | minutes: 10, // in minutes
347 | opIndexes: [0],
348 | previousResults: [], // TODO: A test where this is undefined
349 | questionsRemaining: 0,
350 | timeLeft: 540, // in seconds
351 | totalProblems: 10,
352 | };
353 |
354 | const actual = appendScore(results);
355 | expect(actual).toEqual({
356 | // eslint-disable-next-line no-multi-str
357 | text: 'You answered the questions at a rate of 6 seconds \
358 | per question. (Your best score is 5 seconds per question.)',
359 | newRecordInfo: RECORD_MISS,
360 | });
361 | expect(db.getScores).toHaveBeenCalled();
362 | expect(db.appendScore).toHaveBeenCalled();
363 | });
364 |
365 | describe('getScoreboard', () => {
366 | test('should get null, false and 0 for no scores', () => {
367 | db.getScores.mockReturnValueOnce(undefined);
368 | const expected = {
369 | levels: [],
370 | ops: new Set(),
371 | totals: fillArray(4, 0),
372 | };
373 | const actual = getScoreboard();
374 |
375 | expect(actual).toEqual(expected);
376 | });
377 |
378 | test('should get values for one score', () => {
379 | db.getScores.mockReturnValueOnce([{
380 | levelIndex: 0,
381 | correctCount: 10,
382 | opIndexes: [0],
383 | timePerQuestion: 6,
384 | }]);
385 |
386 | const expected = {
387 | levels: [{ 0: [0, 0, 0, 1] }], // Blue badge for Level A
388 | ops: new Set(['0']), // Operation 0 - addition
389 | totals: [0, 0, 0, 1], // One blue (index 3) badge in total
390 | };
391 |
392 | const actual = getScoreboard();
393 |
394 | expect(actual).toEqual(expected);
395 | });
396 |
397 | test('should get values for mixed operators', () => {
398 | db.getScores.mockReturnValueOnce([{
399 | levelIndex: 0,
400 | correctCount: 10,
401 | opIndexes: [0, 1],
402 | timePerQuestion: 6,
403 | }]);
404 |
405 | const expected = {
406 | levels: [{ '01': [0, 0, 0, 1] }], // Blue badge for Level A
407 | ops: new Set(['01']), // Operations 0 & 1 - addition/subtraction
408 | totals: [0, 0, 0, 1], // One blue (index 3) badge in total
409 | };
410 |
411 | const actual = getScoreboard();
412 |
413 | expect(actual).toEqual(expected);
414 | });
415 |
416 | test('should get values for multiple operations', () => {
417 | db.getScores.mockReturnValueOnce([{
418 | levelIndex: 0,
419 | correctCount: 10,
420 | opIndexes: [0],
421 | timePerQuestion: 6,
422 | }, {
423 | levelIndex: 0,
424 | correctCount: 10,
425 | opIndexes: [1],
426 | timePerQuestion: 6,
427 | }]);
428 |
429 | const expected = {
430 | levels: [{ 0: [0, 0, 0, 1], 1: [0, 0, 0, 1] }], // Blue badge for Level A
431 | ops: new Set(['0', '1']), // Operations 0 & 1 - addition & subtraction
432 | totals: [0, 0, 0, 2], // One blue (index 3) badge in total
433 | };
434 |
435 | const actual = getScoreboard();
436 |
437 | expect(actual).toEqual(expected);
438 | });
439 |
440 | test('should get null, false and 0 for correct answers under 10', () => {
441 | db.getScores.mockReturnValueOnce([{
442 | levelIndex: 0,
443 | correctCount: 9,
444 | opIndexes: [0],
445 | timePerQuestion: 6,
446 | }]);
447 |
448 | const expected = {
449 | levels: [],
450 | ops: new Set(),
451 | totals: fillArray(4, 0),
452 | };
453 |
454 | const actual = getScoreboard();
455 |
456 | expect(actual).toEqual(expected);
457 | });
458 | });
459 |
460 | describe('newRecord()', () => {
461 | test('should return false and NaN if no data', () => {
462 | const { newRecord } = helper;
463 | const input = {
464 | currentRecord: {},
465 | previousResults: [],
466 | startTime: 0,
467 | };
468 | const expected = {
469 | isNewRecord: false,
470 | currentTimePerQuestion: NaN,
471 | existingRecordTimePerQuestion: NaN,
472 | };
473 | const actual = newRecord(input);
474 |
475 | expect(actual).toEqual(expected);
476 | });
477 |
478 | test('should return false and NaN if current record missing', () => {
479 | const { newRecord } = helper;
480 | const input = {
481 | previousResults: [],
482 | startTime: 0,
483 | };
484 | const expected = {
485 | isNewRecord: false,
486 | currentTimePerQuestion: NaN,
487 | existingRecordTimePerQuestion: NaN,
488 | };
489 | const actual = newRecord(input);
490 |
491 | expect(actual).toEqual(expected);
492 | });
493 |
494 | test('should return valid results with valid input', () => {
495 | // 2017-01-01 01:01:01
496 | const start = new Date(2017, 0, 1, 1, 1, 1);
497 | // 2017-01-01 01:01:40
498 | const end = new Date(2017, 0, 1, 1, 1, 40);
499 | const startTime = moment(start);
500 | Date.now = jest.fn(() => end.valueOf());
501 | const { newRecord } = helper;
502 | const input = {
503 | currentRecord: {
504 | timePerQuestion: 6,
505 | },
506 | previousResults: [{
507 | task: [0, 0, 0, 55],
508 | actuals: [55],
509 | }],
510 | startTime,
511 | };
512 | const expected = {
513 | isNewRecord: false,
514 | currentTimePerQuestion: 39,
515 | existingRecordTimePerQuestion: 6,
516 | };
517 | const actual = newRecord(input);
518 |
519 | expect(actual).toEqual(expected);
520 | Date.now.mockClear();
521 | });
522 | });
523 | });
524 |
--------------------------------------------------------------------------------