├── test ├── data │ └── angular-15 │ │ ├── app │ │ ├── polyfills.ts │ │ ├── polyfills.js.map │ │ ├── polyfills.js │ │ ├── app.component.html │ │ ├── main.js.map │ │ ├── main.ts │ │ ├── list-item.component.ts │ │ ├── app.component.ts │ │ ├── main.js │ │ ├── list-item.component.js.map │ │ ├── app.component.js.map │ │ ├── app.module.js.map │ │ ├── list.component.js.map │ │ ├── list.component.ts │ │ ├── app.module.ts │ │ ├── app.component.js │ │ ├── list-item.component.js │ │ ├── app.module.js │ │ └── list.component.js │ │ ├── index-aot.html │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── angular.json │ │ ├── systemjs-angular-loader.js │ │ └── systemjs.config.js ├── .eslintrc ├── angular-selector-errors-test.js ├── wait-for-angular-test.js └── angular-selector-test.js ├── src ├── index.js ├── wait-for-angular.js ├── angular-selector.js └── angularjs-selector.js ├── .publishrc ├── .github ├── workflows │ ├── handle-labels.yml │ ├── no-response.yml │ └── handle-stale.yml └── labels.yml ├── .editoreconfig ├── appveyor.yml ├── .gitattributes ├── .gitignore ├── LICENSE ├── ts-defs └── index.d.ts ├── package.json ├── README.md ├── .eslintrc ├── angular-selector.md └── angularJS-selector.md /test/data/angular-15/app/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone'; 2 | -------------------------------------------------------------------------------- /test/data/angular-15/app/polyfills.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"polyfills.js","sourceRoot":"","sources":["polyfills.ts"],"names":[],"mappings":";;AAAA,6BAA2B"} -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "testcafe" 4 | ], 5 | "extends": "plugin:testcafe/recommended", 6 | "rules": { 7 | "no-unused-expressions": 0 8 | } 9 | } -------------------------------------------------------------------------------- /test/data/angular-15/app/polyfills.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | require("zone.js/dist/zone"); 4 | //# sourceMappingURL=polyfills.js.map -------------------------------------------------------------------------------- /test/data/angular-15/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Angular ({{version}})

3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /test/data/angular-15/app/main.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":";;AAAA,8EAA2E;AAE3E,2CAAyC;AAEzC,IAAA,iDAAsB,GAAE,CAAC,eAAe,CAAC,sBAAS,CAAC,CAAC"} -------------------------------------------------------------------------------- /test/data/angular-15/index-aot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | loading... 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/data/angular-15/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { AppModule } from './app.module'; 4 | 5 | platformBrowserDynamic().bootstrapModule(AppModule); 6 | -------------------------------------------------------------------------------- /test/data/angular-15/app/list-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'list-item', 5 | template: '

{{id}}

