diff --git a/Gruntfile.js b/Gruntfile.js index aff6c35..867415b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,10 +2,24 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-conventional-changelog'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-jasmine-node'); grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), - changelog: { options: { dest: 'CHANGELOG.md' } } + changelog: { options: { dest: 'CHANGELOG.md' } }, + jasmine_node: { + forceexit: true, + captureExceptions: true + }, + watch: { + parser: { + files: ['src/*.js', 'spec/*Spec.js'], + tasks: ['jasmine_node'] + } + } }); + grunt.registerTask('test', 'Run tests for parser code', ['jasmine_node']); + }; diff --git a/package.json b/package.json index 542b2eb..75b78e9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ }, "devDependencies": { "grunt": "~0.4.1", - "grunt-conventional-changelog": "~0.1.0" + "grunt-conventional-changelog": "~0.1.0", + "grunt-contrib-watch": "~0.5.0", + "grunt-jasmine-node": "~0.1.0", + "jasmine-node": "~1.12.0" } } diff --git a/spec/domSpec.js b/spec/domSpec.js new file mode 100644 index 0000000..d10db9d --- /dev/null +++ b/spec/domSpec.js @@ -0,0 +1,110 @@ +var DOM = require('../src/dom.js').DOM; +var normalizeHeaderToId = require('../src/dom.js').normalizeHeaderToId; + +describe('dom', function() { + var dom; + + beforeEach(function() { + dom = new DOM(); + }); + + describe('html', function() { + it('should add ids to all h tags', function() { + dom.html('
short
\nmed
\nlong\nline
\nfn with optional arguments
\ntext bold.
\n, but escape the html escape the content', function() { + expect(new Doc().markdown('bah x\n\n\nangular.k\n\n asdf x')). + toEqual( + ''); + }); + + it('should wrap everything inside a container tag', function() { + var doc = new Doc('@name superman').parse(); + var content = doc.markdown('hello'); + + expect(content).toMatch('bah x\n' + + '
\n' + + '<b>angular</b>.k\n' + + '\n' + + ' asdf x\n'); + }); + + it('should use the content before a colon as the name prefix for the className of the tag container', function() { + var doc = new Doc('@name super: man').parse(); + var content = doc.markdown('hello'); + + expect(content).toMatch('hello
\n'); + }); + + it('should replace text between two tags', function() { + expect(new Doc().markdown('hello
\nx\n# One\nb')). + toMatch('
/); + }); + + it('should ignore nested doc widgets', function() { + expect(new Doc().markdown( + 'before\n\n' + + '')).toEqual( + + '' + + '\ngit bla bla\n\n' + + ''); + }); + + it('should unindent text before processing based on the second line', function() { + expect(new Doc().markdown('first line\n' + + ' second line\n\n' + + ' third line\n' + + ' fourth line\n\n' + + ' fifth line')). + toMatch('before
\n\n' + + '\n' + + 'git bla bla\n' + + '\n' + + 'first line\n' + + 'second line
\n' + + '\n' + + 'third line\n' + + ' fourth line
fifth line
\n'); + }); + + it('should unindent text before processing based on the first line', function() { + expect(new Doc().markdown(' first line\n\n' + + ' second line\n' + + ' third line\n' + + ' fourth line\n\n' + + ' fifth line')). + toMatch(''); + }); + + + describe('inline annotations', function() { + it('should convert inline docs annotations into proper HTML', function() { + expect(new Doc().markdown( + "first line
\n' + + '\n' + + 'second line\n' + + 'third line\n' + + ' fourth line
fifth line
\n\n//!annotate supertext\n" + ) + ).toContain('data-popover data-content="supertext"') + }); + + it('should allow for a custom regular expression for matching', function() { + expect(new Doc().markdown( + "
\n\n//!annotate=\"soon\" supertext\n" + ) + ).toContain('data-popover data-content="supertext" data-title="Info">soon') + }); + + it('should allow for a custom title to be set', function() { + expect(new Doc().markdown( + "soon
\n\n//!annotate=\"soon\" coming soon|supertext\n" + ) + ).toContain('data-popover data-content="supertext" data-title="coming soon">soon') + }); + }); + }); + + describe('trim', function() { + var trim = ngdoc.trim; + it('should remove leading/trailing space', function() { + expect(trim(' \nabc\n ')).toEqual('abc'); + }); + + it('should remove leading space on every line', function() { + expect(trim('\n 1\n 2\n 3\n')).toEqual('1\n 2\n 3'); + }); + }); + + describe('merge', function() { + it('should merge child with parent', function() { + var parent = new Doc({id: 'ng.abc', name: 'ng.abc', section: 'api'}); + var methodA = new Doc({name: 'methodA', methodOf: 'ng.abc'}); + var methodB = new Doc({name: 'methodB', methodOf: 'ng.abc'}); + var propA = new Doc({name: 'propA', propertyOf: 'ng.abc'}); + var propB = new Doc({name: 'propB', propertyOf: 'ng.abc'}); + var eventA = new Doc({name: 'eventA', eventOf: 'ng.abc'}); + var eventB = new Doc({name: 'eventB', eventOf: 'ng.abc'}); + var docs = [methodB, methodA, eventB, eventA, propA, propB, parent]; // keep wrong order; + ngdoc.merge(docs); + expect(docs.length).toEqual(1); + expect(docs[0].id).toEqual('ng.abc'); + expect(docs[0].methods).toEqual([methodA, methodB]); + expect(docs[0].events).toEqual([eventA, eventB]); + expect(docs[0].properties).toEqual([propA, propB]); + }); + }); + + + describe('checkBrokenLinks', function() { + var docs; + + beforeEach(function() { + spyOn(console, 'log'); + docs = [new Doc({section: 'api', id: 'fake.id1', anchors: ['one']}), + new Doc({section: 'api', id: 'fake.id2'}), + new Doc({section: 'api', id: 'fake.id3'})]; + }); + + it('should log warning when a linked page does not exist', function() { + docs.push(new Doc({section: 'api', id: 'with-broken.link', links: ['non-existing-link']})) + ngdoc.checkBrokenLinks(docs); + expect(console.log).toHaveBeenCalled(); + var warningMsg = console.log.argsForCall[0][0] + expect(warningMsg).toContain('WARNING:'); + expect(warningMsg).toContain('non-existing-link'); + expect(warningMsg).toContain('api/with-broken.link'); + }); + + it('should log warning when a linked anchor does not exist', function() { + docs.push(new Doc({section: 'api', id: 'with-broken.link', links: ['api/fake.id1#non-existing']})) + ngdoc.checkBrokenLinks(docs); + expect(console.log).toHaveBeenCalled(); + var warningMsg = console.log.argsForCall[0][0] + expect(warningMsg).toContain('WARNING:'); + expect(warningMsg).toContain('non-existing'); + expect(warningMsg).toContain('api/with-broken.link'); + }); + }); + + //////////////////////////////////////// + + describe('TAG', function() { + describe('@param', function() { + it('should parse with no default', function() { + var doc = new Doc('@name a\n@param {(number|string)} number Number \n to format.'); + doc.parse(); + expect(doc.param).toEqual([{ + type : '(number|string)', + name : 'number', + optional: false, + 'default' : undefined, + description : 'soon
\n' }]); + }); + + it('should parse with default and optional', function() { + var doc = new Doc('@name a\n@param {(number|string)=} [fractionSize=2] desc'); + doc.parse(); + expect(doc.param).toEqual([{ + type : '(number|string)', + name : 'fractionSize', + optional: true, + 'default' : '2', + description : 'Number \nto format.
\n' }]); + }); + }); + + describe('@requires', function() { + it('should parse more @requires tag into array', function() { + var doc = new Doc('@name a\n@requires $service for \n`A`\n@requires $another for `B`'); + doc.ngdoc = 'service'; + doc.parse(); + expect(doc.requires).toEqual([ + {name:'$service', text:'desc
\n'}, + {name:'$another', text:'for \n
\nA
'}]); + expect(doc.html()).toContain('$service'); + expect(doc.html()).toContain('$another'); + expect(doc.html()).toContain('for
\nB
for \n
'); + expect(doc.html()).toContain('A
for
'); + }); + }); + + describe('@scope', function() { + it('should state the new scope will be created', function() { + var doc = new Doc('@name a\n@scope'); + doc.ngdoc = 'directive'; + doc.parse(); + expect(doc.scope).toEqual(''); + expect(doc.html()).toContain('This directive creates new scope.'); + }); + }); + + describe('@priority', function() { + it('should state the priority', function() { + var doc = new Doc('@name a\n@priority 123'); + doc.ngdoc = 'directive'; + doc.parse(); + expect(doc.priority).toEqual('123'); + expect(doc.html()).toContain('This directive executes at priority level 123.'); + }); + }); + + describe('@property', function() { + it('should parse @property tags into array', function() { + var doc = new Doc("@name a\n@property {type} name1 desc\n@property {type} name2 desc"); + doc.parse(); + expect(doc.properties.length).toEqual(2); + }); + + it('should not parse @property without a type', function() { + var doc = new Doc("@property fake", 'test.js', '44'); + expect(function() { doc.parse(); }). + toThrow(new Error("Not a valid 'property' format: fake (found in: test.js:44)")); + }); + + it('should parse @property with type', function() { + var doc = new Doc("@name a\n@property {string} name"); + doc.parse(); + expect(doc.properties[0].name).toEqual('name'); + expect(doc.properties[0].type).toEqual('string'); + }); + + it('should parse @property with optional description', function() { + var doc = new Doc("@name a\n@property {string} name desc rip tion"); + doc.parse(); + expect(doc.properties[0].name).toEqual('name'); + expect(doc.properties[0].description).toEqual('B
'); + }); + + it('should parse @property with type and description both', function() { + var doc = new Doc("@name a\n@property {bool} name desc rip tion"); + doc.parse(); + expect(doc.properties[0].name).toEqual('name'); + expect(doc.properties[0].type).toEqual('bool'); + expect(doc.properties[0].description).toEqual('desc rip tion
\n'); + }); + + }); + + describe('@returns', function() { + it('should not parse @returns without type', function() { + var doc = new Doc("@returns lala"); + expect(function() { doc.parse(); }). + toThrow(); + }); + + + it('should not parse @returns with invalid type', function() { + var doc = new Doc("@returns {xx}x} lala", 'test.js', 34); + expect(function() { doc.parse(); }). + toThrow(new Error("Not a valid 'returns' format: {xx}x} lala (found in: test.js:34)")); + }); + + + it('should parse @returns with type and description', function() { + var doc = new Doc("@name a\n@returns {string} descrip tion"); + doc.parse(); + expect(doc.returns).toEqual({type: 'string', description: 'desc rip tion
\n'}); + }); + + it('should parse @returns with complex type and description', function() { + var doc = new Doc("@name a\n@returns {function(string, number=)} description"); + doc.parse(); + expect(doc.returns).toEqual({type: 'function(string, number=)', description: 'descrip tion
\n'}); + }); + + it('should transform description of @returns with markdown', function() { + var doc = new Doc("@name a\n@returns {string} descrip *tion*"); + doc.parse(); + expect(doc.returns).toEqual({type: 'string', description: 'description
\n'}); + }); + + it('should support multiline content', function() { + var doc = new Doc("@name a\n@returns {string} description\n new line\n another line"); + doc.parse(); + expect(doc.returns). + toEqual({type: 'string', description: 'descrip tion
\n'}); + }); + }); + + describe('@description', function() { + it('should support pre blocks', function() { + var doc = new Doc("@name a\n@descriptiondescription\nnew line\nanother line
\nabc"); + doc.parse(); + expect(doc.description). + toBe(''); + }); + + it('should support multiple pre blocks', function() { + var doc = new Doc("@name a\n@description foo \n<b>abc</b>\nabc\n#bah\nfoo \ncba"); + doc.parse(); + expect(doc.description). + toBe(''); + }); + + it('should support nested @link annotations with or without description', function() { + var doc = new Doc("@name a\n@description " + + 'foo {@link angular.foo}\n\n da {@link angular.foo bar foo bar } \n\n' + + 'dad{@link angular.foo}\n\n' + + 'external{@link http://angularjs.org}\n\n' + + 'external{@link ./static.html}\n\n' + + '{@link angular.directive.ng-foo ng:foo}'); + + doc.section = 'api'; + doc.parse(); + + expect(doc.description). + toContain('foofoo \n' + + '
abc\n' + + 'bah
\n' + + 'foo \n' + + '
cba\nangular.foo
'); + expect(doc.description). + toContain('dabar foo bar
'); + expect(doc.description). + toContain('dadangular.foo
'); + expect(doc.description). + toContain('ng:foo
'); + expect(doc.description). + toContain('http://angularjs.org'); + expect(doc.description). + toContain('./static.html'); + }); + + it('should support line breaks in @link', function() { + var doc = new Doc("@name a\n@description " + + '{@link\napi/angular.foo\na\nb}'); + doc.parse(); + expect(doc.description). + toContain('a b
'); + }); + + }); + + describe('@example', function() { + it('should not remove {{}}', function() { + var doc = new Doc('@name a\n@example text {{ abc }}'); + doc.parse(); + expect(doc.example).toEqual(''); + }); + }); + + describe('@deprecated', function() { + it('should parse @deprecated', function() { + var doc = new Doc('@name a\n@deprecated Replaced with foo.'); + doc.parse(); + expect(doc.deprecated).toBe('Replaced with foo.'); + }); + }); + + describe('@this', function() { + it('should render @this', function() { + var doc = new Doc('@name a\n@this I am self.'); + doc.ngdoc = 'filter'; + doc.parse(); + expect(doc.html()).toContain('text {{ abc }}
\nMethod\'s
\n' + + 'this
' + + '\n'); + expect(doc.html()).toContain('' + + '' + + 'I am self.
\n' + + 'Method\'s
\n' + + 'this
'); + }); + }); + + describe('@animations', function() { + it('should render @this', function() { + var doc = new Doc('@name a\n@animations\nenter - Add text\nleave - Remove text\n'); + doc.ngdoc = 'filter'; + doc.parse(); + expect(doc.html()).toContain( + 'I am self.
\nAnimations
\n' + + '' + + ''); + }); + }); + }); + + describe('usage', function() { + describe('overview', function() { + it('should supress description heading', function() { + var doc = new Doc('@ngdoc overview\n@name angular\n@description\n#heading\ntext'); + doc.parse(); + expect(doc.html()).toContain('text'); + expect(doc.html()).toContain('' + + '
' + + '- enter - Add text
' + + '- leave - Remove text
' + + 'heading
'); + expect(doc.html()).not.toContain('Description'); + }); + }); + + + describe('function', function() { + it('should format', function() { + var doc = new Doc({ + ngdoc:'function', + name:'some.name', + param: [ + {name:'a', type: 'string', optional: true}, + {name:'b', type: 'someType', optional: true, 'default': '"xxx"'}, + {name:'c', type: 'string', description: 'param desc'} + ], + returns: {type: 'number', description: 'return desc'} + }); + doc.html_usage_function(dom); + expect(dom).toContain('name([a][, b], c)'); //TODO(i) the comma position here is lame + expect(dom).toContain('param desc'); + expect(dom).toContain('(optional)'); + expect(dom).toContain('return desc'); + }); + }); + + describe('filter', function() { + it('should format', function() { + var doc = new Doc({ + ngdoc:'formatter', + shortName:'myFilter', + param: [ + {name:'a', type:'string'}, + {name:'b', type:'string'} + ] + }); + doc.html_usage_filter(dom); + expect(dom).toContain('myFilter_expression | myFilter:b'); + expect(dom).toContain('$filter(\'myFilter\')(a, b)'); + }); + }); + + describe('property', function() { + it('should format', function() { + var doc = new Doc({ + ngdoc:'property', + name:'myProp', + type:'string', + returns:{type: 'type', description: 'description'} + }); + doc.html_usage_property(dom); + expect(dom).toContain('myProp'); + expect(dom).toContain('type'); + expect(dom).toContain('description'); + }); + }); + }); + +});