fix: test curriculum challenges (#24180)

This commit is contained in:
Valeriy
2018-10-23 16:21:53 +03:00
committed by mrugesh mohapatra
parent 7da04a348b
commit e099d6486d
29 changed files with 478 additions and 193 deletions

View File

@ -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;

View File

@ -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;

View File

@ -79,9 +79,41 @@ tests:
## Solution
<section id='solution'>
```js
var code = ".fullCard {\nwidth: 245px; border: 1px solid #ccc; border-radius: 5px; margin: 10px 5px; padding: 4px;}"
```html
<style>
h4 {
text-align: center;
}
p {
text-align: justify;
}
.links {
margin-right: 20px;
text-align: left;
}
.fullCard {
width: 245px;
border: 1px solid #ccc;
border-radius: 5px;
margin: 10px 5px;
padding: 4px;
}
.cardContent {
padding: 10px;
}
</style>
<div class="fullCard">
<div class="cardContent">
<div class="cardText">
<h4>Google</h4>
<p>Google was founded by Larry Page and Sergey Brin while they were Ph.D. students at Stanford University.</p>
</div>
<div class="cardLinks">
<a href="https://en.wikipedia.org/wiki/Larry_Page" target="_blank" class="links">Larry Page</a>
<a href="https://en.wikipedia.org/wiki/Sergey_Brin" target="_blank" class="links">Sergey Brin</a>
</div>
</div>
</div>
```
</section>

View File

@ -90,9 +90,42 @@ tests:
## Solution
<section id='solution'>
```js
var code = ".heart {transform: rotate(-45deg);} .heart::after {background-color: pink; border-radius: 50%;} .heart::before {content: \"\"; border-radius: 50%;}"
```html
<style>
.heart {
position: absolute;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: pink;
height: 50px;
width: 50px;
transform: rotate(-45deg);
}
.heart::after {
background-color: pink;
content: "";
border-radius: 50%;
position: absolute;
width: 50px;
height: 50px;
top: 0px;
left: 25px;
}
.heart::before {
content: "";
background-color: pink;
border-radius: 50%;
position: absolute;
width: 50px;
height: 50px;
top: -25px;
left: 0px;
}
</style>
<div class = "heart"></div>
```
</section>

View File

@ -84,7 +84,44 @@ tests:
## Solution
<section id='solution'>
```js
// solution required
```html
<style>
.injected-text {
margin-bottom: -25px;
text-align: center;
}
.box {
border-style: solid;
border-color: black;
border-width: 5px;
text-align: center;
}
.yellow-box {
background-color: yellow;
padding: 10px;
}
.red-box {
background-color: crimson;
color: #fff;
padding: 20px;
margin: -15px;
}
.blue-box {
background-color: blue;
color: #fff;
padding: 20px;
margin: 20px;
margin-top: -15px;
}
</style>
<div class="box yellow-box">
<h5 class="box red-box">padding</h5>
<h5 class="box blue-box">padding</h5>
</div>
```
</section>

View File

@ -54,7 +54,7 @@ tests:
## Solution
<section id='solution'>
```js
// solution required
```html
<h1>Hello World</h1>
```
</section>

View File

@ -81,9 +81,38 @@ tests:
## Solution
<section id='solution'>
```html
<style>
.item1{background:LightSkyBlue;}
.item2{background:LightSalmon;}
.item3{background:PaleTurquoise;}
.item4{background:LightPink;}
.item5{background:PaleGreen;}
```js
var code = ".container {grid-template-areas: \"header header header\" \". content content\" \"footer footer footer\";}"
.container {
font-size: 40px;
min-height: 300px;
width: 100%;
background: LightGray;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
grid-gap: 10px;
grid-template-areas:
"header header header"
". content content"
"footer footer footer";
}
</style>
<div class="container">
<div class="item1">1</div>
<div class="item2">2</div>
<div class="item3">3</div>
<div class="item4">4</div>
<div class="item5">5</div>
</div>
```
</section>

View File

@ -113,9 +113,75 @@ tests:
## Solution
<section id='solution'>
```html
<style>
.item1 {
background: LightSkyBlue;
grid-area: header;
}
```js
var code = "@media (min-width: 400px){.container{ grid-template-areas: \"header header\" \"advert content\" \"footer footer\";}}"
.item2 {
background: LightSalmon;
grid-area: advert;
}
.item3 {
background: PaleTurquoise;
grid-area: content;
}
.item4 {
background: lightpink;
grid-area: footer;
}
.container {
font-size: 1.5em;
min-height: 300px;
width: 100%;
background: LightGray;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 50px auto 1fr auto;
grid-gap: 10px;
grid-template-areas:
"header"
"advert"
"content"
"footer";
}
@media (min-width: 300px){
.container{
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"advert header"
"advert content"
"advert footer";
}
}
@media (min-width: 400px){
.container{
/* change the code below this line */
grid-template-areas:
"header header"
"advert content"
"footer footer";
/* change the code above this line */
}
}
</style>
<div class="container">
<div class="item1">header</div>
<div class="item2">advert</div>
<div class="item3">content</div>
<div class="item4">footer</div>
</div>
```
</section>

View File

@ -25,9 +25,9 @@ tests:
- text: The <code>DisplayMessages</code> component should render an empty <code>div</code> element.
testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); return mockedComponent.find('div').text() === '' })(), 'The <code>DisplayMessages</code> component should render an empty <code>div</code> element.');
- text: The <code>DisplayMessages</code> constructor should be called properly with <code>super</code>, passing in <code>props</code>.
testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g,'); return noWhiteSpace.includes('constructor(props)') && noWhiteSpace.includes('super(props'); })(), 'The <code>DisplayMessages</code> constructor should be called properly with <code>super</code>, passing in <code>props</code>.');
testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g,''); return noWhiteSpace.includes('constructor(props)') && noWhiteSpace.includes('super(props'); })(), 'The <code>DisplayMessages</code> constructor should be called properly with <code>super</code>, passing in <code>props</code>.');
- text: 'The <code>DisplayMessages</code> component should have an initial state equal to <code>{input: "", messages: []}</code>.'
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 <code>DisplayMessages</code> component should have an initial state equal to <code>{input: "", messages: []}</code>.'');'
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 <code>DisplayMessages</code> component should have an initial state equal to <code>{input: \"\", messages: []}</code>.');"
```

View File

@ -26,7 +26,7 @@ tests:
- text: The <code>AppWrapper</code> should render.
testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return mockedComponent.find('AppWrapper').length === 1; })(), 'The <code>AppWrapper</code> should render.');
- text: The <code>Provider</code> wrapper component should have a prop of <code>store</code> 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('<Providerstore={store}>'); })(), 'The <code>Provider</code> wrapper component should have a prop of <code>store</code> 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('<Providerstore={store}>'); })(), 'The <code>Provider</code> wrapper component should have a prop of <code>store</code> passed to it, equal to the Redux store.');
- text: <code>DisplayMessages</code> should render as a child of <code>AppWrapper</code>.
testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return mockedComponent.find('AppWrapper').find('DisplayMessages').length === 1; })(), '<code>DisplayMessages</code> should render as a child of <code>AppWrapper</code>.');
- text: The <code>DisplayMessages</code> component should render an h2, input, button, and <code>ul</code> element.

View File

@ -25,8 +25,8 @@ tests:
testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('ShoppingCart').length === 1; })(), 'The component <code>ShoppingCart</code> should render.');
- text: The component <code>Items</code> should render.
testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('Items').length === 1; })(), 'The component <code>Items</code> should render.');
- text: 'The <code>Items</code> component should have a prop of <code>{ quantity: 10 }</code> passed from the <code>ShoppingCart</code> component.'
testString: 'getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find(''Items'').props().quantity == 10 && getUserInput(''index'').replace(/ /g,'').includes(''<Itemsquantity={10}/>''); })(), ''The <code>Items</code> component should have a prop of <code>{ quantity: 10 }</code> passed from the <code>ShoppingCart</code> component.'');'
- text: "The <code>Items</code> component should have a prop of <code>{ quantity: 10 }</code> passed from the <code>ShoppingCart</code> component."
testString: "getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('Items').props().quantity == 10 && getUserInput('index').replace(/ /g,'').includes('<Itemsquantity={10}/>'); })(), 'The <code>Items</code> component should have a prop of <code>{ quantity: 10 }</code> passed from the <code>ShoppingCart</code> component.');"
```

View File

@ -22,7 +22,7 @@ The <code>renderToString()</code> method is provided on <code>ReactDOMServer</co
```yml
tests:
- text: The <code>App</code> component should render to a string using <code>ReactDOMServer.renderToString</code>.
testString: getUserInput => assert(getUserInput('index').replace(/ /g,').includes('ReactDOMServer.renderToString(<App/>)') && Enzyme.mount(React.createElement(App)).children().name() === 'div', 'The <code>App</code> component should render to a string using <code>ReactDOMServer.renderToString</code>.');
testString: getUserInput => assert(getUserInput('index').replace(/ /g,'').includes('ReactDOMServer.renderToString(<App/>)') && Enzyme.mount(React.createElement(App)).children().name() === 'div', 'The <code>App</code> component should render to a string using <code>ReactDOMServer.renderToString</code>.');
```

View File

@ -27,9 +27,9 @@ tests:
- text: The <code>Camper</code> component should render.
testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(CampSite)); return mockedComponent.find('Camper').length === 1; })(), 'The <code>Camper</code> component should render.');
- text: The <code>Camper</code> component should include default props which assign the string <code>CamperBot</code> to the key <code>name</code>.
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 <code>Camper</code> component should include default props which assign the string <code>CamperBot</code> to the key <code>name</code>.');
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 <code>Camper</code> component should include default props which assign the string <code>CamperBot</code> to the key <code>name</code>.');
- text: The <code>Camper</code> component should include prop types which require the <code>name</code> prop to be of type <code>string</code>.
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 <code>Camper</code> component should include prop types which require the <code>name</code> prop to be of type <code>string</code>.');
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 <code>Camper</code> component should include prop types which require the <code>name</code> prop to be of type <code>string</code>.');
- text: The <code>Camper</code> component should contain a <code>p</code> element with only the text from the <code>name</code> prop.
testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(CampSite)); return mockedComponent.find('p').text() === mockedComponent.find('Camper').props().name; })(), 'The <code>Camper</code> component should contain a <code>p</code> element with only the text from the <code>name</code> prop.');

