/*global afterEach, beforeEach, describe, expect, env, it, jasmine, spyOn */ var hasOwnProp = Object.prototype.hasOwnProperty; describe("jsdoc/util/templateHelper", function() { var helper = require('jsdoc/util/templateHelper'), doclet = require('jsdoc/doclet'), doop = require('jsdoc/util/doop'), logger = require('jsdoc/util/logger'), resolver = require('jsdoc/tutorial/resolver'), taffy = require('taffydb').taffy; helper.registerLink('test', 'path/to/test.html'); it("should exist", function() { expect(helper).toBeDefined(); expect(typeof helper).toBe('object'); }); it("should export a 'setTutorials' function", function() { expect(helper.setTutorials).toBeDefined(); expect(typeof helper.setTutorials).toBe("function"); }); it("should export a 'globalName' property", function() { expect(helper.globalName).toBeDefined(); expect(typeof helper.globalName).toBe("string"); }); it("should export a 'fileExtension' property", function() { expect(helper.fileExtension).toBeDefined(); expect(typeof helper.fileExtension).toBe("string"); }); it("should export a 'scopeToPunc' property", function() { expect(helper.scopeToPunc).toBeDefined(); expect(typeof helper.scopeToPunc).toBe("object"); }); it("should export a 'getUniqueFilename' function", function() { expect(helper.getUniqueFilename).toBeDefined(); expect(typeof helper.getUniqueFilename).toBe("function"); }); it("should export a 'longnameToUrl' property", function() { expect(helper.longnameToUrl).toBeDefined(); expect(typeof helper.longnameToUrl).toBe("object"); }); it("should export a 'linkto' function", function() { expect(helper.linkto).toBeDefined(); expect(typeof helper.linkto).toBe("function"); }); it("should export an 'htmlsafe' function", function() { expect(helper.htmlsafe).toBeDefined(); expect(typeof helper.htmlsafe).toBe("function"); }); it("should export a 'find' function", function() { expect(helper.find).toBeDefined(); expect(typeof helper.find).toBe("function"); }); it("should export a 'getMembers' function", function() { expect(helper.getMembers).toBeDefined(); expect(typeof helper.getMembers).toBe("function"); }); it("should export a 'getAttribs' function", function() { expect(helper.getAttribs).toBeDefined(); expect(typeof helper.getAttribs).toBe("function"); }); it("should export a 'getSignatureTypes' function", function() { expect(helper.getSignatureTypes).toBeDefined(); expect(typeof helper.getSignatureTypes).toBe("function"); }); it("should export a 'getSignatureParams' function", function() { expect(helper.getSignatureParams).toBeDefined(); expect(typeof helper.getSignatureParams).toBe("function"); }); it("should export a 'getSignatureReturns' function", function() { expect(helper.getSignatureReturns).toBeDefined(); expect(typeof helper.getSignatureReturns).toBe("function"); }); it("should export a 'getAncestorLinks' function", function() { expect(helper.getAncestorLinks).toBeDefined(); expect(typeof helper.getAncestorLinks).toBe("function"); }); it("should export a 'addEventListeners' function", function() { expect(helper.addEventListeners).toBeDefined(); expect(typeof helper.addEventListeners).toBe("function"); }); it("should export a 'prune' function", function() { expect(helper.prune).toBeDefined(); expect(typeof helper.prune).toBe("function"); }); it("should export a 'registerLink' function", function() { expect(helper.registerLink).toBeDefined(); expect(typeof helper.registerLink).toBe("function"); }); it("should export a 'tutorialToUrl' function", function() { expect(helper.tutorialToUrl).toBeDefined(); expect(typeof helper.tutorialToUrl).toBe("function"); }); it("should export a 'toTutorial' function", function() { expect(helper.toTutorial).toBeDefined(); expect(typeof helper.toTutorial).toBe("function"); }); it("should export a 'resolveLinks' function", function() { expect(helper.resolveLinks).toBeDefined(); expect(typeof helper.resolveLinks).toBe("function"); }); it("should export a 'resolveAuthorLinks' function", function() { expect(helper.resolveAuthorLinks).toBeDefined(); expect(typeof helper.resolveAuthorLinks).toBe("function"); }); it("should export a 'createLink' function", function() { expect(helper.createLink).toBeDefined(); expect(typeof helper.createLink).toBe("function"); }); describe("setTutorials", function() { // used in tutorialToUrl, toTutorial. it("setting tutorials to null causes all tutorial lookups to fail", function() { // bit of a dodgy test but the best I can manage. setTutorials doesn't do much. helper.setTutorials(null); // should throw error: no 'getByName' in tutorials. expect(function () { return helper.tutorialToUrl('asdf'); }).toThrow(); }); it("setting tutorials to the root tutorial object lets lookups work", function() { helper.setTutorials(resolver.root); spyOn(resolver.root, 'getByName'); helper.tutorialToUrl('asdf'); expect(resolver.root.getByName).toHaveBeenCalled(); }); }); describe("globalName", function() { it("should equal 'global'", function() { expect(helper.globalName).toBe('global'); }); }); describe("fileExtension", function() { it("should equal '.html'", function() { expect(helper.fileExtension).toBe('.html'); }); }); describe("scopeToPunc", function() { it("should map 'static' to '.', 'inner', to '~', 'instance' to '#'", function() { expect(helper.scopeToPunc).toEqual({static: '.', inner: '~', instance: '#'}); }); }); describe("getUniqueFilename", function() { // TODO: needs more tests for unusual values and things that get special treatment (such as // inner members) it('should convert a simple string into the string plus the default extension', function() { var filename = helper.getUniqueFilename('BackusNaur'); expect(filename).toBe('BackusNaur.html'); }); it('should convert a string with slashes into the text following the last slash plus the default extension', function() { var filename = helper.getUniqueFilename('tick/tock'); expect(filename).toMatch(/^tock\.html$/); }); it('should not return the same filename twice', function() { var name = 'polymorphic'; var filename1 = helper.getUniqueFilename(name); var filename2 = helper.getUniqueFilename(name); expect(filename1).not.toBe(filename2); }); it('should not consider the same name with different letter case to be unique', function() { var camel = 'myJavaScriptIdentifier'; var pascal = 'MyJavaScriptIdentifier'; var filename1 = helper.getUniqueFilename(camel); var filename2 = helper.getUniqueFilename(pascal); expect( filename1.toLowerCase() ).not.toBe( filename2.toLowerCase() ); }); it('should remove variations from the longname before generating the filename', function() { var filename = helper.getUniqueFilename('MyClass(foo, bar)'); expect(filename).toBe('MyClass.html'); }); }); describe("longnameToUrl", function() { it("is an object", function() { expect(typeof helper.longnameToUrl).toBe('object'); }); it("has an entry added into it by calling registerLink", function() { helper.registerLink('MySymbol', 'asdf.html'); expect(helper.longnameToUrl.MySymbol).toBeDefined(); expect(helper.longnameToUrl.MySymbol).toBe('asdf.html'); delete helper.longnameToUrl.MySymbol; }); it("adding an entry to it allows me to link with linkto", function() { helper.longnameToUrl.foo2 = 'bar.html'; expect(helper.linkto('foo2')).toBe('foo2'); delete helper.longnameToUrl.foo2; }); }); describe("linkto", function() { beforeEach(function() { helper.longnameToUrl.linktoTest = 'test.html'; helper.longnameToUrl.LinktoFakeClass = 'fakeclass.html'; }); afterEach(function() { delete helper.longnameToUrl.linktoTest; delete helper.longnameToUrl.LinktoFakeClass; }); it('returns the longname if only the longname is specified and has no URL', function() { var link = helper.linkto('example'); expect(link).toBe('example'); }); it('returns the link text if only the link text is specified', function() { var link = helper.linkto(null, 'link text'); expect(link).toBe('link text'); }); it('returns the link text if the longname does not have a URL, and both the longname and ' + 'link text are specified', function() { var link = helper.linkto('example', 'link text'); expect(link).toBe('link text'); }); it('uses the longname as the link text if no link text is provided', function() { var link = helper.linkto('linktoTest'); expect(link).toBe('linktoTest'); }); it('uses the link text if it is specified', function() { var link = helper.linkto('linktoTest', 'link text'); expect(link).toBe('link text'); }); it('includes a "class" attribute in the link if a class is specified', function() { var link = helper.linkto('linktoTest', 'link text', 'myclass'); expect(link).toBe('link text'); }); it('is careful with longnames that are reserved words in JS', function() { // we don't have a registered link for 'constructor' so it should return the text 'link text'. var link = helper.linkto('constructor', 'link text'); expect(typeof link).toBe('string'); expect(link).toBe('link text'); }); it('works correctly with type applications if only the longname is specified', function() { var link = helper.linkto('Array.'); expect(link).toBe('Array.<LinktoFakeClass>'); }); it('works correctly with type applications if a class is not specified', function() { var link = helper.linkto('Array.', 'link text'); expect(link).toBe('Array.<LinktoFakeClass>'); }); it('works correctly with type applications if a class is specified', function() { var link = helper.linkto('Array.', 'link text', 'myclass'); expect(link).toBe('Array.<LinktoFakeClass' + '>'); }); it('works correctly with type applications that include a type union', function() { var link = helper.linkto('Array.<(linktoTest|LinktoFakeClass)>', 'link text'); expect(link).toBe('Array.<(linktoTest|' + 'LinktoFakeClass)>'); }); it('returns a link when a URL is specified', function() { var link = helper.linkto('http://example.com'); expect(link).toBe('http://example.com'); }); it('returns a link if a URL wrapped in angle brackets is specified', function() { var link = helper.linkto(''); expect(link).toBe('http://example.com'); }); it('returns a link with link text if a URL and link text are specified', function() { var link = helper.linkto('http://example.com', 'text'); expect(link).toBe('text'); }); it('returns a link with a fragment ID if a URL and fragment ID are specified', function() { var link = helper.linkto('LinktoFakeClass', null, null, 'fragment'); expect(link).toBe('LinktoFakeClass'); }); it('returns the original text if an inline {@link} tag is specified', function() { var link; var text = '{@link Foo}'; function getLink() { link = helper.linkto(text); } // make sure we're not trying to parse the inline link as a type expression expect(getLink).not.toThrow(); // linkto doesn't process {@link} tags expect(link).toBe(text); }); }); describe("htmlsafe", function() { it('should convert all occurences of < to <', function() { var inp = '

Potentially dangerous.

', out = helper.htmlsafe(inp); expect(out).toBe('<h1>Potentially dangerous.</h1>'); }); it('should convert all occurrences of & to &', function() { var input = 'foo && bar & baz;'; expect( helper.htmlsafe(input) ).toBe('foo && bar & baz;'); }); it ('should not double-convert ampersands', function() { var input = '

Foo & Friends

'; expect( helper.htmlsafe(input) ).toBe('<h1>Foo & Friends</h1>'); }); }); describe("find", function() { var array = [ // match { number: 2, A: true }, // match { number: 1, A: true, D: 'hello', Q: false }, // match { number: 3, A: 'maybe', squiggle: '?' }, // no match (number not in spec) { number: 4, A: true }, // no match (missing top-level property) { A: true } ]; var matches = array.slice(0, 3); var spec = { number: [1, 2, 3], A: [true, 'maybe'] }; it('should find the requested items', function() { expect( helper.find(taffy(array), spec) ).toEqual(matches); }); }); // we can't use toEqual() because TaffyDB adds its own stuff to the array it returns. // instead, we make sure arrays a and b are the same length, and that each object in // array b has all the properties of the corresponding object in array a // used for getMembers and prune tests. function compareObjectArrays(a, b) { expect(a.length).toEqual(b.length); for (var i = 0, l = a.length; i < l; i++) { for (var prop in a[i]) { if ( hasOwnProp.call(a[i], prop) ) { expect(b[i][prop]).toBeDefined(); expect(a[i][prop]).toEqual(b[i][prop]); } } } } describe("getMembers", function() { // instead parse a file from fixtures and verify it? var classes = [ {kind: 'class'}, // global {kind: 'class', memberof: 'SomeNamespace'}, // not global ]; var externals = [ {kind: 'external'}, ]; var events = [ {kind: 'event'}, ]; var mixins = [ {kind: 'mixin'}, ]; var modules = [ {kind: 'module'}, ]; var namespaces = [ {kind: 'namespace'}, ]; var misc = [ {kind: 'function'}, // global {kind: 'member'}, // global {kind: 'constant'}, // global {kind: 'typedef'}, // global {kind: 'constant', memberof: 'module:one/two'}, // not global {kind: 'function', name: 'module:foo', longname: 'module:foo'} // not global ]; var array = classes.concat(externals.concat(events.concat(mixins.concat(modules.concat(namespaces.concat(misc)))))); var data = taffy(array); var members = helper.getMembers(data); // check the output object has properties as expected. it("should have a 'classes' property", function() { expect(members.classes).toBeDefined(); }); it("should have a 'externals' property", function() { expect(members.externals).toBeDefined(); }); it("should have a 'events' property", function() { expect(members.events).toBeDefined(); }); it("should have a 'globals' property", function() { expect(members.globals).toBeDefined(); }); it("should have a 'mixins' property", function() { expect(members.mixins).toBeDefined(); }); it("should have a 'modules' property", function() { expect(members.modules).toBeDefined(); }); it("should have a 'namespaces' property", function() { expect(members.namespaces).toBeDefined(); }); // check that things were found properly. it("classes are detected", function() { compareObjectArrays(classes, members.classes); }); it("externals are detected", function() { compareObjectArrays(externals, members.externals); }); it("events are detected", function() { compareObjectArrays(events, members.events); }); it("mixins are detected", function() { compareObjectArrays(mixins, members.mixins); }); it("modules are detected", function() { compareObjectArrays(modules, members.modules); }); it("namespaces are detected", function() { compareObjectArrays(namespaces, members.namespaces); }); it("globals are detected", function() { compareObjectArrays(misc.slice(0, -2), members.globals); }); }); describe("getAttribs", function() { var doc, attribs; it('should return an array of strings', function() { doc = new doclet.Doclet('/** ljklajsdf */', {}); attribs = helper.getAttribs(doc); expect(Array.isArray(attribs)).toBe(true); }); // tests is an object of test[doclet src] = // if false, we expect attribs to either not contain anything in whatNotToContain, // or be empty (if whatNotToContain was not provided). function doTests(tests, whatNotToContain) { for (var src in tests) { if (tests.hasOwnProperty(src)) { doc = new doclet.Doclet('/** ' + src + ' */', {}); attribs = helper.getAttribs(doc); if (tests[src]) { expect(attribs).toContain(tests[src]); } else { if (Array.isArray(whatNotToContain)) { for (var i = 0; i < whatNotToContain.length; ++i) { expect(attribs).not.toContain(whatNotToContain[i]); } } else { expect(attribs.length).toBe(0); } } } } } it('should detect if a doclet is virtual', function() { var tests = { 'My constant. \n @virtual': 'abstract', 'asdf': false }; doTests(tests); }); it("should detect if a doclet's access is not public", function() { var tests = {'@private': 'private', '@access private': 'private', '@protected': 'protected', '@access protected': 'protected', '@public': false, '@access public': false, 'asdf': false }; doTests(tests); }); it("should detect if a doclet's scope is inner or static AND it is a function or member or constant", function() { var tests = { // by default these are members '@inner': 'inner', '@instance': false, '@global': false, '@static': 'static', '@name Asdf.fdsa': 'static', '@name Outer~inner': 'inner', '@name Fdsa#asdf': false, '@name .log': false, // some tests with functions and constants '@const Asdf#FOO': false, '@const Asdf\n@inner': 'inner', '@function Asdf#myFunction': false, '@function Fdsa.MyFunction': 'static', '@function Fdsa': false, // these are not functions or members or constants, they should not have their scope recorded. '@namespace Fdsa\n@inner': false, '@class asdf': false }; doTests(tests, ['inner', 'static', 'global', 'instance']); }); it("should detect if a doclet is readonly (and its kind is 'member')", function() { var tests = { 'asdf\n @readonly': 'readonly', 'asdf': false, '@name Fdsa#foo\n@readonly': 'readonly', // kind is not 'member'. '@const asdf\n@readonly': 'constant', '@function asdf\n@readonly': false, '@function Asdf#bar\n@readonly': false }; doTests(tests, 'readonly'); }); it("should detect if the doclet is a for constant", function() { var tests = { 'Enum. @enum\n@constant': 'constant', '@function Foo#BAR\n@const': 'constant', '@const Asdf': 'constant' }; doTests(tests, 'constant'); }); it("should detect multiple attributes", function() { var doc = new doclet.Doclet('/** @const module:fdsa~FOO\n@readonly\n@private */', {}); attribs = helper.getAttribs(doc); expect(attribs).toContain('private'); //expect(attribs).toContain('readonly'); // kind is 'constant' not 'member'. expect(attribs).toContain('constant'); expect(attribs).toContain('inner'); }); }); describe("getSignatureTypes", function() { // returns links to allowed types for a doclet. it("returns an empty array if the doclet has no specified type", function() { var doc = new doclet.Doclet('/** @const ASDF */', {}), types = helper.getSignatureTypes(doc); expect(Array.isArray(types)).toBe(true); expect(types.length).toBe(0); }); it("returns a string array of the doclet's types", function() { var doc = new doclet.Doclet('/** @const {number|Array.} ASDF */', {}), types = helper.getSignatureTypes(doc); expect(types.length).toBe(2); expect(types).toContain('number'); expect(types).toContain(helper.htmlsafe('Array.')); // should be HTML safe }); it("creates links for types if relevant", function() { // make some links. helper.longnameToUrl.MyClass = 'MyClass.html'; var doc = new doclet.Doclet('/** @const {MyClass} ASDF */', {}), types = helper.getSignatureTypes(doc); expect(types.length).toBe(1); expect(types).toContain('MyClass'); delete helper.longnameToUrl.MyClass; }); it("uses the cssClass parameter for links if it is provided", function() { // make some links. helper.longnameToUrl.MyClass = 'MyClass.html'; var doc = new doclet.Doclet('/** @const {MyClass} ASDF */', {}), types = helper.getSignatureTypes(doc, 'myCSSClass'); expect(types.length).toBe(1); expect(types).toContain('MyClass'); delete helper.longnameToUrl.MyClass; }); }); describe("getSignatureParams", function() { // retrieves parameter names. // if css class is provided, optional parameters are wrapped in a with that class. it("returns an empty array if the doclet has no specified type", function() { var doc = new doclet.Doclet('/** @function myFunction */', {}), params = helper.getSignatureParams(doc); expect(Array.isArray(params)).toBe(true); expect(params.length).toBe(0); }); it("returns a string array of the doclet's parameter names", function() { var doc = new doclet.Doclet('/** @function myFunction\n @param {string} foo - asdf. */', {}), params = helper.getSignatureParams(doc); expect(params.length).toBe(1); expect(params).toContain('foo'); }); it("wraps optional parameters in if optClass is provided", function() { var doc = new doclet.Doclet( '/** @function myFunction\n' + ' * @param {boolean} foo - explanation.\n' + ' * @param {number} [bar=1] - another explanation.\n' + ' * @param {string} [baz] - another explanation.\n' + ' */', {}), params = helper.getSignatureParams(doc, 'cssClass'); expect(params.length).toBe(3); expect(params).toContain('foo'); expect(params).toContain('bar'); expect(params).toContain('baz'); }); it("doesn't wrap optional parameters in if optClass is not provided", function() { var doc = new doclet.Doclet( '/** @function myFunction\n' + ' * @param {boolean} foo - explanation.\n' + ' * @param {number} [bar=1] - another explanation.\n' + ' * @param {string} [baz] - another explanation.\n' + ' */', {}), params = helper.getSignatureParams(doc); expect(params.length).toBe(3); expect(params).toContain('foo'); expect(params).toContain('bar'); expect(params).toContain('baz'); }); }); describe("getSignatureReturns", function() { // retrieves links to types that the member can return. it("returns a value with correctly escaped HTML", function() { var mockDoclet = { returns: [ { type: { names: [ 'Array.' ] } } ] }; var html = helper.getSignatureReturns(mockDoclet); expect(html).not.toContain('Array.'); expect(html).toContain('Array.<string>'); }); it("returns an empty array if the doclet has no returns", function() { var doc = new doclet.Doclet('/** @function myFunction */', {}), returns = helper.getSignatureReturns(doc); expect(Array.isArray(returns)).toBe(true); expect(returns.length).toBe(0); }); it("returns an empty array if the doclet has @returns but with no type", function() { var doc = new doclet.Doclet('/** @function myFunction\n@returns an interesting result.*/', {}), returns = helper.getSignatureReturns(doc); expect(Array.isArray(returns)).toBe(true); expect(returns.length).toBe(0); }); it("creates links for return types if relevant", function() { // make some links. helper.longnameToUrl.MyClass = 'MyClass.html'; var doc = new doclet.Doclet('/** @function myFunction\n@returns {number|MyClass} an interesting result.*/', {}), returns = helper.getSignatureReturns(doc); expect(returns.length).toBe(2); expect(returns).toContain('MyClass'); expect(returns).toContain('number'); delete helper.longnameToUrl.MyClass; }); it("uses the cssClass parameter for links if it is provided", function() { // make some links. helper.longnameToUrl.MyClass = 'MyClass.html'; var doc = new doclet.Doclet('/** @function myFunction\n@returns {number|MyClass} an interesting result.*/', {}), returns = helper.getSignatureReturns(doc, 'myCssClass'); expect(returns.length).toBe(2); expect(returns).toContain('MyClass'); expect(returns).toContain('number'); delete helper.longnameToUrl.MyClass; }); }); describe("getAncestorLinks", function() { // make a hierarchy. var lackeys = new doclet.Doclet('/** @member lackeys\n@memberof module:mafia/gangs.Sharks~Henchman\n@instance*/', {}), henchman = new doclet.Doclet('/** @class Henchman\n@memberof module:mafia/gangs.Sharks\n@inner */', {}), gang = new doclet.Doclet('/** @namespace module:mafia/gangs.Sharks */', {}), mafia = new doclet.Doclet('/** @module mafia/gangs */', {}), data = taffy([lackeys, henchman, gang, mafia]); // register some links it("returns an empty array if there are no ancestors", function() { var links = helper.getAncestorLinks(data, mafia); expect(Array.isArray(links)).toBe(true); expect(links.length).toBe(0); }); it("returns an array of ancestor names (with preceding punctuation) if there are ancestors, the direct ancestor with following punctuation too", function() { var links = helper.getAncestorLinks(data, lackeys); expect(links.length).toBe(3); expect(links).toContain('~Henchman#'); expect(links).toContain('.Sharks'); expect(links).toContain('mafia/gangs'); links = helper.getAncestorLinks(data, henchman); expect(links.length).toBe(2); expect(links).toContain('.Sharks~'); expect(links).toContain('mafia/gangs'); links = helper.getAncestorLinks(data, gang); expect(links.length).toBe(1); expect(links).toContain('mafia/gangs.'); }); it("adds links if they exist", function() { // register some links helper.longnameToUrl['module:mafia/gangs'] = 'mafia_gangs.html'; helper.longnameToUrl['module:mafia/gangs.Sharks~Henchman'] = 'henchman.html'; var links = helper.getAncestorLinks(data, lackeys); expect(links.length).toBe(3); expect(links).toContain('~Henchman#'); expect(links).toContain('.Sharks'); expect(links).toContain('mafia/gangs'); delete helper.longnameToUrl['module:mafia/gangs']; delete helper.longnameToUrl['module:mafia/gangs.Sharks~Henchman']; }); it("adds cssClass to any link", function() { // register some links helper.longnameToUrl['module:mafia/gangs'] = 'mafia_gangs.html'; helper.longnameToUrl['module:mafia/gangs.Sharks~Henchman'] = 'henchman.html'; var links = helper.getAncestorLinks(data, lackeys, 'myClass'); expect(links.length).toBe(3); expect(links).toContain('~Henchman#'); expect(links).toContain('.Sharks'); expect(links).toContain('mafia/gangs'); delete helper.longnameToUrl['module:mafia/gangs']; delete helper.longnameToUrl['module:mafia/gangs.Sharks~Henchman']; }); }); describe("addEventListeners", function() { var doclets = ( taffy(doop(jasmine.getDocSetFromFile('test/fixtures/listenstag.js').doclets)) ), ev = helper.find(doclets, {longname: 'module:myModule.event:MyEvent'})[0], ev2 = helper.find(doclets, {longname: 'module:myModule~Events.event:Event2'})[0], ev3 = helper.find(doclets, {longname: 'module:myModule#event:Event3'})[0]; helper.addEventListeners(doclets); it("adds a 'listeners' array to events with the longnames of the listeners", function() { expect(Array.isArray(ev.listeners)).toBe(true); expect(Array.isArray(ev2.listeners)).toBe(true); expect(ev.listeners.length).toBe(2); expect(ev.listeners).toContain('module:myModule~MyHandler'); expect(ev.listeners).toContain('module:myModule~AnotherHandler'); expect(ev2.listeners.length).toBe(1); expect(ev2.listeners).toContain('module:myModule~MyHandler'); }); it("does not add listeners for events with no listeners", function() { expect(ev3.listeners).not.toBeDefined(); }); it("does not make spurious doclets if something @listens to a non-existent symbol", function() { expect(helper.find(doclets, {longname: 'event:fakeEvent'}).length).toBe(0); }); }); describe("prune", function() { var array = [ // keep {undocumented: false}, // keep {ignore: false}, // keep {memberof: 'SomeClass'}, // prune {undocumented: true}, // prune {ignore: true}, // prune {memberof: ''} ]; var arrayPrivate = [ // prune (unless env.opts.private is truthy) {access: 'private'} ]; var keep = array.slice(0, 3); it('should prune the correct members', function() { var pruned = helper.prune( taffy(array) )().get(); compareObjectArrays(keep, pruned); }); it('should prune private members if env.opts.private is falsy', function() { var priv = !!env.opts.private; env.opts.private = false; var pruned = helper.prune( taffy(arrayPrivate) )().get(); compareObjectArrays([], pruned); env.opts.private = !!priv; }); it('should not prune private members if env.opts.private is truthy', function() { var priv = !!env.opts.private; env.opts.private = true; var pruned = helper.prune( taffy(arrayPrivate) )().get(); compareObjectArrays(arrayPrivate, pruned); env.opts.private = !!priv; }); }); describe("registerLink", function() { it("adds an entry to exports.longnameToUrl", function() { helper.longnameToUrl.MySymbol = 'asdf.html'; expect(helper.longnameToUrl.MySymbol).toBeDefined(); expect(helper.longnameToUrl.MySymbol).toBe('asdf.html'); delete helper.longnameToUrl.MySymbol; }); it("allows linkto to work", function() { helper.registerLink('MySymbol', 'asdf.html'); expect(helper.linkto('MySymbol')).toBe('MySymbol'); delete helper.longnameToUrl.MySymbol; }); }); describe("tutorialToUrl", function() { function missingTutorial() { var url = helper.tutorialToUrl("be-a-perfect-person-in-just-three-days"); } beforeEach(function() { spyOn(logger, 'error'); helper.setTutorials(resolver.root); }); afterEach(function() { helper.setTutorials(null); }); it('logs an error if the tutorial is missing', function() { helper.tutorialToUrl('be-a-perfect-person-in-just-three-days'); expect(logger.error).toHaveBeenCalled(); }); it("logs an error if the tutorial's name is a reserved JS keyword and it doesn't exist", function() { helper.tutorialToUrl('prototype'); expect(logger.error).toHaveBeenCalled(); }); it("creates links to tutorials if they exist", function() { // load the tutorials we already have for the tutorials tests resolver.load(env.dirname + "/test/tutorials/tutorials"); resolver.resolve(); var url = helper.tutorialToUrl('test'); expect(typeof url).toBe('string'); expect(url).toBe('tutorial-test.html'); }); it("creates links for tutorials where the name is a reserved JS keyword", function() { var url = helper.tutorialToUrl('constructor'); expect(typeof url).toBe('string'); expect(url).toBe('tutorial-constructor.html'); }); it("returns the same link if called multiple times on the same tutorial", function() { expect(helper.tutorialToUrl('test2')).toBe(helper.tutorialToUrl('test2')); }); }); describe("toTutorial", function() { beforeEach(function () { spyOn(logger, 'error'); helper.setTutorials(resolver.root); }); afterEach(function() { helper.setTutorials(null); }); it('logs an error if the first param is missing', function() { helper.toTutorial(); expect(logger.error).toHaveBeenCalled(); }); // missing tutorials it("returns the tutorial name if it's missing and no missingOpts is provided", function() { helper.setTutorials(resolver.root); var link = helper.toTutorial('qwerty'); expect(link).toBe('qwerty'); }); it("returns the tutorial name wrapped in missingOpts.tag if provided and the tutorial is missing", function() { var link = helper.toTutorial('qwerty', 'lkjklqwerty', {tag: 'span'}); expect(link).toBe('qwerty'); }); it("returns the tutorial name wrapped in missingOpts.tag with class missingOpts.classname if provided and the tutorial is missing", function() { var link = helper.toTutorial('qwerty', 'lkjklqwerty', {classname: 'missing'}); expect(link).toBe('qwerty'); link = helper.toTutorial('qwerty', 'lkjklqwerty', {tag: 'span', classname: 'missing'}); expect(link).toBe('qwerty'); }); it("prefixes the tutorial name with missingOpts.prefix if provided and the tutorial is missing", function() { var link = helper.toTutorial('qwerty', 'lkjklqwerty', {tag: 'span', classname: 'missing', prefix: 'TODO-'}); expect(link).toBe('TODO-qwerty'); link = helper.toTutorial('qwerty', 'lkjklqwerty', {prefix: 'TODO-'}); expect(link).toBe('TODO-qwerty'); link = helper.toTutorial('qwerty', 'lkjklqwerty', {prefix: 'TODO-', classname: 'missing'}); expect(link).toBe('TODO-qwerty'); }); // now we do non-missing tutorials. it("returns a link to the tutorial if not missing", function() { // load the tutorials we already have for the tutorials tests resolver.load(env.dirname + "/test/tutorials/tutorials"); resolver.resolve(); var link = helper.toTutorial('constructor', 'The Constructor tutorial'); expect(link).toBe('The Constructor tutorial'); }); it("uses the tutorial's title for the link text if no content parameter is provided", function() { var link = helper.toTutorial('test'); expect(link).toBe('Test tutorial'); }); it("does not apply any of missingOpts if the tutorial was found", function() { var link = helper.toTutorial('test', '', {tag: 'span', classname: 'missing', prefix: 'TODO-'}); expect(link).toBe('Test tutorial'); }); }); // couple of convenience functions letting me set conf variables and restore // them back to the originals later. function setConfTemplatesVariables(hash) { var keys = Object.keys(hash); var storage = {}; for (var i = 0; i < keys.length; ++i) { storage[keys[i]] = env.conf.templates[keys[i]]; // works because hash[key] is a scalar not an array/object env.conf.templates[keys[i]] = hash[keys[i]]; } return storage; } function restoreConfTemplates(storage) { var keys = Object.keys(storage); for (var i = 0; i < keys.length; ++i) { env.conf.templates[keys[i]] = storage[keys[i]]; } } describe("resolveLinks", function() { it('should translate {@link test} into a HTML link.', function() { var input = 'This is a {@link test}.', output = helper.resolveLinks(input); expect(output).toBe('This is a test.'); }); it('should translate {@link unknown} into a simple text.', function() { var input = 'This is a {@link unknown}.', output = helper.resolveLinks(input); expect(output).toBe('This is a unknown.'); }); it('should translate {@link test} into a HTML links multiple times.', function() { var input = 'This is a {@link test} and {@link test}.', output = helper.resolveLinks(input); expect(output).toBe('This is a test and test.'); }); it('should translate [hello there]{@link test} into a HTML link with the custom content.', function() { var input = 'This is a [hello there]{@link test}.', output = helper.resolveLinks(input); expect(output).toBe('This is a hello there.'); }); it('should translate [dummy text] and [hello there]{@link test} into an HTML link with the custom content.', function() { var input = 'This is [dummy text] and [hello there]{@link test}.', output = helper.resolveLinks(input); expect(output).toBe('This is [dummy text] and hello there.'); }); it('should translate [dummy text] and [more] and [hello there]{@link test} into an HTML link with the custom content.', function() { var input = 'This is [dummy text] and [more] and [hello there]{@link test}.', output = helper.resolveLinks(input); expect(output).toBe('This is [dummy text] and [more] and hello there.'); }); it('should ignore [hello there].', function() { var input = 'This is a [hello there].', output = helper.resolveLinks(input); expect(output).toBe(input); }); it('should translate http links in the tag', function() { var input = 'Link to {@link http://github.com}', output = helper.resolveLinks(input); expect(output).toBe('Link to http://github.com'); }); it('should translate ftp links in the tag', function() { var input = 'Link to {@link ftp://foo.bar}', output = helper.resolveLinks(input); expect(output).toBe('Link to ftp://foo.bar'); }); it('should allow pipe to be used as delimiter between href and text (external link)', function() { var input = 'Link to {@link http://github.com|Github}', output = helper.resolveLinks(input); expect(output).toBe('Link to Github'); }); it('should allow pipe to be used as delimiter between href and text (symbol link)', function() { var input = 'Link to {@link test|Test}', output = helper.resolveLinks(input); expect(output).toBe('Link to Test'); }); it('should allow first space to be used as delimiter between href and text (external link)', function() { var input = 'Link to {@link http://github.com Github}', output = helper.resolveLinks(input); expect(output).toBe('Link to Github'); }); it('should allow first space to be used as delimiter between href and text (symbol link)', function() { var input = 'Link to {@link test My Caption}', output = helper.resolveLinks(input); expect(output).toBe('Link to My Caption'); }); it('if pipe and space are present in link tag, use pipe as the delimiter', function() { var input = 'Link to {@link test|My Caption}', output = helper.resolveLinks(input); expect(output).toBe('Link to My Caption'); }); it('Test of {@linkcode } which should be in monospace', function() { var input = 'Link to {@linkcode test}', output = helper.resolveLinks(input); expect(output).toBe('Link to test'); }); it('Test of {@linkplain } which should be in normal font', function() { var input = 'Link to {@linkplain test}', output = helper.resolveLinks(input); expect(output).toBe('Link to test'); }); it('should be careful with linking to links whose names are reserved JS keywords', function() { var input = 'Link to {@link constructor}', output = helper.resolveLinks(input); expect(output).toBe('Link to constructor'); }); it('should allow linebreaks between link tag and content', function() { var input = 'This is a {@link\ntest}.', output = helper.resolveLinks(input); expect(output).toBe('This is a test.'); }); it('should allow linebreaks to separate url from link text', function() { var input = 'This is a {@link\ntest\ntest}.', output = helper.resolveLinks(input); expect(output).toBe('This is a test.'); }); it('should normalize additional newlines to spaces', function() { var input = 'This is a {@link\ntest\ntest\n\ntest}.', output = helper.resolveLinks(input); expect(output).toBe('This is a test test.'); }); it('should allow tabs between link tag and content', function() { var input = 'This is a {@link\ttest}.', output = helper.resolveLinks(input); expect(output).toBe('This is a test.'); }); // conf.monospaceLinks. check that // a) it works it('if conf.monospaceLinks is true, all {@link} should be monospace', function () { var storage = setConfTemplatesVariables({monospaceLinks: true}); var input = 'Link to {@link test}', output = helper.resolveLinks(input); expect(output).toBe('Link to test'); restoreConfTemplates(storage); }); // b) linkcode and linkplain are still respected it('if conf.monospaceLinks is true, all {@linkcode} should still be monospace', function () { var storage = setConfTemplatesVariables({monospaceLinks: true}); var input = 'Link to {@linkcode test}', output = helper.resolveLinks(input); expect(output).toBe('Link to test'); restoreConfTemplates(storage); }); it('if conf.monospaceLinks is true, all {@linkplain} should still be plain', function () { var storage = setConfTemplatesVariables({monospaceLinks: true}); var input = 'Link to {@linkplain test}', output = helper.resolveLinks(input); expect(output).toBe('Link to test'); restoreConfTemplates(storage); }); // conf.cleverLinks. check that // a) it works it('if conf.cleverLinks is true, {@link symbol} should be in monospace', function () { var storage = setConfTemplatesVariables({cleverLinks: true}); var input = 'Link to {@link test}', output = helper.resolveLinks(input); expect(output).toBe('Link to test'); restoreConfTemplates(storage); }); it('if conf.cleverLinks is true, {@link URL} should be in plain text', function () { var storage = setConfTemplatesVariables({cleverLinks: true}); var input = 'Link to {@link http://github.com}', output = helper.resolveLinks(input); expect(output).toBe('Link to http://github.com'); restoreConfTemplates(storage); }); // b) linkcode and linkplain are still respected it('if conf.cleverLinks is true, all {@linkcode} should still be clever', function () { var storage = setConfTemplatesVariables({cleverLinks: true}); var input = 'Link to {@linkcode test}', output = helper.resolveLinks(input); expect(output).toBe('Link to test'); restoreConfTemplates(storage); }); it('if conf.cleverLinks is true, all {@linkplain} should still be plain', function () { var storage = setConfTemplatesVariables({cleverLinks: true}); var input = 'Link to {@linkplain test}', output = helper.resolveLinks(input); expect(output).toBe('Link to test'); restoreConfTemplates(storage); }); // c) if monospaceLinks is additionally `true` it is ignored in favour // of cleverLinks it('if conf.cleverLinks is true and so is conf.monospaceLinks, cleverLinks overrides', function () { var storage = setConfTemplatesVariables({cleverLinks: true, monospaceLinks: true}); var input = 'Link to {@link test} and {@link http://github.com}', output = helper.resolveLinks(input); expect(output).toBe('Link to test and http://github.com'); restoreConfTemplates(storage); }); }); describe("createLink", function() { it('should create a url for a simple global.', function() { var mockDoclet = { kind: 'function', longname: 'foo', name: 'foo' }, url = helper.createLink(mockDoclet); expect(url).toBe('global.html#foo'); }); it('should create a url for a namespace.', function() { var mockDoclet = { kind: 'namespace', longname: 'foo', name: 'foo' }, url = helper.createLink(mockDoclet); expect(url).toBe('foo.html'); }); it('should create a url for a member of a namespace.', function() { var mockDoclet = { kind: 'function', longname: 'ns.foo', name: 'foo', memberof: 'ns' }, url = helper.createLink(mockDoclet); expect(url).toBe('ns.html#foo'); }); var nestedNamespaceDoclet = { kind: 'function', longname: 'ns1.ns2.foo', name: 'foo', memberof: 'ns1.ns2' }; var nestedNamespaceUrl; it('should create a url for a member of a nested namespace.', function() { nestedNamespaceUrl = helper.createLink(nestedNamespaceDoclet); expect(nestedNamespaceUrl).toBe('ns1.ns2.html#foo'); }); it('should return the same value when called twice with the same doclet.', function() { var newUrl = helper.createLink(nestedNamespaceDoclet); expect(newUrl).toBe(nestedNamespaceUrl); }); it('should create a url for a name with invalid characters.', function() { var mockDoclet = { kind: 'function', longname: 'ns1."!"."*foo"', name: '"*foo"', memberof: 'ns1."!"' }, url = helper.createLink(mockDoclet); expect(url).toEqual('_.html#"*foo"'); }); it('should create a url for a function that is the only symbol exported by a module.', function() { var mockDoclet = { kind: 'function', longname: 'module:bar', name: 'module:bar' }; var url = helper.createLink(mockDoclet); expect(url).toEqual('module-bar.html'); }); it('should create a url for a doclet with the wrong kind (caused by incorrect JSDoc tags', function() { var moduleDoclet = { kind: 'module', longname: 'module:baz', name: 'module:baz' }; var badDoclet = { kind: 'member', longname: 'module:baz', name: 'module:baz' }; var moduleDocletUrl = helper.createLink(moduleDoclet); var badDocletUrl = helper.createLink(badDoclet); expect(moduleDocletUrl).toBe('module-baz.html'); expect(badDocletUrl).toBe('module-baz.html'); }); it('should create a url for a function that is a member of a doclet with the wrong kind', function() { var badModuleDoclet = { kind: 'member', longname: 'module:qux', name: 'module:qux' }; var memberDoclet = { kind: 'function', name: 'frozzle', memberof: 'module:qux', scope: 'instance', longname: 'module:qux#frozzle' }; var badModuleDocletUrl = helper.createLink(badModuleDoclet); var memberDocletUrl = helper.createLink(memberDoclet); expect(badModuleDocletUrl).toBe('module-qux.html'); expect(memberDocletUrl).toBe('module-qux.html#frozzle'); }); it('should create a url for an empty package definition', function() { var packageDoclet = { kind: 'package', name: undefined, longname: 'package:undefined' }; var packageDocletUrl = helper.createLink(packageDoclet); expect(packageDocletUrl).toBe('global.html'); }); }); describe("resolveAuthorLinks", function() { // convert Jane Doe to a mailto link. it('should convert email addresses in angle brackets *after* a name to mailto links', function() { var str = ' John Doe ', out = helper.resolveAuthorLinks(str); expect(out).toBe('John Doe'); }); it('should HTML-safe author names', function() { var str = ' John ', out = helper.resolveAuthorLinks(str); expect(out).toBe('' + helper.htmlsafe('John'); }); it('should simply return the input string, HTML-safe, if no email is detected', function() { var str = 'John Doe ', out = helper.resolveAuthorLinks(str); expect(out).toBe(helper.htmlsafe(str)); }); }); });