() => {
    test('simple text', () => {
      const ast = baseParse('some text')
      const text = ast.children[0] as TextNode

      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some text',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: 'some text'
        }
      })
    })

    test('simple text with invalid end tag', () => {
      const onError = jest.fn()
      const ast = baseParse('some text
', {
        onError
      })
      const text = ast.children[0] as TextNode

      expect(onError).toBeCalled()
      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some text',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: 'some text'
        }
      })
    })

    test('text with interpolation', () => {
      const ast = baseParse('some {{ foo + bar }} text')
      const text1 = ast.children[0] as TextNode
      const text2 = ast.children[2] as TextNode

      expect(text1).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some ',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 5, line: 1, column: 6 },
          source: 'some '
        }
      })
      expect(text2).toStrictEqual({
        type: NodeTypes.TEXT,
        content: ' text',
        loc: {
          start: { offset: 20, line: 1, column: 21 },
          end: { offset: 25, line: 1, column: 26 },
          source: ' text'
        }
      })
    })

    test('text with interpolation which has `<`', () => {
      const ast = baseParse('some {{ ad }} text')
      const text1 = ast.children[0] as TextNode
      const text2 = ast.children[2] as TextNode

      expect(text1).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some ',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 5, line: 1, column: 6 },
          source: 'some '
        }
      })
      expect(text2).toStrictEqual({
        type: NodeTypes.TEXT,
        content: ' text',
        loc: {
          start: { offset: 21, line: 1, column: 22 },
          end: { offset: 26, line: 1, column: 27 },
          source: ' text'
        }
      })
    })

    test('text with mix of tags and interpolations', () => {
      const ast = baseParse('some {{ foo < bar + foo }} text')
      const text1 = ast.children[0] as TextNode
      const text2 = (ast.children[1] as ElementNode).children![1] as TextNode

      expect(text1).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some ',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 5, line: 1, column: 6 },
          source: 'some '
        }
      })
      expect(text2).toStrictEqual({
        type: NodeTypes.TEXT,
        content: ' text',
        loc: {
          start: { offset: 32, line: 1, column: 33 },
          end: { offset: 37, line: 1, column: 38 },
          source: ' text'
        }
      })
    })

    test('lonely "<" doesn\'t separate nodes', () => {
      const ast = baseParse('a  {
          if (err.code !== ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME) {
            throw err
          }
        }
      })
      const text = ast.children[0] as TextNode

      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'a < b',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 5, line: 1, column: 6 },
          source: 'a  {
      const ast = baseParse('a {{ b', {
        onError: error => {
          if (error.code !== ErrorCodes.X_MISSING_INTERPOLATION_END) {
            throw error
          }
        }
      })
      const text = ast.children[0] as TextNode

      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'a {{ b',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 6, line: 1, column: 7 },
          source: 'a {{ b'
        }
      })
    })
  }


() => {
      const ast = baseParse('some text')
      const text = ast.children[0] as TextNode

      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some text',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: 'some text'
        }
      })
    }


() => {
      const onError = jest.fn()
      const ast = baseParse('some text</div>', {
        onError
      })
      const text = ast.children[0] as TextNode

      expect(onError).toBeCalled()
      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some text',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: 'some text'
        }
      })
    }


() => {
      const ast = baseParse('some {{ foo + bar }} text')
      const text1 = ast.children[0] as TextNode
      const text2 = ast.children[2] as TextNode

      expect(text1).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some ',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 5, line: 1, column: 6 },
          source: 'some '
        }
      })
      expect(text2).toStrictEqual({
        type: NodeTypes.TEXT,
        content: ' text',
        loc: {
          start: { offset: 20, line: 1, column: 21 },
          end: { offset: 25, line: 1, column: 26 },
          source: ' text'
        }
      })
    }


() => {
      const ast = baseParse('some {{ a<b && c>d }} text')
      const text1 = ast.children[0] as TextNode
      const text2 = ast.children[2] as TextNode

      expect(text1).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some ',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 5, line: 1, column: 6 },
          source: 'some '
        }
      })
      expect(text2).toStrictEqual({
        type: NodeTypes.TEXT,
        content: ' text',
        loc: {
          start: { offset: 21, line: 1, column: 22 },
          end: { offset: 26, line: 1, column: 27 },
          source: ' text'
        }
      })
    }


() => {
      const ast = baseParse('some <span>{{ foo < bar + foo }} text</span>')
      const text1 = ast.children[0] as TextNode
      const text2 = (ast.children[1] as ElementNode).children![1] as TextNode

      expect(text1).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some ',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 5, line: 1, column: 6 },
          source: 'some '
        }
      })
      expect(text2).toStrictEqual({
        type: NodeTypes.TEXT,
        content: ' text',
        loc: {
          start: { offset: 32, line: 1, column: 33 },
          end: { offset: 37, line: 1, column: 38 },
          source: ' text'
        }
      })
    }


() => {
      const ast = baseParse('a < b', {
        onError: err => {
          if (err.code !== ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME) {
            throw err
          }
        }
      })
      const text = ast.children[0] as TextNode

      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'a < b',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 5, line: 1, column: 6 },
          source: 'a < b'
        }
      })
    }


err => {
          if (err.code !== ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME) {
            throw err
          }
        }


() => {
      const ast = baseParse('a {{ b', {
        onError: error => {
          if (error.code !== ErrorCodes.X_MISSING_INTERPOLATION_END) {
            throw error
          }
        }
      })
      const text = ast.children[0] as TextNode

      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'a {{ b',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 6, line: 1, column: 7 },
          source: 'a {{ b'
        }
      })
    }


error => {
          if (error.code !== ErrorCodes.X_MISSING_INTERPOLATION_END) {
            throw error
          }
        }


() => {
    test('simple interpolation', () => {
      const ast = baseParse('{{message}}')
      const interpolation = ast.children[0] as InterpolationNode

      expect(interpolation).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `message`,
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 2, line: 1, column: 3 },
            end: { offset: 9, line: 1, column: 10 },
            source: `message`
          }
        },
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 11, line: 1, column: 12 },
          source: '{{message}}'
        }
      })
    })

    test('it can have tag-like notation', () => {
      const ast = baseParse('{{ a<b }}')
      const interpolation = ast.children[0] as InterpolationNode

      expect(interpolation).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `a<b`,
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 3, line: 1, column: 4 },
            end: { offset: 6, line: 1, column: 7 },
            source: 'a<b'
          }
        },
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: '{{ a {
      const ast = baseParse('{{ ad }}')
      const interpolation1 = ast.children[0] as InterpolationNode
      const interpolation2 = ast.children[1] as InterpolationNode

      expect(interpolation1).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `a<b`,
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 3, line: 1, column: 4 },
            end: { offset: 6, line: 1, column: 7 },
            source: 'a<b'
          }
        },
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: '{{ ad',
          loc: {
            start: { offset: 12, line: 1, column: 13 },
            end: { offset: 15, line: 1, column: 16 },
            source: 'c>d'
          }
        },
        loc: {
          start: { offset: 9, line: 1, column: 10 },
          end: { offset: 18, line: 1, column: 19 },
          source: '{{ c>d }}'
        }
      })
    })

    test('it can have tag-like notation (3)', () => {
      const ast = baseParse('
{{ "
" }}
')
      const element = ast.children[0] as ElementNode
      const interpolation = element.children[0] as InterpolationNode

      expect(interpolation).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          isStatic: false,
          // The `constType` is the default value and will be determined in `transformExpression`.
          constType: ConstantTypes.NOT_CONSTANT,
          content: '"
"',
          loc: {
            start: { offset: 8, line: 1, column: 9 },
            end: { offset: 16, line: 1, column: 17 },
            source: '"
"'
          }
        },
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 19, line: 1, column: 20 },
          source: '{{ "
" }}'
        }
      })
    })

    test('custom delimiters', () => {
      const ast = baseParse('
{msg}
', {
        delimiters: ['{', '}']
      })
      const element = ast.children[0] as ElementNode
      const interpolation = element.children[0] as InterpolationNode

      expect(interpolation).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `msg`,
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 4, line: 1, column: 5 },
            end: { offset: 7, line: 1, column: 8 },
            source: 'msg'
          }
        },
        loc: {
          start: { offset: 3, line: 1, column: 4 },
          end: { offset: 8, line: 1, column: 9 },
          source: '{msg}'
        }
      })
    })
  }


() => {
      const ast = baseParse('{{message}}')
      const interpolation = ast.children[0] as InterpolationNode

      expect(interpolation).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `message`,
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 2, line: 1, column: 3 },
            end: { offset: 9, line: 1, column: 10 },
            source: `message`
          }
        },
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 11, line: 1, column: 12 },
          source: '{{message}}'
        }
      })
    }


() => {
      const ast = baseParse('{{ a<b }}')
      const interpolation = ast.children[0] as InterpolationNode

      expect(interpolation).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `a<b`,
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 3, line: 1, column: 4 },
            end: { offset: 6, line: 1, column: 7 },
            source: 'a<b'
          }
        },
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: '{{ a<b }}'
        }
      })
    }


() => {
      const ast = baseParse('{{ a<b }}{{ c>d }}')
      const interpolation1 = ast.children[0] as InterpolationNode
      const interpolation2 = ast.children[1] as InterpolationNode

      expect(interpolation1).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `a<b`,
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 3, line: 1, column: 4 },
            end: { offset: 6, line: 1, column: 7 },
            source: 'a<b'
          }
        },
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: '{{ a<b }}'
        }
      })

      expect(interpolation2).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          content: 'c>d',
          loc: {
            start: { offset: 12, line: 1, column: 13 },
            end: { offset: 15, line: 1, column: 16 },
            source: 'c>d'
          }
        },
        loc: {
          start: { offset: 9, line: 1, column: 10 },
          end: { offset: 18, line: 1, column: 19 },
          source: '{{ c>d }}'
        }
      })
    }


