The docs are a deeper read. It's recommended you get setup with the plugins and gain an understanding of how CodeRoad works before continuing.



Edit

1. Demo Tutorial In Atom

Demoing Your Tutorial

Open a new directory for demoing your tutorial. Setup a new NPM project file.

> npm init

Add your package name to the dependencies in package.json:

{
  "dependencies": {
      "coderoad-$YOUR-PACKAGE-NAME$": "^0.1.0"
  }
}

Normally you would use > npm install to install the package, but your package isn’t ready to be published yet. Instead, you need to “link” your tutorial package to your demo directory.

NPM link creates a symbolic link between directories. This allows your demo directory to always load your tutorial package.

Inside of your tutorial root directory, run npm link.

> npm link

Inside of your demo root directory, connect the link.

> npm link coderoad-$YOUR-PACKAGE-NAME$
> npm install

Using Atom

Use Builder-CodeRoad to develop your tutorial. Builder-CodeRoad allows you to visualize and test your tutorial as you develop it.

When it’s ready, you can play your tutorial in Atom-Coderoad. Your package should appear as a loaded package. Click on it.

If you make any changes to your tutorial, you’ll have to reload Atom to view them. You can use the Atom command-palette to find “reload” or simply use the reload hot-key (Windows & Linux: alt-ctrl-r, Mac: ctrl-alt-cmd-l).



Edit

2. Tutorial Markdown

CodeRoad tutorials are written in Github Flavored Markdown, then parsed into a coderoad.json file when you run > coderoad build.

Markdown

Each level of header indicates a different section, followed by a description.

  • # Info
  • ## Page
  • + Task

As an example:

# Tutorial Title
A description of your tutorial

## Page One Title
A description of page one

+ A description of task one

+ A description of task two

## Page Two Title
A description of page two

## Final
Final outro content

Read more about Github Flavored Markdown, including how to write tables & syntax highlight code blocks.



Edit

3. CodeRoad API

Of course Markdown couldn’t cover all uses necessary for CodeRoad. Instead, there is a special CodeRoad API which is parsed into the data file whenever you run > coderoad build.

For these API features to work, they must be placed at the beginning of a line.

@import('file')           // ✓
  @import('file')         // ✗

Features can be commented out, allowing you to view different files at a time. Be aware the parser matches content from the beginning of a line.

<!-- @import('file') -->  // ✗
<!-- @import('file')      // ✗
@import('file2') -->      // ✓

@import

@import loads other markdown files. Specify a relative path from the root project directory. If no file extension is provided, it will default to .md.

@import('./path/to/file')
@import('./path/to/file.md')

See an example tutorial file.

@test

Defaults for loading tests are specified in the tutorial package.json file.

{
  "config": {
    "language": "JS",
    "dir": "tutorial",
    "testSuffix": ".js",
    "runner": "mocha-coderoad"
  }
}

dir is appended to all @test calls, and testSuffix is added to the end.

@test loads a test file. It is important that these files are loaded in the correct order. @test can take a single test file, or an array of test files.

@test('path/to/file')
@test(['path/to/file', 'path/to/file2'])

The first example would load the file ./tutorial/path/to/file.spec.js in the project root directory.

See an example using @test.

@hint

@hint loads a string which can be used to provide hints for the user. The order of hints is important: first in, first out.

@hint('A hint for the user')

*@hint* may use code-blocks with syntax highlighting, but they must be wrapped in quotes.

```markdown
@hint("Use the object `{key: val}`")
@hint("`var a = 42;`")
@hint("```js
var a = 42;
```")

@action

@action allows you to run changes in the Editor.

open

Open a file. The path to the file will be from the users root directory.

@action(open('file.js'))
@action(open('path/to/file.js'))

set

Replace all text in a file.

@action(set('// hello world'))
@action(set(`// hello world`))
@action(set(```
  function sayHello() {
    return 'hello world';
  }
```))

insert

Add text to the bottom of the active text editor.

@action(insert('// hello world'))
@action(insert(`// hello world`))
@action(insert(```
  function sayHello() {
    return 'hello world';
  }
```))

write

Write a file in the users project directory. File paths are relative to the users directory.

@action(write('file.js', 'hello world!'))
// writes to 'projectWorkingDirectory/file.js'

writeFromFile

Write a file to the users project directory by reading another file.

@action(writeFromFile('file.js', 'data/example.js'))
// reads from 'tutorial/data/example.js'
// writes to 'projectWorkingDirectory/file.js'

::>

Set the cursor position.

@action(insert(```
  function example() {
    ::>
  }
 ```
))

The cursor position will be set to the last use of ::>.

@onPageComplete

An optional message that will appear when all of the tasks for a page are completed.