View File

@ -30,7 +30,7 @@ tests:
- text: The <code>Items</code> component should render.
testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('Items').length === 1; })(), 'The <code>Items</code> component should render.');
- text: The <code>Items</code> component should include a <code>propTypes</code> check that requires <code>quantity</code> to be a <code>number</code>.
testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/ /g, '); return noWhiteSpace.includes('quantity:PropTypes.number.isRequired') && noWhiteSpace.includes('Items.propTypes='); })(), 'The <code>Items</code> component should include a <code>propTypes</code> check that requires <code>quantity</code> to be a <code>number</code>.');
testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/ /g, ''); return noWhiteSpace.includes('quantity:PropTypes.number.isRequired') && noWhiteSpace.includes('Items.propTypes='); })(), 'The <code>Items</code> component should include a <code>propTypes</code> check that requires <code>quantity</code> to be a <code>number</code>.');
```

View File

@ -22,7 +22,7 @@ Render this component to the DOM using <code>ReactDOM.render()</code>. There is
```yml
tests:
- text: There should be a React component called <code>MyComponent</code>.
testString: getUserInput => assert(getUserInput('index').replace(/\s/g, ').includes('classMyComponentextendsReact.Component{'), 'There should be a React component called <code>MyComponent</code>.');
testString: getUserInput => assert(getUserInput('index').replace(/\s/g, '').includes('classMyComponentextendsReact.Component{'), 'There should be a React component called <code>MyComponent</code>.');
- text: <code>MyComponent</code> should contain an <code>h1</code> tag with text <code>My First React Component!</code> Case and punctuation matter.
testString: assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return mockedComponent.find('h1').text() === 'My First React Component!'; })(), '<code>MyComponent</code> should contain an <code>h1</code> tag with text <code>My First React Component!</code> Case and punctuation matter.');
- text: <code>MyComponent</code> should render to the DOM.