() => {
      const ast = baseParse('
{{ "
" }}
')
      const element = ast.children[0] as ElementNode
      const interpolation = element.children[0] as InterpolationNode

      expect(interpolation).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          isStatic: false,
          // The `constType` is the default value and will be determined in `transformExpression`.
          constType: ConstantTypes.NOT_CONSTANT,
          content: '"
"',
          loc: {
            start: { offset: 8, line: 1, column: 9 },
            end: { offset: 16, line: 1, column: 17 },
            source: '"
"'
          }
        },
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 19, line: 1, column: 20 },
          source: '{{ "
" }}'
        }
      })
    }


() => {
      const ast = baseParse('<p>{msg}</p>', {
        delimiters: ['{', '}']
      })
      const element = ast.children[0] as ElementNode
      const interpolation = element.children[0] as InterpolationNode

      expect(interpolation).toStrictEqual({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `msg`,
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 4, line: 1, column: 5 },
            end: { offset: 7, line: 1, column: 8 },
            source: 'msg'
          }
        },
        loc: {
          start: { offset: 3, line: 1, column: 4 },
          end: { offset: 8, line: 1, column: 9 },
          source: '{msg}'
        }
      })
    }


() => {
    test('empty comment', () => {
      const ast = baseParse('<!---->')
      const comment = ast.children[0] as CommentNode

      expect(comment).toStrictEqual({
        type: NodeTypes.COMMENT,
        content: '',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 7, line: 1, column: 8 },
          source: '<!---->'
        }
      })
    })

    test('simple comment', () => {
      const ast = baseParse('<!--abc-->')
      const comment = ast.children[0] as CommentNode

      expect(comment).toStrictEqual({
        type: NodeTypes.COMMENT,
        content: 'abc',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 10, line: 1, column: 11 },
          source: '<!--abc-->'
        }
      })
    })

    test('two comments', () => {
      const ast = baseParse('<!--abc--><!--def-->')
      const comment1 = ast.children[0] as CommentNode
      const comment2 = ast.children[1] as CommentNode

      expect(comment1).toStrictEqual({
        type: NodeTypes.COMMENT,
        content: 'abc',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 10, line: 1, column: 11 },
          source: '<!--abc-->'
        }
      })
      expect(comment2).toStrictEqual({
        type: NodeTypes.COMMENT,
        content: 'def',
        loc: {
          start: { offset: 10, line: 1, column: 11 },
          end: { offset: 20, line: 1, column: 21 },
          source: '<!--def-->'
        }
      })
    })

    test('comments option', () => {
      __DEV__ = false
      const astDefaultComment = baseParse('<!--abc-->')
      const astNoComment = baseParse('<!--abc-->', { comments: false })
      const astWithComments = baseParse('<!--abc-->', { comments: true })
      __DEV__ = true

      expect(astDefaultComment.children).toHaveLength(0)
      expect(astNoComment.children).toHaveLength(0)
      expect(astWithComments.children).toHaveLength(1)
    })

    // #2217
    test('comments in the <pre> tag should be removed in production mode', () => {
      __DEV__ = false
      const rawText = `<p/><!-- foo --><p/>`
      const ast = baseParse(`<pre>${rawText}</pre>`)
      __DEV__ = true

      expect((ast.children[0] as ElementNode).children).toMatchObject([
        {
          type: NodeTypes.ELEMENT,
          tag: 'p'
        },
        {
          type: NodeTypes.ELEMENT,
          tag: 'p'
        }
      ])
    })
  }


() => {
      const ast = baseParse('<!---->')
      const comment = ast.children[0] as CommentNode

      expect(comment).toStrictEqual({
        type: NodeTypes.COMMENT,
        content: '',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 7, line: 1, column: 8 },
          source: '<!---->'
        }
      })
    }


() => {
      const ast = baseParse('<!--abc-->')
      const comment = ast.children[0] as CommentNode

      expect(comment).toStrictEqual({
        type: NodeTypes.COMMENT,
        content: 'abc',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 10, line: 1, column: 11 },
          source: '<!--abc-->'
        }
      })
    }


() => {
      const ast = baseParse('<!--abc--><!--def-->')
      const comment1 = ast.children[0] as CommentNode
      const comment2 = ast.children[1] as CommentNode

      expect(comment1).toStrictEqual({
        type: NodeTypes.COMMENT,
        content: 'abc',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 10, line: 1, column: 11 },
          source: '<!--abc-->'
        }
      })
      expect(comment2).toStrictEqual({
        type: NodeTypes.COMMENT,
        content: 'def',
        loc: {
          start: { offset: 10, line: 1, column: 11 },
          end: { offset: 20, line: 1, column: 21 },
          source: '<!--def-->'
        }
      })
    }


() => {
      __DEV__ = false
      const astDefaultComment = baseParse('<!--abc-->')
      const astNoComment = baseParse('<!--abc-->', { comments: false })
      const astWithComments = baseParse('<!--abc-->', { comments: true })
      __DEV__ = true

      expect(astDefaultComment.children).toHaveLength(0)
      expect(astNoComment.children).toHaveLength(0)
      expect(astWithComments.children).toHaveLength(1)
    }


() => {
      __DEV__ = false
      const rawText = `<p/><!-- foo --><p/>`
      const ast = baseParse(`<pre>${rawText}</pre>`)
      __DEV__ = true

      expect((ast.children[0] as ElementNode).children).toMatchObject([
        {
          type: NodeTypes.ELEMENT,
          tag: 'p'
        },
        {
          type: NodeTypes.ELEMENT,
          tag: 'p'
        }
      ])
    }


() => {
      const ast = baseParse('
hello
')
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [],
        isSelfClosing: false,
        children: [
          {
            type: NodeTypes.TEXT,
            content: 'hello',
            loc: {
              start: { offset: 5, line: 1, column: 6 },
              end: { offset: 10, line: 1, column: 11 },
              source: 'hello'
            }
          }
        ],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 16, line: 1, column: 17 },
          source: '
hello
'
        }
      })
    }


() => {
      const ast = baseParse('

')
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [],
        isSelfClosing: false,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 11, line: 1, column: 12 },
          source: '

'
        }
      })
    }


() => {
      const ast = baseParse('<div/>after')
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [],

        isSelfClosing: true,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 6, line: 1, column: 7 },
          source: '<div/>'
        }
      })
    }


() => {
      const ast = baseParse('<img>after', {
        isVoidTag: tag => tag === 'img'
      })
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'img',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [],

        isSelfClosing: false,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 5, line: 1, column: 6 },
          source: '<img>'
        }
      })
    }


tag => tag === 'img'


() => {
      const ast = baseParse('<template v-if="ok"></template>')
      const element = ast.children[0]
      expect(element).toMatchObject({
        type: NodeTypes.ELEMENT,
        tagType: ElementTypes.TEMPLATE
      })
    }


() => {
      const ast = baseParse('<template></template>')
      const element = ast.children[0]
      expect(element).toMatchObject({
        type: NodeTypes.ELEMENT,
        tagType: ElementTypes.ELEMENT
      })
    }


() => {
      const ast = baseParse('

<comp></comp><Comp></Comp>', {
        isNativeTag: tag => tag === 'div'
      })

      expect(ast.children[0]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'div',
        tagType: ElementTypes.ELEMENT
      })

      expect(ast.children[1]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'comp',
        tagType: ElementTypes.COMPONENT
      })

      expect(ast.children[2]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'Comp',
        tagType: ElementTypes.COMPONENT
      })
    }


tag => tag === 'div'


() => {
      const ast = baseParse('

<comp></comp><Comp></Comp>')

      expect(ast.children[0]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'div',
        tagType: ElementTypes.ELEMENT
      })

      expect(ast.children[1]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'comp',
        tagType: ElementTypes.ELEMENT
      })

      expect(ast.children[2]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'Comp',
        tagType: ElementTypes.COMPONENT
      })
    }


() => {
      const ast = baseParse(
        `<div></div><div v-is="'foo'"></div><Comp></Comp>`,
        {
          isNativeTag: tag => tag === 'div'
        }
      )

      expect(ast.children[0]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'div',
        tagType: ElementTypes.ELEMENT
      })

      expect(ast.children[1]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'div',
        tagType: ElementTypes.COMPONENT
      })

      expect(ast.children[2]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'Comp',
        tagType: ElementTypes.COMPONENT
      })
    }


() => {
      const ast = baseParse(`<div></div><div v-is="'foo'"></div><Comp></Comp>`)

      expect(ast.children[0]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'div',
        tagType: ElementTypes.ELEMENT
      })

      expect(ast.children[1]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'div',
        tagType: ElementTypes.COMPONENT
      })

      expect(ast.children[2]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'Comp',
        tagType: ElementTypes.COMPONENT
      })
    }


() => {
      const ast = baseParse('

<comp></comp>', {
        isNativeTag: tag => tag === 'div',
        isCustomElement: tag => tag === 'comp'
      })

      expect(ast.children[0]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'div',
        tagType: ElementTypes.ELEMENT
      })

      expect(ast.children[1]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'comp',
        tagType: ElementTypes.ELEMENT
      })
    }


tag => tag === 'comp'


() => {
      const ast = baseParse('

<comp></comp>', {
        isBuiltInComponent: tag => (tag === 'comp' ? Symbol() : void 0)
      })

      expect(ast.children[0]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'div',
        tagType: ElementTypes.ELEMENT
      })

      expect(ast.children[1]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'comp',
        tagType: ElementTypes.COMPONENT
      })
    }


tag => (tag === 'comp' ? Symbol() : void 0)


