mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-06 06:39:18 -05:00
feat(client): support beforeAll in DOM challenge tests (#59001)
This commit is contained in:
committed by
GitHub
parent
1c33d37d8a
commit
96d62330cd
@@ -172,6 +172,7 @@ export type ChallengeNode = {
|
||||
head: string[];
|
||||
hasEditableBoundaries: boolean;
|
||||
helpCategory: string;
|
||||
hooks?: { beforeAll: string };
|
||||
id: string;
|
||||
instructions: string;
|
||||
isComingSoon: boolean;
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
executeChallenge,
|
||||
initConsole,
|
||||
initTests,
|
||||
initHooks,
|
||||
initVisibleEditors,
|
||||
previewMounted,
|
||||
updateChallengeMeta,
|
||||
@@ -86,6 +87,7 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
createFiles,
|
||||
initConsole,
|
||||
initTests,
|
||||
initHooks,
|
||||
initVisibleEditors,
|
||||
updateChallengeMeta,
|
||||
challengeMounted,
|
||||
@@ -108,6 +110,7 @@ interface ShowClassicProps extends Pick<PreviewProps, 'previewMounted'> {
|
||||
challengeFiles: ChallengeFiles;
|
||||
initConsole: (arg0: string) => void;
|
||||
initTests: (tests: Test[]) => void;
|
||||
initHooks: (hooks?: { beforeAll: string }) => void;
|
||||
initVisibleEditors: () => void;
|
||||
isChallengeCompleted: boolean;
|
||||
output: string[];
|
||||
@@ -193,6 +196,7 @@ function ShowClassic({
|
||||
title,
|
||||
description,
|
||||
instructions,
|
||||
hooks,
|
||||
fields: { tests, blockName },
|
||||
challengeType,
|
||||
hasEditableBoundaries,
|
||||
@@ -216,6 +220,7 @@ function ShowClassic({
|
||||
challengeMounted,
|
||||
initConsole,
|
||||
initTests,
|
||||
initHooks,
|
||||
initVisibleEditors,
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
@@ -359,6 +364,7 @@ function ShowClassic({
|
||||
);
|
||||
|
||||
initTests(tests);
|
||||
initHooks(hooks);
|
||||
|
||||
initVisibleEditors();
|
||||
|
||||
@@ -556,6 +562,9 @@ export const query = graphql`
|
||||
superBlock
|
||||
translationPending
|
||||
forumTopicId
|
||||
hooks {
|
||||
beforeAll
|
||||
}
|
||||
fields {
|
||||
blockName
|
||||
slug
|
||||
|
||||
@@ -8,6 +8,7 @@ export const actionTypes = createTypes(
|
||||
[
|
||||
'createFiles',
|
||||
'createQuestion',
|
||||
'initHooks',
|
||||
'initTests',
|
||||
'initConsole',
|
||||
'initLogs',
|
||||
|
||||
@@ -22,6 +22,7 @@ export const createFiles = createAction(
|
||||
|
||||
export const createQuestion = createAction(actionTypes.createQuestion);
|
||||
export const initTests = createAction(actionTypes.initTests);
|
||||
export const initHooks = createAction(actionTypes.initHooks);
|
||||
export const updateTests = createAction(actionTypes.updateTests);
|
||||
export const cancelTests = createAction(actionTypes.cancelTests);
|
||||
export const initConsole = createAction(actionTypes.initConsole);
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
challengeDataSelector,
|
||||
challengeMetaSelector,
|
||||
challengeTestsSelector,
|
||||
challengeHooksSelector,
|
||||
isBuildEnabledSelector,
|
||||
isExecutingSelector,
|
||||
portalDocumentSelector,
|
||||
@@ -110,6 +111,7 @@ export function* executeChallengeSaga({ payload }) {
|
||||
const tests = (yield select(challengeTestsSelector)).map(
|
||||
({ text, testString }) => ({ text, testString })
|
||||
);
|
||||
const hooks = yield select(challengeHooksSelector);
|
||||
yield put(updateTests(tests));
|
||||
|
||||
yield fork(takeEveryLog, consoleProxy);
|
||||
@@ -128,7 +130,7 @@ export function* executeChallengeSaga({ payload }) {
|
||||
const document = yield getContext('document');
|
||||
const testRunner = yield call(
|
||||
getTestRunner,
|
||||
buildData,
|
||||
{ ...buildData, hooks },
|
||||
{ proxyLogger },
|
||||
document
|
||||
);
|
||||
|
||||
@@ -120,6 +120,10 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
challengeTests: payload
|
||||
}),
|
||||
[actionTypes.initHooks]: (state, { payload }) => ({
|
||||
...state,
|
||||
challengeHooks: payload
|
||||
}),
|
||||
[actionTypes.updateTests]: (state, { payload }) => ({
|
||||
...state,
|
||||
challengeTests: payload
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ns } from './action-types';
|
||||
|
||||
export const challengeFilesSelector = state => state[ns].challengeFiles;
|
||||
export const challengeMetaSelector = state => state[ns].challengeMeta;
|
||||
export const challengeHooksSelector = state => state[ns].challengeHooks;
|
||||
export const challengeTestsSelector = state => state[ns].challengeTests;
|
||||
export const consoleOutputSelector = state => state[ns].consoleOut;
|
||||
export const completedChallengesIdsSelector = createSelector(
|
||||
|
||||
@@ -16,6 +16,10 @@ export interface Source {
|
||||
original: { [key: string]: string | null };
|
||||
}
|
||||
|
||||
interface Hooks {
|
||||
beforeAll?: string;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
window?: Window &
|
||||
typeof globalThis & { i18nContent?: i18n; __pyodide: unknown };
|
||||
@@ -23,6 +27,7 @@ export interface Context {
|
||||
element: HTMLIFrameElement;
|
||||
build: string;
|
||||
sources: Source;
|
||||
hooks?: Hooks;
|
||||
loadEnzyme?: () => void;
|
||||
}
|
||||
|
||||
@@ -89,7 +94,7 @@ const DOCUMENT_NOT_FOUND_ERROR = 'misc.document-notfound';
|
||||
// The "fcc-hide-header" class on line 95 is added to ensure that the CSSHelper class ignores this style element
|
||||
// during tests, preventing CSS-related test failures.
|
||||
|
||||
export const createHeader = (id = mainPreviewId) =>
|
||||
const createHeader = (id = mainPreviewId) =>
|
||||
`
|
||||
<base href='' />
|
||||
<style class="fcc-hide-header">
|
||||
@@ -137,6 +142,14 @@ export const createHeader = (id = mainPreviewId) =>
|
||||
</script>
|
||||
`;
|
||||
|
||||
const createBeforeAllScript = (beforeAll?: string) => {
|
||||
if (!beforeAll) return '';
|
||||
|
||||
return `<script>
|
||||
${beforeAll};
|
||||
</script>`;
|
||||
};
|
||||
|
||||
type TestResult =
|
||||
| { pass: boolean }
|
||||
| { err: { message: string; stack?: string } };
|
||||
@@ -368,25 +381,32 @@ const waitForFrame = (frameContext: Context) => {
|
||||
});
|
||||
};
|
||||
|
||||
function writeToFrame(content: string, frame?: FrameDocument) {
|
||||
export const createContent = (
|
||||
id: string,
|
||||
{ build, sources, hooks }: { build: string; sources: Source; hooks?: Hooks }
|
||||
) => {
|
||||
// DOCTYPE should be the first thing written to the frame, so if the user code
|
||||
// includes a DOCTYPE declaration, we need to find it and write it first.
|
||||
const doctype = sources.contents?.match(/^<!DOCTYPE html>/i)?.[0] || '';
|
||||
return (
|
||||
doctype + createBeforeAllScript(hooks?.beforeAll) + createHeader(id) + build
|
||||
);
|
||||
};
|
||||
|
||||
const writeContentToFrame = (id: string) => (frameContext: Context) => {
|
||||
const frame = frameContext.document;
|
||||
|
||||
// it's possible, if the preview is rapidly opened and closed, for the frame
|
||||
// to be null at this point.
|
||||
if (frame) {
|
||||
frame.open();
|
||||
frame.write(content);
|
||||
frame.write(createContent(id, frameContext));
|
||||
frame.close();
|
||||
}
|
||||
}
|
||||
|
||||
const writeContentToFrame = (frameContext: Context) => {
|
||||
const doctype =
|
||||
frameContext.sources.contents?.match(/^<!DOCTYPE html>/i)?.[0] || '';
|
||||
|
||||
writeToFrame(
|
||||
doctype + createHeader(frameContext.element.id) + frameContext.build,
|
||||
frameContext.document
|
||||
);
|
||||
return frameContext;
|
||||
};
|
||||
|
||||
const restoreScrollPosition = (frameContext: Context) => {
|
||||
scrollManager.registerScrollEventListener(frameContext.element);
|
||||
|
||||
if (scrollManager.getPreviewScrollPosition()) {
|
||||
@@ -458,6 +478,7 @@ const createFramer = ({
|
||||
updateWindowFunctions ?? noop,
|
||||
updateProxyConsole(proxyLogger),
|
||||
updateWindowI18next,
|
||||
writeContentToFrame,
|
||||
writeContentToFrame(id),
|
||||
restoreScrollPosition,
|
||||
init(frameReady, proxyLogger)
|
||||
) as (args: Context) => void;
|
||||
|
||||
@@ -9,6 +9,12 @@ dashedName: step-17
|
||||
|
||||
Now create an image tag and give it the `class` `"user-img"`. Use string interpolation to set the `src` attribute to `image` you destructured earlier. Set the `alt` attribute to `author` followed by the text `"avatar"`. Make sure there is a space between the `author` variable and the word `"avatar"`, for example, `"Quincy Larson avatar"`.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should create an `img` element.
|
||||
|
||||
@@ -11,6 +11,11 @@ The next thing you'll show are biographical details about the author. You can do
|
||||
|
||||
Add a paragraph element with the `class` `"bio"`, then interpolate `bio` inside the paragraph element.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ Next, add a link to the author's page on freeCodeCamp News.
|
||||
|
||||
Add an anchor element with the `class` `"author-link"`, interpolate `url` as the value for the `href` attribute, and set `target` to `"_blank"`. For the text of the anchor element, interpolate `author` followed by the text `"'s author page"`. For example, `"Quincy Larson's author page"`.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should create an anchor element.
|
||||
|
||||
@@ -11,6 +11,12 @@ Next, there's not a lot of separation between each author's name and image, and
|
||||
|
||||
Add a `div` element above the author's bio and give it the `class` `"purple-divider"`.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should create a `div` element before your `p` element.
|
||||
|
||||
@@ -22,6 +22,12 @@ Chain the `.then()` method to your `fetch` call. Inside the `.then()` method, ad
|
||||
|
||||
Again, don't terminate the code with a semicolon yet.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should use the `fetch()` method to make a `GET` request to `"https://cdn.freecodecamp.org/curriculum/news-author-page/authors.json"`.
|
||||
|
||||
@@ -11,6 +11,12 @@ The data you get from a `GET` request is not usable at first. To make the data u
|
||||
|
||||
Remove `console.log(res)` and implicitly return `res.json()` instead.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should remove the `console.log(res)`.
|
||||
|
||||
@@ -11,6 +11,11 @@ In order to start working with the data, you will need to use another `.then()`
|
||||
|
||||
Chain another `.then()` to the existing `.then()` method. This time, pass in `data` as the parameter for the callback function. For the callback, use a curly brace because you will have more than one expression. Within your callback function, log `data` to the console to see what it looks like.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ Chain `.catch()` to the last `.then()`. Pass in a callback function with `err` a
|
||||
|
||||
**Note**: Now you can terminate your code with a semicolon. You couldn't do that in the previous steps because you'll signal to JavaScript to stop parsing your code, which will affect the `fetch()` syntax.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should use the `fetch()` method to make a `GET` request to `"https://cdn.freecodecamp.org/curriculum/news-author-page/authors.json"`.
|
||||
|
||||
@@ -15,6 +15,12 @@ Start by using the `let` keyword to create two variables named `startingIndex` a
|
||||
|
||||
Then, create an `authorDataArr` variable and assign it an empty array.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should use `let` to declare a variable named `startingIndex`.
|
||||
|
||||
@@ -24,6 +24,12 @@ The card should have the following structure:
|
||||
- You can use any type of loop to iterate over the `authors` array. Ex. `for`, `forEach`, `map`, etc.
|
||||
- You should use the `innerHTML` property to add the card to the `authorContainer` inside the loop. You will also need to use template literal syntax to add the markup.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
`displayAuthors` should be a function.
|
||||
|
||||
@@ -11,6 +11,12 @@ To see the authors' names on the page, you need to call the `displayAuthors` fun
|
||||
|
||||
First, remove your `console.log()` statement. Then, assign `data` to the `authorDataArr` variable.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should remove the console log showing the `data`.
|
||||
|
||||
@@ -11,6 +11,12 @@ Now `authorDataArr` is the same as the `data` you logged to the console a while
|
||||
|
||||
Inside your `console.log()` statement, add the text `"Author Data Array:"` as the first argument and `authorDataArr` as the second argument. Use comma to separate the text from `authorDataArr`.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should assign `data` to the `authorDataArr` variable
|
||||
|
||||
@@ -9,6 +9,12 @@ dashedName: step-12
|
||||
|
||||
Now create an image tag and give it the `class` `"user-img"`. Use string interpolation to set the `src` attribute to `image` you destructured earlier. Set the `alt` attribute to `author` followed by the text `"avatar"`. Make sure there is a space between the `author` variable and the word `"avatar"`, for example, `"Quincy Larson avatar"`.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should create an `img` element.
|
||||
@@ -38,13 +44,13 @@ assert.include(document.querySelector('img')?.className, "user-img");
|
||||
You should set the `src` attribute of your `img` element to `${image}`.
|
||||
|
||||
```js
|
||||
assert.equal(document.querySelector('img')?.getAttribute('src'), authorDataArr[0].image);
|
||||
assert.equal(document.querySelector('img')?.getAttribute('src'), 'http://not-a-real-url.nowhere/no-image.jpg');
|
||||
```
|
||||
|
||||
You should set the `alt` attribute of your `img` element to `${author} avatar`.
|
||||
|
||||
```js
|
||||
assert.equal(document.querySelector('img')?.getAttribute('alt'), `${authorDataArr[0].author} avatar`);
|
||||
assert.equal(document.querySelector('img')?.getAttribute('alt'), `Whoever avatar`);
|
||||
```
|
||||
|
||||
# --seed--
|
||||
|
||||
@@ -11,6 +11,11 @@ The next thing you'll show are biographical details about the author. You can do
|
||||
|
||||
Add a paragraph element with the `class` `"bio"`, then interpolate `bio` inside the paragraph element.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ Next, add a link to the author's page on freeCodeCamp News.
|
||||
|
||||
Add an anchor element with the `class` `"author-link"`, interpolate `url` as the value for the `href` attribute, and set `target` to `"_blank"`. For the text of the anchor element, interpolate `author` followed by the text `"'s author page"`. For example, `"Quincy Larson's author page"`.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should create an anchor element.
|
||||
|
||||
@@ -11,6 +11,12 @@ Now you have everything you want to include in the UI. The next step is to make
|
||||
|
||||
Create a `fetchMoreAuthors` function with the arrow function syntax. Don't put anything in it yet. Make sure you use curly braces because you'll have more than one expression inside the function.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should use `const` to create a `fetchMoreAuthors` function.
|
||||
|
||||
@@ -9,6 +9,12 @@ dashedName: step-16
|
||||
|
||||
Inside the `fetchMoreAuthors` function, set the `startingIndex` and `endingIndex` variables to `+= 8` each.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should set the `startingIndex` variable to `+=8`.
|
||||
|
||||
@@ -11,6 +11,12 @@ Now call the `displayAuthors` function with a portion of the author data just li
|
||||
|
||||
If you click the `Load More Authors` button after calling the function, it won't work. That's because you still have to add the `click` event listener to the button. You'll do that next.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should call your `displayAuthors` function.
|
||||
|
||||
@@ -13,6 +13,12 @@ Use `addEventListener` to add a `"click"` event listener to `loadMoreBtn`. Also,
|
||||
|
||||
After that, when you click the button you should see 8 more authors.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should call the `addEventListener()` method on your `loadMoreBtn` variable.
|
||||
|
||||
@@ -13,6 +13,12 @@ First, if you click the `Load More Authors` button a couple of times, you'll see
|
||||
|
||||
Inside the `fetchMoreAuthors` function, write an `if` statement and set the condition to `authorDataArr.length <= endingIndex` – meaning there's no more data to load.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should have an `if` statement.
|
||||
|
||||
@@ -9,6 +9,12 @@ dashedName: step-20
|
||||
|
||||
If this condition is met, disable the button by setting its `disabled` property to `true`. Also, set the `textContent` of the button to `"No more data to load"`.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should set the `disabled` property of `loadMoreBtn` to `true`.
|
||||
|
||||
@@ -11,6 +11,12 @@ Next, there's not a lot of separation between each author's name and image, and
|
||||
|
||||
Add a `div` element above the author's bio and give it the `class` `"purple-divider"`.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should create a `div` element before your `p` element.
|
||||
|
||||
@@ -11,6 +11,12 @@ Finally, what if there's an error and the author data fail to load? Then we need
|
||||
|
||||
Inside the `.catch()`, remove the `console.error()` and set the `innerHTML` of the `authorContainer` to a `p` element with the `class` `"error-msg"` and text `"There was an error loading the authors"`.
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should remove your `console.error` and its text.
|
||||
|
||||
@@ -13,6 +13,12 @@ Access the `style` property of the `Load More Authors` button and set `cursor` t
|
||||
|
||||
With that, your author page is complete!
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
window.fetch = () => Promise.resolve({json: () => Promise.resolve([{ author: 'Whoever', image: 'http://not-a-real-url.nowhere/no-image.jpg', url: "http://not-a-real-url.nowhere/", bio: 'words go here' }])});
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
You should access the `style` property of `loadMoreBtn` with a dot notation.
|
||||
|
||||
@@ -280,6 +280,9 @@ const schema = Joi.object()
|
||||
superBlock: Joi.string().regex(slugWithSlashRE),
|
||||
superOrder: Joi.number(),
|
||||
suborder: Joi.number(),
|
||||
hooks: Joi.object().keys({
|
||||
beforeAll: Joi.string().allow('')
|
||||
}),
|
||||
tests: Joi.array()
|
||||
.items(
|
||||
// public challenges
|
||||
|
||||
@@ -48,7 +48,7 @@ const { getChallengesForLang, getMetaForBlock } = require('../get-challenges');
|
||||
const { challengeSchemaValidator } = require('../schema/challenge-schema');
|
||||
const { testedLang, getSuperOrder } = require('../utils');
|
||||
const {
|
||||
createHeader,
|
||||
createContent,
|
||||
testId
|
||||
} = require('../../client/src/templates/Challenges/utils/frame');
|
||||
const { SuperBlocks } = require('../../shared/config/curriculum');
|
||||
@@ -578,8 +578,14 @@ async function createTestRunner(
|
||||
const runsInPythonWorker = buildFunction === buildPythonChallenge;
|
||||
|
||||
const evaluator = await (runsInBrowser
|
||||
? getContextEvaluator(build, sources, code, loadEnzyme)
|
||||
: getWorkerEvaluator(build, sources, code, runsInPythonWorker));
|
||||
? getContextEvaluator({
|
||||
build,
|
||||
sources,
|
||||
code,
|
||||
loadEnzyme,
|
||||
hooks: challenge.hooks
|
||||
})
|
||||
: getWorkerEvaluator({ build, sources, code, runsInPythonWorker }));
|
||||
|
||||
return async ({ text, testString }) => {
|
||||
try {
|
||||
@@ -625,8 +631,8 @@ function replaceChallengeFilesContentsWithSolutions(
|
||||
});
|
||||
}
|
||||
|
||||
async function getContextEvaluator(build, sources, code, loadEnzyme) {
|
||||
await initializeTestRunner(build, sources, code, loadEnzyme);
|
||||
async function getContextEvaluator(config) {
|
||||
await initializeTestRunner(config);
|
||||
|
||||
return {
|
||||
evaluate: async (testString, timeout) =>
|
||||
@@ -641,7 +647,12 @@ async function getContextEvaluator(build, sources, code, loadEnzyme) {
|
||||
};
|
||||
}
|
||||
|
||||
async function getWorkerEvaluator(build, sources, code, runsInPythonWorker) {
|
||||
async function getWorkerEvaluator({
|
||||
build,
|
||||
sources,
|
||||
code,
|
||||
runsInPythonWorker
|
||||
}) {
|
||||
// The python worker clears the globals between tests, so it should be fine
|
||||
// to use the same evaluator for all tests. TODO: check if this is true for
|
||||
// sys, since sys.modules is not being reset.
|
||||
@@ -655,9 +666,15 @@ async function getWorkerEvaluator(build, sources, code, runsInPythonWorker) {
|
||||
};
|
||||
}
|
||||
|
||||
async function initializeTestRunner(build, sources, code, loadEnzyme) {
|
||||
async function initializeTestRunner({
|
||||
build,
|
||||
sources,
|
||||
code,
|
||||
loadEnzyme,
|
||||
hooks
|
||||
}) {
|
||||
await page.reload();
|
||||
await page.setContent(createHeader(testId) + build);
|
||||
await page.setContent(createContent(testId, { build, sources, hooks }));
|
||||
await page.evaluate(
|
||||
async (code, sources, loadEnzyme) => {
|
||||
const getUserInput = fileName => sources[fileName];
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# --description--
|
||||
|
||||
Paragraph 1
|
||||
|
||||
```html
|
||||
code example
|
||||
```
|
||||
|
||||
# --before-all--
|
||||
|
||||
gubbins
|
||||
|
||||
# --hints--
|
||||
|
||||
First hint
|
||||
|
||||
```js
|
||||
// test code
|
||||
```
|
||||
|
||||
Second hint with <code>code</code>
|
||||
|
||||
```js
|
||||
// more test code
|
||||
```
|
||||
|
||||
Third *hint* with <code>code</code> and `inline code`
|
||||
|
||||
```js
|
||||
// more test code
|
||||
if(let x of xs) {
|
||||
console.log(x);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# --description--
|
||||
|
||||
Paragraph 1
|
||||
|
||||
```html
|
||||
code example
|
||||
```
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
// before all code
|
||||
function foo() {
|
||||
return 'bar';
|
||||
}
|
||||
foo();
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
First hint
|
||||
|
||||
```js
|
||||
// test code
|
||||
```
|
||||
|
||||
Second hint with <code>code</code>
|
||||
|
||||
```js
|
||||
// more test code
|
||||
```
|
||||
|
||||
Third *hint* with <code>code</code> and `inline code`
|
||||
|
||||
```js
|
||||
// more test code
|
||||
if(let x of xs) {
|
||||
console.log(x);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# --description--
|
||||
|
||||
Paragraph 1
|
||||
|
||||
```html
|
||||
code example
|
||||
```
|
||||
|
||||
# --before-all--
|
||||
|
||||
# --hints--
|
||||
|
||||
First hint
|
||||
|
||||
```js
|
||||
// test code
|
||||
```
|
||||
|
||||
Second hint with <code>code</code>
|
||||
|
||||
```js
|
||||
// more test code
|
||||
```
|
||||
|
||||
Third *hint* with <code>code</code> and `inline code`
|
||||
|
||||
```js
|
||||
// more test code
|
||||
if(let x of xs) {
|
||||
console.log(x);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
# --description--
|
||||
|
||||
Paragraph 1
|
||||
|
||||
```html
|
||||
code example
|
||||
```
|
||||
|
||||
# --before-all--
|
||||
|
||||
```js
|
||||
// before all code
|
||||
function foo() {
|
||||
return 'bar';
|
||||
}
|
||||
foo();
|
||||
```
|
||||
|
||||
gubbins
|
||||
|
||||
# --hints--
|
||||
|
||||
First hint
|
||||
|
||||
```js
|
||||
// test code
|
||||
```
|
||||
|
||||
Second hint with <code>code</code>
|
||||
|
||||
```js
|
||||
// more test code
|
||||
```
|
||||
|
||||
Third *hint* with <code>code</code> and `inline code`
|
||||
|
||||
```js
|
||||
// more test code
|
||||
if(let x of xs) {
|
||||
console.log(x);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# --description--
|
||||
|
||||
Paragraph 1
|
||||
|
||||
```html
|
||||
code example
|
||||
```
|
||||
|
||||
# --before-all--
|
||||
|
||||
```ts
|
||||
// before all code
|
||||
function foo() {
|
||||
return 'bar';
|
||||
}
|
||||
foo();
|
||||
```
|
||||
|
||||
# --hints--
|
||||
|
||||
First hint
|
||||
|
||||
```js
|
||||
// test code
|
||||
```
|
||||
|
||||
Second hint with <code>code</code>
|
||||
|
||||
```js
|
||||
// more test code
|
||||
```
|
||||
|
||||
Third *hint* with <code>code</code> and `inline code`
|
||||
|
||||
```js
|
||||
// more test code
|
||||
if(let x of xs) {
|
||||
console.log(x);
|
||||
}
|
||||
```
|
||||
@@ -7,6 +7,7 @@ const addFillInTheBlank = require('./plugins/add-fill-in-the-blank');
|
||||
const addFrontmatter = require('./plugins/add-frontmatter');
|
||||
const addSeed = require('./plugins/add-seed');
|
||||
const addSolution = require('./plugins/add-solution');
|
||||
const addBeforeHook = require('./plugins/add-before-hook');
|
||||
const addTests = require('./plugins/add-tests');
|
||||
const addText = require('./plugins/add-text');
|
||||
const addVideoQuestion = require('./plugins/add-video-question');
|
||||
@@ -52,6 +53,7 @@ const processor = unified()
|
||||
.use(addAssignment)
|
||||
.use(addScene)
|
||||
.use(addQuizzes)
|
||||
.use(addBeforeHook)
|
||||
.use(addTests)
|
||||
.use(addText, [
|
||||
'description',
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`add-before-hook plugin should have an output to match the snapshot 1`] = `
|
||||
{
|
||||
"hooks": {
|
||||
"beforeAll": "// before all code
|
||||
function foo() {
|
||||
return 'bar';
|
||||
}
|
||||
foo();",
|
||||
},
|
||||
}
|
||||
`;
|
||||
33
tools/challenge-parser/parser/plugins/add-before-hook.js
Normal file
33
tools/challenge-parser/parser/plugins/add-before-hook.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const { getSection } = require('./utils/get-section');
|
||||
|
||||
function plugin() {
|
||||
return transformer;
|
||||
|
||||
function transformer(tree, file) {
|
||||
const section = getSection(tree, '--before-all--');
|
||||
|
||||
if (section.length === 0) return;
|
||||
if (section.length > 1)
|
||||
throw Error(
|
||||
'#--before-all-- section must only contain a single code block'
|
||||
);
|
||||
|
||||
const codeNode = section[0];
|
||||
|
||||
if (codeNode.type !== 'code')
|
||||
throw Error('#--before-all-- section must contain a code block');
|
||||
if (codeNode.lang !== 'javascript' && codeNode.lang !== 'js')
|
||||
throw Error('#--before-all-- hook must be written in JavaScript');
|
||||
|
||||
const beforeAll = getBeforeAll(codeNode);
|
||||
file.data.hooks = { beforeAll };
|
||||
}
|
||||
}
|
||||
|
||||
function getBeforeAll(codeNode) {
|
||||
const beforeAll = codeNode.value;
|
||||
|
||||
return beforeAll;
|
||||
}
|
||||
|
||||
module.exports = plugin;
|
||||
@@ -0,0 +1,69 @@
|
||||
const parseFixture = require('../__fixtures__/parse-fixture');
|
||||
|
||||
const addBeforeHook = require('./add-before-hook');
|
||||
|
||||
describe('add-before-hook plugin', () => {
|
||||
let withBeforeHookAST,
|
||||
withEmptyHookAST,
|
||||
withInvalidHookAST,
|
||||
withAnotherInvalidHookAST,
|
||||
withNonJSHookAST;
|
||||
|
||||
const plugin = addBeforeHook();
|
||||
let file = { data: {} };
|
||||
|
||||
beforeAll(async () => {
|
||||
withBeforeHookAST = await parseFixture('with-before-hook.md');
|
||||
withEmptyHookAST = await parseFixture('with-empty-before-hook.md');
|
||||
withInvalidHookAST = await parseFixture('with-invalid-before-hook.md');
|
||||
withAnotherInvalidHookAST = await parseFixture(
|
||||
'with-another-invalid-before-hook.md'
|
||||
);
|
||||
withNonJSHookAST = await parseFixture('with-non-js-before-hook.md');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
file = { data: {} };
|
||||
});
|
||||
|
||||
it('returns a function', () => {
|
||||
expect(typeof plugin).toEqual('function');
|
||||
});
|
||||
|
||||
it('adds a `hooks` property to `file.data`', () => {
|
||||
plugin(withBeforeHookAST, file);
|
||||
expect('hooks' in file.data).toBe(true);
|
||||
});
|
||||
|
||||
it('populates `hooks.beforeAll` with the contents of the code block', () => {
|
||||
plugin(withBeforeHookAST, file);
|
||||
expect(file.data.hooks.beforeAll).toBe(`// before all code
|
||||
function foo() {
|
||||
return 'bar';
|
||||
}
|
||||
foo();`);
|
||||
});
|
||||
|
||||
it('should throw an error if the beforeAll section has more than one child', () => {
|
||||
expect(() => plugin(withInvalidHookAST, file)).toThrow(
|
||||
`#--before-all-- section must only contain a single code block`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the beforeAll section does not contain a code block', () => {
|
||||
expect(() => plugin(withAnotherInvalidHookAST, file)).toThrow(
|
||||
`#--before-all-- section must contain a code block`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the code language is not javascript', () => {
|
||||
expect(() => plugin(withNonJSHookAST, file)).toThrow(
|
||||
`#--before-all-- hook must be written in JavaScript`
|
||||
);
|
||||
});
|
||||
|
||||
it('should have an output to match the snapshot', () => {
|
||||
plugin(withBeforeHookAST, file);
|
||||
expect(file.data).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user