From 2ff4fc3dcfc946ae2346a4055d2faa92efee46c9 Mon Sep 17 00:00:00 2001 From: Jordan Hisel Date: Wed, 6 Oct 2021 19:38:29 -0700 Subject: [PATCH 1/2] beefed up testing suite with mutually recursive components, redux connections, integration tests Co-authored-by: Charles Gutwirth Co-authored-by: Lindsay Baird Co-authored-by: Paul Coster --- sapling/package.json | 3 +- sapling/src/test/suite/extension.test.ts | 37 +++- sapling/src/test/suite/parser.test.ts | 79 ++++++- sapling/src/test/test_apps/test_2/index.js | 2 + sapling/src/test/test_apps/test_3/App.jsx | 21 ++ .../test/test_apps/test_3/actions/actions.js | 10 + .../test/test_apps/test_3/components/App.jsx | 55 ----- .../test_3/components/DrillCreator.jsx | 204 ----------------- .../test_3/components/ExerciseCreator.jsx | 208 ------------------ .../test_3/components/ExercisesDisplay.jsx | 42 ---- .../test_3/components/HistoryDisplay.jsx | 40 ---- .../test_apps/test_3/components/Login.jsx | 115 ---------- .../test_apps/test_3/components/Logout.jsx | 11 - .../test/test_apps/test_3/components/Nav.jsx | 40 ---- .../test_apps/test_3/components/Signup.jsx | 129 ----------- .../test_apps/test_3/constants/actionTypes.js | 3 + .../test_3/containers/ConnectedContainer.jsx | 28 +++ .../containers/UnconnectedContainer.jsx | 13 ++ sapling/src/test/test_apps/test_3/index.html | 18 ++ sapling/src/test/test_apps/test_3/index.js | 13 ++ sapling/src/test/test_apps/test_3/index.jsx | 18 -- .../test_apps/test_3/reducers/fakeReducer.js | 27 +++ .../test/test_apps/test_3/reducers/index.js | 6 + sapling/src/test/test_apps/test_3/store.js | 12 + .../test/test_apps/test_8/components/App.jsx | 3 +- .../test/test_apps/test_9/components/App.jsx | 7 +- sapling/src/test/test_apps/test_9/index.js | 2 +- 27 files changed, 260 insertions(+), 886 deletions(-) create mode 100644 sapling/src/test/test_apps/test_3/App.jsx create mode 100644 sapling/src/test/test_apps/test_3/actions/actions.js delete mode 100644 sapling/src/test/test_apps/test_3/components/App.jsx delete mode 100644 sapling/src/test/test_apps/test_3/components/DrillCreator.jsx delete mode 100644 sapling/src/test/test_apps/test_3/components/ExerciseCreator.jsx delete mode 100644 sapling/src/test/test_apps/test_3/components/ExercisesDisplay.jsx delete mode 100644 sapling/src/test/test_apps/test_3/components/HistoryDisplay.jsx delete mode 100644 sapling/src/test/test_apps/test_3/components/Login.jsx delete mode 100644 sapling/src/test/test_apps/test_3/components/Logout.jsx delete mode 100644 sapling/src/test/test_apps/test_3/components/Nav.jsx delete mode 100644 sapling/src/test/test_apps/test_3/components/Signup.jsx create mode 100644 sapling/src/test/test_apps/test_3/constants/actionTypes.js create mode 100644 sapling/src/test/test_apps/test_3/containers/ConnectedContainer.jsx create mode 100644 sapling/src/test/test_apps/test_3/containers/UnconnectedContainer.jsx create mode 100644 sapling/src/test/test_apps/test_3/index.html create mode 100644 sapling/src/test/test_apps/test_3/index.js delete mode 100644 sapling/src/test/test_apps/test_3/index.jsx create mode 100644 sapling/src/test/test_apps/test_3/reducers/fakeReducer.js create mode 100644 sapling/src/test/test_apps/test_3/reducers/index.js create mode 100644 sapling/src/test/test_apps/test_3/store.js diff --git a/sapling/package.json b/sapling/package.json index ed179fe..39329ff 100644 --- a/sapling/package.json +++ b/sapling/package.json @@ -10,8 +10,9 @@ "vscode": "^1.60.0" }, "categories": [ - "Other" + "Visualization" ], + "keywords": ["react", "component hierarchy", "devtools"], "activationEvents": [ "onStartupFinished" ], diff --git a/sapling/src/test/suite/extension.test.ts b/sapling/src/test/suite/extension.test.ts index f6e1453..6070477 100644 --- a/sapling/src/test/suite/extension.test.ts +++ b/sapling/src/test/suite/extension.test.ts @@ -1,4 +1,5 @@ -import * as assert from 'assert'; +import { describe, suite , test, before} from 'mocha'; +import { expect } from 'chai'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it @@ -8,13 +9,33 @@ import * as vscode from 'vscode'; suite('Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); + describe('Sapling loads correctly', () => { + let saplingExtension; + before (() => { + saplingExtension = vscode.extensions.getExtension('team-sapling.sapling'); + }); - test('Sample test 2', () => { - assert.strictEqual(1, 1); - }); + test('Sapling is registered as an extension', () => { + expect(saplingExtension).to.not.be.undefined; + }); + test('Sapling extension is activated after VSCode startup', () => { + expect(saplingExtension.isActive).to.be.true; + }); + + test('Sapling extension package.json exists', () => { + expect(saplingExtension.packageJSON).to.not.be.undefined; + }); + }); + + describe('It registers saplings commands successfully', () => { + let commandList; + before( async () => { + commandList = await vscode.commands.getCommands(); + }); + + test('It registers the sapling.generateTree command', () => { + expect(commandList).to.be.an('array').that.does.include('sapling.generateTree'); + }); + }); }); diff --git a/sapling/src/test/suite/parser.test.ts b/sapling/src/test/suite/parser.test.ts index 4917beb..adbb025 100644 --- a/sapling/src/test/suite/parser.test.ts +++ b/sapling/src/test/suite/parser.test.ts @@ -83,10 +83,12 @@ suite('Parser Test Suite', () => { tree = parser.parse(); }); - test('Should parse destructured imports', () => { - expect(tree.children).to.have.lengthOf(2); + test('Should parse destructured and third party imports', () => { + expect(tree.children).to.have.lengthOf(3); expect(tree.children[0]).to.have.own.property('name').that.is.oneOf(['Switch', 'Route']); expect(tree.children[1]).to.have.own.property('name').that.is.oneOf(['Switch', 'Route']); + expect(tree.children[2]).to.have.own.property('name').that.is.equal('Tippy'); + }); test('reactRouter should be designated as third party and reactRouter', () => { @@ -97,10 +99,28 @@ suite('Parser Test Suite', () => { expect(tree.children[1]).to.have.own.property('reactRouter').to.be.true; }); - //test for third party without reactRouter + test('Tippy should be designated as third party and not reactRouter', () => { + expect(tree.children[2]).to.have.own.property('thirdParty').to.be.true; + expect(tree.children[2]).to.have.own.property('reactRouter').to.be.false; + }); }); - // TEST 3: WOBBEGAINZ + // TEST 3: IDENTIFIES REDUX STORE CONNECTION + describe('It identifies a Redux store connection and designates the component as such', () => { + before(() => { + file = path.join(__dirname, '../../../src/test/test_apps/test_3/index.js'); + parser = new SaplingParser(file); + tree = parser.parse(); + }); + + test('The reduxConnect properties of the connected component and the unconnected component should be true and false, respectively', () => { + expect(tree.children[1].children[0].name).to.equal('ConnectedContainer'); + expect(tree.children[1].children[0]).to.have.own.property('reduxConnect').that.is.true; + + expect(tree.children[1].children[1].name).to.equal('UnconnectedContainer'); + expect(tree.children[1].children[1]).to.have.own.property('reduxConnect').that.is.false; + }); + }); // TEST 4: ALIASED IMPORTS describe('It works for aliases', () => { @@ -156,7 +176,7 @@ suite('Parser Test Suite', () => { }); }); - // TEST 6: Bad import of App2 from App1 Component + // TEST 6: BAD IMPORT OF APP2 FROM APP1 COMPONENT describe('It works for badly imported children nodes', () => { before(() => { file = path.join(__dirname, '../../../src/test/test_apps/test_6/index.js'); @@ -170,7 +190,7 @@ suite('Parser Test Suite', () => { }); }); - // TEST 7: Syntax error in app file causes parser error + // TEST 7: SYNTAX ERROR IN APP FILE CAUSES PARSER ERROR describe('It should log an error when the parser encounters a javascript syntax error', () => { before(() => { file = path.join(__dirname, '../../../src/test/test_apps/test_7/index.js'); @@ -185,7 +205,7 @@ suite('Parser Test Suite', () => { }); }); - // Test 8: Props check + // TEST 8: MULTIPLE PROPS ON ONE COMPONENT describe('It should properly count repeat components and consolidate and grab their props', () => { before(() => { file = path.join(__dirname, '../../../src/test/test_apps/test_8/index.js'); @@ -193,6 +213,25 @@ suite('Parser Test Suite', () => { tree = parser.parse(); }); + test('Grandchild should have a count of 1', () => { + expect(tree.children[0].children[0]).to.have.own.property('count').that.equals(1); + }); + + test('Grandchild should have the correct three props', () => { + expect(tree.children[0].children[0].props).has.own.property('prop1').that.is.true; + expect(tree.children[0].children[0].props).has.own.property('prop2').that.is.true; + expect(tree.children[0].children[0].props).has.own.property('prop3').that.is.true; + }); + }); + + // TEST 9: FINDING DIFFERENT PROPS ACROSS TWO OR MORE IDENTICAL COMPONENTS + describe('It should properly count repeat components and consolidate and grab their props', () => { + before(() => { + file = path.join(__dirname, '../../../src/test/test_apps/test_9/index.js'); + parser = new SaplingParser(file); + tree = parser.parse(); + }); + test('Grandchild should have a count of 2', () => { expect(tree.children[0].children[0]).to.have.own.property('count').that.equals(2); }); @@ -203,7 +242,7 @@ suite('Parser Test Suite', () => { }); }); - // Test 10: check children works and component works + // TEST 10: CHECK CHILDREN WORKS AND COMPONENTS WORK describe('It should render children when children are rendered as values of prop called component', () => { before(() => { file = path.join(__dirname, '../../../src/test/test_apps/test_10/index.jsx'); @@ -219,4 +258,28 @@ suite('Parser Test Suite', () => { expect(tree.children[1].children[4]).to.have.own.property('name').that.is.equal('HistoryDisplay'); }); }); + + // TEST 11: PARSER DOESN'T BREAK UPON RECURSIVE COMPONENTS + describe('It should render the second call of mutually recursive components, but no further', () => { + before(() => { + file = path.join(__dirname, '../../../src/test/test_apps/test_11/index.js'); + parser = new SaplingParser(file); + tree = parser.parse(); + }); + + test('Tree should not be undefined', () => { + expect(tree).to.not.be.undefined; + }); + + test('Tree should have an index component while child App1, grandchild App2, great-grandchild App1', () => { + expect(tree).to.have.own.property('name').that.is.equal('index'); + expect(tree.children).to.have.lengthOf(1); + expect(tree.children[0]).to.have.own.property('name').that.is.equal('App1'); + expect(tree.children[0].children).to.have.lengthOf(1); + expect(tree.children[0].children[0]).to.have.own.property('name').that.is.equal('App2'); + expect(tree.children[0].children[0].children).to.have.lengthOf(1); + expect(tree.children[0].children[0].children[0]).to.have.own.property('name').that.is.equal('App1'); + expect(tree.children[0].children[0].children[0].children).to.have.lengthOf(0); + }); + }); }); \ No newline at end of file diff --git a/sapling/src/test/test_apps/test_2/index.js b/sapling/src/test/test_apps/test_2/index.js index 41e4698..48e6afa 100644 --- a/sapling/src/test/test_apps/test_2/index.js +++ b/sapling/src/test/test_apps/test_2/index.js @@ -1,6 +1,7 @@ import React from 'react'; import { render } from 'react-dom'; import { Switch, Route } from 'react-router-dom'; +import Tippy from 'tippy'; // TEST 2 - Third Party Components, Destructuring Import @@ -9,5 +10,6 @@ render( + , document.getElementById('root')); diff --git a/sapling/src/test/test_apps/test_3/App.jsx b/sapling/src/test/test_apps/test_3/App.jsx new file mode 100644 index 0000000..f02cad8 --- /dev/null +++ b/sapling/src/test/test_apps/test_3/App.jsx @@ -0,0 +1,21 @@ +import React, { Component } from 'react'; +import ConnectedContainer from './containers/ConnectedContainer' +import UnconnectedContainer from './containers/UnconnectedContainer' + + +class App extends Component { + constructor(props) { + super(props); + } + + render() { + return ( +
+ + +
+ ); + } +} + +export default App; diff --git a/sapling/src/test/test_apps/test_3/actions/actions.js b/sapling/src/test/test_apps/test_3/actions/actions.js new file mode 100644 index 0000000..c235427 --- /dev/null +++ b/sapling/src/test/test_apps/test_3/actions/actions.js @@ -0,0 +1,10 @@ +import * as types from '../constants/actionTypes'; + +export const fakeAction1Creator = () => ({ + type: types.FAKE_ACTION_1, +}); + +export const fakeAction2Creator = () => ({ + type: types.FAKE_ACTION_2, +}); + diff --git a/sapling/src/test/test_apps/test_3/components/App.jsx b/sapling/src/test/test_apps/test_3/components/App.jsx deleted file mode 100644 index eb0feb6..0000000 --- a/sapling/src/test/test_apps/test_3/components/App.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { useState } from 'react'; -import { Switch, Route, Redirect } from 'react-router-dom'; - -// Import React Components -import Nav from './Nav.jsx'; -import ExercisesDisplay from './ExercisesDisplay.jsx'; -import ExerciseCreator from './ExerciseCreator.jsx'; -import DrillCreator from './DrillCreator.jsx'; -import HistoryDisplay from './HistoryDisplay.jsx'; -import Signup from './Signup.jsx'; -import Login from './Login.jsx'; -import Logout from './Logout.jsx'; - -// App Component -const App = () => { - const [userInfo, setUserInfo] = useState({ name: '', email: '' }); - - return ( -
-
- ); -}; - -export default App; diff --git a/sapling/src/test/test_apps/test_3/components/DrillCreator.jsx b/sapling/src/test/test_apps/test_3/components/DrillCreator.jsx deleted file mode 100644 index 3955655..0000000 --- a/sapling/src/test/test_apps/test_3/components/DrillCreator.jsx +++ /dev/null @@ -1,204 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useParams, Link, Redirect } from 'react-router-dom'; - -const DrillCreator = () => { - const { id } = useParams(); - const [drillData, setDrillData] = useState({}); - const [redirect, setRedirect] = useState(false); - const [formVals, setFormVals] = useState({ - exercise_id: id, - weight: '', - sets: '', - reps: '', - rest_interval: '', - }); - - // Helper function to update state formVals on form change - const updateFormVal = (key, val) => { - setFormVals({ ...formVals, [key]: val }); - }; - - // TODO MAKE REAL API CALL OR LIFT STATE TO APP - // Is there a route for creating a drill? I only see createExercise - const getExercise = () => { - fetch(`/api/exercise/${id}`) - .then((response) => { - if (response.status === 200) { - return response.json(); - } - throw new Error('Error when trying to get exercise details'); - }) - .then((data) => { - console.log('exercise drill data is', data); - setDrillData(data); - }) - .catch((error) => console.error(error)); - }; - - // Get exercise data for drill info (CURRENTLY FAKE DATA) - useEffect(() => { - console.log('Getting data from server for drill'); - getExercise(); - }, []); - - // Function to submit drill form data to server, create new drill - const createDrill = () => { - console.log('trying to create new drill', formVals); - - fetch('/api/drill', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formVals), - }) - .then((response) => { - console.log('drill create response', response.status); - if (response.status === 201) { - return response.json(); - } - throw new Error('error when trying to create a drill'); - }) - .then((data) => { - console.log('response is 201, data is', data); - setRedirect(true); - }) - .catch((error) => console.error(error)); - }; - - const { weight, sets, reps, rest_interval } = formVals; - - // Redirect to home page if drill created successfully - if (redirect === true) { - return ; - } - - return ( -
-

Create a new drill:

-

- Exercise Name: {drillData.name}

-

- Exercise Description: {drillData.description}

-

- Exercise Type: {drillData.type}

-

- Last Weight (LBs): {drillData.last_weight}

-

- Last Reps: {drillData.last_reps}

-

- Last Sets: {drillData.last_sets}

-

- Last Rest (Mins): {drillData.last_rest} -

- - {/* DRILL INPUT FORM */} -
{ - e.preventDefault(); - createDrill(); - }} - > - - {/* DRILL WEIGHT INPUT */} - -
- - {/* DRILL SETS INPUT */} - -
- - {/* DRILL REPS INPUT */} - -
- - {/*DRILL REST INPUT */} - -
- - {/* FORM SUBMIT BUTTON */} - - - {/* FORM CANCEL BUTTON */} - - - - -
-
- ); -}; - -export default DrillCreator; diff --git a/sapling/src/test/test_apps/test_3/components/ExerciseCreator.jsx b/sapling/src/test/test_apps/test_3/components/ExerciseCreator.jsx deleted file mode 100644 index 36a6a8e..0000000 --- a/sapling/src/test/test_apps/test_3/components/ExerciseCreator.jsx +++ /dev/null @@ -1,208 +0,0 @@ -import React, { useState } from 'react'; -import { Link, Redirect } from 'react-router-dom'; - -// React element allowing users to create a new exercise via form -const ExerciseCreator = () => { - const [redirect, setRedirect] = useState(false); - const [formVals, setFormVals] = useState({ - name: '', - description: '', - type_id: '1', - init_weight: '', - init_reps: '', - init_sets: '', - init_rest: '', - }); - - // Helper function to update state formVals on form change - const updateFormVal = (key, val) => { - setFormVals({ ...formVals, [key]: val }); - }; - - // Function to submit new exercise form data to server for processing - const createExercise = () => { - console.log('Trying to create exercise: ', formVals); - fetch('/api/exercise', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formVals), - }) - .then((response) => { - // If creation successful, redirect to exercises - console.log('CREATE RESPONSE: ', response.status); - if (response.status === 200) { - return response.json(); - } - throw new Error('Error when trying to login a user!'); - }).then((data) => { - console.log('Added new exercise: ', data); - setRedirect(true); - }) - .catch((err) => console.error(err)); - }; - - const { - name, description, type, init_weight, init_reps, init_sets, init_rest, - } = formVals; - - // If successfully created new exercise, redirect to '/' route: - if (redirect) { - return ; - } - - return ( -
-

Create a new Exercise:

- - {/* NEW EXERCISE FORM */} -
{ - e.preventDefault(); - createExercise(); - }} - > - - {/* EXERCISE NAME INPUT */} - - { - console.log('Updated createEx formVals: ', e.target.value); - updateFormVal('name', e.target.value); - }} - value={name} - name="name" - required - /> -
- - {/* EXERCISE TYPE INPUT */} - - -
- - {/* EXERCISE DESCRIPTION INPUT */} - -