From 8a8a25f5654f8a7c2cc8fb6b2d29369e63b1f75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 22 Apr 2017 10:54:17 -0700 Subject: [PATCH] Support snapshot testing (#24) * Add angular-snapshot serializer * Add HTMLElementPlugin until Jest 20 release * Add jest as a peer dep * Update examples with snapshots --- HTMLElementPlugin.js | 102 ++++++++++++++++++ angular-snapshot.js | 64 +++++++++++ circle.yml | 4 +- .../__snapshots__/app.component.spec.ts.snap | 17 +++ example/src/app/app.component.html | 3 + example/src/app/app.component.spec.ts | 9 +- example/src/app/app.component.ts | 1 + .../__snapshots__/calc.component.spec.ts.snap | 20 ++++ example/src/app/calc/calc.component.html | 7 +- example/src/app/calc/calc.component.spec.ts | 8 +- example/src/app/calc/calc.component.ts | 14 ++- package.json | 3 + setupJest.js | 5 + 13 files changed, 247 insertions(+), 10 deletions(-) create mode 100644 HTMLElementPlugin.js create mode 100644 angular-snapshot.js create mode 100644 example/src/app/__snapshots__/app.component.spec.ts.snap create mode 100644 example/src/app/calc/__snapshots__/calc.component.spec.ts.snap diff --git a/HTMLElementPlugin.js b/HTMLElementPlugin.js new file mode 100644 index 0000000..1eaef11 --- /dev/null +++ b/HTMLElementPlugin.js @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +'use strict'; + +function escapeHTML(str) { + return str.replace(//g, '>'); +} + +const HTML_ELEMENT_REGEXP = /(HTML\w*?Element)/; +const test = isHTMLElement; + +function isHTMLElement(value) { + return ( + value !== undefined && + value !== null && + value.nodeType === 1 && + value.constructor !== undefined && + HTML_ELEMENT_REGEXP.test(value.constructor.name) + ); +} + +function printChildren(flatChildren, print, indent, colors, opts) { + return flatChildren + .map(node => { + if (typeof node === 'object') { + return print(node, print, indent, colors, opts); + } else if (typeof node === 'string') { + return colors.content.open + escapeHTML(node) + colors.content.close; + } else { + return print(node); + } + }) + .join(opts.edgeSpacing); +} + +function printAttributes(attributes, print, indent, colors, opts) { + return attributes + .sort() + .map(attribute => { + return ( + opts.spacing + + indent(colors.prop.open + attribute.name + colors.prop.close + '=') + + colors.value.open + + `"${attribute.value}"` + + colors.value.close + ); + }) + .join(''); +} + +const print = ( + element, + print, + indent, + opts, + colors +) => { + let result = colors.tag.open + '<'; + const elementName = element.tagName.toLowerCase(); + result += elementName + colors.tag.close; + + const hasAttributes = element.attributes && element.attributes.length; + if (hasAttributes) { + const attributes = Array.prototype.slice.call(element.attributes); + result += printAttributes(attributes, print, indent, colors, opts); + } + + const flatChildren = Array.prototype.slice.call(element.children); + if (!flatChildren.length && element.textContent) { + flatChildren.push(element.textContent.trim()); + } + + const closeInNewLine = hasAttributes && !opts.min; + if (flatChildren.length) { + const children = printChildren(flatChildren, print, indent, colors, opts); + result += + colors.tag.open + + (closeInNewLine ? '\n' : '') + + '>' + + colors.tag.close + + (children && opts.edgeSpacing + indent(children) + opts.edgeSpacing) + + colors.tag.open + + '' + + colors.tag.close; + } else { + result += + colors.tag.open + (closeInNewLine ? '\n' : ' ') + '/>' + colors.tag.close; + } + + return result; +}; + +module.exports = ({print, test}); diff --git a/angular-snapshot.js b/angular-snapshot.js new file mode 100644 index 0000000..af8716d --- /dev/null +++ b/angular-snapshot.js @@ -0,0 +1,64 @@ +const printAttributes = (val, attributes, print, indent, colors, opts) => { + return attributes + .sort() + .map(attribute => { + return ( + opts.spacing + + indent(colors.prop.open + attribute + colors.prop.close + '=') + + colors.value.open + + (val.componentInstance[attribute] && + val.componentInstance[attribute].constructor + ? '{[Function ' + + val.componentInstance[attribute].constructor.name + + ']}' + : `"${val.componentInstance[attribute]}"`) + + colors.value.close + ); + }) + .join(''); +}; + +const print = (val, print, indent, opts, colors) => { + let result = ''; + let componentAttrs = ''; + + const componentName = val.componentRef._elDef.element.name; + const componentInstance = print(val.componentInstance); + const nodes = val.componentRef._view.nodes + .filter(node => node.hasOwnProperty('renderElement')) + .map(node => print(node.renderElement)) + .join('\n'); + + const attributes = Object.keys(val.componentInstance); + + if (attributes.length) { + componentAttrs += printAttributes( + val, + attributes, + print, + indent, + colors, + opts + ); + } + + return ( + '<' + + componentName + + componentAttrs + + (componentAttrs.length ? '\n' : '') + + '>\n' + + indent(nodes) + + '\n' + ); +}; + +const test = val => + val !== undefined && + val !== null && + typeof val === 'object' && + Object.prototype.hasOwnProperty.call(val, 'componentRef'); + +module.exports = {print, test}; diff --git a/circle.yml b/circle.yml index 787c6a2..22a8f7b 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: environment: - YARN_VERSION: 0.20.3 + YARN_VERSION: 0.22.0 PATH: "${PATH}:${HOME}/.yarn/bin:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" node: version: 7 @@ -20,4 +20,4 @@ dependencies: test: override: - yarn run test:ci - - yarn link && cd example && yarn run test:ci && yarn run test:coverage + - yarn link && cd example && yarn link jest-preset-angular && yarn run test:ci && yarn run test:coverage diff --git a/example/src/app/__snapshots__/app.component.spec.ts.snap b/example/src/app/__snapshots__/app.component.spec.ts.snap new file mode 100644 index 0000000..6f5c039 --- /dev/null +++ b/example/src/app/__snapshots__/app.component.spec.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AppComponent snaps 1`] = ` + +
+

+ +

+
+
+`; diff --git a/example/src/app/app.component.html b/example/src/app/app.component.html index b6931b5..049ef82 100644 --- a/example/src/app/app.component.html +++ b/example/src/app/app.component.html @@ -1,3 +1,6 @@

{{title}} +

diff --git a/example/src/app/app.component.spec.ts b/example/src/app/app.component.spec.ts index c740bcd..01077ef 100644 --- a/example/src/app/app.component.spec.ts +++ b/example/src/app/app.component.spec.ts @@ -1,5 +1,6 @@ import { TestBed, async } from '@angular/core/testing'; - +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CalcComponent } from 'app/calc/calc.component'; import { AppComponent } from './app.component'; describe('AppComponent', () => { @@ -8,6 +9,7 @@ describe('AppComponent', () => { declarations: [ AppComponent ], + schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -17,6 +19,11 @@ describe('AppComponent', () => { expect(app).toBeTruthy(); })); + it('snaps', () => { + const fixture = TestBed.createComponent(AppComponent); + expect(fixture).toMatchSnapshot(); + }) + it(`should have as title 'app works!'`, async(() => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; diff --git a/example/src/app/app.component.ts b/example/src/app/app.component.ts index ff63e05..5a7bf05 100644 --- a/example/src/app/app.component.ts +++ b/example/src/app/app.component.ts @@ -7,4 +7,5 @@ import { Component } from '@angular/core'; }) export class AppComponent { title = 'app works!'; + hasClass = true; } diff --git a/example/src/app/calc/__snapshots__/calc.component.spec.ts.snap b/example/src/app/calc/__snapshots__/calc.component.spec.ts.snap new file mode 100644 index 0000000..b461cf3 --- /dev/null +++ b/example/src/app/calc/__snapshots__/calc.component.spec.ts.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CalcComponent should snap 1`] = ` + +
+

+ calc works! +

+
+
+`; diff --git a/example/src/app/calc/calc.component.html b/example/src/app/calc/calc.component.html index e63c7a5..4094e65 100644 --- a/example/src/app/calc/calc.component.html +++ b/example/src/app/calc/calc.component.html @@ -1,3 +1,8 @@ -

