diff --git a/README.md b/README.md
index 0ee9be12..f5aa8668 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,8 @@ JSDoc.
In rare cases, users may have their Java CLASSPATH configured to override the
included Rhino and point to an older version of Rhino instead. If this is the
-case, simply correct the CLASSPATH to remove the older Rhino.
+case, simply correct the CLASSPATH to remove the older Rhino. (On OS X, you may
+need to remove the file `~/Library/Java/Extensions/js.jar`.)
The version of Rhino distributed with JSDoc 3 can be found here:
https://github.com/hegemonic/rhino
diff --git a/lib/jsdoc/tutorial.js b/lib/jsdoc/tutorial.js
index 28f794db..81ef5b5b 100644
--- a/lib/jsdoc/tutorial.js
+++ b/lib/jsdoc/tutorial.js
@@ -6,6 +6,27 @@
var markdown = require('jsdoc/util/markdown');
+/** Removes child tutorial from the parent. Does *not* unset child.parent though.
+ @param {Tutorial} parent - parent tutorial.
+ @param {Tutorial} child - Old child.
+ @private
+ */
+function removeChild(parent, child) {
+ var index = parent.children.indexOf(child);
+ if (index != -1) {
+ parent.children.splice(index, 1);
+ }
+}
+
+/** Adds a child to the parent tutorial. Does *not* set child.parent though.
+ @param {Tutorial} parent - parent tutorial.
+ @param {Tutorial} child - New child.
+ @private
+ */
+function addChild(parent, child) {
+ parent.children.push(child);
+}
+
/**
@module jsdoc/tutorial
*/
@@ -28,33 +49,32 @@ exports.Tutorial = function(name, content, type) {
};
/** Moves children from current parent to different one.
- @param {Tutorial} parent - New parent.
+ @param {?Tutorial} parent - New parent. If null, the tutorial has no parent.
*/
exports.Tutorial.prototype.setParent = function(parent) {
// removes node from old parent
if (this.parent) {
- this.parent.removeChild(this);
+ removeChild(this.parent, this);
}
this.parent = parent;
- this.parent.addChild(this);
+ if (parent) {
+ addChild(parent, this);
+ }
};
/** Removes children from current node.
@param {Tutorial} child - Old child.
*/
exports.Tutorial.prototype.removeChild = function(child) {
- var index = this.children.indexOf(child);
- if (index != -1) {
- this.children.splice(index, 1);
- }
+ child.setParent(null);
};
/** Adds new children to current node.
@param {Tutorial} child - New child.
*/
exports.Tutorial.prototype.addChild = function(child) {
- this.children.push(child);
+ child.setParent(this);
};
/** Prepares source.
diff --git a/lib/jsdoc/tutorial/resolver.js b/lib/jsdoc/tutorial/resolver.js
index 1cb7884d..0a54bbb2 100644
--- a/lib/jsdoc/tutorial/resolver.js
+++ b/lib/jsdoc/tutorial/resolver.js
@@ -11,20 +11,80 @@
var tutorial = require('jsdoc/tutorial'),
fs = require('jsdoc/fs'),
+ error = require('jsdoc/util/error'),
path = require('path'),
hasOwnProp = Object.prototype.hasOwnProperty,
conf = {},
tutorials = {},
finder = /^(.*)\.(x(?:ht)?ml|html?|md|markdown|json)$/i;
+/** checks if `conf` is the metadata for a single tutorial.
+ * A tutorial's metadata has a property 'title' and/or a property 'children'.
+ * @param {object} json - the object we want to test (typically from JSON.parse)
+ * @returns {boolean} whether `json` could be the metadata for a tutorial.
+ */
+function isTutorialJSON(json) {
+ // if conf.title exists or conf.children exists, it is metadata for a tutorial
+ return (json.hasOwnProperty('title') || json.hasOwnProperty('children'));
+}
+
+/** Helper function that adds tutorial configuration to the `conf` variable.
+ * This helps when multiple tutorial configurations are specified in one object,
+ * or when a tutorial's children are specified as tutorial configurations as
+ * opposed to an array of tutorial names.
+ *
+ * Recurses as necessary to ensure all tutorials are added.
+ *
+ * @param {string} name - if `meta` is a configuration for a single tutorial,
+ * this is that tutorial's name.
+ * @param {object} meta - object that contains tutorial information.
+ * Can either be for a single tutorial, or for multiple
+ * (where each key in `meta` is the tutorial name and each
+ * value is the information for a single tutorial).
+ * Additionally, a tutorial's 'children' property may
+ * either be an array of strings (names of the child tutorials),
+ * OR an object giving the configuration for the child tutorials.
+ */
+function addTutorialConf(name, meta) {
+ var names, i;
+ if (isTutorialJSON(meta)) {
+ // if the children are themselves tutorial defintions as opposed to an
+ // array of strings, add each child.
+ if (meta.hasOwnProperty('children') && !Array.isArray(meta.children)) {
+ names = Object.keys(meta.children);
+ for (i = 0; i < names.length; ++i) {
+ addTutorialConf(names[i], meta.children[names[i]]);
+ }
+ // replace with an array of names.
+ meta.children = names;
+ }
+ // check if the tutorial has already been defined...
+ if (conf.hasOwnProperty(name)) {
+ error.handle(new Error("Tutorial " + name + "'s metadata is defined multiple times, only the first will be used."));
+ } else {
+ conf[name] = meta;
+ }
+ } else {
+ // it's an object of tutorials, the keys are th etutorial names.
+ names = Object.keys(meta);
+ for (i = 0; i < names.length; ++i) {
+ addTutorialConf(names[i], meta[names[i]]);
+ }
+ }
+}
+
/** Adds new tutorial.
@param {tutorial.Tutorial} current - New tutorial.
*/
exports.addTutorial = function(current) {
- tutorials[current.name] = current;
+ if (tutorials.hasOwnProperty(current.name)) {
+ error.handle(new Error("Tutorial with name " + current.name + " exists more than once, not adding (same name, different file extensions?)"));
+ } else {
+ tutorials[current.name] = current;
- // default temporary parent
- current.setParent(exports.root);
+ // default temporary parent
+ current.setParent(exports.root);
+ }
};
/** Root tutorial.
@@ -77,7 +137,8 @@ exports.load = function(_path) {
// configuration file
case 'json':
- conf[name] = JSON.parse(content);
+ var meta = JSON.parse(content);
+ addTutorialConf(name, meta);
// don't add this as a tutorial
return;
@@ -117,7 +178,7 @@ exports.resolve = function() {
if (item.children) {
item.children.forEach(function(child) {
if (!(child in tutorials)) {
- require('jsdoc/util/error').handle( new Error("Missing child tutorial: " + child) );
+ error.handle( new Error("Missing child tutorial: " + child) );
}
else {
tutorials[child].setParent(current);
diff --git a/test/specs/jsdoc/tutorial.js b/test/specs/jsdoc/tutorial.js
index 9330fa89..1644c534 100644
--- a/test/specs/jsdoc/tutorial.js
+++ b/test/specs/jsdoc/tutorial.js
@@ -1,4 +1,226 @@
/*global describe: true, env: true, it: true */
describe("jsdoc/tutorial", function() {
- //TODO
-});
\ No newline at end of file
+ var tutorial = require('jsdoc/tutorial'),
+ name = "tuteID",
+ content = "Tutorial content blah blah blah & <",
+ tute = new tutorial.Tutorial(name, content, tutorial.TYPES.NOTAVALUE),
+ par = new tutorial.Tutorial('parent', "# This is the parent tutorial's content & stuff A_B X_Y",
+ tutorial.TYPES.MARKDOWN),
+ par2 = new tutorial.Tutorial('parent2', "
This is the second parent tutorial
",
+ tutorial.TYPES.HTML);
+
+
+ it("module should exist", function() {
+ expect(tutorial).toBeDefined();
+ expect(typeof tutorial).toBe("object");
+ });
+
+ it("should export a Tutorial function", function() {
+ expect(tutorial.Tutorial).toBeDefined();
+ expect(typeof tutorial.Tutorial).toBe("function");
+ });
+
+ it("should export a TYPES object", function() {
+ expect(tutorial.TYPES).toBeDefined();
+ expect(typeof tutorial.TYPES).toBe("object");
+ });
+
+ describe("tutorial.TYPES", function() {
+ it("should have a HTML property", function() {
+ expect(tutorial.TYPES.HTML).toBeDefined();
+ });
+
+ it("should have a MARKDOWN property", function() {
+ expect(tutorial.TYPES.MARKDOWN).toBeDefined();
+ });
+ });
+
+ describe("Tutorial", function() {
+ it("should have 'setParent' function", function() {
+ expect(tutorial.Tutorial.prototype.setParent).toBeDefined();
+ expect(typeof tutorial.Tutorial.prototype.setParent).toBe("function");
+ });
+
+ it("should have 'removeChild' function", function() {
+ expect(tutorial.Tutorial.prototype.removeChild).toBeDefined();
+ expect(typeof tutorial.Tutorial.prototype.removeChild).toBe("function");
+ });
+
+ it("should have 'addChild' function", function() {
+ expect(tutorial.Tutorial.prototype.addChild).toBeDefined();
+ expect(typeof tutorial.Tutorial.prototype.addChild).toBe("function");
+ });
+
+ it("should have 'parse' function", function() {
+ expect(tutorial.Tutorial.prototype.parse).toBeDefined();
+ expect(typeof tutorial.Tutorial.prototype.parse).toBe("function");
+ });
+
+ it("should have a 'name' property", function() {
+ expect(tute.name).toBeDefined();
+ expect(typeof tute.name).toBe("string");
+ expect(tute.name).toBe(name);
+ });
+
+ it("should have a 'title' property, by default set to to the tute's name", function() {
+ expect(tute.title).toBeDefined();
+ expect(typeof tute.title).toBe("string");
+ expect(tute.title).toBe(name);
+ // Testing of overriding a tutorial's title in its JSON file is
+ // covered in tutorial/resolver.js tests.
+ });
+
+ it("should have a 'content' property set to the tutorial's content", function() {
+ expect(tute.content).toBeDefined();
+ expect(typeof tute.content).toBe("string");
+ expect(tute.content).toBe(content);
+ });
+
+ it("should have a 'type' property set to the tutorial's type", function() {
+ expect(par.type).toBeDefined();
+ expect(typeof par.type).toBe(typeof tutorial.TYPES.MARKDOWN);
+ expect(par.type).toBe(tutorial.TYPES.MARKDOWN);
+ });
+
+ it("should have a 'parent' property, initially null", function() {
+ expect(tute.parent).toBeDefined();
+ expect(tute.parent).toBe(null);
+ });
+
+ it("should have a 'children' property, an empty array", function() {
+ expect(tute.children).toBeDefined();
+ expect(Array.isArray(tute.children)).toBe(true);
+ expect(tute.children.length).toBe(0);
+ });
+
+ describe("setParent", function() {
+ it("adding a parent sets the child's 'parent' property", function() {
+ tute.setParent(par);
+ expect(tute.parent).toBe(par);
+ });
+
+ it("adding a parent adds the child to the parent's 'children' property", function() {
+ expect(par.children).toContain(tute);
+ });
+
+ it("re-parenting removes the child from the previous parent", function() {
+ tute.setParent(par2);
+
+ expect(tute.parent).toBe(par2);
+ expect(par2.children).toContain(tute);
+ expect(par.children).not.toContain(tute);
+ });
+
+ it("calling setParent with a null parent unsets the child's parent and removes the child from its previous parent", function() {
+ expect(par2.children).toContain(tute);
+ tute.setParent(null);
+
+ expect(tute.parent).toBe(null);
+ expect(par2.children).not.toContain(tute);
+ });
+ });
+
+ describe("addChild", function() {
+ it("adding a child tutorial adds the child to the parent's 'children' property", function() {
+ tute.setParent(null);
+ var n = par.children.length;
+
+ par.addChild(tute);
+
+ expect(par.children.length).toBe(n + 1);
+ expect(par.children).toContain(tute);
+ });
+
+ it("adding a child tutorial sets the child's parent to to the parent tutorial", function() {
+ expect(tute.parent).toBe(par);
+ });
+
+ it("adding a child tutorial removes the child from its old parent", function() {
+ // tue is currently owned by par; we reparent it to par2
+ expect(tute.parent).toBe(par);
+ par2.addChild(tute);
+
+ expect(tute.parent).toBe(par2);
+ expect(par.children).not.toContain(tute);
+ expect(par2.children).toContain(tute);
+ });
+ });
+
+ describe("removeChild", function() {
+ function removeChild() {
+ par2.removeChild(par);
+ }
+
+ it("removing a tutorial that is not a child silently passes", function() {
+ var n = par2.children.length;
+ expect(removeChild).not.toThrow();
+ expect(par2.children.length).toBe(n);
+ });
+
+ it("removing a child removes the child from the parent's 'children' property", function() {
+ tute.setParent(par2);
+ expect(par2.children.length).toBe(1);
+
+ par2.removeChild(tute);
+
+ expect(par2.children).not.toContain(tute);
+ expect(par2.children.length).toBe(0);
+ });
+
+ it("removing a child unsets the child's 'parent' property", function() {
+ expect(tute.parent).toBe(null);
+ });
+ });
+
+ describe("various inheritance tests with addChild, setParent and removeChild", function() {
+ it("parenting and unparenting via addChild, setParent and removeChild makes sure inheritance is set accordingly", function() {
+ // unparent everything.
+ tute.setParent(null);
+ par.setParent(null);
+ par2.setParent(null);
+
+ // let tute belong to par
+ tute.setParent(par);
+ expect(tute.parent).toBe(par);
+ expect(par2.children.length).toBe(0);
+ expect(par.children.length).toBe(1);
+ expect(par.children[0]).toBe(tute);
+
+ // addChild tute to par2. its parent should now be par2, and
+ // it can't be the child of two parents
+ par2.addChild(tute);
+ expect(tute.parent).toBe(par2);
+ expect(par.children.length).toBe(0);
+ expect(par2.children.length).toBe(1);
+ expect(par2.children[0]).toBe(tute);
+
+ // removeChild tute from par2. tute should now be unparented.
+ par2.removeChild(tute);
+ expect(tute.parent).toBe(null);
+ expect(par.children.length).toBe(0);
+ expect(par2.children.length).toBe(0);
+ });
+ });
+
+ describe("parse", function() {
+ it("Tutorials with HTML type return content as-is", function() {
+ expect(par2.parse()).toBe("This is the second parent tutorial
");
+ });
+
+ it("Tutorials with MARKDOWN type go through the markdown parser, respecting configuration options", function() {
+ var old = env.conf.markdown;
+ env.conf.markdown = {parser: 'evilstreak'};
+ expect(par.parse()).toBe("This is the parent tutorial's content & stuff AB XY
");
+
+ env.conf.markdown = {parser: 'gfm'};
+ expect(par.parse()).toBe("This is the parent tutorial's content & stuff A_B X_Y
");
+
+ env.conf.markdown = old;
+ });
+
+ it("Tutorials with unrecognised type are returned as-is", function() {
+ expect(tute.parse()).toBe(content);
+ });
+ });
+ });
+});
diff --git a/test/specs/jsdoc/tutorial/resolver.js b/test/specs/jsdoc/tutorial/resolver.js
index 0322ad4d..5d1260b0 100644
--- a/test/specs/jsdoc/tutorial/resolver.js
+++ b/test/specs/jsdoc/tutorial/resolver.js
@@ -1,33 +1,214 @@
/*global afterEach: true, describe: true, env: true, expect: true, it: true */
describe("jsdoc/tutorial/resolver", function() {
- /*jshint evil: true */
-
- // TODO: more tests
-
var resolver = require('jsdoc/tutorial/resolver'),
+ tutorial = require('jsdoc/tutorial'),
lenient = !!env.opts.lenient,
log = eval(console.log);
- function missingTutorial() {
- resolver.load(__dirname + "/test/tutorials/incomplete");
+ /*jshint evil: true */
+ it("should exist", function() {
+ expect(resolver).toBeDefined();
+ expect(typeof resolver).toEqual('object');
+ });
+
+ it("should export a 'addTutorial' function", function() {
+ expect(resolver.addTutorial).toBeDefined();
+ expect(typeof resolver.addTutorial).toEqual("function");
+ });
+
+ it("should export a 'load' function", function() {
+ expect(resolver.load).toBeDefined();
+ expect(typeof resolver.load).toEqual("function");
+ });
+
+ it("should export a 'resolve' function", function() {
+ expect(resolver.resolve).toBeDefined();
+ expect(typeof resolver.resolve).toEqual("function");
+ });
+
+ it("should export a 'root' tutorial", function() {
+ expect(resolver.root).toBeDefined();
+ expect(resolver.root instanceof tutorial.Tutorial).toEqual(true);
+ });
+
+ it("exported 'root' tutorial should export a 'getByName' function", function() {
+ expect(resolver.root.getByName).toBeDefined();
+ expect(typeof resolver.root.getByName).toEqual("function");
+ });
+
+ // note: every time we addTutorial or run the resolver, we are *adding*
+ // to the root tutorial.
+
+ // addTutorial
+ var tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML);
+ resolver.addTutorial(tute);
+ describe("addTutorial", function() {
+
+ it("should add a default parent of the root tutorial", function() {
+ expect(tute.parent).toEqual(resolver.root);
+ });
+
+ it("should be added to the root tutorial as a child", function() {
+ expect(resolver.root.children[0]).toEqual(tute);
+ });
+ });
+
+ // root.getByName
+ describe("root.getByName", function() {
+ it("can retrieve tutorials by name", function() {
+ expect(resolver.root.getByName('myTutorial')).toEqual(tute);
+ });
+ });
+
+ // load
+ resolver.load(__dirname + "/test/tutorials/tutorials");
+ var childNames = resolver.root.children.map(function (t) { return t.name; }),
+ test = resolver.root.getByName('test'),
+ test2 = resolver.root.getByName('test2'),
+ test3 = resolver.root.getByName('test3'),
+ test4 = resolver.root.getByName('test4');
+ test6 = resolver.root.getByName('test6');
+
+ describe("load", function() {
+
+ it("all tutorials are added, initially as top-level tutorials", function() {
+ // check they were added
+ expect(test).toBeDefined();
+ expect(test2).toBeDefined();
+ expect(test3).toBeDefined();
+ expect(test4).toBeDefined();
+ expect(test6).toBeDefined();
+ // check they are top-level in resolver.root
+ expect(childNames.indexOf('test')).not.toEqual(-1);
+ expect(childNames.indexOf('test2')).not.toEqual(-1);
+ expect(childNames.indexOf('test3')).not.toEqual(-1);
+ expect(childNames.indexOf('test4')).not.toEqual(-1);
+ expect(childNames.indexOf('test6')).not.toEqual(-1);
+ });
+
+ it("non-tutorials are skipped", function() {
+ expect(resolver.root.getByName('multple')).toBeUndefined();
+ expect(resolver.root.getByName('test5')).toBeUndefined();
+ });
+
+
+ it("tutorial types are determined correctly", function() {
+ // test.html, test2.markdown, test3.html, test4.md, test6.xml
+ expect(test.type).toEqual(tutorial.TYPES.HTML);
+ expect(test2.type).toEqual(tutorial.TYPES.MARKDOWN);
+ expect(test3.type).toEqual(tutorial.TYPES.HTML);
+ expect(test4.type).toEqual(tutorial.TYPES.MARKDOWN);
+ expect(test6.type).toEqual(tutorial.TYPES.HTML);
+ });
+
+ });
+
+ // resolve
+ // myTutorial
+ // test
+ // |- test2
+ // |- test6
+ // |- test3
+ // |- test4
+ describe("resolve", function() {
resolver.resolve();
- }
+ it("hierarchy is resolved properly no matter how the children property is defined", function() {
+ // root has child 'test'
+ expect(resolver.root.children.length).toEqual(2);
+ expect(resolver.root.children.indexOf(test)).not.toEqual(-1);
+ expect(test.parent).toEqual(resolver.root);
- afterEach(function() {
- env.opts.lenient = lenient;
- console.log = log;
+ // test has child 'test2'
+ expect(test.children.length).toEqual(1);
+ expect(test.children[0]).toEqual(test2);
+ expect(test2.parent).toEqual(test);
+
+ // test2 has children test3, test6
+ expect(test2.children.length).toEqual(2);
+ expect(test2.children.indexOf(test3)).not.toEqual(-1);
+ expect(test2.children.indexOf(test6)).not.toEqual(-1);
+ expect(test3.parent).toEqual(test2);
+ expect(test6.parent).toEqual(test2);
+
+ // test3 has child test4
+ expect(test3.children.length).toEqual(1);
+ expect(test3.children[0]).toEqual(test4);
+ expect(test4.parent).toEqual(test3);
+ });
+
+ it("tutorials without configuration files have titles matching filenames", function() {
+ // test6.xml didn't have a metadata
+ expect(test6.title).toEqual('test6');
+ });
+
+ it("tutorials with configuration files have titles matching filenames", function() {
+ // test.json had info for just test.json
+ expect(test.title).toEqual("Test tutorial");
+ });
+
+ it("multiple tutorials can appear in a configuration file", function() {
+ expect(test2.title).toEqual("Test 2");
+ expect(test3.title).toEqual("Test 3");
+ expect(test4.title).toEqual("Test 4");
+ });
});
- it("throws an exception for missing tutorials if the lenient option is not enabled", function() {
- env.opts.lenient = false;
+ // error reporting.
+ describe("Error reporting", function() {
+ // Tests for error reporting.
+ function missingTutorial() {
+ resolver.load(__dirname + "/test/tutorials/incomplete");
+ resolver.resolve();
+ }
+ function duplicateNamedTutorials() {
+ // can't add a tutorial if another with its name has already been added
+ resolver.addTutorial(tute);
+ }
+ function duplicateDefinedTutorials() {
+ // can't have a tutorial's metadata defined twice in .json files
+ resolver.load(__dirname + "/test/tutorials/duplicateDefined");
+ resolver.resolve();
+ }
- expect(missingTutorial).toThrow();
+ afterEach(function() {
+ env.opts.lenient = lenient;
+ console.log = log;
+ });
+
+ it("throws an exception for missing tutorials if the lenient option is not enabled", function() {
+ env.opts.lenient = false;
+
+ expect(missingTutorial).toThrow();
+ });
+
+ it("doesn't throw an exception for missing tutorials if the lenient option is enabled", function() {
+ console.log = function() {};
+ env.opts.lenient = true;
+
+ expect(missingTutorial).not.toThrow();
+ });
+
+ it("throws an exception for duplicate-named tutorials (e.g. test.md, test.html) if the lenient option is not enabled", function() {
+ env.opts.lenient = false;
+ expect(duplicateNamedTutorials).toThrow();
+ });
+
+ it("doesn't throw an exception for duplicate-named tutorials (e.g. test.md, test.html) if the lenient option is not enabled", function() {
+ console.log = function() {};
+ env.opts.lenient = true;
+ expect(duplicateNamedTutorials).not.toThrow();
+ });
+
+ it("throws an exception for tutorials defined twice in .jsons if the lenient option is not enabled", function() {
+ env.opts.lenient = false;
+ expect(duplicateDefinedTutorials).toThrow();
+ });
+
+ it("doesn't throw an exception for tutorials defined twice in .jsons if the lenient option is not enabled", function() {
+ console.log = function() {};
+ env.opts.lenient = true;
+ expect(duplicateDefinedTutorials).not.toThrow();
+ });
});
- it("doesn't throw an exception for missing tutorials if the lenient option is enabled", function() {
- console.log = function() {};
- env.opts.lenient = true;
-
- expect(missingTutorial).not.toThrow();
- });
-});
\ No newline at end of file
+});
diff --git a/test/tutorials/duplicateDefined/asdf.html b/test/tutorials/duplicateDefined/asdf.html
new file mode 100644
index 00000000..1f8a6622
--- /dev/null
+++ b/test/tutorials/duplicateDefined/asdf.html
@@ -0,0 +1 @@
+tutorial ASDF
diff --git a/test/tutorials/duplicateDefined/asdf.json b/test/tutorials/duplicateDefined/asdf.json
new file mode 100644
index 00000000..c769dc54
--- /dev/null
+++ b/test/tutorials/duplicateDefined/asdf.json
@@ -0,0 +1 @@
+{"title": "Conflicting title"}
diff --git a/test/tutorials/duplicateDefined/index.json b/test/tutorials/duplicateDefined/index.json
new file mode 100644
index 00000000..9ceb182f
--- /dev/null
+++ b/test/tutorials/duplicateDefined/index.json
@@ -0,0 +1,5 @@
+{
+ "asdf": {
+ "title": "Tutorial Asdf"
+ }
+}
diff --git a/test/tutorials/incomplete/test.html b/test/tutorials/incomplete/parent.html
similarity index 100%
rename from test/tutorials/incomplete/test.html
rename to test/tutorials/incomplete/parent.html
diff --git a/test/tutorials/incomplete/parent.json b/test/tutorials/incomplete/parent.json
new file mode 100644
index 00000000..f2e4765a
--- /dev/null
+++ b/test/tutorials/incomplete/parent.json
@@ -0,0 +1 @@
+{"title": "missing child tutorial", "children": ["child"]}
diff --git a/test/tutorials/incomplete/test.json b/test/tutorials/incomplete/test.json
deleted file mode 100644
index 078dc3f5..00000000
--- a/test/tutorials/incomplete/test.json
+++ /dev/null
@@ -1 +0,0 @@
-{"title": "missing child tutorial", "children": ["test2"]}
diff --git a/test/tutorials/tutorials/multiple.json b/test/tutorials/tutorials/multiple.json
new file mode 100644
index 00000000..994d8655
--- /dev/null
+++ b/test/tutorials/tutorials/multiple.json
@@ -0,0 +1,12 @@
+{
+ "test2": {
+ "title": "Test 2",
+ "children": ["test3", "test6"]
+ },
+ "test3": {
+ "title": "Test 3",
+ "children": {
+ "test4": {"title": "Test 4"}
+ }
+ }
+}
diff --git a/test/tutorials/tutorials/test2.json b/test/tutorials/tutorials/test2.json
deleted file mode 100644
index 3c7d98c5..00000000
--- a/test/tutorials/tutorials/test2.json
+++ /dev/null
@@ -1 +0,0 @@
-{"title": "Test 2"}
diff --git a/test/tutorials/tutorials/test3.htm b/test/tutorials/tutorials/test3.htm
new file mode 100644
index 00000000..8eac0524
--- /dev/null
+++ b/test/tutorials/tutorials/test3.htm
@@ -0,0 +1,3 @@
+Test3.html
+
+{@link Test}
diff --git a/test/tutorials/tutorials/test4.md b/test/tutorials/tutorials/test4.md
new file mode 100644
index 00000000..0be836ad
--- /dev/null
+++ b/test/tutorials/tutorials/test4.md
@@ -0,0 +1 @@
+# test4.md
diff --git a/test/tutorials/tutorials/test5.txt b/test/tutorials/tutorials/test5.txt
new file mode 100644
index 00000000..1769f0de
--- /dev/null
+++ b/test/tutorials/tutorials/test5.txt
@@ -0,0 +1 @@
+Should not be included as a tutorial.
diff --git a/test/tutorials/tutorials/test6.xml b/test/tutorials/tutorials/test6.xml
new file mode 100644
index 00000000..1c489be3
--- /dev/null
+++ b/test/tutorials/tutorials/test6.xml
@@ -0,0 +1 @@
+test 6 - has no metadata