diff --git a/curriculum/addAssertsToTapTest.js b/curriculum/addAssertsToTapTest.js index 58fb62a3e1..610492185b 100644 --- a/curriculum/addAssertsToTapTest.js +++ b/curriculum/addAssertsToTapTest.js @@ -12,11 +12,13 @@ function createIsAssert(tapTest, isThing) { function addAssertsToTapTest(tapTest) { const assert = tapTest.assert; + assert.isTrue = createIsAssert(tapTest, v => v === true); assert.isArray = createIsAssert(tapTest, _.isArray); assert.isBoolean = createIsAssert(tapTest, _.isBoolean); assert.isString = createIsAssert(tapTest, _.isString); assert.isNumber = createIsAssert(tapTest, _.isNumber); assert.isUndefined = createIsAssert(tapTest, _.isUndefined); + assert.isNaN = createIsAssert(tapTest, _.isNaN); assert.deepEqual = tapTest.deepEqual; assert.equal = tapTest.equal; diff --git a/curriculum/challengeTitles.js b/curriculum/challengeTitles.js index a43e144972..2f568fb1c4 100644 --- a/curriculum/challengeTitles.js +++ b/curriculum/challengeTitles.js @@ -1,16 +1,17 @@ -import _ from 'lodash'; - class ChallengeTitles { constructor() { this.knownTitles = []; } check(title) { if (typeof title !== 'string') { - throw new Error(`Expected a valid string for ${title}, but got a(n) ${typeof title}`); - } else if (title.length === 0) { - throw new Error(`Expected a title length greater than 0`); + throw new Error( + `Expected a valid string for ${title}, but got a(n) ${typeof title}` + ); + } + const titleToCheck = title.replace(/\s+/g, '').toLowerCase(); + if (titleToCheck.length === 0) { + throw new Error('Expected a title length greater than 0'); } - const titleToCheck = title.toLowerCase().replace(/\s+/g, ''); const isKnown = this.knownTitles.includes(titleToCheck); if (isKnown) { throw new Error(` @@ -23,4 +24,4 @@ class ChallengeTitles { } } -export default ChallengeTitles; +module.exports = ChallengeTitles; diff --git a/curriculum/challenges/english/01-responsive-web-design/applied-visual-design/adjust-the-width-of-an-element-using-the-width-property.english.md b/curriculum/challenges/english/01-responsive-web-design/applied-visual-design/adjust-the-width-of-an-element-using-the-width-property.english.md index bc769f110a..5bbfc0997d 100644 --- a/curriculum/challenges/english/01-responsive-web-design/applied-visual-design/adjust-the-width-of-an-element-using-the-width-property.english.md +++ b/curriculum/challenges/english/01-responsive-web-design/applied-visual-design/adjust-the-width-of-an-element-using-the-width-property.english.md @@ -79,9 +79,41 @@ tests: ## Solution
- -```js -var code = ".fullCard {\nwidth: 245px; border: 1px solid #ccc; border-radius: 5px; margin: 10px 5px; padding: 4px;}" +```html + +
+
+
+

Google

+

Google was founded by Larry Page and Sergey Brin while they were Ph.D. students at Stanford University.

+
+ +
+
```
diff --git a/curriculum/challenges/english/01-responsive-web-design/applied-visual-design/create-a-more-complex-shape-using-css-and-html.english.md b/curriculum/challenges/english/01-responsive-web-design/applied-visual-design/create-a-more-complex-shape-using-css-and-html.english.md index f3d66dbbc3..288b6ed41e 100644 --- a/curriculum/challenges/english/01-responsive-web-design/applied-visual-design/create-a-more-complex-shape-using-css-and-html.english.md +++ b/curriculum/challenges/english/01-responsive-web-design/applied-visual-design/create-a-more-complex-shape-using-css-and-html.english.md @@ -90,9 +90,42 @@ tests: ## Solution
- -```js -var code = ".heart {transform: rotate(-45deg);} .heart::after {background-color: pink; border-radius: 50%;} .heart::before {content: \"\"; border-radius: 50%;}" +```html + +
```
diff --git a/curriculum/challenges/english/01-responsive-web-design/basic-css/add-a-negative-margin-to-an-element.english.md b/curriculum/challenges/english/01-responsive-web-design/basic-css/add-a-negative-margin-to-an-element.english.md index 0a9b2b9b92..53e40126d3 100644 --- a/curriculum/challenges/english/01-responsive-web-design/basic-css/add-a-negative-margin-to-an-element.english.md +++ b/curriculum/challenges/english/01-responsive-web-design/basic-css/add-a-negative-margin-to-an-element.english.md @@ -84,7 +84,44 @@ tests: ## Solution
-```js -// solution required +```html + + +
+
padding
+
padding
+
```
diff --git a/curriculum/challenges/english/01-responsive-web-design/basic-html-and-html5/say-hello-to-html-elements.english.md b/curriculum/challenges/english/01-responsive-web-design/basic-html-and-html5/say-hello-to-html-elements.english.md index 3842460f24..a92667a69e 100644 --- a/curriculum/challenges/english/01-responsive-web-design/basic-html-and-html5/say-hello-to-html-elements.english.md +++ b/curriculum/challenges/english/01-responsive-web-design/basic-html-and-html5/say-hello-to-html-elements.english.md @@ -54,7 +54,7 @@ tests: ## Solution
-```js -// solution required +```html +

Hello World

```
diff --git a/curriculum/challenges/english/01-responsive-web-design/css-grid/divide-the-grid-into-an-area-template.english.md b/curriculum/challenges/english/01-responsive-web-design/css-grid/divide-the-grid-into-an-area-template.english.md index 7877db6b4f..0fbf8c7e08 100644 --- a/curriculum/challenges/english/01-responsive-web-design/css-grid/divide-the-grid-into-an-area-template.english.md +++ b/curriculum/challenges/english/01-responsive-web-design/css-grid/divide-the-grid-into-an-area-template.english.md @@ -81,9 +81,38 @@ tests: ## Solution
+```html + + +
+
1
+
2
+
3
+
4
+
5
+
```
diff --git a/curriculum/challenges/english/01-responsive-web-design/css-grid/use-media-queries-to-create-responsive-layouts.english.md b/curriculum/challenges/english/01-responsive-web-design/css-grid/use-media-queries-to-create-responsive-layouts.english.md index cc23fb27a9..a53ef10b8b 100644 --- a/curriculum/challenges/english/01-responsive-web-design/css-grid/use-media-queries-to-create-responsive-layouts.english.md +++ b/curriculum/challenges/english/01-responsive-web-design/css-grid/use-media-queries-to-create-responsive-layouts.english.md @@ -113,9 +113,75 @@ tests: ## Solution
+```html + + +
+
header
+
advert
+
content
+
footer
+
```
diff --git a/curriculum/challenges/english/03-front-end-libraries/react-and-redux/getting-started-with-react-redux.english.md b/curriculum/challenges/english/03-front-end-libraries/react-and-redux/getting-started-with-react-redux.english.md index 5fb8d315f1..0ba5c0428f 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react-and-redux/getting-started-with-react-redux.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react-and-redux/getting-started-with-react-redux.english.md @@ -25,9 +25,9 @@ tests: - text: The DisplayMessages component should render an empty div element. testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); return mockedComponent.find('div').text() === '' })(), 'The DisplayMessages component should render an empty div element.'); - text: The DisplayMessages constructor should be called properly with super, passing in props. - testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g,'); return noWhiteSpace.includes('constructor(props)') && noWhiteSpace.includes('super(props'); })(), 'The DisplayMessages constructor should be called properly with super, passing in props.'); + testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g,''); return noWhiteSpace.includes('constructor(props)') && noWhiteSpace.includes('super(props'); })(), 'The DisplayMessages constructor should be called properly with super, passing in props.'); - text: 'The DisplayMessages component should have an initial state equal to {input: "", messages: []}.' - testString: 'assert((function() { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); const initialState = mockedComponent.state(); return typeof initialState === ''object'' && initialState.input === '''' && Array.isArray(initialState.messages) && initialState.messages.length === 0; })(), ''The DisplayMessages component should have an initial state equal to {input: "", messages: []}.'');' + testString: "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); const initialState = mockedComponent.state(); return typeof initialState === 'object' && initialState.input === '' && Array.isArray(initialState.messages) && initialState.messages.length === 0; })(), 'The DisplayMessages component should have an initial state equal to {input: \"\", messages: []}.');" ``` diff --git a/curriculum/challenges/english/03-front-end-libraries/react-and-redux/use-provider-to-connect-redux-to-react.english.md b/curriculum/challenges/english/03-front-end-libraries/react-and-redux/use-provider-to-connect-redux-to-react.english.md index e8cb5bbe21..8afecb47fe 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react-and-redux/use-provider-to-connect-redux-to-react.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react-and-redux/use-provider-to-connect-redux-to-react.english.md @@ -26,7 +26,7 @@ tests: - text: The AppWrapper should render. testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return mockedComponent.find('AppWrapper').length === 1; })(), 'The AppWrapper should render.'); - text: The Provider wrapper component should have a prop of store passed to it, equal to the Redux store. - testString: getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return getUserInput('index').replace(/\s/g,').includes(''); })(), 'The Provider wrapper component should have a prop of store passed to it, equal to the Redux store.'); + testString: getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return getUserInput('index').replace(/\s/g,'').includes(''); })(), 'The Provider wrapper component should have a prop of store passed to it, equal to the Redux store.'); - text: DisplayMessages should render as a child of AppWrapper. testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return mockedComponent.find('AppWrapper').find('DisplayMessages').length === 1; })(), 'DisplayMessages should render as a child of AppWrapper.'); - text: The DisplayMessages component should render an h2, input, button, and ul element. diff --git a/curriculum/challenges/english/03-front-end-libraries/react/override-default-props.english.md b/curriculum/challenges/english/03-front-end-libraries/react/override-default-props.english.md index 22b6c9b8e9..e19e51ee57 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/override-default-props.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/override-default-props.english.md @@ -25,8 +25,8 @@ tests: testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('ShoppingCart').length === 1; })(), 'The component ShoppingCart should render.'); - text: The component Items should render. testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('Items').length === 1; })(), 'The component Items should render.'); - - text: 'The Items component should have a prop of { quantity: 10 } passed from the ShoppingCart component.' - testString: 'getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find(''Items'').props().quantity == 10 && getUserInput(''index'').replace(/ /g,'').includes(''''); })(), ''The Items component should have a prop of { quantity: 10 } passed from the ShoppingCart component.'');' + - text: "The Items component should have a prop of { quantity: 10 } passed from the ShoppingCart component." + testString: "getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('Items').props().quantity == 10 && getUserInput('index').replace(/ /g,'').includes(''); })(), 'The Items component should have a prop of { quantity: 10 } passed from the ShoppingCart component.');" ``` diff --git a/curriculum/challenges/english/03-front-end-libraries/react/render-react-on-the-server-with-rendertostring.english.md b/curriculum/challenges/english/03-front-end-libraries/react/render-react-on-the-server-with-rendertostring.english.md index 4b8385d188..885482d0ac 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/render-react-on-the-server-with-rendertostring.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/render-react-on-the-server-with-rendertostring.english.md @@ -22,7 +22,7 @@ The renderToString() method is provided on ReactDOMServerApp component should render to a string using ReactDOMServer.renderToString. - testString: getUserInput => assert(getUserInput('index').replace(/ /g,').includes('ReactDOMServer.renderToString()') && Enzyme.mount(React.createElement(App)).children().name() === 'div', 'The App component should render to a string using ReactDOMServer.renderToString.'); + testString: getUserInput => assert(getUserInput('index').replace(/ /g,'').includes('ReactDOMServer.renderToString()') && Enzyme.mount(React.createElement(App)).children().name() === 'div', 'The App component should render to a string using ReactDOMServer.renderToString.'); ``` diff --git a/curriculum/challenges/english/03-front-end-libraries/react/review-using-props-with-stateless-functional-components.english.md b/curriculum/challenges/english/03-front-end-libraries/react/review-using-props-with-stateless-functional-components.english.md index eaa11f5a7f..fdf4016cf4 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/review-using-props-with-stateless-functional-components.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/review-using-props-with-stateless-functional-components.english.md @@ -27,9 +27,9 @@ tests: - text: The Camper component should render. testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(CampSite)); return mockedComponent.find('Camper').length === 1; })(), 'The Camper component should render.'); - text: The Camper component should include default props which assign the string CamperBot to the key name. - testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g, '); const verify1 = 'Camper.defaultProps={name:\'CamperBot\'}'; const verify2 = 'Camper.defaultProps={name:"CamperBot"}'; return (noWhiteSpace.includes(verify1) || noWhiteSpace.includes(verify2)); })(), 'The Camper component should include default props which assign the string CamperBot to the key name.'); + testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g, ''); const verify1 = 'Camper.defaultProps={name:\'CamperBot\'}'; const verify2 = 'Camper.defaultProps={name:"CamperBot"}'; return (noWhiteSpace.includes(verify1) || noWhiteSpace.includes(verify2)); })(), 'The Camper component should include default props which assign the string CamperBot to the key name.'); - text: The Camper component should include prop types which require the name prop to be of type string. - testString: getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(CampSite)); const noWhiteSpace = getUserInput('index').replace(/\s/g, '); const verifyDefaultProps = 'Camper.propTypes={name:PropTypes.string.isRequired}'; return noWhiteSpace.includes(verifyDefaultProps); })(), 'The Camper component should include prop types which require the name prop to be of type string.'); + testString: getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(CampSite)); const noWhiteSpace = getUserInput('index').replace(/\s/g, ''); const verifyDefaultProps = 'Camper.propTypes={name:PropTypes.string.isRequired}'; return noWhiteSpace.includes(verifyDefaultProps); })(), 'The Camper component should include prop types which require the name prop to be of type string.'); - text: The Camper component should contain a p element with only the text from the name prop. testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(CampSite)); return mockedComponent.find('p').text() === mockedComponent.find('Camper').props().name; })(), 'The Camper component should contain a p element with only the text from the name prop.'); diff --git a/curriculum/challenges/english/03-front-end-libraries/react/use-proptypes-to-define-the-props-you-expect.english.md b/curriculum/challenges/english/03-front-end-libraries/react/use-proptypes-to-define-the-props-you-expect.english.md index 5593f53ef6..152a9302de 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/use-proptypes-to-define-the-props-you-expect.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/use-proptypes-to-define-the-props-you-expect.english.md @@ -30,7 +30,7 @@ tests: - text: The Items component should render. testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('Items').length === 1; })(), 'The Items component should render.'); - text: The Items component should include a propTypes check that requires quantity to be a number. - testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/ /g, '); return noWhiteSpace.includes('quantity:PropTypes.number.isRequired') && noWhiteSpace.includes('Items.propTypes='); })(), 'The Items component should include a propTypes check that requires quantity to be a number.'); + testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/ /g, ''); return noWhiteSpace.includes('quantity:PropTypes.number.isRequired') && noWhiteSpace.includes('Items.propTypes='); })(), 'The Items component should include a propTypes check that requires quantity to be a number.'); ``` diff --git a/curriculum/challenges/english/03-front-end-libraries/react/write-a-react-component-from-scratch.english.md b/curriculum/challenges/english/03-front-end-libraries/react/write-a-react-component-from-scratch.english.md index a61c0f46be..ebef357540 100644 --- a/curriculum/challenges/english/03-front-end-libraries/react/write-a-react-component-from-scratch.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/react/write-a-react-component-from-scratch.english.md @@ -22,7 +22,7 @@ Render this component to the DOM using ReactDOM.render(). There is ```yml tests: - text: There should be a React component called MyComponent. - testString: getUserInput => assert(getUserInput('index').replace(/\s/g, ').includes('classMyComponentextendsReact.Component{'), 'There should be a React component called MyComponent.'); + testString: getUserInput => assert(getUserInput('index').replace(/\s/g, '').includes('classMyComponentextendsReact.Component{'), 'There should be a React component called MyComponent.'); - text: MyComponent should contain an h1 tag with text My First React Component! Case and punctuation matter. testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return mockedComponent.find('h1').text() === 'My First React Component!'; })(), 'MyComponent should contain an h1 tag with text My First React Component! Case and punctuation matter.'); - text: MyComponent should render to the DOM. diff --git a/curriculum/challenges/english/03-front-end-libraries/redux/combine-multiple-reducers.english.md b/curriculum/challenges/english/03-front-end-libraries/redux/combine-multiple-reducers.english.md index 636b621578..bce57794b2 100644 --- a/curriculum/challenges/english/03-front-end-libraries/redux/combine-multiple-reducers.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/redux/combine-multiple-reducers.english.md @@ -29,9 +29,9 @@ tests: - text: The authReducer should toggle the state of authenticated between true and false. testString: 'assert((function() { store.dispatch({type: LOGIN}); const loggedIn = store.getState().auth.authenticated; store.dispatch({type: LOGOUT}); const loggedOut = store.getState().auth.authenticated; return loggedIn === true && loggedOut === false })(), ''The authReducer should toggle the state of authenticated between true and false.'');' - text: 'The store state should have two keys: count, which holds a number, and auth, which holds an object. The auth object should have a property of authenticated, which holds a boolean.' - testString: 'assert((function() { const state = store.getState(); return typeof state.auth === ''object'' && typeof state.auth.authenticated === ''boolean'' && typeof state.count === ''number'' })(), ''The store state should have two keys: count, which holds a number, and auth, which holds an object. The auth object should have a property of authenticated, which holds a boolean.'');' + testString: "assert((function() { const state = store.getState(); return typeof state.auth === 'object' && typeof state.auth.authenticated === 'boolean' && typeof state.count === 'number' })(), 'The store state should have two keys: count, which holds a number, and auth, which holds an object. The auth object should have a property of authenticated, which holds a boolean.');" - text: The rootReducer should be a function that combines the counterReducer and the authReducer. - testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g,'); return typeof rootReducer === 'function' && noWhiteSpace.includes('Redux.combineReducers') })(), 'The rootReducer should be a function that combines the counterReducer and the authReducer.'); + testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g,''); return typeof rootReducer === 'function' && noWhiteSpace.includes('Redux.combineReducers') })(), 'The rootReducer should be a function that combines the counterReducer and the authReducer.'); ``` diff --git a/curriculum/challenges/english/03-front-end-libraries/redux/dispatch-an-action-event.english.md b/curriculum/challenges/english/03-front-end-libraries/redux/dispatch-an-action-event.english.md index efe04a2e5e..fca66d9d03 100644 --- a/curriculum/challenges/english/03-front-end-libraries/redux/dispatch-an-action-event.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/redux/dispatch-an-action-event.english.md @@ -27,7 +27,7 @@ tests: - text: The store should be initialized with an object with property login set to false. testString: assert(store.getState().login === false, 'The store should be initialized with an object with property login set to false.'); - text: The store.dispatch() method should be used to dispatch an action of type LOGIN. - testString: 'getUserInput => assert((function() { let noWhiteSpace = getUserInput(''index'').replace(/\s/g,''); return noWhiteSpace.includes(''store.dispatch(loginAction())'') || noWhiteSpace.includes(''store.dispatch({type: \''LOGIN\''})'') === true })(), ''The store.dispatch() method should be used to dispatch an action of type LOGIN.'');' + testString: "getUserInput => assert((function() { let noWhiteSpace = getUserInput('index').replace(/\\s/g,''); return noWhiteSpace.includes('store.dispatch(loginAction())') || noWhiteSpace.includes('store.dispatch({type: \\'LOGIN\\'})') === true })(), 'The store.dispatch() method should be used to dispatch an action of type LOGIN.');" ``` diff --git a/curriculum/challenges/english/03-front-end-libraries/redux/use-const-for-action-types.english.md b/curriculum/challenges/english/03-front-end-libraries/redux/use-const-for-action-types.english.md index e0ff5c6561..bc3ff0c977 100644 --- a/curriculum/challenges/english/03-front-end-libraries/redux/use-const-for-action-types.english.md +++ b/curriculum/challenges/english/03-front-end-libraries/redux/use-const-for-action-types.english.md @@ -34,9 +34,9 @@ tests: - text: The authReducer function should handle multiple action types with a switch statement. testString: getUserInput => assert((function() { return typeof authReducer === 'function' && getUserInput('index').toString().includes('switch') && getUserInput('index').toString().includes('case') && getUserInput('index').toString().includes('default') })(), 'The authReducer function should handle multiple action types with a switch statement.'); - text: LOGIN and LOGOUT should be declared as const values and should be assigned strings of LOGINand LOGOUT. - testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').toString().replace(/\s/g,'); return (noWhiteSpace.includes('constLOGIN=\'LOGIN\') || noWhiteSpace.includes('constLOGIN="LOGIN"')) && (noWhiteSpace.includes('constLOGOUT=\'LOGOUT\') || noWhiteSpace.includes('constLOGOUT="LOGOUT"')) })(), 'LOGIN and LOGOUT should be declared as const values and should be assigned strings of LOGINand LOGOUT.'); + testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').toString().replace(/\s/g,''); return (noWhiteSpace.includes('constLOGIN=\'LOGIN\'') || noWhiteSpace.includes('constLOGIN="LOGIN"')) && (noWhiteSpace.includes('constLOGOUT=\'LOGOUT\'') || noWhiteSpace.includes('constLOGOUT="LOGOUT"')) })(), 'LOGIN and LOGOUT should be declared as const values and should be assigned strings of LOGINand LOGOUT.'); - text: The action creators and the reducer should reference the LOGIN and LOGOUT constants. - testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').toString().replace(/\s/g,'); return noWhiteSpace.includes('caseLOGIN:') && noWhiteSpace.includes('caseLOGOUT:') && noWhiteSpace.includes('type:LOGIN') && noWhiteSpace.includes('type:LOGOUT') })(), 'The action creators and the reducer should reference the LOGIN and LOGOUT constants.'); + testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').toString().replace(/\s/g,''); return noWhiteSpace.includes('caseLOGIN:') && noWhiteSpace.includes('caseLOGOUT:') && noWhiteSpace.includes('type:LOGIN') && noWhiteSpace.includes('type:LOGOUT') })(), 'The action creators and the reducer should reference the LOGIN and LOGOUT constants.'); ``` diff --git a/curriculum/challenges/english/06-information-security-and-quality-assurance/advanced-node-and-express/announce-new-users.english.md b/curriculum/challenges/english/06-information-security-and-quality-assurance/advanced-node-and-express/announce-new-users.english.md index 8e434b46e1..30491df9ab 100644 --- a/curriculum/challenges/english/06-information-security-and-quality-assurance/advanced-node-and-express/announce-new-users.english.md +++ b/curriculum/challenges/english/06-information-security-and-quality-assurance/advanced-node-and-express/announce-new-users.english.md @@ -36,7 +36,7 @@ tests: - text: Event 'user' is emitted with name, currentUsers, and connected testString: getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /io.emit.*('|")user('|").*name.*currentUsers.*connected/gi, 'You should have an event emitted named user sending name, currentUsers, and connected'); }, xhr => { throw new Error(xhr.statusText); }) - text: Client properly handling and displaying the new data from event 'user' - testString: getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|")user('|")[^]*num-users/gi, 'You should change the text of #num-users within on your client within the "user" even listener to show the current users connected'); assert.match(data, /socket.on.*('|")user('|")[^]*messages.*li/gi, 'You should append a list item to #messages on your client within the "user" event listener to annouce a user came or went'); }, xhr => { throw new Error(xhr.statusText); }) + testString: "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")user('|\")[^]*num-users/gi, 'You should change the text of #num-users within on your client within the \"user\" even listener to show the current users connected'); assert.match(data, /socket.on.*('|\")user('|\")[^]*messages.*li/gi, 'You should append a list item to #messages on your client within the \"user\" event listener to annouce a user came or went'); }, xhr => { throw new Error(xhr.statusText); })" ``` diff --git a/curriculum/challenges/english/06-information-security-and-quality-assurance/advanced-node-and-express/send-and-display-chat-messages.english.md b/curriculum/challenges/english/06-information-security-and-quality-assurance/advanced-node-and-express/send-and-display-chat-messages.english.md index f23017b443..209bbc41b9 100644 --- a/curriculum/challenges/english/06-information-security-and-quality-assurance/advanced-node-and-express/send-and-display-chat-messages.english.md +++ b/curriculum/challenges/english/06-information-security-and-quality-assurance/advanced-node-and-express/send-and-display-chat-messages.english.md @@ -27,7 +27,7 @@ tests: - text: Server listens for 'chat message' then emits it properly testString: getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /socket.on.*('|")chat message('|")[^]*io.emit.*('|")chat message('|").*name.*message/gi, 'Your server should listen to the socket for "chat message" then emit to all users "chat message" with name and message in the data object'); }, xhr => { throw new Error(xhr.statusText); }) - text: Client properly handling and displaying the new data from event 'chat message' - testString: getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|")chat message('|")[^]*messages.*li/gi, 'You should append a list item to #messages on your client within the "chat message" event listener to display the new message'); }, xhr => { throw new Error(xhr.statusText); }) + testString: "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")chat message('|\")[^]*messages.*li/gi, 'You should append a list item to #messages on your client within the \"chat message\" event listener to display the new message'); }, xhr => { throw new Error(xhr.statusText); })" ``` diff --git a/curriculum/challenges/english/08-coding-interview-prep/project-euler/problem-162-hexadecimal-numbers.english.md b/curriculum/challenges/english/08-coding-interview-prep/project-euler/problem-162-hexadecimal-numbers.english.md index bd2e9c751a..690b237e51 100644 --- a/curriculum/challenges/english/08-coding-interview-prep/project-euler/problem-162-hexadecimal-numbers.english.md +++ b/curriculum/challenges/english/08-coding-interview-prep/project-euler/problem-162-hexadecimal-numbers.english.md @@ -27,7 +27,7 @@ Give your answer as a hexadecimal number. ```yml tests: - text: euler162() should return 3D58725572C62302. - testString: assert.strictEqual(euler162(), 3D58725572C62302, 'euler162() should return 3D58725572C62302.'); + testString: assert.strictEqual(euler162(), '3D58725572C62302', 'euler162() should return 3D58725572C62302.'); ``` diff --git a/curriculum/challenges/english/08-coding-interview-prep/project-euler/problem-284-steady-squares.english.md b/curriculum/challenges/english/08-coding-interview-prep/project-euler/problem-284-steady-squares.english.md index 7e2b54ff63..798f0e1369 100644 --- a/curriculum/challenges/english/08-coding-interview-prep/project-euler/problem-284-steady-squares.english.md +++ b/curriculum/challenges/english/08-coding-interview-prep/project-euler/problem-284-steady-squares.english.md @@ -27,7 +27,7 @@ Find the sum of the digits of all the n-digit steady squares in the base 14 numb ```yml tests: - text: euler284() should return 5a411d7b. - testString: assert.strictEqual(euler284(), 5a411d7b, 'euler284() should return 5a411d7b.'); + testString: assert.strictEqual(euler284(), '5a411d7b', 'euler284() should return 5a411d7b.'); ``` diff --git a/curriculum/mongoIds.js b/curriculum/mongoIds.js index 034a0b48cf..03d96b578b 100644 --- a/curriculum/mongoIds.js +++ b/curriculum/mongoIds.js @@ -1,5 +1,5 @@ -import _ from 'lodash'; -import { isMongoId } from 'validator'; +const _ = require('lodash'); +const { isMongoId } = require('validator'); class MongoIds { constructor() { @@ -21,4 +21,4 @@ class MongoIds { } } -export default MongoIds; +module.exports = MongoIds; diff --git a/curriculum/package.json b/curriculum/package.json index 78773da345..f4b46b404c 100644 --- a/curriculum/package.json +++ b/curriculum/package.json @@ -21,17 +21,17 @@ "prepare": "npm run build", "repack": "babel-node ./repack.js", "semantic-release": "semantic-release", - "test": "babel-node ./test-challenges.js | tap-spec", + "test": "node ./test-challenges.js | tap-spec", "unpack": "babel-node ./unpack.js" }, "dependencies": { - "@freecodecamp/challenge-md-parser": "^1.0.0", "invariant": "^2.2.4" }, "devDependencies": { "@commitlint/cli": "^7.0.0", "@commitlint/config-conventional": "^7.0.1", "@commitlint/travis-cli": "^7.0.0", + "@freecodecamp/challenge-md-parser": "^1.0.0", "@semantic-release/changelog": "^2.0.2", "@semantic-release/git": "^5.0.0", "babel-cli": "^6.3.17", diff --git a/curriculum/schema/challengeSchema.js b/curriculum/schema/challengeSchema.js index d7fa28c123..2bcc621dab 100644 --- a/curriculum/schema/challengeSchema.js +++ b/curriculum/schema/challengeSchema.js @@ -4,18 +4,16 @@ Joi.objectId = require('joi-objectid')(Joi); const schema = Joi.object().keys({ block: Joi.string(), blockId: Joi.objectId(), + challengeOrder: Joi.number(), challengeType: Joi.number() .min(0) .max(9) .required(), checksum: Joi.number(), dashedName: Joi.string(), - description: Joi.array() - .items(Joi.string().allow('')) - .required(), + description: Joi.string().required(), fileName: Joi.string(), - files: Joi.object().pattern( - /(jsx?|html|css|sass)$/, + files: Joi.array().items( Joi.object().keys({ key: Joi.string(), ext: Joi.string(), @@ -32,6 +30,7 @@ const schema = Joi.object().keys({ videoUrl: Joi.string().allow(''), helpRoom: Joi.string(), id: Joi.objectId().required(), + instructions: Joi.string().required(), isBeta: Joi.bool(), isComingSoon: Joi.bool(), isLocked: Joi.bool(), diff --git a/curriculum/test-challenges.js b/curriculum/test-challenges.js index 4b084778b4..46ea00c90e 100644 --- a/curriculum/test-challenges.js +++ b/curriculum/test-challenges.js @@ -1,39 +1,43 @@ -/* eslint-disable no-eval, no-process-exit, no-unused-vars */ +/* eslint-disable no-process-exit, no-unused-vars */ -import { Observable } from 'rx'; -import tape from 'tape'; +const { Observable } = require('rx'); +const tape = require('tape'); +const { flatten } = require('lodash'); +const vm = require('vm'); +const path = require('path'); +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }); -import getChallenges from './getChallenges'; +const { getChallengesForLang } = require('./getChallenges'); -import MongoIds from './mongoIds'; -import ChallengeTitles from './challengeTitles'; -import addAssertsToTapTest from './addAssertsToTapTest'; -import { validateChallenge } from './schema/challengeSchema'; +const MongoIds = require('./mongoIds'); +const ChallengeTitles = require('./challengeTitles'); +const addAssertsToTapTest = require('./addAssertsToTapTest'); +const { validateChallenge } = require('./schema/challengeSchema'); -// modern challengeType -const modern = 6; +const { LOCALE: lang } = process.env; + +const { challengeTypes } = require('../client/utils/challengeTypes'); let mongoIds = new MongoIds(); let challengeTitles = new ChallengeTitles(); -function evaluateTest( +function checkSyntax(test, tapTest) { + try { + // eslint-disable-next-line + new vm.Script(test.testString); + tapTest.pass(test.text); + } catch (e) { + tapTest.fail(e); + } +} + +function evaluateHtmlJsTest( solution, assert, - react, - redux, - reactRedux, - head, - tail, + files, test, tapTest ) { - let code = solution; - - /* NOTE: Provide dependencies for React/Redux challenges - * and configure testing environment - */ - let React, ReactDOM, Redux, ReduxThunk, ReactRedux, Enzyme, document; - // Fake Deep Equal dependency const DeepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b); @@ -53,173 +57,250 @@ function evaluateTest( return o; }; - if (react || redux || reactRedux) { - // Provide dependencies, just provide all of them - React = require('react'); - ReactDOM = require('react-dom'); - Redux = require('redux'); - ReduxThunk = require('redux-thunk'); - ReactRedux = require('react-redux'); - Enzyme = require('enzyme'); - const Adapter15 = require('enzyme-adapter-react-15'); - Enzyme.configure({ adapter: new Adapter15() }); - - /* Transpile ALL the code - * (we may use JSX in head or tail or tests, too): */ - const transform = require('babel-standalone').transform; - const options = { presets: ['es2015', 'react'] }; - - head = transform(head, options).code; - solution = transform(solution, options).code; - tail = transform(tail, options).code; - test = transform(test, options).code; + let sandbox = { + assert, + code: solution, + DeepEqual, + DeepFreeze, + test: test.testString + }; + if (files.html) { + const { head, tail } = files.html; const { JSDOM } = require('jsdom'); - // Mock DOM document for ReactDOM.render method - const jsdom = new JSDOM(` - - -
- - - `); - const { window } = jsdom; - - // Mock DOM for ReactDOM tests - document = window.document; - global.window = window; - global.document = window.document; + const jsdom = new JSDOM(` + + + ${head} + ${solution} + ${tail} + + `); + const jQuery = require('jquery')(jsdom.window); + sandbox = { + ...sandbox, + window: jsdom.window, + document: jsdom.window.document, + $: jQuery + }; } + let scriptString = ''; + if (files.js) { + const { head, tail } = files.js; + scriptString = head + '\n' + solution + '\n' + tail + '\n'; + } + + try { + const context = vm.createContext(sandbox); + scriptString += ` + const testResult = eval(test); + if (typeof testResult === 'function') { + testResult(() => code); + }`; + const script = new vm.Script(scriptString); + script.runInContext(context); + } catch (e) { + // console.log(scriptString); + // console.log(e); + tapTest.fail(e); + // process.exit(1); + } +} + +function evaluateReactReduxTest() { + /* NOTE: Provide dependencies for React/Redux challenges + * and configure testing environment + */ + // let React, ReactDOM, Redux, ReduxThunk, ReactRedux, Enzyme, document; + + // if (react || redux || reactRedux) { + // // Provide dependencies, just provide all of them + // React = require('react'); + // ReactDOM = require('react-dom'); + // Redux = require('redux'); + // ReduxThunk = require('redux-thunk'); + // ReactRedux = require('react-redux'); + // Enzyme = require('enzyme'); + // const Adapter15 = require('enzyme-adapter-react-15'); + // Enzyme.configure({ adapter: new Adapter15() }); + + // /* Transpile ALL the code + // * (we may use JSX in head or tail or tests, too): */ + // const transform = require('babel-standalone').transform; + // const options = { presets: ['es2015', 'react'] }; + + // head = transform(head, options).code; + // solution = transform(solution, options).code; + // tail = transform(tail, options).code; + // test = transform(test, options).code; + + // const { JSDOM } = require('jsdom'); + // // Mock DOM document for ReactDOM.render method + // const jsdom = new JSDOM(` + // + // + //
+ // + // + // `); + // const { window } = jsdom; + + // // Mock DOM for ReactDOM tests + // document = window.document; + // global.window = window; + // global.document = window.document; + // } + /* eslint-enable no-unused-vars */ - try { - (() => { - return eval( - head + '\n' + solution + '\n' + tail + '\n' + test.testString - ); - })(); - } catch (e) { - console.log(head + '\n' + solution + '\n' + tail + '\n' + test.testString); - console.log(e); - tapTest.fail(e); - process.exit(1); - } + + // No support for async tests + // const isAsync = s => s.includes('(async () => '); + + // try { + // if (!isAsync(test.testString)) { + // const context = vm.createContext(sandbox); + // const scriptString = + // head + '\n' + solution + '\n' + tail + '\n' + ` + // const testResult = eval(test); + // if (typeof testResult === 'function') { + // testResult(() => code); + // }`; + // const script = new vm.Script(scriptString); + // script.runInContext(context); + // } else { + // // For async tests only check syntax + // // eslint-disable-next-line + // new vm.Script(test.testString); + // tapTest.pass(test.text); + // } + // } catch (e) { + // console.log(head + '\n' + solution + '\n' + tail + '\n' + test.testString); + // // console.log(e); + // tapTest.fail(e); + // // process.exit(1); + // } } function createTest({ title, id = '', + challengeType, tests = [], solutions = [], - files = [], - react = false, - redux = false, - reactRedux = false + files = [] }) { mongoIds.check(id, title); challengeTitles.check(title); - solutions = solutions.filter(solution => !!solution); - tests = tests.filter(test => !!test); - - // No support for async tests - const isAsync = s => s.includes('(async () => '); - if (isAsync(tests.join(''))) { - console.log(`Replacing Async Tests for Challenge ${title}`); - tests = tests.map( - challengeTestSource => - isAsync(challengeTestSource) - ? "assert(true, 'message: great');" - : challengeTestSource - ); + // if title starts with [word] [number], for example `Problem 5`, + // tap-spec does not recognize it as test suite. + const titleRe = new RegExp('^([a-z]+\\s+)(\\d+.*)$', 'i'); + const match = titleRe.exec(title); + if (match) { + title = `${match[1]}#${match[2]}`; } - const { head, tail } = Object.keys(files) - .map(key => files[key]) - .reduce( - (result, file) => ({ - head: result.head + ';' + file.head.join('\n'), - tail: result.tail + ';' + file.tail.join('\n') - }), - { head: '', tail: '' } - ); - const plan = tests.length; - if (!plan) { - return Observable.just({ - title, - type: 'missing' + + const testSuite = Observable.fromCallback(tape)(title); + + tests = tests.filter(test => !!test.testString); + if (tests.length === 0) { + return testSuite.flatMap(tapTest => { + tapTest.end(); + return Observable.just(title); }); } - return Observable.fromCallback(tape)(title) - .doOnNext( - tapTest => (solutions.length ? tapTest.plan(plan) : tapTest.end()) - ) - .flatMap(tapTest => { - if (solutions.length <= 0) { - return Observable.just({ - title, - type: 'missing' - }); - } + const noSolution = new RegExp('// solution required'); + solutions = solutions.filter(solution => ( + !!solution && !noSolution.test(solution) + )); + const skipTests = challengeType !== challengeTypes.html && + challengeType !== challengeTypes.js && + challengeType !== challengeTypes.bonfire && + challengeType !== challengeTypes.zipline; + + // For problems without a solution, check only the syntax of the tests. + if (solutions.length === 0 || skipTests) { + return testSuite.flatMap(tapTest => { + tapTest.plan(tests.length); + tests.forEach(test => { + checkSyntax(test, tapTest); + }); + return Observable.just(title); + }); + } + + const exts = Array.from(new Set(files.map(({ ext }) => ext))); + const groupedFiles = exts.reduce((result, ext) => { + const file = files.filter(file => file.ext === ext ).reduce( + (result, file) => ({ + head: result.head + ';' + file.head, + tail: result.tail + ';' + file.tail + }), + { head: '', tail: '' } + ); + return { + ...result, + [ext]: file + }; + }, {}); + + const plan = tests.length * solutions.length; + return testSuite + .flatMap(tapTest => { + tapTest.plan(plan); return ( Observable.just(tapTest) .map(addAssertsToTapTest) - /* eslint-disable no-unused-vars */ - // assert and code used within the eval .doOnNext(assert => { solutions.forEach(solution => { tests.forEach(test => { - evaluateTest( + evaluateHtmlJsTest( solution, assert, - react, - redux, - reactRedux, - head, - tail, + groupedFiles, test, tapTest ); }); }); }) - .map(() => ({ title })) + .ignoreElements() ); }); } -Observable.from(getChallenges()) - .do(({ challenges }) => { - challenges.forEach(challenge => { - const result = validateChallenge(challenge); - if (result.error) { - console.log(result.value); - throw new Error(result.error); - } - }); +Observable.fromPromise(getChallengesForLang(lang || 'english')) + .flatMap(curriculum => { + const allChallenges = Object.keys(curriculum) + .map(key => curriculum[key].blocks) + .reduce((challengeArray, superBlock) => { + const challengesForBlock = Object.keys(superBlock).map( + key => superBlock[key].challenges + ); + return [...challengeArray, ...flatten(challengesForBlock)]; + }, []); + return Observable.from(allChallenges); }) - .flatMap(challengeSpec => { - return Observable.from(challengeSpec.challenges); + .do(challenge => { + const result = validateChallenge(challenge); + if (result.error) { + console.log(result.value); + throw new Error(result.error); + } }) - .filter(({ challengeType }) => challengeType !== modern) .flatMap(challenge => { return createTest(challenge); }) - .map(({ title, type }) => { - if (type === 'missing') { - return title; - } - return false; - }) - .filter(title => !!title) .toArray() .subscribe( noSolutions => { if (noSolutions) { console.log( - '# These challenges have no solutions\n- [ ] ' + - noSolutions.join('\n- [ ] ') + `# These challenges have no solutions (${noSolutions.length})\n` + + '- [ ] ' + noSolutions.join('\n- [ ] ') ); } }, diff --git a/package.json b/package.json index d473dfc279..22f6bac204 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test": "npm-run-all -p test:*", "test-ci": "npm test", "test:client": "cd ./client && npm test && cd ../", - "test:curriculum": "echo 'Warning: TODO - Define Testing.'", + "test:curriculum": "cd ./curriculum && npm test && cd ../", "test:guide-directories": "node ./tools/scripts/ci/ensure-guide-page-naming.js", "test:server": "echo 'Warning: TODO - Define Testing.'", "test:tools": "jest ./tools" diff --git a/tools/challenge-md-parser/index.js b/tools/challenge-md-parser/index.js index 6e08341fa5..0f9fc92ea2 100644 --- a/tools/challenge-md-parser/index.js +++ b/tools/challenge-md-parser/index.js @@ -10,6 +10,7 @@ const frontmatterToData = require('./frontmatter-to-data'); const textToData = require('./text-to-data'); const testsToData = require('./tests-to-data'); const challengeSeedToData = require('./challengeSeed-to-data'); +const solutionsToData = require('./solution-to-data'); const processor = unified() .use(markdown) @@ -18,6 +19,7 @@ const processor = unified() .use(testsToData) .use(remark2rehype, { allowDangerousHTML: true }) .use(raw) + .use(solutionsToData) .use(textToData, ['description', 'instructions']) .use(challengeSeedToData) // the plugins below are just to stop the processor from throwing diff --git a/tools/challenge-md-parser/solution-to-data.js b/tools/challenge-md-parser/solution-to-data.js index 1b63733f0c..66a742b17b 100644 --- a/tools/challenge-md-parser/solution-to-data.js +++ b/tools/challenge-md-parser/solution-to-data.js @@ -10,7 +10,10 @@ function createPlugin() { const solutions = selectAll('code', node).map( element => element.children[0].value ); - file.data.solutions = solutions; + file.data = { + ...file.data, + solutions + }; } } visit(tree, 'element', visitor);