+

calc works!

diff --git a/example/src/app/calc/calc.component.spec.ts b/example/src/app/calc/calc.component.spec.ts index e52a55c..31412fe 100644 --- a/example/src/app/calc/calc.component.spec.ts +++ b/example/src/app/calc/calc.component.spec.ts @@ -8,9 +8,9 @@ describe('CalcComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ CalcComponent ] + declarations: [CalcComponent] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { @@ -19,7 +19,7 @@ describe('CalcComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should snap', () => { + expect(fixture).toMatchSnapshot(); }); }); diff --git a/example/src/app/calc/calc.component.ts b/example/src/app/calc/calc.component.ts index 915ac80..4ec4e7f 100644 --- a/example/src/app/calc/calc.component.ts +++ b/example/src/app/calc/calc.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, Input } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; @Component({ selector: 'app-calc', @@ -6,10 +7,19 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./calc.component.css'] }) export class CalcComponent implements OnInit { + @Input() hasAClass; + prop1: number; + observable$: Observable; - constructor() { } + constructor() { + this.init(); + this.prop1 = 1337; + } ngOnInit() { } + init() { + return 'Imma method'; + } } diff --git a/package.json b/package.json index e021de5..9fc6f68 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "jest": "^19.0.2", "typescript": "^2.2.2" }, + "peerDependencies": { + "jest": "^19.0.2" + }, "scripts": { "test": "jest", "test:ci": "jest -i" diff --git a/setupJest.js b/setupJest.js index 12c2cc5..64add11 100644 --- a/setupJest.js +++ b/setupJest.js @@ -6,6 +6,8 @@ require('zone.js/dist/sync-test'); require('zone.js/dist/async-test'); require('zone.js/dist/fake-async-test'); require('jest-zone-patch'); +const angularSnapshot = require('./angular-snapshot'); +const HTMLElementPlugin = require('./HTMLElementPlugin'); const { getTestBed } = require('@angular/core/testing'); const { BrowserDynamicTestingModule, @@ -16,3 +18,6 @@ getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting() ); + +expect.addSnapshotSerializer(HTMLElementPlugin); +expect.addSnapshotSerializer(angularSnapshot);