Skip to content

Commit

Permalink
Support Angular 9. Bump version (v0.4.0) (#19)
Browse files Browse the repository at this point in the history
* Separate Angular versions

* Apply @enten changes

* Use npx

* Refactoring

* Refactoring, adding Angular 8 case

* Requested changes, bump version (v0.4.0)

* Requested changes: error messages
  • Loading branch information
Farfurix authored Apr 3, 2020
1 parent 28edf50 commit dc29e45
Show file tree
Hide file tree
Showing 47 changed files with 741 additions and 132 deletions.
15 changes: 13 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,19 @@ Temporary Items
.apdisk

node_modules/*
test/data/angularjs/node_modules/*
test/data/angular-4/node_modules/*
test/data/angular-8/node_modules/*
test/data/angular-9/node_modules/*
package-lock.json
test/data/angularjs/package-lock.json
test/data/angular-4/package-lock.json
test/data/angular-8/package-lock.json
test/data/angular-9/package-lock.json
.idea/*
lib/*
test/data/lib/*
test/data/angular/app/*.js
test/data/angular/app/*.js.map
test/data/angular-4/app/*.js
test/data/angular-4/app/*.js.map
test/data/angular-8/dist/*
test/data/angular-9/dist/*
38 changes: 13 additions & 25 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "testcafe-angular-selectors",
"version": "0.3.2",
"version": "0.4.0",
"description": "Angular selectors for TestCafe",
"author": {
"name": "Developer Express Inc.",
Expand All @@ -17,46 +17,34 @@
],
"license": "MIT",
"scripts": {
"install-angularjs-deps": "cd ./test/data/angularjs && npm i",
"install-angular-4-deps": "cd ./test/data/angular-4 && npm i",
"install-angular-8-deps": "cd ./test/data/angular-8 && npm i",
"install-angular-9-deps": "cd ./test/data/angular-9 && npm i",
"postinstall": "npm run install-angularjs-deps && npm run install-angular-4-deps && npm run install-angular-8-deps && npm run install-angular-9-deps",
"lint": "eslint src/*.js test/*.js",
"http-server": "http-server ./ -s",
"compile-angular-4-app": "cd test/data/angular-4 && tsc -p ./",
"compile-angular-8-app": "cd test/data/angular-8 && npx ng build angular-app",
"compile-angular-9-app": "cd test/data/angular-9 && npx ng build angular-app",
"compile-angular-apps": "npm run compile-angular-4-app && npm run compile-angular-8-app && npm run compile-angular-9-app",
"testcafe": "testcafe all test/*-test.js --app \"npm run http-server\"",
"test": "npm run lint && npm run build && npm run compile-angular-site && npm run testcafe",
"test": "npm run lint && npm run build && npm run compile-angular-apps && npm run testcafe",
"build": "babel src --out-dir lib",
"publish-please": "publish-please",
"prepublish": "publish-please guard",
"compile-angular-site": "tsc -p test/data/angular"
"prepublish": "publish-please guard"
},
"devDependencies": {
"@angular/animations": "^4.0.3",
"@angular/common": "~4.0.0",
"@angular/compiler": "~4.0.0",
"@angular/compiler-cli": "~4.0.0",
"@angular/core": "~4.0.0",
"@angular/forms": "~4.0.0",
"@angular/http": "~4.0.0",
"@angular/platform-browser": "~4.0.0",
"@angular/platform-browser-dynamic": "~4.0.0",
"@angular/platform-server": "~4.0.0",
"@angular/router": "~4.0.0",
"@angular/tsc-wrapped": "~4.0.0",
"@angular/upgrade": "~4.0.0",
"angular": "^1.6.4",
"angular-in-memory-web-api": "~0.3.1",
"babel-cli": "^6.22.2",
"babel-eslint": "^7.1.1",
"babel-plugin-transform-for-of-as-array": "^1.0.3",
"babel-preset-env": "^1.6.0",
"babel-preset-es2015-loose": "^6.1.4",
"core-js": "^2.4.1",
"eslint": "4.18.2",
"eslint-plugin-testcafe": "^0.2.1",
"http-server": "^0.9.0",
"publish-please": "^5.4.3",
"rxjs": "^5.4.3",
"systemjs": "0.19.39",
"testcafe": "^0.18.0",
"typescript": "^2.5.2",
"zone.js": "^0.8.4"
"testcafe": "^1.8.3"
},
"keywords": [
"testcafe",
Expand Down
92 changes: 65 additions & 27 deletions src/angular-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,23 @@ export default Selector(complexSelector => {
throw new Error(`If the selector parameter is passed it should be a string, but it was ${typeof selector}`);
}

function getComponentTag (debugElement) {
return debugElement.nativeElement.tagName.toLowerCase();
validateSelector(complexSelector);

// NOTE: Angular version 9 or higher
const walkingNativeElementsMode = window.ng && typeof window.ng.getComponent === 'function';
// NOTE: Angular version 8 or lower
const walkingDebugElementsMode = window.ng && typeof window.ng.probe === 'function';

const isPageReadyForTesting = (walkingNativeElementsMode || walkingDebugElementsMode) &&
typeof window.getAllAngularRootElements === 'function';

if (!isPageReadyForTesting) {
throw new Error(`The tested page does not use Angular or did not load correctly.
Use the 'waitForAngular' function to ensure the page is ready for testing.`);
}

function getNativeElementTag (nativeElement) {
return nativeElement.tagName.toLowerCase();
}

function getTagList (componentSelector) {
Expand All @@ -17,42 +32,52 @@ export default Selector(complexSelector => {
.map(el => el.trim().toLowerCase());
}

function filterNodes (rootDebugElement, tags) {
const foundNodes = [];

function walkDebugElements (debugElement, tagIndex, checkFn) {
if (checkFn(debugElement, tagIndex)) {
function filterNodes (rootElement, tags) {
function walkElements (element, tagIndex, checkFn) {
if (checkFn(element, tagIndex)) {
if (tagIndex === tags.length - 1) {
foundNodes.push(debugElement.nativeElement);
if (walkingNativeElementsMode)
foundNodes.push(element);
else
foundNodes.push(element.nativeElement);

return;
}

tagIndex++;
}

for (const childDebugElement of debugElement.children)
walkDebugElements(childDebugElement, tagIndex, checkFn);
for (const childElement of element.children)
walkElements(childElement, tagIndex, checkFn);
}

walkDebugElements(rootDebugElement, 0, (debugElement, tagIndex) => {
function checkDebugElement (debugElement, tagIndex) {
if (!debugElement.componentInstance)
return false;

return tags[tagIndex] === getComponentTag(debugElement);
});
return tags[tagIndex] === getNativeElementTag(debugElement.nativeElement);
}

return foundNodes;
}
function checkNativeElement (nativeElement, tagIndex) {
const componentInstance = window.ng.getComponent(nativeElement);

validateSelector(complexSelector);
if (!componentInstance)
return false;

const isPageReadyForTesting = window.ng && typeof window.ng.probe === 'function' &&
typeof window.getAllAngularRootElements === 'function';
return tags[tagIndex] === getNativeElementTag(nativeElement);
}

if (!isPageReadyForTesting) {
throw new Error(`The page doesn't contain Angular components or they are not loaded completely
or your Angular app is not in a development mode.
Use the 'waitForAngular' function to ensure the components are loaded.`);
const foundNodes = [];

if (walkingNativeElementsMode)
walkElements(rootElement, 0, checkNativeElement);
else {
const debugElementRoot = window.ng.probe(rootElement);

walkElements(debugElementRoot, 0, checkDebugElement);
}

return foundNodes;
}

// NOTE: If there are multiple roots on the page we find a target in the first root only
Expand All @@ -61,15 +86,28 @@ export default Selector(complexSelector => {
if (!complexSelector)
return rootElement;

const tags = getTagList(complexSelector);
const debugElementRoot = window.ng.probe(rootElement);
const tags = getTagList(complexSelector);

return filterNodes(debugElementRoot, tags);
return filterNodes(rootElement, tags);

}).addCustomMethods({
getAngular: (node, fn) => {
const debugElement = window.ng.probe(node);
const state = debugElement.componentInstance;
let state;

// NOTE: Angular version 9 or higher
if (typeof window.ng.getComponent === 'function') {
state = window.ng.getComponent(node);

// NOTE: We cannot handle this circular reference in a replicator. So we remove it from the returned component state.
if (state && '__ngContext__' in state)
state = JSON.parse(JSON.stringify(state, (key, value) => key !== '__ngContext__' ? value : void 0));
}
// NOTE: Angular version 8 or lower
else {
const debugElement = window.ng.probe(node);

state = debugElement && debugElement.componentInstance;
}

if (typeof fn === 'function')
return fn({ state });
Expand Down
41 changes: 33 additions & 8 deletions src/wait-for-angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,43 @@ export default ClientFunction(ms => {
window.clearInterval(pingIntervalId);
};

const isThereAngularInDevelopmentMode = () => {
if (window.ng && typeof window.ng.probe === 'function' &&
typeof window.getAllAngularRootElements === 'function') {
const rootElements = window.getAllAngularRootElements();
const firstRootDebugElement = rootElements &&
rootElements.length ? window.ng.probe(rootElements[0]) : null;
const getFirstRootElement = () => {
if (typeof window.getAllAngularRootElements === 'function') {
const rootElements = window.getAllAngularRootElements();

return !!(firstRootDebugElement && firstRootDebugElement.injector);
return rootElements && rootElements.length ? rootElements[0] : null;
}

return null;
};

const isElementInjectorExists = (firstRootElement) => {
if (window.ng) {
// NOTE: Angular version 9 or higher
if (typeof window.ng.getInjector === 'function') {
const firstRootInjector = window.ng.getInjector(firstRootElement);
const injectorConstructorName = firstRootInjector && firstRootInjector.constructor &&
firstRootInjector.constructor.name;

return !!injectorConstructorName && injectorConstructorName.toLowerCase() === 'nodeinjector';
}
// NOTE: Angular version 8 or lower
else if (typeof window.ng.probe === 'function') {
const firstRootDebugElement = window.ng.probe(firstRootElement);

return !!(firstRootDebugElement && firstRootDebugElement.injector);
}
}

return false;
};

const isThereAngularInDevelopmentMode = () => {
const firstRootElement = getFirstRootElement();

return !!firstRootElement && isElementInjectorExists(firstRootElement);
};

const check = () => {
if (isThereAngularInDevelopmentMode()) {
clearTimeouts();
Expand All @@ -36,7 +60,8 @@ export default ClientFunction(ms => {

pingTimeoutId = window.setTimeout(() => {
clearTimeouts();
reject(new Error('Cannot find Angular in development mode.'));
reject(new Error(`Cannot find information about Angular components. The tested application should be deployed in development mode.
For more information, see https://angular.io/guide/deployment.`));
}, WAIT_TIMEOUT);

check();
Expand Down
2 changes: 1 addition & 1 deletion test/angular-selector-errors-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ test('should throw an exception if window.ng does not not exist', async t => {
await t.expect(false).ok('The selector should throw an error but it doesn\'t.');
}
catch (e) {
await t.expect(e.errMsg).contains('Use the \'waitForAngular\' function to ensure the components are loaded.');
await t.expect(e.errMsg).contains('The tested page does not use Angular or did not load correctly.');
}
});
65 changes: 36 additions & 29 deletions test/angular-selector-test.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
import { AngularSelector, waitForAngular } from '../lib';

fixture `AngularSelector`
.page('http://localhost:8080/test/data/angular')
.beforeEach(async () => {
await waitForAngular();
runTests('AngularSelector (Angular v4)', 'http://localhost:8080/test/data/angular-4');
runTests('AngularSelector (Angular v8)', 'http://localhost:8080/test/data/angular-8/dist/index-aot.html');
runTests('AngularSelector (Angular v9)', 'http://localhost:8080/test/data/angular-9/dist/index-aot.html');

function runTests (fixtureLabel, pageUrl) {
fixture(fixtureLabel)
.page(pageUrl)
.beforeEach(async () => {
await waitForAngular();
});

test('root', async t => {
const root = AngularSelector();
const rootAngular = await root.getAngular();

await t
.expect(root.exists).ok()
.expect(rootAngular.rootProp1).eql(1);

await t.expect(rootAngular.hasOwnProperty('__ngContext__')).notOk();
});

test('root', async t => {
const root = AngularSelector();
const rootAngular = await root.getAngular();
test('selector', async t => {
const list = AngularSelector('list');
const listAngular = await list.getAngular();

await t
.expect(root.exists).ok()
.expect(rootAngular.rootProp1).eql(1);
});

test('selector', async t => {
const list = AngularSelector('list');
const listAngular = await list.getAngular();

await t.expect(list.count).eql(2)
.expect(AngularSelector('list-item').count).eql(6)
.expect(listAngular.id).eql('list1');
});

test('composite selector', async t => {
const listItem = AngularSelector('list list-item');
const listItemAngular6 = await listItem.nth(5).getAngular();
const listItemAngular5Id = listItem.nth(4).getAngular(({ state }) => state.id);
await t.expect(list.count).eql(2)
.expect(AngularSelector('list-item').count).eql(6)
.expect(listAngular.id).eql('list1');
});

await t.expect(listItem.count).eql(6)
.expect(listItemAngular6.id).eql('list2-item3')
.expect(listItemAngular5Id).eql('list2-item2');
});
test('composite selector', async t => {
const listItem = AngularSelector('list list-item');
const listItemAngular6 = await listItem.nth(5).getAngular();
const listItemAngular5Id = listItem.nth(4).getAngular(({ state }) => state.id);

await t.expect(listItem.count).eql(6)
.expect(listItemAngular6.id).eql('list2-item3')
.expect(listItemAngular5Id).eql('list2-item2');
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
<h1 id="angular-version">Angular ({{version}})</h1>
<list id="list1"></list>
<list id="list2"></list>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Component, VERSION} from '@angular/core';
import { Component, VERSION } from '@angular/core';

@Component({
selector: 'my-app',
Expand All @@ -7,4 +7,4 @@ import {Component, VERSION} from '@angular/core';
export class AppComponent {
rootProp1 = 1;
version = VERSION.full
}
}
Loading

0 comments on commit dc29e45

Please sign in to comment.