## Page 1
Page 1 description

@onPageComplete('Next we'll look at page 2')

## Final

Giving a page the title “Final” results in the content being presented in the outro.

What’s Next

Further editor actions will be added to CodeRoad. These may include:

  • replacing content
  • decorating keywords

Feel free to suggest a feature.



Edit

4. Unit Testing

Unit tests are used to determine:

  • if a task passes or fails
  • feedback a user gets when a test fails

Tests are loaded in their order from the tutorial markdown files, and concat together into one temporary file. This means that any import or require statements placed at the top of the first file will be shared by all tests for that page.

Tests should indicate which task they are, in order to quickly determine which task index has failed. For the mocha-coderoad test runner, this is as easy as adding the task number to describe block.

describe('01 first task');
describe('04 fourth task');

Test Statements

It makes sense to write test statements using ‘should’, ‘must’ or negative statements. Remember, the failing test message will be delivered as feedback to the user.

it('should be a function')
it('must be a function')
it('isn\'t a function')


Edit

5. Loaders

Use a loader to run the user saved file in the context of your file. Think of a loader as a way to place the file your testing inside of your test file.

// paths are prefixed with "BASE"
const userTextEditorContent = require('BASE/path/to/file');
const dataFile = require('BASE/path/to/data');

The keyword “BASE” will be over-ridden with an absolute path to the file.

Note: When using spies, stubs or mocks, initiate them above your loader call.

Testing

If the editor exports a variable or function, units tests are easy to setup.

user file
export const a = 1;
unit test
expect(a).to.be(1);

Otherwise, there is a slightly more complicated way to test globals.

Unit Tests with Globals

Running unit tests on globals requires two additional steps.

  1. capture the file as a module
  2. use a built-in __get__ method to capture the target
user file
const a = 1;
unit test
// 1. capture the file's module.
// Note that BASE will be replaced with an absolute path to the project dir
const file = require('BASE/path/to/file.js');
// 2. use the __get__ method to capture the target variable
const a = filter.__get__('a');

expect(a).to.be(1);


Edit

6. Test Snippets

Test Runners may have pre-made Atom snippets to quickly generate test files. See mocha-coderoad snippets as an example.

  • Open up Atom -> Open Your Snippets.
  • Add the contents of snippets.cson to your global snippets.cson file.
  • Read the test runner README to see which snippets are available and which prefixes you should use.

For example, typing the snippet mochacr-f will generate an entire set of tests for a mocha-coderoad function. See an example:

Mocha Coderoad Test Snippets



Edit

7. Test Examples

Here are examples using mocha with chai’s expect. See the Chai/Expect docs. Also keep in mind that most uses cases are covered by test snippets.

exists

it('doesn\'t exist', () => {
    expect(target).to.not.be.undefined;
});

type

it('should be a function', () => {
    expect(target).to.be.a('function');
});

function params

it('should have two parameters', () => {
    expect(target).to.have.length(2);
});

function returns

it('should add one to the number', () => {
    expect(addOne(1)).to.equal(2);
});

equals

it('should be 42', () => {
    expect(target).to.equal(42);
});

deep equals (with objects or arrays)

it('should be {a: 42}', () => {
    expect(target).to.deep.equal({a: 42});
});

regex

it('should include the variable "count"', () => {
    const regex = new RegExp('count');
    // __text__ is an added property that provides the text version of the file
    const string = target.__text__;
    expect(string).to.match(regex);
});
it('should access the property "prop"', () => {
    const regex1 = /\.prop/;            // dot notation
    const regex2 = /\[["']prop["']\]/;  // bracket notation
    const string = target.__text__;
    const result = !!string.match(regex1) || !!string.match(regex2);
    expect(result).to.be.true;
});

spies

You can use sinon or chai-spies to create a spy. See an example below:

> npm i -s chai-spies

const chai = require('chai');
const spies = require('chai-spies');
const expect = chai.expect;
chai.use(spies);

// setup the console.log spy listener
let spy = chai.spy.on(console, 'log');

// load the user file content
const example = require('BASE/path/to/example.js');

it('should write "hello world" to the console', () => {
    expect(spy).to.have.been.called.with('hello world');
});

Dynamic Tests

There are situations where you might want to change data based on the current task. In this case, be careful, as all earlier tests must continue to pass.

Some variables are passed into the test runner through the node environment process.env.

See an example of dynamic data based on the task position below:

const data = [1, 2, 3];

if (process.env.TASK_POSITION === '4') {
    data = [1, 2, 3, 4];
}

Tests can also change based on the task position.

if (process.env.TASK_POSITION !== '4') {
    it('should do this', function () { ... });
} else {
    it('should to that', function () { ... });
}

See a full example.



Edit

8. Config

CodeRoad tutorial configurations can be set in the package.json config.

{
  "config": {
    "language": "JS",
    "dir": "tutorial",
    "testSuffix": ".js",
    "runner": "mocha-coderoad",
    "edit": true
  }
}

This section will likely expand in the future. For now, let’s go over some of the configurations.

language

The programming language being used (for example: “JS”, “Python”, etc.)

dir

The relative path to the unit test directory (from your tutorial directory). This makes writing unit tests paths easier.

testSuffix

The common suffix for unit tests. Also making writing unit test paths shorter.

runner

Specified test runner. Currently only mocha-coderoad is available.

edit

If set to true, Atom-CodeRoad will allow users to submit issues or submit markdown pull requests. You will also need to specify “repository” & “bugs” in your package.json file. See an example config file.



Edit

9. Publishing

Make sure your tutorial is ready and user tested before publishing to NPM.

In the future this process will be further simplified by > coderoad publish [version].

Completing your package.json

  • “name” - beginning with coderoad-
  • “description” - about your tutorial
  • “author” or “authors” information
  • “keywords” - about your tutorial
  • “repository” - if added, Atom-CodeRoad can allow edits
  • “bugs” - if added, Atom-CodeRoad can provide a link to your github issues
  • “dependencies” - including your test runner, and test framework tools
  • “config” - see config

See an example of a complete package.json file.

Versioning

Versioning is handled by:

  • the package.json declared “version”
  • the git tag

    > git tag 0.1.0
    > git push --tags
    

Try to make sure the two specified versions match.

Publishing to NPM

Publishing to NPM is easy, which may be one of the reasons why the NPM registry is so crowded with packages. Please ensure your package name is prefixed with coderoad- to limit package registry cluttering.

Have an NPM account before running publish.

> npm publish

Promoting your Tutorial

As CodeRoad is a new project, there is not yet a place to display tutorials. If you create something for CodeRoad please send an email to coderoadapp@gmail.com, or tweet at @coderoadapp.



Edit

10. Test Runners

A CodeRoad test runner works by creating a child process and calling a test framework with target files, then returning the result as a JSON object.

In this way, the test runner not only determines how unit tests will be written, but it actually determines the programming language used in the tutorial.

Any programming language can potentially be used with CodeRoad, you need only change the test runner. This is possible because tests are called from the command line.

Current Test Runners

We need more test runners. Why not build one?

How to Build a New Test Runner

If you’re interested in helping CodeRoad support a different programming language or test framework of your choice, here’s how to set up the test runner.

The test runner should spawn a child process. Think of this like your program opening up a terminal, typing in some command line commands to run tests, then collecting and returning the the results to Atom-CodeRoad. See an example child process created inside of Atom for mocha-coderoad.

The test runner is called in Atom-CodeRoad with three ordered inputs, the final acting as a callback function that returns the result.

See a brief example from the mocha-coderoad runner, as well as a code summary below:

// input: an object with keys of
export default function runner({testString, config, handleResult}) {
  /* ... */
  handleResult(result); // returns test result
}

Also notice that the runner in the above example handles any console.log statements. A special character string is added before the result, any data without that match is passed to the log.

if (!match) {
  try {
    console.log(data);
  } catch (e) {
    console.log(data);
  }
  return;
}

In order to process the data correctly, CodeRoad overrides the console.log function with another function that parses the output and returns it’s proper type.

Let’s look at these three test runner keys that are passed into the runner as an object:

1. testString

A string with all test input. This includes unit tests, the user input from the text editor, and some helper functions behind the scenes.

2. config

A JSON object of configurations that may be needed for setting up your runner. See an example below:

{
  "dir": "path/to/user/project",
  "taskPosition": 0
}

3. handleResult

A callback function that should be called with the result object. Results should pass if all tests pass, or fail if even a single test fails.

The result should output the taskPosition after the test. The field change represents the difference between the starting ‘taskPosition’ and the resulting ‘taskPosition’.

pass
{
  "pass": true,
  "taskPosition": 1,
  "change": 1,
  "msg": "Task 1 Complete",
  "completed": false
}

completed indicates all tests have passed. pass indicates at least one test has passed.

fail
{
  "pass": false,
  "taskPosition": 0,
  "change": 0,
  "msg": "`var secret` should be 42",
  "timedOut": false
}

If you need help setting up a new test runner, please send an email to coderoadapp@gmail.com.



Edit

11. Roadmap

CodeRoad will become more flexible & powerful with time.

v1.0.0

  • More test runners for different programming languages
  • Solutions when stuck

v2.0.0

  • User accounts
  • Tutorial ratings
  • Features API, to allow for video, quizzes, etc.
  • Optional page templates
  • Suggestions?