UNPKG

short-jsdoc

Version:

short and simple jsdoc Object Oriented syntax format and implementation

1,339 lines (1,011 loc) 39.9 kB
var JsDocMaker = typeof _ === 'undefined' ? require('../../src/jsdocmaker/main.js') : JsDocMaker; var _ = typeof _ === 'undefined' ? require('underscore') : _; describe("JsDocMaker", function() { describe("Basic jsdoc parser", function() { var jsdoc, maker, Apple, Lion, Lemon, MyModel; beforeEach(function() { var code = '//@class Lemon'+'\n'+ '//this is a no module class'+'\n'+ '//@constructor the Lemon public constructor signature @param {Color} color'+'\n'+ '//@constructor another constructor for the Lemon class @param {Number} size'+'\n'+ '//?@method tricky this comment should be ignored b the parser because it starts with the special prefix ?'+'\n'+ '//@method glow'+'\n'+ '//@module livingThings'+'\n'+ '//@class Apple @extend Fruit '+'\n'+ '/*@method beEatenBy apples have this privilege @param {Mouth} mouth the mouth to be used @param {Int} amount @return {String} the bla*/' + '\n' + '//@property {Color} color the main color of this fruit'+'\n'+ '//@class Lion the lion class is on living things'+'\n'+ '//@event angry triggered when the lion gets angry'+'\n'+ '//@module model @class MyModel @extends Framework.Model'+'\n'+ '//@attribute {String} name this is the name of the model'+'\n'+ '//@attribute {Apple} apple this is the apple fruit'+'\n'+ ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("init", function() { Apple = jsdoc.classes['livingThings.Apple']; expect(Apple).toBeDefined(); Lion = jsdoc.classes['livingThings.Lion']; expect(Lion).toBeDefined(); Lemon = jsdoc.classes['__DefaultModule.Lemon']; expect(Lemon).toBeDefined(); MyModel = jsdoc.classes['model.MyModel']; expect(MyModel).toBeDefined(); }); it("classes and modules", function() { expect(Lemon.extends.name).toBe('Object'); expect(jsdoc.modules.livingThings).toBeDefined(); expect(Apple.module.name).toBe('livingThings'); expect(Apple.absoluteName).toBe('livingThings.Apple'); //extends is not binded because we never declared the parent class expect(Apple.extends.name).toBe('Fruit'); expect(Apple.extends.error).toBe('NAME_NOT_FOUND'); expect(Lion.module.name).toBe('livingThings'); }); it("methods", function() { expect(Apple.methods.beEatenBy).toBeDefined(); expect(Apple.methods.beEatenBy.name).toBe('beEatenBy'); expect(Apple.methods.beEatenBy.absoluteName).toBe('livingThings.Apple.beEatenBy'); }); it("properties", function() { expect(Apple.properties.color.name).toBe('color'); expect(Apple.properties.color.absoluteName).toBe('livingThings.Apple.color'); expect(Apple.properties.color.type.name).toBe('Color'); expect(Apple.properties.color.text).toBe('the main color of this fruit'); }); it("events", function() { expect(Lion.events.angry.name).toBe('angry'); expect(Lion.events.angry.absoluteName).toBe('livingThings.Lion.angry'); expect(Lion.events.angry.text).toBe('triggered when the lion gets angry'); }); it("attributes", function() { expect(MyModel.attributes.name.name).toBe('name'); expect(MyModel.attributes.name.absoluteName).toBe('model.MyModel.name'); expect(MyModel.attributes.name.text).toBe('this is the name of the model'); expect(MyModel.attributes.name.type.name).toBe('String'); expect(MyModel.attributes.apple.type.name).toBe('Apple'); expect(MyModel.attributes.apple.type.absoluteName).toBe('livingThings.Apple'); }); it("constructors", function() { expect(Lemon.constructors.length).toBe(2); expect(Lemon.constructors[0].name).toBe('0'); expect(Lemon.constructors[0].absoluteName.indexOf('Lemon.0')>0).toBe(true); expect(Lemon.constructors[0].text).toBe('the Lemon public constructor signature'); expect(Lemon.constructors[0].params[0].name).toBe('color'); expect(Lemon.constructors[0].params[0].type.name).toBe('Color'); expect(Lemon.constructors[1].text).toBe('another constructor for the Lemon class'); /*'//@constructor the Lemon public constructor signature @param {Color} color'+'\n'+ '//@constructor another constructor for the Lemon class @param {Number} size'+'\n'+*/ }); it("should ignore comments starting with '?' character", function() { expect(Lemon.methods.glow.name).toBe('glow'); expect(Lemon.methods.tricky).toBe(undefined); }); it("method's params", function() { expect(Apple.methods.beEatenBy.params.length).toBe(2); expect(Apple.methods.beEatenBy.params[0].name).toBe('mouth'); expect(Apple.methods.beEatenBy.params[0].type.name).toBe('Mouth'); expect(Apple.methods.beEatenBy.params[0].type.error).toBe('NAME_NOT_FOUND'); //because it was never declared expect(Apple.methods.beEatenBy.params[0].text).toBe('the mouth to be used'); expect(Apple.methods.beEatenBy.params[1].name).toBe('amount'); expect(Apple.methods.beEatenBy.params[1].type.name).toBe('Int'); }); it("method's return", function() { expect(Apple.methods.beEatenBy.returns.type.name).toBe('String'); expect(Apple.methods.beEatenBy.returns.text).toBe('the bla'); }); }); describe("@interface and @implements", function() { it('@implement and @class', function() { var code = '//@module interfacetest @class Interface1 this is interface 1 @method superInter @return {Number} '+'\n'+ '//@class Interface2 @extends Interface1 '+'\n'+ '//@class Interface3 '+'\n'+ '/*@method inter2Method foo bar and apples have this privilege @param {Mouth} mouth the mouth to be used @param {Int} amount @return {String} the bla*/' + '\n' + '//@class SomeConcrete2 hello this is a concrete class implementing multiple interfaces \n'+ '//@implements Interface1 @implements Interface3\n'+ ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; var SomeConcrete2 = jsdoc.classes['interfacetest.SomeConcrete2']; expect(SomeConcrete2.implements.length).toBe(2); expect(_.find(SomeConcrete2.implements, function(i){return i.absoluteName==='interfacetest.Interface1'}).methods.superInter.returns.type.name).toBe('Number'); }); it('@implement and @interface', function() { var code = // '// @alias annotation interface class\n' + '/* @alias annotation interface class \n */' + '//@module interfacetest2\n'+ '//@interface Interface1 this is interface 1 @method superInter @return {Number} '+'\n'+ '//@interface Interface2 @extends Interface1 '+'\n'+ '//@interface Interface3 '+'\n'+ '/*@method inter2Method foo bar and apples have this privilege @param {Mouth} mouth the mouth to be used \n'+ '@param {Int} amount @return {String} the bla*/' + '\n' + '//@class SomeConcrete2 hello this is a concrete class implementing multiple interfaces \n'+ '//@implements Interface1 @implements Interface3\n'+ ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; var SomeConcrete2 = jsdoc.classes['interfacetest2.SomeConcrete2'] // console.log(jsdoc, SomeConcrete2) expect(SomeConcrete2.implements.length).toBe(2); expect(_.find(SomeConcrete2.implements, function(i){return i.absoluteName==='interfacetest2.Interface1'}).methods.superInter.returns.type.name).toBe('Number'); }); }) describe("type binding & generics", function() { var jsdoc, maker; beforeEach(function() { var code = '//@module livingThings2' + '\n' + '//@class Animal ' + '\n' + '//@method run @param {int} amount in kilometers @final @static' + '\n' + '//@class Monkey @extend Animal @module livingThings2' + '\n' + '//@method eat way of feeding' + '\n' + '//@param {Int} amount' + '\n' + '//@param {Food} food what is eaten' + '\n' + '//@return {Energy} the total energy generated afte rthe proccess' + '\n' + ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("should be able to parse some javascript code", function() { expect(jsdoc.modules.livingThings2).toBeDefined(); var Monkey = jsdoc.classes['livingThings2.Monkey']; expect(Monkey).toBeDefined(); expect(Monkey.absoluteName).toBe('livingThings2.Monkey'); expect(Monkey.name).toBe('Monkey'); expect(Monkey.extends.name).toBe('Animal'); expect(Monkey.extends.absoluteName).toBe('livingThings2.Animal'); expect(Monkey.extends.name).toBe('Animal'); expect(Monkey.extends.methods.run.params.length).toBe(1); expect(_(Monkey.extends.methods.run.modifiers).contains('final')).toBe(true); expect(_(Monkey.extends.methods.run.modifiers).contains('static')).toBe(true); }); }); it("class extends referring later bug", function() { var jsdoc, maker; var code = '/*' + '\n' + '@module a @class A @class B sl kdfjslkd f @class C @class B @extends A' + '\n' + '*/ ' + '\n' + ''; maker = new JsDocMaker(); maker.addFile(code, 'name.js'); jsdoc = maker.jsdoc(); maker.postProccess(); maker.postProccessBinding(); expect(jsdoc.classes['a.B'].extends.absoluteName).toBe('a.A'); }); describe("custom native types", function() { var jsdoc, maker; beforeEach(function() { var code = '//@module office' + '\n' + '//@class Machine' + '\n' + '//TODO some text documenting the machine here please' + '\n' + '//@method calculate @param {Object<String,Array<HomeFinance>>} finances' + '\n' + '//@property {Bag<Eye>} eye' + '\n' + '//@class Eye a reutilizable eye' + '\n' + ''; maker = new JsDocMaker(); //before parsing we register the custom native types Bag and HomeFinance. Just give an url. _(maker.customNativeTypes).extend({ Bag: 'http://mylang.com/api/Bag' , HomeFinance: 'http://mylang.com/api/HomeFinance' }); maker.parseFile(code, 'genericstest1'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("custom natives should be binded", function() { var Machine = jsdoc.classes['office.Machine']; expect(Machine.text).toBe('TODO some text documenting the machine here please'); var param1 = Machine.methods.calculate.params[0].type.params[1].params[0]; expect(param1.name).toBe('HomeFinance'); expect(param1.nativeTypeUrl).toBe('http://mylang.com/api/HomeFinance'); var param2 = Machine.properties.eye.type; expect(param2.name).toBe('Bag'); expect(param2.nativeTypeUrl).toBe('http://mylang.com/api/Bag'); }); }); describe("Block comments shouln't remove the indentation chars", function() { var jsdoc, maker, Apple, Lion, Lemon; beforeEach(function() { var code = '/*'+'\n'+ '@class Apple'+'\n'+ 'This is a comment that includes'+'\n'+ 'new lines and'+'\n'+ ' some'+'\n'+ ' indentation'+'\n'+ ''+'\n'+ ''+'\n'+ ' var a = {a:1};'+'\n'+ '@extends Vegetable'+'\n'+ '*/'+'\n'+ '/*@method beEatenBy apples have this privilege @param {Mouth} mouth the mouth to be used @param {Int} amount @return {String} the bla*/' + '\n' + ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("init", function() { Apple = jsdoc.classes['__DefaultModule.Apple']; expect(Apple).toBeDefined(); expect(Apple.text.indexOf('\n\tsome')>0).toBe(true); expect(Apple.text.indexOf('\n\t\tindentation')>0).toBe(true); expect(Apple.text.indexOf('\n\n\tvar a = {a:1};')>0).toBe(true); }); }); describe("lineCommentSeparator configurable property", function() { var jsdoc, maker, C1; beforeEach(function() { var code = '//@module m1'+'\n'+ '//@class C1'+'\n'+ '//Some C1 class text'+'\n'+ '//Some other C1 class text'+'\n'+ ''; maker = new JsDocMaker(); maker.lineCommentSeparator = '888898888'; maker.parseFile(code, 'file1'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("user can replace the strings between adjacent Line Comments", function() { C1 = jsdoc.classes['m1.C1']; expect(C1.text).toBe("888898888 Some C1 class text 888898888 Some other C1 class text"); }); }); describe("module and class names can contain chars . and _", function() { var jsdoc, maker, C1; beforeEach(function() { var code = '//@module org.sgx.myprogram1'+'\n'+ '//some text for this module'+'\n'+ '//@class Program1'+'\n'+ '//Some Program1 class text'+'\n'+ '//@class Program1.Layout some other text'+'\n'+ '//@module other_module this module name is separated with _'+'\n'+ '//@class My_Program some other _ text @extends Program1.Layout'+'\n'+ ''; maker = new JsDocMaker(); maker.parseFile(code, 'file1'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it(". should be able to be used in class and module names", function() { var module = jsdoc.modules['org.sgx.myprogram1']; expect(module.name).toBe('org.sgx.myprogram1'); expect(module.text).toBe('some text for this module'); var Program1 = jsdoc.classes['org.sgx.myprogram1.Program1']; expect(Program1.name).toBe('Program1'); expect(Program1.text).toBe('Some Program1 class text'); var Program1Layout = jsdoc.classes['org.sgx.myprogram1.Program1.Layout']; expect(Program1Layout.name).toBe('Program1.Layout'); expect(Program1Layout.text).toBe('some other text'); }); it("_ should be able to be used in class and module names", function() { var other_module = jsdoc.modules.other_module; expect(other_module.name).toBe('other_module'); expect(other_module.text).toBe('this module name is separated with _'); var My_Program = jsdoc.classes['other_module.My_Program']; expect(My_Program.name).toBe('My_Program'); expect(My_Program.text).toBe('some other _ text'); expect(My_Program.extends.absoluteName).toBe('org.sgx.myprogram1.Program1.Layout'); }); }); describe("inherited methods and properties", function() { var jsdoc, maker; beforeEach(function() { var code = '//@module vehicles' + '\n' + '//@class Vehicle' + '\n' + '//@method move all vehicles move. Subclasses must override this. @param {Vector2D} direction @param {Nmber}' + '\n' + '//@property {Number} mass the mass of this Vehicle' + '\n' + '//@event hit triggered whenever this vehicle hits another object' + '\n' + '//@class Car @extends Vehicle' + '\n' + '//@method balance @param {String} eficiency' + '\n' + '//@class VMW @extends Car' + '\n' + '//@method deployAirbag' + '\n' + '//@class MotorBike @extends Vehicle' + '\n' + '//@method doTheWilly' + '\n' + ''; maker = new JsDocMaker(); maker.parseFile(code, 'genericstest1'); maker.postProccess(); maker.postProccessBinding(); maker.postProccessInherited(); // <-- important - explicitlyask the framework to calculate inherited methods&properties jsdoc = maker.data; }); it("inherited methods, properties and events. We have the method JsDocMaker.classOwnsProperty to know if a property is inherited", function() { var Vehicle = jsdoc.classes['vehicles.Vehicle']; expect(Vehicle.inherited.properties.mass).not.toBeDefined(); expect(Vehicle.events.hit.text).toBe('triggered whenever this vehicle hits another object'); expect(Vehicle.inherited.events.hit).not.toBeDefined(); var Car = jsdoc.classes['vehicles.Car']; expect(Car.inherited.methods.move.absoluteName).toBe('vehicles.Vehicle.move'); expect(Car.inherited.methods.move.ownerClass).toBe('vehicles.Vehicle'); expect(Car.inherited.methods.move.text).toBe('all vehicles move. Subclasses must override this.'); expect(Car.inherited.balance).not.toBeDefined(); expect(Car.inherited.properties.mass.type.name).toBe('Number'); expect(Car.inherited.properties.mass.text).toBe('the mass of this Vehicle'); var VMW = jsdoc.classes['vehicles.VMW']; expect(VMW.inherited.methods.move.absoluteName).toBe('vehicles.Vehicle.move'); expect(VMW.inherited.methods.move.ownerClass).toBe('vehicles.Vehicle'); expect(VMW.inherited.methods.move.text).toBe('all vehicles move. Subclasses must override this.'); expect(VMW.inherited.methods.balance.absoluteName).toBe('vehicles.Car.balance'); expect(VMW.inherited.methods.balance.params[0].type.name).toBe('String'); // expect(VMW.inherited.methods.balance.inheritedFrom.absoluteName).toBe('vehicles.Car'); expect(VMW.inherited.properties.mass.absoluteName).toBe('vehicles.Vehicle.mass'); expect(VMW.inherited.events.hit.text).toBe('triggered whenever this vehicle hits another object'); // expect(VMW.inherited.events.hit.inheritedFrom.absoluteName).toBe('vehicles.Vehicle'); }); it("We have the method JsDocMaker.classOwnsProperty to know if a property is inherited", function() { var VMW = jsdoc.classes['vehicles.VMW']; expect(JsDocMaker.classOwnsProperty(VMW, VMW.inherited.methods.move)).toBe(false); expect(JsDocMaker.classOwnsProperty(VMW, VMW.methods.deployAirbag)).toBe(true); }); }); describe("plugin utilities for recursing", function() { var jsdoc, maker; it("JsDocMaker.recurseAST visit nodes and types of the AST - children first", function() { var code = '//@module office '+ '\n' + '//@class Computer' + '\n' + '//@method putmusic @param {Object<String,Array<Song>>} songs' + '\n' + '//@method prepareMate @param {mate:Mate,termo:Termo,heat:Array<Source>} mateConfiguration' + '\n' + ''; maker = new JsDocMaker(); maker.parseFile(code); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; // we define a function to visit each AST node - we will search for a children @versionfoo and if any set as a property var output = []; var nodeVisitor = function(node) { output.push(node.name + '--'); }; var typeVisitor = function(type) { output.push(type.name + ','); }; maker.recurseAST(nodeVisitor, typeVisitor); var expected = 'Computer--putmusic--songs--String,Song,Array,Object,--prepareMate--mateConfiguration--Mate,Termo,Source,Array,Object,--Object--office--'; expect(output.join('')).toBe(expected); }); it("example 1: user can use recurseAST to install a visitor for doing its own post processing", function() { var code = '//@module office @versionfoo 3.2' + '\n' + '//@class Computer' + '\n' + '//you can do excel here' + '\n' + '//@versionfoo 1.2' + '\n' + '//@method putmusic @param {Object<String,Array<Song>>} songs' + '\n' + '//@versionfoo 1.0' + '\n' + ''; maker = new JsDocMaker(); maker.parseFile(code); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; // we define a function to visit each AST node - we will search for a children @versionfoo and if any set as a property var astVisitor = function(node) { var versionfoo = _(node.children||[]).find(function(child) { return child.annotation === 'versionfoo'; }); if(versionfoo && versionfoo.name) { node.versionfoo = versionfoo.name; } }; maker.recurseAST(astVisitor); expect(jsdoc.modules.office.versionfoo).toBe('3.2'); }); it("JsDocMaker.recurseType: recursing complex types children - for doing it ony in a single type object", function() { var code = '//@module mymodule bla bla ' + '\n' + '//@class C bla bla ' + '\n' + '//@property {S} p bla bla ' + '\n' + '//@property {Apple<String,Apple>} prop1 bla ' + '\n' + '//@property {a:String,b:Apple,c:Array<Apple<Object,Animal>>} prop2 bla ' + '\n' + '//@property {Array<String>|Apple|Object<String,Array<String>>} prop3 bla ' + '\n' + ''; maker = new JsDocMaker(); maker.addFile(code, 'name.js'); jsdoc = maker.jsdoc(); maker.postProccess(); maker.postProccessBinding(); var t1 = jsdoc.classes['mymodule.C'].properties.prop2.type; var t2 = jsdoc.classes['mymodule.C'].properties.prop3.type; var output = []; JsDocMaker.recurseType(t1, function(type) { output.push(type.name); }); expect(output.join(',')).toBe('String,Apple,Object,Animal,Apple,Array,Object'); output = []; JsDocMaker.recurseType(t2, function(type) { output.push(type.name); }); expect(output.join(',')).toBe('String,Array,Apple,String,String,Array,Object,'); }); }); describe("multiple types", function() { it("user can indicate multiple optional types by separating with |", function() { var code = '//@module cssutils' + '\n' + '//@class CSSExtractor' + '\n' + '//@method extract @param {String|HTMLElement|jQuery|Array<HTMLElement>|Object<String,Array<String>>} el @return {Object}' + '\n' + ''; var maker = new JsDocMaker(); maker.parseFile(code); maker.postProccess(); maker.postProccessBinding(); var jsdoc = maker.data; var type = jsdoc.classes['cssutils.CSSExtractor'].methods.extract.params[0].type; expect(_(type).isArray()).toBe(true); expect(type.length).toBe(5); expect(type[0].name).toBe('String'); expect(_(type[0].nativeTypeUrl).isString()).toBe(true); expect(type[1].name).toBe('HTMLElement'); expect(type[2].name).toBe('jQuery'); expect(type[3].name).toBe('Array'); expect(_(type[3].nativeTypeUrl).isString()).toBe(true); expect(type[3].params.length).toBe(1); expect(type[3].params[0].name).toBe('HTMLElement'); expect(type[4].name).toBe('Object'); expect(type[4].params.length).toBe(2); expect(type[4].params[0].name).toBe('String'); expect(type[4].params[1].name).toBe('Array'); expect(type[4].params[1].params[0].name).toBe('String'); }); }); describe("types with spaces", function() { it("types can contain spaces", function() { var code = '//@module stuff1' + '\n' + '//@class Something' + '\n' + '//@property {Object<String, Array<String > >} aProperty' + '\n' +''; var maker = new JsDocMaker(); maker.parseFile(code); maker.postProccess(); maker.postProccessBinding(); var jsdoc = maker.data; var type = jsdoc.classes['stuff1.Something'].properties.aProperty.type; expect(type.name).toBe('Object'); expect(type.params[0].name).toBe('String'); expect(type.params[1].name).toBe('Array'); expect(type.params[1].params[0].name).toBe('String'); }); }); // describe("support alternative comment block syntax", function() // { // describe("block commments with style", function() // { // it("/** style blocks", function() // { // var code = // '//@module stuff2' + '\n' + // '/**@class Sky\n * some text\n * and another line\n */' + '\n'; // var maker = new JsDocMaker(); // maker.parseFile(code); // maker.postProccess(); // maker.postProccessBinding(); // var jsdoc = maker.data; // var Sky = jsdoc.classes['stuff2.Sky']; // expect(Sky.text).toBe('some text\n and another line'); // }); // }); // }); describe("support comment preprocessor", function() { it("for example one can install a pcomment preprocessor for adding/removing fragment to comments", function() { var code = '//@module stuff3' + '\n' + '/**@class Vanilla some text @author sgx */' + '\n'; var maker = new JsDocMaker(); var plugin = { name: 'author replace my example' , execute: function(options) { options.node.value = options.node.value.replace(/@author\s+\w+/gi, '') + ' @author thief'; } }; maker.commentPreprocessorPlugins.add(plugin); //then do the parsing maker.parseFile(code); maker.postProccess(); maker.postProccessBinding(); var jsdoc = maker.data; var Vanilla = jsdoc.classes['stuff3.Vanilla']; var author = _(Vanilla.children).find(function(c){return c.annotation === 'author'; }); expect(author.name).toBe('thief'); }); }); describe("custom child annotation", function() { it("custom annotations will be parsed in 'children' property and can contain characters '.', '-', '_'", function() { var jsdoc, maker; var code = '//@module m1' + '\n' + '//@customAnnotation1 {Type} name text text' + '\n' + '//@custom-annotation2 {Type} name text text' + '\n' + '//@custom.Annotation3 {Type} name text text' + '\n' + '//@custom_Annotation4 {Type} name text text' + '\n' + '//' + '\n' + ''; maker = new JsDocMaker(); maker.addFile(code, 'name.js'); jsdoc = maker.jsdoc(); maker.postProccess(); maker.postProccessBinding(); // console.log(jsdoc.modules['m1'].children) expect(!!_.find(jsdoc.modules['m1'].children, function(c){return c.annotation==='customAnnotation1';})).toBe(true) expect(!!_.find(jsdoc.modules['m1'].children, function(c){return c.annotation==='custom-annotation2';})).toBe(true) expect(!!_.find(jsdoc.modules['m1'].children, function(c){return c.annotation==='custom-annotation2';})).toBe(true) expect(!!_.find(jsdoc.modules['m1'].children, function(c){return c.annotation==='custom_Annotation4';})).toBe(true) }); }); describe("support custom type parsers", function() { it("example1: we define the custom type syntax {#lemmon(prop1)} that returns a relevant type object", function() { var code = '//@module customTypeParsers' + '\n' + '/*@class Vanilla some text ' + '\n' + '@method method1' + '\n' + '@return {#lemmon(acid,lazy,green)} */' + '\n' + ''; var maker = new JsDocMaker(); // define and regiter a custom type syntax: var customTypeParser = { name: 'lemmon' , parse: function(s) { // variable s is the text body of the custom type for example 'acid,lazy,green'. // we return the following object as this type obejct implementation. return { name: 'Object' , lemmonProperties: s.split(',') }; } }; maker.registerTypeParser(customTypeParser); //then do the parsing maker.parseFile(code); maker.postProccess(); maker.postProccessBinding(); var jsdoc = maker.data; var Vanilla = jsdoc.classes['customTypeParsers.Vanilla']; var return1 = Vanilla.methods.method1.returns.type; expect(return1.lemmonProperties[0]).toBe('acid'); expect(return1.lemmonProperties[1]).toBe('lazy'); expect(return1.lemmonProperties[2]).toBe('green'); expect(return1.name).toBe('Object'); }); }); describe("support a literal object custom type implementation", function() { it("the type {#obj(prop1:Type1,...)} is supported out of the box", function() { var code = '//@module customTypeParsers2' + '\n' + '/*@class Vanilla2 some text ' + '\n' + '@method method1' + '\n' + '@return {#obj(prop:Type,prop2:Type2<Type3>)} some text*/' + '\n' + '//@method method2 blabla' + '\n' + '//@param {#obj(id:String,objectDic:Object<String>)} param1 some text' + '\n' + ''; var maker = new JsDocMaker(); //then do the parsing maker.parseFile(code); maker.postProccess(); maker.postProccessBinding(); var jsdoc = maker.data; var Vanilla = jsdoc.classes['customTypeParsers2.Vanilla2']; var returns = Vanilla.methods.method1.returns; expect(returns.type.name).toBe('Object'); expect(returns.type.properties.prop.name).toBe('Type'); expect(returns.type.properties.prop2.name).toBe('Type2'); expect(returns.type.properties.prop2.params[0].name).toBe('Type3'); var param1 = Vanilla.methods.method2.params[0]; expect(param1.type.name).toBe('Object'); expect(param1.type.properties.id.name).toBe('String'); expect(param1.type.properties.objectDic.name).toBe('Object'); }); }); describe("method throw exception", function() { var jsdoc, maker; beforeEach(function() { var code = '//@module throwtest1' + '\n' + '//@class CompilerException special exception for compiler errors @extends IOException ' + '\n' + // '//@param {Number} error_line @param {String} error_msg' + '\n' + '//@class IOException throwed when an IO error occurs @extends Error ' + '\n' + '//@class Thrower ' + '\n' + '//@method method1 @param {String} p some text' + '\n' + '//@param {Number} p1 sdf sdf' + '\n' + '//@throws {IOException} if a IO error occurs' + '\n' + '//@param {Number} p3' + '\n' + '//@throws {CompilerException} if a compiler error error occurs' + '\n' + '//@returns {SomeResult} or null in case of an error' + '\n' + ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("@throws nodes have a type and text", function() { var method1 = jsdoc.classes['throwtest1.Thrower'].methods.method1; expect(method1.throws[0].type.name).toBe('IOException'); expect(method1.throws[0].text).toBe('if a IO error occurs'); expect(method1.throws[0].type.text).toBe('throwed when an IO error occurs'); expect(method1.throws[0].type.extends.name).toBe('Error'); expect(method1.throws[1].type.name).toBe('CompilerException'); expect(method1.throws[1].text).toBe('if a compiler error error occurs'); expect(method1.throws[1].type.text).toBe('special exception for compiler errors'); expect(method1.throws[1].type.extends.name).toBe('IOException'); expect(method1.params[0].name).toBe('p'); expect(method1.params[1].name).toBe('p1'); expect(method1.params[2].name).toBe('p3'); expect(method1.returns.type.name).toBe('SomeResult'); }); }); describe("custom base class", function() { var jsdoc, maker; beforeEach(function() { var code = '//@module custombaseclass1' + '\n' + '//@class Test1 blabla ' + '\n' + '//@class Object yes I can go crazy and make Object just a concrete common name @extends Test1 ' + '\n' + ''; JsDocMaker.DEFAULT_CLASS = 'MyDefaultClass'; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("yes now Object is no longer the default-base class", function() { var Test1 = jsdoc.classes['custombaseclass1.Test1']; expect(Test1.extends.name).toBe('MyDefaultClass'); }); it("is just a concrete class", function() { var _Object = jsdoc.classes['custombaseclass1.Object']; expect(_Object.extends.name).toBe('Test1'); }); }); describe("several definitions", function() { var jsdoc, maker; beforeEach(function() { var code = '//@module mymodule the first text for mymodule' + '\n' + '//@class MyClass some text for myclass' + '\n' + '//@method m1 blabalbal @param p1 @param p2' + '\n' + '//@module othermodule' + '\n' + '//@class MyClass this text is from another class' + '\n' + '//@class Other class this text is from another class' + '\n' + '//@method m1 blabalbal @param p1 @param p2' + '\n' + '//@module mymodule a second text for mymodule' + '\n' + '//@class MyClass a second text for my class' + '\n' + ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("Two definitions of the same module or class should preserve all the texts", function() { var mymodule = jsdoc.modules.mymodule; expect(mymodule.text).toBe('the first text for mymodule' + JsDocMaker.MULTIPLE_TEXT_SEPARATOR + 'a second text for mymodule'); var MyClass = jsdoc.classes['mymodule.MyClass']; expect(MyClass.text).toBe('some text for myclass' + JsDocMaker.MULTIPLE_TEXT_SEPARATOR + 'a second text for my class'); }); }); describe("talking about the same class in different places", function() { var jsdoc, maker; beforeEach(function() { var code = '//@module mymodule' + '\n' + '//@class MyClass some text for myclass' + '\n' + 'var MyClass = function(){}' + '\n' + '//@method m1 blabalbal @param p1 something in the rain @param p2 smells nasty' + '\n' + 'MyClass.prototype.m1 = function(p1, p2){};' + '\n' + '//@class OtherClass some text for the other class' + '\n' + 'var MyClass = function(){}' + '\n' + '//@method m3 blabalbal @param a @param b' + '\n' + 'MyClass.prototype.m3 = function(a, b){};' + '\n' + '//@class MyClass' + '\n' + '//@method m2 bleblebel @param c @param d' + '\n' + 'MyClass.prototype.m2 = function(c, d){};' + '\n' + ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("be able to add more information to the same class later in the code", function() { var MyClass = jsdoc.classes['mymodule.MyClass']; expect(_(MyClass.methods).keys().length).toBe(2); expect(MyClass.methods.m1.text).toBe('blabalbal'); expect(MyClass.methods.m2.text).toBe('bleblebel'); expect(MyClass.methods.m1.params[0].name).toBe('p1'); expect(MyClass.methods.m1.params[0].text).toBe('something in the rain'); expect(MyClass.methods.m2.params[0].name).toBe('c'); expect(MyClass.methods.m2.params[1].name).toBe('d'); }); //TODO: the same should be for modules }); describe("referring classes with special names", function() { var jsdoc, maker; beforeEach(function() { var code = '//@module mymodule' + '\n' + '//@class Easy easy named class' + '\n' + '//@class MiniBike.Futuristic_2 a class using all allowed characters' + '\n' + 'var MiniBikeFuturistic_2 = function(){}' + '\n' + '//@class MiniBike.Futuristic_3 class extending a class with strange name @extends MiniBike.Futuristic_2' + '\n' + '//@method foo_bar @return MiniBike.Futuristic_2' + '\n' + '//@property {Array<MiniBike.Futuristic_3>} something' + '\n' + '//@property {Array<Easy>} easy' + '\n' + ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; }); it("classes with all accepted chars referred from complex objects", function() { var C1 = jsdoc.classes['mymodule.MiniBike.Futuristic_2']; var C2 = jsdoc.classes['mymodule.MiniBike.Futuristic_3']; var p1 = C2.properties.something; var p2 = C2.properties.easy; expect(C1.name).toBe('MiniBike.Futuristic_2'); expect(C1.absoluteName).toBe('mymodule.MiniBike.Futuristic_2'); expect(C2.extends.name).toBe('MiniBike.Futuristic_2'); expect(p1.type.params[0].name).toBe('MiniBike.Futuristic_3'); }); }); describe("parsing multiple files using addFile and jsdoc()", function() { var jsdoc, maker; beforeEach(function() { var files = { 'code1.js': '//@module mymodule' + '\n' + '//@class Easy easy named class' + '\n' + '//@method getState @returns {Car}' + '\n' + '' , '/opt/lamp/code2.js': '//@module mymodule2' + '\n' + '//@class Easy2 easy named class' + '\n' + '//@method getState2 @returns {Car2}' + '\n' + '' }; maker = new JsDocMaker(); _(files).each(function(value, name) { maker.addFile(value, name); }); jsdoc = maker.jsdoc(); }); it("parsed AST should contain references to file names and file location", function() { var m = jsdoc.classes['mymodule.Easy'].methods.getState; expect(jsdoc.classes['mymodule.Easy'].fileName).toBe('code1.js'); expect(jsdoc.classes['mymodule2.Easy2'].methods.getState2.fileName).toBe('/opt/lamp/code2.js'); //wecaan access all the information about a file var f2 = jsdoc.files[jsdoc.classes['mymodule2.Easy2'].methods.getState2.fileName]; expect(f2.fileName).toBe('/opt/lamp/code2.js'); //TODO: test if we can get correct source location. }); }); describe("object literal notation", function() { var jsdoc, maker; beforeEach(function() { var code = '//@module mymodule' + '\n' + '//@class Easy easy named class' + '\n' + '//@method getState @returns {sname:String,soptions:EasyConfiguration,complex1:Array<Object>}' + '\n' + '//@property {name:String,options:EasyConfiguration} p ' + '\n' + ''; maker = new JsDocMaker(); maker.addFile(code, 'name.js'); jsdoc = maker.jsdoc(); maker.postProccess(); maker.postProccessBinding(); }); it("any type support the syntax {a:A,b:B} with recursive evaluation", function() { var t1 = jsdoc.classes['mymodule.Easy'].properties.p.type; expect(t1.name).toBe('Object'); expect(t1.properties.name.name).toBe('String'); expect(t1.properties.options.name).toBe('EasyConfiguration'); var t2 = jsdoc.classes['mymodule.Easy'].methods.getState.returns.type; expect(t2.properties.complex1.name).toBe('Array'); expect(t2.properties.complex1.params[0].name).toBe('Object'); }); }); describe("@function", function() { it("a module can contain functions", function() { var jsdoc, maker; var code = '/*' + '\n' + '@module a' + '\n' + '@function f1 @param {C} f11 @return {C} f1r' + '\n' + '@class A @method m sl @param mp1 @return {mr1}' + '\n' + '@function f2 @param f21 @return f2r' + '\n' + '@throws {A} if a IO error occurs' + '\n' + '@class C @class B @extends A @method bm1 @param bmp1' + '\n' + '@function f3 @param f31 @return f3r' + '\n' + '*/ ' + '\n' + ''; maker = new JsDocMaker(); maker.addFile(code, 'name.js'); jsdoc = maker.jsdoc(); maker.postProccess(); maker.postProccessBinding(); expect(jsdoc.modules['a'].functions.length).toBe(3) var f1 = _.find(jsdoc.modules['a'].functions, function(f){return f.name==='f1'}); expect(f1.absoluteName === 'a.f1').toBe(true); expect(f1.params[0].type.absoluteName === 'a.C').toBe(true); expect(f1.returns.type.absoluteName === 'a.C').toBe(true); var f2 = _.find(jsdoc.modules['a'].functions, function(f){return f.name==='f2'}); expect(f2.throws[0].type.absoluteName === 'a.A').toBe(true) // console.log(f2.throws) }); it('can bind @function as types', function() { var code = '//@module functionAsTypesModule\n'+ '//@function F1 @param {Array<Number>} sortNumbers @return {Array<Number>}\n'+ '//@class C1\n'+ '//@method m1 @param {F1} fn @param {Boolean} opacity\n'+ ''; maker = new JsDocMaker(); maker.parseFile(code, 'textarea'); maker.postProccess(); maker.postProccessBinding(); jsdoc = maker.data; var paramType = jsdoc.classes['functionAsTypesModule.C1'].methods.m1.params[0].type expect(paramType.absoluteName).toBe('functionAsTypesModule.F1') expect(paramType.annotation).toBe('function') }); }); describe("line comments and markdown", function() { it("line comments must support multiple markdown paragraph by default", function() { var jsdoc, maker; var code = '//@module m1' + '\n' + '//#title 1' + '\n' + '//This is a paragraph 1l slkdjf lskdjflkdf' + '\n' + '//paragraph 1 still continue lastwordofpara1' + '\n' + '//' + '\n' + '//firstwordofpara2 this is pargaraph 2 lskdjf lsk flk sjldf' + '\n' + '//still paragraph 2lsdkjf lksjdlf skjldf' + '\n' + ''; maker = new JsDocMaker(); maker.addFile(code, 'name.js'); jsdoc = maker.jsdoc(); maker.postProccess(); maker.postProccessBinding(); expect(!!jsdoc.modules['m1'].text.match(/lastwordofpara1 \n \n firstwordofpara2/)).toBe(true) }); }); describe("annotations names", function() { it("can contain : and - characters", function() { var jsdoc, maker; maker = new JsDocMaker(); maker.addFile( '//@module mod:first-one some text 1' + '\n' + '//@class some:thi-ng some text 2' + '\n' + '//@event after:append-viewshared1 some text 3' + '\n' + '//@childtag child:custom-tag some text 4' + '\n' + '' ) jsdoc = maker.jsdoc(); maker.postProccess(); maker.postProccessBinding(); expect(jsdoc.classes['mod:first-one.some:thi-ng'].text.indexOf('some text 2')!==-1).toBe(true); expect(jsdoc.classes['mod:first-one.some:thi-ng'].events['after:append-viewshared1'].text.indexOf('some text 3')!==-1).toBe(true); expect(jsdoc.classes['mod:first-one.some:thi-ng'].events['after:append-viewshared1'].children[0].name==='child:custom-tag').toBe(true); }); }); });