'],
                [html.Expansion, 'count', 'plural', 1, '{count, plural, =0 {msg}}'],
                [html.ExpansionCase, '=0', 2, '=0 {msg}'],
              ]);
        });
        it('should not report a value span for an attribute without a value', () => {
          const ast = parser.parse('
', 'TestComp');
          expect((ast.rootNodes[0] as html.Element).attrs[0].valueSpan).toBeUndefined();
        });
        it('should report a value span for an attibute with a value', () => {
          const ast = parser.parse('
', 'TestComp');
          const attr = (ast.rootNodes[0] as html.Element).attrs[0];
          expect(attr.valueSpan.start.offset).toEqual(9);
          expect(attr.valueSpan.end.offset).toEqual(13);
        });
      });
      describe('visitor', () => {
        it('should visit text nodes', () => {
          const result = humanizeDom(parser.parse('text', 'TestComp'));
          expect(result).toEqual([[html.Text, 'text', 0]]);
        });
        it('should visit element nodes', () => {
          const result = humanizeDom(parser.parse('
', 'TestComp'));
          expect(result).toEqual([[html.Element, 'div', 0]]);
        });
        it('should visit attribute nodes', () => {
          const result = humanizeDom(parser.parse('
', 'TestComp'));
          expect(result).toContain([html.Attribute, 'id', 'foo']);
        });
        it('should visit all nodes', () => {
          const result =
              parser.parse('
a b 
', 'TestComp');
          const accumulator: html.Node[] = [];
          const visitor = new class {
            visit(node: html.Node, context: any) { accumulator.push(node); }
            visitElement(element: html.Element, context: any): any {
              html.visitAll(this, element.attrs);
              html.visitAll(this, element.children);
            }
            visitAttribute(attribute: html.Attribute, context: any): any {}
            visitText(text: html.Text, context: any): any {}
            visitComment(comment: html.Comment, context: any): any {}
            visitExpansion(expansion: html.Expansion, context: any): any {
              html.visitAll(this, expansion.cases);
            }
            visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {}
          };
          html.visitAll(visitor, result.rootNodes);
          expect(accumulator.map(n => n.constructor)).toEqual([
            html.Element, html.Attribute, html.Element, html.Attribute, html.Text, html.Element,
            html.Text
          ]);
        });
        it('should skip typed visit if visit() returns a truthy value', () => {
          const visitor = new class {
            visit(node: html.Node, context: any) { return true; }
            visitElement(element: html.Element, context: any): any { throw Error('Unexpected'); }
            visitAttribute(attribute: html.Attribute, context: any): any {
              throw Error('Unexpected');
            }
            visitText(text: html.Text, context: any): any { throw Error('Unexpected'); }
            visitComment(comment: html.Comment, context: any): any { throw Error('Unexpected'); }
            visitExpansion(expansion: html.Expansion, context: any): any {
              throw Error('Unexpected');
            }
            visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
              throw Error('Unexpected');
            }
          };
          const result = parser.parse('
', 'TestComp');
          const traversal = html.visitAll(visitor, result.rootNodes);
          expect(traversal).toEqual([true, true]);
        });
      });
      describe('errors', () => {
        it('should report unexpected closing tags', () => {
          const errors = parser.parse('
', 'TestComp').errors;
          expect(errors.length).toEqual(1);
          expect(humanizeErrors(errors)).toEqual([['p', 'Unexpected closing tag "p"', '0:5']]);
        });
        it('should report subsequent open tags without proper close tag', () => {
          const errors = parser.parse('
', 'TestComp').errors;
          expect(errors.length).toEqual(1);
          expect(humanizeErrors(errors)).toEqual([['div', 'Unexpected closing tag "div"', '0:4']]);
        });
        it('should report closing tag for void elements', () => {
          const errors = parser.parse('
', 'TestComp').errors;
          expect(errors.length).toEqual(1);
          expect(humanizeErrors(errors)).toEqual([
            ['input', 'Void elements do not have end tags "input"', '0:7']
          ]);
        });
        it('should report self closing html element', () => {
          const errors = parser.parse('
', 'TestComp').errors;
          expect(errors.length).toEqual(1);
          expect(humanizeErrors(errors)).toEqual([
            ['p', 'Only void and foreign elements can be self closed "p"', '0:0']
          ]);
        });
        it('should report self closing custom element', () => {
          const errors = parser.parse('
', 'TestComp').errors;
          expect(errors.length).toEqual(1);
          expect(humanizeErrors(errors)).toEqual([
            ['my-cmp', 'Only void and foreign elements can be self closed "my-cmp"', '0:0']
          ]);
        });
        it('should also report lexer errors', () => {
          const errors = parser.parse('
', 'TestComp').errors;
          expect(errors.length).toEqual(2);
          expect(humanizeErrors(errors)).toEqual([
            [TokenType.COMMENT_START, 'Unexpected character "e"', '0:3'],
            ['p', 'Unexpected closing tag "p"', '0:14']
          ]);
        });
      });
    });
  });
}
export function humanizeErrors(errors: ParseError[]): any[] {
  return errors.map(e => {
    if (e instanceof TreeError) {
      // Parser errors
      return [
e.elementName, e.msg, humanizeLineColumn(e.span.start)];
    }
    // Tokenizer errors
    return [(e).tokenType, e.msg, humanizeLineColumn(e.span.start)];
  });
}