View File

@ -29,9 +29,9 @@ tests:
- text: The <code>authReducer</code> should toggle the <code>state</code> of <code>authenticated</code> between <code>true</code> and <code>false</code>.
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 <code>authReducer</code> should toggle the <code>state</code> of <code>authenticated</code> between <code>true</code> and <code>false</code>.'');'
- text: 'The store <code>state</code> should have two keys: <code>count</code>, which holds a number, and <code>auth</code>, which holds an object. The <code>auth</code> object should have a property of <code>authenticated</code>, 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 <code>state</code> should have two keys: <code>count</code>, which holds a number, and <code>auth</code>, which holds an object. The <code>auth</code> object should have a property of <code>authenticated</code>, 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 <code>state</code> should have two keys: <code>count</code>, which holds a number, and <code>auth</code>, which holds an object. The <code>auth</code> object should have a property of <code>authenticated</code>, which holds a boolean.');"
- text: The <code>rootReducer</code> should be a function that combines the <code>counterReducer</code> and the <code>authReducer</code>.
testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g,'); return typeof rootReducer === 'function' && noWhiteSpace.includes('Redux.combineReducers') })(), 'The <code>rootReducer</code> should be a function that combines the <code>counterReducer</code> and the <code>authReducer</code>.');
testString: getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/\s/g,''); return typeof rootReducer === 'function' && noWhiteSpace.includes('Redux.combineReducers') })(), 'The <code>rootReducer</code> should be a function that combines the <code>counterReducer</code> and the <code>authReducer</code>.');
```

View File

@ -27,7 +27,7 @@ tests:
- text: The store should be initialized with an object with property <code>login</code> set to <code>false</code>.
testString: assert(store.getState().login === false, 'The store should be initialized with an object with property <code>login</code> set to <code>false</code>.');
- text: The <code>store.dispatch()</code> method should be used to dispatch an action of type <code>LOGIN</code>.
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 <code>store.dispatch()</code> method should be used to dispatch an action of type <code>LOGIN</code>.'');'
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 <code>store.dispatch()</code> method should be used to dispatch an action of type <code>LOGIN</code>.');"
```

