aboutsummaryrefslogtreecommitdiffstats
path: root/docs/src/callback.js
blob: aaf69cde785752c2e3bace5b63433852e708b718 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function noop(){}

function chain(delegateFn, explicitDone){
  var onDoneFn = noop;
  var onErrorFn = function(e){
    console.error(e.stack || e);
    process.exit(-1);
  };
  var waitForCount = 1;
  delegateFn = delegateFn || noop;
  var stackError = new Error('capture stack');

  function decrementWaitFor() {
    waitForCount--;
    if (waitForCount == 0)
      onDoneFn();
  }

  function self(){
    try {
      return delegateFn.apply(self, arguments);
    } catch (error) {
      self.error(error);
    } finally {
      if (!explicitDone)
        decrementWaitFor();
    }
  };
  self.onDone = function(callback){
    onDoneFn = callback;
    return self;
  };
  self.onError = function(callback){
    onErrorFn = callback;
    return self;
  };
  self.waitFor = function(callback){
    if (waitForCount == 0)
      throw new Error("Can not wait on already called callback.");
    waitForCount++;
    return chain(callback).onDone(decrementWaitFor).onError(self.error);
  };

  self.waitMany = function(callback){
    if (waitForCount == 0)
      throw new Error("Can not wait on already called callback.");
    waitForCount++;
    return chain(callback, true).onDone(decrementWaitFor).onError(self.error);
  };

  self.done = function(callback){
    decrementWaitFor();
  };

  self.error = function(error) {
    var stack = stackError.stack.split(/\n\r?/).splice(2);
    var nakedStack = [];
    stack.forEach(function(frame){
      if (!frame.match(/callback\.js:\d+:\d+\)$/))
        nakedStack.push(frame);
    });
    error.stack = error.stack + '\nCalled from:\n' + nakedStack.join('\n');
    onErrorFn(error);
  };

  return self;
}

exports.chain = chain;
'n354' href='#n354'>354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
var ngdoc = require('../src/ngdoc.js');
var DOM = require('../src/dom.js').DOM;

