From fd7a8c0d5ecb77de2bd4657392b7a49c33061ae9 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Tue, 30 Jun 2020 06:33:28 +0200 Subject: [PATCH] feat: allow next challenge's seed to be a solution (#39145) * feat: allow next challenge's seed to be a solution --- curriculum/test/test-challenges.js | 18 +- curriculum/test/utils/sort-challenges.js | 5 + curriculum/test/utils/sort-challenges.test.js | 252 ++++++++++++++++++ 3 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 curriculum/test/utils/sort-challenges.js create mode 100644 curriculum/test/utils/sort-challenges.test.js diff --git a/curriculum/test/test-challenges.js b/curriculum/test/test-challenges.js index 5fdbed433c..675dc094e8 100644 --- a/curriculum/test/test-challenges.js +++ b/curriculum/test/test-challenges.js @@ -23,7 +23,7 @@ const { const { assert, AssertionError } = require('chai'); const Mocha = require('mocha'); -const { flatten } = require('lodash'); +const { flatten, isEmpty } = require('lodash'); const jsdom = require('jsdom'); @@ -51,6 +51,7 @@ const { } = require('../../client/src/templates/Challenges/utils/build'); const { createPoly } = require('../../utils/polyvinyl'); +const { sortChallenges } = require('./utils/sort-challenges'); const testEvaluator = require('../../client/config/test-evaluator').filename; @@ -226,7 +227,9 @@ async function getChallenges(lang) { return [...challengeArray, ...flatten(challengesForBlock)]; }, []) ); - return challenges; + // This matches the order Gatsby uses (via a GraphQL query). Ideally both + // should be sourced and sorted using a single query, but we're not there yet. + return sortChallenges(challenges); } function validateBlock(challenge) { @@ -245,7 +248,7 @@ function populateTestsForLang({ lang, challenges, meta }) { describe(`Check challenges (${lang})`, function() { this.timeout(5000); - challenges.forEach(challenge => { + challenges.forEach((challenge, id) => { const dashedBlockName = dasherize(challenge.block); describe(challenge.block || 'No block', function() { describe(challenge.title || 'No title', function() { @@ -367,6 +370,15 @@ function populateTestsForLang({ lang, challenges, meta }) { }); let { solutions = [] } = challenge; + // if there are no solutions in the challenge, it's assumed the next + // challenge's seed will be a solution to the current challenge. + // This is expected to happen in the project based curriculum. + if (isEmpty(solutions)) { + const nextChallenge = challenges[id + 1]; + if (nextChallenge) { + solutions = [nextChallenge.files[0].contents]; + } + } const noSolution = new RegExp('// solution required'); solutions = solutions.filter( solution => !!solution && !noSolution.test(solution) diff --git a/curriculum/test/utils/sort-challenges.js b/curriculum/test/utils/sort-challenges.js new file mode 100644 index 0000000000..0fe819252e --- /dev/null +++ b/curriculum/test/utils/sort-challenges.js @@ -0,0 +1,5 @@ +const { sortBy } = require('lodash'); + +exports.sortChallenges = arr => { + return sortBy(arr, ['superOrder', 'order', 'challengeOrder']); +}; diff --git a/curriculum/test/utils/sort-challenges.test.js b/curriculum/test/utils/sort-challenges.test.js new file mode 100644 index 0000000000..43afcbaf2c --- /dev/null +++ b/curriculum/test/utils/sort-challenges.test.js @@ -0,0 +1,252 @@ +/* global expect */ +const { sortChallenges } = require('./sort-challenges'); + +const challenges = [ + { + name: 'HTML - project 1 - step 1', + superOrder: 1, + order: 1, + challengeOrder: 1 + }, + { + name: 'HTML - project 1 - step 2', + superOrder: 1, + order: 1, + challengeOrder: 2 + }, + { + name: 'HTML - project 1 - step 3', + superOrder: 1, + order: 1, + challengeOrder: 3 + }, + { + name: 'HTML - project 1 - step 4', + superOrder: 1, + order: 1, + challengeOrder: 4 + }, + { + name: 'HTML - project 2 - step 1', + superOrder: 1, + order: 2, + challengeOrder: 1 + }, + { + name: 'HTML - project 2 - step 2', + superOrder: 1, + order: 2, + challengeOrder: 2 + }, + { + name: 'HTML - project 2 - step 3', + superOrder: 1, + order: 2, + challengeOrder: 3 + }, + { + name: 'HTML - project 2 - step 4', + superOrder: 1, + order: 2, + challengeOrder: 4 + }, + { + name: 'HTML - project 3 - step 1', + superOrder: 1, + order: 3, + challengeOrder: 1 + }, + { + name: 'HTML - project 3 - step 2', + superOrder: 1, + order: 3, + challengeOrder: 2 + }, + { + name: 'HTML - project 3 - step 3', + superOrder: 1, + order: 3, + challengeOrder: 3 + }, + { + name: 'HTML - project 3 - step 4', + superOrder: 1, + order: 3, + challengeOrder: 4 + }, + { + name: 'CSS - project 1 - step 1', + superOrder: 2, + order: 1, + challengeOrder: 1 + }, + { + name: 'CSS - project 1 - step 2', + superOrder: 2, + order: 1, + challengeOrder: 2 + }, + { + name: 'CSS - project 1 - step 3', + superOrder: 2, + order: 1, + challengeOrder: 3 + }, + { + name: 'CSS - project 1 - step 4', + superOrder: 2, + order: 1, + challengeOrder: 4 + }, + { + name: 'CSS - project 2 - step 1', + superOrder: 2, + order: 2, + challengeOrder: 1 + }, + { + name: 'CSS - project 2 - step 2', + superOrder: 2, + order: 2, + challengeOrder: 2 + }, + { + name: 'CSS - project 2 - step 3', + superOrder: 2, + order: 2, + challengeOrder: 3 + }, + { + name: 'CSS - project 2 - step 4', + superOrder: 2, + order: 2, + challengeOrder: 4 + }, + { + name: 'CSS - project 3 - step 1', + superOrder: 2, + order: 3, + challengeOrder: 1 + }, + { + name: 'CSS - project 3 - step 2', + superOrder: 2, + order: 3, + challengeOrder: 2 + }, + { + name: 'CSS - project 3 - step 3', + superOrder: 2, + order: 3, + challengeOrder: 3 + }, + { + name: 'CSS - project 3 - step 4', + superOrder: 2, + order: 3, + challengeOrder: 4 + }, + { + name: 'JS - project 1 - step 1', + superOrder: 3, + order: 1, + challengeOrder: 1 + }, + { + name: 'JS - project 1 - step 2', + superOrder: 3, + order: 1, + challengeOrder: 2 + }, + { + name: 'JS - project 1 - step 3', + superOrder: 3, + order: 1, + challengeOrder: 3 + }, + { + name: 'JS - project 1 - step 4', + superOrder: 3, + order: 1, + challengeOrder: 4 + }, + { + name: 'JS - project 2 - step 1', + superOrder: 3, + order: 2, + challengeOrder: 1 + }, + { + name: 'JS - project 2 - step 2', + superOrder: 3, + order: 2, + challengeOrder: 2 + }, + { + name: 'JS - project 2 - step 3', + superOrder: 3, + order: 2, + challengeOrder: 3 + }, + { + name: 'JS - project 2 - step 4', + superOrder: 3, + order: 2, + challengeOrder: 4 + }, + { + name: 'JS - project 3 - step 1', + superOrder: 3, + order: 3, + challengeOrder: 1 + }, + { + name: 'JS - project 3 - step 2', + superOrder: 3, + order: 3, + challengeOrder: 2 + }, + { + name: 'JS - project 3 - step 3', + superOrder: 3, + order: 3, + challengeOrder: 3 + }, + { + name: 'JS - project 3 - step 4', + superOrder: 3, + order: 3, + challengeOrder: 4 + } +]; + +describe('sortChallenges', () => { + it('sorts challenges by superblock, block and challenge order', () => { + const copyOfChallenges = [...challenges]; + shuffle(copyOfChallenges); + const actualChallenges = sortChallenges(copyOfChallenges); + + expect(actualChallenges).toEqual(challenges); + }); + + it('does not change the original array', () => { + const copyOfChallenges = [...challenges]; + copyOfChallenges[0] = { + name: 'JS - project 3 - step 4', + superOrder: 3, + order: 3, + challengeOrder: 4 + }; + const actualChallenges = sortChallenges(copyOfChallenges); + + expect(actualChallenges[0]).not.toEqual(copyOfChallenges[0]); + }); +}); + +// Use the Fisher–Yates shuffle algorithm to shuffle array +const shuffle = arr => { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } +};