diff --git a/README.md b/README.md index 482e61c..14502cb 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,11 @@ The configuration file is created by matching the `level` and `step` ids between Tutorial description. -## L1 This is a level with id = 1 +## 1. This is a level with id = 1 This level has two steps... -### L1S1 First step +### 1.1 First step The first step with id L1S1. The Step id should start with the level id. @@ -62,7 +62,7 @@ The first step with id L1S1. The Step id should start with the level id. - The second hint that will show - The third and final hint, as it is last in order -### L1S2 The second step +### 1.2 The second step The second step... ``` @@ -72,10 +72,10 @@ The second step... ```yaml --- levels: - - id: L1 + - id: "1" config: {} steps: - - id: L1S1 + - id: "1.1" setup: files: - package.json @@ -86,7 +86,7 @@ levels: - package.json commands: - npm install - - id: L1S2 + - id: "1.2" setup: files: - src/server.js @@ -104,23 +104,19 @@ commit 8e0e3a42ae565050181fdb68298114df21467a74 (HEAD -> v2, origin/v2) Author: creator Date: Sun May 3 16:16:01 2020 -0700 - L1S1Q setup step 1 for level 1 + 1.1 setup for level 1, step 1 commit 9499611fc9b311040dcabaf2d98439fc0c356cc9 Author: creator Date: Sun May 3 16:13:37 2020 -0700 - L1S2A checkout solution for level 1, step 2 + 1.1S solution for level 1, step 1 commit c5c62041282579b495d3589b2eb1fdda2bcd7155 Author: creator Date: Sun May 3 16:11:42 2020 -0700 - L1S2Q setup level 1, step 2 + 1.2 setup for level 1, step 2 ``` -Note that the step `L1S2` has two commits, one with the suffix `Q` and another one with `A`. The suffixes mean `Question` and `Answer`, respectively, and refer to the unit tests and the commit that makes them pass. - -Steps defined as questions are **required** as they are meant to set the task to be executed by the student. The answer is optional and should be used when a commit must be loaded to verify the student's solution. - -If there are multiple commits for a level or step, they are captured in order. +Note that the step `1.1` has two commits, one with the suffix `S`. The first commit refers to the required tests and setup, while the second optional commit contains the solution. If there are multiple commits for a level or step, they are captured in order. diff --git a/package-lock.json b/package-lock.json index 1c870ac..30e5a83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@coderoad/cli", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5bdd89d..85e3bc4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coderoad/cli", - "version": "0.3.1", + "version": "0.4.0", "description": "A CLI to build the configuration file for Coderoad Tutorials", "keywords": [ "coderoad", diff --git a/src/build.ts b/src/build.ts index 150dbc0..cd7dbc2 100644 --- a/src/build.ts +++ b/src/build.ts @@ -102,7 +102,7 @@ async function build(args: string[]) { try { const valid = validateSchema(skeletonSchema, skeleton); if (!valid) { - console.error("Tutorial validation failed. See above to see what to fix"); + console.error("Skeleton validation failed. See above to see what to fix"); return; } } catch (e) { diff --git a/src/schema/meta.ts b/src/schema/meta.ts index 71bf7b8..97479f4 100644 --- a/src/schema/meta.ts +++ b/src/schema/meta.ts @@ -95,5 +95,40 @@ export default { }, additionalProperties: false, }, + setup_action_without_commits: { + type: "object", + description: + "A collection of files/commands that run when a level/step or solution is loaded", + properties: { + files: { + $ref: "#/definitions/file_array", + }, + commands: { + $ref: "#/definitions/command_array", + }, + watchers: { + type: "array", + items: { + $ref: "#/definitions/file_path", + // uniqueItems: true, + }, + description: + "An array file paths that, when updated, will trigger the test runner to run", + }, + filter: { + type: "string", + description: + "A regex pattern that will be passed to the test runner to limit the number of tests running", + examples: ["^TestSuiteName"], + }, + subtasks: { + type: "boolean", + description: + 'A feature that shows subtasks: all active test names and the status of the tests (pass/fail). Use together with "filter"', + examples: [true], + }, + }, + additionalProperties: false, + }, }, }; diff --git a/src/schema/skeleton.ts b/src/schema/skeleton.ts index 4f57b63..35c9353 100644 --- a/src/schema/skeleton.ts +++ b/src/schema/skeleton.ts @@ -53,9 +53,9 @@ export default { examples: ["coderoad"], }, setup: { - $ref: "#/definitions/setup_action", + $ref: "#/definitions/setup_action_without_commits", description: - "Setup commits or commands used for setting up the test runner on tutorial launch", + "Setup actions or commands used for setting up the test runner on tutorial launch", }, }, required: ["command", "args"], @@ -135,9 +135,9 @@ export default { examples: ["L1", "L11"], }, setup: { - $ref: "#/definitions/setup_action", + $ref: "#/definitions/setup_action_without_commits", description: - "An optional point for loading commits, running commands or opening files", + "An optional point for running actions, commands or opening files", }, steps: { type: "array", @@ -152,18 +152,18 @@ export default { setup: { allOf: [ { - $ref: "#/definitions/setup_action", + $ref: "#/definitions/setup_action_without_commits", description: - "A point for loading commits. It can also run commands and/or open files", + "A point for running actions, commands and/or opening files", }, ], }, solution: { allOf: [ { - $ref: "#/definitions/setup_action", + $ref: "#/definitions/setup_action_without_commits", description: - "The solution commits that can be loaded if the user gets stuck. It can also run commands and/or open files", + "The solution can be loaded if the user gets stuck. It can run actions, commands and/or open files", }, { required: [], diff --git a/src/schema/tutorial.ts b/src/schema/tutorial.ts index 6a605c9..062c9cc 100644 --- a/src/schema/tutorial.ts +++ b/src/schema/tutorial.ts @@ -211,7 +211,7 @@ export default { examples: ["Have you tried doing X?"], }, }, - required: ["content", "setup", "solution"], + required: ["content", "setup"], }, }, }, diff --git a/src/utils/commits.ts b/src/utils/commits.ts index 48fdee2..17c2956 100644 --- a/src/utils/commits.ts +++ b/src/utils/commits.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import util from "util"; import * as path from "path"; +import { ListLogSummary } from "simple-git/typings/response"; import gitP, { SimpleGit } from "simple-git/promise"; import { validateCommitOrder } from "./validateCommits"; @@ -15,6 +16,54 @@ type GetCommitOptions = { export type CommitLogObject = { [position: string]: string[] }; + + +export function parseCommits(logs: ListLogSummary): { [hash: string]: string[]} { + // Filter relevant logs + const commits: CommitLogObject = {}; + const positions: string[] = []; + + for (const commit of logs.all) { + const matches = commit.message.match( + /^(?INIT)|(L?(?\d+)[S|\.]?(?\d+)?(?[Q|A|T|S])?)/ + ); + + if (matches && matches.length) { + // Use an object of commit arrays to collect all commits + const { groups } = matches + let position + if (groups.init) { + position = 'INIT' + } else if (groups.levelId && groups.stepId) { + let stepType + // @deprecated Q + if (!groups.stepType || ['Q', 'T'].includes(groups.stepType)) { + stepType = 'T' // test + // @deprecated A + } else if (!groups.stepType || ['A', 'S'].includes(groups.stepType)) { + stepType = 'S' // solution + } + position = `${groups.levelId}.${groups.stepId}:${stepType}` + } else if (groups.levelId) { + position = groups.levelId + } else { + console.warn(`No matcher for commit "${commit.message}"`) + } + commits[position] = [...(commits[position] || []), commit.hash] + positions.unshift(position); + } else { + const initMatches = commit.message.match(/^INIT/); + if (initMatches && initMatches.length) { + commits.INIT = [...(commits.INIT || []), commit.hash] + positions.unshift("INIT"); + } + } + } + // validate order + validateCommitOrder(positions); + return commits; +} + export async function getCommits({ localDir, codeBranch, @@ -49,48 +98,16 @@ export async function getCommits({ // track the original branch in case of failure const originalBranch = branches.current; - // Filter relevant logs - const commits: CommitLogObject = {}; - try { // Checkout the code branches await git.checkout(codeBranch); // Load all logs const logs = await git.log(); - const positions: string[] = []; - - for (const commit of logs.all) { - const matches = commit.message.match( - /^(?(?L\d+)(S\d+))(?[QA])?/ - ); - - if (matches && matches.length) { - // Use an object of commit arrays to collect all commits - const position = matches[0]; - if (!commits[position]) { - // does not exist, create the list - commits[position] = [commit.hash]; - } else { - // add to the list - commits[position].push(commit.hash); - } - positions.unshift(position); - } else { - const initMatches = commit.message.match(/^INIT/); - if (initMatches && initMatches.length) { - if (!commits.INIT) { - // does not exist, create the list - commits.INIT = [commit.hash]; - } else { - // add to the list - commits.INIT.push(commit.hash); - } - positions.unshift("INIT"); - } - } - } - validateCommitOrder(positions); + + const commits = parseCommits(logs); + + return commits; } catch (e) { console.error("Error with checkout or commit matching"); throw new Error(e.message); @@ -100,6 +117,4 @@ export async function getCommits({ // cleanup the tmp directory await rmdir(tmpDir, { recursive: true }); } - - return commits; } diff --git a/src/utils/parse.ts b/src/utils/parse.ts index f8bf49d..443fbf5 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -4,10 +4,7 @@ import * as T from "../../typings/tutorial"; type TutorialFrame = { summary: T.TutorialSummary; - levels: { - [levelKey: string]: T.Level; - }; - steps: { [stepKey: string]: Partial }; + levels: T.Level[]; }; export function parseMdContent(md: string): TutorialFrame | never { @@ -33,8 +30,7 @@ export function parseMdContent(md: string): TutorialFrame | never { title: "", description: "", }, - levels: {}, - steps: {}, + levels: [], }; // Capture summary @@ -49,23 +45,20 @@ export function parseMdContent(md: string): TutorialFrame | never { mdContent.summary.description = summaryMatch.groups.tutorialDescription.trim(); } - let current = { level: "0", step: "0" }; + let current = { level: -1, step: -1 }; // Identify each part of the content parts.forEach((section: string) => { // match level - const levelRegex = /^(#{2}\s(?L\d+)\s(?.*)[\n\r]*(>\s(?.*))?[\n\r]+(?[^]*))/; + const levelRegex = /^(#{2}\s(?L?\d+\.?)\s(?.*)[\n\r]*(>\s(?.*))?[\n\r]+(?[^]*))/; const levelMatch: RegExpMatchArray | null = section.match(levelRegex); + if (levelMatch && levelMatch.groups) { - const { - levelId, - levelTitle, - levelSummary, - levelContent, - } = levelMatch.groups; + current = { level: current.level + 1, step: -1 }; + const { levelTitle, levelSummary, levelContent } = levelMatch.groups; // @ts-ignore - mdContent.levels[levelId] = { - id: levelId, + mdContent.levels[current.level] = { + id: (current.level + 1).toString(), title: levelTitle.trim(), summary: levelSummary && levelSummary.trim().length @@ -75,23 +68,22 @@ export function parseMdContent(md: string): TutorialFrame | never { omission: "...", }), content: levelContent.trim(), + steps: [], }; - current = { level: levelId, step: "0" }; } else { // match step - const stepRegex = /^(#{3}\s(?(?L\d+)S\d+)\s(?.*)[\n\r]+(?[^]*))/; + const stepRegex = /^(#{3}\s(?.*)[\n\r]+(?[^]*))/; const stepMatch: RegExpMatchArray | null = section.match(stepRegex); if (stepMatch && stepMatch.groups) { + current = { level: current.level, step: current.step + 1 }; const { stepId, stepContent } = stepMatch.groups; - - mdContent.steps[stepId] = { - id: stepId, + mdContent.levels[current.level].steps[current.step] = { + id: `${current.level + 1}.${current.step + 1}`, content: stepContent.trim(), }; - current = { ...current, step: stepId }; } else { // parse hints from stepContent - const hintDetectRegex = /^(#{4}\sHINTS[\n\r]+(\*\s(?[^]*))[\n\r]+)+/; + const hintDetectRegex = /^(#{4}\sHINTS[\n\r]+([\*|\-]\s(?[^]*))[\n\r]+)+/; const hintMatch = section.match(hintDetectRegex); if (!!hintMatch) { const hintItemRegex = /[\n\r]+\*\s/; @@ -100,7 +92,7 @@ export function parseMdContent(md: string): TutorialFrame | never { .slice(1) // remove #### HINTS .map((h) => h.trim()); if (hints.length) { - mdContent.steps[current.step].hints = hints; + mdContent.levels[current.level].steps[current.step].hints = hints; } } } @@ -135,33 +127,30 @@ export function parse(params: ParseParams): any { }; } - // merge content and tutorial - if (params.skeleton.levels && params.skeleton.levels.length) { - parsed.levels = params.skeleton.levels - .map((level: T.Level, levelIndex: number) => { - const levelContent = mdContent.levels[level.id]; + // merge content levels and tutorial - if (!levelContent) { - return null; - } + parsed.levels = mdContent.levels.map( + (mdLevel: T.Level, mdLevelIndex: number) => { + // add level setup commits + let level: T.Level = { ...mdLevel }; - level = { ...level, ...levelContent }; - - // add level setup commits - const levelSetupKey = level.id; - if (params.commits[levelSetupKey]) { - level.setup = { - ...(level.setup || {}), - commits: params.commits[levelSetupKey], - }; - } + const configLevel = params.skeleton.levels[mdLevelIndex]; + if (configLevel) { // add level step commits - try { - level.steps = (level.steps || []).map( - (step: T.Step, stepIndex: number) => { - const stepKey = `${levelSetupKey}S${stepIndex + 1}`; - const stepSetupKey = `${stepKey}Q`; + const { steps, ...configLevelProps } = configLevel; + level = { ...configLevelProps, ...level }; + if (steps) { + steps.forEach((step: T.Step, index: number) => { + try { + const mdStep = level.steps[index]; + + step = { + ...step, + ...mdStep, + }; + + const stepSetupKey = `${step.id}:T`; if (params.commits[stepSetupKey]) { if (!step.setup) { step.setup = { @@ -171,7 +160,7 @@ export function parse(params: ParseParams): any { step.setup.commits = params.commits[stepSetupKey]; } - const stepSolutionKey = `${stepKey}A`; + const stepSolutionKey = `${step.id}:S`; if (params.commits[stepSolutionKey]) { if (!step.solution) { step.solution = { @@ -180,27 +169,35 @@ export function parse(params: ParseParams): any { } step.solution.commits = params.commits[stepSolutionKey]; } + } catch (error) { + console.error("Error parsing level steps"); + console.warn(JSON.stringify(level.steps)); + console.error(error.message); + } + // update level step + level.steps[index] = step; + }); + } + } - // add markdown - const stepMarkdown: Partial = mdContent.steps[step.id]; - if (stepMarkdown) { - step = { ...step, ...stepMarkdown }; - } + if (params.commits[level.id]) { + if (!level.setup) { + level.setup = {}; + } + level.setup.commits = params.commits[level.id]; + } - step.id = `${stepKey}`; - return step; - } - ); - } catch (error) { - console.log(JSON.stringify(level.steps)); - console.error("Error parsing level steps"); - console.error(error.message); + // @deprecated L1 system + if (params.commits[`L${level.id}`]) { + if (!level.setup) { + level.setup = {}; } + level.setup.commits = params.commits[`L${level.id}`]; + } - return level; - }) - .filter((l: T.Level | null) => !!l); - } + return level; + } + ); return parsed; } diff --git a/src/utils/validateCommits.ts b/src/utils/validateCommits.ts index 80d714d..f4d8890 100644 --- a/src/utils/validateCommits.ts +++ b/src/utils/validateCommits.ts @@ -3,35 +3,61 @@ export function validateCommitOrder(positions: string[]): boolean { // loop over positions const errors: number[] = []; - let previous = { level: 0, step: 0 }; - let current = { level: 0, step: 0 }; + let previous = { level: 0, step: 0, type: "" }; + let current = { level: 0, step: 0, type: "" }; positions.forEach((position: string, index: number) => { if (position === "INIT") { if (previous.level !== 0 && previous.step !== 0) { errors.push(index); } - current = { level: 0, step: 0 }; + current = { level: 0, step: 0, type: "" }; return; } else { - const levelMatch = position.match(/^L([0-9]+)Q?$/); - const stepMatch = position.match(/^L([0-9]+)S([0-9]+)[Q|A]?$/); + // @deprecate - remove L|Q + const levelMatch = position.match(/^(?[0-9]+)$/); + // @deprecate - remove S|Q|A + const stepMatch = position.match( + /^(?[0-9]+)\.(?[0-9]+):(?[T|S])$/ + ); if (levelMatch) { // allows next level or step - const [_, levelString] = levelMatch; + const levelString = levelMatch?.groups?.level; + if (!levelString) { + console.warn(`No commit level match for ${position}`); + return; + } const level = Number(levelString); - current = { level, step: 0 }; + current = { level, step: 0, type: "" }; } else if (stepMatch) { // allows next level or step - const [_, levelString, stepString] = stepMatch; + if (!stepMatch?.groups?.level || !stepMatch?.groups.step) { + console.warn(`No commit step match for ${position}`); + return; + } + const { level: levelString, step: stepString } = stepMatch.groups; + const level = Number(levelString); const step = Number(stepString); - current = { level, step }; + const type = stepMatch?.groups.stepType; + + const sameStep = previous.level === level && previous.step === step; + + if ( + // tests should come before the solution + (sameStep && type === "T" && previous.type === "S") || + // step should have tests + (!sameStep && type === "S") + ) { + errors.push(index); + } + current = { level, step, type }; } else { // error console.warn(`Invalid commit position: ${position}`); return; } if ( + // levels or steps are out of order current.level < previous.level || (current.level === previous.level && current.step < previous.step) ) { diff --git a/src/utils/validateMarkdown.ts b/src/utils/validateMarkdown.ts index c4d13bb..9933c67 100644 --- a/src/utils/validateMarkdown.ts +++ b/src/utils/validateMarkdown.ts @@ -24,11 +24,11 @@ const validations: Validation[] = [ }, }, { - message: "should have a level `##` with a format of `L[0-9]+`", + message: "should have a level `##` with a format of `[0-9]+.`", validate: (t) => { const headers = t.match(/^#{2}\s(.+)$/gm) || []; for (const header of headers) { - if (!header.match(/^#{2}\s(L\d+)\s(.+)$/)) { + if (!header.match(/^#{2}\s(\d+\.)\s(.+)$/)) { return false; } } @@ -36,11 +36,11 @@ const validations: Validation[] = [ }, }, { - message: "should have a step `###` with a format of `L[0-9]+S[0-9]+`", + message: "should have a step `###` with a format of `[0-9].[0-9]+`", validate: (t) => { const headers = t.match(/^#{3}\s(.+)$/gm) || []; for (const header of headers) { - if (!header.match(/^#{3}\s(L\d+)S\d+/)) { + if (!header.match(/^#{3}\s(\d+\.\d+)/)) { return false; } } @@ -60,9 +60,9 @@ export function validateMarkdown(md: string): boolean { for (const v of validations) { if (!v.validate(text)) { valid = false; - if (process.env.NODE_ENV !== "test") { - console.warn(v.message); - } + // if (process.env.NODE_ENV !== "test") { + console.warn(v.message); + // } } } diff --git a/tests/commitOrder.test.ts b/tests/commitOrder.test.ts index 38e1035..640b25f 100644 --- a/tests/commitOrder.test.ts +++ b/tests/commitOrder.test.ts @@ -1,47 +1,61 @@ import { validateCommitOrder } from "../src/utils/validateCommits"; describe("commitOrder", () => { - it("should return true if order is valid", () => { - const positions = ["INIT", "L1", "L1S1", "L1S2", "L2", "L2S1"]; - const result = validateCommitOrder(positions); - expect(result).toBe(true); - }); - it("should return true if valid with duplicates", () => { - const positions = [ - "INIT", - "INIT", - "L1", - "L1", - "L1S1", - "L1S1", - "L1S2", - "L1S2", - "L2", - "L2", - "L2S1", - "L2S1", - ]; - const result = validateCommitOrder(positions); - expect(result).toBe(true); - }); - it("should return false if INIT is out of order", () => { - const positions = ["INIT", "L1", "L1S1", "L1S2", "INIT", "L2", "L2S1"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); - }); - it("should return false if level after step is out of order", () => { - const positions = ["INIT", "L1", "L1S1", "L1S2", "L2S1", "L2"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); - }); - it("should return false if level is out of order", () => { - const positions = ["INIT", "L1", "L3", "L2"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); - }); - it("should return false if step is out of order", () => { - const positions = ["INIT", "L1", "L1S1", "L1S3", "L1S2"]; - const result = validateCommitOrder(positions); - expect(result).toBe(false); + describe("#.# format", () => { + it("should return true if order is valid", () => { + const positions = ["INIT", "1", "1.1:T", "1.2:T", "2", "2.1:T"]; + const result = validateCommitOrder(positions); + expect(result).toBe(true); + }); + it("should return true if valid with duplicates", () => { + const positions = [ + "INIT", + "INIT", + "1", + "1", + "1.1:T", + "1.1:T", + "1.1:S", + "1.1:S", + "1.2:T", + "1.2:S", + "2", + "2", + "2.1:T", + "2.1:S", + ]; + const result = validateCommitOrder(positions); + expect(result).toBe(true); + }); + it("should return false if INIT is out of order", () => { + const positions = ["INIT", "1", "1.1:T", "1.2:T", "INIT", "2", "2.1:T"]; + const result = validateCommitOrder(positions); + expect(result).toBe(false); + }); + it("should return false if level after step is out of order", () => { + const positions = ["INIT", "1", "1.1:T", "1.2:T", "2.1:T", "2"]; + const result = validateCommitOrder(positions); + expect(result).toBe(false); + }); + it("should return false if level is out of order", () => { + const positions = ["INIT", "1", "3", "2"]; + const result = validateCommitOrder(positions); + expect(result).toBe(false); + }); + it("should return false if step is out of order", () => { + const positions = ["INIT", "1", "1.1:T", "1.3:T", "1.2:T"]; + const result = validateCommitOrder(positions); + expect(result).toBe(false); + }); + it("should return false if solution is before step", () => { + const positions = ["INIT", "1", "1.1:S", "1.1:T", "1.2:T"]; + const result = validateCommitOrder(positions); + expect(result).toBe(false); + }); + it("should return false if solution but no test step", () => { + const positions = ["INIT", "1", "1.1:S", "1.2:T"]; + const result = validateCommitOrder(positions); + expect(result).toBe(false); + }); }); }); diff --git a/tests/commitParse.test.ts b/tests/commitParse.test.ts new file mode 100644 index 0000000..00e1548 --- /dev/null +++ b/tests/commitParse.test.ts @@ -0,0 +1,151 @@ +import { parseCommits } from "../src/utils/commits"; + +describe("commitParse", () => { + it("should parse out #. commits", () => { + const logs = { + all: [ + { + message: "INIT", + hash: "1", + }, + { + message: "1. First Level", + hash: "2", + }, + { + message: "1.1 First Step", + hash: "3", + }, + ], + total: 2, + latest: {}, + }; + const commits = parseCommits(logs); + expect(commits).toEqual({ + INIT: ["1"], + "1": ["2"], + "1.1:T": ["3"], + }); + }); + // @deprecated - remove L# + it("should parse out L# commits", () => { + const logs = { + all: [ + { + message: "INIT", + hash: "1", + }, + { + message: "L1 First Level", + hash: "2", + }, + { + message: "L1S1 First Step", + hash: "3", + }, + ], + total: 2, + latest: {}, + }; + const commits = parseCommits(logs); + expect(commits).toEqual({ + INIT: ["1"], + "1": ["2"], + "1.1:T": ["3"], + }); + }); + // @deprecated - remove with QA + it("should parse out #.Q|A commits", () => { + const logs = { + all: [ + { + message: "INIT", + hash: "1", + }, + { + message: "1. First Level", + hash: "2", + }, + { + message: "1.1Q First Step", + hash: "3", + }, + { + message: "1.1A First Step Solution", + hash: "4", + }, + ], + total: 2, + latest: {}, + }; + const commits = parseCommits(logs); + expect(commits).toEqual({ + INIT: ["1"], + "1": ["2"], + "1.1:T": ["3"], + "1.1:S": ["4"], + }); + }); + it("should parse out #.T|S commits", () => { + const logs = { + all: [ + { + message: "INIT", + hash: "1", + }, + { + message: "1. First Level", + hash: "2", + }, + { + message: "1.1T First Step", + hash: "3", + }, + { + message: "1.1S First Step Solution", + hash: "4", + }, + ], + total: 2, + latest: {}, + }; + const commits = parseCommits(logs); + expect(commits).toEqual({ + INIT: ["1"], + "1": ["2"], + "1.1:T": ["3"], + "1.1:S": ["4"], + }); + }); + it("should parse out #._|S commits", () => { + const logs = { + all: [ + { + message: "INIT", + hash: "1", + }, + { + message: "1. First Level", + hash: "2", + }, + { + message: "1.1 First Step", + hash: "3", + }, + { + message: "1.1S First Step Solution", + hash: "4", + }, + ], + total: 2, + latest: {}, + }; + const commits = parseCommits(logs); + expect(commits).toEqual({ + INIT: ["1"], + "1": ["2"], + "1.1:T": ["3"], + "1.1:S": ["4"], + }); + }); +}); diff --git a/tests/markdown.test.ts b/tests/markdown.test.ts index 9b90028..0ab15bf 100644 --- a/tests/markdown.test.ts +++ b/tests/markdown.test.ts @@ -5,7 +5,7 @@ describe("validate markdown", () => { const md = ` Description. -## L1 Put Level's title here +## 1. Put Level's title here > Level's summary: a short description of the level's content in one line. @@ -19,7 +19,7 @@ Description. # Another Title -## L1 Put Level's title here +## 1. Put Level's title here > Level's summary: a short description of the level's content in one line. @@ -29,7 +29,7 @@ Some text that describes the level`; Description. -## L1 Put Level's title here +## 1. Put Level's title here > Level's summary: a short description of the level's content in one line. @@ -45,7 +45,7 @@ Some text that describes the level it("should return false if missing a summary description", () => { const md = `# A Title -## L1 Put Level's title here +## 1. Put Level's title here > Level's summary: a short description of the level's content in one line. @@ -79,10 +79,11 @@ A description Some text that describes the level -### A Step +### Missing step id First step `; + expect(validateMarkdown(md)).toBe(false); }); it("should return true for valid markdown", () => { @@ -90,13 +91,13 @@ First step Description. -## L1 Put Level's title here +## 1. Put Level's title here > Level's summary: a short description of the level's content in one line. Some text that describes the level -### L1S1 +### 1.1 First Step`; expect(validateMarkdown(md)).toBe(true); @@ -114,19 +115,19 @@ Should not be a problem \`\`\` -## L1 Put Level's title here +## 1. Put Level's title here > Level's summary: a short description of the level's content in one line. Some text that describes the level \`\`\` -## Another Level in markdown +## 2. Another Level in markdown Should not be an issue \`\`\` -### L1S1 +### 1.1 First Step`; expect(validateMarkdown(md)).toBe(true); diff --git a/tests/parse.test.ts b/tests/parse.test.ts index c4d6ca9..77369c4 100644 --- a/tests/parse.test.ts +++ b/tests/parse.test.ts @@ -32,7 +32,7 @@ Short description to be shown as a tutorial's subtitle. Description. -## L1 Put Level's title here +## 1. Put Level's title here > Level's summary: a short description of the level's content in one line. @@ -40,7 +40,7 @@ Some text `; const skeleton = { - levels: [{ id: "L1" }], + levels: [{ id: "1" }], }; const result = parse({ @@ -51,7 +51,7 @@ Some text const expected = { levels: [ { - id: "L1", + id: "1", title: "Put Level's title here", summary: "Level's summary: a short description of the level's content in one line.", @@ -68,7 +68,7 @@ Some text Description. -## L1 Put Level's title here +## 1. Put Level's title here > Level's summary: a short description of the level's content in one line. @@ -78,7 +78,7 @@ Some text const skeleton = { levels: [ { - id: "L1", + id: "1", setup: { files: [], commits: [] }, solution: { files: [], commits: [] }, steps: [], @@ -93,7 +93,7 @@ Some text const expected = { levels: [ { - id: "L1", + id: "1", title: "Put Level's title here", summary: "Level's summary: a short description of the level's content in one line.", @@ -112,12 +112,12 @@ Some text Description. -## L1 Put Level's title here +## 1. Put Level's title here Some text that becomes the summary `; - const skeleton = { levels: [{ id: "L1" }] }; + const skeleton = { levels: [{ id: "1" }] }; const result = parse({ text: md, skeleton, @@ -126,7 +126,7 @@ Some text that becomes the summary const expected = { levels: [ { - id: "L1", + id: "1", title: "Put Level's title here", summary: "Some text that becomes the summary", content: "Some text that becomes the summary", @@ -142,12 +142,12 @@ Some text that becomes the summary Description. -## L1 Put Level's title here +## 1. Put Level's title here Some text that becomes the summary and goes beyond the maximum length of 80 so that it gets truncated at the end `; - const skeleton = { levels: [{ id: "L1" }] }; + const skeleton = { levels: [{ id: "1" }] }; const result = parse({ text: md, skeleton, @@ -156,7 +156,7 @@ Some text that becomes the summary and goes beyond the maximum length of 80 so t const expected = { levels: [ { - id: "L1", + id: "1", title: "Put Level's title here", summary: "Some text that becomes the summary", content: "Some text that becomes the summary", @@ -171,14 +171,14 @@ Some text that becomes the summary and goes beyond the maximum length of 80 so t Description. -## L1 Put Level's title here +## 1. Put Level's title here Some text. But not including this line. `; - const skeleton = { levels: [{ id: "L1" }] }; + const skeleton = { levels: [{ id: "1" }] }; const result = parse({ text: md, skeleton, @@ -187,7 +187,7 @@ But not including this line. const expected = { levels: [ { - id: "L1", + id: "1", title: "Put Level's title here", summary: "Some text.", content: "Some text.\n\nBut not including this line.", @@ -203,7 +203,7 @@ But not including this line. Description. -## L1 Put Level's title here +## 1. Put Level's title here > @@ -212,7 +212,7 @@ Some text. But not including this line. `; - const skeleton = { levels: [{ id: "L1" }] }; + const skeleton = { levels: [{ id: "1" }] }; const result = parse({ text: md, skeleton, @@ -221,7 +221,7 @@ But not including this line. const expected = { levels: [ { - id: "L1", + id: "1", title: "Put Level's title here", summary: "Some text.", content: "Some text.\n\nBut not including this line.", @@ -239,7 +239,7 @@ Description. Second description line -## L1 Titles +## 1. Titles First line @@ -248,7 +248,7 @@ Second line Third line `; - const skeleton = { levels: [{ id: "L1" }] }; + const skeleton = { levels: [{ id: "1" }] }; const result = parse({ text: md, skeleton, @@ -260,7 +260,7 @@ Third line }, levels: [ { - id: "L1", + id: "1", summary: "Some text that becomes the summary", content: "First line\n\nSecond line\n\nThird line", }, @@ -275,21 +275,21 @@ Third line Description. -## L1 Title +## 1. Title First line -### L1S1 Step +### 1.1 The first step `; const skeleton = { levels: [ { - id: "L1", + id: "1", steps: [ { - id: "L1S1", + id: "1.1", }, ], }, @@ -299,7 +299,7 @@ The first step text: md, skeleton, commits: { - L1S1Q: ["abcdefg1"], + "1.1Q": ["abcdefg1"], }, }); const expected = { @@ -308,12 +308,12 @@ The first step }, levels: [ { - id: "L1", + id: "1", summary: "First line", content: "First line", steps: [ { - id: "L1S1", + id: "1.1", content: "The first step", setup: { commits: ["abcdefg1"], @@ -331,21 +331,21 @@ The first step Description. -## L1 Title +## 1. Title First line -### L1S1 Step +### 1.1 Step The first step `; const skeleton = { levels: [ { - id: "L1", + id: "1", steps: [ { - id: "L1S1", + id: "1.1", }, ], }, @@ -355,7 +355,7 @@ The first step text: md, skeleton, commits: { - L1S1Q: ["abcdefg1", "123456789"], + "1.1Q": ["abcdefg1", "123456789"], }, }); const expected = { @@ -364,12 +364,12 @@ The first step }, levels: [ { - id: "L1", + id: "1", summary: "First line", content: "First line", steps: [ { - id: "L1S1", + id: "1.1", content: "The first step", setup: { commits: ["abcdefg1", "123456789"], @@ -387,18 +387,18 @@ The first step Description. -## L1 Title +## 1. Title First line -### L1S1 +### 1.1 The first step `; const skeleton = { levels: [ { - id: "L1", + id: "1", }, ], }; @@ -406,7 +406,7 @@ The first step text: md, skeleton, commits: { - L1: ["abcdefg1"], + "1": ["abcdefg1"], }, }); const expected = { @@ -415,7 +415,7 @@ The first step }, levels: [ { - id: "L1", + id: "1", summary: "First line", content: "First line", setup: { @@ -432,11 +432,11 @@ The first step Description. -## L1 Title +## 1. Title First line -### L1S1 +### 1.1 The first step @@ -451,10 +451,10 @@ Another line const skeleton = { levels: [ { - id: "L1", + id: "1", steps: [ { - id: "L1S1", + id: "1.1", }, ], }, @@ -464,12 +464,12 @@ Another line text: md, skeleton, commits: { - L1: ["abcdefg1"], - L1S1Q: ["12345678"], + "1": ["abcdefg1"], + "1.1Q": ["12345678"], }, }); const expected = { - id: "L1S1", + id: "1.1", setup: { commits: ["12345678"], }, @@ -484,21 +484,21 @@ Another line Description. -## L1 Title +## 1. Title First line -### L1S1 Step +### 1.1 Step The first step `; const skeleton = { levels: [ { - id: "L1", + id: "1", steps: [ { - id: "L1S1", + id: "1.1", setup: { commands: ["npm install"], files: ["someFile.js"], @@ -519,8 +519,8 @@ The first step text: md, skeleton, commits: { - L1S1Q: ["abcdefg1", "123456789"], - L1S1A: ["1gfedcba", "987654321"], + "1.1Q": ["abcdefg1", "123456789"], + "1.1A": ["1gfedcba", "987654321"], }, }); const expected = { @@ -529,12 +529,12 @@ The first step }, levels: [ { - id: "L1", + id: "1", summary: "First line", content: "First line", steps: [ { - id: "L1S1", + id: "1.1", content: "The first step", setup: { commits: ["abcdefg1", "123456789"], @@ -562,33 +562,33 @@ The first step Description. -## L1 Title 1 +## 1. Title 1 First level content. -### L1S1 +### 1.1 The first step -### L1S2 +### 1.2 The second step -## L2 Title 2 +## 2. Title 2 Second level content. -### L2S1 +### 2.1 The third step `; const skeleton = { levels: [ { - id: "L1", + id: "1", steps: [ { - id: "L1S1", + id: "1.1", setup: { commands: ["npm install"], files: ["someFile.js"], @@ -602,7 +602,7 @@ The third step }, }, { - id: "L1S2", + id: "1.2", setup: { commands: ["npm install"], files: ["someFile.js"], @@ -618,12 +618,12 @@ The third step ], }, { - id: "L2", + id: "2", summary: "Second level content.", content: "First level content.", steps: [ { - id: "L2S1", + id: "2.1", setup: { commands: ["npm install"], files: ["someFile.js"], @@ -644,12 +644,12 @@ The third step text: md, skeleton, commits: { - L1S1Q: ["abcdef1", "123456789"], - L1S1A: ["1fedcba", "987654321"], - L1S2Q: ["2abcdef"], - L1S2A: ["3abcdef"], - L2S1Q: ["4abcdef"], - L2S1A: ["5abcdef"], + "1.1Q": ["abcdef1", "123456789"], + "1.1A": ["1fedcba", "987654321"], + "1.2Q": ["2abcdef"], + "1.2A": ["3abcdef"], + "2.1Q": ["4abcdef"], + "2.1A": ["5abcdef"], }, }); const expected = { @@ -658,13 +658,13 @@ The third step }, levels: [ { - id: "L1", + id: "1", title: "Title 1", summary: "First level content.", content: "First level content.", steps: [ { - id: "L1S1", + id: "1.1", content: "The first step", setup: { commits: ["abcdef1", "123456789"], @@ -681,7 +681,7 @@ The third step }, }, { - id: "L1S2", + id: "1.2", content: "The second step", setup: { commits: ["2abcdef"], @@ -700,13 +700,13 @@ The third step ], }, { - id: "L2", + id: "2", title: "Title 2", summary: "Second level content.", content: "Second level content.", steps: [ { - id: "L2S1", + id: "2.1", content: "The third step", setup: { commits: ["4abcdef"], @@ -734,11 +734,11 @@ The third step Description. -## L1 Title 1 +## 1. Title 1 First level content. -### L1S1 +### 1.1 The first step @@ -746,10 +746,10 @@ The first step const skeleton = { levels: [ { - id: "L1", + id: "1", steps: [ { - id: "L1S1", + id: "1.1", }, ], }, @@ -759,7 +759,7 @@ The first step text: md, skeleton, commits: { - L1S1Q: ["abcdef1", "123456789"], + "1.1Q": ["abcdef1", "123456789"], }, }); const expected = { @@ -768,13 +768,13 @@ The first step }, levels: [ { - id: "L1", + id: "1", title: "Title 1", summary: "First level content.", content: "First level content.", steps: [ { - id: "L1S1", + id: "1.1", content: "The first step", setup: { commits: ["abcdef1", "123456789"], @@ -941,11 +941,11 @@ Description. Description. -## L1 Title 1 +## 1. Title 1 First level content. -### L1S1 +### 1.1 The first step @@ -958,10 +958,10 @@ The first step const skeleton = { levels: [ { - id: "L1", + id: "1", steps: [ { - id: "L1S1", + id: "1.1", }, ], }, @@ -971,7 +971,7 @@ The first step text: md, skeleton, commits: { - L1S1Q: ["abcdef1", "123456789"], + "1.1Q": ["abcdef1", "123456789"], }, }); const expected = { @@ -980,13 +980,13 @@ The first step }, levels: [ { - id: "L1", + id: "1", title: "Title 1", summary: "First level content.", content: "First level content.", steps: [ { - id: "L1S1", + id: "1.1", content: "The first step", setup: { commits: ["abcdef1", "123456789"], @@ -1005,11 +1005,11 @@ The first step Description. -## L1 Title 1 +## 1. Title 1 First level content. -### L1S1 +### 1.1 The first step @@ -1027,10 +1027,10 @@ And spans multiple lines. const skeleton = { levels: [ { - id: "L1", + id: "1", steps: [ { - id: "L1S1", + id: "1.1", }, ], }, @@ -1040,7 +1040,7 @@ And spans multiple lines. text: md, skeleton, commits: { - L1S1Q: ["abcdef1", "123456789"], + "1.1Q": ["abcdef1", "123456789"], }, }); const expected = { @@ -1049,13 +1049,13 @@ And spans multiple lines. }, levels: [ { - id: "L1", + id: "1", title: "Title 1", summary: "First level content.", content: "First level content.", steps: [ { - id: "L1S1", + id: "1.1", content: "The first step", setup: { commits: ["abcdef1", "123456789"], @@ -1077,11 +1077,11 @@ And spans multiple lines. Description. -## L1 Title 1 +## 1. Title 1 First level content. -### L1S1 +### 1.1 The first step @@ -1096,20 +1096,20 @@ var a = 1; And spans multiple lines. -### L1S2 +### 1.2 The second uninterrupted step `; const skeleton = { levels: [ { - id: "L1", + id: "1", steps: [ { - id: "L1S1", + id: "1.1", }, { - id: "L1S2", + id: "1.2", }, ], }, @@ -1119,9 +1119,9 @@ The second uninterrupted step text: md, skeleton, commits: { - L1S1Q: ["abcdef1"], - L1S1A: ["123456789"], - L1S2Q: ["fedcba1"], + "1.1Q": ["abcdef1"], + "1.1A": ["123456789"], + "1.2Q": ["fedcba1"], }, }); const expected = { @@ -1130,13 +1130,13 @@ The second uninterrupted step }, levels: [ { - id: "L1", + id: "1", title: "Title 1", summary: "First level content.", content: "First level content.", steps: [ { - id: "L1S1", + id: "1.1", content: "The first step", setup: { commits: ["abcdef1"], @@ -1150,7 +1150,7 @@ The second uninterrupted step ], }, { - id: "L1S2", + id: "1.2", content: "The second uninterrupted step", setup: { commits: ["fedcba1"], diff --git a/tests/skeleton.test.ts b/tests/skeleton.test.ts index 4025bc8..57a5f3c 100644 --- a/tests/skeleton.test.ts +++ b/tests/skeleton.test.ts @@ -30,7 +30,7 @@ const validJson = { { steps: [ { - id: "L1S1", + id: "1.1", setup: { files: ["package.json"], }, @@ -39,7 +39,7 @@ const validJson = { }, }, { - id: "L1S2", + id: "1.2", setup: { commands: ["npm install"], }, @@ -48,7 +48,7 @@ const validJson = { }, }, { - id: "L1S3", + id: "1.3", setup: { files: ["package.json"], watchers: ["package.json", "node_modules/some-package"], @@ -58,7 +58,7 @@ const validJson = { }, }, { - id: "L1S4", + id: "1.4", setup: { commands: [], filter: "^Example 2", @@ -66,7 +66,7 @@ const validJson = { }, }, ], - id: "L1", + id: "1", }, ], }; @@ -186,7 +186,7 @@ describe("validate skeleton", () => { const valid = validateSkeleton(json); expect(valid).toBe(false); }); - it("should fial if level is missing id", () => { + it("should fail if level is missing id", () => { const level1 = { ...validJson.levels[0], id: undefined }; const json = { ...validJson, diff --git a/tests/tutorial.test.ts b/tests/tutorial.test.ts index 2f7dde7..f0e809c 100644 --- a/tests/tutorial.test.ts +++ b/tests/tutorial.test.ts @@ -39,7 +39,7 @@ describe("validate tutorial", () => { }, levels: [ { - id: "L1", + id: "1", title: "Level 1", summary: "The first level", content: "The first level", diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts index 82b1e2e..2b7769c 100644 --- a/typings/tutorial.d.ts +++ b/typings/tutorial.d.ts @@ -25,8 +25,8 @@ export type Level = { export type Step = { id: string; content: string; - setup: StepActions; - solution: Maybe; + setup?: StepActions; + solution?: Maybe; subtasks?: { [testName: string]: boolean }; hints?: string[]; }; @@ -48,7 +48,7 @@ export type TutorialSummary = { export type StepActions = { commands?: string[]; - commits: string[]; + commits?: string[]; files?: string[]; watchers?: string[]; filter?: string;