From 1484b1881c8ea530c15e5686eeea362c7a55d1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 4 Dec 2018 15:11:46 +0100 Subject: [PATCH 1/6] v7.0.0-alpha.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a18192f4d..7b48adce48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jest-preset-angular", - "version": "6.0.1", + "version": "7.0.0-alpha.0", "description": "Jest preset configuration for Angular projects", "main": "index.js", "repository": "git@github.com:thymikee/jest-preset-angular.git", From 671a0479754d18ea2d09e472e6d38b13f1a9c404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 4 Dec 2018 15:18:45 +0100 Subject: [PATCH 2/6] v7.0.0-alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b48adce48..7bdd33b407 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jest-preset-angular", - "version": "7.0.0-alpha.0", + "version": "7.0.0-alpha.1", "description": "Jest preset configuration for Angular projects", "main": "index.js", "repository": "git@github.com:thymikee/jest-preset-angular.git", From aabaa9f73a6bdd166b92fd929ca136f155f58adb Mon Sep 17 00:00:00 2001 From: Tho Date: Fri, 7 Dec 2018 00:08:16 +0100 Subject: [PATCH 3/6] Transform templateUrl, styleUrls and styles everywhere (#211) * Fix transformer to handle templateUrl everywhere templateUrl can also occur e. g. in tests, outside of decorators. These should also be transformed. * Update readme - preprocessor => transformer * Make comments match new implementation * Update comments and docs --- README.md | 4 +- .../InlineHtmlStripStylesTransformer.test.ts | 37 ++++ ...ineHtmlStripStylesTransformer.test.ts.snap | 28 +++ package.json | 1 + src/InlineHtmlStripStylesTransformer.ts | 179 ++++++------------ yarn.lock | 5 + 6 files changed, 132 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 028b425f08..b6ad41422a 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,9 @@ import './jestGlobalMocks'; // browser mocks globally available for every test * `"setupTestFrameworkScriptFile"` – this is the heart of our config, in this file we'll setup and patch environment within tests are running * `"transformIgnorePatterns"` – unfortunately some modules (like @ngrx ) are released as TypeScript files, not pure JavaScript; in such cases we cannot ignore them (all node_modules are ignored by default), so they can be transformed through TS compiler like any other module in our project. -## [Preprocessor](https://github.com/thymikee/jest-preset-angular/blob/master/preprocessor.js) +## [AST Transformer](https://github.com/thymikee/jest-preset-angular/blob/master/src/InlineHtmlStripStylesTransformer.ts) Jest doesn't run in browser nor through dev server. It uses jsdom to abstract browser environment. So we have to cheat a little and inline our templates and get rid of styles (we're not testing CSS) because otherwise Angular will try to make XHR call for our templates and fail miserably. -I used a scrappy regex to accomplish this with minimum effort, but you can also write a babel plugin to make it bulletproof. And btw, don't bother about perf here – Jest heavily caches transforms. That's why you need to run Jest with `--no-cache` flag every time you change it. - ## Angular testing environment setup If you look at your `src/test.ts` (or similar bootstrapping test file) file you'll see similarities to [`setupJest.js`](https://github.com/thymikee/jest-preset-angular/blob/master/setupJest.js). What we're doing here is we're adding globals required by Angular. With [jest-zone-patch](https://github.com/thymikee/jest-zone-patch) we also make sure Jest test methods run in Zone context. Then we initialize the Angular testing environment like normal. diff --git a/__tests__/InlineHtmlStripStylesTransformer.test.ts b/__tests__/InlineHtmlStripStylesTransformer.test.ts index 7b973955fc..e0449c0e72 100644 --- a/__tests__/InlineHtmlStripStylesTransformer.test.ts +++ b/__tests__/InlineHtmlStripStylesTransformer.test.ts @@ -88,6 +88,37 @@ const CODE_WITH_CUSTOM_DECORATOR = ` } ` +const CODE_TEST_WITH_TEMPLATE_URL_OVERRIDE = ` +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AComponent } from './a.component'; + +describe('AComponent', () => { + let fixture: ComponentFixture, + instance: AComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + AComponent, + ], + }).overrideComponent(AComponent, { + set: { + templateUrl: '../__mocks__/alert-follow-stub.component.html', + }, + }); + + fixture = TestBed.createComponent(AComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should render the component', () => { + expect(fixture).toMatchSnapshot(); + }); +}); +` + const createFactory = () => { @@ -138,4 +169,10 @@ describe('inlining template and stripping styles', () => { expect(out.outputText).toMatchSnapshot() }) + + it('should handle templateUrl in test file outside decorator', () => { + const out = transpile(CODE_TEST_WITH_TEMPLATE_URL_OVERRIDE) + + expect(out.outputText).toMatchSnapshot() + }) }) diff --git a/__tests__/__snapshots__/InlineHtmlStripStylesTransformer.test.ts.snap b/__tests__/__snapshots__/InlineHtmlStripStylesTransformer.test.ts.snap index dc01f9add4..2a83bb10f7 100644 --- a/__tests__/__snapshots__/InlineHtmlStripStylesTransformer.test.ts.snap +++ b/__tests__/__snapshots__/InlineHtmlStripStylesTransformer.test.ts.snap @@ -57,6 +57,34 @@ exports.AngularComponent = AngularComponent; " `; +exports[`inlining template and stripping styles should handle templateUrl in test file outside decorator 1`] = ` +"\\"use strict\\"; +Object.defineProperty(exports, \\"__esModule\\", { value: true }); +var testing_1 = require(\\"@angular/core/testing\\"); +var a_component_1 = require(\\"./a.component\\"); +describe('AComponent', function () { + var fixture, instance; + beforeEach(testing_1.async(function () { + testing_1.TestBed.configureTestingModule({ + declarations: [ + a_component_1.AComponent, + ], + }).overrideComponent(a_component_1.AComponent, { + set: { + template: require('../__mocks__/alert-follow-stub.component.html'), + }, + }); + fixture = testing_1.TestBed.createComponent(a_component_1.AComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + })); + it('should render the component', function () { + expect(fixture).toMatchSnapshot(); + }); +}); +" +`; + exports[`inlining template and stripping styles should inline non-relative templateUrl assignment and make it relative 1`] = ` "\\"use strict\\"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { diff --git a/package.json b/package.json index 7bdd33b407..46d7642e87 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "ts-jest": "~23.10.0" }, "devDependencies": { + "@types/node": "^10.12.12", "jest": "^23.6.0", "typescript": "^3.2.1" }, diff --git a/src/InlineHtmlStripStylesTransformer.ts b/src/InlineHtmlStripStylesTransformer.ts index 6a2b48c653..08ea6f2adf 100644 --- a/src/InlineHtmlStripStylesTransformer.ts +++ b/src/InlineHtmlStripStylesTransformer.ts @@ -4,59 +4,30 @@ * */ - - /* * IMPLEMENTATION DETAILS: - * This transformer handles two concerns: removing styles and inlining referenced templates, - * as they both are handled at the same location in the AST. + * This transformer handles two concerns: removing styles and inlining referenced templates. * - * The Component can be located anywhere in a file, except inside another Angular Component. - * The Decorator is not checked for the name 'Component', as someone might import it under - * a different name, or have their custom, modified version of the component decorator. - * Instead it checks for specific properties inside any class decorator. + * The assignments can be located anywhere in a file. * Caveats: - * All properties 'templateUrl', 'styles', 'styleUrls' inside ANY decorator will be modified. - * If the decorator content is referenced, it will not work: - * ```ts - * const componentArgs = {} - * @Component(componentArgs) - * class MyComponent { } - * ``` - * - * The AST has to look like this: - * - * ClassDeclaration - * Decorator - * CallExpression - * ObjectLiteralExpression - * PropertyAssignment - * Identifier - * StringLiteral - * - * Where some additional Check have to be made to identify the node as the required kind: + * All properties 'templateUrl', 'styles', 'styleUrls' ANYWHERE will be modified, even if they + * are not used in the context of an Angular Component. * - * ClassDeclaration: isClassDeclaration - * Decorator - * CallExpression: isCallExpression - * ObjectLiteralExpression: isObjectLiteral - * PropertyAssignment: isPropertyAssignment - * Identifier: isIdentifier - * StringLiteral: isStringLiteral + * The AST has to simply look like this anywhere in a ts file: * + * PropertyAssignment + * Identifier + * Initializer */ -// take care of importing only types, for the rest use injected `ts` +// only import types, for the rest use injected `ConfigSet.compilerModule` import TS, { Node, SourceFile, TransformationContext, Transformer, Visitor, - ClassDeclaration, - CallExpression, - ObjectLiteralExpression, PropertyAssignment, Identifier, StringLiteral, @@ -104,18 +75,6 @@ export const version = 1 */ export function factory(cs: ConfigSet) { - /** - * Array Flatten function, as there were problems to make the compiler use - * esnext's Array.flat, can be removed in the future. - * @param arr Array to be flattened - */ - function flatten(arr: (T | T[] | T[][])[]): T[] { - return arr.reduce( - (xs: T[], x) => Array.isArray(x) ? xs.concat(flatten(x as T[])) : xs.concat(x), - [] - ) as T[] - } - /** * Our compiler (typescript, or a module with typescript-like interface) */ @@ -125,71 +84,61 @@ export function factory(cs: ConfigSet) { * Traverses the AST down to the relevant assignments in the decorator * argument and returns them in an array. */ - function getPropertyAssignmentsToTransform(classDeclaration: ClassDeclaration) { - return flatten(classDeclaration.decorators! - .map(dec => dec.expression) - .filter(ts.isCallExpression) - .map(callExpr => (callExpr as CallExpression).arguments - .filter(ts.isObjectLiteralExpression) - .map(arg => (arg as ObjectLiteralExpression).properties - .filter(ts.isPropertyAssignment) - .map(arg => arg as PropertyAssignment) - .filter(propAss => ts.isIdentifier(propAss.name)) - .filter(propAss => TRANSFORM_PROPS.includes((propAss.name as Identifier).text)) - ) - ) - ) + function isPropertyAssignmentToTransform(node: Node): node is PropertyAssignment { + return ts.isPropertyAssignment(node) && + ts.isIdentifier(node.name) && + TRANSFORM_PROPS.includes(node.name.text) } /** - * Clones the node, identifies the properties to transform in the decorator and modifies them. - * @param node class declaration with decorators + * Clones the assignment and manipulates it depending on its name. + * @param node the property assignment to change */ - function transfromDecoratorForJest(node: ClassDeclaration) { - - const mutable = ts.getMutableClone(node) - const assignments = getPropertyAssignmentsToTransform(mutable) - - assignments.forEach(assignment => { - switch ((assignment.name as Identifier).text) { - case TEMPLATE_URL: - // we can reuse the right-hand-side literal from the assignment - let templatePathLiteral = assignment.initializer - - // fix templatePathLiteral if it was a non-relative path - if (ts.isStringLiteral(assignment.initializer)) { - const templatePathStringLiteral: StringLiteral = assignment.initializer; - // match if it starts with ./ or ../ or / - if (templatePathStringLiteral.text && - !templatePathStringLiteral.text.match(/^(\.\/|\.\.\/|\/)/)) { - // make path relative by appending './' - templatePathLiteral = ts.createStringLiteral(`./${templatePathStringLiteral.text}`) - } + function transfromPropertyAssignmentForJest(node: PropertyAssignment) { + + const mutableAssignment = ts.getMutableClone(node) + + const assignmentNameText = (mutableAssignment.name as Identifier).text + + switch (assignmentNameText) { + case TEMPLATE_URL: + // reuse the right-hand-side literal from the assignment + let templatePathLiteral = mutableAssignment.initializer + + // fix templatePathLiteral if it was a non-relative path + if (ts.isStringLiteral(mutableAssignment.initializer)) { + const templatePathStringLiteral: StringLiteral = mutableAssignment.initializer; + // match if it starts with ./ or ../ or / + if (templatePathStringLiteral.text && + !templatePathStringLiteral.text.match(/^(\.\/|\.\.\/|\/)/)) { + // make path relative by appending './' + templatePathLiteral = ts.createStringLiteral(`./${templatePathStringLiteral.text}`) } + } + + // replace 'templateUrl' with 'template' + mutableAssignment.name = ts.createIdentifier(TEMPLATE) + // replace current initializer with require(path) + mutableAssignment.initializer = ts.createCall( + /* expression */ ts.createIdentifier(REQUIRE), + /* type arguments */ undefined, + /* arguments array */ [templatePathLiteral] + ) + break; + case STYLES: + case STYLE_URLS: + // replace initializer array with empty array + mutableAssignment.initializer = ts.createArrayLiteral() + break; + } - // replace 'templateUrl' with 'template' - assignment.name = ts.createIdentifier(TEMPLATE) - // replace current initializer with require(path) - assignment.initializer = ts.createCall( - /* expression */ ts.createIdentifier(REQUIRE), - /* type arguments */ undefined, - /* arguments array */ [templatePathLiteral] - ) - break; - case STYLES: - case STYLE_URLS: - // replace initializer array with empty array - assignment.initializer = ts.createArrayLiteral() - break; - } - }) - return mutable + return mutableAssignment } /** * Create a source file visitor which will visit all nodes in a source file * @param ctx The typescript transformation context - * @param sf The owning source file + * @param _ The owning source file */ function createVisitor(ctx: TransformationContext, _: SourceFile) { @@ -202,30 +151,22 @@ export function factory(cs: ConfigSet) { let resultNode: Node // before we create a deep clone to modify, we make sure that - // this class has the decorator arguments of interest. - if ( - ts.isClassDeclaration(node) && - node.decorators && - getPropertyAssignmentsToTransform(node).length - ) { - // get mutable node and change properties - // NOTE: classes can be inside methods, but we do not - // look for them inside Angular Components! - // recursion ends here, as ts.visitEachChild is not called. - resultNode = transfromDecoratorForJest(node) + // this is an assignment which we want to transform + if (isPropertyAssignmentToTransform(node)) { + + // get transformed node with changed properties + resultNode = transfromPropertyAssignmentForJest(node) } else { - // look for other classes with decorators - // classes can be inside other statements (like if blocks) + // look for interesting assignments inside this node resultNode = ts.visitEachChild(node, visitor, ctx) } - // finally returns the currently visited node + // finally return the currently visited node return resultNode } return visitor } - // returns the transformer factory return (ctx: TransformationContext): Transformer => (sf: SourceFile) => ts.visitNode(sf, createVisitor(ctx, sf)) } diff --git a/yarn.lock b/yarn.lock index c4e064377f..8d3ef06672 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,11 @@ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.10.tgz#4897974cc317bf99d4fe6af1efa15957fa9c94de" integrity sha512-DC8xTuW/6TYgvEg3HEXS7cu9OijFqprVDXXiOcdOKZCU/5PJNLZU37VVvmZHdtMiGOa8wAA/We+JzbdxFzQTRQ== +"@types/node@^10.12.12": + version "10.12.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.12.tgz#e15a9d034d9210f00320ef718a50c4a799417c47" + integrity sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A== + "@types/node@^6.0.46": version "6.0.88" resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.88.tgz#f618f11a944f6a18d92b5c472028728a3e3d4b66" From c537a175b3712cc78fb805edb587d3843c12fdb1 Mon Sep 17 00:00:00 2001 From: Ahn Date: Tue, 4 Dec 2018 17:59:57 +0100 Subject: [PATCH 4/6] Adjust CHANGELOG (#208) - Highlighting BREAKING text. - Change `Adjustment` to `Chore` to keep consistency of CHANGELOG template. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b6b91524..36ab3d81a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## Changelog (master) -* Breaking: Upgrade ts-jest to ^23.10.4 and use ast-transformer instead of processor ([#204](https://github.com/thymikee/jest-preset-angular/pull/204)) -* Adjustment: Remove template literal character escaping (reverts [#34](https://github.com/thymikee/jest-preset-angular/pull/34)) +* (**BREAKING**): Upgrade ts-jest to ^23.10.4 and use ast-transformer instead of processor ([#204](https://github.com/thymikee/jest-preset-angular/pull/204)) +* Chore: Remove template literal character escaping (reverts [#34](https://github.com/thymikee/jest-preset-angular/pull/34)) #### Migration Guide From b1e721cc5b8d5bd74cd29f78b1ea7c758aa989fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 5 Dec 2018 12:08:42 +0100 Subject: [PATCH 5/6] feat: adjust semver range of jest-zone-patch (#209) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 46d7642e87..37bd9edd35 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "license": "MIT", "dependencies": { "@types/jest": "^23.3.10", - "jest-zone-patch": "^0.0.8", + "jest-zone-patch": ">=0.0.9 <1.0.0", "ts-jest": "~23.10.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 8d3ef06672..7edb666639 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2188,10 +2188,10 @@ jest-worker@^23.2.0: dependencies: merge-stream "^1.0.1" -jest-zone-patch@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/jest-zone-patch/-/jest-zone-patch-0.0.8.tgz#90fa3b5b60e95ad3e624dd2c3eb59bb1dcabd371" - integrity sha1-kPo7W2DpWtPmJN0sPrWbsdyr03E= +"jest-zone-patch@>=0.0.9 <1.0.0": + version "0.0.9" + resolved "https://registry.yarnpkg.com/jest-zone-patch/-/jest-zone-patch-0.0.9.tgz#ee6823317798a3ae967e82be963f0b1f80b03d3f" + integrity sha512-QmlUxg2NIvLHSqexS3nqQ+Kid6o+5cxYnlcTi204XwsRcBm+mVYodpe+9ddcooKR9ATBIUl4zY247Uky/PgXhw== jest@^23.6.0: version "23.6.0" From 61cfeb3904a6d1421cd92be761ffa8cf2d64db62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 11 Dec 2018 12:50:20 +0100 Subject: [PATCH 6/6] v7.0.0-alpha.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 37bd9edd35..ca2eff1122 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jest-preset-angular", - "version": "7.0.0-alpha.1", + "version": "7.0.0-alpha.2", "description": "Jest preset configuration for Angular projects", "main": "index.js", "repository": "git@github.com:thymikee/jest-preset-angular.git",