' 6 | }) 7 | export class ListItemComponent { 8 | @Input() id: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const AngularJSSelector = require('./angularjs-selector'); 2 | const AngularSelector = require('./angular-selector'); 3 | const waitForAngular = require('./wait-for-angular'); 4 | 5 | module.exports = { AngularJSSelector, AngularSelector, waitForAngular }; 6 | -------------------------------------------------------------------------------- /test/data/angular-15/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, VERSION } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'my-app', 5 | templateUrl: './app.component.html' 6 | }) 7 | export class AppComponent { 8 | rootProp1 = 1; 9 | version = VERSION.full 10 | } 11 | -------------------------------------------------------------------------------- /.publishrc: -------------------------------------------------------------------------------- 1 | { 2 | "validations": { 3 | "vulnerableDependencies": false, 4 | "uncommittedChanges": true, 5 | "untrackedFiles": true, 6 | "sensitiveData": true, 7 | "branch": "master", 8 | "gitTag": true 9 | }, 10 | "confirm": true, 11 | "publishTag": "latest", 12 | "prePublishScript": "npm test" 13 | } -------------------------------------------------------------------------------- /.github/workflows/handle-labels.yml: -------------------------------------------------------------------------------- 1 | name: 'Label Actions' 2 | 3 | on: 4 | issues: 5 | types: [labeled, unlabeled] 6 | pull_request_target: 7 | types: [labeled, unlabeled] 8 | 9 | jobs: 10 | reaction: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: DevExpress/testcafe-build-system/actions/handle-labels@main 14 | -------------------------------------------------------------------------------- /.editoreconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [{.eslintrc,package.json,.travis.yml}] 15 | indent_size = 2 -------------------------------------------------------------------------------- /test/data/angular-15/app/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var platform_browser_dynamic_1 = require("@angular/platform-browser-dynamic"); 4 | var app_module_1 = require("./app.module"); 5 | (0, platform_browser_dynamic_1.platformBrowserDynamic)().bootstrapModule(app_module_1.AppModule); 6 | //# sourceMappingURL=main.js.map -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | NODEJS_VERSION: "stable" 3 | 4 | image: 5 | - Visual Studio 2022 6 | 7 | install: 8 | - ps: >- 9 | Install-Product node $env:NODEJS_VERSION 10 | 11 | choco install GoogleChrome 12 | 13 | choco install Firefox 14 | 15 | - cmd: >- 16 | npm install 17 | 18 | build: off 19 | 20 | test_script: 21 | - cmd: >- 22 | npm test -------------------------------------------------------------------------------- /test/data/angular-15/app/list-item.component.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"list-item.component.js","sourceRoot":"","sources":["list-item.component.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,sCAAiD;AAMjD;IAAA;IAEA,CAAC;IADY;QAAR,IAAA,YAAK,GAAE;;iDAAY;IADX,iBAAiB;QAJ7B,IAAA,gBAAS,EAAC;YACP,QAAQ,EAAE,WAAW;YACrB,QAAQ,EAAE,eAAe;SAC5B,CAAC;OACW,iBAAiB,CAE7B;IAAD,wBAAC;CAAA,AAFD,IAEC;AAFY,8CAAiB"} -------------------------------------------------------------------------------- /test/data/angular-15/app/app.component.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app.component.js","sourceRoot":"","sources":["app.component.ts"],"names":[],"mappings":";;;;;;;;;AAAA,sCAAmD;AAMnD;IAAA;QACI,cAAS,GAAG,CAAC,CAAC;QACd,YAAO,GAAG,cAAO,CAAC,IAAI,CAAA;IAC1B,CAAC;IAHY,YAAY;QAJxB,IAAA,gBAAS,EAAC;YACP,QAAQ,EAAE,QAAQ;YAClB,WAAW,EAAE,sBAAsB;SACtC,CAAC;OACW,YAAY,CAGxB;IAAD,mBAAC;CAAA,AAHD,IAGC;AAHY,oCAAY"} -------------------------------------------------------------------------------- /test/data/angular-15/app/app.module.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app.module.js","sourceRoot":"","sources":["app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,sCAAyC;AACzC,8DAA0D;AAC1D,iDAA+C;AAC/C,mDAAiD;AACjD,6DAA0D;AAa1D;IAAA;IACA,CAAC;IADY,SAAS;QAXrB,IAAA,eAAQ,EAAC;YACN,OAAO,EAAE;gBACL,gCAAa;aAChB;YACD,YAAY,EAAE;gBACV,4BAAY;gBACZ,8BAAa;gBACb,uCAAiB;aACpB;YACD,SAAS,EAAE,CAAC,4BAAY,CAAC;SAC5B,CAAC;OACW,SAAS,CACrB;IAAD,gBAAC;CAAA,AADD,IACC;AADY,8BAAS"} -------------------------------------------------------------------------------- /test/data/angular-15/app/list.component.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"list.component.js","sourceRoot":"","sources":["list.component.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,sCAAiD;AAQjD;IAAA;IAMA,CAAC;IAHG,6BAAK,GAAL,UAAM,OAAe;QACjB,OAAO,IAAI,CAAC,EAAE,GAAG,OAAO,GAAG,OAAO,CAAC;IACvC,CAAC;IAJQ;QAAR,IAAA,YAAK,GAAE;;6CAAY;IADX,aAAa;QANzB,IAAA,gBAAS,EAAC;YACP,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,0LAEkD;SAC/D,CAAC;OACW,aAAa,CAMzB;IAAD,oBAAC;CAAA,AAND,IAMC;AANY,sCAAa"} -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain -------------------------------------------------------------------------------- /test/data/angular-15/app/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'list', 5 | template: ` 6 | 7 | ` 8 | }) 9 | export class ListComponent { 10 | @Input() id: string; 11 | 12 | getId(postfix: string) { 13 | return this.id + '-item' + postfix; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/data/angular-15/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-15", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@angular-devkit/build-angular": "^15.1.6", 6 | "@angular/cli": "^15.1.6", 7 | "@angular/compiler-cli": "^15.1.5", 8 | "@angular/forms": "^15.1.5", 9 | "@angular/router": "^15.1.5", 10 | "@angular/upgrade": "^15.1.5", 11 | "core-js": "^3.28.0", 12 | "rxjs": "^7.8.0", 13 | "systemjs": "^6.13.0", 14 | "typescript": "^4.9.5", 15 | "zone.js": "^0.12.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/data/angular-15/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppComponent } from './app.component'; 4 | import { ListComponent } from './list.component'; 5 | import { ListItemComponent } from './list-item.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | BrowserModule 10 | ], 11 | declarations: [ 12 | AppComponent, 13 | ListComponent, 14 | ListItemComponent 15 | ], 16 | bootstrap: [AppComponent] 17 | }) 18 | export class AppModule { 19 | } 20 | -------------------------------------------------------------------------------- /test/data/angular-15/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "lib": [ "es2015", "dom" ], 11 | "removeComments": false, 12 | "noImplicitAny": true, 13 | "skipLibCheck": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "outDir": "dist" 19 | }, 20 | "exclude": [ 21 | "node_modules/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test/angular-selector-errors-test.js: -------------------------------------------------------------------------------- 1 | import { AngularSelector } from '../src'; 2 | 3 | fixture `Angular selector errors`; 4 | 5 | test('should throw an exception for non-valid selectors', async t => { 6 | for (const selector of [null, false, {}, 42]) { 7 | try { 8 | await AngularSelector(selector); 9 | await t.expect(false).ok('The selector should throw an error but it doesn\'t.'); 10 | } 11 | catch (e) { 12 | await t.expect(e.errMsg).contains(`If the selector parameter is passed it should be a string, but it was ${typeof selector}`); 13 | } 14 | } 15 | }); 16 | 17 | test('should throw an exception if window.ng does not not exist', async t => { 18 | try { 19 | await AngularSelector('list'); 20 | await t.expect(false).ok('The selector should throw an error but it doesn\'t.'); 21 | } 22 | catch (e) { 23 | await t.expect(e.errMsg).contains('The tested page does not use Angular or did not load correctly.'); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | name: No Response 2 | 3 | # Both `issue_comment` and `scheduled` event types are required for this Action 4 | # to work properly. 5 | on: 6 | issue_comment: 7 | types: [created] 8 | schedule: 9 | # Schedule for five minutes after the hour, every hour 10 | - cron: '5 * * * *' 11 | 12 | jobs: 13 | noResponse: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: lee-dohm/no-response@v0.5.0 17 | with: 18 | token: ${{ github.token }} 19 | daysUntilClose: 10 20 | responseRequiredLabel: "STATE: Need clarification" 21 | closeComment: > 22 | This issue was automatically closed because there was no response 23 | to our request for more information from the original author. 24 | Currently, we don't have enough information to take action. 25 | Please reach out to us if you find the necessary information 26 | and are able to share it. We are also eager to know if you resolved 27 | the issue on your own and can share your findings with everyone. 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | node_modules/* 50 | test/data/angular-15/node_modules/* 51 | package-lock.json 52 | test/data/angular-15/package-lock.json 53 | .idea/* 54 | lib/* 55 | test/data/lib/* 56 | test/data/angular-15/dist/* 57 | test/data/angular-15/.angular/* 58 | node_modules 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2017-2021 Developer Express Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/wait-for-angular-test.js: -------------------------------------------------------------------------------- 1 | import { waitForAngular } from '../src'; 2 | 3 | fixture `Wait for Angular`; 4 | 5 | test('default timeout', async t => { 6 | const start = +new Date(); 7 | 8 | try { 9 | await waitForAngular(); 10 | } 11 | catch (e) { 12 | const executionTime = +new Date() - start; 13 | 14 | await t 15 | .expect(executionTime).gte(10000) 16 | .expect(executionTime).lte(12000); 17 | } 18 | }); 19 | 20 | test('custom timeout', async t => { 21 | const start = +new Date(); 22 | 23 | try { 24 | await waitForAngular(1000); 25 | } 26 | catch (e) { 27 | const executionTime = +new Date() - start; 28 | 29 | await t 30 | .expect(executionTime).gte(1000) 31 | .expect(executionTime).lte(2000); 32 | } 33 | }); 34 | 35 | test('error message', async t => { 36 | try { 37 | await waitForAngular(100); 38 | await t.expect(false).ok('Should raise an error'); 39 | } 40 | catch (e) { 41 | await t.expect(e.errMsg).contains('Cannot find information about Angular components. The tested application should be deployed in development mode.'); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /test/data/angular-15/app/app.component.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | Object.defineProperty(exports, "__esModule", { value: true }); 9 | exports.AppComponent = void 0; 10 | var core_1 = require("@angular/core"); 11 | var AppComponent = /** @class */ (function () { 12 | function AppComponent() { 13 | this.rootProp1 = 1; 14 | this.version = core_1.VERSION.full; 15 | } 16 | AppComponent = __decorate([ 17 | (0, core_1.Component)({ 18 | selector: 'my-app', 19 | templateUrl: './app.component.html' 20 | }) 21 | ], AppComponent); 22 | return AppComponent; 23 | }()); 24 | exports.AppComponent = AppComponent; 25 | //# sourceMappingURL=app.component.js.map -------------------------------------------------------------------------------- /ts-defs/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Selector, ClientFunction } from 'testcafe'; 2 | 3 | interface Dictionary { 4 | [name: string]: any; 5 | } 6 | 7 | export type State = object | Dictionary; 8 | 9 | export type AngularComponent< 10 | S extends State = {}, 11 | > = { 12 | state: S, 13 | }; 14 | 15 | type DefaultAngularComponent = AngularComponent; 16 | 17 | declare global { 18 | type AngularComponent< 19 | S extends State = {}, 20 | > = { 21 | state: S, 22 | }; 23 | interface Selector { 24 | getAngular(): Promise; 25 | getAngular(filter?: (angularInternal: C) => T): Promise; 26 | } 27 | } 28 | 29 | export function waitForAngular(timeout?: number): Promise; 30 | 31 | export function AngularSelector(selector: string): Selector; 32 | export function AngularSelector(): Selector; 33 | 34 | export namespace AngularJSSelector { 35 | function byModel(model: string, parentSelector?: Selector): Selector; 36 | function byBinding(binding: string, parentSelector?: Selector): Selector; 37 | function byExactBinding(exactBinding: string, parentSelector?: Selector): Selector; 38 | function byOptions(options: string, parentSelector?: Selector): Selector; 39 | function byRepeater(repeater: string, parentSelector?: Selector): Selector; 40 | function byExactRepeater(exactRepeater: string, parentSelector?: Selector): Selector; 41 | } 42 | -------------------------------------------------------------------------------- /test/data/angular-15/app/list-item.component.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.ListItemComponent = void 0; 13 | var core_1 = require("@angular/core"); 14 | var ListItemComponent = /** @class */ (function () { 15 | function ListItemComponent() { 16 | } 17 | __decorate([ 18 | (0, core_1.Input)(), 19 | __metadata("design:type", String) 20 | ], ListItemComponent.prototype, "id", void 0); 21 | ListItemComponent = __decorate([ 22 | (0, core_1.Component)({ 23 | selector: 'list-item', 24 | template: '

{{id}}

' 25 | }) 26 | ], ListItemComponent); 27 | return ListItemComponent; 28 | }()); 29 | exports.ListItemComponent = ListItemComponent; 30 | //# sourceMappingURL=list-item.component.js.map -------------------------------------------------------------------------------- /test/angular-selector-test.js: -------------------------------------------------------------------------------- 1 | import { AngularSelector, waitForAngular } from '../src'; 2 | 3 | runTests('AngularSelector (Angular v15)', 'http://localhost:8080/test/data/angular-15/dist/index-aot.html'); 4 | 5 | function runTests (fixtureLabel, pageUrl) { 6 | fixture(fixtureLabel) 7 | .page(pageUrl) 8 | .beforeEach(async () => { 9 | await waitForAngular(); 10 | }); 11 | 12 | test('root', async t => { 13 | const root = AngularSelector(); 14 | const rootAngular = await root.getAngular(); 15 | 16 | await t 17 | .expect(root.exists).ok() 18 | .expect(rootAngular.rootProp1).eql(1); 19 | 20 | //eslint-disable-next-line 21 | await t.expect(rootAngular.hasOwnProperty('__ngContext__')).notOk(); 22 | }); 23 | 24 | test('selector', async t => { 25 | const list = AngularSelector('list'); 26 | const listAngular = await list.getAngular(); 27 | 28 | await t.expect(list.count).eql(2) 29 | .expect(AngularSelector('list-item').count).eql(6) 30 | .expect(listAngular.id).eql('list1'); 31 | }); 32 | 33 | test('composite selector', async t => { 34 | const listItem = AngularSelector('list list-item'); 35 | const listItemAngular6 = await listItem.nth(5).getAngular(); 36 | const listItemAngular5Id = listItem.nth(4).getAngular(({ state }) => state.id); 37 | 38 | await t.expect(listItem.count).eql(6) 39 | .expect(listItemAngular6.id).eql('list2-item3') 40 | .expect(listItemAngular5Id).eql('list2-item2'); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/data/angular-15/app/app.module.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | Object.defineProperty(exports, "__esModule", { value: true }); 9 | exports.AppModule = void 0; 10 | var core_1 = require("@angular/core"); 11 | var platform_browser_1 = require("@angular/platform-browser"); 12 | var app_component_1 = require("./app.component"); 13 | var list_component_1 = require("./list.component"); 14 | var list_item_component_1 = require("./list-item.component"); 15 | var AppModule = /** @class */ (function () { 16 | function AppModule() { 17 | } 18 | AppModule = __decorate([ 19 | (0, core_1.NgModule)({ 20 | imports: [ 21 | platform_browser_1.BrowserModule 22 | ], 23 | declarations: [ 24 | app_component_1.AppComponent, 25 | list_component_1.ListComponent, 26 | list_item_component_1.ListItemComponent 27 | ], 28 | bootstrap: [app_component_1.AppComponent] 29 | }) 30 | ], AppModule); 31 | return AppModule; 32 | }()); 33 | exports.AppModule = AppModule; 34 | //# sourceMappingURL=app.module.js.map -------------------------------------------------------------------------------- /test/data/angular-15/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "projects": { 5 | "angular-app": { 6 | "projectType": "application", 7 | "root": "", 8 | "sourceRoot": "app", 9 | "prefix": "app", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "index-aot.html", 16 | "main": "app/main.ts", 17 | "polyfills": "app/polyfills.ts", 18 | "tsConfig": "tsconfig.json", 19 | "aot": true 20 | }, 21 | "configurations": { 22 | "production": { 23 | "budgets": [ 24 | { 25 | "type": "initial", 26 | "maximumWarning": "500kb", 27 | "maximumError": "1mb" 28 | }, 29 | { 30 | "type": "anyComponentStyle", 31 | "maximumWarning": "2kb", 32 | "maximumError": "4kb" 33 | } 34 | ], 35 | "outputHashing": "all" 36 | }, 37 | "development": { 38 | "buildOptimizer": false, 39 | "optimization": false, 40 | "vendorChunk": true, 41 | "extractLicenses": false, 42 | "sourceMap": true, 43 | "namedChunks": true 44 | } 45 | }, 46 | "defaultConfiguration": "development" 47 | } 48 | } 49 | } 50 | }, 51 | "defaultProject": "angular-app", 52 | "cli": { 53 | "analytics": false 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testcafe-angular-selectors", 3 | "version": "0.4.1", 4 | "description": "Angular selectors for TestCafe", 5 | "author": { 6 | "name": "Developer Express Inc.", 7 | "url": "https://devexpress.com" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/devexpress/testcafe-angular-selectors" 12 | }, 13 | "main": "src/index", 14 | "files": [ 15 | "src", 16 | "ts-defs" 17 | ], 18 | "license": "MIT", 19 | "scripts": { 20 | "install-angular-15-deps": "cd ./test/data/angular-15 && npm i --force", 21 | "install-angular-deps": "npm run install-angular-15-deps", 22 | "lint": "eslint src/*.js test/*.js", 23 | "http-server": "http-server ./ -s", 24 | "compile-angular-15-app": "cd test/data/angular-15 && npx ng build angular-app", 25 | "compile-angular-apps": "npm run compile-angular-15-app", 26 | "testcafe": "testcafe chrome,firefox,edge test/*-test.js --app \"npm run http-server\"", 27 | "test": "npm run install-angular-deps && npm run lint && npm run compile-angular-apps && npm run testcafe", 28 | "publish-please": "publish-please", 29 | "prepublish": "publish-please guard" 30 | }, 31 | "devDependencies": { 32 | "@babel/eslint-parser": "^7.1.1", 33 | "eslint": "^8.17.0", 34 | "eslint-plugin-testcafe": "^0.2.1", 35 | "http-server": "^14.1.1", 36 | "publish-please": "^5.5.2", 37 | "testcafe": "^2.3.1" 38 | }, 39 | "keywords": [ 40 | "testcafe", 41 | "angular", 42 | "selectors", 43 | "plugin" 44 | ], 45 | "peerDependencies": { 46 | "testcafe": ">=0.18.0" 47 | }, 48 | "types": "./ts-defs/index.d.ts", 49 | "overrides": { 50 | "lodash": "^4.17.21" 51 | }, 52 | "resolutions": { 53 | "lodash": "^4.17.21" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/data/angular-15/app/list.component.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.ListComponent = void 0; 13 | var core_1 = require("@angular/core"); 14 | var ListComponent = /** @class */ (function () { 15 | function ListComponent() { 16 | } 17 | ListComponent.prototype.getId = function (postfix) { 18 | return this.id + '-item' + postfix; 19 | }; 20 | __decorate([ 21 | (0, core_1.Input)(), 22 | __metadata("design:type", String) 23 | ], ListComponent.prototype, "id", void 0); 24 | ListComponent = __decorate([ 25 | (0, core_1.Component)({ 26 | selector: 'list', 27 | template: "\n \n " 28 | }) 29 | ], ListComponent); 30 | return ListComponent; 31 | }()); 32 | exports.ListComponent = ListComponent; 33 | //# sourceMappingURL=list.component.js.map -------------------------------------------------------------------------------- /test/data/angular-15/systemjs-angular-loader.js: -------------------------------------------------------------------------------- 1 | // NOTE: https://github.com/angular/quickstart/blob/master/src/systemjs-angular-loader.js 2 | 3 | var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*)/gm; 4 | var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g; 5 | var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g; 6 | 7 | module.exports.translate = function(load){ 8 | if (load.source.indexOf('moduleId') != -1) return load; 9 | 10 | var url = document.createElement('a'); 11 | url.href = load.address; 12 | 13 | var basePathParts = url.pathname.split('/'); 14 | 15 | basePathParts.pop(); 16 | var basePath = basePathParts.join('/'); 17 | 18 | var baseHref = document.createElement('a'); 19 | baseHref.href = this.baseURL; 20 | baseHref = baseHref.pathname; 21 | 22 | if (!baseHref.startsWith('/base/')) { // it is not karma 23 | basePath = basePath.replace(baseHref, ''); 24 | } 25 | 26 | load.source = load.source 27 | .replace(templateUrlRegex, function(match, quote, url){ 28 | var resolvedUrl = url; 29 | 30 | if (url.startsWith('.')) { 31 | resolvedUrl = basePath + url.substr(1); 32 | } 33 | 34 | return 'templateUrl: "' + resolvedUrl + '"'; 35 | }) 36 | .replace(stylesRegex, function(match, relativeUrls) { 37 | var urls = []; 38 | 39 | while ((match = stringRegex.exec(relativeUrls)) !== null) { 40 | if (match[2].startsWith('.')) { 41 | urls.push('"' + basePath + match[2].substr(1) + '"'); 42 | } else { 43 | urls.push('"' + match[2] + '"'); 44 | } 45 | } 46 | 47 | return "styleUrls: [" + urls.join(', ') + "]"; 48 | }); 49 | 50 | return load; 51 | }; 52 | -------------------------------------------------------------------------------- /.github/workflows/handle-stale.yml: -------------------------------------------------------------------------------- 1 | name: "Mark stale issues and pull requests" 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | workflow_dispatch: 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v3 11 | with: 12 | stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for a long period. It will be closed and archived if no further activity occurs. However, we may return to this issue in the future. If it still affects you or you have any additional information regarding it, please leave a comment and we will keep it open." 13 | stale-pr-message: "This pull request has been automatically marked as stale because it has not had any activity for a long period. It will be closed and archived if no further activity occurs. However, we may return to this pull request in the future. If it is still relevant or you have any additional information regarding it, please leave a comment and we will keep it open." 14 | close-issue-message: "We're closing this issue after a prolonged period of inactivity. If it still affects you, please add a comment to this issue with up-to-date information. Thank you." 15 | close-pr-message: "We're closing this pull request after a prolonged period of inactivity. If it is still relevant, please ask for this pull request to be reopened. Thank you." 16 | stale-issue-label: "STATE: Stale" 17 | stale-pr-label: "STATE: Stale" 18 | days-before-stale: 365 19 | days-before-close: 10 20 | exempt-issue-labels: "AREA: docs,FREQUENCY: critical,FREQUENCY: level 2,HELP WANTED,!IMPORTANT!,STATE: Need clarification,STATE: Need response,STATE: won't fix,support center" 21 | exempt-pr-labels: "AREA: docs,FREQUENCY: critical,FREQUENCY: level 2,HELP WANTED,!IMPORTANT!,STATE: Need clarification,STATE: Need response,STATE: won't fix,support center" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | The TestCafe team no longer maintains the `testcafe-angular-selectors` repository. If you want to take over the project, we'll be happy to hand it over. To contact the team, create a new GitHub issue. 3 | 4 | ## testcafe-angular-selectors 5 | 6 | This plugin provides [Selector](https://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/selectors.html) extensions that make it easier to test Angular applications with [TestCafe](https://github.com/DevExpress/testcafe/). 7 | These extensions allow you to create a `Selector` to find elements on the page in a way that is native to Angular applications. 8 | 9 | ### Install 10 | 11 | ```sh 12 | npm install testcafe-angular-selectors 13 | ``` 14 | 15 | ### Usage 16 | 17 | This module includes separate helpers for Angular and AngularJS applications. 18 | 19 | See the following topics for more details: 20 | 21 | * [Angular Selector extentions](./angular-selector.md) 22 | * [AngularJS Selector extentions](./angularJS-selector.md) 23 | 24 | ### Examples 25 | 26 | For AngularJS applications, you need to use `AngularJSSelector` that contains a set of static methods to search by the specified bindings (`byModel`, `byBinding` and etc.). 27 | 28 | ```js 29 | import { AngularJSSelector } from 'testcafe-angular-selectors'; 30 | ... 31 | const newTodoItem = AngularJSSelector.byModel('newTodo'); 32 | ``` 33 | 34 | For Angular applications, this module provides the capability to select an HTML element by an Angular's component selector or nested component selectors. 35 | You can find more information about Angular's component selector in the [angular.io documentation topic](https://angular.io/api/core/Component). 36 | Also, this module provides the `waitForAngular` helper method. Use it to wait until Angular's component tree is loaded. 37 | 38 | ```js 39 | import { AngularSelector, waitForAngular } from 'testcafe-angular-selectors'; 40 | 41 | fixture `App tests` 42 | .page('http://angular-app-url') 43 | .beforeEach(async () => { 44 | await waitForAngular(); 45 | }); 46 | 47 | test('test', async t => { 48 | const firstListItem = AngularSelector('list list-item'); 49 | }); 50 | ``` 51 | -------------------------------------------------------------------------------- /src/wait-for-angular.js: -------------------------------------------------------------------------------- 1 | /*global Promise*/ 2 | 3 | const { ClientFunction } = require('testcafe'); 4 | 5 | module.exports = ClientFunction(ms => { 6 | return new Promise((resolve, reject) => { 7 | let pingIntervalId = null; 8 | 9 | let pingTimeoutId = null; 10 | const WAIT_TIMEOUT = ms || 10000; 11 | const PING_INTERVAL = 100; 12 | 13 | const clearTimeouts = () => { 14 | window.clearTimeout(pingTimeoutId); 15 | window.clearInterval(pingIntervalId); 16 | }; 17 | 18 | const getFirstRootElement = () => { 19 | if (typeof window.getAllAngularRootElements === 'function') { 20 | const rootElements = window.getAllAngularRootElements(); 21 | 22 | return rootElements && rootElements.length ? rootElements[0] : null; 23 | } 24 | 25 | return null; 26 | }; 27 | 28 | const isElementInjectorExists = (firstRootElement) => { 29 | if (window.ng) { 30 | // NOTE: Angular version 9 or higher 31 | if (typeof window.ng.getInjector === 'function') { 32 | const firstRootInjector = window.ng.getInjector(firstRootElement); 33 | const injectorConstructorName = firstRootInjector && firstRootInjector.constructor && 34 | firstRootInjector.constructor.name; 35 | 36 | return !!injectorConstructorName && injectorConstructorName.toLowerCase() === 'nodeinjector'; 37 | } 38 | // NOTE: Angular version 8 or lower 39 | else if (typeof window.ng.probe === 'function') { 40 | const firstRootDebugElement = window.ng.probe(firstRootElement); 41 | 42 | return !!(firstRootDebugElement && firstRootDebugElement.injector); 43 | } 44 | } 45 | 46 | return false; 47 | }; 48 | 49 | const isThereAngularInDevelopmentMode = () => { 50 | const firstRootElement = getFirstRootElement(); 51 | 52 | return !!firstRootElement && isElementInjectorExists(firstRootElement); 53 | }; 54 | 55 | const check = () => { 56 | if (isThereAngularInDevelopmentMode()) { 57 | clearTimeouts(); 58 | resolve(); 59 | } 60 | }; 61 | 62 | pingTimeoutId = window.setTimeout(() => { 63 | clearTimeouts(); 64 | reject(new Error(`Cannot find information about Angular components. The tested application should be deployed in development mode. 65 | For more information, see https://angular.io/guide/deployment.`)); 66 | }, WAIT_TIMEOUT); 67 | 68 | check(); 69 | pingIntervalId = window.setInterval(check, PING_INTERVAL); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/data/angular-15/systemjs.config.js: -------------------------------------------------------------------------------- 1 | /*global System*/ 2 | 3 | (function () { 4 | System.config({ 5 | paths: { 6 | 'npm:': '/node_modules/' 7 | }, 8 | map: { 9 | 'app': 'app', 10 | '@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js', 11 | '@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js', 12 | '@angular/core': 'npm:@angular/core/bundles/core.umd.js', 13 | '@angular/common': 'npm:@angular/common/bundles/common.umd.js', 14 | '@angular/common/http': 'npm:@angular/common/bundles/common-http.umd.js', 15 | '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', 16 | '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', 17 | '@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js', 18 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 19 | '@angular/router': 'npm:@angular/router/bundles/router.umd.js', 20 | '@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js', 21 | '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', 22 | '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js', 23 | '@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js', 24 | 'rxjs': 'npm:rxjs', 25 | 'tslib': 'npm:tslib/tslib.js', 26 | 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js' 27 | }, 28 | packages: { 29 | app: { 30 | main: './main.js', 31 | defaultExtension: 'js', 32 | meta: { 33 | './*.js': { 34 | loader: 'systemjs-angular-loader.js' 35 | } 36 | } 37 | }, 38 | 'rxjs/ajax': { 39 | main: 'index.js', 40 | defaultExtension: 'js' 41 | }, 42 | 'rxjs/operators': { 43 | main: 'index.js', 44 | defaultExtension: 'js' 45 | }, 46 | 'rxjs/testing': { 47 | main: 'index.js', 48 | defaultExtension: 'js' 49 | }, 50 | 'rxjs/websocket': { 51 | main: 'index.js', 52 | defaultExtension: 'js' 53 | }, 54 | 'rxjs': { 55 | main: 'index.js', 56 | defaultExtension: 'js' 57 | }, 58 | } 59 | }); 60 | })(); 61 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "parserOptions": { 4 | "requireConfigFile": false 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "no-alert": 2, 9 | "no-array-constructor": 2, 10 | "no-caller": 2, 11 | "no-catch-shadow": 2, 12 | "no-console": 0, 13 | "no-eval": 2, 14 | "no-extend-native": 2, 15 | "no-extra-bind": 2, 16 | "no-implied-eval": 2, 17 | "no-iterator": 2, 18 | "no-label-var": 2, 19 | "no-labels": 2, 20 | "no-lone-blocks": 2, 21 | "no-loop-func": 2, 22 | "no-multi-str": 2, 23 | "no-native-reassign": 2, 24 | "no-new": 2, 25 | "no-new-func": 0, 26 | "no-new-object": 2, 27 | "no-new-wrappers": 2, 28 | "no-octal-escape": 2, 29 | "no-proto": 2, 30 | "no-return-assign": 2, 31 | "no-script-url": 2, 32 | "no-sequences": 2, 33 | "no-shadow": 2, 34 | "no-shadow-restricted-names": 2, 35 | "no-spaced-func": 2, 36 | "no-undef-init": 2, 37 | "no-unused-expressions": 2, 38 | "no-with": 2, 39 | "camelcase": 2, 40 | "comma-spacing": 2, 41 | "consistent-return": 2, 42 | "eqeqeq": 2, 43 | "semi": 2, 44 | "semi-spacing": [ 45 | 2, 46 | { 47 | "before": false, 48 | "after": true 49 | } 50 | ], 51 | "space-infix-ops": 2, 52 | "space-unary-ops": [ 53 | 2, 54 | { 55 | "words": true, 56 | "nonwords": false 57 | } 58 | ], 59 | "yoda": [ 60 | 2, 61 | "never" 62 | ], 63 | "brace-style": [ 64 | 2, 65 | "stroustrup", 66 | { 67 | "allowSingleLine": false 68 | } 69 | ], 70 | "eol-last": 2, 71 | "indent": 2, 72 | "key-spacing": [ 73 | 2, 74 | { 75 | "align": "value" 76 | } 77 | ], 78 | "max-nested-callbacks": [ 79 | 2, 80 | 3 81 | ], 82 | "new-parens": 2, 83 | "newline-after-var": [ 84 | 2, 85 | "always" 86 | ], 87 | "no-lonely-if": 2, 88 | "no-multiple-empty-lines": [ 89 | 2, 90 | { 91 | "max": 2 92 | } 93 | ], 94 | "no-nested-ternary": 2, 95 | "no-underscore-dangle": 0, 96 | "no-unneeded-ternary": 2, 97 | "object-curly-spacing": [ 98 | 2, 99 | "always" 100 | ], 101 | "operator-assignment": [ 102 | 2, 103 | "always" 104 | ], 105 | "quotes": [ 106 | 2, 107 | "single", 108 | "avoid-escape" 109 | ], 110 | "space-before-blocks": [ 111 | 2, 112 | "always" 113 | ], 114 | "prefer-const": 2, 115 | "no-path-concat": 2, 116 | "no-undefined": 2, 117 | "keyword-spacing": 2, 118 | "strict": 0, 119 | "curly": [ 120 | 2, 121 | "multi-or-nest" 122 | ], 123 | "dot-notation": 0, 124 | "no-else-return": 2, 125 | "one-var": [ 126 | 2, 127 | "never" 128 | ], 129 | "no-multi-spaces": [ 130 | 2, 131 | { 132 | "exceptions": { 133 | "VariableDeclarator": true, 134 | "AssignmentExpression": true 135 | } 136 | } 137 | ], 138 | "radix": 2, 139 | "no-extra-parens": 2, 140 | "new-cap": [ 141 | 2, 142 | { 143 | "capIsNew": false 144 | } 145 | ], 146 | "space-before-function-paren": [ 147 | 2, 148 | "always" 149 | ], 150 | "no-use-before-define": [ 151 | 2, 152 | "nofunc" 153 | ], 154 | "handle-callback-err": 0 155 | }, 156 | "env": { 157 | "node": true, 158 | "browser": true 159 | } 160 | } -------------------------------------------------------------------------------- /angular-selector.md: -------------------------------------------------------------------------------- 1 | # Angular Selector Extentions 2 | 3 | ## Prerequisites 4 | 5 | Ensure that your application is bootstrapped in the development mode. 6 | By default, this is true, unless the code contains the [enableProdMode](https://angular.io/api/core/enableProdMode) method call. 7 | 8 | If your application runs in the production mode, you won't be able to use `AngularSelector`. 9 | 10 | ## Usage 11 | 12 | ### Wait for application to be ready to run tests 13 | 14 | To wait until the Angular's component tree is loaded, add the `waitForAngular` method to fixture's `beforeEach` hook. 15 | 16 | ```js 17 | import { waitForAngular } from 'testcafe-angular-selectors'; 18 | 19 | fixture `App tests` 20 | .page('http://angular-app-url') 21 | .beforeEach(async () => { 22 | await waitForAngular(); 23 | }); 24 | ``` 25 | 26 | Default timeout for `waitForAngular` is 10000 ms. 27 | You can specify a custom timeout value - `waitForAngular (5000)`. 28 | 29 | ### Create selectors for Angular components 30 | 31 | `AngularSelector` allows you to select an HTML element by Angular's component selector or nested component selectors. 32 | 33 | Suppose you have the following markup 34 | 35 | ```html 36 | 37 | 38 | 39 | 40 | ``` 41 | 42 | To get the root Angular element, use the `AngularSelector` constructor without parameters. 43 | 44 | ```js 45 | import { AngularSelector } from 'testcafe-angular-selectors'; 46 | ... 47 | const rootAngular = AngularSelector(); 48 | ``` 49 | 50 | The rootAngular variable will contain the `` element. 51 | 52 | > If your application has multiple roots, `AngularSelector` will return the first root returned by the `window.getAllAngularRootElements` function 53 | 54 | To get a root DOM element for a component, pass the component selector to the `AngularSelector` constructor. 55 | 56 | ```js 57 | import { AngularSelector } from 'testcafe-angular-selectors'; 58 | 59 | const listComponent = AngularSelector('list'); 60 | ``` 61 | 62 | To obtain a nested component, you can use a combined selector. 63 | 64 | ```js 65 | import { AngularSelector } from 'testcafe-angular-selectors'; 66 | 67 | const listItemComponent = AngularSelector('list list-item'); 68 | ``` 69 | 70 | You can combine Angular selectors with TestCafe's Selector filter functions like `.find`, `.withText`, `.nth` and [others](http://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/selectors.html#functional-style-selectors). 71 | 72 | ```js 73 | import { AngularSelector } from 'testcafe-angular-selectors'; 74 | 75 | const myAppTitle = AngularSelector().find('h1'); 76 | 77 | ``` 78 | 79 | See more examples [here](test/angular-selector-test.js). 80 | 81 | ### Obtaining component's state 82 | 83 | As an alternative to [DOM Node State properties](http://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/dom-node-state.html), you can obtain the state of an Angular component. 84 | 85 | To obtain the component state, use the Angular selector's `.getAngular()` method. 86 | 87 | The `.getAngular()` method returns a [client function](https://devexpress.github.io/testcafe/documentation/test-api/obtaining-data-from-the-client.html). This function resolves to an object that contains component's state. 88 | 89 | The returned client function can be passed to assertions activating the [Smart Assertion Query mechanism](https://devexpress.github.io/testcafe/documentation/test-api/assertions/#smart-assertion-query-mechanism). 90 | 91 | ```js 92 | import { AngularSelector } from 'testcafe-angular-selectors'; 93 | 94 | const list = AngularSelector('list'); 95 | const listAngular = await list.getAngular(); 96 | ... 97 | await t.expect(listAngular.testProp).eql(1); 98 | ``` 99 | 100 | As an alternative, the `.getAngular()` method can take a function that returns the required state property. 101 | This function acts as a filter. Its argument is an object returned by `.getAngular()`, i.e. `{ state: ...}`. 102 | 103 | ```js 104 | import { AngularSelector } from 'testcafe-angular-selectors'; 105 | 106 | const list = AngularSelector('list'); 107 | ... 108 | await t.expect(list.getAngular(({ state }) => state.testProp)).eql(1); 109 | ``` 110 | -------------------------------------------------------------------------------- /src/angular-selector.js: -------------------------------------------------------------------------------- 1 | const { Selector } = require('testcafe'); 2 | 3 | module.exports = Selector(complexSelector => { 4 | function validateSelector (selector) { 5 | if (selector !== void 0 && typeof selector !== 'string') 6 | throw new Error(`If the selector parameter is passed it should be a string, but it was ${typeof selector}`); 7 | } 8 | 9 | validateSelector(complexSelector); 10 | 11 | // NOTE: Angular version 9 or higher 12 | const walkingNativeElementsMode = window.ng && typeof window.ng.getComponent === 'function'; 13 | // NOTE: Angular version 8 or lower 14 | const walkingDebugElementsMode = window.ng && typeof window.ng.probe === 'function'; 15 | 16 | const isPageReadyForTesting = (walkingNativeElementsMode || walkingDebugElementsMode) && 17 | typeof window.getAllAngularRootElements === 'function'; 18 | 19 | if (!isPageReadyForTesting) { 20 | throw new Error(`The tested page does not use Angular or did not load correctly. 21 | Use the 'waitForAngular' function to ensure the page is ready for testing.`); 22 | } 23 | 24 | function getNativeElementTag (nativeElement) { 25 | return nativeElement.tagName.toLowerCase(); 26 | } 27 | 28 | function getTagList (componentSelector) { 29 | return componentSelector 30 | .split(' ') 31 | .filter(el => !!el) 32 | .map(el => el.trim().toLowerCase()); 33 | } 34 | 35 | function filterNodes (rootElement, tags) { 36 | const foundNodes = []; 37 | 38 | function walkElements (element, tagIndex, checkFn) { 39 | if (checkFn(element, tagIndex)) { 40 | if (tagIndex === tags.length - 1) { 41 | if (walkingNativeElementsMode) 42 | foundNodes.push(element); 43 | else 44 | foundNodes.push(element.nativeElement); 45 | 46 | return; 47 | } 48 | 49 | tagIndex++; 50 | } 51 | 52 | for (const childElement of element.children) 53 | walkElements(childElement, tagIndex, checkFn); 54 | } 55 | 56 | function checkDebugElement (debugElement, tagIndex) { 57 | if (!debugElement.componentInstance) 58 | return false; 59 | 60 | return tags[tagIndex] === getNativeElementTag(debugElement.nativeElement); 61 | } 62 | 63 | function checkNativeElement (nativeElement, tagIndex) { 64 | const componentInstance = window.ng.getComponent(nativeElement); 65 | 66 | if (!componentInstance) 67 | return false; 68 | 69 | return tags[tagIndex] === getNativeElementTag(nativeElement); 70 | } 71 | 72 | if (walkingNativeElementsMode) 73 | walkElements(rootElement, 0, checkNativeElement); 74 | else { 75 | const debugElementRoot = window.ng.probe(rootElement); 76 | 77 | walkElements(debugElementRoot, 0, checkDebugElement); 78 | } 79 | 80 | return foundNodes; 81 | } 82 | 83 | // NOTE: If there are multiple roots on the page we find a target in the first root only 84 | const rootElement = window.getAllAngularRootElements()[0]; 85 | 86 | if (!complexSelector) 87 | return rootElement; 88 | 89 | const tags = getTagList(complexSelector); 90 | 91 | return filterNodes(rootElement, tags); 92 | 93 | }).addCustomMethods({ 94 | getAngular: (node, fn) => { 95 | let state; 96 | 97 | // NOTE: Angular version 9 or higher 98 | if (typeof window.ng.getComponent === 'function') { 99 | state = window.ng.getComponent(node); 100 | 101 | // NOTE: We cannot handle this circular reference in a replicator. So we remove it from the returned component state. 102 | if (state && '__ngContext__' in state) 103 | state = JSON.parse(JSON.stringify(state, (key, value) => key !== '__ngContext__' ? value : void 0)); 104 | } 105 | // NOTE: Angular version 8 or lower 106 | else { 107 | const debugElement = window.ng.probe(node); 108 | 109 | state = debugElement && debugElement.componentInstance; 110 | } 111 | 112 | if (typeof fn === 'function') 113 | return fn({ state }); 114 | 115 | return state; 116 | } 117 | }); 118 | -------------------------------------------------------------------------------- /angularJS-selector.md: -------------------------------------------------------------------------------- 1 | # AngularJS selector extentions 2 | `AngularJSSelector` contains a set of static methods to search for an HTML element by the specified binding (`byModel`, `byBinding` and etc.). 3 | 4 | ## Usage 5 | 6 | ```js 7 | import { AngularJSSelector } from 'testcafe-angular-selectors'; 8 | import { Selector } from 'testcafe'; 9 | 10 | fixture `TestFixture` 11 | .page('http://todomvc.com/examples/angularjs/'); 12 | 13 | test('add new item', async t => { 14 | await t 15 | .typeText(AngularJSSelector.byModel('newTodo'), 'new item') 16 | .pressKey('enter') 17 | .expect(Selector('#todo-list').visible).ok(); 18 | }); 19 | ``` 20 | 21 | See more examples [here](test/angularjs-selector-test.js). 22 | 23 | ## API 24 | ### byBinding 25 | Find elements by text binding. Does a partial match, so any elements bound to variables containing the input string will be returned. 26 | ```js 27 | AngularJSSelector.byBinding(bindingDescriptor, parentSelector) 28 | ``` 29 | Parameter | Description 30 | --------------------------- | ----------- 31 | bindingDescriptor | The JavaScript expression to which the element's `textContent` is bound. 32 | parentSelector *(optional)* | A TestCafe [selector](https://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/selectors.html). If specified, TestCafe will search for the target element among the descendants of the element identified by this selector. 33 | 34 | > We don't support deprecated syntax `AngularJSSelector.byBinding('{{person.name}}')` 35 | 36 | ### byExactBinding 37 | Find elements by exact binding. 38 | ```js 39 | AngularJSSelector.byExactBinding(bindingDescriptor, parentSelector) 40 | ``` 41 | Parameter | Description 42 | --------------------------- | ----------- 43 | bindingDescriptor | The JavaScript expression to which the element's `textContent` is bound. 44 | parentSelector *(optional)* | A TestCafe [selector](https://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/selectors.html). If specified, TestCafe will search for the target element among the descendants of the element identified by this selector. 45 | 46 | ### byModel 47 | Find elements by 'ng-model' expression 48 | ```js 49 | AngularJSSelector.byModel(model, parentSelector) 50 | ``` 51 | Parameter | Description 52 | --------------------------- | ----------- 53 | model | The JavaScript expression used to bind a property on the scope to an input, select, textarea (or a custom form control). 54 | parentSelector *(optional)* | A TestCafe [selector](https://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/selectors.html). If specified, TestCafe will search for the target element among the descendants of the element identified by this selector. 55 | 56 | ### byOptions 57 | 58 | Find elements by 'ng-options' expression. 59 | ```js 60 | AngularJSSelector.byOptions(optionsDescriptor, parentSelector) 61 | ``` 62 | Parameter | Description 63 | --------------------------- | ----------- 64 | optionsDescriptor | The JavaScript expression used to generate a list of