() => {
      const ast = baseParse('<slot></slot><Comp></Comp>')

      expect(ast.children[0]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'slot',
        tagType: ElementTypes.SLOT
      })

      expect(ast.children[1]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tag: 'Comp',
        tagType: ElementTypes.COMPONENT
      })
    }


() => {
      const ast = baseParse('

')
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'id',
            value: undefined,
            loc: {
              start: { offset: 5, line: 1, column: 6 },
              end: { offset: 7, line: 1, column: 8 },
              source: 'id'
            }
          }
        ],

        isSelfClosing: false,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 14, line: 1, column: 15 },
          source: '

'
        }
      })
    }


() => {
      const ast = baseParse('

')
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'id',
            value: {
              type: NodeTypes.TEXT,
              content: '',
              loc: {
                start: { offset: 8, line: 1, column: 9 },
                end: { offset: 10, line: 1, column: 11 },
                source: '""'
              }
            },
            loc: {
              start: { offset: 5, line: 1, column: 6 },
              end: { offset: 10, line: 1, column: 11 },
              source: 'id=""'
            }
          }
        ],

        isSelfClosing: false,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 17, line: 1, column: 18 },
          source: '

'
        }
      })
    }


() => {
      const ast = baseParse("

")
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'id',
            value: {
              type: NodeTypes.TEXT,
              content: '',
              loc: {
                start: { offset: 8, line: 1, column: 9 },
                end: { offset: 10, line: 1, column: 11 },
                source: "''"
              }
            },
            loc: {
              start: { offset: 5, line: 1, column: 6 },
              end: { offset: 10, line: 1, column: 11 },
              source: "id=''"
            }
          }
        ],

        isSelfClosing: false,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 17, line: 1, column: 18 },
          source: "

"
        }
      })
    }


() => {
      const ast = baseParse('
\'">
')
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'id',
            value: {
              type: NodeTypes.TEXT,
              content: ">'",
              loc: {
                start: { offset: 8, line: 1, column: 9 },
                end: { offset: 12, line: 1, column: 13 },
                source: '">\'"'
              }
            },
            loc: {
              start: { offset: 5, line: 1, column: 6 },
              end: { offset: 12, line: 1, column: 13 },
              source: 'id=">\'"'
            }
          }
        ],

        isSelfClosing: false,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 19, line: 1, column: 20 },
          source: '
\'">
'
        }
      })
    }


() => {
      const ast = baseParse("
\"'>
")
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'id',
            value: {
              type: NodeTypes.TEXT,
              content: '>"',
              loc: {
                start: { offset: 8, line: 1, column: 9 },
                end: { offset: 12, line: 1, column: 13 },
                source: "'>\"'"
              }
            },
            loc: {
              start: { offset: 5, line: 1, column: 6 },
              end: { offset: 12, line: 1, column: 13 },
              source: "id='>\"'"
            }
          }
        ],

        isSelfClosing: false,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 19, line: 1, column: 20 },
          source: "
\"'>
"
        }
      })
    }


() => {
      const ast = baseParse('

')
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'id',
            value: {
              type: NodeTypes.TEXT,
              content: 'a/',
              loc: {
                start: { offset: 8, line: 1, column: 9 },
                end: { offset: 10, line: 1, column: 11 },
                source: 'a/'
              }
            },
            loc: {
              start: { offset: 5, line: 1, column: 6 },
              end: { offset: 10, line: 1, column: 11 },
              source: 'id=a/'
            }
          }
        ],

        isSelfClosing: false,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 17, line: 1, column: 18 },
          source: '

'
        }
      })
    }


() => {
      const ast = baseParse('

')
      const element = ast.children[0] as ElementNode

      expect(element).toStrictEqual({
        type: NodeTypes.ELEMENT,
        ns: Namespaces.HTML,
        tag: 'div',
        tagType: ElementTypes.ELEMENT,
        codegenNode: undefined,
        props: [
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'id',
            value: {
              type: NodeTypes.TEXT,
              content: 'a',
              loc: {
                start: { offset: 8, line: 1, column: 9 },
                end: { offset: 9, line: 1, column: 10 },
                source: 'a'
              }
            },
            loc: {
              start: { offset: 5, line: 1, column: 6 },
              end: { offset: 9, line: 1, column: 10 },
              source: 'id=a'
            }
          },
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'class',
            value: {
              type: NodeTypes.TEXT,
              content: 'c',
              loc: {
                start: { offset: 16, line: 1, column: 17 },
                end: { offset: 19, line: 1, column: 20 },
                source: '"c"'
              }
            },
            loc: {
              start: { offset: 10, line: 1, column: 11 },
              end: { offset: 19, line: 1, column: 20 },
              source: 'class="c"'
            }
          },
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'inert',
            value: undefined,
            loc: {
              start: { offset: 20, line: 1, column: 21 },
              end: { offset: 25, line: 1, column: 26 },
              source: 'inert'
            }
          },
          {
            type: NodeTypes.ATTRIBUTE,
            name: 'style',
            value: {
              type: NodeTypes.TEXT,
              content: '',
              loc: {
                start: { offset: 32, line: 1, column: 33 },
                end: { offset: 34, line: 1, column: 35 },
                source: "''"
              }
            },
            loc: {
              start: { offset: 26, line: 1, column: 27 },
              end: { offset: 34, line: 1, column: 35 },
              source: "style=''"
            }
          }
        ],

        isSelfClosing: false,
        children: [],
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 41, line: 1, column: 42 },
          source: '

'
        }
      })
    }


() => {
      const ast = baseParse('<div v-if/>')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'if',
        arg: undefined,
        modifiers: [],
        exp: undefined,
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 9, line: 1, column: 10 },
          source: 'v-if'
        }
      })
    }


() => {
      const ast = baseParse('<div v-if="a"/>')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'if',
        arg: undefined,
        modifiers: [],
        exp: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'a',
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 11, line: 1, column: 12 },
            end: { offset: 12, line: 1, column: 13 },
            source: 'a'
          }
        },
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 13, line: 1, column: 14 },
          source: 'v-if="a"'
        }
      })
    }


() => {
      const ast = baseParse('<div v-on:click/>')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'on',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'click',
          isStatic: true,
          constType: ConstantTypes.CAN_STRINGIFY,

          loc: {
            source: 'click',
            start: {
              column: 11,
              line: 1,
              offset: 10
            },
            end: {
              column: 16,
              line: 1,
              offset: 15
            }
          }
        },
        modifiers: [],
        exp: undefined,
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 15, line: 1, column: 16 },
          source: 'v-on:click'
        }
      })
    }


() => {
      const ast = baseParse('<div v-on:[event]/>')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'on',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'event',
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,

          loc: {
            source: '[event]',
            start: {
              column: 11,
              line: 1,
              offset: 10
            },
            end: {
              column: 18,
              line: 1,
              offset: 17
            }
          }
        },
        modifiers: [],
        exp: undefined,
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 17, line: 1, column: 18 },
          source: 'v-on:[event]'
        }
      })
    }


() => {
      const ast = baseParse('<div v-on.enter/>')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'on',
        arg: undefined,
        modifiers: ['enter'],
        exp: undefined,
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 15, line: 1, column: 16 },
          source: 'v-on.enter'
        }
      })
    }


() => {
      const ast = baseParse('<div v-on.enter.exact/>')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'on',
        arg: undefined,
        modifiers: ['enter', 'exact'],
        exp: undefined,
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 21, line: 1, column: 22 },
          source: 'v-on.enter.exact'
        }
      })
    }


() => {
      const ast = baseParse('<div v-on:click.enter.exact/>')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'on',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'click',
          isStatic: true,
          constType: ConstantTypes.CAN_STRINGIFY,

          loc: {
            source: 'click',
            start: {
              column: 11,
              line: 1,
              offset: 10
            },
            end: {
              column: 16,
              line: 1,
              offset: 15
            }
          }
        },
        modifiers: ['enter', 'exact'],
        exp: undefined,
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 27, line: 1, column: 28 },
          source: 'v-on:click.enter.exact'
        }
      })
    }


() => {
      const ast = baseParse('<div v-on:[a.b].camel/>')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'on',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'a.b',
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,

          loc: {
            source: '[a.b]',
            start: {
              column: 11,
              line: 1,
              offset: 10
            },
            end: {
              column: 16,
              line: 1,
              offset: 15
            }
          }
        },
        modifiers: ['camel'],
        exp: undefined,
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 21, line: 1, column: 22 },
          source: 'v-on:[a.b].camel'
        }
      })
    }


() => {
      const ast = baseParse('<div :a=b />')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'bind',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'a',
          isStatic: true,
          constType: ConstantTypes.CAN_STRINGIFY,

          loc: {
            source: 'a',
            start: {
              column: 7,
              line: 1,
              offset: 6
            },
            end: {
              column: 8,
              line: 1,
              offset: 7
            }
          }
        },
        modifiers: [],
        exp: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'b',
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,

          loc: {
            start: { offset: 8, line: 1, column: 9 },
            end: { offset: 9, line: 1, column: 10 },
            source: 'b'
          }
        },
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 9, line: 1, column: 10 },
          source: ':a=b'
        }
      })
    }


() => {
      const ast = baseParse('<div :a.sync=b />')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'bind',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'a',
          isStatic: true,
          constType: ConstantTypes.CAN_STRINGIFY,

          loc: {
            source: 'a',
            start: {
              column: 7,
              line: 1,
              offset: 6
            },
            end: {
              column: 8,
              line: 1,
              offset: 7
            }
          }
        },
        modifiers: ['sync'],
        exp: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'b',
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,

          loc: {
            start: { offset: 13, line: 1, column: 14 },
            end: { offset: 14, line: 1, column: 15 },
            source: 'b'
          }
        },
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 14, line: 1, column: 15 },
          source: ':a.sync=b'
        }
      })
    }


