feat(client): support beforeAll in DOM challenge tests (#59001)

This commit is contained in:
Oliver Eyton-Williams
2025-02-28 13:03:18 +01:00
committed by GitHub
parent 1c33d37d8a
commit 96d62330cd
43 changed files with 531 additions and 25 deletions

View File

@@ -172,6 +172,7 @@ export type ChallengeNode = {
head: string[];
hasEditableBoundaries: boolean;
helpCategory: string;
hooks?: { beforeAll: string };
id: string;
instructions: string;
isComingSoon: boolean;

View File

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

View File

@@ -8,6 +8,7 @@ export const actionTypes = createTypes(
[
'createFiles',
'createQuestion',
'initHooks',
'initTests',
'initConsole',
'initLogs',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)`.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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