describe('ngdoc', function() {
  var Doc = ngdoc.Doc;
  var dom;

  beforeEach(function() {
    dom = new DOM();
    this.addMatchers({
      toContain: function(text) {
        this.actual = this.actual.toString();
        return this.actual.indexOf(text) > -1;
      }
    });
  });

  describe('Doc', function() {
    describe('metadata', function() {

      it('should find keywords and filter ignored words', function() {
        expect(new Doc('\nHello: World! @ignore. $abc').keywords()).toEqual('$abc hello world');
        expect(new Doc('The `ng:class-odd` and').keywords()).toEqual('ng:class-odd');
      });

      it('should get property and methods', function() {
        var doc = new Doc('Document');
        doc.properties.push(new Doc('Proprety'));
        doc.properties.push(new Doc('Method'));
        expect(doc.keywords()).toEqual('document method proprety');
      });

      it('should have shortName', function() {
        var d1 = new Doc('@name a.b.c').parse();
        var d2 = new Doc('@name a.b.ng-c').parse();
        var d3 = new Doc('@name some text: more text').parse();
        expect(ngdoc.metadata([d1])[0].shortName).toEqual('c');
        expect(ngdoc.metadata([d2])[0].shortName).toEqual('ng-c');
        expect(ngdoc.metadata([d3])[0].shortName).toEqual('more text');
      });

    });

    describe('parse', function() {
      it('should convert @names into properties', function() {
        var doc = new Doc('\n@name name\n@desc\ndesc\ndesc2\n@dep\n');
        doc.parse();
        expect(doc.name).toEqual('name');
        expect(doc.desc).toEqual('desc\ndesc2');
        expect(doc.dep).toEqual('');
      });

      it('should parse parameters', function() {
        var doc = new Doc(
            '@name a\n' +
            '@param {*} a short\n' +
            '@param {Type} b med\n' +
            '@param {Class=} [c=2] long\nline\n' +
            '@param {function(number, string=)} d fn with optional arguments');
        doc.parse();
        expect(doc.param).toEqual([
           {name:'a', description:'<p>short</p>', type:'*', optional:false, 'default':undefined},
           {name:'b', description:'<p>med</p>', type:'Type', optional:false, 'default':undefined},
           {name:'c', description:'<p>long\nline</p>', type:'Class', optional:true, 'default':'2'},
           {name:'d', description:'<p>fn with optional arguments</p>',
             type: 'function(number, string=)', optional: false, 'default':undefined}
         ]);
      });

      it('should parse return', function() {
        var doc = new Doc('@name a\n@returns {Type} text *bold*.');
        doc.parse();
        expect(doc.returns).toEqual({
          type: 'Type',
          description: '<p>text <em>bold</em>.</p>'
        });
      });

      it('should parse filename', function() {
        var doc = new Doc('@name friendly name', 'docs/a.b.ngdoc', 1);
        doc.parse(0);
        expect(doc.id).toEqual('a.b');
        expect(doc.name).toEqual('friendly name');
      });

      it('should store all links', function() {
        var doc = new Doc('@name a\n@description {@link api/angular.link}');
        doc.parse();

        expect(doc.links).toContain('api/angular.link');
      });

      describe('convertUrlToAbsolute', function() {
        var doc;

        beforeEach(function() {
          doc = new Doc({section: 'section'});
        });

        it('should not change absolute url', function() {
          expect(doc.convertUrlToAbsolute('guide/index')).toEqual('guide/index');
        });

        it('should prepend current section to relative url', function() {
          expect(doc.convertUrlToAbsolute('angular.widget')).toEqual('section/angular.widget');
        });

        it('should change id to index if not specified', function() {
          expect(doc.convertUrlToAbsolute('guide/')).toEqual('guide/index');
        });
      });

      describe('sorting', function() {
        function property(name) {
          return function(obj) {return obj[name];};
        }
        function noop() {}
        function doc(type, name){
          return {
              id: name,
              ngdoc: type,
              keywords: noop
          };
        }

        var dev_guide_overview = doc('overview', 'dev_guide.overview');
        var dev_guide_bootstrap = doc('function', 'dev_guide.bootstrap');

        it('should put angular.fn() in front of dev_guide.overview, etc', function() {
          expect(ngdoc.metadata([dev_guide_overview, dev_guide_bootstrap]).map(property('id')))
            .toEqual(['dev_guide.overview', 'dev_guide.bootstrap']);
        });
      });
    });
  });

  describe('markdown', function() {
    it('should not replace anything in <pre>, but escape the html escape the content', function() {
      expect(new Doc().markdown('bah x\n<pre>\n<b>angular</b>.k\n</pre>\n asdf x')).
        toEqual(
            '<p>bah x\n' +
            '<pre class="prettyprint linenums">\n' +
            '&lt;b&gt;angular&lt;/b&gt;.k\n' +
            '</pre>\n' +
            ' asdf x</p>');
    });

    it('should replace text between two <pre></pre> tags', function() {
      expect(new Doc().markdown('<pre>x</pre>\n# One\n<pre>b</pre>')).
        toMatch('</pre>\n\n<h1 id="one">One</h1>\n\n<pre');
    });

    it('should replace inline variable type hints', function() {
      expect(new Doc().markdown('{@type string}')).
        toMatch(/<a\s+.*?class=".*?type-hint type-hint-string.*?".*?>/);
    });

    it('should ignore nested doc widgets', function() {
      expect(new Doc().markdown(
        'before<div class="tabbable">\n' +
          '<div class="tab-pane well" id="git-mac" ng:model="Git on Mac/Linux">' +
          '\ngit bla bla\n</doc:tutorial-instruction>\n' +
        '</doc:tutorial-instructions>')).toEqual(

        '<p>before<div class="tabbable">\n' +
          '<div class="tab-pane well" id="git-mac" ng:model="Git on Mac/Linux">\n' +
          'git bla bla\n' +
          '</doc:tutorial-instruction>\n' +
        '</doc:tutorial-instructions></p>');
      });

    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('<p>first line\n' +
                'second line</p>\n\n' +
                '<pre><code>third line\n' +
                ' fourth line\n</code></pre>\n\n' +
                '<p>fifth line</p>');
    });

    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('<p>first line</p>\n\n' +
                '<pre><code>second line\n' +
                'third line\n' +
                ' fourth line\n</code></pre>\n\n' +
                '<p>fifth line</p>');
    });


  });

  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('links checking', function() {
      var docs;
      beforeEach(function() {
        spyOn(console, 'log');
        docs = [new Doc({section: 'api', id: 'fake.id1', links: ['non-existing-link']}),
                new Doc({section: 'api', id: 'fake.id2'}),
                new Doc({section: 'api', id: 'fake.id3'})];
      });

      it('should log warning when any link doesn\'t exist', function() {
        ngdoc.merge(docs);
        expect(console.log).toHaveBeenCalled();
        expect(console.log.argsForCall[0][0]).toContain('WARNING:');
      });

      it('should say which link doesn\'t exist', function() {
        ngdoc.merge(docs);
        expect(console.log.argsForCall[0][0]).toContain('non-existing-link');
      });

      it('should say where is the non-existing link', function() {
        ngdoc.merge(docs);
        expect(console.log.argsForCall[0][0]).toContain('api/fake.id1');
      });
    });
  });

  ////////////////////////////////////////

  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 : '<p>Number \nto format.</p>' }]);
      });

      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 : '<p>desc</p>' }]);
      });
    });

    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:'<p>for \n<code>A</code></p>'},
          {name:'$another', text:'<p>for <code>B</code></p>'}]);
        expect(doc.html()).toContain('<a href="api/ng.$service">$service</a>');
        expect(doc.html()).toContain('<a href="api/ng.$another">$another</a>');
        expect(doc.html()).toContain('<p>for \n<code>A</code></p>');
        expect(doc.html()).toContain('<p>for <code>B</code></p>');
      });
    });

    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('<p>desc rip tion</p>');
      });

      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('<p>desc rip tion</p>');
      });

    });

    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: '<p>descrip tion</p>'});
      });

      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: '<p>description</p>'});
      });

      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: '<p>descrip <em>tion</em></p>'});
      });

      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: '<p>description\nnew line\nanother line</p>'});
      });
    });

    describe('@description', function() {
      it('should support pre blocks', function() {
        var doc = new Doc("@name a\n@description <pre><b>abc</b></pre>");
        doc.parse();
        expect(doc.description).
          toBe('<pre class="prettyprint linenums">&lt;b&gt;abc&lt;/b&gt;</pre>');
      });

      it('should support multiple pre blocks', function() {
        var doc = new Doc("@name a\n@description foo \n<pre>abc</pre>\n#bah\nfoo \n<pre>cba</pre>");
        doc.parse();
        expect(doc.description).
          toBe('<p>foo \n' +
               '<pre class="prettyprint linenums">abc</pre>\n\n' +
               '<h1 id="bah">bah</h1>\n\n' +
               '<p>foo \n' +
               '<pre class="prettyprint linenums">cba</pre>');

      });

      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('foo <a href="api/angular.foo"><code>angular.foo</code></a>');
        expect(doc.description).
          toContain('da <a href="api/angular.foo"><code>bar foo bar</code></a>');
        expect(doc.description).
          toContain('dad<a href="api/angular.foo"><code>angular.foo</code></a>');
        expect(doc.description).
          toContain('<a href="api/angular.directive.ng-foo"><code>ng:foo</code></a>');
        expect(doc.description).
          toContain('<a href="http://angularjs.org">http://angularjs.org</a>');
        expect(doc.description).
          toContain('<a href="./static.html">./static.html</a>');
      });

      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 href="api/angular.foo"><code>a b</code></a>');
      });

    });

    describe('@example', function() {
      it('should not remove {{}}', function() {
        var doc = new Doc('@name a\n@example text {{ abc }}');
        doc.parse();
        expect(doc.example).toEqual('<p>text {{ abc }}</p>');
      });
    });

    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('<h3>Method\'s <code>this</code></h3>\n' +
            '<div>' +
            '<p>I am self.</p>' +
            '</div>\n');
        expect(doc.html()).toContain('<h3>Method\'s <code>this</code></h3>\n' +
            '<div><p>I am self.</p></div>');
      });
    });

    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(
            '<h3 id="Animations">Animations</h3>\n' +
            '<div class="animations">' +
              '<ul>' +
                '<li>enter - Add text</li>' +
                '<li>leave - Remove text</li>' +
              '</ul>' +
            '</div>');
      });
    });
  });

  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('<h2 id="heading">heading</h2>');
        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');
      });
    });
  });

});