() => {
      const ast = baseParse('<div @a=b />')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'on',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'a',
          isStatic: true,
          constType: ConstantTypes.CAN_STRINGIFY,

          loc: {
            source: 'a',
            start: {
              column: 7,
              line: 1,
              offset: 6
            },
            end: {
              column: 8,
              line: 1,
              offset: 7
            }
          }
        },
        modifiers: [],
        exp: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'b',
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,

          loc: {
            start: { offset: 8, line: 1, column: 9 },
            end: { offset: 9, line: 1, column: 10 },
            source: 'b'
          }
        },
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 9, line: 1, column: 10 },
          source: '@a=b'
        }
      })
    }


() => {
      const ast = baseParse('<div @a.enter=b />')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'on',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'a',
          isStatic: true,
          constType: ConstantTypes.CAN_STRINGIFY,

          loc: {
            source: 'a',
            start: {
              column: 7,
              line: 1,
              offset: 6
            },
            end: {
              column: 8,
              line: 1,
              offset: 7
            }
          }
        },
        modifiers: ['enter'],
        exp: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'b',
          isStatic: false,
          constType: ConstantTypes.NOT_CONSTANT,

          loc: {
            start: { offset: 14, line: 1, column: 15 },
            end: { offset: 15, line: 1, column: 16 },
            source: 'b'
          }
        },
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 15, line: 1, column: 16 },
          source: '@a.enter=b'
        }
      })
    }


() => {
      const ast = baseParse('<Comp #a="{ b }" />')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toStrictEqual({
        type: NodeTypes.DIRECTIVE,
        name: 'slot',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'a',
          isStatic: true,
          constType: ConstantTypes.CAN_STRINGIFY,
          loc: {
            source: 'a',
            start: {
              column: 8,
              line: 1,
              offset: 7
            },
            end: {
              column: 9,
              line: 1,
              offset: 8
            }
          }
        },
        modifiers: [],
        exp: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: '{ b }',
          isStatic: false,
          // The `constType` is the default value and will be determined in transformExpression
          constType: ConstantTypes.NOT_CONSTANT,
          loc: {
            start: { offset: 10, line: 1, column: 11 },
            end: { offset: 15, line: 1, column: 16 },
            source: '{ b }'
          }
        },
        loc: {
          start: { offset: 6, line: 1, column: 7 },
          end: { offset: 16, line: 1, column: 17 },
          source: '#a="{ b }"'
        }
      })
    }


() => {
      const ast = baseParse('<Comp v-slot:foo.bar="{ a }" />')
      const directive = (ast.children[0] as ElementNode).props[0]

      expect(directive).toMatchObject({
        type: NodeTypes.DIRECTIVE,
        name: 'slot',
        arg: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: 'foo.bar',
          isStatic: true,
          constType: ConstantTypes.CAN_STRINGIFY,
          loc: {
            source: 'foo.bar',
            start: {
              column: 14,
              line: 1,
              offset: 13
            },
            end: {
              column: 21,
              line: 1,
              offset: 20
            }
          }
        }
      })
    }


() => {
      const ast = baseParse(
        `<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` +
          `<div :id="foo"><Comp/>{{ bar }}</div>`
      )

      const divWithPre = ast.children[0] as ElementNode
      expect(divWithPre.props).toMatchObject([
        {
          type: NodeTypes.ATTRIBUTE,
          name: `:id`,
          value: {
            type: NodeTypes.TEXT,
            content: `foo`
          },
          loc: {
            source: `:id="foo"`,
            start: {
              line: 1,
              column: 12
            },
            end: {
              line: 1,
              column: 21
            }
          }
        }
      ])
      expect(divWithPre.children[0]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tagType: ElementTypes.ELEMENT,
        tag: `Comp`
      })
      expect(divWithPre.children[1]).toMatchObject({
        type: NodeTypes.TEXT,
        content: `{{ bar }}`
      })

      // should not affect siblings after it
      const divWithoutPre = ast.children[1] as ElementNode
      expect(divWithoutPre.props).toMatchObject([
        {
          type: NodeTypes.DIRECTIVE,
          name: `bind`,
          arg: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            isStatic: true,
            content: `id`
          },
          exp: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            isStatic: false,
            content: `foo`
          },
          loc: {
            source: `:id="foo"`,
            start: {
              line: 2,
              column: 6
            },
            end: {
              line: 2,
              column: 15
            }
          }
        }
      ])
      expect(divWithoutPre.children[0]).toMatchObject({
        type: NodeTypes.ELEMENT,
        tagType: ElementTypes.COMPONENT,
        tag: `Comp`
      })
      expect(divWithoutPre.children[1]).toMatchObject({
        type: NodeTypes.INTERPOLATION,
        content: {
          type: NodeTypes.SIMPLE_EXPRESSION,
          content: `bar`,
          isStatic: false
        }
      })
    }


() => {
      const ast = baseParse('
hello
after')
      const element = ast.children[0] as ElementNode
      const text = element.children[0] as TextNode

      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'hello',
        loc: {
          start: { offset: 5, line: 1, column: 6 },
          end: { offset: 10, line: 1, column: 11 },
          source: 'hello'
        }
      })
    }


() => {
    const ast = baseParse('<div :class="{ some: condition }" />')

    expect(ast.children).toHaveLength(1)
    expect(ast.children[0]).toMatchObject({ tag: 'div' })
  }


() => {
    const ast = baseParse(
      `<div :class="{ some: condition }" />\n` +
        `<p v-bind:style="{ color: 'red' }"/>`
    )

    expect(ast).toMatchSnapshot()

    expect(ast.children).toHaveLength(2)
    expect(ast.children[0]).toMatchObject({ tag: 'div' })
    expect(ast.children[1]).toMatchObject({ tag: 'p' })
  }


() => {
    const ast = baseParse(
      `<div :class="{ some: condition }">\n` +
        `  <p v-bind:style="{ color: 'red' }"/>\n` +
        `  <!-- a comment with <html> inside it -->\n` +
        `</div>`
    )

    expect(ast).toMatchSnapshot()

    expect(ast.children).toHaveLength(1)
    const el = ast.children[0] as any
    expect(el).toMatchObject({
      tag: 'div'
    })
    expect(el.children).toHaveLength(2)
    expect(el.children[0]).toMatchObject({
      tag: 'p'
    })
    expect(el.children[1]).toMatchObject({
      type: NodeTypes.COMMENT
    })
  }


() => {
    expect(() => {
      baseParse(`<div>\n<span>\n</div>\n</span>`)
    }).toThrow('Element is missing end tag.')

    const spy = jest.fn()
    const ast = baseParse(`<div>\n<span>\n</div>\n</span>`, {
      onError: spy
    })

    expect(spy.mock.calls).toMatchObject([
      [
        {
          code: ErrorCodes.X_MISSING_END_TAG,
          loc: {
            start: {
              offset: 6,
              line: 2,
              column: 1
            }
          }
        }
      ],
      [
        {
          code: ErrorCodes.X_INVALID_END_TAG,
          loc: {
            start: {
              offset: 20,
              line: 4,
              column: 1
            }
          }
        }
      ]
    ])

    expect(ast).toMatchSnapshot()
  }


() => {
      baseParse(`<div>\n<span>\n</div>\n</span>`)
    }


() => {
    const [foo, bar, but, baz] = baseParse(
      `
foo
 is {{ bar }} but {{ baz }}`.trim()
    ).children

    let offset = 0
    expect(foo.loc.start).toEqual({ line: 1, column: 1, offset })
    offset += foo.loc.source.length
    expect(foo.loc.end).toEqual({ line: 2, column: 5, offset })

    expect(bar.loc.start).toEqual({ line: 2, column: 5, offset })
    const barInner = (bar as InterpolationNode).content
    offset += 3
    expect(barInner.loc.start).toEqual({ line: 2, column: 8, offset })
    offset += barInner.loc.source.length
    expect(barInner.loc.end).toEqual({ line: 2, column: 11, offset })
    offset += 3
    expect(bar.loc.end).toEqual({ line: 2, column: 14, offset })

    expect(but.loc.start).toEqual({ line: 2, column: 14, offset })
    offset += but.loc.source.length
    expect(but.loc.end).toEqual({ line: 2, column: 19, offset })

    expect(baz.loc.start).toEqual({ line: 2, column: 19, offset })
    const bazInner = (baz as InterpolationNode).content
    offset += 3
    expect(bazInner.loc.start).toEqual({ line: 2, column: 22, offset })
    offset += bazInner.loc.source.length
    expect(bazInner.loc.end).toEqual({ line: 2, column: 25, offset })
    offset += 3
    expect(baz.loc.end).toEqual({ line: 2, column: 28, offset })
  }