View File

@ -34,9 +34,9 @@ tests:
- text: The <code>authReducer</code> 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 <code>authReducer</code> function should handle multiple action types with a switch statement.');
- text: <code>LOGIN</code> and <code>LOGOUT</code> should be declared as <code>const</code> values and should be assigned strings of <code>LOGIN</code>and <code>LOGOUT</code>.
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"')) })(), '<code>LOGIN</code> and <code>LOGOUT</code> should be declared as <code>const</code> values and should be assigned strings of <code>LOGIN</code>and <code>LOGOUT</code>.');
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"')) })(), '<code>LOGIN</code> and <code>LOGOUT</code> should be declared as <code>const</code> values and should be assigned strings of <code>LOGIN</code>and <code>LOGOUT</code>.');
- text: The action creators and the reducer should reference the <code>LOGIN</code> and <code>LOGOUT</code> 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 <code>LOGIN</code> and <code>LOGOUT</code> 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 <code>LOGIN</code> and <code>LOGOUT</code> constants.');
```

View File

@ -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); })"
```

View File

@ -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); })"
```

View File

@ -27,7 +27,7 @@ Give your answer as a hexadecimal number.
```yml
tests:
- text: <code>euler162()</code> should return 3D58725572C62302.
testString: assert.strictEqual(euler162(), 3D58725572C62302, '<code>euler162()</code> should return 3D58725572C62302.');
testString: assert.strictEqual(euler162(), '3D58725572C62302', '<code>euler162()</code> should return 3D58725572C62302.');
```

View File

@ -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: <code>euler284()</code> should return 5a411d7b.
testString: assert.strictEqual(euler284(), 5a411d7b, '<code>euler284()</code> should return 5a411d7b.');
testString: assert.strictEqual(euler284(), '5a411d7b', '<code>euler284()</code> should return 5a411d7b.');
```

View File

@ -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;

View File

@ -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",

View File

@ -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(),

View File

@ -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(`<!doctype html>
const jsdom = new JSDOM(`
<!doctype html>
<html>
<body>
<div id="challenge-node"></div>
</body>
${head}
${solution}
${tail}
</html>
`);
const { window } = jsdom;
// Mock DOM for ReactDOM tests
document = window.document;
global.window = window;
global.document = window.document;
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(`<!doctype html>
// <html>
// <body>
// <div id="challenge-node"></div>
// </body>
// </html>
// `);
// 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(
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);
});
}
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.join('\n'),
tail: result.tail + ';' + file.tail.join('\n')
head: result.head + ';' + file.head,
tail: result.tail + ';' + file.tail
}),
{ head: '', tail: '' }
);
const plan = tests.length;
if (!plan) {
return Observable.just({
title,
type: 'missing'
});
}
return {
...result,
[ext]: file
};
}, {});
return Observable.fromCallback(tape)(title)
.doOnNext(
tapTest => (solutions.length ? tapTest.plan(plan) : tapTest.end())
)
const plan = tests.length * solutions.length;
return testSuite
.flatMap(tapTest => {
if (solutions.length <= 0) {
return Observable.just({
title,
type: 'missing'
});
}
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 => {
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);
})
.do(challenge => {
const result = validateChallenge(challenge);
if (result.error) {
console.log(result.value);
throw new Error(result.error);
}
});
})
.flatMap(challengeSpec => {
return Observable.from(challengeSpec.challenges);
})
.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- [ ] ')
);
}
},

View File

@ -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"

View File

@ -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

View File

@ -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);