From 2a7b220a4f4ddaa485ce2078fd92fb50516e8f2b Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Mon, 7 Jul 2025 12:46:09 +0200 Subject: [PATCH] feat: support beforeEach and afterEach (#60921) Co-authored-by: Shaun Hamilton --- client/gatsby-node.js | 6 + client/src/redux/prop-types.ts | 8 +- .../src/templates/Challenges/classic/show.tsx | 5 +- .../src/templates/Challenges/utils/frame.ts | 5 +- .../global-scope-and-functions.md | 43 +---- .../66ed41f912d0bb1dc62da5dd.md | 38 ++--- curriculum/schema/challenge-schema.js | 4 +- .../__fixtures__/with-after-each-hook.md | 40 +++++ .../with-another-invalid-after-each-hook.md | 34 ++++ .../with-another-invalid-before-each-hook.md | 34 ++++ .../__fixtures__/with-before-each-hook.md | 40 +++++ .../with-invalid-after-each-hook.md | 42 +++++ .../with-invalid-before-each-hook.md | 42 +++++ .../with-non-js-after-each-hook.md | 40 +++++ .../with-non-js-before-each-hook.md | 40 +++++ tools/challenge-parser/parser/index.js | 4 +- ...ok.test.js.snap => add-hooks.test.js.snap} | 0 .../parser/plugins/add-before-hook.js | 33 ---- .../parser/plugins/add-before-hook.test.js | 67 -------- .../parser/plugins/add-hooks.js | 40 +++++ .../parser/plugins/add-hooks.test.js | 149 ++++++++++++++++++ 21 files changed, 549 insertions(+), 165 deletions(-) create mode 100644 tools/challenge-parser/parser/__fixtures__/with-after-each-hook.md create mode 100644 tools/challenge-parser/parser/__fixtures__/with-another-invalid-after-each-hook.md create mode 100644 tools/challenge-parser/parser/__fixtures__/with-another-invalid-before-each-hook.md create mode 100644 tools/challenge-parser/parser/__fixtures__/with-before-each-hook.md create mode 100644 tools/challenge-parser/parser/__fixtures__/with-invalid-after-each-hook.md create mode 100644 tools/challenge-parser/parser/__fixtures__/with-invalid-before-each-hook.md create mode 100644 tools/challenge-parser/parser/__fixtures__/with-non-js-after-each-hook.md create mode 100644 tools/challenge-parser/parser/__fixtures__/with-non-js-before-each-hook.md rename tools/challenge-parser/parser/plugins/__snapshots__/{add-before-hook.test.js.snap => add-hooks.test.js.snap} (100%) delete mode 100644 tools/challenge-parser/parser/plugins/add-before-hook.js delete mode 100644 tools/challenge-parser/parser/plugins/add-before-hook.test.js create mode 100644 tools/challenge-parser/parser/plugins/add-hooks.js create mode 100644 tools/challenge-parser/parser/plugins/add-hooks.test.js diff --git a/client/gatsby-node.js b/client/gatsby-node.js index 8c6e587d802..79f797459b1 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -300,6 +300,7 @@ exports.createSchemaCustomization = ({ actions }) => { challengeFiles: [FileContents] chapter: String explanation: String + hooks: Hooks notes: String url: String assignments: [String] @@ -379,6 +380,11 @@ exports.createSchemaCustomization = ({ actions }) => { distractors: [String] answer: String } + type Hooks { + beforeEach: String + afterEach: String + beforeAll: String + } `; createTypes(typeDefs); }; diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index 1c8851a4ba3..7787c2d3e11 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -192,7 +192,7 @@ export type ChallengeNode = { head: string[]; hasEditableBoundaries: boolean; helpCategory: string; - hooks?: { beforeAll: string }; + hooks?: Hooks; id: string; instructions: string; isComingSoon: boolean; @@ -240,6 +240,12 @@ export type ChallengeNode = { }; }; +export interface Hooks { + beforeAll?: string; + beforeEach?: string; + afterEach?: string; +} + type Quiz = { questions: QuizQuestion[]; }; diff --git a/client/src/templates/Challenges/classic/show.tsx b/client/src/templates/Challenges/classic/show.tsx index bd129aefd7f..2b885056bba 100644 --- a/client/src/templates/Challenges/classic/show.tsx +++ b/client/src/templates/Challenges/classic/show.tsx @@ -20,6 +20,7 @@ import type { ChallengeFiles, ChallengeMeta, ChallengeNode, + Hooks, ResizeProps, SavedChallenge, SavedChallengeFiles, @@ -110,7 +111,7 @@ interface ShowClassicProps extends Pick { challengeFiles: ChallengeFiles; initConsole: (arg0: string) => void; initTests: (tests: Test[]) => void; - initHooks: (hooks?: { beforeAll: string }) => void; + initHooks: (hooks?: Hooks) => void; initVisibleEditors: () => void; isChallengeCompleted: boolean; output: string; @@ -579,6 +580,8 @@ export const query = graphql` forumTopicId hooks { beforeAll + beforeEach + afterEach } fields { blockName diff --git a/client/src/templates/Challenges/utils/frame.ts b/client/src/templates/Challenges/utils/frame.ts index e267041f388..7e0cb5ce84f 100644 --- a/client/src/templates/Challenges/utils/frame.ts +++ b/client/src/templates/Challenges/utils/frame.ts @@ -11,6 +11,7 @@ import type { FrameDocument, PythonDocument } from '../../../../../tools/client-plugins/browser-scripts'; +import { Hooks } from '../../../redux/prop-types'; export const helperVersion = _helperVersion; @@ -28,10 +29,6 @@ export interface Source { editableContents: string; } -interface Hooks { - beforeAll?: string; -} - export interface Context { window?: Window & typeof globalThis & { diff --git a/curriculum/challenges/english/02-javascript-algorithms-and-data-structures/basic-javascript/global-scope-and-functions.md b/curriculum/challenges/english/02-javascript-algorithms-and-data-structures/basic-javascript/global-scope-and-functions.md index 8055086e22c..738066bf41f 100644 --- a/curriculum/challenges/english/02-javascript-algorithms-and-data-structures/basic-javascript/global-scope-and-functions.md +++ b/curriculum/challenges/english/02-javascript-algorithms-and-data-structures/basic-javascript/global-scope-and-functions.md @@ -18,6 +18,12 @@ Using `let` or `const`, declare a global variable named `myGlobal` outside of an Inside function `fun1`, assign `5` to `oopsGlobal` ***without*** using the `var`, `let` or `const` keywords. +# --before-each-- + +```js +var oopsGlobal; +``` + # --hints-- `myGlobal` should be defined @@ -41,45 +47,12 @@ assert(/(let|const)\s+myGlobal/.test(__helpers.removeJSComments(code))); `oopsGlobal` should be a global variable and have a value of `5` ```js -assert(typeof oopsGlobal != 'undefined' && oopsGlobal === 5); +fun1(); +assert(typeof oopsGlobal != 'undefined'); ``` # --seed-- -## --before-user-code-- - -```js -var logOutput = ""; -var originalConsole = console -function capture() { - var nativeLog = console.log; - console.log = function (message) { - logOutput = message; - if(nativeLog.apply) { - nativeLog.apply(originalConsole, arguments); - } else { - var nativeMsg = Array.prototype.slice.apply(arguments).join(' '); - nativeLog(nativeMsg); - } - }; -} - -function uncapture() { - console.log = originalConsole.log; -} -var oopsGlobal; -capture(); -``` - -## --after-user-code-- - -```js -fun1(); -fun2(); -uncapture(); -(function() { return logOutput || "console.log never called"; })(); -``` - ## --seed-contents-- ```js diff --git a/curriculum/challenges/english/25-front-end-development/lab-javascript-trivia-bot/66ed41f912d0bb1dc62da5dd.md b/curriculum/challenges/english/25-front-end-development/lab-javascript-trivia-bot/66ed41f912d0bb1dc62da5dd.md index f765b99f446..16e33fc19b4 100644 --- a/curriculum/challenges/english/25-front-end-development/lab-javascript-trivia-bot/66ed41f912d0bb1dc62da5dd.md +++ b/curriculum/challenges/english/25-front-end-development/lab-javascript-trivia-bot/66ed41f912d0bb1dc62da5dd.md @@ -23,12 +23,19 @@ Fulfill the user stories below and get all the tests to pass to complete the lab 1. You should log the `codingFact` to the console a third time. 1. You should log `"It was fun sharing these facts with you. Goodbye! - (botName) from (botLocation)."` to the console as a farewell statement from the bot. +# --before-each-- + +```js +const spy = __helpers.spyOn(console, 'log'); +const getLogs = () => spy.calls.map(call => call?.[0]); +``` + # --hints-- You should log `"Hello! I'm your coding fun fact guide!"` to the console. ```js -assert.equal(output[0], "Hello! I'm your coding fun fact guide!") +assert.equal(getLogs()[0], "Hello! I'm your coding fun fact guide!") ``` You should declare a `botName` variable. Double check for any spelling or casing errors. @@ -71,7 +78,7 @@ You should log to the console `"My name is (botName) and I live on (botLocation) ```js const codeWithoutComments = __helpers.removeJSComments(code); -assert.equal(output[1], `My name is ${botName} and I live on ${botLocation}.`) +assert.equal(getLogs()[1], `My name is ${botName} and I live on ${botLocation}.`) assert.match(codeWithoutComments, /is ("|')\s*\+\s*botName\s*\+\s*("|') and I live on \2\s*\+\s*botLocation\s*\+\s*('|")\./) ``` @@ -79,7 +86,7 @@ You should log to the console `"My favorite programming language is (favoriteLan ```js const codeWithoutComments = __helpers.removeJSComments(code); -assert.equal(output[2], `My favorite programming language is ${favoriteLanguage}.`) +assert.equal(getLogs()[2], `My favorite programming language is ${favoriteLanguage}.`) assert.match(codeWithoutComments, /language is ('|")\s*\+\s*favoriteLanguage\s*\+\s*('|")\./); ``` @@ -106,7 +113,7 @@ You should log `codingFact` to the console. ```js const codeWithoutComments = __helpers.removeJSComments(code); const loggingCodingFacts = codeWithoutComments.match(/console\.log\(\s*codingFact\s*\)/g) -assert.include(output[3], favoriteLanguage); +assert.include(getLogs()[3], favoriteLanguage); assert.isAtLeast(loggingCodingFacts.length, 1); ``` @@ -116,8 +123,8 @@ You should assign a new value to `codingFact` that also contains `favoriteLangua const codeWithoutComments = __helpers.removeJSComments(code); const loggingCodingFacts = codeWithoutComments.match(/console\.log\(\s*codingFact\s*\)/g) const [first, second, third] = codeWithoutComments.match(/(let )?\s*codingFact\s*=\s*(("|')?.+?\2?\s*\+\s*|favoriteLanguage\s*\+\s*(("|')?.+?\2?))/g); -assert.include(output[4], favoriteLanguage); -assert.notEqual(output[4], output[3]); +assert.include(getLogs()[4], favoriteLanguage); +assert.notEqual(getLogs()[4], getLogs()[3]); assert.isAtLeast(loggingCodingFacts.length, 2); assert.exists(second); assert.isNotEmpty(codingFact); @@ -129,9 +136,9 @@ You should assign a value to `codingFact` for the third time that also contains const codeWithoutComments = __helpers.removeJSComments(code); const loggingCodingFacts = codeWithoutComments.match(/console\.log\(\s*codingFact\s*\)/g) const [first, second, third] = codeWithoutComments.match(/(let )?\s*codingFact\s*=\s*(("|')?.+?\2?\s*\+\s*|favoriteLanguage\s*\+\s*(("|')?.+?\2?))/g); -assert.include(output[5], favoriteLanguage); -assert.notEqual(output[5], output[4]); -assert.equal(output[5], codingFact); +assert.include(getLogs()[5], favoriteLanguage); +assert.notEqual(getLogs()[5], getLogs()[4]); +assert.equal(getLogs()[5], codingFact); assert.lengthOf(loggingCodingFacts, 3); assert.exists(third); assert.isNotEmpty(codingFact); @@ -141,23 +148,12 @@ You should log to the console `"It was fun sharing these facts with you. Goodbye ```js const codeWithoutComments = __helpers.removeJSComments(code); -assert.equal(output[6], `It was fun sharing these facts with you. Goodbye! - ${botName} from ${botLocation}.`); +assert.equal(getLogs()[6], `It was fun sharing these facts with you. Goodbye! - ${botName} from ${botLocation}.`); assert.match(codeWithoutComments, /\. Goodbye! - ("|')\s*\+\s*botName\s*\+\s*('|") from \2\s*\+\s*botLocation\s*\+\s*("|')\./) ``` # --seed-- -## --before-user-code-- - -```js -const temp = console.log -const output = [] -console.log = function (...args) { -temp(...args) -output.push(...args) -} -``` - ## --seed-contents-- ```js diff --git a/curriculum/schema/challenge-schema.js b/curriculum/schema/challenge-schema.js index 8eab41bbc97..2d9805c6550 100644 --- a/curriculum/schema/challenge-schema.js +++ b/curriculum/schema/challenge-schema.js @@ -302,7 +302,9 @@ const schema = Joi.object() superOrder: Joi.number(), suborder: Joi.number(), hooks: Joi.object().keys({ - beforeAll: Joi.string().allow('') + beforeAll: Joi.string().allow(''), + beforeEach: Joi.string().allow(''), + afterEach: Joi.string().allow('') }), tests: Joi.array() .items( diff --git a/tools/challenge-parser/parser/__fixtures__/with-after-each-hook.md b/tools/challenge-parser/parser/__fixtures__/with-after-each-hook.md new file mode 100644 index 00000000000..dd7cf8c27a2 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-after-each-hook.md @@ -0,0 +1,40 @@ +# --description-- + +Paragraph 1 + +```html +code example +``` + +# --after-each-- + +```js +// after each code +function cleanup() { + return 'cleaned up'; +} +cleanup(); +``` + +# --hints-- + +First hint + +```js +// test code +``` + +Second hint with code + +```js +// more test code +``` + +Third *hint* with code and `inline code` + +```js +// more test code +if(let x of xs) { + console.log(x); +} +``` diff --git a/tools/challenge-parser/parser/__fixtures__/with-another-invalid-after-each-hook.md b/tools/challenge-parser/parser/__fixtures__/with-another-invalid-after-each-hook.md new file mode 100644 index 00000000000..51301e721cc --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-another-invalid-after-each-hook.md @@ -0,0 +1,34 @@ +# --description-- + +Paragraph 1 + +```html +code example +``` + +# --after-each-- + +gubbins + +# --hints-- + +First hint + +```js +// test code +``` + +Second hint with code + +```js +// more test code +``` + +Third *hint* with code and `inline code` + +```js +// more test code +if(let x of xs) { + console.log(x); +} +``` diff --git a/tools/challenge-parser/parser/__fixtures__/with-another-invalid-before-each-hook.md b/tools/challenge-parser/parser/__fixtures__/with-another-invalid-before-each-hook.md new file mode 100644 index 00000000000..61117072886 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-another-invalid-before-each-hook.md @@ -0,0 +1,34 @@ +# --description-- + +Paragraph 1 + +```html +code example +``` + +# --before-each-- + +gubbins + +# --hints-- + +First hint + +```js +// test code +``` + +Second hint with code + +```js +// more test code +``` + +Third *hint* with code and `inline code` + +```js +// more test code +if(let x of xs) { + console.log(x); +} +``` diff --git a/tools/challenge-parser/parser/__fixtures__/with-before-each-hook.md b/tools/challenge-parser/parser/__fixtures__/with-before-each-hook.md new file mode 100644 index 00000000000..716b042c572 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-before-each-hook.md @@ -0,0 +1,40 @@ +# --description-- + +Paragraph 1 + +```html +code example +``` + +# --before-each-- + +```js +// before each code +function setup() { + return 'initialized'; +} +setup(); +``` + +# --hints-- + +First hint + +```js +// test code +``` + +Second hint with code + +```js +// more test code +``` + +Third *hint* with code and `inline code` + +```js +// more test code +if(let x of xs) { + console.log(x); +} +``` diff --git a/tools/challenge-parser/parser/__fixtures__/with-invalid-after-each-hook.md b/tools/challenge-parser/parser/__fixtures__/with-invalid-after-each-hook.md new file mode 100644 index 00000000000..ac74046d979 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-invalid-after-each-hook.md @@ -0,0 +1,42 @@ +# --description-- + +Paragraph 1 + +```html +code example +``` + +# --after-each-- + +```js +// after each code +function cleanup() { + return 'cleaned up'; +} +cleanup(); +``` + +gubbins + +# --hints-- + +First hint + +```js +// test code +``` + +Second hint with code + +```js +// more test code +``` + +Third *hint* with code and `inline code` + +```js +// more test code +if(let x of xs) { + console.log(x); +} +``` diff --git a/tools/challenge-parser/parser/__fixtures__/with-invalid-before-each-hook.md b/tools/challenge-parser/parser/__fixtures__/with-invalid-before-each-hook.md new file mode 100644 index 00000000000..bfb942decbb --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-invalid-before-each-hook.md @@ -0,0 +1,42 @@ +# --description-- + +Paragraph 1 + +```html +code example +``` + +# --before-each-- + +```js +// before each code +function setup() { + return 'initialized'; +} +setup(); +``` + +gubbins + +# --hints-- + +First hint + +```js +// test code +``` + +Second hint with code + +```js +// more test code +``` + +Third *hint* with code and `inline code` + +```js +// more test code +if(let x of xs) { + console.log(x); +} +``` diff --git a/tools/challenge-parser/parser/__fixtures__/with-non-js-after-each-hook.md b/tools/challenge-parser/parser/__fixtures__/with-non-js-after-each-hook.md new file mode 100644 index 00000000000..597598b2dd9 --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-non-js-after-each-hook.md @@ -0,0 +1,40 @@ +# --description-- + +Paragraph 1 + +```html +code example +``` + +# --after-each-- + +```ts +// after each code +function cleanup() { + return 'cleaned up'; +} +cleanup(); +``` + +# --hints-- + +First hint + +```js +// test code +``` + +Second hint with code + +```js +// more test code +``` + +Third *hint* with code and `inline code` + +```js +// more test code +if(let x of xs) { + console.log(x); +} +``` diff --git a/tools/challenge-parser/parser/__fixtures__/with-non-js-before-each-hook.md b/tools/challenge-parser/parser/__fixtures__/with-non-js-before-each-hook.md new file mode 100644 index 00000000000..5ab1d2a060f --- /dev/null +++ b/tools/challenge-parser/parser/__fixtures__/with-non-js-before-each-hook.md @@ -0,0 +1,40 @@ +# --description-- + +Paragraph 1 + +```html +code example +``` + +# --before-each-- + +```ts +// before each code +function setup() { + return 'initialized'; +} +setup(); +``` + +# --hints-- + +First hint + +```js +// test code +``` + +Second hint with code + +```js +// more test code +``` + +Third *hint* with code and `inline code` + +```js +// more test code +if(let x of xs) { + console.log(x); +} +``` diff --git a/tools/challenge-parser/parser/index.js b/tools/challenge-parser/parser/index.js index 0b2f5943084..d8b76a4bf26 100644 --- a/tools/challenge-parser/parser/index.js +++ b/tools/challenge-parser/parser/index.js @@ -7,7 +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 addHooks = require('./plugins/add-hooks'); const addTests = require('./plugins/add-tests'); const addText = require('./plugins/add-text'); const addVideoQuestion = require('./plugins/add-video-question'); @@ -53,7 +53,7 @@ const processor = unified() .use(addAssignment) .use(addScene) .use(addQuizzes) - .use(addBeforeHook) + .use(addHooks) .use(addTests) .use(addText, [ 'description', diff --git a/tools/challenge-parser/parser/plugins/__snapshots__/add-before-hook.test.js.snap b/tools/challenge-parser/parser/plugins/__snapshots__/add-hooks.test.js.snap similarity index 100% rename from tools/challenge-parser/parser/plugins/__snapshots__/add-before-hook.test.js.snap rename to tools/challenge-parser/parser/plugins/__snapshots__/add-hooks.test.js.snap diff --git a/tools/challenge-parser/parser/plugins/add-before-hook.js b/tools/challenge-parser/parser/plugins/add-before-hook.js deleted file mode 100644 index 53e8ab3b984..00000000000 --- a/tools/challenge-parser/parser/plugins/add-before-hook.js +++ /dev/null @@ -1,33 +0,0 @@ -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; diff --git a/tools/challenge-parser/parser/plugins/add-before-hook.test.js b/tools/challenge-parser/parser/plugins/add-before-hook.test.js deleted file mode 100644 index 414b74e0192..00000000000 --- a/tools/challenge-parser/parser/plugins/add-before-hook.test.js +++ /dev/null @@ -1,67 +0,0 @@ -const parseFixture = require('../__fixtures__/parse-fixture'); - -const addBeforeHook = require('./add-before-hook'); - -describe('add-before-hook plugin', () => { - let withBeforeHookAST, - withInvalidHookAST, - withAnotherInvalidHookAST, - withNonJSHookAST; - - const plugin = addBeforeHook(); - let file = { data: {} }; - - beforeAll(async () => { - withBeforeHookAST = await parseFixture('with-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(); - }); -}); diff --git a/tools/challenge-parser/parser/plugins/add-hooks.js b/tools/challenge-parser/parser/plugins/add-hooks.js new file mode 100644 index 00000000000..1d9437794ad --- /dev/null +++ b/tools/challenge-parser/parser/plugins/add-hooks.js @@ -0,0 +1,40 @@ +const { getSection } = require('./utils/get-section'); + +function plugin() { + return transformer; + + function transformer(tree, file) { + const beforeAll = getHook(tree, '--before-all--'); + const beforeEach = getHook(tree, '--before-each--'); + const afterEach = getHook(tree, '--after-each--'); + + if (!beforeAll && !beforeEach && !afterEach) return; + + file.data.hooks = file.data.hooks = { + ...(beforeAll && { beforeAll }), + ...(beforeEach && { beforeEach }), + ...(afterEach && { afterEach }) + }; + } +} + +function getHook(tree, sectionName) { + const section = getSection(tree, sectionName); + + if (section.length === 0) return; + if (section.length > 1) + throw Error( + `# ${sectionName} section must only contain a single code block` + ); + + const codeNode = section[0]; + + if (codeNode.type !== 'code') + throw Error(`# ${sectionName} section must contain a code block`); + if (codeNode.lang !== 'javascript' && codeNode.lang !== 'js') + throw Error(`# ${sectionName} hook must be written in JavaScript`); + + return codeNode.value; +} + +module.exports = plugin; diff --git a/tools/challenge-parser/parser/plugins/add-hooks.test.js b/tools/challenge-parser/parser/plugins/add-hooks.test.js new file mode 100644 index 00000000000..4243b3d7063 --- /dev/null +++ b/tools/challenge-parser/parser/plugins/add-hooks.test.js @@ -0,0 +1,149 @@ +const parseFixture = require('../__fixtures__/parse-fixture'); + +const addBeforeHook = require('./add-hooks'); + +describe('add-before-hook plugin', () => { + let withBeforeHookAST, + withInvalidHookAST, + withAnotherInvalidHookAST, + withNonJSHookAST, + withBeforeEachHookAST, + withInvalidBeforeEachHookAST, + withAnotherInvalidBeforeEachHookAST, + withNonJSBeforeEachHookAST, + withAfterEachHookAST, + withInvalidAfterEachHookAST, + withAnotherInvalidAfterEachHookAST, + withNonJSAfterEachHookAST; + + const plugin = addBeforeHook(); + let file = { data: {} }; + + beforeAll(async () => { + withBeforeHookAST = await parseFixture('with-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'); + withBeforeEachHookAST = await parseFixture('with-before-each-hook.md'); + withInvalidBeforeEachHookAST = await parseFixture( + 'with-invalid-before-each-hook.md' + ); + withAnotherInvalidBeforeEachHookAST = await parseFixture( + 'with-another-invalid-before-each-hook.md' + ); + withNonJSBeforeEachHookAST = await parseFixture( + 'with-non-js-before-each-hook.md' + ); + withAfterEachHookAST = await parseFixture('with-after-each-hook.md'); + withInvalidAfterEachHookAST = await parseFixture( + 'with-invalid-after-each-hook.md' + ); + withAnotherInvalidAfterEachHookAST = await parseFixture( + 'with-another-invalid-after-each-hook.md' + ); + withNonJSAfterEachHookAST = await parseFixture( + 'with-non-js-after-each-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(); + }); + + it('populates `hooks.beforeEach` with the contents of the code block', () => { + plugin(withBeforeEachHookAST, file); + expect(file.data.hooks.beforeEach).toBe(`// before each code +function setup() { + return 'initialized'; +} +setup();`); + }); + + it('should throw an error if the beforeEach section has more than one child', () => { + expect(() => plugin(withInvalidBeforeEachHookAST, file)).toThrow( + `# --before-each-- section must only contain a single code block` + ); + }); + + it('should throw an error if the beforeEach section does not contain a code block', () => { + expect(() => plugin(withAnotherInvalidBeforeEachHookAST, file)).toThrow( + `# --before-each-- section must contain a code block` + ); + }); + + it('should throw an error if the beforeEach code language is not javascript', () => { + expect(() => plugin(withNonJSBeforeEachHookAST, file)).toThrow( + `# --before-each-- hook must be written in JavaScript` + ); + }); + + it('populates `hooks.afterEach` with the contents of the code block', () => { + plugin(withAfterEachHookAST, file); + expect(file.data.hooks.afterEach).toBe(`// after each code +function cleanup() { + return 'cleaned up'; +} +cleanup();`); + }); + + it('should throw an error if the afterEach section has more than one child', () => { + expect(() => plugin(withInvalidAfterEachHookAST, file)).toThrow( + `# --after-each-- section must only contain a single code block` + ); + }); + + it('should throw an error if the afterEach section does not contain a code block', () => { + expect(() => plugin(withAnotherInvalidAfterEachHookAST, file)).toThrow( + `# --after-each-- section must contain a code block` + ); + }); + + it('should throw an error if the afterEach code language is not javascript', () => { + expect(() => plugin(withNonJSAfterEachHookAST, file)).toThrow( + `# --after-each-- hook must be written in JavaScript` + ); + }); +});