() => {
    test('use default map', () => {
      const ast: any = baseParse('><&'"&foo;')

      expect(ast.children.length).toBe(1)
      expect(ast.children[0].type).toBe(NodeTypes.TEXT)
      expect(ast.children[0].content).toBe('><&\'"&foo;')
    })

    test('use the given map', () => {
      const ast: any = baseParse('&∪', {
        decodeEntities: text => text.replace('∪', '\u222A\uFE00'),
        onError: () => {} // Ignore errors
      })

      expect(ast.children.length).toBe(1)
      expect(ast.children[0].type).toBe(NodeTypes.TEXT)
      expect(ast.children[0].content).toBe('&\u222A\uFE00')
    })
  }


() => {
      const ast: any = baseParse('><&'"&foo;')

      expect(ast.children.length).toBe(1)
      expect(ast.children[0].type).toBe(NodeTypes.TEXT)
      expect(ast.children[0].content).toBe('><&\'"&foo;')
    }


() => {
      const ast: any = baseParse('&∪', {
        decodeEntities: text => text.replace('∪', '\u222A\uFE00'),
        onError: () => {} // Ignore errors
      })

      expect(ast.children.length).toBe(1)
      expect(ast.children[0].type).toBe(NodeTypes.TEXT)
      expect(ast.children[0].content).toBe('&\u222A\uFE00')
    }


text => text.replace('∪', '\u222A\uFE00')


() => {
    it('should remove whitespaces at start/end inside an element', () => {
      const ast = baseParse(`<div>   <span/>    </div>`)
      expect((ast.children[0] as ElementNode).children.length).toBe(1)
    })

    it('should remove whitespaces w/ newline between elements', () => {
      const ast = baseParse(`<div/> \n <div/> \n <div/>`)
      expect(ast.children.length).toBe(3)
      expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true)
    })

    it('should remove whitespaces adjacent to comments', () => {
      const ast = baseParse(`<div/> \n <!--foo--> <div/>`)
      expect(ast.children.length).toBe(3)
      expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
      expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
      expect(ast.children[2].type).toBe(NodeTypes.ELEMENT)
    })

    it('should remove whitespaces w/ newline between comments and elements', () => {
      const ast = baseParse(`<div/> \n <!--foo--> \n <div/>`)
      expect(ast.children.length).toBe(3)
      expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
      expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
      expect(ast.children[2].type).toBe(NodeTypes.ELEMENT)
    })

    it('should NOT remove whitespaces w/ newline between interpolations', () => {
      const ast = baseParse(`{{ foo }} \n {{ bar }}`)
      expect(ast.children.length).toBe(3)
      expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
      expect(ast.children[1]).toMatchObject({
        type: NodeTypes.TEXT,
        content: ' '
      })
      expect(ast.children[2].type).toBe(NodeTypes.INTERPOLATION)
    })

    it('should NOT remove whitespaces w/o newline between elements', () => {
      const ast = baseParse(`<div/> <div/> <div/>`)
      expect(ast.children.length).toBe(5)
      expect(ast.children.map(c => c.type)).toMatchObject([
        NodeTypes.ELEMENT,
        NodeTypes.TEXT,
        NodeTypes.ELEMENT,
        NodeTypes.TEXT,
        NodeTypes.ELEMENT
      ])
    })

    it('should condense consecutive whitespaces in text', () => {
      const ast = baseParse(`   foo  \n    bar     baz     `)
      expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `)
    })

    it('should remove leading newline character immediately following the pre element start tag', () => {
      const ast = baseParse(`<pre>\n  foo  bar  </pre>`, {
        isPreTag: tag => tag === 'pre'
      })
      expect(ast.children).toHaveLength(1)
      const preElement = ast.children[0] as ElementNode
      expect(preElement.children).toHaveLength(1)
      expect((preElement.children[0] as TextNode).content).toBe(`  foo  bar  `)
    })

    it('should NOT remove leading newline character immediately following child-tag of pre element', () => {
      const ast = baseParse(`<pre><span></span>\n  foo  bar  </pre>`, {
        isPreTag: tag => tag === 'pre'
      })
      const preElement = ast.children[0] as ElementNode
      expect(preElement.children).toHaveLength(2)
      expect((preElement.children[1] as TextNode).content).toBe(
        `\n  foo  bar  `
      )
    })

    it('should NOT condense whitespaces in RCDATA text mode', () => {
      const ast = baseParse(`<textarea>Text:\n   foo</textarea>`, {
        getTextMode: ({ tag }) =>
          tag === 'textarea' ? TextModes.RCDATA : TextModes.DATA
      })
      const preElement = ast.children[0] as ElementNode
      expect(preElement.children).toHaveLength(1)
      expect((preElement.children[0] as TextNode).content).toBe(`Text:\n   foo`)
    })
  }


() => {
      const ast = baseParse(`<div>   <span/>    </div>`)
      expect((ast.children[0] as ElementNode).children.length).toBe(1)
    }


() => {
      const ast = baseParse(`<div/> \n <div/> \n <div/>`)
      expect(ast.children.length).toBe(3)
      expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true)
    }


c => c.type === NodeTypes.ELEMENT


() => {
      const ast = baseParse(`<div/> \n <!--foo--> <div/>`)
      expect(ast.children.length).toBe(3)
      expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
      expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
      expect(ast.children[2].type).toBe(NodeTypes.ELEMENT)
    }


() => {
      const ast = baseParse(`<div/> \n <!--foo--> \n <div/>`)
      expect(ast.children.length).toBe(3)
      expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
      expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
      expect(ast.children[2].type).toBe(NodeTypes.ELEMENT)
    }


() => {
      const ast = baseParse(`{{ foo }} \n {{ bar }}`)
      expect(ast.children.length).toBe(3)
      expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
      expect(ast.children[1]).toMatchObject({
        type: NodeTypes.TEXT,
        content: ' '
      })
      expect(ast.children[2].type).toBe(NodeTypes.INTERPOLATION)
    }


() => {
      const ast = baseParse(`<div/> <div/> <div/>`)
      expect(ast.children.length).toBe(5)
      expect(ast.children.map(c => c.type)).toMatchObject([
        NodeTypes.ELEMENT,
        NodeTypes.TEXT,
        NodeTypes.ELEMENT,
        NodeTypes.TEXT,
        NodeTypes.ELEMENT
      ])
    }


c => c.type


() => {
      const ast = baseParse(`   foo  \n    bar     baz     `)
      expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `)
    }


() => {
      const ast = baseParse(`<pre>\n  foo  bar  </pre>`, {
        isPreTag: tag => tag === 'pre'
      })
      expect(ast.children).toHaveLength(1)
      const preElement = ast.children[0] as ElementNode
      expect(preElement.children).toHaveLength(1)
      expect((preElement.children[0] as TextNode).content).toBe(`  foo  bar  `)
    }


tag => tag === 'pre'


() => {
      const ast = baseParse(`<pre><span></span>\n  foo  bar  </pre>`, {
        isPreTag: tag => tag === 'pre'
      })
      const preElement = ast.children[0] as ElementNode
      expect(preElement.children).toHaveLength(2)
      expect((preElement.children[1] as TextNode).content).toBe(
        `\n  foo  bar  `
      )
    }


() => {
      const ast = baseParse(`<textarea>Text:\n   foo</textarea>`, {
        getTextMode: ({ tag }) =>
          tag === 'textarea' ? TextModes.RCDATA : TextModes.DATA
      })
      const preElement = ast.children[0] as ElementNode
      expect(preElement.children).toHaveLength(1)
      expect((preElement.children[0] as TextNode).content).toBe(`Text:\n   foo`)
    }


({ tag }) =>
          tag === 'textarea' ? TextModes.RCDATA : TextModes.DATA


() => {
        for (const { code, errors, options } of patterns[key]) {
          test(
            code.replace(
              /[\r\n]/g,
              c => `\\x0${c.codePointAt(0)!.toString(16)};`
            ),
            () => {
              const spy = jest.fn()
              const ast = baseParse(code, {
                getNamespace: (tag, parent) => {
                  const ns = parent ? parent.ns : Namespaces.HTML
                  if (ns === Namespaces.HTML) {
                    if (tag === 'svg') {
                      return (Namespaces.HTML + 1) as any
                    }
                  }
                  return ns
                },
                getTextMode: ({ tag }) => {
                  if (tag === 'textarea') {
                    return TextModes.RCDATA
                  }
                  if (tag === 'script') {
                    return TextModes.RAWTEXT
                  }
                  return TextModes.DATA
                },
                ...options,
                onError: spy
              })

              expect(
                spy.mock.calls.map(([err]) => ({
                  type: err.code,
                  loc: err.loc.start
                }))
              ).toMatchObject(errors)
              expect(ast).toMatchSnapshot()
            }
          )
        }
      }


c => `\\x0${c.codePointAt(0)!.toString(16)};`


() => {
              const spy = jest.fn()
              const ast = baseParse(code, {
                getNamespace: (tag, parent) => {
                  const ns = parent ? parent.ns : Namespaces.HTML
                  if (ns === Namespaces.HTML) {
                    if (tag === 'svg') {
                      return (Namespaces.HTML + 1) as any
                    }
                  }
                  return ns
                },
                getTextMode: ({ tag }) => {
                  if (tag === 'textarea') {
                    return TextModes.RCDATA
                  }
                  if (tag === 'script') {
                    return TextModes.RAWTEXT
                  }
                  return TextModes.DATA
                },
                ...options,
                onError: spy
              })

              expect(
                spy.mock.calls.map(([err]) => ({
                  type: err.code,
                  loc: err.loc.start
                }))
              ).toMatchObject(errors)
              expect(ast).toMatchSnapshot()
            }


(tag, parent) => {
                  const ns = parent ? parent.ns : Namespaces.HTML
                  if (ns === Namespaces.HTML) {
                    if (tag === 'svg') {
                      return (Namespaces.HTML + 1) as any
                    }
                  }
                  return ns
                }


({ tag }) => {
                  if (tag === 'textarea') {
                    return TextModes.RCDATA
                  }
                  if (tag === 'script') {
                    return TextModes.RAWTEXT
                  }
                  return TextModes.DATA
                }


([err]) => ({
                  type: err.code,
                  loc: err.loc.start
                })


function parse(text: string) {
    let { tokens } = Lexer.scan(text);
    return Parser.parse(tokens);
}


export function locationToArray(location: Location) {
    return [
        location.start.line,
        location.start.column,
        location.end.line,
        location.end.column
    ];
}


() => {
        expect(Parser.parse([])).to.deep.include({
            statements: [],
            errors: []
        });
    }


() => {
        expect(parse(`
            sub main()
                call()a
            end sub
        `).errors.map(x => locationToArray(x.location))).to.eql([
            [3, 22, 3, 23]
        ]);
    }


x => locationToArray(x.location)


() => {
            let { statements, errors } = parse(`
                function log() as UNKNOWN_TYPE
                end function
            `);
            expect(errors.length).to.be.greaterThan(0);
            expect(statements[0]).to.exist;
        }


() => {
            expect(parse(`
                function printNumber()
                    if true then
                        print 1
                    else if true
                        print 2
                    end if
                end function
            `).errors[0]?.message).not.to.exist;
        }


() => {
            expect(parse(`If true Then print "error" : Stop`).errors[0]?.message).to.not.exist;
        }


() => {
            let { tokens } = Lexer.scan(
                'function boolToNumber() as string\n\n' +
                '   if true then\n\n' +
                '       print 1\n\n' +
                '   elseif true then\n\n' +
                '       print 0\n\n' +
                '   else\n\n' +
                '       print 1\n\n' +
                '   end if\n\n' +
                'end function\n\n'
            );
            expect(Parser.parse(tokens).errors[0]?.message).to.not.exist;
        }


() => {
            let { tokens } = Lexer.scan(`
                sub main()
                    person.a
                end sub

            `);
            let { errors } = Parser.parse(tokens) as any;
            expect(errors).to.be.lengthOf(1, 'Error count should be 0');
        }


() => {
            let { tokens } = Lexer.scan(`print a.`);
            let { statements, errors } = Parser.parse(tokens);
            let printStatement = statements[0] as PrintStatement;
            expect(errors).to.be.empty;
            expect(printStatement).to.be.instanceof(PrintStatement);
            expect(printStatement.expressions[0]).to.be.instanceof(DottedGetExpression);
        }


() => {
                let { tokens } = Lexer.scan(`
                    'line 1
                    'line 2
                    'line 3
                `);
                let { errors, statements } = Parser.parse(tokens) as any;
                expect(errors).to.be.lengthOf(0, 'Error count should be 0');

                expect(statements[0].text).to.equal(`'line 1\n'line 2\n'line 3`);
            }


() => {
                let { tokens } = Lexer.scan(`
                    'line 1

                    'line 2

                    'line 3
                `);
                let { errors, statements } = Parser.parse(tokens) as any;
                expect(errors).to.be.lengthOf(0, 'Error count should be 0');

                expect(statements).to.be.lengthOf(3);

                expect(statements[0].text).to.equal(`'line 1`);
                expect(statements[1].text).to.equal(`'line 2`);
                expect(statements[2].text).to.equal(`'line 3`);
            }


() => {
                let { tokens } = Lexer.scan(`
                    sub main()
                        print "hi" 'comment 1
                    end sub
                `);
                let { errors, statements } = Parser.parse(tokens);
                expect(errors).to.be.lengthOf(0, 'Error count should be 0');

                expect((statements as any)[0].func.body.statements[1].text).to.equal(`'comment 1`);
            }


() => {
                let { tokens } = Lexer.scan(`
                    'comment 1
                    function a()
                    end function
                    'comment 2
                `);
                let { errors, statements } = Parser.parse(tokens);
                expect(errors).to.be.lengthOf(0, 'Error count should be 0');
                expect((statements as any)[0].text).to.equal(`'comment 1`);
                expect((statements as any)[2].text).to.equal(`'comment 2`);
            }


() => {
                let { tokens } = Lexer.scan(`
                    obj = {
                        "name": true,
                        'comment
                    }
                `);
                let { errors } = Parser.parse(tokens);
                expect(errors).to.be.lengthOf(0, 'Error count should be 0');
            }


() => {
                let { tokens } = Lexer.scan(`
                    sub Main()
                        name = "Hello"
                        DoSomething(name) 'comment 1
                    end sub
                `);
                let { errors, statements } = Parser.parse(tokens) as any;
                expect(errors).to.be.lengthOf(0, 'Should have zero errors');

                expect(statements[0].func.body.statements[2].text).to.equal(`'comment 1`);
            }


() => {
                let { tokens } = Lexer.scan(`
                    function a() 'comment 1
                        'comment 2
                        num = 1
                        'comment 3
                    end function 'comment 4
                `);
                let { errors, statements } = Parser.parse(tokens) as any;
                expect(errors).to.be.lengthOf(0, 'Should have zero errors');

                expect(statements[0].func.body.statements[0].text).to.equal(`'comment 1`);
                expect(statements[0].func.body.statements[1].text).to.equal(`'comment 2`);
                expect(statements[0].func.body.statements[3].text).to.equal(`'comment 3`);
                expect(statements[1].text).to.equal(`'comment 4`);
            }


() => {
                let { tokens } = Lexer.scan(`
                    function a()
                        if true then 'comment 1
                            'comment 2
                            print "hello"
                            'comment 3
                        else if true then 'comment 4
                            'comment 5
                            print "hello"
                            'comment 6
                        else 'comment 7
                            'comment 8
                            print "hello"
                            'comment 9
                        end if 'comment 10
                    end function
                `);
                let { errors, statements } = Parser.parse(tokens) as any;
                expect(errors).to.be.lengthOf(0, 'Should have zero errors');
                let ifStmt = statements[0].func.body.statements[0];

                expect(ifStmt.thenBranch.statements[0].text).to.equal(`'comment 1`);
                expect(ifStmt.thenBranch.statements[1].text).to.equal(`'comment 2`);
                expect(ifStmt.thenBranch.statements[3].text).to.equal(`'comment 3`);

                expect(ifStmt.elseIfs[0].thenBranch.statements[0].text).to.equal(`'comment 4`);
                expect(ifStmt.elseIfs[0].thenBranch.statements[1].text).to.equal(`'comment 5`);
                expect(ifStmt.elseIfs[0].thenBranch.statements[3].text).to.equal(`'comment 6`);

                expect(ifStmt.elseBranch.statements[0].text).to.equal(`'comment 7`);
                expect(ifStmt.elseBranch.statements[1].text).to.equal(`'comment 8`);
                expect(ifStmt.elseBranch.statements[3].text).to.equal(`'comment 9`);

                expect(statements[0].func.body.statements[1].text).to.equal(`'comment 10`);

            }


() => {
                let { tokens } = Lexer.scan(`
                    function a()
                        while true 'comment 1
                            'comment 2
                            print "true"
                            'comment 3
                        end while 'comment 4
                    end function
                `);
                let { errors, statements } = Parser.parse(tokens) as any;
                expect(errors).to.be.lengthOf(0, 'Error count should be zero');
                let stmt = statements[0].func.body.statements[0];

                expect(stmt.body.statements[0].text).to.equal(`'comment 1`);
                expect(stmt.body.statements[1].text).to.equal(`'comment 2`);
                expect(stmt.body.statements[3].text).to.equal(`'comment 3`);

                expect(statements[0].func.body.statements[1].text).to.equal(`'comment 4`);
            }


() => {
                let { tokens } = Lexer.scan(`
                    function a()
                        for i = 0 to 10 step 1 'comment 1
                            'comment 2
                            print 1
                            'comment 3
                        end for 'comment 4
                    end function
                `);
                let { errors, statements } = Parser.parse(tokens) as any;
                expect(errors).to.be.lengthOf(0, 'Error count should be zero');
                let stmt = statements[0].func.body.statements[0];

                expect(stmt.body.statements[0].text).to.equal(`'comment 1`);
                expect(stmt.body.statements[1].text).to.equal(`'comment 2`);
                expect(stmt.body.statements[3].text).to.equal(`'comment 3`);

                expect(statements[0].func.body.statements[1].text).to.equal(`'comment 4`);
            }


() => {
                let { tokens } = Lexer.scan(`
                    function a()
                        for each val in [1,2,3] 'comment 1
                            'comment 2
                            print 1
                            'comment 3
                        end for 'comment 4
                    end function
                `);
                let { errors, statements } = Parser.parse(tokens) as any;
                expect(errors).to.be.lengthOf(0, 'Error count should be zero');
                let stmt = statements[0].func.body.statements[0];

                expect(stmt.body.statements[0].text).to.equal(`'comment 1`);
                expect(stmt.body.statements[1].text).to.equal(`'comment 2`);
                expect(stmt.body.statements[3].text).to.equal(`'comment 3`);

                expect(statements[0].func.body.statements[1].text).to.equal(`'comment 4`);
            }


() => {
        it('emits events', () => {
            let parser = new Parser();
            let count = 0;
            let handler = parser.onError(() => {
                count++;
            });
            parser.parse(Lexer.scan('function').tokens);
            parser.parse(Lexer.scan('function').tokens);
            expect(count).to.equal(2);
            //disposing the listener stops new counts
            handler.dispose();
            parser.parse(Lexer.scan('function').tokens);
            expect(count).to.equal(2);
        });
        describe('onErrorOnce', () => {
            it('stops emitting after first error', () => {
                let parser = new Parser();
                let count = 0;
                parser.onErrorOnce(() => {
                    count++;
                });
                parser.parse(Lexer.scan('function').tokens);
                parser.parse(Lexer.scan('function').tokens);
                expect(count).to.equal(1);
            });
        });
    }


() => {
            let parser = new Parser();
            let count = 0;
            let handler = parser.onError(() => {
                count++;
            });
            parser.parse(Lexer.scan('function').tokens);
            parser.parse(Lexer.scan('function').tokens);
            expect(count).to.equal(2);
            //disposing the listener stops new counts
            handler.dispose();
            parser.parse(Lexer.scan('function').tokens);
            expect(count).to.equal(2);
        }


() => {
                count++;
            }


() => {
            it('stops emitting after first error', () => {
                let parser = new Parser();
                let count = 0;
                parser.onErrorOnce(() => {
                    count++;
                });
                parser.parse(Lexer.scan('function').tokens);
                parser.parse(Lexer.scan('function').tokens);
                expect(count).to.equal(1);
            });
        }


() => {
                let parser = new Parser();
                let count = 0;
                parser.onErrorOnce(() => {
                    count++;
                });
                parser.parse(Lexer.scan('function').tokens);
                parser.parse(Lexer.scan('function').tokens);
                expect(count).to.equal(1);
            }


() => {
                    count++;
                }


() => {
        describe('`then`', () => {
            it('is not allowed as a local identifier', () => {
                let { errors } = parse(`
                    sub main()
                        then = true
                    end sub
                `);
                expect(errors).to.be.lengthOf(1);
            });
            it('is allowed as an AA property name', () => {
                let { errors } = parse(`
                    sub main()
                        person = {
                            then: true
                        }
                        person.then = false
                        print person.then
                    end sub
                `);
                expect(errors[0]?.message).not.to.exist;
            });
        });
        it('"end" is not allowed as a local identifier', () => {
            let { errors } = parse(`
                sub main()
                    end = true
                end sub
            `);
            expect(errors).to.be.lengthOf(1);
        });
        it('none of them can be used as local variables', () => {
            let reservedWords = new Set(ReservedWords);
            //remove the rem keyword because it's a comment...won't cause error
            reservedWords.delete('rem');
            for (let reservedWord of reservedWords) {
                let { tokens } = Lexer.scan(`
                    sub main()
                        ${reservedWord} = true
                    end sub
                `);
                let { errors } = Parser.parse(tokens);
                expect(errors, `assigning to reserved word "${reservedWord}" should have been an error`).to.be.length.greaterThan(0);
            }
        });
    }


() => {
            it('is not allowed as a local identifier', () => {
                let { errors } = parse(`
                    sub main()
                        then = true
                    end sub
                `);
                expect(errors).to.be.lengthOf(1);
            });
            it('is allowed as an AA property name', () => {
                let { errors } = parse(`
                    sub main()
                        person = {
                            then: true
                        }
                        person.then = false
                        print person.then
                    end sub
                `);
                expect(errors[0]?.message).not.to.exist;
            });
        }


() => {
                let { errors } = parse(`
                    sub main()
                        then = true
                    end sub
                `);
                expect(errors).to.be.lengthOf(1);
            }


() => {
                let { errors } = parse(`
                    sub main()
                        person = {
                            then: true
                        }
                        person.then = false
                        print person.then
                    end sub
                `);
                expect(errors[0]?.message).not.to.exist;
            }


() => {
            let { errors } = parse(`
                sub main()
                    end = true
                end sub
            `);
            expect(errors).to.be.lengthOf(1);
        }


() => {
            let reservedWords = new Set(ReservedWords);
            //remove the rem keyword because it's a comment...won't cause error
            reservedWords.delete('rem');
            for (let reservedWord of reservedWords) {
                let { tokens } = Lexer.scan(`
                    sub main()
                        ${reservedWord} = true
                    end sub
                `);
                let { errors } = Parser.parse(tokens);
                expect(errors, `assigning to reserved word "${reservedWord}" should have been an error`).to.be.length.greaterThan(0);
            }
        }


(resolve, reject) => {
      this.connectionResolver = resolve
      this.connectionRejecter = reject
      ipcRenderer.send('port:add', this.port)
    }


(_, addedPort) => {
    if (addedPort === this.port) {
      ipcRenderer.on('message', this.onMessage)
      this.connector.sendMessage(this.port, 'ping')
    }
  }


(_, port) => {
    if (port === this.port) {
      this.connectionRejecter(
        new Error('Something running on this port already')
      )
    }
  }


(_, message) => {
    if (message.port !== this.port) {
      return
    }

    if (message.type === 'ping') {
      this.connector.sendMessage(this.port, 'pong')
      return this.connectionResolver()
    }

    this.messageCallback(message)
  }


(portInstance) => portInstance.port === port


class Port {
  port: string
  messageCallback: MessageCallback
  connector: BackendConnector
  connectionResolver: Function
  connectionRejecter: Function
  constructor(
    connector: BackendConnector,
    port: string,
    messageCallback: MessageCallback
  ) {
    this.port = port
    this.messageCallback = messageCallback
    this.connector = connector
    ipcRenderer.on('port:added', this.onPortAdded)
    ipcRenderer.on('port:exists', this.onPortExists)
  }
  connect() {
    return new Promise((resolve, reject) => {
      this.connectionResolver = resolve
      this.connectionRejecter = reject
      ipcRenderer.send('port:add', this.port)
    })
  }
  equals(port: string) {
    return this.port === port
  }
  onPortAdded = (_, addedPort) => {
    if (addedPort === this.port) {
      ipcRenderer.on('message', this.onMessage)
      this.connector.sendMessage(this.port, 'ping')
    }
  }
  onPortExists = (_, port) => {
    if (port === this.port) {
      this.connectionRejecter(
        new Error('Something running on this port already')
      )
    }
  }
  onMessage = (_, message) => {
    if (message.port !== this.port) {
      return
    }

    if (message.type === 'ping') {
      this.connector.sendMessage(this.port, 'pong')
      return this.connectionResolver()
    }

    this.messageCallback(message)
  }
  destroy() {
    ipcRenderer.removeListener('port:added', this.onPortAdded)
    ipcRenderer.removeListener('port:exists', this.onPortExists)
    ipcRenderer.removeListener('message', this.onMessage)
  }
}


class BackendConnector {
  addedPorts: Port[] = []
  getPort(port: string) {
    return this.addedPorts.find((portInstance) => portInstance.port === port)
  }
  sendMessage(port: string, eventName: string, payload: object = null) {
    ipcRenderer.send('message', {
      port,
      type: eventName,
      data: payload,
    })
  }
  addPort(port: string, messageCallback: MessageCallback) {
    if (this.addedPorts.find((portInstance) => portInstance.port === port)) {
      return
    }

    const portInstance = new Port(this, port, messageCallback)

    portInstance.connect()
    this.addedPorts.push(portInstance)
  }
  removePort(portToRemove: string) {
    const port = this.getPort(portToRemove)
    port.destroy()
    this.addedPorts.splice(this.addedPorts.indexOf(port), 1)
    ipcRenderer.send('port:remove', portToRemove)
  }
  relaunch() {
    ipcRenderer.send('relaunch')
  }
}


type Message = {
  type: string
  port: string
}


type MessageCallback = (error: Error, message?: Message) => void


connect() {
    return new Promise((resolve, reject) => {
      this.connectionResolver = resolve
      this.connectionRejecter = reject
      ipcRenderer.send('port:add', this.port)
    })
  }


equals(port: string) {
    return this.port === port
  }


destroy() {
    ipcRenderer.removeListener('port:added', this.onPortAdded)
    ipcRenderer.removeListener('port:exists', this.onPortExists)
    ipcRenderer.removeListener('message', this.onMessage)
  }


getPort(port: string) {
    return this.addedPorts.find((portInstance) => portInstance.port === port)
  }


sendMessage(port: string, eventName: string, payload: object = null) {
    ipcRenderer.send('message', {
      port,
      type: eventName,
      data: payload,
    })
  }


addPort(port: string, messageCallback: MessageCallback) {
    if (this.addedPorts.find((portInstance) => portInstance.port === port)) {
      return
    }

    const portInstance = new Port(this, port, messageCallback)

    portInstance.connect()
    this.addedPorts.push(portInstance)
  }


removePort(portToRemove: string) {
    const port = this.getPort(portToRemove)
    port.destroy()
    this.addedPorts.splice(this.addedPorts.indexOf(port), 1)
    ipcRenderer.send('port:remove', portToRemove)
  }


relaunch() {
    ipcRenderer.send('relaunch')
  }


([id, type, purpose]) => Object.freeze({ id, type, purpose })


(n) => isValid(n)


(u: UnspentType): CodesByPurpose => {
  // Do tcomb type checking in js projects that use this lib
  if (!UnspentTypeTcomb.is(u)) {
    throw new Error(`invalid unspent type: ${u}`);
  }

  return new CodesByPurpose(u);
}


(c: ChainCode): UnspentType => {
  const code = codeList.find(({ id }) => id === c);
  if (!code) {
    throw new ErrorInvalidCode(c);
  }
  return code.type;
}


({ id }) => id === c


(c: ChainCode): boolean => codeList.some(({ id }) => id === c)


<T>(v: T | undefined): T => {
  if (v === undefined) {
    throw new Error(`expected value to be defined`);
  }
  return v;
}


({ type }) => type === t


({ purpose, id }): [Purpose, ChainCode] => [purpose, id]


({ purpose }) => purpose === p


({ type, id }): [UnspentType, ChainCode] => [type, id]


(instance: CodeGroup) => instance.has.bind(instance)


export class ErrorInvalidCode extends Error {
  constructor(code: ChainCode) {
    super(`invalid code ${code}`);
  }
}


export class CodeGroup {
  public values: ReadonlyArray<ChainCode>;
  constructor(values: Iterable<ChainCode>) {
    this.values = Object.freeze([...values]);
  }

  public has(code: ChainCode): boolean {
    if (!isValid(code)) {
      throw new ErrorInvalidCode(code);
    }
    return this.values.includes(code);
  }
}


export class CodesByPurpose extends CodeGroup {
  public internal: ChainCode;
  public external: ChainCode;

  constructor(t: UnspentType) {
    const codeMap: Map<Purpose, ChainCode> = new Map(
      codeList.filter(({ type }) => type === t).map(({ purpose, id }): [Purpose, ChainCode] => [purpose, id])
    );
    if (codeMap.size !== 2) {
      throw new Error(`unexpected number of codes for type ${t}`);
    }

    super(codeMap.values());

    this.internal = throwIfUndefined(codeMap.get(Purpose.internal));
    this.external = throwIfUndefined(codeMap.get(Purpose.external));
  }
}


export class CodesByType extends CodeGroup {
  public p2sh: ChainCode;
  public p2shP2wsh: ChainCode;
  public p2wsh: ChainCode;
  public p2tr: ChainCode;

  constructor(p: Purpose) {
    const codeMap: Map<UnspentType, ChainCode> = new Map(
      codeList.filter(({ purpose }) => purpose === p).map(({ type, id }): [UnspentType, ChainCode] => [type, id])
    );
    if (codeMap.size !== 4) {
      throw new Error(`unexpected number of codes`);
    }

    super(codeMap.values());

    this.p2sh = throwIfUndefined(codeMap.get(UnspentType.p2sh));
    this.p2shP2wsh = throwIfUndefined(codeMap.get(UnspentType.p2shP2wsh));
    this.p2wsh = throwIfUndefined(codeMap.get(UnspentType.p2wsh));
    this.p2tr = throwIfUndefined(codeMap.get(UnspentType.p2tr));
  }
}


interface ICode {
  id: ChainCode;
  type: UnspentType;
  purpose: Purpose;
}


export enum UnspentType {
  p2pkh = 'p2pkh',
  p2sh = 'p2sh',
  p2shP2wsh = 'p2shP2wsh',
  p2wpkh = 'p2wpkh',
  p2wsh = 'p2wsh',
  p2tr = 'p2tr',
}


export enum Purpose {
  internal = 'internal',
  external = 'external',
}


export type ChainCode = number;


public has(code: ChainCode): boolean {
    if (!isValid(code)) {
      throw new ErrorInvalidCode(code);
    }
    return this.values.includes(code);
  }


@customElement("ha-cover-tilt-controls")
class HaCoverTiltControls extends LitElement {
  @property({ attribute: false }) public hass!: HomeAssistant;

  @property({ attribute: false }) stateObj!: HassEntity;

  @internalProperty() private _entityObj?: CoverEntity;

  protected updated(changedProperties: PropertyValues): void {
    super.updated(changedProperties);

    if (changedProperties.has("stateObj")) {
      this._entityObj = new CoverEntity(this.hass, this.stateObj);
    }
  }

  protected render(): TemplateResult {
    if (!this._entityObj) {
      return html``;
    }

    return html` <ha-icon-button
        class=${classMap({
          invisible: !this._entityObj.supportsOpenTilt,
        })}
        label=${this.hass.localize(
          "ui.dialogs.more_info_control.open_tilt_cover"
        )}
        icon="hass:arrow-top-right"
        @click=${this._onOpenTiltTap}
        .disabled=${this._computeOpenDisabled()}
      ></ha-icon-button>
      <ha-icon-button
        class=${classMap({
          invisible: !this._entityObj.supportsStop,
        })}
        label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
        icon="hass:stop"
        @click=${this._onStopTiltTap}
        .disabled=${this.stateObj.state === UNAVAILABLE}
      ></ha-icon-button>
      <ha-icon-button
        class=${classMap({
          invisible: !this._entityObj.supportsCloseTilt,
        })}
        label=${this.hass.localize(
          "ui.dialogs.more_info_control.close_tilt_cover"
        )}
        icon="hass:arrow-bottom-left"
        @click=${this._onCloseTiltTap}
        .disabled=${this._computeClosedDisabled()}
      ></ha-icon-button>`;
  }

  private _computeOpenDisabled(): boolean {
    if (this.stateObj.state === UNAVAILABLE) {
      return true;
    }
    const assumedState = this.stateObj.attributes.assumed_state === true;
    return this._entityObj.isFullyOpenTilt && !assumedState;
  }

  private _computeClosedDisabled(): boolean {
    if (this.stateObj.state === UNAVAILABLE) {
      return true;
    }
    const assumedState = this.stateObj.attributes.assumed_state === true;
    return this._entityObj.isFullyClosedTilt && !assumedState;
  }

  private _onOpenTiltTap(ev): void {
    ev.stopPropagation();
    this._entityObj.openCoverTilt();
  }

  private _onCloseTiltTap(ev): void {
    ev.stopPropagation();
    this._entityObj.closeCoverTilt();
  }

  private _onStopTiltTap(ev): void {
    ev.stopPropagation();
    this._entityObj.stopCoverTilt();
  }

  static get styles(): CSSResult {
    return css`
      :host {
        white-space: nowrap;
      }
      .invisible {
        visibility: hidden !important;
      }
    `;
  }
}


interface HTMLElementTagNameMap {
    "ha-cover-tilt-controls": HaCoverTiltControls;
  }


protected updated(changedProperties: PropertyValues): void {
    super.updated(changedProperties);

    if (changedProperties.has("stateObj")) {
      this._entityObj = new CoverEntity(this.hass, this.stateObj);
    }
  }


protected render(): TemplateResult {
    if (!this._entityObj) {
      return html``;
    }

    return html` <ha-icon-button
        class=${classMap({
          invisible: !this._entityObj.supportsOpenTilt,
        })}
        label=${this.hass.localize(
          "ui.dialogs.more_info_control.open_tilt_cover"
        )}
        icon="hass:arrow-top-right"
        @click=${this._onOpenTiltTap}
        .disabled=${this._computeOpenDisabled()}
      ></ha-icon-button>
      <ha-icon-button
        class=${classMap({
          invisible: !this._entityObj.supportsStop,
        })}
        label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
        icon="hass:stop"
        @click=${this._onStopTiltTap}
        .disabled=${this.stateObj.state === UNAVAILABLE}
      ></ha-icon-button>
      <ha-icon-button
        class=${classMap({
          invisible: !this._entityObj.supportsCloseTilt,
        })}
        label=${this.hass.localize(
          "ui.dialogs.more_info_control.close_tilt_cover"
        )}
        icon="hass:arrow-bottom-left"
        @click=${this._onCloseTiltTap}
        .disabled=${this._computeClosedDisabled()}
      ></ha-icon-button>`;
  }


private _computeOpenDisabled(): boolean {
    if (this.stateObj.state === UNAVAILABLE) {
      return true;
    }
    const assumedState = this.stateObj.attributes.assumed_state === true;
    return this._entityObj.isFullyOpenTilt && !assumedState;
  }


private _computeClosedDisabled(): boolean {
    if (this.stateObj.state === UNAVAILABLE) {
      return true;
    }
    const assumedState = this.stateObj.attributes.assumed_state === true;
    return this._entityObj.isFullyClosedTilt && !assumedState;
  }


private _onOpenTiltTap(ev): void {
    ev.stopPropagation();
    this._entityObj.openCoverTilt();
  }


private _onCloseTiltTap(ev): void {
    ev.stopPropagation();
    this._entityObj.closeCoverTilt();
  }


private _onStopTiltTap(ev): void {
    ev.stopPropagation();
    this._entityObj.stopCoverTilt();
  }


() => {
  it('should add', () => {
    expect(new Cmp753Component().add753(1)).toBe(754);
  });
}


() => {
    expect(new Cmp753Component().add753(1)).toBe(754);
  }


/**
Extract the keys from a type where the value type of the key do not include `null`.

@example
```
import {NonNullableKeys} from 'type-fest';

interface Example {
	a: string;
	b: string | number;
	c: string | null;
	d: {} | null;
	e?: number;
}

type Keys = NonNullableKeys<Example>;
//=> 'a' | 'b'
```

@category Object
*/
export type NonNullableKeys<Base> = keyof {
  // Pick all keys where the value type of the key include `null`
  [K in keyof Base as null extends Base[K] ? never : K]: Base[K]
};


export interface PlaceModuleFunctionMap extends HashMap<PlaceModuleFunction> {}


export interface PlaceModuleFunction {
    /** Arity of the function. See https://apidock.com/ruby/Method/arity */
    arity: number;
    /** Map of available paramters for the function */
    params: HashMap<[string, any] | [string]>;
    /** Order of the parameters to pass to the server */
    order: string[];
}


/** Allowable query parameters for systems index endpoint */
export interface PlaceSystemsQueryOptions extends PlaceResourceQueryOptions {
    /** Zone ID to filter the returned values on */
    zone_id?: string;
    /** Driver ID to filter the returned values on */
    module_id?: string;
}


/** Allowable query parameters for systems show endpoint */
export interface PlaceSystemShowOptions {
    /** Whether to return zone and module data for the system */
    complete?: boolean;
}


export type NetInfo = any


export type NetworkCallback = any


export function createRouter() {
  return _createRouter({
    // use appropriate history implementation for server/client
    // import.meta.env.SSR is injected by Vite.
    history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
    routes,
  })
}


(path) => {
  const name = path.match(/\.\/pages(.*)\/index\.vue$/)![1].toLowerCase()
  return {
    path: name === '' ? '/' : name,
    component: pages[path], // () => import('./pages/*.vue')
  }
}


/**
 * @module RPC-Error
 */
/** Holds detailed error information */
export declare class RpcError extends Error {
    /** Detailed error information */
    json: any;
    details: any;
    constructor(json: any);
}


/**
 * Represents an object that contains the details of a migration.
 */
export interface MigrationResult {
  /**
   * The traversal result of the underlying traverser.
   */
  readonly traversalResult: TraversalResult;

  /**
   * The number of documents that have been migrated.
   */
  readonly migratedDocCount: number;
}


() => {
  let component: ChooseEquipmentDialogComponent;
  let fixture: ComponentFixture<ChooseEquipmentDialogComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ ChooseEquipmentDialogComponent ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(ChooseEquipmentDialogComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
}


async () => {
    await TestBed.configureTestingModule({
      declarations: [ ChooseEquipmentDialogComponent ]
    })
    .compileComponents();
  }


() => {
    fixture = TestBed.createComponent(ChooseEquipmentDialogComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }