diff --git a/.gitignore b/.gitignore index 9995f912d..7b21aa525 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ tags .DS_Store npm-debug.log nativescript-angular*.tgz +package-lock.json tests/app/**/*.js tests/test-output.txt diff --git a/.travis.yml b/.travis.yml index 8fe66b1b1..273f0a9c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,22 +2,24 @@ env: global: - NODE_VERSION=6.11.1 - EMULATOR_API_LEVEL=19 - - ANDROID_VERSION=23 - - ANDROID_BUILD_TOOLS_VERSION=23.0.1 + - ANDROID_VERSION=25 + - ANDROID_BUILD_TOOLS_VERSION=25.0.2 - ANDROID_ABI=armeabi-v7a - - ANDROID_TAG=google_apis - - APPIUM_VERSION=1.6.5 + - APPIUM_VERSION=1.7.0 + - EMULATOR_NAME=test language: android jdk: - oraclejdk8 android: components: - - platform-tools - tools + - platform-tools - build-tools-$ANDROID_BUILD_TOOLS_VERSION - android-$ANDROID_VERSION + - android-$EMULATOR_API_LEVEL + - extra-android-support - extra-android-m2repository - - sys-img-armeabi-v7a-android-$EMULATOR_API_LEVEL + - sys-img-$ANDROID_ABI-android-$EMULATOR_API_LEVEL before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock cache: @@ -27,6 +29,7 @@ cache: install: - nvm install $NODE_VERSION - npm install -g typings +- echo y | $ANDROID_HOME/tools/android update sdk --filter tools --all --no-ui - wget -O ./nativescript.tgz "https://s3.amazonaws.com/nativescript-ci/build_result/nativescript.tgz" - npm install -g nativescript.tgz --ignore-scripts - tns usage-reporting disable @@ -36,12 +39,10 @@ install: - npm run tslint - cd ../tests - npm install -- npm i appium@$APPIUM_VERSION before_script: - - echo no | android create avd --force -n test -t android-$EMULATOR_API_LEVEL --abi $ANDROID_ABI - - emulator -memory 1024 -avd test -no-audio -no-window & + - echo no | android create avd --force -n $EMULATOR_NAME -t android-$EMULATOR_API_LEVEL --abi $ANDROID_ABI -c 12M + - emulator -avd $EMULATOR_NAME -no-skin -no-audio -no-window & - android-wait-for-emulator script: - tns build android - android-wait-for-emulator -- npm run appium --runtype=android19 --appium=$APPIUM_VERSION diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bddf5fb4..abcff17b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ + +# [4.4.0](https://github.com/NativeScript/nativescript-angular/compare/4.2.0...4.4.0) (2017-10-11) + + +### Bug Fixes + +* **forms:** default to unsetValue for value accessors (#846) ([6940955](https://github.com/NativeScript/nativescript-angular/commit/6940955)) +* **forms:** disable onTouch for date, number and selectedIndex value accessors (#986) ([b4b5ef6](https://github.com/NativeScript/nativescript-angular/commit/b4b5ef6)), closes [#887](https://github.com/NativeScript/nativescript-angular/issues/887) + + +### Features + +* add `exportAs` logic for `isActive` on `routerLinkActive` directive (#940) ([147d35a](https://github.com/NativeScript/nativescript-angular/commit/147d35a)) +* **Http:** expand support for request on local files (#982) ([b95184f](https://github.com/NativeScript/nativescript-angular/commit/b95184f)) +* **styling:** Allow loading .css files as a fallback if no .scss file is found(#954) (#955) ([696e914](https://github.com/NativeScript/nativescript-angular/commit/696e914)) +* Angular 4.4 support (#1002) ([c264453](https://github.com/NativeScript/nativescript-angular/commit/c264453)) + + +### Reverts + +* **router:** page navigation with RouteReuseStrategy (#987) ([421978e](https://github.com/NativeScript/nativescript-angular/commit/421978e)) + + + # [4.2.0](https://github.com/NativeScript/nativescript-angular/compare/3.1.3...4.2.0) (2017-08-09) @@ -12,7 +36,7 @@ module (usually named `AppModule`). All other NgModules in the app (both feature and lazy-loaded ones) should import the `NativeScriptCommonModule` instead. -The behaviour is alligned with `BrowserModule` and `CommonModule` in web +The behavior is aligned with `BrowserModule` and `CommonModule` in web Angular apps described in [this](https://angular.io/guide/ngmodule-faq#q-browser-vs-common-module) guide. Migration steps: diff --git a/README.md b/README.md index 44d7e2a50..2416a971a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ $ tns test android --emulator # Developer workflow: ## Setup: -Use `npm link` to link `nativescript-angular` in `tests` and `ng-sample` progects: +Use `npm link` to link `nativescript-angular` in `tests` and `ng-sample` projects: ``` cd nativescript-angular diff --git a/e2e/renderer/.gitignore b/e2e/renderer/.gitignore new file mode 100644 index 000000000..5a67ca289 --- /dev/null +++ b/e2e/renderer/.gitignore @@ -0,0 +1,9 @@ +platforms +node_modules +hooks + +app/**/*.js +e2e/**/*.js +test-results.xml +e2e/reports + diff --git a/e2e/renderer/.vscode/launch.json b/e2e/renderer/.vscode/launch.json new file mode 100644 index 000000000..cd10b18ce --- /dev/null +++ b/e2e/renderer/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Mocha Tests", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "args": [ + "-u", + "tdd", + "--timeout", + "999999", + "--colors", + "--reuseDevice", + "--runType", + "sim.iPhone7.iOS110", + "${workspaceRoot}/e2e" + ], + "internalConsoleOptions": "openOnSessionStart" + } + ] + } \ No newline at end of file diff --git a/e2e/renderer/.vscode/settings.json b/e2e/renderer/.vscode/settings.json new file mode 100644 index 000000000..699fb4408 --- /dev/null +++ b/e2e/renderer/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "typescript.check.tscVersion": false, + "window.zoomLevel": 0, + "workbench.iconTheme": "vscode-icons", + "vsicons.dontShowNewVersionMessage": true, + "workbench.colorTheme": "Default Light+", + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/*.js": true, + "**/*.map": true, + "node_modules": true, + "hooks": true, + "platforms": true + } +} \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/Android/AndroidManifest.xml b/e2e/renderer/app/App_Resources/Android/AndroidManifest.xml new file mode 100644 index 000000000..9db832151 --- /dev/null +++ b/e2e/renderer/app/App_Resources/Android/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/e2e/renderer/app/App_Resources/Android/app.gradle b/e2e/renderer/app/App_Resources/Android/app.gradle new file mode 100644 index 000000000..b9e2b2982 --- /dev/null +++ b/e2e/renderer/app/App_Resources/Android/app.gradle @@ -0,0 +1,23 @@ +// Add your native dependencies here: + +// Uncomment to add recyclerview-v7 dependency +//dependencies { +// compile 'com.android.support:recyclerview-v7:+' +//} + +android { + defaultConfig { + generatedDensities = [] + applicationId = "org.nativescript.renderer" + + //override supported platforms + // ndk { + // abiFilters.clear() + // abiFilters "armeabi-v7a" + // } + + } + aaptOptions { + additionalParameters "--no-version-vectors" + } +} diff --git a/e2e/renderer/app/App_Resources/Android/drawable-hdpi/background.png b/e2e/renderer/app/App_Resources/Android/drawable-hdpi/background.png new file mode 100644 index 000000000..eb381c258 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-hdpi/background.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-hdpi/icon.png b/e2e/renderer/app/App_Resources/Android/drawable-hdpi/icon.png new file mode 100755 index 000000000..1034356e2 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-hdpi/icon.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-hdpi/logo.png b/e2e/renderer/app/App_Resources/Android/drawable-hdpi/logo.png new file mode 100644 index 000000000..5218f4c90 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-hdpi/logo.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-ldpi/background.png b/e2e/renderer/app/App_Resources/Android/drawable-ldpi/background.png new file mode 100644 index 000000000..748b2adf5 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-ldpi/background.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-ldpi/icon.png b/e2e/renderer/app/App_Resources/Android/drawable-ldpi/icon.png new file mode 100755 index 000000000..ddfc17a71 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-ldpi/icon.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-ldpi/logo.png b/e2e/renderer/app/App_Resources/Android/drawable-ldpi/logo.png new file mode 100644 index 000000000..b9e102a76 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-ldpi/logo.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-mdpi/background.png b/e2e/renderer/app/App_Resources/Android/drawable-mdpi/background.png new file mode 100644 index 000000000..efeaf2907 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-mdpi/background.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-mdpi/icon.png b/e2e/renderer/app/App_Resources/Android/drawable-mdpi/icon.png new file mode 100755 index 000000000..486e41091 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-mdpi/icon.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-mdpi/logo.png b/e2e/renderer/app/App_Resources/Android/drawable-mdpi/logo.png new file mode 100644 index 000000000..626338766 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-mdpi/logo.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-nodpi/splash_screen.xml b/e2e/renderer/app/App_Resources/Android/drawable-nodpi/splash_screen.xml new file mode 100644 index 000000000..ada77f92c --- /dev/null +++ b/e2e/renderer/app/App_Resources/Android/drawable-nodpi/splash_screen.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/Android/drawable-xhdpi/background.png b/e2e/renderer/app/App_Resources/Android/drawable-xhdpi/background.png new file mode 100644 index 000000000..612bbd072 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-xhdpi/background.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-xhdpi/icon.png b/e2e/renderer/app/App_Resources/Android/drawable-xhdpi/icon.png new file mode 100644 index 000000000..f29188209 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-xhdpi/icon.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-xhdpi/logo.png b/e2e/renderer/app/App_Resources/Android/drawable-xhdpi/logo.png new file mode 100644 index 000000000..ad8ee2f4b Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-xhdpi/logo.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-xxhdpi/background.png b/e2e/renderer/app/App_Resources/Android/drawable-xxhdpi/background.png new file mode 100644 index 000000000..0fa88e235 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-xxhdpi/background.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-xxhdpi/icon.png b/e2e/renderer/app/App_Resources/Android/drawable-xxhdpi/icon.png new file mode 100644 index 000000000..4f69cb25b Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-xxhdpi/icon.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-xxhdpi/logo.png b/e2e/renderer/app/App_Resources/Android/drawable-xxhdpi/logo.png new file mode 100644 index 000000000..668327832 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-xxhdpi/logo.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-xxxhdpi/background.png b/e2e/renderer/app/App_Resources/Android/drawable-xxxhdpi/background.png new file mode 100644 index 000000000..c650f6438 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-xxxhdpi/background.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-xxxhdpi/icon.png b/e2e/renderer/app/App_Resources/Android/drawable-xxxhdpi/icon.png new file mode 100644 index 000000000..50887a856 Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-xxxhdpi/icon.png differ diff --git a/e2e/renderer/app/App_Resources/Android/drawable-xxxhdpi/logo.png b/e2e/renderer/app/App_Resources/Android/drawable-xxxhdpi/logo.png new file mode 100644 index 000000000..fa6331c8d Binary files /dev/null and b/e2e/renderer/app/App_Resources/Android/drawable-xxxhdpi/logo.png differ diff --git a/e2e/renderer/app/App_Resources/Android/values-v21/colors.xml b/e2e/renderer/app/App_Resources/Android/values-v21/colors.xml new file mode 100644 index 000000000..a64641a9d --- /dev/null +++ b/e2e/renderer/app/App_Resources/Android/values-v21/colors.xml @@ -0,0 +1,4 @@ + + + #3d5afe + \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/Android/values-v21/styles.xml b/e2e/renderer/app/App_Resources/Android/values-v21/styles.xml new file mode 100644 index 000000000..dac8727c8 --- /dev/null +++ b/e2e/renderer/app/App_Resources/Android/values-v21/styles.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/Android/values/colors.xml b/e2e/renderer/app/App_Resources/Android/values/colors.xml new file mode 100644 index 000000000..74ad8829c --- /dev/null +++ b/e2e/renderer/app/App_Resources/Android/values/colors.xml @@ -0,0 +1,7 @@ + + + #F5F5F5 + #757575 + #33B5E5 + #272734 + \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/Android/values/styles.xml b/e2e/renderer/app/App_Resources/Android/values/styles.xml new file mode 100644 index 000000000..1e8c7f29b --- /dev/null +++ b/e2e/renderer/app/App_Resources/Android/values/styles.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..1953734f4 --- /dev/null +++ b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,92 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "icon-83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png new file mode 100644 index 000000000..9e15af09d Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png new file mode 100644 index 000000000..7b9e55537 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png new file mode 100644 index 000000000..76f61ec1f Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png new file mode 100644 index 000000000..15b06db11 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 000000000..585065f94 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 000000000..a450c421d Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 000000000..457b6d94c Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 000000000..fa5a6ac86 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png new file mode 100644 index 000000000..94abcf70d Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 000000000..2e71dd3a0 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 000000000..4abc9ec50 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/Contents.json b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 000000000..4414bad08 --- /dev/null +++ b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "Default-736h@3x.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "Default-Landscape@3x.png", + "minimum-system-version" : "8.0", + "orientation" : "landscape", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "667h", + "filename" : "Default-667h@2x.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default@2x.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "retina4", + "filename" : "Default-568h@2x.png", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "filename" : "Default-Portrait.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "filename" : "Default-Landscape.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "filename" : "Default-Portrait@2x.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "filename" : "Default-Landscape@2x.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default.png", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default@2x.png", + "extent" : "full-screen", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "extent" : "full-screen", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "filename" : "Default-Portrait.png", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "filename" : "Default-Landscape.png", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "filename" : "Default-Portrait@2x.png", + "extent" : "full-screen", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "filename" : "Default-Landscape@2x.png", + "extent" : "full-screen", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png new file mode 100644 index 000000000..d7f17fcd2 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png new file mode 100644 index 000000000..b88415405 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png new file mode 100644 index 000000000..faab4b631 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png new file mode 100644 index 000000000..3365ba3cd Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png new file mode 100644 index 000000000..a44945c1a Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png new file mode 100644 index 000000000..e6dca6269 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png new file mode 100644 index 000000000..1a5007962 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png new file mode 100644 index 000000000..73d8b920f Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png new file mode 100644 index 000000000..9f1f6ce3e Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png new file mode 100644 index 000000000..514fc5cde Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json new file mode 100644 index 000000000..4f4e9c506 --- /dev/null +++ b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchScreen-AspectFill.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchScreen-AspectFill@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png new file mode 100644 index 000000000..c293f9c7a Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png new file mode 100644 index 000000000..233693a6e Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json new file mode 100644 index 000000000..23c0ffd7a --- /dev/null +++ b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchScreen-Center.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchScreen-Center@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png new file mode 100644 index 000000000..a5a775a2b Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png new file mode 100644 index 000000000..154c19343 Binary files /dev/null and b/e2e/renderer/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png differ diff --git a/e2e/renderer/app/App_Resources/iOS/Info.plist b/e2e/renderer/app/App_Resources/iOS/Info.plist new file mode 100644 index 000000000..ea3e3ea23 --- /dev/null +++ b/e2e/renderer/app/App_Resources/iOS/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiresFullScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/e2e/renderer/app/App_Resources/iOS/LaunchScreen.storyboard b/e2e/renderer/app/App_Resources/iOS/LaunchScreen.storyboard new file mode 100644 index 000000000..2ad9471e1 --- /dev/null +++ b/e2e/renderer/app/App_Resources/iOS/LaunchScreen.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/e2e/renderer/app/App_Resources/iOS/build.xcconfig b/e2e/renderer/app/App_Resources/iOS/build.xcconfig new file mode 100644 index 000000000..4b0118490 --- /dev/null +++ b/e2e/renderer/app/App_Resources/iOS/build.xcconfig @@ -0,0 +1,7 @@ +// You can add custom settings here +// for example you can uncomment the following line to force distribution code signing +// CODE_SIGN_IDENTITY = iPhone Distribution +// To build for device with Xcode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html +// DEVELOPMENT_TEAM = YOUR_TEAM_ID; +ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; +ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; diff --git a/e2e/renderer/app/README.md b/e2e/renderer/app/README.md new file mode 100644 index 000000000..ebe60c416 --- /dev/null +++ b/e2e/renderer/app/README.md @@ -0,0 +1,5 @@ +# NativeScript Tutorial Angular Template + +This repo serves as the starting point for NativeScript’s [Angular Getting Started Guide](https://docs.nativescript.org/angular/tutorial/ng-chapter-0). + +Please file any issues with this template on the [NativeScript/docs repository](https://github.com/nativescript/docs), which is where the tutorial content lives. \ No newline at end of file diff --git a/e2e/renderer/app/action-bar/action-bar-dynamic-items.component.ts b/e2e/renderer/app/action-bar/action-bar-dynamic-items.component.ts new file mode 100644 index 000000000..0614e0798 --- /dev/null +++ b/e2e/renderer/app/action-bar/action-bar-dynamic-items.component.ts @@ -0,0 +1,27 @@ +import { Component } from "@angular/core"; + +@Component({ + template: ` + + + + + + + + + + + + + ` +}) +export class ActionBarDynamicItemsComponent { + public showNavigationButton = true; + public show1 = true; + public show2 = true; +} + diff --git a/e2e/renderer/app/action-bar/action-bar-extension.component.ts b/e2e/renderer/app/action-bar/action-bar-extension.component.ts new file mode 100644 index 000000000..7d3b9b8e8 --- /dev/null +++ b/e2e/renderer/app/action-bar/action-bar-extension.component.ts @@ -0,0 +1,16 @@ +import { Component } from "@angular/core"; + +@Component({ + template: ` + + + + + + + + ` +}) +export class ActionBarExtensionComponent { + public show = true; +} diff --git a/e2e/renderer/app/app-routing.module.ts b/e2e/renderer/app/app-routing.module.ts new file mode 100644 index 000000000..a975d5be8 --- /dev/null +++ b/e2e/renderer/app/app-routing.module.ts @@ -0,0 +1,89 @@ +import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; +import { NativeScriptRouterModule } from "nativescript-angular/router"; + +import { ActionBarDynamicItemsComponent } from "./action-bar/action-bar-dynamic-items.component"; +import { ActionBarExtensionComponent } from "./action-bar/action-bar-extension.component"; + +import { ListComponent } from "./list.component"; +import { NgForComponent } from "./ngfor.component"; +import { NgForOfComponent } from "./ngforof.component"; +import { NgIfNoLayoutComponent } from "./ngif-no-layout.component"; +import { NgIfInbetweenComponent } from "./ngif-inbetween.component"; +import { NgIfElseComponent } from "./ngifelse.component"; +import { NgIfThenElseComponent } from "./ngif-then-else.component"; +import { NgIfSubsequent } from "./ngif-subsequent.component"; +import { ContentViewComponent } from "./content-view.component"; + +export const routes = [ + { + path: "", + redirectTo: "/list", + pathMatch: "full" + }, + { + path: "action-bar-dynamic", + component: ActionBarDynamicItemsComponent, + }, + { + path: "action-bar-extension", + component: ActionBarExtensionComponent, + }, + { + path: "list", + component: ListComponent, + }, + { + path: "ngfor", + component: NgForComponent, + }, + { + path: "ngforof", + component: NgForOfComponent, + }, + { + path: "ngif-no-layout", + component: NgIfNoLayoutComponent, + }, + { + path: "ngif-inbetween", + component: NgIfInbetweenComponent, + }, + { + path: "ngifelse", + component: NgIfElseComponent, + }, + { + path: "ngif-then-else", + component: NgIfThenElseComponent, + }, + { + path: "ngif-subsequent", + component: NgIfSubsequent, + }, + { + path: "content-view", + component: ContentViewComponent, + }, +]; + +export const navigatableComponents = [ + ActionBarDynamicItemsComponent, + ActionBarExtensionComponent, + + ListComponent, + NgForComponent, + NgForOfComponent, + NgIfNoLayoutComponent, + NgIfInbetweenComponent, + NgIfElseComponent, + NgIfThenElseComponent, + NgIfSubsequent, + ContentViewComponent, +]; + +@NgModule({ + imports: [ NativeScriptRouterModule.forRoot(routes) ], + exports: [ NativeScriptRouterModule ], +}) +export class AppRoutingModule { } + diff --git a/e2e/renderer/app/app.component.ts b/e2e/renderer/app/app.component.ts new file mode 100644 index 000000000..2311d63e0 --- /dev/null +++ b/e2e/renderer/app/app.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + template: `` +}) +export class AppComponent { } + diff --git a/e2e/renderer/app/app.css b/e2e/renderer/app/app.css new file mode 100644 index 000000000..e69de29bb diff --git a/e2e/renderer/app/app.module.ts b/e2e/renderer/app/app.module.ts new file mode 100644 index 000000000..c5bf7e4dd --- /dev/null +++ b/e2e/renderer/app/app.module.ts @@ -0,0 +1,33 @@ +import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; +import { NativeScriptModule } from "nativescript-angular/nativescript.module"; + +import { + AppRoutingModule, + navigatableComponents, +} from "./app-routing.module"; + +import { AppComponent } from "./app.component"; +import { ItemsService } from "./items.service"; + +import { rendererTraceCategory, viewUtilCategory } from "nativescript-angular/trace"; +import { setCategories, enable } from "trace"; +setCategories(rendererTraceCategory + "," + viewUtilCategory); +enable(); + +@NgModule({ + declarations: [ + AppComponent, + ...navigatableComponents, + ], + bootstrap: [AppComponent], + providers: [ + ItemsService + ], + imports: [ + NativeScriptModule, + AppRoutingModule, + ], + schemas: [NO_ERRORS_SCHEMA], +}) +export class AppModule {} + diff --git a/e2e/renderer/app/content-view.component.ts b/e2e/renderer/app/content-view.component.ts new file mode 100644 index 000000000..a2aeb50ba --- /dev/null +++ b/e2e/renderer/app/content-view.component.ts @@ -0,0 +1,30 @@ +import { Component } from "@angular/core"; + +@Component({ + template: ` + + + + + + + + + + + + + + + + + ` +}) +export class ContentViewComponent { + public show = true; + + toggle() { + this.show = !this.show; + } +} + diff --git a/e2e/renderer/app/items-accessor.ts b/e2e/renderer/app/items-accessor.ts new file mode 100644 index 000000000..58774bd5a --- /dev/null +++ b/e2e/renderer/app/items-accessor.ts @@ -0,0 +1,18 @@ +import { ItemsService } from "./items.service"; + +export class ItemsAccessor { + public items: number[]; + + constructor(public itemsService: ItemsService) { + this.items = itemsService.getAll(); + } + + add() { + this.items = this.itemsService.add(this.items); + } + + remove(item) { + this.items = this.itemsService.remove(this.items, item); + } + +} diff --git a/e2e/renderer/app/items.service.ts b/e2e/renderer/app/items.service.ts new file mode 100644 index 000000000..94a33ef3f --- /dev/null +++ b/e2e/renderer/app/items.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class ItemsService { + private items = [ 0 ]; + + getAll() { + return [...this.items]; + } + + add(items) { + return [ + ...items, + items.length, + ]; + } + + remove(items, item?: number) { + const index = item ? items.indexOf(item) : items.length - 1; + + return this.removeAt(items, index); + } + + private removeAt(items: any[], index: number) { + items = [ + ...items.slice(0, index), + ...items.slice(index + 1), + ]; + + console.log(`Removed ${index}th element`); + + return items; + } +} diff --git a/e2e/renderer/app/list.component.ts b/e2e/renderer/app/list.component.ts new file mode 100644 index 000000000..ddbaa8b95 --- /dev/null +++ b/e2e/renderer/app/list.component.ts @@ -0,0 +1,20 @@ +import { Component } from "@angular/core"; + +@Component({ + template: ` + + + + + + + + + + + + + ` +}) +export class ListComponent { +} diff --git a/e2e/renderer/app/main.aot.ts b/e2e/renderer/app/main.aot.ts new file mode 100644 index 000000000..98bf134fc --- /dev/null +++ b/e2e/renderer/app/main.aot.ts @@ -0,0 +1,4 @@ +import { platformNativeScript } from "nativescript-angular/platform-static"; +import { AppModuleNgFactory } from "./app.module.ngfactory"; + +platformNativeScript().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/e2e/renderer/app/main.ts b/e2e/renderer/app/main.ts new file mode 100644 index 000000000..639bfd513 --- /dev/null +++ b/e2e/renderer/app/main.ts @@ -0,0 +1,4 @@ +import { platformNativeScriptDynamic } from "nativescript-angular/platform"; +import { AppModule } from "./app.module"; + +platformNativeScriptDynamic().bootstrapModule(AppModule); diff --git a/e2e/renderer/app/ngfor.component.ts b/e2e/renderer/app/ngfor.component.ts new file mode 100644 index 000000000..a3a6238fc --- /dev/null +++ b/e2e/renderer/app/ngfor.component.ts @@ -0,0 +1,21 @@ +import { Component } from "@angular/core"; + +import { ItemsService } from "./items.service"; +import { ItemsAccessor } from "./items-accessor"; + +@Component({ + template: ` + + + + + + + ` +}) +export class NgForComponent extends ItemsAccessor { + constructor(public itemsService: ItemsService) { + super(itemsService); + } +} + diff --git a/e2e/renderer/app/ngforof.component.ts b/e2e/renderer/app/ngforof.component.ts new file mode 100644 index 000000000..79ad8e3d9 --- /dev/null +++ b/e2e/renderer/app/ngforof.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; + +import { ItemsService } from "./items.service"; +import { ItemsAccessor } from "./items-accessor"; + +@Component({ + template: ` + + + + + + + + + + + ` +}) +export class NgForOfComponent extends ItemsAccessor { + constructor(public itemsService: ItemsService) { + super(itemsService); + } +} + diff --git a/e2e/renderer/app/ngif-inbetween.component.ts b/e2e/renderer/app/ngif-inbetween.component.ts new file mode 100644 index 000000000..a8a644186 --- /dev/null +++ b/e2e/renderer/app/ngif-inbetween.component.ts @@ -0,0 +1,30 @@ +import { Component } from "@angular/core"; + +@Component({ + template: ` + + + + + + + + + + + + + ` +}) +export class NgIfInbetweenComponent { + public show = true; + + toggle() { + this.show = !this.show; + } +} + diff --git a/e2e/renderer/app/ngif-no-layout.component.ts b/e2e/renderer/app/ngif-no-layout.component.ts new file mode 100644 index 000000000..087e14b55 --- /dev/null +++ b/e2e/renderer/app/ngif-no-layout.component.ts @@ -0,0 +1,24 @@ +import { Component } from "@angular/core"; + +@Component({ + template: ` + + + + + + + + + + + ` +}) +export class NgIfNoLayoutComponent { + public show = false; + + toggle() { + this.show = !this.show; + } +} + diff --git a/e2e/renderer/app/ngif-subsequent.component.ts b/e2e/renderer/app/ngif-subsequent.component.ts new file mode 100644 index 000000000..1e7125c95 --- /dev/null +++ b/e2e/renderer/app/ngif-subsequent.component.ts @@ -0,0 +1,23 @@ +import { Component} from "@angular/core"; + +@Component({ + moduleId: module.id, + selector: "renderer-test", + template: ` + + + + + + + + + + + + ` +}) +export class NgIfSubsequent { + public first: boolean = false; + public second: boolean = false; +} diff --git a/e2e/renderer/app/ngif-then-else.component.ts b/e2e/renderer/app/ngif-then-else.component.ts new file mode 100644 index 000000000..6d4113190 --- /dev/null +++ b/e2e/renderer/app/ngif-then-else.component.ts @@ -0,0 +1,33 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "ng-if-then-else", + template: ` + + + + + + + + + + + + + + + + + + ` +}) +export class NgIfThenElseComponent { + public show: boolean = true; + + toggle() { + this.show = !this.show; + } +} + + diff --git a/e2e/renderer/app/ngifelse.component.ts b/e2e/renderer/app/ngifelse.component.ts new file mode 100644 index 000000000..645c0a201 --- /dev/null +++ b/e2e/renderer/app/ngifelse.component.ts @@ -0,0 +1,26 @@ +import { Component } from "@angular/core"; + +@Component({ + template: ` + + + + + + + + + + + + + ` +}) +export class NgIfElseComponent { + public show: boolean = true; + + toggle() { + this.show = !this.show; + } +} + diff --git a/e2e/renderer/app/package.json b/e2e/renderer/app/package.json new file mode 100644 index 000000000..d5356c0cc --- /dev/null +++ b/e2e/renderer/app/package.json @@ -0,0 +1,5 @@ +{ + "main": "main.js", + "name": "nativescript-template-ng-tutorial", + "version": "3.1.0" +} \ No newline at end of file diff --git a/e2e/renderer/e2e/action-bar.e2e-spec.ts b/e2e/renderer/e2e/action-bar.e2e-spec.ts new file mode 100644 index 000000000..312df8a19 --- /dev/null +++ b/e2e/renderer/e2e/action-bar.e2e-spec.ts @@ -0,0 +1,148 @@ +import { + AppiumDriver, + createDriver, + SearchOptions, + UIElement +} from "nativescript-dev-appium"; + +import { isOnTheLeft } from "./helpers/location"; + +describe("Action Bar scenario", () => { + let driver: AppiumDriver; + + describe("dynamically add/remove ActionItems", async () => { + let firstActionItem: UIElement; + let secondActionItem: UIElement; + let toggleFirstButton: UIElement; + let toggleSecondButton: UIElement; + + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("ActionBar dynamic", SearchOptions.exact); + await navigationButton.click(); + + const actionBar = + await driver.findElementByText("Action Bar Dynamic Items", SearchOptions.exact); + }); + + it("should find elements", async () => { + firstActionItem = await driver.findElementByText("one"); + secondActionItem = await driver.findElementByText("two"); + + toggleFirstButton = await driver.findElementByText("toggle 1"); + toggleSecondButton = await driver.findElementByText("toggle 2"); + }); + + it("should initially render the action items in the correct order", async () => { + await checkOrderIsCorrect(); + }); + + it("should detach first element when its condition is false", done => { + (async () => { + await toggleFirst(); + + try { + await driver.findElementByText("one", SearchOptions.exact); + } catch (e) { + done(); + } + })(); + }); + + it("should attach first element in the correct position", async () => { + await toggleFirst(); + await checkOrderIsCorrect(); + }); + + it("should detach second element when its condition is false", done => { + (async () => { + await toggleSecond(); + + try { + await driver.findElementByText("two", SearchOptions.exact); + } catch (e) { + done(); + } + })(); + }); + + it("should attach second element in the correct position", async () => { + await toggleSecond(); + await checkOrderIsCorrect(); + }); + + it("should detach and then reattach both at correct places", async () => { + await toggleFirst(); + await toggleSecond(); + + await toggleFirst(); + await toggleSecond(); + + await checkOrderIsCorrect(); + }); + + const checkOrderIsCorrect = async () => { + await isOnTheLeft(firstActionItem, secondActionItem); + }; + + const toggleFirst = async () => { + await toggleFirstButton.click(); + }; + + const toggleSecond = async () => { + await toggleSecondButton.click(); + }; + + }); + + describe("Action Bar extension with dynamic ActionItem", async () => { + let toggleButton: UIElement; + let conditional: UIElement; + + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("ActionBarExtension", SearchOptions.exact); + await navigationButton.click(); + }); + + it("should find elements", async () => { + toggleButton = await driver.findElementByText("toggle"); + conditional = await driver.findElementByText("conditional"); + }); + + it("should detach conditional action item when its condition is false", done => { + (async () => { + await toggle(); + + try { + await driver.findElementByText("conditional", SearchOptions.exact); + } catch (e) { + done(); + } + })(); + }); + + it("should reattach conditional action item at correct place", async () => { + await toggle(); + await checkOrderIsCorrect(); + }); + + const checkOrderIsCorrect = async () => { + await isOnTheLeft(toggleButton, conditional); + }; + + const toggle = async () => { + await toggleButton.click(); + }; + }); +}); diff --git a/e2e/renderer/e2e/config/appium.capabilities.json b/e2e/renderer/e2e/config/appium.capabilities.json new file mode 100644 index 000000000..4f1b4407d --- /dev/null +++ b/e2e/renderer/e2e/config/appium.capabilities.json @@ -0,0 +1,100 @@ +{ + "nexus5": { + "browserName": "", + "platformName": "Android", + "platformVersion": "6.0", + "deviceName": "device", + "udid": "077e4a47003b7698", + "appActivity": "com.tns.NativeScriptActivity", + "app": "" + }, + "android19": { + "platformName": "Android", + "platformVersion": "4.4", + "deviceName": "Emulator-Api19-Default", + "avd": "Emulator-Api19-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android21": { + "platformName": "Android", + "platformVersion": "5.0", + "deviceName": "Emulator-Api21-Default", + "avd": "Emulator-Api21-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android23": { + "platformName": "Android", + "platformVersion": "6.0", + "deviceName": "Emulator-Api23-Default", + "avd": "Emulator-Api23-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android24": { + "platformName": "Android", + "platformVersion": "7.0", + "deviceName": "Emulator-Api24-Default", + "avd": "Emulator-Api24-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android25": { + "platformName": "Android", + "platformVersion": "7.1", + "deviceName": "Emulator-Api25-Google", + "avd": "Emulator-Api25-Google", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android26": { + "platformName": "Android", + "platformVersion": "8.0", + "deviceName": "Emulator-Api26-Google", + "avd": "Emulator-Api26-Google", + "lt": 60000, + "automationName": "UIAutomator2", + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "sim.iPhone7.iOS100": { + "platformName": "iOS", + "platformVersion": "10.0", + "deviceName": "iPhone 7 100", + "noReset": false, + "fullReset": false, + "app": "" + }, + "sim.iPhone7.iOS110": { + "platformName": "iOS", + "platformVersion": "11.0", + "deviceName": "iPhone 7 110", + "noReset": false, + "fullReset": false, + "app": "" + } +} \ No newline at end of file diff --git a/e2e/renderer/e2e/config/mocha.opts b/e2e/renderer/e2e/config/mocha.opts new file mode 100644 index 000000000..ea635b0dc --- /dev/null +++ b/e2e/renderer/e2e/config/mocha.opts @@ -0,0 +1,4 @@ +--timeout 140000 +--recursive e2e +--reporter mocha-multi +--reporter-options spec=-,mocha-junit-reporter=test-results.xml \ No newline at end of file diff --git a/e2e/renderer/e2e/helpers/location.ts b/e2e/renderer/e2e/helpers/location.ts new file mode 100644 index 000000000..9f2c07400 --- /dev/null +++ b/e2e/renderer/e2e/helpers/location.ts @@ -0,0 +1,19 @@ +import { assert } from "chai"; +import { UIElement } from "nativescript-dev-appium"; + +export const isAbove = async (first: UIElement, second: UIElement) => { + + const { y: firstY } = await first.location(); + const { y: secondY } = await second.location(); + + assert.isTrue(firstY < secondY); +} + +export const isOnTheLeft = async (first: UIElement, second: UIElement) => { + + const { x: firstX } = await first.location(); + const { x: secondX } = await second.location(); + + assert.isTrue(firstX < secondX); +} + diff --git a/e2e/renderer/e2e/ngfor.e2e-spec.ts b/e2e/renderer/e2e/ngfor.e2e-spec.ts new file mode 100644 index 000000000..ff0f0a162 --- /dev/null +++ b/e2e/renderer/e2e/ngfor.e2e-spec.ts @@ -0,0 +1,117 @@ +import { + AppiumDriver, + createDriver, + SearchOptions, + UIElement +} from "nativescript-dev-appium"; + +import { isAbove } from "./helpers/location"; + +describe("ngFor scenario", () => { + let driver: AppiumDriver; + let addButton: UIElement; + let removeButton: UIElement; + let elements: UIElement[] = []; + let lastAddedElementId = 0; + + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("NgFor", SearchOptions.exact); + await navigationButton.click(); + + const actionBar = + await driver.findElementByText("ngFor", SearchOptions.exact); + }); + + it("should find elements", async () => { + const first = await driver.findElementByText( + lastAddedElementId.toString(), SearchOptions.exact); + elements.push(first); + + addButton = await driver.findElementByText("add", SearchOptions.exact); + removeButton = await driver.findElementByText("remove", SearchOptions.exact); + + await isAbove(first, addButton); + }); + + it("should render elements in correct order", async () => { + await isAbove(elements[0], addButton); + await isAbove(addButton, removeButton); + }); + + it("should place new elements in the right places", async () => { + for (let i = 0; i < 5; i += 1) { + await addElement(); + await checkAppendedCorrectly(); + } + }); + + it("shouldn't reorder elements when last is removed", async () => { + while (elements.length) { + await removeElement(); + await checkCorrectOrderAll(); + } + }); + + it("should render new elements correctly after all old ones are removed", async () => { + for (let i = 0; i < 5; i += 1) { + await addElement(); + await checkCorrectOrderAll(); + } + }); + + it("shouldn't reorder elements when middle is removed", async () => { + const middleIndex = Math.floor(elements.length / 2); + await removeElement(middleIndex); + await checkCorrectOrderAll(); + }); + + const addElement = async () => { + await addButton.click(); + + lastAddedElementId += 1; + const newElement = await driver.findElementByText( + lastAddedElementId.toString(), SearchOptions.exact); + + elements.push(newElement); + }; + + const removeElement = async (index?: number) => { + index; + if (index) { + let element = await elements[index]; + await element.click(); + } else { + index = elements.length - 1; + await removeButton.click(); + } + + elements.splice(index, 1); + lastAddedElementId -= 1; + }; + + const checkAppendedCorrectly = async () => { + const lastAdded = await driver.findElementByText( + lastAddedElementId.toString(), SearchOptions.exact); + + await isAbove(elements.slice(-2)[0], lastAdded); + await isAbove(lastAdded, addButton); + await isAbove(addButton, removeButton); + }; + + const checkCorrectOrderAll = async () => { + for (let i = 0; i < elements.length - 1; i += 1) { + await isAbove(elements[i], elements[i + 1]); + } + + if (elements.length) { + await isAbove(elements.slice(-1)[0], addButton); + } + await isAbove(addButton, removeButton); + }; +}); \ No newline at end of file diff --git a/e2e/renderer/e2e/ngforof.e2e-spec.ts b/e2e/renderer/e2e/ngforof.e2e-spec.ts new file mode 100644 index 000000000..23f6b0eee --- /dev/null +++ b/e2e/renderer/e2e/ngforof.e2e-spec.ts @@ -0,0 +1,147 @@ +import { + AppiumDriver, + createDriver, + SearchOptions, + UIElement +} from "nativescript-dev-appium"; + +import { isAbove } from "./helpers/location"; + +interface ElementTuple { + label: UIElement, + button: UIElement, +} + +describe("ngForOf scenario", () => { + let driver: AppiumDriver; + let addButton: UIElement; + let removeButton: UIElement; + let elements: ElementTuple[] = []; + let lastAddedElementId = 0; + + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("NgForOf", SearchOptions.exact); + await navigationButton.click(); + + const actionBar = + await driver.findElementByText("ngForOf", SearchOptions.exact); + }); + + it("should find elements", async () => { + const firstElement = await getElement(lastAddedElementId); + elements.push(firstElement); + + addButton = await driver.findElementByText("add", SearchOptions.exact); + removeButton = await driver.findElementByText("remove", SearchOptions.exact); + + await elementTupleCorrectlyRendered(firstElement); + await isAbove(firstElement.button, addButton); + }); + + it("should render elements in correct order", async () => { + await elementTupleCorrectlyRendered(elements[0]); + await isAbove(elements[0].button, addButton); + await isAbove(addButton, removeButton); + }); + + + it("should place new elements in the right places", async () => { + for (let i = 0; i < 2; i += 1) { + await addElement(); + await checkAppendedCorrectly(); + } + }); + + it("shouldn't reorder elements when last is removed", async () => { + while (elements.length) { + await removeElement(); + await checkCorrectOrderAll(); + } + }); + + it("should render new elements correctly after all old ones are removed", async () => { + for (let i = 0; i < 5; i += 1) { + await addElement(); + await checkCorrectOrderAll(); + } + }); + + it("shouldn't reorder elements when middle is removed", async () => { + const middleIndex = Math.floor(elements.length / 2); + await removeElement(middleIndex); + await checkCorrectOrderAll(); + }); + + + const addElement = async () => { + await addButton.click(); + + lastAddedElementId += 1; + const newElement = await getElement(lastAddedElementId); + + elements.push(newElement); + }; + + const removeElement = async (index?: number) => { + if (index) { + let { button } = await elements[index]; + await button.click(); + } else { + index = elements.length - 1; + await removeButton.click(); + } + + elements.splice(index, 1); + lastAddedElementId -= 1; + }; + + const checkAppendedCorrectly = async () => { + const lastAdded = await getElement(lastAddedElementId); + + await elementIsAbove(elements.slice(-2)[0], lastAdded); + await isAbove(lastAdded.button, addButton); + await isAbove(addButton, removeButton); + }; + + const checkCorrectOrderAll = async () => { + for (let i = 0; i < elements.length - 1; i += 1) { + await elementIsAbove(elements[i], elements[i + 1]); + } + + if (elements.length) { + const last = elements.slice(-1)[0]; + await elementTupleCorrectlyRendered(last); + await isAbove(last.button, addButton); + } + + await isAbove(addButton, removeButton); + }; + + const elementIsAbove = async (first: ElementTuple, second: ElementTuple) => { + await elementTupleCorrectlyRendered(first); + await elementTupleCorrectlyRendered(second); + + await isAbove(first.button, second.label); + }; + + const elementTupleCorrectlyRendered = async (element: ElementTuple) => { + await isAbove(element.label, element.button); + }; + + const getElement = async (id: number) => { + const label = await driver.findElementByText( + "label: " + id.toString(), SearchOptions.exact); + + const button = await driver.findElementByText( + id.toString(), SearchOptions.exact); + + return { label, button }; + }; +}); + diff --git a/e2e/renderer/e2e/ngif.e2e-spec.ts b/e2e/renderer/e2e/ngif.e2e-spec.ts new file mode 100644 index 000000000..4684fe4df --- /dev/null +++ b/e2e/renderer/e2e/ngif.e2e-spec.ts @@ -0,0 +1,370 @@ +import { + AppiumDriver, + createDriver, + SearchOptions, + UIElement +} from "nativescript-dev-appium"; + +import { isAbove } from "./helpers/location"; + +import { assert } from "chai"; + +describe("ngIf scenario", () => { + let driver: AppiumDriver; + let toggleButton: UIElement; + + describe("without layout", async () => { + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("NgIf no layout", SearchOptions.exact); + await navigationButton.click(); + + const actionBar = + await driver.findElementByText("ngIf - no layout", SearchOptions.exact); + }); + + it("should find elements", async () => { + await driver.findElementByText("false", SearchOptions.exact); + toggleButton = await driver.findElementByText("Toggle", SearchOptions.exact); + }); + + it("show 'true' button when show is true", async () => { + await toggleButton.click(); + + await driver.findElementByText("true", SearchOptions.exact); + }); + }); + + describe("label inbetween", async () => { + let firstButton: UIElement; + let secondButton: UIElement; + let conditionalLabel: UIElement; + let toggle: UIElement; + + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("NgIf inbetween", SearchOptions.exact); + await navigationButton.click(); + + const actionBar = + await driver.findElementByText("ngIf - inbetween", SearchOptions.exact); + }); + + it("should find elements", async () => { + firstButton = await driver.findElementByText("Button 1", SearchOptions.exact); + secondButton = await driver.findElementByText("Button 2", SearchOptions.exact); + toggleButton = await driver.findElementByText("Toggle", SearchOptions.exact); + + conditionalLabel = await driver.findElementByText("Label", SearchOptions.exact); + const labelIsDisplayed = await conditionalLabel.isDisplayed(); + assert.isTrue(labelIsDisplayed); + }); + + it("detach label when condition is false", done => { + (async () => { + await toggleButton.click(); + + try { + await driver.findElementByText("Label", SearchOptions.exact); + } catch (e) { + done(); + } + })(); + }); + }); + + describe("with else template", async () => { + let ifButton: UIElement; + let elseButton: UIElement; + let toggle: UIElement; + + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("NgIfElse", SearchOptions.exact); + await navigationButton.click(); + + const actionBar = + await driver.findElementByText("ngIfElse", SearchOptions.exact); + }); + + it("should find elements", async () => { + toggleButton = await driver.findElementByText("Toggle", SearchOptions.exact); + ifButton = await driver.findElementByText("If", SearchOptions.exact); + }); + + it("shouldn't render 'else' template when condition is true", done => { + driver.findElementByText("Else", SearchOptions.exact) + .then(_ => { throw new Error("Else template found!"); }) + .catch(() => done()); + }); + + it("should attach 'else' template when condition is changed to false", async () => { + await toggleButton.click(); + + elseButton = await driver.findElementByText("Else", SearchOptions.exact); + }); + + it("should detach 'if' template when condition is changed to false", done => { + driver.findElementByText("If", SearchOptions.exact) + .then(_ => { throw new Error("If template found!"); }) + .catch(() => done()); + }); + + it("should swap the content when condition is changed", done => { + (async () => { + await toggleButton.click(); + + try { + await driver.findElementByText("Else", SearchOptions.exact); + } catch (e) { + done(); + } + })(); + }); + }); + + describe("with then-else template", async () => { + let thenButton: UIElement; + let elseButton: UIElement; + let toggle: UIElement; + + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("NgIf Then Else", SearchOptions.exact); + await navigationButton.click(); + + const actionBar = + await driver.findElementByText("ngIf Then Else", SearchOptions.exact); + }); + + it("should find elements", async () => { + toggleButton = await driver.findElementByText("Toggle", SearchOptions.exact); + thenButton = await driver.findElementByText("Then", SearchOptions.exact); + }); + + it("shouldn't render 'else' template when condition is true", done => { + driver.findElementByText("Else", SearchOptions.exact) + .then(_ => { throw new Error("Else template found!"); }) + .catch(() => done()); + }); + + it("should attach 'else' template when condition is changed to false", async () => { + await toggleButton.click(); + + elseButton = await driver.findElementByText("Else", SearchOptions.exact); + }); + + it("should detach 'then' template when condition is changed to false", done => { + driver.findElementByText("Then", SearchOptions.exact) + .then(_ => { throw new Error("Then template found!"); }) + .catch(() => done()); + }); + + it("should swap the content when condition is changed", done => { + (async () => { + await toggleButton.click(); + + try { + await driver.findElementByText("Else", SearchOptions.exact); + } catch (e) { + done(); + } + })(); + }); + }); + + describe("then-else templates inside content view", async () => { + let thenButton: UIElement; + let elseButton: UIElement; + let toggle: UIElement; + + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("Content view", SearchOptions.exact); + await navigationButton.click(); + + const actionBar = + await driver.findElementByText("Content View", SearchOptions.exact); + }); + + it("should find elements", async () => { + toggleButton = await driver.findElementByText("Toggle", SearchOptions.exact); + thenButton = await driver.findElementByText("Then", SearchOptions.exact); + }); + + it("shouldn't render 'else' template when condition is true", done => { + driver.findElementByText("Else", SearchOptions.exact) + .then(_ => { throw new Error("Else template found!"); }) + .catch(() => done()); + }); + + it("should attach 'else' template when condition is changed to false", async () => { + await toggleButton.click(); + + elseButton = await driver.findElementByText("Else", SearchOptions.exact); + }); + + it("should detach 'then' template when condition is changed to false", done => { + driver.findElementByText("Then", SearchOptions.exact) + .then(_ => { throw new Error("Then template found!"); }) + .catch(() => done()); + }); + + it("should swap the content when condition is changed", done => { + (async () => { + await toggleButton.click(); + + try { + await driver.findElementByText("Else", SearchOptions.exact); + } catch (e) { + done(); + } + })(); + }); + }); + + describe("subsequent ifs", async () => { + let firstButton: UIElement; + let secondButton: UIElement; + let firstLabel: UIElement; + let secondLabel: UIElement; + + before(async () => { + driver = await createDriver(); + await driver.driver.resetApp(); + }); + + it("should navigate to page", async () => { + const navigationButton = + await driver.findElementByText("NgIf Subsequent Ifs", SearchOptions.exact); + await navigationButton.click(); + }); + + it("should find elements", async () => { + firstButton = await driver.findElementByText("Toggle first", SearchOptions.exact); + secondButton = await driver.findElementByText("Toggle second", SearchOptions.exact); + + firstLabel = await driver.findElementByText("== 1 ==", SearchOptions.exact); + secondLabel = await driver.findElementByText("== 2 ==", SearchOptions.exact); + + assert.isDefined(firstButton); + assert.isDefined(secondButton); + assert.isDefined(firstLabel); + assert.isDefined(secondLabel); + }); + + it("should toggle on first view", async () => { + await firstButton.click(); + + let conditional = await driver.findElementByText("first", SearchOptions.exact); + + await isAbove(firstLabel, conditional); + await isAbove(conditional, secondLabel); + }); + + it("should toggle off first view", done => { + (async () => { + await firstButton.click(); + + driver.findElementByText("first", SearchOptions.exact, 500) + .then(_ => { throw new Error("first label found!"); }) + .catch(() => done()); + })(); + }); + + it("should toggle on second view", async () => { + await secondButton.click(); + + let conditional = await driver.findElementByText("second", SearchOptions.exact); + await isAbove(firstLabel, conditional); + await isAbove(conditional, secondLabel); + }); + + it("should toggle off second view", done => { + (async () => { + await secondButton.click(); + + driver.findElementByText("first", SearchOptions.exact, 500) + .then(_ => { throw new Error("first label found!"); }) + .catch(() => done()); + })(); + }); + + it("should toggle on both views", async () => { + await firstButton.click(); + await secondButton.click(); + + let conditional1 = await driver.findElementByText("first", SearchOptions.exact); + let conditional2 = await driver.findElementByText("second", SearchOptions.exact); + await isAbove(firstLabel, conditional1); + await isAbove(conditional1, conditional2); + await isAbove(conditional2, secondLabel); + }); + + it("should toggle off both views", done => { + (async () => { + await firstButton.click(); + await secondButton.click(); + + driver.findElementByText("first", SearchOptions.exact, 500) + .then(_ => { throw new Error("first label found!"); }) + .catch(() => { + driver.findElementByText("second", SearchOptions.exact, 500) + .then(_ => { throw new Error("second label found!"); }) + .catch(() => done()); + }); + })(); + }); + + it("should toggle on both views in reverse", async () => { + await secondButton.click(); + await firstButton.click(); + + let conditional1 = await driver.findElementByText("first", SearchOptions.exact); + let conditional2 = await driver.findElementByText("second", SearchOptions.exact); + await isAbove(firstLabel, conditional1); + await isAbove(conditional1, conditional2); + await isAbove(conditional2, secondLabel); + }); + + it("should toggle off both views in reverse", done => { + (async () => { + await secondButton.click(); + await firstButton.click(); + + driver.findElementByText("first", SearchOptions.exact, 500) + .then(_ => { throw new Error("first label found!"); }) + .catch(() => { + driver.findElementByText("second", SearchOptions.exact, 500) + .then(_ => { throw new Error("second label found!"); }) + .catch(() => done()); + }); + })(); + }); + }); +}); diff --git a/e2e/renderer/e2e/setup.ts b/e2e/renderer/e2e/setup.ts new file mode 100644 index 000000000..549020395 --- /dev/null +++ b/e2e/renderer/e2e/setup.ts @@ -0,0 +1,19 @@ +import { startServer, stopServer, createDriver, AppiumDriver } from "nativescript-dev-appium"; + +let driver: AppiumDriver; + +before("start server", async () => { + await startServer(); + driver = await createDriver(); +}); + +after("stop server", async () => { + await driver.quit(); + await stopServer(); +}); + +afterEach(async function () { + if (this.currentTest.state === "failed") { + await driver.logScreenshot(this.currentTest.title); + } +}); \ No newline at end of file diff --git a/e2e/renderer/e2e/tsconfig.json b/e2e/renderer/e2e/tsconfig.json new file mode 100644 index 000000000..18b6c4302 --- /dev/null +++ b/e2e/renderer/e2e/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "importHelpers": false, + "types": [ + "node", + "mocha", + "chai" + ], + "lib": [ + "es6", + "dom" + ] + } +} \ No newline at end of file diff --git a/e2e/renderer/package.json b/e2e/renderer/package.json new file mode 100644 index 000000000..94403d63c --- /dev/null +++ b/e2e/renderer/package.json @@ -0,0 +1,51 @@ +{ + "description": "NativeScript Application", + "license": "SEE LICENSE IN ", + "readme": "NativeScript Application", + "repository": "", + "nativescript": { + "id": "org.nativescript.renderer", + "tns-android": { + "version": "next" + } + }, + "dependencies": { + "@angular/animations": "~4.4.1", + "@angular/common": "~4.4.1", + "@angular/compiler": "~4.4.1", + "@angular/core": "~4.4.1", + "@angular/forms": "~4.4.1", + "@angular/http": "~4.4.1", + "@angular/platform-browser": "~4.4.1", + "@angular/router": "~4.4.1", + "nativescript-angular": "file:../../nativescript-angular", + "nativescript-intl": "^3.0.0", + "reflect-metadata": "~0.1.8", + "rxjs": "~5.3.0", + "tns-core-modules": "next", + "zone.js": "~0.8.2" + }, + "devDependencies": { + "@types/chai": "^4.0.2", + "@types/mocha": "^2.2.41", + "@types/node": "^7.0.5", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "chai": "~4.1.1", + "chai-as-promised": "~7.1.1", + "colors": "^1.1.2", + "lazy": "1.0.11", + "mocha": "~3.5.0", + "mocha-junit-reporter": "^1.13.0", + "mocha-multi": "^0.11.0", + "nativescript-dev-appium": "next", + "nativescript-dev-typescript": "~0.4.0", + "tslib": "^1.7.1", + "typescript": "~2.2.1" + }, + "scripts": { + "e2e": "tsc -p e2e && mocha --opts ./e2e/config/mocha.opts", + "compile-tests-w": "tsc -p e2e --watch" + } +} diff --git a/e2e/renderer/tsconfig.json b/e2e/renderer/tsconfig.json new file mode 100644 index 000000000..14205dd1b --- /dev/null +++ b/e2e/renderer/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noEmitHelpers": true, + "noEmitOnError": true, + "lib": [ + "es6", + "dom", + "es2015.iterable" + ], + "baseUrl": ".", + "paths": { + "*": [ + "./node_modules/tns-core-modules/*", + "./node_modules/*" + ] + } + }, + "exclude": [ + "node_modules", + "platforms", + "**/*.aot.ts", + "e2e" + ] +} \ No newline at end of file diff --git a/e2e/router/.gitignore b/e2e/router/.gitignore new file mode 100644 index 000000000..8e824499e --- /dev/null +++ b/e2e/router/.gitignore @@ -0,0 +1,10 @@ +.vscode + +platforms +node_modules +hooks + +/**/*.js +/**/*.map +e2e/reports +test-results.xml \ No newline at end of file diff --git a/e2e/router/app/App_Resources/Android/AndroidManifest.xml b/e2e/router/app/App_Resources/Android/AndroidManifest.xml new file mode 100644 index 000000000..9db832151 --- /dev/null +++ b/e2e/router/app/App_Resources/Android/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/e2e/router/app/App_Resources/Android/app.gradle b/e2e/router/app/App_Resources/Android/app.gradle new file mode 100644 index 000000000..8df47fb37 --- /dev/null +++ b/e2e/router/app/App_Resources/Android/app.gradle @@ -0,0 +1,23 @@ +// Add your native dependencies here: + +// Uncomment to add recyclerview-v7 dependency +//dependencies { +// compile 'com.android.support:recyclerview-v7:+' +//} + +android { + defaultConfig { + generatedDensities = [] + applicationId = "org.nativescript.router" + + //override supported platforms + // ndk { + // abiFilters.clear() + // abiFilters "armeabi-v7a" + // } + + } + aaptOptions { + additionalParameters "--no-version-vectors" + } +} diff --git a/e2e/router/app/App_Resources/Android/drawable-nodpi/background.png b/e2e/router/app/App_Resources/Android/drawable-nodpi/background.png new file mode 100644 index 000000000..748b2adf5 Binary files /dev/null and b/e2e/router/app/App_Resources/Android/drawable-nodpi/background.png differ diff --git a/e2e/router/app/App_Resources/Android/drawable-nodpi/icon.png b/e2e/router/app/App_Resources/Android/drawable-nodpi/icon.png new file mode 100755 index 000000000..ddfc17a71 Binary files /dev/null and b/e2e/router/app/App_Resources/Android/drawable-nodpi/icon.png differ diff --git a/e2e/router/app/App_Resources/Android/drawable-nodpi/logo.png b/e2e/router/app/App_Resources/Android/drawable-nodpi/logo.png new file mode 100644 index 000000000..b9e102a76 Binary files /dev/null and b/e2e/router/app/App_Resources/Android/drawable-nodpi/logo.png differ diff --git a/e2e/router/app/App_Resources/Android/drawable-nodpi/splash_screen.xml b/e2e/router/app/App_Resources/Android/drawable-nodpi/splash_screen.xml new file mode 100644 index 000000000..ada77f92c --- /dev/null +++ b/e2e/router/app/App_Resources/Android/drawable-nodpi/splash_screen.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/e2e/router/app/App_Resources/Android/values-v21/colors.xml b/e2e/router/app/App_Resources/Android/values-v21/colors.xml new file mode 100644 index 000000000..a64641a9d --- /dev/null +++ b/e2e/router/app/App_Resources/Android/values-v21/colors.xml @@ -0,0 +1,4 @@ + + + #3d5afe + \ No newline at end of file diff --git a/e2e/router/app/App_Resources/Android/values-v21/styles.xml b/e2e/router/app/App_Resources/Android/values-v21/styles.xml new file mode 100644 index 000000000..dac8727c8 --- /dev/null +++ b/e2e/router/app/App_Resources/Android/values-v21/styles.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/e2e/router/app/App_Resources/Android/values/colors.xml b/e2e/router/app/App_Resources/Android/values/colors.xml new file mode 100644 index 000000000..74ad8829c --- /dev/null +++ b/e2e/router/app/App_Resources/Android/values/colors.xml @@ -0,0 +1,7 @@ + + + #F5F5F5 + #757575 + #33B5E5 + #272734 + \ No newline at end of file diff --git a/e2e/router/app/App_Resources/Android/values/styles.xml b/e2e/router/app/App_Resources/Android/values/styles.xml new file mode 100644 index 000000000..1e8c7f29b --- /dev/null +++ b/e2e/router/app/App_Resources/Android/values/styles.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..1953734f4 --- /dev/null +++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,92 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "icon-83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png new file mode 100644 index 000000000..9e15af09d Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png new file mode 100644 index 000000000..7b9e55537 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png new file mode 100644 index 000000000..76f61ec1f Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png new file mode 100644 index 000000000..15b06db11 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 000000000..585065f94 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 000000000..a450c421d Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 000000000..457b6d94c Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 000000000..fa5a6ac86 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png new file mode 100644 index 000000000..94abcf70d Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 000000000..2e71dd3a0 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 000000000..4abc9ec50 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 000000000..4414bad08 --- /dev/null +++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "Default-736h@3x.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "736h", + "filename" : "Default-Landscape@3x.png", + "minimum-system-version" : "8.0", + "orientation" : "landscape", + "scale" : "3x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "667h", + "filename" : "Default-667h@2x.png", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default@2x.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "retina4", + "filename" : "Default-568h@2x.png", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "filename" : "Default-Portrait.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "filename" : "Default-Landscape.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "filename" : "Default-Portrait@2x.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "filename" : "Default-Landscape@2x.png", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default.png", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default@2x.png", + "extent" : "full-screen", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "filename" : "Default-568h@2x.png", + "extent" : "full-screen", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "filename" : "Default-Portrait.png", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "filename" : "Default-Landscape.png", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "filename" : "Default-Portrait@2x.png", + "extent" : "full-screen", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "to-status-bar", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "filename" : "Default-Landscape@2x.png", + "extent" : "full-screen", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png new file mode 100644 index 000000000..d7f17fcd2 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png new file mode 100644 index 000000000..b88415405 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png new file mode 100644 index 000000000..faab4b631 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png new file mode 100644 index 000000000..3365ba3cd Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png new file mode 100644 index 000000000..a44945c1a Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png new file mode 100644 index 000000000..e6dca6269 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png new file mode 100644 index 000000000..1a5007962 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png new file mode 100644 index 000000000..73d8b920f Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png new file mode 100644 index 000000000..9f1f6ce3e Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png new file mode 100644 index 000000000..514fc5cde Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json new file mode 100644 index 000000000..4f4e9c506 --- /dev/null +++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchScreen-AspectFill.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchScreen-AspectFill@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png new file mode 100644 index 000000000..c293f9c7a Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png new file mode 100644 index 000000000..233693a6e Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json new file mode 100644 index 000000000..23c0ffd7a --- /dev/null +++ b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchScreen-Center.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchScreen-Center@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png new file mode 100644 index 000000000..a5a775a2b Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png differ diff --git a/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png new file mode 100644 index 000000000..154c19343 Binary files /dev/null and b/e2e/router/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png differ diff --git a/e2e/router/app/App_Resources/iOS/Info.plist b/e2e/router/app/App_Resources/iOS/Info.plist new file mode 100644 index 000000000..ea3e3ea23 --- /dev/null +++ b/e2e/router/app/App_Resources/iOS/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiresFullScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/e2e/router/app/App_Resources/iOS/LaunchScreen.storyboard b/e2e/router/app/App_Resources/iOS/LaunchScreen.storyboard new file mode 100644 index 000000000..2ad9471e1 --- /dev/null +++ b/e2e/router/app/App_Resources/iOS/LaunchScreen.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/e2e/router/app/App_Resources/iOS/build.xcconfig b/e2e/router/app/App_Resources/iOS/build.xcconfig new file mode 100644 index 000000000..4b0118490 --- /dev/null +++ b/e2e/router/app/App_Resources/iOS/build.xcconfig @@ -0,0 +1,7 @@ +// You can add custom settings here +// for example you can uncomment the following line to force distribution code signing +// CODE_SIGN_IDENTITY = iPhone Distribution +// To build for device with Xcode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html +// DEVELOPMENT_TEAM = YOUR_TEAM_ID; +ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; +ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; diff --git a/e2e/router/app/README.md b/e2e/router/app/README.md new file mode 100644 index 000000000..ebe60c416 --- /dev/null +++ b/e2e/router/app/README.md @@ -0,0 +1,5 @@ +# NativeScript Tutorial Angular Template + +This repo serves as the starting point for NativeScript’s [Angular Getting Started Guide](https://docs.nativescript.org/angular/tutorial/ng-chapter-0). + +Please file any issues with this template on the [NativeScript/docs repository](https://github.com/nativescript/docs), which is where the tutorial content lives. \ No newline at end of file diff --git a/e2e/router/app/app-routing.module.ts b/e2e/router/app/app-routing.module.ts new file mode 100644 index 000000000..dcc881a54 --- /dev/null +++ b/e2e/router/app/app-routing.module.ts @@ -0,0 +1,57 @@ +import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; +import { NativeScriptRouterModule } from "nativescript-angular/router"; + +import { FirstComponent } from "./first/first.component" +import { SecondComponent } from "./second/second.component" +import { MasterComponent } from "./second/master.component" +import { DetailComponent } from "./second/detail.component" + +export const routes = [ + { + path: "", + redirectTo: "/first", + pathMatch: "full" + }, + { + path: "first", + component: FirstComponent, + }, + { + path: "second/:depth", component: SecondComponent, + children: [ + { path: "", component: MasterComponent }, + { path: "detail/:id", component: DetailComponent } + ] + }, + { + path: "c-less", + children: [ + { + path: "deep/:depth", + component: SecondComponent, + children: [ + { path: "", component: MasterComponent }, + { path: "detail/:id", component: DetailComponent } + ] + } + ] + }, + { + path: "lazy", + loadChildren: "./lazy/lazy.module#LazyModule", + } +]; + +export const navigatableComponents = [ + FirstComponent, + SecondComponent, + MasterComponent, + DetailComponent +]; + +@NgModule({ + imports: [NativeScriptRouterModule.forRoot(routes)], + exports: [NativeScriptRouterModule], +}) +export class AppRoutingModule { } + diff --git a/e2e/router/app/app.component.ts b/e2e/router/app/app.component.ts new file mode 100644 index 000000000..2311d63e0 --- /dev/null +++ b/e2e/router/app/app.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + template: `` +}) +export class AppComponent { } + diff --git a/e2e/router/app/app.css b/e2e/router/app/app.css new file mode 100644 index 000000000..3890d9f3a --- /dev/null +++ b/e2e/router/app/app.css @@ -0,0 +1,17 @@ +.header { + font-size: 32; + white-space: normal; +} + +.nested-header { + font-size: 26; +} + +.nested-outlet { + margin: 20; + background-color: lightgreen; +} + +Label { + text-align: center; +} \ No newline at end of file diff --git a/e2e/router/app/app.module.ts b/e2e/router/app/app.module.ts new file mode 100644 index 000000000..5d7dcbdbc --- /dev/null +++ b/e2e/router/app/app.module.ts @@ -0,0 +1,35 @@ +import { NgModule, NgModuleFactoryLoader, NO_ERRORS_SCHEMA } from "@angular/core"; +import { NativeScriptModule } from "nativescript-angular/nativescript.module"; +import { NSModuleFactoryLoader } from "nativescript-angular/router"; + +import "./rxjs-operators"; + +import { + AppRoutingModule, + navigatableComponents, +} from "./app-routing.module"; + +import { AppComponent } from "./app.component"; + +import { rendererTraceCategory, viewUtilCategory, routeReuseStrategyTraceCategory, routerTraceCategory } from "nativescript-angular/trace"; +import { setCategories, enable } from "trace"; +setCategories(routerTraceCategory + "," + routeReuseStrategyTraceCategory); +enable(); + +@NgModule({ + declarations: [ + AppComponent, + ...navigatableComponents, + ], + bootstrap: [AppComponent], + providers: [ + { provide: NgModuleFactoryLoader, useClass: NSModuleFactoryLoader } + ], + imports: [ + NativeScriptModule, + AppRoutingModule, + ], + schemas: [NO_ERRORS_SCHEMA], +}) +export class AppModule { } + diff --git a/e2e/router/app/first/first.component.ts b/e2e/router/app/first/first.component.ts new file mode 100644 index 000000000..5748e7b2b --- /dev/null +++ b/e2e/router/app/first/first.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit, OnDestroy, OnChanges } from "@angular/core"; +import { ActivatedRoute, Router, Route } from "@angular/router"; +import { Location } from "@angular/common"; +import { RouterExtensions } from "nativescript-angular/router"; + +import { Page } from "ui/page"; +import { Observable } from "rxjs/Observable"; + +@Component({ + selector: "first", + template: ` + + + + + + + + + + + + + ` +}) +export class FirstComponent implements OnInit, OnDestroy { + public message: string = ""; + constructor(private routerExt: RouterExtensions, page: Page) { + console.log("FirstComponent - constructor() page: " + page); + } + + ngOnInit() { + console.log("FirstComponent - ngOnInit()"); + } + + ngOnDestroy() { + console.log("FirstComponent - ngOnDestroy()"); + } + + goBack() { + this.message = ""; + if (this.routerExt.canGoBack()) { + this.routerExt.back(); + } else { + this.message = "canGoBack() - false" + } + } +} diff --git a/e2e/router/app/lazy/lazy-componentless-route.component.ts b/e2e/router/app/lazy/lazy-componentless-route.component.ts new file mode 100644 index 000000000..a3df10fd5 --- /dev/null +++ b/e2e/router/app/lazy/lazy-componentless-route.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit, OnDestroy, OnChanges } from "@angular/core"; +import { ActivatedRoute, Router, Route } from "@angular/router"; +import { Location } from "@angular/common"; +import { RouterExtensions } from "nativescript-angular/router"; + +import { Page } from "ui/page"; +import { Observable } from "rxjs/Observable"; + +@Component({ + selector: "lazy", + template: ` + + + + + + + + + ` +}) +export class LazyComponentlessRouteComponent implements OnInit, OnDestroy { + public message: string = ""; + constructor(private routerExt: RouterExtensions, page: Page) { + console.log("LazyNestedRouteComponent - constructor() page: " + page); + } + + ngOnInit() { + console.log("LazyNestedRouteComponent - ngOnInit()"); + } + + ngOnDestroy() { + console.log("LazyNestedRouteComponent - ngOnDestroy()"); + } + + goBack() { + this.message = ""; + if (this.routerExt.canGoBack()) { + this.routerExt.back(); + } else { + this.message = "canGoBack() - false" + } + } +} diff --git a/e2e/router/app/lazy/lazy.component.ts b/e2e/router/app/lazy/lazy.component.ts new file mode 100644 index 000000000..94e3f72b6 --- /dev/null +++ b/e2e/router/app/lazy/lazy.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit, OnDestroy, OnChanges } from "@angular/core"; +import { ActivatedRoute, Router, Route } from "@angular/router"; +import { Location } from "@angular/common"; +import { RouterExtensions } from "nativescript-angular/router"; + +import { Page } from "ui/page"; +import { Observable } from "rxjs/Observable"; + +@Component({ + selector: "lazy", + template: ` + + + + + + + + + ` +}) +export class LazyComponent implements OnInit, OnDestroy { + public message: string = ""; + constructor(private routerExt: RouterExtensions, page: Page) { + console.log("LazyComponent - constructor() page: " + page); + } + + ngOnInit() { + console.log("LazyComponent - ngOnInit()"); + } + + ngOnDestroy() { + console.log("LazyComponent - ngOnDestroy()"); + } + + goBack() { + this.message = ""; + if (this.routerExt.canGoBack()) { + this.routerExt.back(); + } else { + this.message = "canGoBack() - false" + } + } +} diff --git a/e2e/router/app/lazy/lazy.module.ts b/e2e/router/app/lazy/lazy.module.ts new file mode 100644 index 000000000..3f622e33a --- /dev/null +++ b/e2e/router/app/lazy/lazy.module.ts @@ -0,0 +1,38 @@ +import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; +import { Route } from "@angular/router"; + +import { NativeScriptModule } from "nativescript-angular/nativescript.module"; +import { NativeScriptRouterModule } from "nativescript-angular/router"; + +import { LazyComponent } from "./lazy.component"; +import { LazyComponentlessRouteComponent } from "./lazy-componentless-route.component"; + +const routes: Route[] = [ + { + path: "home", + component: LazyComponent + }, + { + path: "nest", + children: [ + { + path: "more", + component: LazyComponentlessRouteComponent + } + ] + } +]; + +@NgModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + NativeScriptModule, + NativeScriptRouterModule, + NativeScriptRouterModule.forChild(routes) + ], + declarations: [ + LazyComponent, + LazyComponentlessRouteComponent + ] +}) +export class LazyModule { } \ No newline at end of file diff --git a/e2e/router/app/main.aot.ts b/e2e/router/app/main.aot.ts new file mode 100644 index 000000000..98bf134fc --- /dev/null +++ b/e2e/router/app/main.aot.ts @@ -0,0 +1,4 @@ +import { platformNativeScript } from "nativescript-angular/platform-static"; +import { AppModuleNgFactory } from "./app.module.ngfactory"; + +platformNativeScript().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/e2e/router/app/main.ts b/e2e/router/app/main.ts new file mode 100644 index 000000000..639bfd513 --- /dev/null +++ b/e2e/router/app/main.ts @@ -0,0 +1,4 @@ +import { platformNativeScriptDynamic } from "nativescript-angular/platform"; +import { AppModule } from "./app.module"; + +platformNativeScriptDynamic().bootstrapModule(AppModule); diff --git a/e2e/router/app/package.json b/e2e/router/app/package.json new file mode 100644 index 000000000..d5356c0cc --- /dev/null +++ b/e2e/router/app/package.json @@ -0,0 +1,5 @@ +{ + "main": "main.js", + "name": "nativescript-template-ng-tutorial", + "version": "3.1.0" +} \ No newline at end of file diff --git a/e2e/router/app/rxjs-operators.ts b/e2e/router/app/rxjs-operators.ts new file mode 100644 index 000000000..480bbe40f --- /dev/null +++ b/e2e/router/app/rxjs-operators.ts @@ -0,0 +1 @@ +import "rxjs/add/operator/map"; \ No newline at end of file diff --git a/e2e/router/app/second/detail.component.ts b/e2e/router/app/second/detail.component.ts new file mode 100644 index 000000000..e005f6dee --- /dev/null +++ b/e2e/router/app/second/detail.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit, OnDestroy } from "@angular/core"; +import { ActivatedRoute, Router, Route } from "@angular/router"; +import { Location } from "@angular/common"; +import { Page } from "ui/page"; +import { Observable } from "rxjs/Observable"; + +@Component({ + selector: "detail", + template: ` + + + + + + ` +}) +export class DetailComponent { + public id$: Observable; + + constructor(private router: Router, private route: ActivatedRoute) { + console.log("DetailComponent - constructor()"); + this.id$ = route.params.map(r => r["id"]); + } + + ngOnInit() { + console.log("DetailComponent - ngOnInit()"); + } + + ngOnDestroy() { + console.log("DetailComponent - ngOnDestroy()"); + } +} \ No newline at end of file diff --git a/e2e/router/app/second/master.component.ts b/e2e/router/app/second/master.component.ts new file mode 100644 index 000000000..08605d5e5 --- /dev/null +++ b/e2e/router/app/second/master.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit, OnDestroy } from "@angular/core"; + +@Component({ + selector: "master", + template: ` + + + + + + ` +}) +export class MasterComponent implements OnInit, OnDestroy { + public details: Array = [1, 2, 3]; + + constructor() { + console.log("MasterComponent - constructor()"); + } + + ngOnInit() { + console.log("MasterComponent - ngOnInit()"); + } + + ngOnDestroy() { + console.log("MasterComponent - ngOnDestroy()"); + } +} \ No newline at end of file diff --git a/e2e/router/app/second/second.component.ts b/e2e/router/app/second/second.component.ts new file mode 100644 index 000000000..75acb847b --- /dev/null +++ b/e2e/router/app/second/second.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit, OnDestroy } from "@angular/core"; +import { ActivatedRoute, Router, Route } from "@angular/router"; + +import { RouterExtensions } from "nativescript-angular/router"; +import { Page } from "ui/page"; +import { Observable } from "rxjs/Observable"; + +@Component({ + selector: "second", + template: ` + + + + + + + + + + + + + ` +}) +export class SecondComponent implements OnInit, OnDestroy { + public depth$: Observable; + public nextDepth$: Observable; + + constructor(private routerExt: RouterExtensions, route: ActivatedRoute, page: Page) { + console.log("SecondComponent - constructor() page: " + page); + this.depth$ = route.params.map(r => r["depth"]); + this.nextDepth$ = route.params.map(r => +r["depth"] + 1); + } + + ngOnInit() { + console.log("SecondComponent - ngOnInit()"); + } + + ngOnDestroy() { + console.log("SecondComponent - ngOnDestroy()"); + } + + goBack() { + this.routerExt.back(); + } +} \ No newline at end of file diff --git a/e2e/router/e2e/config/appium.capabilities.json b/e2e/router/e2e/config/appium.capabilities.json new file mode 100644 index 000000000..4f1b4407d --- /dev/null +++ b/e2e/router/e2e/config/appium.capabilities.json @@ -0,0 +1,100 @@ +{ + "nexus5": { + "browserName": "", + "platformName": "Android", + "platformVersion": "6.0", + "deviceName": "device", + "udid": "077e4a47003b7698", + "appActivity": "com.tns.NativeScriptActivity", + "app": "" + }, + "android19": { + "platformName": "Android", + "platformVersion": "4.4", + "deviceName": "Emulator-Api19-Default", + "avd": "Emulator-Api19-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android21": { + "platformName": "Android", + "platformVersion": "5.0", + "deviceName": "Emulator-Api21-Default", + "avd": "Emulator-Api21-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android23": { + "platformName": "Android", + "platformVersion": "6.0", + "deviceName": "Emulator-Api23-Default", + "avd": "Emulator-Api23-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android24": { + "platformName": "Android", + "platformVersion": "7.0", + "deviceName": "Emulator-Api24-Default", + "avd": "Emulator-Api24-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android25": { + "platformName": "Android", + "platformVersion": "7.1", + "deviceName": "Emulator-Api25-Google", + "avd": "Emulator-Api25-Google", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "android26": { + "platformName": "Android", + "platformVersion": "8.0", + "deviceName": "Emulator-Api26-Google", + "avd": "Emulator-Api26-Google", + "lt": 60000, + "automationName": "UIAutomator2", + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": false, + "fullReset": false, + "app": "" + }, + "sim.iPhone7.iOS100": { + "platformName": "iOS", + "platformVersion": "10.0", + "deviceName": "iPhone 7 100", + "noReset": false, + "fullReset": false, + "app": "" + }, + "sim.iPhone7.iOS110": { + "platformName": "iOS", + "platformVersion": "11.0", + "deviceName": "iPhone 7 110", + "noReset": false, + "fullReset": false, + "app": "" + } +} \ No newline at end of file diff --git a/e2e/router/e2e/config/mocha.opts b/e2e/router/e2e/config/mocha.opts new file mode 100644 index 000000000..796ec4724 --- /dev/null +++ b/e2e/router/e2e/config/mocha.opts @@ -0,0 +1,4 @@ +--timeout 80000 +--recursive e2e +--reporter mocha-multi +--reporter-options spec=-,mocha-junit-reporter=test-results.xml \ No newline at end of file diff --git a/e2e/router/e2e/router.e2e-spec.ts b/e2e/router/e2e/router.e2e-spec.ts new file mode 100644 index 000000000..82dcda30c --- /dev/null +++ b/e2e/router/e2e/router.e2e-spec.ts @@ -0,0 +1,398 @@ +import { + AppiumDriver, + UIElement, + createDriver, + SearchOptions, +} from "nativescript-dev-appium"; + +describe("Simple navigate and back", () => { + let driver: AppiumDriver; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + }); + + it("should find First", async () => { + await assureFirstComponent(driver); + }); + + it("should navigate to Second(1)/master", async () => { + await findAndClick(driver, "GO TO SECOND"); + + await assureSecondComponent(driver, 1); + await assureNestedMasterComponent(driver); + }); + + it("should navigate back to First", async () => { + await goBack(driver); + await assureFirstComponent(driver); + }); +}); + +describe("Navigate inside nested outlet", () => { + let driver: AppiumDriver; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + }); + + it("should find First", async () => { + await assureFirstComponent(driver); + }); + + it("should navigate to Second(1)/master", async () => { + await findAndClick(driver, "GO TO SECOND"); + + await assureSecondComponent(driver, 1) + await assureNestedMasterComponent(driver); + }); + + it("should navigate to Second(1)/detail(1) and back", async () => { + const detailBtn = await driver.findElementByText("DETAIL 1", SearchOptions.exact); + detailBtn.click(); + await assureSecondComponent(driver, 1) + await assureNestedDetailComponent(driver, 1); + + await goBack(driver); + await assureSecondComponent(driver, 1) + await assureNestedMasterComponent(driver); + }); + + it("should navigate to Second(1)/detail(2) and back", async () => { + const detailBtn = await driver.findElementByText("DETAIL 2", SearchOptions.exact); + detailBtn.click(); + await assureSecondComponent(driver, 1) + await assureNestedDetailComponent(driver, 2); + + await goBack(driver); + await assureSecondComponent(driver, 1) + await assureNestedMasterComponent(driver); + }); + + it("should navigate back to First", async () => { + await goBack(driver); + await assureFirstComponent(driver); + }); +}); + +describe("Navigate to same component with different param", () => { + let driver: AppiumDriver; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + }); + + it("should find First", async () => { + await assureFirstComponent(driver); + }); + + it("should navigate to Second(1)/master", async () => { + await findAndClick(driver, "GO TO SECOND"); + + await assureSecondComponent(driver, 1) + await assureNestedMasterComponent(driver); + }); + + it("should navigate to Second(2)/master", async () => { + const navigationButton = + await driver.findElementByText("GO TO NEXT SECOND", SearchOptions.exact); + navigationButton.click(); + + await assureSecondComponent(driver, 2) + await assureNestedMasterComponent(driver); + }); + + it("should navigate back to Second(1)/master", async () => { + await goBack(driver); + + await assureSecondComponent(driver, 1) + await assureNestedMasterComponent(driver); + }); + + it("should navigate back to First", async () => { + await goBack(driver); + await assureFirstComponent(driver); + }); +}); + +describe("Nested navigation + page navigation", () => { + let driver: AppiumDriver; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + }); + + it("should find First", async () => { + await assureFirstComponent(driver); + }); + + it("should navigate to Second(1)/master", async () => { + await findAndClick(driver, "GO TO SECOND"); + + await assureSecondComponent(driver, 1) + await assureNestedMasterComponent(driver); + }); + + it("should navigate to Second(1)/detail(1)", async () => { + const detailBtn = await driver.findElementByText("DETAIL 1", SearchOptions.exact); + detailBtn.click(); + + await assureSecondComponent(driver, 1) + await assureNestedDetailComponent(driver, 1); + }); + + it("should navigate to Second(2)/master", async () => { + const navigationButton = + await driver.findElementByText("GO TO NEXT SECOND", SearchOptions.exact); + navigationButton.click(); + + await assureSecondComponent(driver, 2) + await assureNestedMasterComponent(driver); + }); + + it("should navigate to Second(2)/detail(2)", async () => { + const detailBtn = await driver.findElementByText("DETAIL 2", SearchOptions.exact); + detailBtn.click(); + + await assureSecondComponent(driver, 2) + await assureNestedDetailComponent(driver, 2); + }); + + it("should navigate to First", async () => { + await findAndClick(driver, "GO TO FIRST"); + + await assureFirstComponent(driver); + }); + + it("should navigate the whole stack", async () => { + await goBack(driver); + await assureSecondComponent(driver, 2) + await assureNestedDetailComponent(driver, 2); + + await goBack(driver); + await assureSecondComponent(driver, 2) + await assureNestedMasterComponent(driver); + + await goBack(driver); + await assureSecondComponent(driver, 1) + await assureNestedDetailComponent(driver, 1); + + await goBack(driver); + await assureSecondComponent(driver, 1) + await assureNestedMasterComponent(driver); + + await goBack(driver); + await assureFirstComponent(driver); + }); +}); + +describe("Shouldn't be able to navigate back on startup", () => { + let driver: AppiumDriver; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + }); + + it("should find First", async () => { + await assureFirstComponent(driver); + }); + + it("shouldn't be able to go back", async () => { + await goBack(driver); + await driver.findElementByText("canGoBack() - false", SearchOptions.exact); + }); +}); + +describe("Shouldn't be able to navigate back after cleared history", () => { + let driver: AppiumDriver; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + }); + + it("should find First", async () => { + await assureFirstComponent(driver); + }); + + it("should navigate to Second(1)/master", async () => { + await findAndClick(driver, "GO TO SECOND"); + + await assureSecondComponent(driver, 1) + await assureNestedMasterComponent(driver); + }); + + it("should navigate to Second(1)/master", async () => { + await findAndClick(driver, "GO TO FIRST(CLEAR)"); + + await assureFirstComponent(driver); + }); + + it("shouldn't be able to go back", async () => { + await goBack(driver); + await driver.findElementByText("canGoBack() - false", SearchOptions.exact); + }); +}); + +describe("Navigate to componentless route", () => { + let driver: AppiumDriver; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + }); + + it("should find First", async () => { + await assureFirstComponent(driver); + }); + + it("should navigate to ComponentlessSecond(100)/detail(200)", async () => { + const navigationButton = + await driver.findElementByText("GO TO C-LESS SECOND", SearchOptions.exact); + navigationButton.click(); + + await assureSecondComponent(driver, 100) + await assureNestedDetailComponent(driver, 200); + }); + + it("should navigate to First", async () => { + await findAndClick(driver, "GO TO FIRST"); + + await assureFirstComponent(driver); + }); + + it("should navigate the whole stack", async () => { + await goBack(driver); + await assureSecondComponent(driver, 100) + await assureNestedDetailComponent(driver, 200); + + await goBack(driver); + await assureFirstComponent(driver); + }); +}); + +describe("Navigate to lazy module", () => { + let driver: AppiumDriver; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + }); + + it("should find First", async () => { + await assureFirstComponent(driver); + }); + + it("should navigate to lazy/home", async () => { + await findAndClick(driver, "GO TO LAZY HOME"); + await assureLazyComponent(driver); + }); + + it("should navigate to First", async () => { + await findAndClick(driver, "GO TO FIRST"); + await assureFirstComponent(driver); + }); + + it("should navigate back to lazy/home", async () => { + await goBack(driver); + await assureLazyComponent(driver); + }); + + it("should navigate to First again", async () => { + await findAndClick(driver, "GO TO FIRST"); + await assureFirstComponent(driver); + }); + + it("should navigate the whole stack", async () => { + await goBack(driver); + await assureLazyComponent(driver); + + await goBack(driver); + await assureFirstComponent(driver); + }); +}); + +describe("Navigate to componentless lazy module route", () => { + let driver: AppiumDriver; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + }); + + it("should find First", async () => { + await assureFirstComponent(driver); + }); + + it("should navigate to nest/more (componentless lazy route)", async () => { + await findAndClick(driver, "GO TO C-LESS LAZY"); + + await assureComponentlessLazyComponent(driver); + }); + + it("should navigate to lazy/home", async () => { + await findAndClick(driver, "GO TO LAZY HOME"); + + await assureLazyComponent(driver); + }); + + it("should navigate to First", async () => { + await findAndClick(driver, "GO TO FIRST"); + + await assureFirstComponent(driver); + }); + + it("should navigate the whole stack", async () => { + await goBack(driver); + await assureLazyComponent(driver); + + await goBack(driver); + await assureComponentlessLazyComponent(driver); + + await goBack(driver); + await assureFirstComponent(driver); + }); +}); + +async function assureFirstComponent(driver: AppiumDriver) { + await driver.findElementByText("FirstComponent", SearchOptions.exact); +} + +async function assureLazyComponent(driver: AppiumDriver) { + await driver.findElementByText("LazyComponent", SearchOptions.exact); +} + +async function assureComponentlessLazyComponent(driver: AppiumDriver) { + await driver.findElementByText("Lazy Componentless Route", SearchOptions.exact); +} + +async function assureSecondComponent(driver: AppiumDriver, param: number) { + await driver.findElementByText("SecondComponent", SearchOptions.exact); + await driver.findElementByText(`param: ${param}`, SearchOptions.exact); +} + +async function assureNestedMasterComponent(driver: AppiumDriver) { + await driver.findElementByText("NestedMaster", SearchOptions.exact); +} + +async function assureNestedDetailComponent(driver: AppiumDriver, param: number) { + await driver.findElementByText("NestedDetail", SearchOptions.exact); + await driver.findElementByText(`nested-param: ${param}`, SearchOptions.exact); +} + +async function goBack(driver: AppiumDriver) { + const backButton = await driver.findElementByText("BACK", SearchOptions.exact); + await backButton.click(); + //await driver.navBack(); +} + +async function findAndClick(driver: AppiumDriver, text: string) { + const navigationButton = + await driver.findElementByText(text, SearchOptions.exact); + navigationButton.click(); +} \ No newline at end of file diff --git a/e2e/router/e2e/setup.ts b/e2e/router/e2e/setup.ts new file mode 100644 index 000000000..0c0add14a --- /dev/null +++ b/e2e/router/e2e/setup.ts @@ -0,0 +1,19 @@ +import { startServer, stopServer , createDriver , AppiumDriver } from "nativescript-dev-appium"; + +let driver: AppiumDriver; + +before("start server", async () => { + await startServer(); + driver = await createDriver(); +}); + +after("stop server", async () => { + await driver.quit(); + await stopServer(); +}); + +afterEach(async function () { + if (this.currentTest.state === "failed") { + await driver.logScreenshot(this.currentTest.title); + } +}); \ No newline at end of file diff --git a/e2e/router/e2e/tsconfig.json b/e2e/router/e2e/tsconfig.json new file mode 100644 index 000000000..18b6c4302 --- /dev/null +++ b/e2e/router/e2e/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "importHelpers": false, + "types": [ + "node", + "mocha", + "chai" + ], + "lib": [ + "es6", + "dom" + ] + } +} \ No newline at end of file diff --git a/e2e/router/package.json b/e2e/router/package.json new file mode 100644 index 000000000..e99f48117 --- /dev/null +++ b/e2e/router/package.json @@ -0,0 +1,54 @@ +{ + "description": "NativeScript Application", + "license": "SEE LICENSE IN ", + "readme": "NativeScript Application", + "repository": "", + "nativescript": { + "id": "org.nativescript.router", + "tns-android": { + "version": "3.2.0-2017-9-4-1" + }, + "tns-ios": { + "version": "3.2.0" + } + }, + "dependencies": { + "@angular/animations": "~4.4.1", + "@angular/common": "~4.4.1", + "@angular/compiler": "~4.4.1", + "@angular/core": "~4.4.1", + "@angular/forms": "~4.4.1", + "@angular/http": "~4.4.1", + "@angular/platform-browser": "~4.4.1", + "@angular/router": "~4.4.1", + "nativescript-angular": "file:../../nativescript-angular", + "nativescript-intl": "^3.0.0", + "reflect-metadata": "~0.1.8", + "rxjs": "~5.3.0", + "tns-core-modules": "next", + "zone.js": "~0.8.2" + }, + "devDependencies": { + "@types/chai": "^4.0.2", + "@types/mocha": "^2.2.41", + "@types/node": "^7.0.5", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "chai": "~4.1.1", + "chai-as-promised": "~7.1.1", + "colors": "^1.1.2", + "lazy": "1.0.11", + "mocha": "~3.5.0", + "mocha-junit-reporter": "^1.13.0", + "mocha-multi": "^0.11.0", + "nativescript-dev-appium": "next", + "nativescript-dev-typescript": "~0.4.0", + "tslib": "^1.7.1", + "typescript": "~2.2.1" + }, + "scripts": { + "e2e": "tsc -p e2e && mocha --opts ./e2e/config/mocha.opts", + "compile-tests-w": "tsc -p e2e --watch" + } +} diff --git a/e2e/router/tsconfig.json b/e2e/router/tsconfig.json new file mode 100644 index 000000000..14205dd1b --- /dev/null +++ b/e2e/router/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noEmitHelpers": true, + "noEmitOnError": true, + "lib": [ + "es6", + "dom", + "es2015.iterable" + ], + "baseUrl": ".", + "paths": { + "*": [ + "./node_modules/tns-core-modules/*", + "./node_modules/*" + ] + } + }, + "exclude": [ + "node_modules", + "platforms", + "**/*.aot.ts", + "e2e" + ] +} \ No newline at end of file diff --git a/nativescript-angular/animations.ts b/nativescript-angular/animations/animations.module.ts similarity index 87% rename from nativescript-angular/animations.ts rename to nativescript-angular/animations/animations.module.ts index 7b5f2d2f3..7f2b3adea 100644 --- a/nativescript-angular/animations.ts +++ b/nativescript-angular/animations/animations.module.ts @@ -13,10 +13,10 @@ import { ɵBrowserAnimationBuilder as BrowserAnimationBuilder, } from "@angular/platform-browser/animations"; -import { NativeScriptAnimationEngine } from "./animations/animation-engine"; -import { NativeScriptAnimationDriver } from "./animations/animation-driver"; -import { NativeScriptModule } from "./nativescript.module"; -import { NativeScriptRendererFactory } from "./renderer"; +import { NativeScriptAnimationEngine } from "./animation-engine"; +import { NativeScriptAnimationDriver } from "./animation-driver"; +import { NativeScriptModule } from "../nativescript.module"; +import { NativeScriptRendererFactory } from "../renderer"; @Injectable() export class InjectableAnimationEngine extends NativeScriptAnimationEngine { diff --git a/nativescript-angular/animations/index.ts b/nativescript-angular/animations/index.ts new file mode 100644 index 000000000..d5754740b --- /dev/null +++ b/nativescript-angular/animations/index.ts @@ -0,0 +1 @@ +export * from "./animations.module"; diff --git a/nativescript-angular/collection-facade.ts b/nativescript-angular/collection-facade.ts deleted file mode 100644 index 08c2f6742..000000000 --- a/nativescript-angular/collection-facade.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* tslint:disable */ -//Copied unexported functions from @angular/core/src/facade/collection -import { - isJsObject, isArray, getSymbolIterator, - isPresent, isBlank -} from "./lang-facade"; - -export function isListLikeIterable(obj: any): boolean { - if (!isJsObject(obj)) return false; - return isArray(obj) || - (!(obj instanceof Map) && // JS Map are iterables but return entries as [k, v] - getSymbolIterator() in obj); // JS Iterable have a Symbol.iterator prop -} - -export class ListWrapper { - // JS has no way to express a statically fixed size list, but dart does so we - // keep both methods. - static createFixedSize(size: number): any[] { return new Array(size); } - static createGrowableSize(size: number): any[] { return new Array(size); } - static clone(array: T[]): T[] { return array.slice(0); } - static forEachWithIndex(array: T[], fn: (t: T, n: number) => void) { - for (let i = 0; i < array.length; i++) { - fn(array[i], i); - } - } - static first(array: T[]): T { - if (!array) return null; - return array[0]; - } - static last(array: T[]): T { - if (!array || array.length == 0) return null; - return array[array.length - 1]; - } - static indexOf(array: T[], value: T, startIndex: number = 0): number { - return array.indexOf(value, startIndex); - } - static contains(list: T[], el: T): boolean { return list.indexOf(el) !== -1; } - static reversed(array: T[]): T[] { - let a = ListWrapper.clone(array); - return a.reverse(); - } - static concat(a: any[], b: any[]): any[] { return a.concat(b); } - static insert(list: T[], index: number, value: T) { list.splice(index, 0, value); } - static removeAt(list: T[], index: number): T { - let res = list[index]; - list.splice(index, 1); - return res; - } - static removeAll(list: T[], items: T[]) { - for (let i = 0; i < items.length; ++i) { - let index = list.indexOf(items[i]); - list.splice(index, 1); - } - } - static remove(list: T[], el: T): boolean { - let index = list.indexOf(el); - if (index > -1) { - list.splice(index, 1); - return true; - } - return false; - } - static clear(list: any[]) { list.length = 0; } - static isEmpty(list: any[]): boolean { return list.length == 0; } - static fill(list: any[], value: any, start: number = 0, end: number = null) { - (list).fill(value, start, end === null ? list.length : end); - } - static equals(a: any[], b: any[]): boolean { - if (a.length != b.length) return false; - for (let i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) return false; - } - return true; - } - static slice(l: T[], from: number = 0, to: number = null): T[] { - return l.slice(from, to === null ? undefined : to); - } - static splice(l: T[], from: number, length: number): T[] { return l.splice(from, length); } - static sort(l: T[], compareFn?: (a: T, b: T) => number) { - if (isPresent(compareFn)) { - l.sort(compareFn); - } else { - l.sort(); - } - } - static toString(l: T[]): string { return l.toString(); } - static toJSON(l: T[]): string { return JSON.stringify(l); } - - static maximum(list: T[], predicate: (t: T) => number): T { - if (list.length == 0) { - return null; - } - let solution: any /** TODO #???? */ = null; - let maxValue = -Infinity; - for (let index = 0; index < list.length; index++) { - let candidate = list[index]; - if (isBlank(candidate)) { - continue; - } - let candidateValue = predicate(candidate); - if (candidateValue > maxValue) { - solution = candidate; - maxValue = candidateValue; - } - } - return solution; - } - - static flatten(list: Array): T[] { - let target: any[] = []; - _flattenArray(list, target); - return target; - } - - static addAll(list: Array, source: Array): void { - for (let i = 0; i < source.length; i++) { - list.push(source[i]); - } - } -} - -function _flattenArray(source: any[], target: any[]): any[] { - if (isPresent(source)) { - for (let i = 0; i < source.length; i++) { - let item = source[i]; - if (isArray(item)) { - _flattenArray(item, target); - } else { - target.push(item); - } - } - } - return target; -} diff --git a/nativescript-angular/directives.ts b/nativescript-angular/directives.ts deleted file mode 100644 index 2e07c59d5..000000000 --- a/nativescript-angular/directives.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ListViewComponent, TemplateKeyDirective } from "./directives/list-view-comp"; -import { TabViewDirective, TabViewItemDirective } from "./directives/tab-view"; -import { - ActionBarComponent, - ActionBarScope, - ActionItemDirective, - NavigationButtonDirective -} from "./directives/action-bar"; -import { AndroidFilterComponent, IosFilterComponent } from "./directives/platform-filters"; - -export const NS_DIRECTIVES = [ - ListViewComponent, - TemplateKeyDirective, - TabViewDirective, - TabViewItemDirective, - ActionBarComponent, - ActionBarScope, - ActionItemDirective, - NavigationButtonDirective, - AndroidFilterComponent, - IosFilterComponent, -]; - -export { - ListViewComponent, - SetupItemViewArgs, - TemplateKeyDirective -} from "./directives/list-view-comp"; -export { TextValueAccessor } from "./value-accessors/text-value-accessor"; -export { CheckedValueAccessor } from "./value-accessors/checked-value-accessor"; -export { DateValueAccessor } from "./value-accessors/date-value-accessor"; -export { TimeValueAccessor } from "./value-accessors/time-value-accessor"; -export { NumberValueAccessor } from "./value-accessors/number-value-accessor"; -export { SelectedIndexValueAccessor } from "./value-accessors/selectedIndex-value-accessor"; -export { TabViewDirective, TabViewItemDirective } from "./directives/tab-view"; -export { - ActionBarComponent, - ActionBarScope, - ActionItemDirective, - NavigationButtonDirective -} from "./directives/action-bar"; -export { AndroidFilterComponent, IosFilterComponent } from "./directives/platform-filters"; diff --git a/nativescript-angular/directives/action-bar.ts b/nativescript-angular/directives/action-bar.ts index 5158a1532..1b9053dd2 100644 --- a/nativescript-angular/directives/action-bar.ts +++ b/nativescript-angular/directives/action-bar.ts @@ -1,54 +1,87 @@ import { Directive, Component, ElementRef, Optional, OnDestroy } from "@angular/core"; -import { ActionItem, ActionBar, NavigationButton } from "tns-core-modules/ui/action-bar"; +import { + ActionBar, + ActionItem, + ActionItems, + NavigationButton, +} from "tns-core-modules/ui/action-bar"; import { Page } from "tns-core-modules/ui/page"; -import { View } from "tns-core-modules/ui/core/view"; import { isBlank } from "../lang-facade"; import { - InvisibleNode, NgView, ViewClassMeta, + ViewExtensions, + isInvisibleNode, + isView, registerElement, } from "../element-registry"; +export function isActionItem(view: any): view is ActionItem { + return view instanceof ActionItem; +} + +export function isNavigationButton(view: any): view is NavigationButton { + return view instanceof NavigationButton; +} + +type NgActionBar = (ActionBar & ViewExtensions); + const actionBarMeta: ViewClassMeta = { skipAddToDom: true, - insertChild: (parent: NgView, child: NgView) => { - const bar = (parent); - const childView = child; - - if (child instanceof InvisibleNode) { + insertChild: (parent: NgActionBar, child: NgView, next: any) => { + if (isInvisibleNode(child)) { return; - } else if (child instanceof NavigationButton) { - bar.navigationButton = childView; - childView.parent = bar; - } else if (child instanceof ActionItem) { - bar.actionItems.addItem(childView); - childView.parent = bar; - } else if (child instanceof View) { - bar.titleView = childView; + } else if (isNavigationButton(child)) { + parent.navigationButton = child; + child.templateParent = parent; + } else if (isActionItem(child)) { + addActionItem(parent, child, next); + child.templateParent = parent; + } else if (isView(child)) { + parent.titleView = child; } }, - removeChild: (parent: NgView, child: NgView) => { - const bar = (parent); - const childView = child; - - if (child instanceof InvisibleNode) { + removeChild: (parent: NgActionBar, child: NgView) => { + if (isInvisibleNode(child)) { return; - } else if (child instanceof NavigationButton) { - if (bar.navigationButton === childView) { - bar.navigationButton = null; + } else if (isNavigationButton(child)) { + if (parent.navigationButton === child) { + parent.navigationButton = null; } - childView.parent = null; - } else if (child instanceof ActionItem) { - bar.actionItems.removeItem(childView); - childView.parent = null; - } else if (child instanceof View && bar.titleView && bar.titleView === childView) { - bar.titleView = null; + + child.templateParent = null; + } else if (isActionItem(child)) { + parent.actionItems.removeItem(child); + child.templateParent = null; + } else if (isView(child) && parent.titleView && parent.titleView === child) { + parent.titleView = null; } }, }; +const addActionItem = (bar: NgActionBar, item: ActionItem, next: ActionItem) => { + if (next) { + insertActionItemBefore(bar, item, next); + } else { + appendActionItem(bar, item); + } +}; + +const insertActionItemBefore = (bar: NgActionBar, item: ActionItem, next: ActionItem) => { + const actionItems: ActionItems = bar.actionItems; + const actionItemsCollection: ActionItem[] = actionItems.getItems(); + + const indexToInsert = actionItemsCollection.indexOf(next); + actionItemsCollection.splice(indexToInsert, 0, item); + + (actionItems).setItems(actionItemsCollection); +}; + +const appendActionItem = (bar: NgActionBar, item: ActionItem) => { + bar.actionItems.addItem(item); +}; + registerElement("ActionBar", () => require("ui/action-bar").ActionBar, actionBarMeta); registerElement("ActionItem", () => require("ui/action-bar").ActionItem); registerElement("NavigationButton", () => require("ui/action-bar").NavigationButton); diff --git a/nativescript-angular/directives/index.ts b/nativescript-angular/directives/index.ts new file mode 100644 index 000000000..9a8a6fd69 --- /dev/null +++ b/nativescript-angular/directives/index.ts @@ -0,0 +1,37 @@ +import { ListViewComponent, TemplateKeyDirective } from "./list-view-comp"; +import { TabViewDirective, TabViewItemDirective } from "./tab-view"; +import { + ActionBarComponent, + ActionBarScope, + ActionItemDirective, + NavigationButtonDirective +} from "./action-bar"; +import { AndroidFilterComponent, IosFilterComponent } from "./platform-filters"; + +export { + ListViewComponent, + SetupItemViewArgs, + TemplateKeyDirective +} from "./list-view-comp"; + +export { TabViewDirective, TabViewItemDirective } from "./tab-view"; +export { + ActionBarComponent, + ActionBarScope, + ActionItemDirective, + NavigationButtonDirective +} from "./action-bar"; +export { AndroidFilterComponent, IosFilterComponent } from "./platform-filters"; + +export const NS_DIRECTIVES = [ + ListViewComponent, + TemplateKeyDirective, + TabViewDirective, + TabViewItemDirective, + ActionBarComponent, + ActionBarScope, + ActionItemDirective, + NavigationButtonDirective, + AndroidFilterComponent, + IosFilterComponent, +]; diff --git a/nativescript-angular/directives/list-view-comp.ts b/nativescript-angular/directives/list-view-comp.ts index f2237df90..351441cce 100644 --- a/nativescript-angular/directives/list-view-comp.ts +++ b/nativescript-angular/directives/list-view-comp.ts @@ -18,6 +18,7 @@ import { TemplateRef, ViewChild, ViewContainerRef, + ɵisListLikeIterable as isListLikeIterable } from "@angular/core"; import { ListView, ItemEventData } from "tns-core-modules/ui/list-view"; import { View, KeyedTemplate } from "tns-core-modules/ui/core/view"; @@ -26,7 +27,6 @@ import { ObservableArray } from "tns-core-modules/data/observable-array"; import { profile } from "tns-core-modules/profiling"; import { getSingleViewRecursive } from "../element-registry"; -import { isListLikeIterable } from "../collection-facade"; import { listViewLog, listViewError } from "../trace"; const NG_VIEW = "_ngViewRef"; diff --git a/nativescript-angular/dom-adapter.ts b/nativescript-angular/dom-adapter.ts index f577c5ae0..e228f92a5 100644 --- a/nativescript-angular/dom-adapter.ts +++ b/nativescript-angular/dom-adapter.ts @@ -2,7 +2,6 @@ import { Type } from "@angular/core"; import { ɵDomAdapter } from "@angular/platform-browser"; import { rendererLog } from "./trace"; -import { print } from "./lang-facade"; export class NativeScriptDomAdapter implements ɵDomAdapter { static makeCurrent() { @@ -27,15 +26,15 @@ export class NativeScriptDomAdapter implements ɵDomAdapter { } log(arg: any): void { - print(arg); + console.log(arg); } logError(arg: any): void { - print(arg); + console.log(arg); } logGroup(arg: any): void { - print(arg); + console.log(arg); } logGroupEnd(): void { diff --git a/nativescript-angular/element-registry.ts b/nativescript-angular/element-registry.ts index 9a0f044aa..35f111cf8 100644 --- a/nativescript-angular/element-registry.ts +++ b/nativescript-angular/element-registry.ts @@ -2,25 +2,30 @@ import { View } from "tns-core-modules/ui/core/view"; import { LayoutBase } from "tns-core-modules/ui/layouts/layout-base"; export type NgView = (View & ViewExtensions); -export type NgElement = NgView | InvisibleNode; export interface ViewExtensions { + meta: ViewClassMeta; nodeType: number; nodeName: string; templateParent: NgView; + nextSibling: NgView; + firstChild: NgView; + lastChild: NgView; ngCssClasses: Map; - meta: ViewClassMeta; } export interface ViewClass { new (): View; } -export abstract class InvisibleNode extends View implements ViewExtensions { +export abstract class InvisibleNode extends View implements NgView { meta: { skipAddToDom: boolean }; - templateParent: NgView; nodeType: number; nodeName: string; + templateParent: NgView; + nextSibling: NgView; + firstChild: NgView; + lastChild: NgView; ngCssClasses: Map; constructor() { @@ -42,7 +47,7 @@ export class CommentNode extends InvisibleNode { super(); this.meta = { - skipAddToDom: false, + skipAddToDom: true, }; this.id = CommentNode.id.toString(); CommentNode.id += 1; @@ -67,14 +72,22 @@ const getClassName = instance => instance.constructor.name; export interface ViewClassMeta { skipAddToDom?: boolean; - insertChild?: (parent: NgView, child: NgView, atIndex: number) => void; - removeChild?: (parent: NgView, child: NgView) => void; + insertChild?: (parent: any, child: any, next?: any) => void; + removeChild?: (parent: any, child: any) => void; } export function isDetachedElement(element): boolean { return (element && element.meta && element.meta.skipAddToDom); } +export function isView(view: any): view is NgView { + return view instanceof View; +} + +export function isInvisibleNode(view: any): view is InvisibleNode { + return view instanceof InvisibleNode; +} + export type ViewResolver = () => ViewClass; const elementMap = new Map(); diff --git a/nativescript-angular/file-system/ns-file-system.ts b/nativescript-angular/file-system/ns-file-system.ts index f8cf590d5..222a54c12 100644 --- a/nativescript-angular/file-system/ns-file-system.ts +++ b/nativescript-angular/file-system/ns-file-system.ts @@ -1,12 +1,20 @@ import { Injectable } from "@angular/core"; -import { knownFolders, Folder } from "tns-core-modules/file-system"; +import { knownFolders, Folder, File } from "tns-core-modules/file-system"; // Allows greater flexibility with `file-system` and Angular // Also provides a way for `file-system` to be mocked for testing @Injectable() export class NSFileSystem { - public currentApp(): Folder { - return knownFolders.currentApp(); - } + public currentApp(): Folder { + return knownFolders.currentApp(); + } + + public fileFromPath(path: string): File { + return File.fromPath(path); + } + + public fileExists(path: string): boolean { + return File.exists(path); + } } diff --git a/nativescript-angular/forms.ts b/nativescript-angular/forms/forms.module.ts similarity index 51% rename from nativescript-angular/forms.ts rename to nativescript-angular/forms/forms.module.ts index e449fc79f..814b08727 100644 --- a/nativescript-angular/forms.ts +++ b/nativescript-angular/forms/forms.module.ts @@ -1,11 +1,13 @@ import { NgModule } from "@angular/core"; import { FormsModule } from "@angular/forms"; -import { TextValueAccessor } from "./value-accessors/text-value-accessor"; -import { CheckedValueAccessor } from "./value-accessors/checked-value-accessor"; -import { DateValueAccessor } from "./value-accessors/date-value-accessor"; -import { TimeValueAccessor } from "./value-accessors/time-value-accessor"; -import { NumberValueAccessor } from "./value-accessors/number-value-accessor"; -import { SelectedIndexValueAccessor } from "./value-accessors/selectedIndex-value-accessor"; +import { + TextValueAccessor, + CheckedValueAccessor, + DateValueAccessor, + TimeValueAccessor, + NumberValueAccessor, + SelectedIndexValueAccessor +} from "./value-accessors"; export const FORMS_DIRECTIVES = [ TextValueAccessor, diff --git a/nativescript-angular/forms/index.ts b/nativescript-angular/forms/index.ts new file mode 100644 index 000000000..a95e3e129 --- /dev/null +++ b/nativescript-angular/forms/index.ts @@ -0,0 +1 @@ +export * from "./forms.module"; diff --git a/nativescript-angular/value-accessors/base-value-accessor.ts b/nativescript-angular/forms/value-accessors/base-value-accessor.ts similarity index 77% rename from nativescript-angular/value-accessors/base-value-accessor.ts rename to nativescript-angular/forms/value-accessors/base-value-accessor.ts index 25ac8aa7a..5f46ca98a 100644 --- a/nativescript-angular/value-accessors/base-value-accessor.ts +++ b/nativescript-angular/forms/value-accessors/base-value-accessor.ts @@ -1,5 +1,7 @@ import { ControlValueAccessor } from "@angular/forms"; -import { View } from "tns-core-modules/ui/core/view"; +import { View, unsetValue } from "tns-core-modules/ui/core/view"; + +import { isBlank } from "../../lang-facade"; export class BaseValueAccessor implements ControlValueAccessor { private pendingChangeNotification: any = 0; @@ -28,5 +30,9 @@ export class BaseValueAccessor implements ControlValueAccess this.view.isEnabled = !isDisabled; } - writeValue(_: any) { } + writeValue(_: any) {} + + protected normalizeValue(value: any): any { + return isBlank(value) ? unsetValue : value; + } } diff --git a/nativescript-angular/value-accessors/checked-value-accessor.ts b/nativescript-angular/forms/value-accessors/checked-value-accessor.ts similarity index 92% rename from nativescript-angular/value-accessors/checked-value-accessor.ts rename to nativescript-angular/forms/value-accessors/checked-value-accessor.ts index af1a22289..6053ee700 100644 --- a/nativescript-angular/value-accessors/checked-value-accessor.ts +++ b/nativescript-angular/forms/value-accessors/checked-value-accessor.ts @@ -34,6 +34,7 @@ export class CheckedValueAccessor extends BaseValueAccessor { // tslint: } writeValue(value: any): void { - this.view.checked = value; + const normalized = super.normalizeValue(value); + this.view.checked = normalized; } } diff --git a/nativescript-angular/value-accessors/date-value-accessor.ts b/nativescript-angular/forms/value-accessors/date-value-accessor.ts similarity index 93% rename from nativescript-angular/value-accessors/date-value-accessor.ts rename to nativescript-angular/forms/value-accessors/date-value-accessor.ts index 9e66bbc92..c7f8419a9 100644 --- a/nativescript-angular/value-accessors/date-value-accessor.ts +++ b/nativescript-angular/forms/value-accessors/date-value-accessor.ts @@ -25,7 +25,6 @@ const DATE_VALUE_ACCESSOR = { "date-picker[ngModel],date-picker[formControlName],date-picker[formControl]", providers: [DATE_VALUE_ACCESSOR], host: { - "(touch)": "onTouched()", "(dateChange)": "onChange($event.value)", }, }) @@ -35,6 +34,7 @@ export class DateValueAccessor extends BaseValueAccessor { // tslint } writeValue(value: any): void { - this.view.date = value; + const normalized = super.normalizeValue(value); + this.view.date = normalized; } } diff --git a/nativescript-angular/forms/value-accessors/index.ts b/nativescript-angular/forms/value-accessors/index.ts new file mode 100644 index 000000000..351151606 --- /dev/null +++ b/nativescript-angular/forms/value-accessors/index.ts @@ -0,0 +1,7 @@ +export * from "./base-value-accessor"; +export * from "./text-value-accessor"; +export * from "./checked-value-accessor"; +export * from "./date-value-accessor"; +export * from "./time-value-accessor"; +export * from "./number-value-accessor"; +export * from "./selectedIndex-value-accessor"; diff --git a/nativescript-angular/value-accessors/number-value-accessor.ts b/nativescript-angular/forms/value-accessors/number-value-accessor.ts similarity index 92% rename from nativescript-angular/value-accessors/number-value-accessor.ts rename to nativescript-angular/forms/value-accessors/number-value-accessor.ts index 44451f96e..32275d4ea 100644 --- a/nativescript-angular/value-accessors/number-value-accessor.ts +++ b/nativescript-angular/forms/value-accessors/number-value-accessor.ts @@ -24,7 +24,6 @@ const NUMBER_VALUE_ACCESSOR = { "slider[ngModel],slider[formControlName],slider[formControl]", providers: [NUMBER_VALUE_ACCESSOR], host: { - "(touch)": "onTouched()", "(valueChange)": "onChange($event.value)", }, }) @@ -34,6 +33,7 @@ export class NumberValueAccessor extends BaseValueAccessor { // tslint:d } writeValue(value: any): void { - this.view.value = value; + const normalized = super.normalizeValue(value); + this.view.value = normalized; } } diff --git a/nativescript-angular/value-accessors/selectedIndex-value-accessor.ts b/nativescript-angular/forms/value-accessors/selectedIndex-value-accessor.ts similarity index 96% rename from nativescript-angular/value-accessors/selectedIndex-value-accessor.ts rename to nativescript-angular/forms/value-accessors/selectedIndex-value-accessor.ts index 005a40f98..2c9a7ddf1 100644 --- a/nativescript-angular/value-accessors/selectedIndex-value-accessor.ts +++ b/nativescript-angular/forms/value-accessors/selectedIndex-value-accessor.ts @@ -38,7 +38,6 @@ export type SelectableView = {selectedIndex: number} & View; "tab-view[ngModel],tab-view[formControlName],tab-view[formControl]", providers: [SELECTED_INDEX_VALUE_ACCESSOR], host: { - "(touch)": "onTouched()", "(selectedIndexChange)": "onChange($event.value)", }, }) @@ -51,7 +50,8 @@ export class SelectedIndexValueAccessor extends BaseValueAccessor { // tslint:d } writeValue(value: any): void { - this.view.text = value; + const normalized = super.normalizeValue(value); + this.view.text = normalized; } } diff --git a/nativescript-angular/value-accessors/time-value-accessor.ts b/nativescript-angular/forms/value-accessors/time-value-accessor.ts similarity index 93% rename from nativescript-angular/value-accessors/time-value-accessor.ts rename to nativescript-angular/forms/value-accessors/time-value-accessor.ts index d62b170e7..6be6851ee 100644 --- a/nativescript-angular/value-accessors/time-value-accessor.ts +++ b/nativescript-angular/forms/value-accessors/time-value-accessor.ts @@ -36,6 +36,7 @@ export class TimeValueAccessor extends BaseValueAccessor { // tslint } writeValue(value: any): void { - this.view.time = value; + const normalized = super.normalizeValue(value); + this.view.time = normalized; } } diff --git a/nativescript-angular/http-client/http-client.module.ts b/nativescript-angular/http-client/http-client.module.ts new file mode 100644 index 000000000..ea6623d5b --- /dev/null +++ b/nativescript-angular/http-client/http-client.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from "@angular/core"; + +// IMPORTant: Importing "@angular/common/http" for the first time overwrites the +// global.__extends function. +const cachedExtends = global.__extends; +import { HttpClientModule, HttpBackend } from "@angular/common/http"; +global.__extends = cachedExtends; + +import { NSFileSystem } from "../file-system/ns-file-system"; +import { NsHttpBackEnd } from "./ns-http-backend"; + +@NgModule({ + providers: [ + NSFileSystem, + NsHttpBackEnd, + { provide: HttpBackend, useExisting: NsHttpBackEnd }, + ], + imports: [ + HttpClientModule, + ], + exports: [ + HttpClientModule, + ] +}) +export class NativeScriptHttpClientModule { +} diff --git a/nativescript-angular/http-client/http-utils.ts b/nativescript-angular/http-client/http-utils.ts new file mode 100644 index 000000000..ae0ecc6f8 --- /dev/null +++ b/nativescript-angular/http-client/http-utils.ts @@ -0,0 +1,52 @@ +import { NSFileSystem } from "../file-system/ns-file-system"; + +import { Observable } from "rxjs/Observable"; +import { Observer } from "rxjs/Observer"; +import { path } from "tns-core-modules/file-system/file-system"; + +export type httpResponseFactory = (url: string, body: any, status: number) => T; +export type httpErrorFactory = (url: string, body: any, status: number) => any; + +export function isLocalRequest(url: string): boolean { + return url.indexOf("~") === 0 || url.indexOf("/") === 0; +} + +export function getAbsolutePath(url: string, nsFileSystem: NSFileSystem): string { + url = url.replace("~", "").replace("/", ""); + url = path.join(nsFileSystem.currentApp().path, url); + return url; +} + +export function processLocalFileRequest( + url: string, + nsFileSystem: NSFileSystem, + successResponse: httpResponseFactory, + errorResponse: httpErrorFactory): Observable { + + url = getAbsolutePath(url, nsFileSystem); + + // request from local app resources + return new Observable((observer: Observer) => { + if (nsFileSystem.fileExists(url)) { + const localFile = nsFileSystem.fileFromPath(url); + localFile.readText() + .then((data) => { + try { + const json = JSON.parse(data); + observer.next(successResponse(url, json, 200)); + observer.complete(); + } catch (error) { + // Even though the response status was 2xx, this is still an error. + // The parse error contains the text of the body that failed to parse. + const errorResult = { error, text: data }; + observer.error(errorResponse(url, errorResult, 200)); + } + }, (err: Object) => { + observer.error(errorResponse(url, err, 400)); + + }); + } else { + observer.error(errorResponse(url, "Not Found", 404)); + } + }); +} diff --git a/nativescript-angular/http-client/index.ts b/nativescript-angular/http-client/index.ts new file mode 100644 index 000000000..da2e04db0 --- /dev/null +++ b/nativescript-angular/http-client/index.ts @@ -0,0 +1,2 @@ +export * from "./http-client.module"; +export * from "./ns-http-backend"; diff --git a/nativescript-angular/http-client/ns-http-backend.ts b/nativescript-angular/http-client/ns-http-backend.ts new file mode 100644 index 000000000..6cf2f2bf4 --- /dev/null +++ b/nativescript-angular/http-client/ns-http-backend.ts @@ -0,0 +1,62 @@ +import { Injectable } from "@angular/core"; +import { + HttpRequest, HttpEvent, + XhrFactory, HttpResponse, + HttpErrorResponse, HttpXhrBackend +} from "@angular/common/http"; +import { Observable } from "rxjs/Observable"; + +import { NSFileSystem } from "../file-system/ns-file-system"; +import { isLocalRequest, processLocalFileRequest } from "./http-utils"; + +@Injectable() +export class NsHttpBackEnd extends HttpXhrBackend { + constructor(xhrFactory: XhrFactory, private nsFileSystem: NSFileSystem) { + super(xhrFactory); + } + + handle(req: HttpRequest): Observable> { + let result: Observable>; + + if (isLocalRequest(req.url)) { + result = this.handleLocalFileRequest(req.url); + } else { + result = super.handle(req); + } + + return result; + } + + private handleLocalFileRequest(url: string): Observable> { + return processLocalFileRequest( + url, + this.nsFileSystem, + createSuccessResponse, + createErrorResponse + ); + } +} + +function createSuccessResponse( + url: string, + body: any, + status: number): HttpEvent { + return new HttpResponse({ + url, + body, + status, + statusText: "OK" + }); +} + +function createErrorResponse( + url: string, + body: any, + status: number): HttpErrorResponse { + return new HttpErrorResponse({ + url, + error: body, + status, + statusText: "ERROR" + }); +} diff --git a/nativescript-angular/http.ts b/nativescript-angular/http.ts deleted file mode 100644 index 5f6583a02..000000000 --- a/nativescript-angular/http.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - Http, XHRBackend, RequestOptions -} from "@angular/http"; -import { NSXSRFStrategy, NSHttp } from "./http/ns-http"; -import { NSFileSystem } from "./file-system/ns-file-system"; - -import { NgModule } from "@angular/core"; -import { HttpModule, XSRFStrategy } from "@angular/http"; - -export { NSHttp } from "./http/ns-http"; - -export function nsHttpFactory(backend, options, nsFileSystem) { - return new NSHttp(backend, options, nsFileSystem); -} - -export function nsXSRFStrategyFactory() { - return new NSXSRFStrategy(); -} - -@NgModule({ - providers: [ - {provide: XSRFStrategy, useFactory: nsXSRFStrategyFactory}, - NSFileSystem, - {provide: Http, useFactory: nsHttpFactory, - deps: [XHRBackend, RequestOptions, NSFileSystem]} - ], - imports: [ - HttpModule, - ], - exports: [ - HttpModule, - ] -}) -export class NativeScriptHttpModule { -} diff --git a/nativescript-angular/http/http.module.ts b/nativescript-angular/http/http.module.ts new file mode 100644 index 000000000..b6d35172c --- /dev/null +++ b/nativescript-angular/http/http.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from "@angular/core"; +import { Http, XHRBackend, RequestOptions, HttpModule, XSRFStrategy } from "@angular/http"; + +import { NSXSRFStrategy, NSHttp } from "./ns-http"; +import { NSFileSystem } from "../file-system/ns-file-system"; + +export { NSHttp } from "./ns-http"; + +export function nsHttpFactory(backend, options, nsFileSystem) { + return new NSHttp(backend, options, nsFileSystem); +} + +export function nsXSRFStrategyFactory() { + return new NSXSRFStrategy(); +} + +@NgModule({ + providers: [ + { provide: XSRFStrategy, useFactory: nsXSRFStrategyFactory }, + NSFileSystem, + { + provide: Http, useFactory: nsHttpFactory, + deps: [XHRBackend, RequestOptions, NSFileSystem] + } + ], + imports: [ + HttpModule, + ], + exports: [ + HttpModule, + ] +}) +export class NativeScriptHttpModule { +} diff --git a/nativescript-angular/http/index.ts b/nativescript-angular/http/index.ts new file mode 100644 index 000000000..720657322 --- /dev/null +++ b/nativescript-angular/http/index.ts @@ -0,0 +1 @@ +export * from "./http.module"; diff --git a/nativescript-angular/http/ns-http.ts b/nativescript-angular/http/ns-http.ts index 6b6fff33e..e0f04b661 100644 --- a/nativescript-angular/http/ns-http.ts +++ b/nativescript-angular/http/ns-http.ts @@ -2,6 +2,7 @@ import { Injectable } from "@angular/core"; import { Http, ConnectionBackend, + Request, RequestOptions, RequestOptionsArgs, ResponseOptions, @@ -10,54 +11,62 @@ import { } from "@angular/http"; import { Observable } from "rxjs/Observable"; import "rxjs/add/observable/fromPromise"; + +import { isLocalRequest, processLocalFileRequest } from "../http-client/http-utils"; + import { NSFileSystem } from "../file-system/ns-file-system"; export class NSXSRFStrategy { - public configureRequest(_req: any) { - // noop - } + public configureRequest(_req: any) { + // noop + } } @Injectable() export class NSHttp extends Http { - constructor(backend: ConnectionBackend, defaultOptions: RequestOptions, private nsFileSystem: NSFileSystem) { - super(backend, defaultOptions); - } - - /** - * Performs a request with `get` http method. - * Uses a local file if `~/` resource is requested. - */ - get(url: string, options?: RequestOptionsArgs): Observable { - if (url.indexOf("~") === 0 || url.indexOf("/") === 0) { - // normalize url - url = url.replace("~", "").replace("/", ""); - // request from local app resources - return Observable.fromPromise(new Promise((resolve, reject) => { - let app = this.nsFileSystem.currentApp(); - let localFile = app.getFile(url); - if (localFile) { - localFile.readText().then((data) => { - resolve(responseOptions(data, 200, url)); - }, (err: Object) => { - reject(responseOptions(err, 400, url)); - }); + constructor(backend: ConnectionBackend, defaultOptions: RequestOptions, private nsFileSystem: NSFileSystem) { + super(backend, defaultOptions); + } + + /** + * Performs a request with `request` http method. + */ + request(req: string | Request, options?: RequestOptionsArgs): Observable { + const urlString = typeof req === "string" ? req : req.url; + if (isLocalRequest(urlString)) { + return this.requestLocalFile(urlString); } else { - reject(responseOptions("Not Found", 404, url)); + return super.request(req, options); } - })); - } else { - return super.get(url, options); } - } + + /** + * Performs a request with `get` http method. + */ + get(url: string, options?: RequestOptionsArgs): Observable { + if (isLocalRequest(url)) { + return this.requestLocalFile(url); + } else { + return super.get(url, options); + } + } + + private requestLocalFile(url: string): Observable { + return processLocalFileRequest( + url, + this.nsFileSystem, + createResponse, + createResponse + ); + } } -function responseOptions(body: string | Object, status: number, url: string): Response { - return new Response(new ResponseOptions({ - body: body, - status: status, - statusText: "OK", - type: status === 200 ? ResponseType.Default : ResponseType.Error, - url: url - })); +function createResponse(url: string, body: string | Object, status: number): Response { + return new Response(new ResponseOptions({ + body: body, + status: status, + statusText: "OK", + type: status === 200 ? ResponseType.Default : ResponseType.Error, + url: url + })); } diff --git a/nativescript-angular/index.ts b/nativescript-angular/index.ts index 2270a8ac4..e5f4bfb8b 100644 --- a/nativescript-angular/index.ts +++ b/nativescript-angular/index.ts @@ -26,4 +26,4 @@ export { registerElement, } from "./element-registry"; -export * from "./value-accessors/base-value-accessor"; +export * from "./forms/value-accessors/base-value-accessor"; diff --git a/nativescript-angular/lang-facade.ts b/nativescript-angular/lang-facade.ts index 424b1c64a..31b567065 100644 --- a/nativescript-angular/lang-facade.ts +++ b/nativescript-angular/lang-facade.ts @@ -1,8 +1,3 @@ -/* tslint:disable */ -//Copied unexported functions from @angular/core/src/facade/lang -var globalScope = global; -export var global = globalScope; - export function isPresent(obj: any): boolean { return obj !== undefined && obj !== null; } @@ -10,58 +5,3 @@ export function isPresent(obj: any): boolean { export function isBlank(obj: any): boolean { return obj === undefined || obj === null; } - -export function isDate(obj: any): obj is Date { - return obj instanceof Date && !isNaN(obj.valueOf()); -} - -export function print(obj: Error | Object) { - console.log(obj); -} - -export function isJsObject(o: any): boolean { - return o !== null && (typeof o === 'function' || typeof o === 'object'); -} - -export function isArray(obj: any): boolean { - return Array.isArray(obj); -} - -// When Symbol.iterator doesn't exist, retrieves the key used in es6-shim -declare var Symbol: any; -let _symbolIterator: any = null; -export function getSymbolIterator(): string|symbol { - if (isBlank(_symbolIterator)) { - if (isPresent((globalScope).Symbol) && isPresent(Symbol.iterator)) { - _symbolIterator = Symbol.iterator; - } else { - // es6-shim specific logic - let keys = Object.getOwnPropertyNames(Map.prototype); - for (let i = 0; i < keys.length; ++i) { - let key = keys[i]; - if (key !== 'entries' && key !== 'size' && - (Map as any).prototype[key] === Map.prototype['entries']) { - _symbolIterator = key; - } - } - } - } - return _symbolIterator; -} - -export function setValueOnPath(global: any, path: string, value: any) { - let parts = path.split('.'); - let obj: any = global; - while (parts.length > 1) { - let name = parts.shift(); - if (obj.hasOwnProperty(name) && isPresent(obj[name])) { - obj = obj[name]; - } else { - obj = obj[name] = {}; - } - } - if (obj === undefined || obj === null) { - obj = {}; - } - obj[parts.shift()] = value; -} diff --git a/nativescript-angular/nativescript.module.ts b/nativescript-angular/nativescript.module.ts index e5b34007e..305890ab4 100644 --- a/nativescript-angular/nativescript.module.ts +++ b/nativescript-angular/nativescript.module.ts @@ -21,7 +21,7 @@ import { NativeScriptRendererFactory } from "./renderer"; import { DetachedLoader } from "./common/detached-loader"; export function errorHandlerFactory() { - return new ErrorHandler(true); + return new ErrorHandler(); } @NgModule({ diff --git a/nativescript-angular/package.json b/nativescript-angular/package.json index 2685c0fb6..673f98e03 100644 --- a/nativescript-angular/package.json +++ b/nativescript-angular/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "nativescript-angular", - "version": "4.2.0", + "version": "4.4.0", "description": "An Angular renderer that lets you build mobile apps with NativeScript.", "homepage": "https://www.nativescript.org/", "bugs": "https://github.com/NativeScript/nativescript-angular/issues", @@ -43,32 +43,32 @@ "reflect-metadata": "^0.1.8" }, "peerDependencies": { - "@angular/common": "~4.2.5", - "@angular/compiler": "~4.2.5", - "@angular/core": "~4.2.5", - "@angular/forms": "~4.2.5", - "@angular/http": "~4.2.5", - "@angular/platform-browser": "~4.2.5", - "@angular/router": "~4.2.5", + "@angular/common": "~4.4.1", + "@angular/compiler": "~4.4.1", + "@angular/core": "~4.4.1", + "@angular/forms": "~4.4.1", + "@angular/http": "~4.4.1", + "@angular/platform-browser": "~4.4.1", + "@angular/router": "~4.4.1", "rxjs": "^5.0.1", - "tns-core-modules": "^3.1.0 || >3.2.0-", + "tns-core-modules": "^3.1.0 || >3.3.0-", "zone.js": "^0.8.4" }, "devDependencies": { - "@angular/animations": "~4.2.5", - "@angular/common": "~4.2.5", - "@angular/compiler": "~4.2.5", - "@angular/compiler-cli": "~4.2.5", - "@angular/core": "~4.2.5", - "@angular/forms": "~4.2.5", - "@angular/http": "~4.2.5", - "@angular/platform-browser": "~4.2.5", - "@angular/router": "~4.2.5", + "@angular/animations": "~4.4.1", + "@angular/common": "~4.4.1", + "@angular/compiler": "~4.4.1", + "@angular/compiler-cli": "~4.4.1", + "@angular/core": "~4.4.1", + "@angular/forms": "~4.4.1", + "@angular/http": "~4.4.1", + "@angular/platform-browser": "~4.4.1", + "@angular/router": "~4.4.1", "codelyzer": "^3.1.2", "rxjs": "^5.4.2", "tns-core-modules": "next", "tslint": "^5.5.0", - "typescript": "^2.4.1", + "typescript": "^2.5.1", "zone.js": "^0.8.12" } } diff --git a/nativescript-angular/platform-common.ts b/nativescript-angular/platform-common.ts index 374d9276c..a5fbb67e1 100644 --- a/nativescript-angular/platform-common.ts +++ b/nativescript-angular/platform-common.ts @@ -18,18 +18,17 @@ import { EventEmitter, Provider, Sanitizer, - OpaqueToken + InjectionToken } from "@angular/core"; - -// Work around a TS bug requiring an import of OpaqueToken without using it -if ((global).___TS_UNUSED) { - (() => { - return OpaqueToken; - })(); -} +import { DOCUMENT } from "@angular/common"; import { rendererLog, rendererError } from "./trace"; -import { PAGE_FACTORY, PageFactory, defaultPageFactoryProvider, setRootPage } from "./platform-providers"; +import { + PAGE_FACTORY, + PageFactory, + defaultPageFactoryProvider, + setRootPage +} from "./platform-providers"; import { start, setCssFileName } from "tns-core-modules/application"; import { topmost, NavigationEntry } from "tns-core-modules/ui/frame"; @@ -43,6 +42,12 @@ export const onAfterLivesync = new EventEmitter>(); let lastBootstrappedModule: WeakRef>; type BootstrapperAction = () => Promise>; +// Work around a TS bug requiring an import of OpaqueToken without using it +if ((global).___TS_UNUSED) { + (() => { + return InjectionToken; + })(); +} export interface AppOptions { bootInExistingPage?: boolean; cssFile?: string; @@ -57,9 +62,16 @@ export class NativeScriptSanitizer extends Sanitizer { } } +export class NativeScriptDocument { + createElement(tag: string) { + throw new Error("NativeScriptDocument is not DOM Document. There is no createElement() method."); + } +} + export const COMMON_PROVIDERS = [ defaultPageFactoryProvider, { provide: Sanitizer, useClass: NativeScriptSanitizer }, + { provide: DOCUMENT, useClass: NativeScriptDocument }, ]; export class NativeScriptPlatformRef extends PlatformRef { @@ -82,7 +94,7 @@ export class NativeScriptPlatformRef extends PlatformRef { bootstrapModule( moduleType: Type, compilerOptions: CompilerOptions | CompilerOptions[] = [] - ): Promise> { + ): Promise> { this._bootstrapper = () => this.platform.bootstrapModule(moduleType, compilerOptions); this.bootstrapApp(); diff --git a/nativescript-angular/platform-providers.ts b/nativescript-angular/platform-providers.ts index 466fa54c9..86d47e0a2 100644 --- a/nativescript-angular/platform-providers.ts +++ b/nativescript-angular/platform-providers.ts @@ -1,19 +1,13 @@ +import { InjectionToken } from "@angular/core"; + import { topmost, Frame } from "tns-core-modules/ui/frame"; +import { View } from "tns-core-modules/ui/core/view"; import { Page } from "tns-core-modules/ui/page"; -import { OpaqueToken } from "@angular/core"; import { device, Device } from "tns-core-modules/platform"; -import * as platform from "tns-core-modules/platform"; - -export const APP_ROOT_VIEW = new OpaqueToken("App Root View"); -export const DEVICE = new OpaqueToken("platfrom device"); -export const PAGE_FACTORY = new OpaqueToken("page factory"); -// Work around a TS bug requiring an import of platform.Device without using it -if ((global).___TS_UNUSED) { - (() => { - return platform; - })(); -} +export const APP_ROOT_VIEW = new InjectionToken("App Root View"); +export const DEVICE = new InjectionToken("platform device"); +export const PAGE_FACTORY = new InjectionToken("page factory"); let _rootPageRef: WeakRef; diff --git a/nativescript-angular/platform.ts b/nativescript-angular/platform.ts index 1c418e794..f1a6ad80b 100644 --- a/nativescript-angular/platform.ts +++ b/nativescript-angular/platform.ts @@ -35,6 +35,7 @@ if ((global).___TS_UNUSED) { import "./dom-adapter"; import { NativeScriptElementSchemaRegistry } from "./schema-registry"; +import { NSFileSystem } from "./file-system/ns-file-system"; import { FileSystemResourceLoader } from "./resource-loader"; export const NS_COMPILER_PROVIDERS = [ @@ -43,6 +44,7 @@ export const NS_COMPILER_PROVIDERS = [ provide: COMPILER_OPTIONS, useValue: { providers: [ + NSFileSystem, { provide: ResourceLoader, useClass: FileSystemResourceLoader }, { provide: ElementSchemaRegistry, useClass: NativeScriptElementSchemaRegistry }, ] diff --git a/nativescript-angular/renderer.ts b/nativescript-angular/renderer.ts index ddf247c0b..cdf550fca 100644 --- a/nativescript-angular/renderer.ts +++ b/nativescript-angular/renderer.ts @@ -11,7 +11,6 @@ import { topmost } from "tns-core-modules/ui/frame"; import { profile } from "tns-core-modules/profiling"; import { APP_ROOT_VIEW, DEVICE, getRootPage } from "./platform-providers"; -import { isBlank } from "./lang-facade"; import { ViewUtil } from "./view-util"; import { NgView, InvisibleNode } from "./element-registry"; import { rendererLog as traceLog } from "./trace"; @@ -23,6 +22,11 @@ export const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`; export const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`; const ATTR_SANITIZER = /-/g; +export interface ElementReference { + previous: NgView; + next: NgView; +} + @Injectable() export class NativeScriptRendererFactory implements RendererFactory2 { private componentRenderers = new Map(); @@ -55,7 +59,7 @@ export class NativeScriptRendererFactory implements RendererFactory2 { } let renderer: NativeScriptRenderer = this.componentRenderers.get(type.id); - if (!isBlank(renderer)) { + if (renderer) { return renderer; } @@ -85,15 +89,16 @@ export class NativeScriptRenderer extends Renderer2 { } @profile - appendChild(parent: any, newChild: NgView): void { + appendChild(parent: NgView, newChild: NgView): void { traceLog(`NativeScriptRenderer.appendChild child: ${newChild} parent: ${parent}`); this.viewUtil.insertChild(parent, newChild); } @profile - insertBefore(parent: NgView, newChild: NgView, refChildIndex: number): void { - traceLog(`NativeScriptRenderer.insertBefore child: ${newChild} parent: ${parent}`); - this.viewUtil.insertChild(parent, newChild, refChildIndex); + insertBefore(parent: NgView, newChild: NgView, { previous, next }: ElementReference): void { + traceLog(`NativeScriptRenderer.insertBefore child: ${newChild} ` + + `parent: ${parent} previous: ${previous} next: ${next}`); + this.viewUtil.insertChild(parent, newChild, previous, next); } @profile @@ -110,14 +115,18 @@ export class NativeScriptRenderer extends Renderer2 { @profile parentNode(node: NgView): any { - traceLog("NativeScriptRenderer.parentNode for node: " + node); + traceLog(`NativeScriptRenderer.parentNode for node: ${node}`); return node.parent || node.templateParent; } @profile - nextSibling(node: NgView): number { - traceLog(`NativeScriptRenderer.nextSibling ${node}`); - return this.viewUtil.nextSiblingIndex(node); + nextSibling(node: NgView): ElementReference { + traceLog(`NativeScriptRenderer.nextSibling of ${node} is ${node.nextSibling}`); + + return { + previous: node, + next: node.nextSibling, + }; } @profile diff --git a/nativescript-angular/resource-loader.ts b/nativescript-angular/resource-loader.ts index e3c2ba4db..43a5091bc 100644 --- a/nativescript-angular/resource-loader.ts +++ b/nativescript-angular/resource-loader.ts @@ -1,25 +1,72 @@ -import { path, knownFolders, File } from "tns-core-modules/file-system"; +import { Injectable } from "@angular/core"; import { ResourceLoader } from "@angular/compiler"; +import { path } from "tns-core-modules/file-system"; +import { NSFileSystem } from "./file-system/ns-file-system"; + +const extensionsFallbacks = [ + [".scss", ".css"], + [".sass", ".css"], + [".less", ".css"] +]; + +@Injectable() export class FileSystemResourceLoader extends ResourceLoader { - resolve(url: string, baseUrl: string): string { - // Angular assembles absolute URL's and prefixes them with // + constructor(private fs: NSFileSystem) { + super(); + } + + get(url: string): Promise { + const resolvedPath = this.resolve(url); + + const templateFile = this.fs.fileFromPath(resolvedPath); + + return templateFile.readText(); + } + + resolve(url: string): string { + const normalizedUrl = this.resolveRelativeUrls(url); + + if (this.fs.fileExists(normalizedUrl)) { + return normalizedUrl; + } + + const { candidates: fallbackCandidates, resource: fallbackResource } = + this.fallbackResolve(normalizedUrl); + + if (fallbackResource) { + return fallbackResource; + } + + throw new Error(`Could not resolve ${url}. Looked for: ${normalizedUrl}, ${fallbackCandidates}`); + } + + private resolveRelativeUrls(url: string): string { + // Angular assembles absolute URLs and prefixes them with // if (url.indexOf("/") !== 0) { - // Resolve relative URL's based on the app root. - return path.join(baseUrl, url); + // Resolve relative URLs based on the app root. + return path.join(this.fs.currentApp().path, url); } else { return url; } } - get(url: string): Promise { - const appDir = knownFolders.currentApp().path; - const templatePath = this.resolve(url, appDir); + private fallbackResolve(url: string): + ({ resource: string, candidates: string[] }) { - if (!File.exists(templatePath)) { - throw new Error(`File ${templatePath} does not exist. Resolved from: ${url}.`); - } - let templateFile = File.fromPath(templatePath); - return templateFile.readText(); + const candidates = extensionsFallbacks + .filter(([extension]) => url.endsWith(extension)) + .map(([extension, fallback]) => + this.replaceExtension(url, extension, fallback)); + + const resource = candidates.find(candidate => this.fs.fileExists(candidate)); + + return { candidates, resource }; + } + + private replaceExtension(fileName: string, oldExtension: string, newExtension: string): string { + const baseName = fileName.substr(0, fileName.length - oldExtension.length); + return baseName + newExtension; } } + diff --git a/nativescript-angular/router/index.ts b/nativescript-angular/router/index.ts new file mode 100644 index 000000000..d13492a9e --- /dev/null +++ b/nativescript-angular/router/index.ts @@ -0,0 +1 @@ +export * from "./router.module"; diff --git a/nativescript-angular/router/ns-location-strategy.ts b/nativescript-angular/router/ns-location-strategy.ts index 3a0f2a0b1..c474f2a24 100644 --- a/nativescript-angular/router/ns-location-strategy.ts +++ b/nativescript-angular/router/ns-location-strategy.ts @@ -67,7 +67,7 @@ export class NSLocationStrategy extends LocationStrategy { replaceState(state: any, title: string, url: string, queryParams: string): void { if (this.states.length > 0) { - routerLog("NSLocationStrategy.replaceState changing exisitng state: " + + routerLog("NSLocationStrategy.replaceState changing existing state: " + `${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`); const topState = this.peekState(); topState.state = state; @@ -104,7 +104,7 @@ export class NSLocationStrategy extends LocationStrategy { if (state.isPageNavigation) { // This was a page navigation - so navigate through frame. routerLog("NSLocationStrategy.back() while not navigating back but top" + - " state is page - will call frame.goback()"); + " state is page - will call frame.goBack()"); this.frame.goBack(); } else { // Nested navigation - just pop the state diff --git a/nativescript-angular/router/ns-route-reuse-strategy.ts b/nativescript-angular/router/ns-route-reuse-strategy.ts new file mode 100644 index 000000000..d6b489180 --- /dev/null +++ b/nativescript-angular/router/ns-route-reuse-strategy.ts @@ -0,0 +1,146 @@ +import { Injectable } from "@angular/core"; +import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from "@angular/router"; + +import { routeReuseStrategyLog as log } from "../trace"; +import { NSLocationStrategy } from "./ns-location-strategy"; +import { + destroyComponentRef, + findTopActivatedRouteNodeForOutlet, + pageRouterActivatedSymbol +} from "./page-router-outlet"; + +interface CacheItem { + key: string; + state: DetachedRouteHandle; +} + +/** + * Detached state cache + */ +class DetachedStateCache { + private cache = new Array(); + + public get length(): number { + return this.cache.length; + } + + public push(cacheItem: CacheItem) { + this.cache.push(cacheItem); + } + + public pop(): CacheItem { + return this.cache.pop(); + } + + public peek(): CacheItem { + return this.cache[this.cache.length - 1]; + } + + public clear() { + log(`DetachedStateCache.clear() ${this.cache.length} items will be destroyed`); + + while (this.cache.length > 0) { + const state = this.cache.pop().state; + if (!state.componentRef) { + throw new Error("No componentRed found in DetachedRouteHandle"); + } + + destroyComponentRef(state.componentRef); + } + } +} + +/** + * Detaches subtrees loaded inside PageRouterOutlet in forward navigation + * and reattaches them on back. + * Reuses routes as long as their route config is the same. + */ +@Injectable() +export class NSRouteReuseStrategy implements RouteReuseStrategy { + private cache: DetachedStateCache = new DetachedStateCache(); + + constructor(private location: NSLocationStrategy) { } + + shouldDetach(route: ActivatedRouteSnapshot): boolean { + route = findTopActivatedRouteNodeForOutlet(route); + + const key = getSnapshotKey(route); + const isPageActivated = route[pageRouterActivatedSymbol]; + const isBack = this.location._isPageNavigatingBack(); + const shouldDetach = !isBack && isPageActivated; + + log(`shouldDetach isBack: ${isBack} key: ${key} result: ${shouldDetach}`); + + return shouldDetach; + } + + shouldAttach(route: ActivatedRouteSnapshot): boolean { + route = findTopActivatedRouteNodeForOutlet(route); + + const key = getSnapshotKey(route); + const isBack = this.location._isPageNavigatingBack(); + const shouldAttach = isBack && this.cache.peek().key === key; + + log(`shouldAttach isBack: ${isBack} key: ${key} result: ${shouldAttach}`); + + return shouldAttach; + } + + store(route: ActivatedRouteSnapshot, state: DetachedRouteHandle): void { + route = findTopActivatedRouteNodeForOutlet(route); + + const key = getSnapshotKey(route); + log(`store key: ${key}, state: ${state}`); + + if (state) { + this.cache.push({ key, state }); + } else { + const topItem = this.cache.peek(); + if (topItem.key === key) { + this.cache.pop(); + } else { + throw new Error("Trying to pop from DetachedStateCache but keys don't match. " + + `expected: ${topItem.key} actual: ${key}`); + } + } + } + + retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { + route = findTopActivatedRouteNodeForOutlet(route); + + const key = getSnapshotKey(route); + const isBack = this.location._isPageNavigatingBack(); + const cachedItem = this.cache.peek(); + + let state = null; + if (isBack && cachedItem && cachedItem.key === key) { + state = cachedItem.state; + } + + log(`retrieved isBack: ${isBack} key: ${key} state: ${state}`); + + return state; + } + + shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { + const shouldReuse = future.routeConfig === curr.routeConfig; + + if (shouldReuse && curr && curr[pageRouterActivatedSymbol]) { + // When reusing route - copy the pageRouterActivated to the new snapshot + // It's needed in shouldDetach to determine if the route should be detached. + future[pageRouterActivatedSymbol] = curr[pageRouterActivatedSymbol]; + } + + log(`shouldReuseRoute result: ${shouldReuse}`); + + return shouldReuse; + } + + clearCache() { + this.cache.clear(); + } +} + +function getSnapshotKey(snapshot: ActivatedRouteSnapshot): string { + return snapshot.pathFromRoot.join("->"); +} diff --git a/nativescript-angular/router/ns-router-link-active.ts b/nativescript-angular/router/ns-router-link-active.ts index b47f4b9de..9d0210e74 100644 --- a/nativescript-angular/router/ns-router-link-active.ts +++ b/nativescript-angular/router/ns-router-link-active.ts @@ -6,7 +6,7 @@ import { import { Subscription } from "rxjs/Subscription"; import { NavigationEnd, Router, UrlTree } from "@angular/router"; -import { containsTree } from "../router-url-tree"; +import { containsTree } from "./private-imports/router-url-tree"; import { NSRouterLink } from "./ns-router-link"; @@ -53,12 +53,16 @@ import { NSRouterLink } from "./ns-router-link"; * * @stable */ -@Directive({ selector: "[nsRouterLinkActive]" }) +@Directive({ + selector: "[nsRouterLinkActive]", + exportAs: "routerLinkActive", +}) export class NSRouterLinkActive implements OnChanges, OnDestroy, AfterContentInit { // tslint:disable-line:max-line-length directive-class-suffix @ContentChildren(NSRouterLink) links: QueryList; private classes: string[] = []; private subscription: Subscription; + private active: boolean = false; @Input() nsRouterLinkActiveOptions: { exact: boolean } = { exact: false }; @@ -70,6 +74,10 @@ export class NSRouterLinkActive implements OnChanges, OnDestroy, AfterContentIni }); } + get isActive(): boolean { + return this.active; + } + ngAfterContentInit(): void { this.links.changes.subscribe(() => this.update()); this.update(); @@ -91,12 +99,16 @@ export class NSRouterLinkActive implements OnChanges, OnDestroy, AfterContentIni if (!this.links) { return; } - - const currentUrlTree = this.router.parseUrl(this.router.url); - const isActiveLinks = this.reduceList(currentUrlTree, this.links); - this.classes.forEach( - c => this.renderer.setElementClass( - this.element.nativeElement, c, isActiveLinks)); + const hasActiveLinks = this.hasActiveLinks(); + // react only when status has changed to prevent unnecessary dom updates + if (this.active !== hasActiveLinks) { + const currentUrlTree = this.router.parseUrl(this.router.url); + const isActiveLinks = this.reduceList(currentUrlTree, this.links); + this.classes.forEach( + c => this.renderer.setElementClass( + this.element.nativeElement, c, isActiveLinks)); + } + Promise.resolve(hasActiveLinks).then(active => this.active = active); } private reduceList(currentUrlTree: UrlTree, q: QueryList): boolean { @@ -106,4 +118,13 @@ export class NSRouterLinkActive implements OnChanges, OnDestroy, AfterContentIni this.nsRouterLinkActiveOptions.exact); }, false); } + + private isLinkActive(router: Router): (link: NSRouterLink) => boolean { + return (link: NSRouterLink) => + router.isActive(link.urlTree, this.nsRouterLinkActiveOptions.exact); + } + + private hasActiveLinks(): boolean { + return this.links.some(this.isLinkActive(this.router)); + } } diff --git a/nativescript-angular/router/page-router-outlet.ts b/nativescript-angular/router/page-router-outlet.ts index 9f7541ec9..2695026c4 100644 --- a/nativescript-angular/router/page-router-outlet.ts +++ b/nativescript-angular/router/page-router-outlet.ts @@ -2,11 +2,12 @@ import { Attribute, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, Directive, Inject, InjectionToken, Injector, - OnDestroy, OnInit, + OnDestroy, OnInit, EventEmitter, Output, Type, ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, + ActivatedRouteSnapshot, ChildrenOutletContexts, PRIMARY_OUTLET, } from "@angular/router"; @@ -18,12 +19,12 @@ import { profile } from "tns-core-modules/profiling"; import { BehaviorSubject } from "rxjs/BehaviorSubject"; -import { isPresent } from "../lang-facade"; import { DEVICE, PAGE_FACTORY, PageFactory } from "../platform-providers"; -import { routerLog } from "../trace"; +import { routerLog as log } from "../trace"; import { DetachedLoader } from "../common/detached-loader"; import { ViewUtil } from "../view-util"; import { NSLocationStrategy } from "./ns-location-strategy"; +import { NSRouteReuseStrategy } from "./ns-route-reuse-strategy"; export class PageRoute { activatedRoute: BehaviorSubject; @@ -33,79 +34,87 @@ export class PageRoute { } } +// Used to "mark" ActivatedRoute snapshots that are handled in PageRouterOutlet +export const pageRouterActivatedSymbol = Symbol("page-router-activated"); +export const loaderRefSymbol = Symbol("loader-ref"); + +export function destroyComponentRef(componentRef: ComponentRef) { + if (componentRef) { + const loaderRef = componentRef[loaderRefSymbol]; + if (loaderRef) { + loaderRef.destroy(); + } + componentRef.destroy(); + } +} + class ChildInjector implements Injector { constructor( private providers: ProviderMap, private parent: Injector - ) {} + ) { } - get(token: Type|InjectionToken, notFoundValue?: T): T { - return this.providers.get(token) || this.parent.get(token, notFoundValue); + get(token: Type | InjectionToken, notFoundValue?: T): T { + let localValue = this.providers.get(token); + if (localValue) { + return localValue; + } + + return this.parent.get(token, notFoundValue); } } +type ProviderMap = Map | InjectionToken, any>; + /** - * Reference Cache + * There are cases where multiple activatedRoute nodes should be associated/handled by the same PageRouterOutlet. + * We can gat additional ActivatedRoutes nodes when there is: + * - Lazy loading - there is an additional ActivatedRoute node for the RouteConfig with the `loadChildren` setup + * - Componentless routes - there is an additional ActivatedRoute node for the componentless RouteConfig + * + * Example: + * R <-- root + * | + * feature (lazy module) <-- RouteConfig: { path: "lazy", loadChildren: "./feature/feature.module#FeatureModule" } + * | + * module (componentless route) <-- RouteConfig: { path: "module", children: [...] } // Note: No 'component' + * | + * home <-- RouteConfig: { path: "module", component: MyComponent } - this is what we get as activatedRoute param + * + * In these cases we will mark the top-most node (feature). NSRouteReuseStrategy will detach the tree there and + * use this ActivateRoute as a kay for caching. */ -class RefCache { - private cache = new Array(); +export function findTopActivatedRouteNodeForOutlet(activatedRoute: ActivatedRouteSnapshot): ActivatedRouteSnapshot { + let outletActivatedRoute = activatedRoute; - public get length(): number { - return this.cache.length; - } - - public push(cacheItem: CacheItem) { - this.cache.push(cacheItem); - } + while (outletActivatedRoute.parent && + outletActivatedRoute.parent.routeConfig && + !outletActivatedRoute.parent.routeConfig.component) { - public pop(): CacheItem { - return this.cache.pop(); + outletActivatedRoute = outletActivatedRoute.parent; } - public peek(): CacheItem { - return this.cache[this.cache.length - 1]; - } - - public clear(): void { - while (this.length) { - RefCache.destroyItem(this.pop()); - } - } - - public static destroyItem(item: CacheItem) { - if (isPresent(item.componentRef)) { - item.componentRef.destroy(); - } - - if (isPresent(item.loaderRef)) { - item.loaderRef.destroy(); - } - } + return outletActivatedRoute; } -interface CacheItem { - componentRef: ComponentRef; - reusedRoute: PageRoute; - loaderRef?: ComponentRef; +function routeToString(activatedRoute: ActivatedRoute | ActivatedRouteSnapshot): string { + return activatedRoute.pathFromRoot.join("->"); } - -type ProviderMap = Map|InjectionToken, any>; - -const log = (msg: string) => routerLog(msg); - @Directive({ selector: "page-router-outlet" }) // tslint:disable-line:directive-selector export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-line:directive-class-suffix - private activated: ComponentRef|null = null; - private _activatedRoute: ActivatedRoute|null = null; - private refCache: RefCache = new RefCache(); + private activated: ComponentRef | null = null; + private _activatedRoute: ActivatedRoute | null = null; + private isInitialPage: boolean = true; private detachedLoaderFactory: ComponentFactory; - private itemsToDestroy: CacheItem[] = []; private name: string; private viewUtil: ViewUtil; + @Output("activate") activateEvents = new EventEmitter(); // tslint:disable-line:no-output-rename + @Output("deactivate") deactivateEvents = new EventEmitter(); // tslint:disable-line:no-output-rename + /** @deprecated from Angular since v4 */ get locationInjector(): Injector { return this.location.injector; } /** @deprecated from Angular since v4 */ @@ -140,7 +149,8 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l private frame: Frame, private changeDetector: ChangeDetectorRef, @Inject(DEVICE) device: Device, - @Inject(PAGE_FACTORY) private pageFactory: PageFactory + @Inject(PAGE_FACTORY) private pageFactory: PageFactory, + private routeReuseStrategy: NSRouteReuseStrategy ) { this.name = name || PRIMARY_OUTLET; @@ -148,11 +158,6 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l this.viewUtil = new ViewUtil(device); this.detachedLoaderFactory = resolver.resolveComponentFactory(DetachedLoader); - log("DetachedLoaderFactory loaded"); - } - - ngOnDestroy(): void { - this.parentContexts.onChildOutletDestroyed(this.name); } ngOnInit(): void { @@ -176,41 +181,29 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l } } - deactivate(): void { - if (this.locationStrategy._isPageNavigatingBack()) { - log("PageRouterOutlet.deactivate() while going back - should destroy"); - if (!this.isActivated) { - return; - } + ngOnDestroy(): void { + this.parentContexts.onChildOutletDestroyed(this.name); + } - const poppedItem = this.refCache.pop(); - const poppedRef = poppedItem.componentRef; + deactivate(): void { + if (!this.locationStrategy._isPageNavigatingBack()) { + throw new Error("Currently not in page back navigation" + + " - component should be detached instead of deactivated."); + } - if (this.activated !== poppedRef) { - throw new Error("Current componentRef is different for cached componentRef"); - } + log("PageRouterOutlet.deactivate() while going back - should destroy"); - RefCache.destroyItem(poppedItem); - this.activated = null; - } else { - log("PageRouterOutlet.deactivate() while going forward - do nothing"); + if (!this.isActivated) { + return; } - } - private destroyQueuedCacheItems() { - while (this.itemsToDestroy.length > 0) { - this.destroyCacheItem(this.itemsToDestroy.pop()); - } - } + const c = this.activated.instance; + destroyComponentRef(this.activated); - private destroyCacheItem(poppedItem: CacheItem) { - if (isPresent(poppedItem.componentRef)) { - poppedItem.componentRef.destroy(); - } + this.activated = null; + this._activatedRoute = null; - if (isPresent(poppedItem.loaderRef)) { - poppedItem.loaderRef.destroy(); - } + this.deactivateEvents.emit(c); } /** @@ -221,24 +214,26 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l throw new Error("Outlet is not activated"); } - this.location.detach(); - const cmp = this.activated; + log("PageRouterOutlet.detach() - " + routeToString(this._activatedRoute)); + + const component = this.activated; this.activated = null; this._activatedRoute = null; - return cmp; + return component; } /** * Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree */ attach(ref: ComponentRef, activatedRoute: ActivatedRoute) { - log("PageRouterOutlet.attach()" + - "when RouteReuseStrategy instructs to re-attach " + - "previously detached subtree"); + log("PageRouterOutlet.attach() - " + routeToString(activatedRoute)); this.activated = ref; this._activatedRoute = activatedRoute; - this.location.insert(ref.hostView); + + this.markActivatedRoute(activatedRoute); + + this.locationStrategy._finishBackPageNavigation(); } /** @@ -248,20 +243,22 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l @profile activateWith( activatedRoute: ActivatedRoute, - resolver: ComponentFactoryResolver|null - ): void { + resolver: ComponentFactoryResolver | null): void { - log("PageRouterOutlet.activateWith() - " + - "instanciating new component during commit phase of a navigation"); + if (this.locationStrategy._isPageNavigatingBack()) { + throw new Error("Currently in page back navigation - component should be reattached instead of activated."); + } + + log("PageRouterOutlet.activateWith() - " + routeToString(activatedRoute)); this._activatedRoute = activatedRoute; + + this.markActivatedRoute(activatedRoute); + resolver = resolver || this.resolver; - if (this.locationStrategy._isPageNavigatingBack()) { - this.activateOnGoBack(activatedRoute); - } else { - this.activateOnGoForward(activatedRoute, resolver); - } + this.activateOnGoForward(activatedRoute, resolver); + this.activateEvents.emit(this.activated.instance); } private activateOnGoForward( @@ -271,6 +268,7 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l const pageRoute = new PageRoute(activatedRoute); const providers = this.initProvidersMap(activatedRoute, pageRoute); + const childInjector = new ChildInjector(providers, this.location.injector); const factory = this.getComponentFactory(activatedRoute, loadedResolver); @@ -282,12 +280,6 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l this.activated = this.location.createComponent( factory, this.location.length, childInjector, []); this.changeDetector.markForCheck(); - - this.refCache.push({ - componentRef: this.activated, - reusedRoute: pageRoute, - loaderRef: null, - }); } else { log("PageRouterOutlet.activate() forward navigation - " + "create detached loader in the loader container"); @@ -306,11 +298,7 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l this.activated = loaderRef.instance.loadWithFactory(factory); this.loadComponentInPage(page, this.activated); - this.refCache.push({ - componentRef: this.activated, - reusedRoute: pageRoute, - loaderRef, - }); + this.activated[loaderRefSymbol] = loaderRef; } } @@ -329,17 +317,6 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l return providers; } - private activateOnGoBack(activatedRoute: ActivatedRoute): void { - log("PageRouterOutlet.activate() - Back navigation, so load from cache"); - - this.locationStrategy._finishBackPageNavigation(); - - const cacheItem = this.refCache.peek(); - cacheItem.reusedRoute.activatedRoute.next(activatedRoute); - - this.activated = cacheItem.componentRef; - } - @profile private loadComponentInPage(page: Page, componentRef: ComponentRef): void { // Component loaded. Find its root native view. @@ -349,9 +326,6 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l // Add it to the new page page.content = componentView; - page.on(Page.navigatedToEvent, () => setTimeout(() => { - this.destroyQueuedCacheItems(); - })); page.on(Page.navigatedFromEvent, (global).Zone.current.wrap((args: NavigatedData) => { if (args.isBackNavigation) { this.locationStrategy._beginBackPageNavigation(); @@ -360,17 +334,29 @@ export class PageRouterOutlet implements OnDestroy, OnInit { // tslint:disable-l })); const navOptions = this.locationStrategy._beginPageNavigation(); + + // Clear refCache if navigation with clearHistory + if (navOptions.clearHistory) { + const clearCallback = () => setTimeout(() => { + this.routeReuseStrategy.clearCache(); + page.off(Page.navigatedToEvent, clearCallback); + }); + + page.on(Page.navigatedToEvent, clearCallback); + } + this.frame.navigate({ create: () => { return page; }, clearHistory: navOptions.clearHistory, animated: navOptions.animated, transition: navOptions.transition }); + } - // Clear refCache if navigation with clearHistory - if (navOptions.clearHistory) { - this.refCache.clear(); - } + private markActivatedRoute(activatedRoute: ActivatedRoute) { + const nodeToMark = findTopActivatedRouteNodeForOutlet(activatedRoute.snapshot); + nodeToMark[pageRouterActivatedSymbol] = true; + log("Activated route marked as page: " + routeToString(nodeToMark)); } // NOTE: Using private APIs - potential break point! diff --git a/nativescript-angular/router-url-tree.ts b/nativescript-angular/router/private-imports/router-url-tree.ts similarity index 100% rename from nativescript-angular/router-url-tree.ts rename to nativescript-angular/router/private-imports/router-url-tree.ts diff --git a/nativescript-angular/router.ts b/nativescript-angular/router/router.module.ts similarity index 64% rename from nativescript-angular/router.ts rename to nativescript-angular/router/router.module.ts index d5494bcfb..066c437d0 100644 --- a/nativescript-angular/router.ts +++ b/nativescript-angular/router/router.module.ts @@ -5,20 +5,21 @@ import { Optional, SkipSelf, } from "@angular/core"; -import { RouterModule, Routes, ExtraOptions } from "@angular/router"; +import { RouterModule, Routes, ExtraOptions, RouteReuseStrategy } from "@angular/router"; import { LocationStrategy, PlatformLocation } from "@angular/common"; import { Frame } from "tns-core-modules/ui/frame"; -import { NSRouterLink } from "./router/ns-router-link"; -import { NSRouterLinkActive } from "./router/ns-router-link-active"; -import { PageRouterOutlet } from "./router/page-router-outlet"; -import { NSLocationStrategy, LocationState } from "./router/ns-location-strategy"; -import { NativescriptPlatformLocation } from "./router/ns-platform-location"; -import { RouterExtensions } from "./router/router-extensions"; -import { NativeScriptCommonModule } from "./common"; +import { NSRouterLink } from "./ns-router-link"; +import { NSRouterLinkActive } from "./ns-router-link-active"; +import { PageRouterOutlet } from "./page-router-outlet"; +import { NSLocationStrategy, LocationState } from "./ns-location-strategy"; +import { NativescriptPlatformLocation } from "./ns-platform-location"; +import { NSRouteReuseStrategy } from "./ns-route-reuse-strategy"; +import { RouterExtensions } from "./router-extensions"; +import { NativeScriptCommonModule } from "../common"; -export { PageRoute } from "./router/page-router-outlet"; -export { RouterExtensions } from "./router/router-extensions"; -export { NSModuleFactoryLoader } from "./router/ns-module-factory-loader"; +export { PageRoute } from "./page-router-outlet"; +export { RouterExtensions } from "./router-extensions"; +export { NSModuleFactoryLoader } from "./ns-module-factory-loader"; export type LocationState = LocationState; @NgModule({ @@ -37,6 +38,8 @@ export type LocationState = LocationState; NativescriptPlatformLocation, { provide: PlatformLocation, useClass: NativescriptPlatformLocation }, RouterExtensions, + NSRouteReuseStrategy, + { provide: RouteReuseStrategy, useExisting: NSRouteReuseStrategy } ], imports: [ RouterModule, diff --git a/nativescript-angular/trace.ts b/nativescript-angular/trace.ts index 800b96b22..8f81c0005 100644 --- a/nativescript-angular/trace.ts +++ b/nativescript-angular/trace.ts @@ -2,7 +2,9 @@ import { write, categories, messageType } from "tns-core-modules/trace"; export const animationsTraceCategory = "ns-animations"; export const rendererTraceCategory = "ns-renderer"; +export const viewUtilCategory = "ns-view-util"; export const routerTraceCategory = "ns-router"; +export const routeReuseStrategyTraceCategory = "ns-route-reuse-strategy"; export const listViewTraceCategory = "ns-list-view"; export function animationsLog(message: string): void { @@ -17,10 +19,18 @@ export function rendererError(message: string): void { write(message, rendererTraceCategory, messageType.error); } +export function viewUtilLog(msg): void { + write(msg, viewUtilCategory); +} + export function routerLog(message: string): void { write(message, routerTraceCategory); } +export function routeReuseStrategyLog(message: string): void { + write(message, routeReuseStrategyTraceCategory); +} + export function styleError(message: string): void { write(message, categories.Style, messageType.error); } diff --git a/nativescript-angular/view-util.ts b/nativescript-angular/view-util.ts index 99b196197..f1075bac0 100644 --- a/nativescript-angular/view-util.ts +++ b/nativescript-angular/view-util.ts @@ -6,21 +6,22 @@ import { LayoutBase } from "tns-core-modules/ui/layouts/layout-base"; import { CommentNode, InvisibleNode, - NgElement, NgView, TextNode, ViewExtensions, getViewClass, getViewMeta, isDetachedElement, + isInvisibleNode, isKnownView, + isView, } from "./element-registry"; import { platformNames, Device } from "tns-core-modules/platform"; -import { rendererLog as traceLog } from "./trace"; +import { viewUtilLog as traceLog } from "./trace"; -const XML_ATTRIBUTES = Object.freeze(["style", "rows", "columns", "fontAttributes"]); const ELEMENT_NODE_TYPE = 1; +const XML_ATTRIBUTES = Object.freeze(["style", "rows", "columns", "fontAttributes"]); const whiteSpaceSplitter = /\s+/; export type ViewExtensions = ViewExtensions; @@ -30,10 +31,6 @@ export type NgContentView = ContentView & ViewExtensions; export type NgPlaceholder = Placeholder & ViewExtensions; export type BeforeAttachAction = (view: View) => void; -export function isView(view: any): view is NgView { - return view instanceof View; -} - export function isLayout(view: any): view is NgLayoutBase { return view instanceof LayoutBase; } @@ -53,54 +50,174 @@ export class ViewUtil { this.isAndroid = device.os === platformNames.android; } - public insertChild(parent: any, child: NgElement, atIndex: number = -1) { - if (child instanceof InvisibleNode) { - child.templateParent = parent; + public insertChild( + parent: View, + child: View, + previous?: NgView, + next?: NgView + ) { + if (!parent) { + return; } - if (!parent || isDetachedElement(child)) { - return; + const extendedParent = this.ensureNgViewExtensions(parent); + const extendedChild = this.ensureNgViewExtensions(child); + + if (!previous) { + previous = extendedParent.lastChild; + } + this.addToQueue(extendedParent, extendedChild, previous, next); + + if (isInvisibleNode(child)) { + extendedChild.templateParent = extendedParent; + } + + if (!isDetachedElement(child)) { + const nextVisual = this.findNextVisual(next); + this.addToVisualTree(extendedParent, extendedChild, nextVisual); + } + } + + private addToQueue( + parent: NgView, + child: NgView, + previous: NgView, + next: NgView + ): void { + traceLog(`ViewUtil.addToQueue parent: ${parent}, view: ${child}, ` + + `previous: ${previous}, next: ${next}`); + + if (previous) { + previous.nextSibling = child; + } else { + parent.firstChild = child; } + if (next) { + child.nextSibling = next; + } else { + this.appendToQueue(parent, child); + } + } + + private appendToQueue(parent: NgView, view: NgView) { + traceLog(`ViewUtil.appendToQueue parent: ${parent} view: ${view}`); + + if (parent.lastChild) { + parent.lastChild.nextSibling = view; + } + + parent.lastChild = view; + } + + private addToVisualTree(parent: NgView, child: NgView, next: NgView): void { + traceLog(`ViewUtil.addToVisualTree parent: ${parent}, view: ${child}, next: ${next}`); + if (parent.meta && parent.meta.insertChild) { - parent.meta.insertChild(parent, child, atIndex); + parent.meta.insertChild(parent, child, next); } else if (isLayout(parent)) { - if (child.parent === parent) { - const index = (parent).getChildIndex(child); - if (index !== -1) { - parent.removeChild(child); - } - } - if (atIndex !== -1) { - parent.insertChild(child, atIndex); - } else { - parent.addChild(child); - } + this.insertToLayout(parent, child, next); } else if (isContentView(parent)) { parent.content = child; - } else if (parent && parent._addChildFromBuilder) { - parent._addChildFromBuilder(child.nodeName, child); + } else if (parent && (parent)._addChildFromBuilder) { + (parent)._addChildFromBuilder(child.nodeName, child); } } - public removeChild(parent: any, child: NgElement) { - if (!parent || isDetachedElement(child)) { + private insertToLayout( + parent: NgLayoutBase, + child: NgView, + next: NgView + ): void { + if (child.parent === parent) { + this.removeLayoutChild(parent, child); + } + + const nextVisual = this.findNextVisual(next); + if (nextVisual) { + const index = parent.getChildIndex(nextVisual); + parent.insertChild(child, index); + } else { + parent.addChild(child); + } + } + + private findNextVisual(view: NgView): NgView { + let next = view; + while (next && isDetachedElement(next)) { + next = next.nextSibling; + } + + return next; + } + + public removeChild(parent: View, child: View) { + traceLog(`ViewUtil.removeChild parent: ${parent} child: ${child}`); + + if (!parent) { return; } - if (parent.meta && parent.meta.removeChild) { - parent.meta.removeChild(parent, child); - } else if (isLayout(parent)) { - parent.removeChild(child); - } else if (isContentView(parent)) { - if (parent.content === child) { - parent.content = null; - } - } else if (isView(parent)) { - parent._removeView(child); + const extendedParent = this.ensureNgViewExtensions(parent); + const extendedChild = this.ensureNgViewExtensions(child); + + this.removeFromQueue(extendedParent, extendedChild); + this.removeFromVisualTree(extendedParent, extendedChild); + } + + private removeFromQueue(parent: NgView, child: NgView) { + traceLog(`ViewUtil.removeFromQueue parent: ${parent} child: ${child}`); + + if (parent.firstChild === child && parent.lastChild === child) { + parent.firstChild = null; + parent.lastChild = null; + return; + } + + if (parent.firstChild === child) { + parent.firstChild = child.nextSibling; + } + + const previous = this.findPreviousElement(parent, child); + if (parent.lastChild === child) { + parent.lastChild = previous; + } + + if (previous) { + previous.nextSibling = child.nextSibling; } } + // NOTE: This one is O(n) - use carefully + private findPreviousElement(parent: NgView, child: NgView): NgView { + traceLog(`ViewUtil.findPreviousElement parent: ${parent} child: ${child}`); + + let previousVisual; + if (isLayout(parent)) { + previousVisual = this.getPreviousVisualElement(parent, child); + } + + let previous = previousVisual || parent.firstChild; + + // since detached elements are not added to the visual tree, + // we need to find the actual previous sibling of the view, + // which may as well be an invisible node + while (previous && previous !== child && previous.nextSibling !== child) { + previous = previous.nextSibling; + } + + return previous; + } + + private getPreviousVisualElement(parent: NgLayoutBase, child: NgView): NgView { + const elementIndex = parent.getChildIndex(child); + + if (elementIndex > 0) { + return parent.getChildAt(elementIndex - 1) as NgView; + } + } + + // NOTE: This one is O(n) - use carefully public getChildIndex(parent: any, child: NgView) { if (isLayout(parent)) { return parent.getChildIndex(child); @@ -109,6 +226,30 @@ export class ViewUtil { } } + private removeFromVisualTree(parent: NgView, child: NgView) { + traceLog(`ViewUtil.findPreviousElement parent: ${parent} child: ${child}`); + + if (parent.meta && parent.meta.removeChild) { + parent.meta.removeChild(parent, child); + } else if (isLayout(parent)) { + this.removeLayoutChild(parent, child); + } else if (isContentView(parent) && parent.content === child) { + parent.content = null; + parent.lastChild = null; + parent.firstChild = null; + } else if (isView(parent)) { + parent._removeView(child); + } + } + + private removeLayoutChild(parent: NgLayoutBase, child: NgView): void { + const index = parent.getChildIndex(child); + + if (index !== -1) { + parent.removeChild(child); + } + } + public createComment(): InvisibleNode { return new CommentNode(); } @@ -125,18 +266,34 @@ export class ViewUtil { } const viewClass = getViewClass(name); - let view = new viewClass(); - view.nodeName = name; - view.meta = getViewMeta(name); + const view = new viewClass(); + const ngView = this.setNgViewExtensions(view, name); + + return ngView; + } + + private ensureNgViewExtensions(view: View): NgView { + if (view.hasOwnProperty("meta")) { + return view as NgView; + } else { + const name = view.typeName; + const ngView = this.setNgViewExtensions(view, name); + + return ngView; + } + } + + private setNgViewExtensions(view: View, name: string): NgView { + const ngView = view as NgView; + ngView.nodeName = name; + ngView.meta = getViewMeta(name); // we're setting the node type of the view // to 'element' because of checks done in the - // dom animation engine: - // tslint:disable-next-line:max-line-length - // https://github.com/angular/angular/blob/master/packages/animations/browser/src/render/dom_animation_engine.ts#L70-L81 - view.nodeType = ELEMENT_NODE_TYPE; + // dom animation engine + ngView.nodeType = ELEMENT_NODE_TYPE; - return view; + return ngView; } public setProperty(view: NgView, attributeName: string, value: any, namespace?: string): void { @@ -168,34 +325,11 @@ export class ViewUtil { } } - // finds the node in the parent's views and returns the next index - // returns -1 if the node has no parent or next sibling - public nextSiblingIndex(node: NgView): number { - const parent = node.parent; - if (!parent) { - return -1; - } - - let index = 0; - let found = false; - parent.eachChild(child => { - if (child === node) { - found = true; - } - - index += 1; - return !found; - }); - - return found ? index : -1; - } - private runsIn(platform: string): boolean { return (platform === "ios" && this.isIos) || (platform === "android" && this.isAndroid); } - private setPropertyInternal(view: NgView, attributeName: string, value: any): void { traceLog(`Setting attribute: ${attributeName}=${value} to ${view}`); @@ -275,4 +409,3 @@ export class ViewUtil { view.style[styleName] = unsetValue; } } - diff --git a/nativescript-angular/yarn.lock b/nativescript-angular/yarn.lock deleted file mode 100644 index 2ade79c72..000000000 --- a/nativescript-angular/yarn.lock +++ /dev/null @@ -1,381 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@angular/animations@~4.0.0 || ~4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.1.0.tgz#97b642aee01b5406e03ec65e499342ba91e2dd38" - -"@angular/common@~4.0.0 || ~4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.1.0.tgz#4370f569e51ddd99963b7f4aa58c1a5dcc5fea52" - -"@angular/compiler-cli@~4.0.0 || ~4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-4.1.0.tgz#727aaada8bfd94285e9818995925048f7fdf1200" - dependencies: - "@angular/tsc-wrapped" "4.1.0" - minimist "^1.2.0" - reflect-metadata "^0.1.2" - -"@angular/compiler@~4.0.0 || ~4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.1.0.tgz#be1ade5b6aec81f03c29d52bcb95925a28900dcb" - -"@angular/core@~4.0.0 || ~4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.1.0.tgz#72ec173316879571880c9c483ed6dfc0caab94b0" - -"@angular/forms@~4.0.0 || ~4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.1.0.tgz#8eae2a45c4ba064b377f9280e59c012b5dac6b80" - -"@angular/http@~4.0.0 || ~4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/http/-/http-4.1.0.tgz#7ba0c4d044dee964021b7cf19cb146a2c31577a5" - -"@angular/platform-browser@~4.0.0 || ~4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.1.0.tgz#b981386be1a36f2af7f0679447fd97b7267b25de" - -"@angular/router@~4.0.0 || ~4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-4.1.0.tgz#dd3563662f95ca3aa3dd9ff13c6ed4bea1d90b06" - -"@angular/tsc-wrapped@4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@angular/tsc-wrapped/-/tsc-wrapped-4.1.0.tgz#07cbd61d91adde4c2daf9a41605152952b8832b3" - dependencies: - tsickle "^0.21.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - -app-root-path@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46" - -babel-code-frame@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" - dependencies: - chalk "^1.1.0" - esutils "^2.0.2" - js-tokens "^3.0.0" - -balanced-match@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" - -brace-expansion@^1.0.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" - dependencies: - balanced-match "^0.4.1" - concat-map "0.0.1" - -chalk@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -codelyzer@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-3.0.1.tgz#ba66b7b2aa564fe9f45d6004b4003ad2cf116828" - dependencies: - app-root-path "^2.0.1" - css-selector-tokenizer "^0.7.0" - cssauron "^1.4.0" - semver-dsl "^1.0.1" - source-map "^0.5.6" - sprintf-js "^1.0.3" - -colors@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -css-selector-tokenizer@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" - dependencies: - cssesc "^0.1.0" - fastparse "^1.1.1" - regexpu-core "^1.0.0" - -cssauron@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/cssauron/-/cssauron-1.4.0.tgz#a6602dff7e04a8306dc0db9a551e92e8b5662ad8" - dependencies: - through X.X.X - -cssesc@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" - -diff@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" - -escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - -fastparse@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" - -findup-sync@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" - dependencies: - glob "~5.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -glob@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@~5.0.0: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -js-tokens@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - -"minimatch@2 || 3", minimatch@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" - dependencies: - brace-expansion "^1.0.0" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - -mkdirp@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -nativescript-intl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/nativescript-intl/-/nativescript-intl-3.0.0.tgz#82ee9be7d377172b3c4295734723037628e186a7" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -optimist@~0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-parse@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" - -reflect-metadata@^0.1.2, reflect-metadata@^0.1.8: - version "0.1.10" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.10.tgz#b4f83704416acad89988c9b15635d47e03b9344a" - -regenerate@^1.2.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" - -regexpu-core@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" - dependencies: - jsesc "~0.5.0" - -resolve@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" - dependencies: - path-parse "^1.0.5" - -rxjs@^5.0.1: - version "5.3.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.3.0.tgz#d88ccbdd46af290cbdb97d5d8055e52453fabe2d" - dependencies: - symbol-observable "^1.0.1" - -semver-dsl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/semver-dsl/-/semver-dsl-1.0.1.tgz#d3678de5555e8a61f629eed025366ae5f27340a0" - dependencies: - semver "^5.3.0" - -semver@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - -source-map-support@^0.4.2: - version "0.4.15" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" - dependencies: - source-map "^0.5.6" - -source-map@^0.5.6: - version "0.5.6" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" - -sprintf-js@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -symbol-observable@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" - -through@X.X.X: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - -tns-core-modules-widgets@internal-preview: - version "3.1.0-2017-04-28-266" - resolved "https://registry.yarnpkg.com/tns-core-modules-widgets/-/tns-core-modules-widgets-3.1.0-2017-04-28-266.tgz#56d82a9952604e064b76bbd232d56cad9a13337a" - -tns-core-modules@internal-preview: - version "3.1.0-2017-04-26-6412" - resolved "https://registry.yarnpkg.com/tns-core-modules/-/tns-core-modules-3.1.0-2017-04-26-6412.tgz#ae7717292d511e3e6fcac14330d0b0adc18e905d" - dependencies: - tns-core-modules-widgets internal-preview - -tsickle@^0.21.0: - version "0.21.6" - resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.21.6.tgz#53b01b979c5c13fdb13afb3fb958177e5991588d" - dependencies: - minimist "^1.2.0" - mkdirp "^0.5.1" - source-map "^0.5.6" - source-map-support "^0.4.2" - -tslint@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.1.0.tgz#51a47baeeb58956fcd617bd2cf00e2ef0eea2ed9" - dependencies: - babel-code-frame "^6.22.0" - colors "^1.1.2" - diff "^3.2.0" - findup-sync "~0.3.0" - glob "^7.1.1" - optimist "~0.6.0" - resolve "^1.3.2" - semver "^5.3.0" - tsutils "^1.4.0" - -tsutils@^1.4.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.8.0.tgz#bf8118ed8e80cd5c9fc7d75728c7963d44ed2f52" - -typescript@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.2.tgz#f0f045e196f69a72f06b25fd3bd39d01c3ce9984" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -zone.js@^0.8.4: - version "0.8.9" - resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.9.tgz#34aaa9a3ec6d0e4acebd1b761adafa590473638b" diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index 2b0c85ed1..5e860f71c 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -3,25 +3,30 @@ import { platformNativeScriptDynamic } from "nativescript-angular/platform"; import { NativeScriptAnimationsModule } from "nativescript-angular/animations"; import { onAfterLivesync, onBeforeLivesync } from "nativescript-angular/platform-common"; import { NgModule } from "@angular/core"; +import { DOCUMENT } from '@angular/common'; import { Router } from "@angular/router"; import { NativeScriptRouterModule } from "nativescript-angular/router"; import { NativeScriptFormsModule } from "nativescript-angular/forms"; import { NativeScriptHttpModule } from "nativescript-angular/http"; +import { NativeScriptHttpClientModule } from "nativescript-angular/http-client"; + import { rendererTraceCategory, routerTraceCategory, listViewTraceCategory, animationsTraceCategory, + routeReuseStrategyTraceCategory, } from "nativescript-angular/trace"; import { PAGE_FACTORY, PageFactory, PageFactoryOptions } from "nativescript-angular/platform-providers"; import { Page } from "ui/page"; import { Color } from "color"; import { setCategories, enable } from "trace"; -setCategories( - `${animationsTraceCategory},${rendererTraceCategory}` -); +// setCategories( +// `${animationsTraceCategory},${rendererTraceCategory}` +// ); // setCategories(routerTraceCategory); // setCategories(listViewTraceCategory); +setCategories(`${routeReuseStrategyTraceCategory}`); enable(); import { RendererTest } from "./examples/renderer-test"; @@ -32,6 +37,7 @@ import { ListTemplateSelectorTest } from "./examples/list/template-selector"; import { ListTestAsync, ListTestFilterAsync } from "./examples/list/list-test-async"; import { ImageTest } from "./examples/image/image-test"; import { HttpTest } from "./examples/http/http-test"; +import { HttpClientTest } from "./examples/http-client/http-client-test"; import { ActionBarTest } from "./examples/action-bar/action-bar-test"; import { ModalTest } from "./examples/modal/modal-test"; import { PlatfromDirectivesTest } from "./examples/platform-directives/platform-directives-test"; @@ -51,6 +57,7 @@ import { AnimationNgClassTest } from "./examples/animation/animation-ngclass-tes import { AnimationStatesTest } from "./examples/animation/animation-states-test"; import { AnimationStatesMultiTest } from "./examples/animation/animation-states-multi-test"; + @NgModule({ declarations: [ ], @@ -58,12 +65,14 @@ import { AnimationStatesMultiTest } from "./examples/animation/animation-states- NativeScriptModule, NativeScriptFormsModule, NativeScriptHttpModule, + NativeScriptHttpClientModule, NativeScriptRouterModule, ], exports: [ NativeScriptModule, NativeScriptFormsModule, NativeScriptHttpModule, + NativeScriptHttpClientModule, NativeScriptRouterModule, ], providers: [] @@ -128,6 +137,7 @@ const customPageFactoryProvider = { // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(ImageTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(ModalTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(HttpTest)); +platformNativeScriptDynamic().bootstrapModule(makeExampleModule(HttpClientTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(PlatfromDirectivesTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(ActionBarTest)); @@ -139,7 +149,7 @@ const customPageFactoryProvider = { // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(LoginAppComponent)); // animations -platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest)); +// platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationStatesMultiTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationNgClassTest)); // platformNativeScriptDynamic().bootstrapModule(makeExampleModule(AnimationKeyframesTest)); diff --git a/ng-sample/app/examples/http-client/data.json b/ng-sample/app/examples/http-client/data.json new file mode 100644 index 000000000..52d4169d4 --- /dev/null +++ b/ng-sample/app/examples/http-client/data.json @@ -0,0 +1,8 @@ +{ + "results": [ + { + "title": "Test", + "description": "Testing Http local and remote." + } + ] +} diff --git a/ng-sample/app/examples/http-client/http-client-test.ts b/ng-sample/app/examples/http-client/http-client-test.ts new file mode 100644 index 000000000..7af6fb9d6 --- /dev/null +++ b/ng-sample/app/examples/http-client/http-client-test.ts @@ -0,0 +1,122 @@ +import { Component, Inject, Injectable } from "@angular/core"; +import { + HttpClient, HTTP_INTERCEPTORS, HttpEventType, HttpErrorResponse, + HttpEvent, HttpInterceptor, HttpHandler, HttpRequest +} from "@angular/common/http"; +import { Observable } from "rxjs/Observable"; +import "rxjs/add/operator/do"; + +@Injectable() +export class CustomInterceptor implements HttpInterceptor { + intercept(req: HttpRequest, next: HttpHandler): Observable> { + console.log(`[CustomInterceptor] intercept url: ${req.url}`); + + return next.handle(req) + .do(event => { + console.log(`[CustomInterceptor] handled type: ${HttpEventType[event.type]} url: ${req.url}`); + }); + } +} + +interface DataResults { + results: Array; +} + +interface LocalData { + title: string; + description: string; +} + +interface RemoteData { + name: { first: string }; + email: string; +} + +@Component({ + selector: "http-client-test", + template: ` + + + + + + + + + + + + `, + styles: [` + #title { margin-top:20; } + Label { margin: 5 20; } + `], +}) +export class HttpClientTest { + static providers = [ + { + provide: HTTP_INTERCEPTORS, + useClass: CustomInterceptor, + multi: true, + } + ]; + + public title: string; + public description: string; + public error: string; + + constructor(private http: HttpClient) { + } + + public loadLocal() { + this.http.get>("~/examples/http/data.json") + .subscribe((response) => { + let user = response.results[0]; + this.onSuccess(user.title, user.description); + }, (error) => { + this.onError(error); + }); + } + + public loadRemote() { + this.http.get>(`https://randomuser.me/api/?results=1&nat=us`) + .subscribe((response) => { + const user = response.results[0]; + this.onSuccess(user.email, user.name.first); + }, (error) => { + this.onError(error); + }); + } + + public loadNonexistentLocal() { + this.http.get>("~/non/existent/app/folder/data.json") + .subscribe((response) => { + this.onSuccess("strange?!", ""); + }, (error) => { + this.onError(error); + }); + } + + public loadNonexistentRemote() { + this.http.get>("https://google.com/non/existent/url/data.json") + .subscribe((response) => { + this.onSuccess("strange?!", ""); + }, (error) => { + this.onError(error); + }); + } + + private onSuccess(title: string, description: string) { + this.title = title; + this.description = description; + this.error = ""; + } + + private onError(error: HttpErrorResponse) { + console.log("onError " + error); + console.dir(error); + this.title = ""; + this.description = ""; + this.error = error.message; + } +} diff --git a/ng-sample/app/examples/list/list-test-async.ts b/ng-sample/app/examples/list/list-test-async.ts index 146ee9cba..ed635997c 100644 --- a/ng-sample/app/examples/list/list-test-async.ts +++ b/ng-sample/app/examples/list/list-test-async.ts @@ -15,11 +15,11 @@ import "rxjs/add/operator/combineLatest"; - + @@ -68,11 +68,11 @@ export class ListTestAsync { - + diff --git a/ng-sample/app/examples/list/list-test.ts b/ng-sample/app/examples/list/list-test.ts index ccec8a964..9390b6dfd 100644 --- a/ng-sample/app/examples/list/list-test.ts +++ b/ng-sample/app/examples/list/list-test.ts @@ -42,26 +42,26 @@ export class ItemComponent implements AfterViewChecked, DoCheck { - + ` // TEMPLATE WITH COMPONENT - // + // // IN-PLACE TEMPLATE - // + // }) export class ListTest { public myItems: Array; diff --git a/ng-sample/app/examples/list/template-selector.ts b/ng-sample/app/examples/list/template-selector.ts index 4557b7e9c..54c11b28e 100644 --- a/ng-sample/app/examples/list/template-selector.ts +++ b/ng-sample/app/examples/list/template-selector.ts @@ -43,13 +43,13 @@ export class HeaderComponent implements DoCheck { row="1" margin="10" [itemTemplateSelector]="templateSelector"> - + - + diff --git a/ng-sample/app/examples/renderer-test.ts b/ng-sample/app/examples/renderer-test.ts index 7ceb33849..232aaed4a 100644 --- a/ng-sample/app/examples/renderer-test.ts +++ b/ng-sample/app/examples/renderer-test.ts @@ -1,7 +1,5 @@ import { Component, Directive, Host, ElementRef, Input } from "@angular/core"; import { Observable } from "data/observable"; -import { TextValueAccessor } from "nativescript-angular/value-accessors/text-value-accessor"; -import { CheckedValueAccessor } from "nativescript-angular/value-accessors/checked-value-accessor"; import { SegmentedBarItem } from "ui/segmented-bar"; diff --git a/ng-sample/app/examples/router/clear-history-test.ts b/ng-sample/app/examples/router/clear-history-test.ts index c83b40936..dba5a8e86 100644 --- a/ng-sample/app/examples/router/clear-history-test.ts +++ b/ng-sample/app/examples/router/clear-history-test.ts @@ -40,10 +40,10 @@ class LocationLogService { (service.routerEvents$ | async)" margin="10"> - + diff --git a/ng-sample/app/examples/router/page-router-outlet-nested-test.ts b/ng-sample/app/examples/router/page-router-outlet-nested-test.ts index 104278c94..5f5e07da5 100644 --- a/ng-sample/app/examples/router/page-router-outlet-nested-test.ts +++ b/ng-sample/app/examples/router/page-router-outlet-nested-test.ts @@ -1,5 +1,5 @@ import { Component, OnInit, OnDestroy } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute, Router, Route } from "@angular/router"; import { Location } from "@angular/common"; import { Page } from "ui/page"; import { Observable } from "rxjs/Observable"; @@ -12,8 +12,8 @@ import "rxjs/add/operator/map"; - - + + ` }) @@ -70,6 +70,14 @@ class DetailComponent { console.log("DetailComponent.constructor()"); this.id$ = route.params.map(r => r["id"]); } + + ngOnInit() { + console.log("DetailComponent - ngOnInit()"); + } + + ngOnDestroy() { + console.log("DetailComponent - ngOnDestroy()"); + } } @Component({ @@ -79,7 +87,7 @@ class DetailComponent { - + @@ -121,8 +129,11 @@ class SecondComponent implements OnInit, OnDestroy { template: `` }) export class PageRouterOutletNestedAppComponent { - static routes = [ - { path: "", component: FirstComponent }, + static routes: Route[] = [ + { path: "", redirectTo: "/second/1/detail/3", pathMatch: "full" }, + { + path: "first", component: FirstComponent, + }, { path: "second/:depth", component: SecondComponent, children: [ diff --git a/ng-sample/app/examples/router/router-outlet-test.ts b/ng-sample/app/examples/router/router-outlet-test.ts index fd9803c93..76fa8a8c0 100644 --- a/ng-sample/app/examples/router/router-outlet-test.ts +++ b/ng-sample/app/examples/router/router-outlet-test.ts @@ -49,14 +49,14 @@ class SecondComponent implements OnInit, OnDestroy { template: ` - - + + - + ` diff --git a/ng-sample/app/examples/router/styles.css b/ng-sample/app/examples/router/styles.css index f613be9a5..0182ca37a 100644 --- a/ng-sample/app/examples/router/styles.css +++ b/ng-sample/app/examples/router/styles.css @@ -5,8 +5,8 @@ } .subtitle { - font-size: 20; - horizontal-align: center; + font-size: 20; + horizontal-align: center; margin: 10; } @@ -53,17 +53,21 @@ button { horizontal-align: center; } -.odd { - background-color: white; +.odd { + background-color: white; margin: 2; } -.even { - background-color: whitesmoke; - margin: 2; +.even { + background-color: whitesmoke; + margin: 2; } .stretch { horizontal-align: stretch; margin: 0 10; -} \ No newline at end of file +} + +.rlaActive { + color:blue; +} diff --git a/ng-sample/app/examples/tab-view/tab-view-test.html b/ng-sample/app/examples/tab-view/tab-view-test.html index f66474024..5caed39c8 100644 --- a/ng-sample/app/examples/tab-view/tab-view-test.html +++ b/ng-sample/app/examples/tab-view/tab-view-test.html @@ -1,9 +1,9 @@ - + diff --git a/ng-sample/package.json b/ng-sample/package.json index 1d07d71b5..6009d22fd 100644 --- a/ng-sample/package.json +++ b/ng-sample/package.json @@ -32,15 +32,15 @@ }, "homepage": "https://github.com/NativeScript/template-hello-world", "dependencies": { - "@angular/animations": "~4.2.4", - "@angular/common": "~4.2.4", - "@angular/compiler": "~4.2.4", - "@angular/core": "~4.2.4", - "@angular/forms": "~4.2.4", - "@angular/http": "~4.2.4", - "@angular/platform-browser": "~4.2.4", - "@angular/platform-browser-dynamic": "~4.2.4", - "@angular/router": "~4.2.4", + "@angular/animations": "~4.4.1", + "@angular/common": "~4.4.1", + "@angular/compiler": "~4.4.1", + "@angular/core": "~4.4.1", + "@angular/forms": "~4.4.1", + "@angular/http": "~4.4.1", + "@angular/platform-browser": "~4.4.1", + "@angular/platform-browser-dynamic": "~4.4.1", + "@angular/router": "~4.4.1", "nativescript-angular": "file:../nativescript-angular", "rxjs": "^5.3.0", "tns-core-modules": "next", diff --git a/tests/.gitignore b/tests/.gitignore index 9b0e2ef3f..df14611cc 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -6,6 +6,10 @@ typings .tscache app/**/*.js +e2e/**/*.js app/**/*.map +e2e/**/*.map +e2e/reports +test-results.xml platforms lib diff --git a/tests/app/app.component.ts b/tests/app/app.component.ts index 55dc77724..e997d6eb0 100644 --- a/tests/app/app.component.ts +++ b/tests/app/app.component.ts @@ -1,6 +1,6 @@ -import {Component} from "@angular/core"; -import {SinglePageMain} from "./single-page-main.component"; -import {MultiPageMain} from "./multi-page-main.component"; +import { Component } from "@angular/core"; +import { SinglePageMain } from "./single-page-main.component"; +import { MultiPageMain } from "./multi-page-main.component"; @Component({ selector: "my-app", diff --git a/tests/app/tests/http-client-ns-backend.ts b/tests/app/tests/http-client-ns-backend.ts new file mode 100644 index 000000000..e5baf8d2c --- /dev/null +++ b/tests/app/tests/http-client-ns-backend.ts @@ -0,0 +1,80 @@ +// make sure you import mocha-config before @angular/core +import { assert } from "./test-config"; +import { NSFileSystem } from "nativescript-angular/file-system/ns-file-system"; +import { NsHttpBackEnd } from "nativescript-angular/http-client"; + +import { XhrFactory, HttpRequest, HttpResponse, HttpErrorResponse } from "@angular/common/http"; +import { File } from "tns-core-modules/file-system"; + +class NSFileSystemMock implements NSFileSystem { + public currentApp(): any { + return { path: "/app/dir" }; + } + + public fileFromPath(path: string): any { + if (path === "/app/dir/data.json") { + return { + readText: () => { return Promise.resolve(` { "result": "success" } `); } + }; + } + throw new Error("Opening non-existing file"); + } + + public fileExists(path: string): boolean { + return path === "/app/dir/data.json"; + } +} +class XhrFactoryMock implements XhrFactory { + build(): XMLHttpRequest { + throw new Error("Hi, from XhrFactoryMock!"); + } +} + +describe("NsHttpBackEnd ", () => { + let backend: NsHttpBackEnd; + + before(() => { + backend = new NsHttpBackEnd(new XhrFactoryMock(), new NSFileSystemMock()); + }); + + it("should work with local files prefixed with '~'", (done) => { + const req = new HttpRequest("GET", "~/data.json"); + let nextCalled = false; + backend.handle(req).subscribe( + (response: HttpResponse<{ result: string }>) => { + assert.equal(response.body.result, "success"); + nextCalled = true; + }, (error) => { + done(error); + }, () => { + assert.isTrue(nextCalled, "next callback should be called with result."); + done(); + }); + }); + + it("should return 404 for non-existing local files prefixed with '~'", (done) => { + const req = new HttpRequest("GET", "~/non/existing/file.json"); + backend.handle(req).subscribe( + (response) => { + assert.fail("next callback should not be called for non existing file."); + }, (error: HttpErrorResponse) => { + assert.equal(error.status, 404); + done(); + }, () => { + assert.fail("next callback should not be called for non existing file."); + }); + }); + + it("should fallback to XHR backend when requesting remote files", (done) => { + const req = new HttpRequest("GET", "https://nativescript.org/"); + backend.handle(req).subscribe( + (response) => { + assert.fail("next callback should not be called for non existing file."); + }, (error: Error) => { + assert.equal(error.message, "Hi, from XhrFactoryMock!"); + done(); + }, () => { + assert.fail("next callback should not be called for non existing file."); + }); + }); +}); diff --git a/tests/app/tests/http.ts b/tests/app/tests/http.ts index b9456f95f..e02af2ad0 100644 --- a/tests/app/tests/http.ts +++ b/tests/app/tests/http.ts @@ -1,16 +1,39 @@ // make sure you import mocha-config before @angular/core -import {assert} from "./test-config"; +import { assert } from "./test-config"; import { async, inject, } from "@angular/core/testing"; -import {ReflectiveInjector} from "@angular/core"; -import {BaseRequestOptions, ConnectionBackend, Http, Response, ResponseOptions} from "@angular/http"; +import { ReflectiveInjector, Injectable } from "@angular/core"; +import { Request, BaseRequestOptions, ConnectionBackend, Http, Response, ResponseOptions } from "@angular/http"; import "rxjs/add/operator/map"; -import {MockBackend} from "@angular/http/testing"; -import {NSHttp} from "nativescript-angular/http/ns-http"; -import {NSFileSystem} from "nativescript-angular/file-system/ns-file-system"; -import {NSFileSystemMock, FileResponses} from "./mocks/ns-file-system.mock"; +import { MockBackend } from "@angular/http/testing"; +import { NSHttp } from "nativescript-angular/http/ns-http"; +import { NSFileSystem } from "nativescript-angular/file-system/ns-file-system"; + +const AWESOME_TEAM: string = '[{"name":"Alex"}, {"name":"Rosen"}, {"name":"Panayot"}]'; + +// Filesystem mock +@Injectable() +export class NSFileSystemMock implements NSFileSystem { + public currentApp(): any { + return { path: "/app/dir" }; + } + + public fileFromPath(path: string): any { + if (path === "/app/dir/test.json") { + return { + readText: () => { return Promise.resolve(AWESOME_TEAM); } + }; + } + throw new Error("Opening non-existing file"); + } + + public fileExists(path: string): boolean { + return path === "/app/dir/test.json"; + } + +} describe("Http", () => { let http: Http; @@ -22,15 +45,15 @@ describe("Http", () => { MockBackend, { provide: NSFileSystem, useClass: NSFileSystemMock }, { - provide: Http, - useFactory: function ( - connectionBackend: ConnectionBackend, - defaultOptions: BaseRequestOptions, - nsFileSystem: NSFileSystem) { + provide: Http, + useFactory: function ( + connectionBackend: ConnectionBackend, + defaultOptions: BaseRequestOptions, + nsFileSystem: NSFileSystem) { // HACK: cast backend to any to work around an angular typings problem return new NSHttp(connectionBackend, defaultOptions, nsFileSystem); - }, - deps: [MockBackend, BaseRequestOptions, NSFileSystem] + }, + deps: [MockBackend, BaseRequestOptions, NSFileSystem] } ]); @@ -45,6 +68,25 @@ describe("Http", () => { }); }); + it("request method should work with local files prefixed with '~'", () => { + http.request("~/test.json").map(res => res.json()).subscribe((response: any) => { + assert.strictEqual(3, response.length); + assert.strictEqual("Alex", response[0].name); + }); + }); + + it("request method using Request type should work with local files prefixed with '~'", () => { + const url = "~/test.json"; + const req = new Request({ + method: 'GET', + url + }); + http.request(req).map(res => res.json()).subscribe((response: any) => { + assert.strictEqual(3, response.length); + assert.strictEqual("Alex", response[0].name); + }); + }); + it("should work with local files prefixed with '/'", () => { http.get("/test.json").map(res => res.json()).subscribe((response: any) => { assert.strictEqual(3, response.length); @@ -59,6 +101,6 @@ describe("Http", () => { assert.strictEqual(3, response.length); assert.strictEqual("Rosen", response[1].name); }); - connection.mockRespond(new Response(new ResponseOptions({ body: FileResponses.AWESOME_TEAM }))); + connection.mockRespond(new Response(new ResponseOptions({ body: AWESOME_TEAM }))); }); }); diff --git a/tests/app/tests/mocks/ns-file-system.mock.ts b/tests/app/tests/mocks/ns-file-system.mock.ts deleted file mode 100644 index 0f841aff5..000000000 --- a/tests/app/tests/mocks/ns-file-system.mock.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {Injectable} from "@angular/core"; -import {ResponseType, Response, ResponseOptions} from "@angular/http"; - -export class FileResponses { - public static AWESOME_TEAM: string = '[{"name":"Alex"}, {"name":"Rosen"}, {"name":"Panayot"}]'; -} - -// Folder mock -class Folder { - public getFile(url: string): any { - let data; - switch (url) { - case "test.json": - data = FileResponses.AWESOME_TEAM; - break; - default: - throw (new Error("Unsupported file for the testing mock - ns-file-system-mock")); - } - return { - readText: () => { - return new Promise((resolve) => { - resolve(data); - }); - } - }; - } -} - -// Filesystem mock -@Injectable() -export class NSFileSystemMock { - public currentApp(): Folder { - return new Folder(); - } -} - diff --git a/tests/app/tests/ns-location-strategy.ts b/tests/app/tests/ns-location-strategy.ts index 3a89ff541..13fd898a9 100644 --- a/tests/app/tests/ns-location-strategy.ts +++ b/tests/app/tests/ns-location-strategy.ts @@ -19,7 +19,7 @@ class FakeFrame extends View implements Frame { } } - navigate(entry: NavigationEntry) { } + navigate(entry: any) { } constructor(private backCB?: () => void) { super(); diff --git a/tests/app/tests/property-sets.ts b/tests/app/tests/property-sets.ts index e62bb8c74..3b5c5253e 100644 --- a/tests/app/tests/property-sets.ts +++ b/tests/app/tests/property-sets.ts @@ -11,11 +11,14 @@ import {Red} from "color/known-colors"; import {device, platformNames} from "platform"; import {createDevice} from "./test-utils"; -class TestView extends View implements ViewExtensions { - public nodeName: string = "TestView"; +class TestView extends View implements NgView { + public meta: ViewClassMeta = { skipAddToDom: false }; public nodeType: number = 1; + public nodeName: string = "TestView"; public templateParent: NgView = null; - public meta: ViewClassMeta = { skipAddToDom: false }; + public nextSibling: NgView; + public firstChild: NgView; + public lastChild: NgView; public ngCssClasses: Map = new Map(); public stringValue: string = ""; @@ -43,10 +46,10 @@ describe("setting View properties", () => { it("doesn\'t convert number values", () => { let view = new TestView(); viewUtil.setProperty(view, "numValue", "42"); - assert.strictEqual("42", view.numValue); + assert.strictEqual(42, view.numValue); viewUtil.setProperty(view, "numValue", "42."); - assert.strictEqual("42.", view.numValue); + assert.strictEqual(42., view.numValue); viewUtil.setProperty(view, "numValue", 0); assert.strictEqual(0, view.numValue); @@ -55,9 +58,9 @@ describe("setting View properties", () => { it("doesn\'t convert boolean values", () => { let view = new TestView(); viewUtil.setProperty(view, "boolValue", "true"); - assert.strictEqual("true", view.boolValue); + assert.strictEqual(true, view.boolValue); viewUtil.setProperty(view, "boolValue", "false"); - assert.strictEqual("false", view.boolValue); + assert.strictEqual(false, view.boolValue); }); it("sets style values", () => { diff --git a/tests/app/tests/renderer-tests.ts b/tests/app/tests/renderer-tests.ts index 6efc463c8..3c2ccd506 100644 --- a/tests/app/tests/renderer-tests.ts +++ b/tests/app/tests/renderer-tests.ts @@ -291,7 +291,7 @@ describe("Renderer E2E", () => { it("ngIf hides component when false", () => { return testApp.loadComponent(NgIfLabel).then((componentRef) => { const componentRoot = componentRef.instance.elementRef.nativeElement; - assert.equal("(ProxyViewContainer (CommentNode))", dumpView(componentRoot)); + assert.equal("(ProxyViewContainer)", dumpView(componentRoot)); }); }); @@ -302,7 +302,7 @@ describe("Renderer E2E", () => { component.show = true; testApp.appRef.tick(); - assert.equal("(ProxyViewContainer (CommentNode), (Label))", dumpView(componentRoot)); + assert.equal("(ProxyViewContainer (Label))", dumpView(componentRoot)); }); }); @@ -314,12 +314,11 @@ describe("Renderer E2E", () => { component.show = true; testApp.appRef.tick(); assert.equal( - "(ProxyViewContainer (StackLayout (CommentNode), (Label), (Button)))", + "(ProxyViewContainer (StackLayout (Label), (Button)))", dumpView(componentRoot)); }); }); - it("ngIf shows elements in correct order when multiple are rendered and there's *ngIf", () => { return testApp.loadComponent(NgIfMultiple).then((componentRef) => { const component = componentRef.instance; @@ -333,8 +332,7 @@ describe("Renderer E2E", () => { "(Label[text=1]), " + "(Label[text=2]), " + "(Label[text=3]), " + - "(CommentNode), " + // ng-reflect comment - "(Label[text=4]), " + // the content to be displayed and its anchor + "(Label[text=4]), " + // the content to be conditionally displayed "(Label[text=5])" + ")" + ")", @@ -348,13 +346,15 @@ describe("Renderer E2E", () => { const componentRoot = component.elementRef.nativeElement; testApp.appRef.tick(); + assert.equal( "(ProxyViewContainer " + "(StackLayout " + - "(CommentNode), " + // ng-reflect comment - "(Label[text=If]), (CommentNode)))", // the content to be displayed and its anchor + "(Label[text=If])" + + ")" + + ")", - dumpView(componentRoot, true)); + dumpView(componentRoot, true)); }); }); @@ -368,10 +368,11 @@ describe("Renderer E2E", () => { assert.equal( "(ProxyViewContainer " + "(StackLayout " + - "(CommentNode), " + // ng-reflect comment - "(Label[text=Else]), (CommentNode)))", // the content to be displayed and its anchor + "(Label[text=Else])" + + ")" + + ")", - dumpView(componentRoot, true)); + dumpView(componentRoot, true)); }); }); @@ -384,11 +385,11 @@ describe("Renderer E2E", () => { assert.equal( "(ProxyViewContainer " + "(StackLayout " + - "(CommentNode), " + // ng-reflect comment - "(Label[text=Then]), (CommentNode), " + // the content to be displayed and its anchor - "(CommentNode)))", // the anchor for the else template + "(Label[text=Then])" + + ")" + + ")", - dumpView(componentRoot, true)); + dumpView(componentRoot, true)); }); }); @@ -403,11 +404,11 @@ describe("Renderer E2E", () => { assert.equal( "(ProxyViewContainer " + "(StackLayout " + - "(CommentNode), " + // the content to be displayed - "(Label[text=Else]), (CommentNode), " + // the content to be displayed - "(CommentNode)))", // the content to be displayed + "(Label[text=Else])" + + ")" + + ")", - dumpView(componentRoot, true)); + dumpView(componentRoot, true)); }); }); @@ -415,7 +416,7 @@ describe("Renderer E2E", () => { return testApp.loadComponent(NgForLabel).then((componentRef) => { const componentRoot = componentRef.instance.elementRef.nativeElement; assert.equal( - "(ProxyViewContainer (CommentNode), (Label[text=one]), (Label[text=two]), (Label[text=three]))", + "(ProxyViewContainer (Label[text=one]), (Label[text=two]), (Label[text=three]))", dumpView(componentRoot, true)); }); }); @@ -429,7 +430,7 @@ describe("Renderer E2E", () => { testApp.appRef.tick(); assert.equal( - "(ProxyViewContainer (CommentNode), (Label[text=one]), (Label[text=three]))", + "(ProxyViewContainer (Label[text=one]), (Label[text=three]))", dumpView(componentRoot, true)); }); }); @@ -443,7 +444,7 @@ describe("Renderer E2E", () => { testApp.appRef.tick(); assert.equal( - "(ProxyViewContainer (CommentNode), " + + "(ProxyViewContainer " + "(Label[text=one]), (Label[text=new]), (Label[text=two]), (Label[text=three]))", dumpView(componentRoot, true)); }); diff --git a/tests/app/tests/value-accessor-tests.ts b/tests/app/tests/value-accessor-tests.ts index c1d6c53fc..57b7173ed 100644 --- a/tests/app/tests/value-accessor-tests.ts +++ b/tests/app/tests/value-accessor-tests.ts @@ -1,22 +1,24 @@ // make sure you import mocha-config before @angular/core -import {assert} from "./test-config"; -import {View} from "ui/core/view"; -import {Slider} from "ui/slider"; -import {Switch} from "ui/switch"; -import {DatePicker} from "ui/date-picker"; -import {TimePicker} from "ui/time-picker"; -import {ListPicker} from "ui/list-picker"; -import {TextField} from "ui/text-field"; -import {NumberValueAccessor} from "nativescript-angular/value-accessors/number-value-accessor"; -import {CheckedValueAccessor} from "nativescript-angular/value-accessors/checked-value-accessor"; -import {DateValueAccessor} from "nativescript-angular/value-accessors/date-value-accessor"; -import {TimeValueAccessor} from "nativescript-angular/value-accessors/time-value-accessor"; -import {SelectedIndexValueAccessor} from "nativescript-angular/value-accessors/selectedIndex-value-accessor"; -import {TextValueAccessor} from "nativescript-angular/value-accessors/text-value-accessor"; -import {ElementRef} from "@angular/core"; +import { assert } from "./test-config"; +import { View } from "ui/core/view"; +import { Slider } from "ui/slider"; +import { Switch } from "ui/switch"; +import { DatePicker } from "ui/date-picker"; +import { TimePicker } from "ui/time-picker"; +import { ListPicker } from "ui/list-picker"; +import { TextField } from "ui/text-field"; +import { + NumberValueAccessor, + CheckedValueAccessor, + DateValueAccessor, + TimeValueAccessor, + SelectedIndexValueAccessor, + TextValueAccessor +} from "nativescript-angular/forms/value-accessors"; +import { ElementRef } from "@angular/core"; class TestElementRef implements ElementRef { - constructor(public nativeElement: View) {}; + constructor(public nativeElement: View) { }; } class TestNumberValueAccessor extends NumberValueAccessor { @@ -138,7 +140,7 @@ describe("two-way binding via ng-model", () => { accessor.writeValue("blah"); assert.equal("blah", accessor.view.text); - accessor.writeValue({toString: () => "stringified"}); + accessor.writeValue({ toString: () => "stringified" }); assert.equal("stringified", accessor.view.text); }); }); diff --git a/tests/app/tests/xhr-paths.ts b/tests/app/tests/xhr-paths.ts index 9a4927515..c3de6a6dc 100644 --- a/tests/app/tests/xhr-paths.ts +++ b/tests/app/tests/xhr-paths.ts @@ -1,22 +1,72 @@ // make sure you import mocha-config before @angular/core -import {assert} from "./test-config"; -import {FileSystemResourceLoader} from "nativescript-angular/resource-loader"; +import { assert } from "./test-config"; +import { FileSystemResourceLoader } from "nativescript-angular/resource-loader"; + +import { File } from "tns-core-modules/file-system"; +import { NSFileSystem } from "nativescript-angular/file-system/ns-file-system"; + +class NSFileSystemMock { + public currentApp(): any { + return { path: "/app/dir" }; + } + + public fileFromPath(path: string): File { + return null; + } + + public fileExists(path: string): boolean { + // mycomponent.html always exists + // mycomponent.css is the other file + return path.indexOf("mycomponent.html") >= 0 || path === "/app/dir/mycomponent.css"; + } +} +const fsMock = new NSFileSystemMock(); describe("XHR name resolution", () => { + let resourceLoader: FileSystemResourceLoader; + before(() => { + resourceLoader = new FileSystemResourceLoader(new NSFileSystemMock()); + }); + it("resolves relative paths from app root", () => { - const xhr = new FileSystemResourceLoader(); - assert.strictEqual("/app/dir/mydir/mycomponent.html", xhr.resolve("mydir/mycomponent.html", "/app/dir")); + assert.strictEqual("/app/dir/mydir/mycomponent.html", resourceLoader.resolve("mydir/mycomponent.html")); }); it("resolves double-slashed absolute paths as is", () => { - const xhr = new FileSystemResourceLoader(); - assert.strictEqual("//app/mydir/mycomponent.html", xhr.resolve("//app/mydir/mycomponent.html", "/app/dir")); + assert.strictEqual("//app/mydir/mycomponent.html", resourceLoader.resolve("//app/mydir/mycomponent.html")); }); it("resolves single-slashed absolute paths as is", () => { - const xhr = new FileSystemResourceLoader(); assert.strictEqual( "/data/data/app/mydir/mycomponent.html", - xhr.resolve("/data/data/app/mydir/mycomponent.html", "/app/dir")); + resourceLoader.resolve("/data/data/app/mydir/mycomponent.html")); + }); + + it("resolves existing CSS file", () => { + assert.strictEqual( + "/app/dir/mycomponent.css", + resourceLoader.resolve("mycomponent.css")); + }); + + it("resolves non-existing .scss file to existing .css file", () => { + assert.strictEqual( + "/app/dir/mycomponent.css", + resourceLoader.resolve("mycomponent.scss")); + }); + + it("resolves non-existing .sass file to existing .css file", () => { + assert.strictEqual( + "/app/dir/mycomponent.css", + resourceLoader.resolve("mycomponent.sass")); + }); + + it("resolves non-existing .less file to existing .css file", () => { + assert.strictEqual( + "/app/dir/mycomponent.css", + resourceLoader.resolve("mycomponent.less")); + }); + + it("throws for non-existing file that has no fallbacks", () => { + assert.throws(() => resourceLoader.resolve("does-not-exist.css")); }); }); diff --git a/tests/e2e-tests/lazy-load-routing.js b/tests/e2e-tests/lazy-load-routing.js deleted file mode 100644 index 044103081..000000000 --- a/tests/e2e-tests/lazy-load-routing.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -var nsAppium = require("nativescript-dev-appium"); - -describe("lazy load routing", function () { - this.timeout(360000); - var driver; - - before(function () { - driver = nsAppium.createDriver(); - }); - - after(function () { - return driver - .quit() - .finally(function () { - console.log("Driver quit successfully"); - }); - }); - - it("loads default path", function () { - return driver - .waitForElementByAccessibilityId("first-lazy-load", 300000) - .elementByAccessibilityId("first-lazy-load") - .should.eventually.exist - .text().should.eventually.equal("First: lazy-load") - }); - - it("navigates and returns", function () { - var expectedHookLog = [ - "first.init", // <-- load - "second.init", // <-- forward (first component is not destroyed) - "second.destroy"] // <-- back (first component is reused) - .join(","); - return driver - .waitForElementByAccessibilityId("first-navigate-lazy-load", 300000) - .elementByAccessibilityId("first-navigate-lazy-load") - .should.eventually.exist - .tap() - .elementByAccessibilityId("second-lazy-load") - .should.eventually.exist - .text().should.eventually.equal("Second: lazy-load") - .elementByAccessibilityId("router-location-strategy-states-lazy-load") - .text().should.eventually.equal("/first/lazy-load,/second/lazy-load") - .elementByAccessibilityId("second-navigate-back-lazy-load") - .should.eventually.exist - .tap() - .elementByAccessibilityId("first-lazy-load") - .should.eventually.exist - .text().should.eventually.equal("First: lazy-load") - .elementByAccessibilityId("hooks-log-lazy-load") - .text().should.eventually.equal(expectedHookLog) - }); - - it("navigates and clear history", function() { - return driver - .waitForElementByAccessibilityId("first-navigate-lazy-load", 300000) - .elementByAccessibilityId("first-navigate-clear-history-lazy-load") - .should.eventually.exist - .tap() - .elementByAccessibilityId("second-lazy-load") - .should.eventually.exist - .text().should.eventually.equal("Second: lazy-load") - .elementByAccessibilityId("router-location-strategy-states-lazy-load") - .text().should.eventually.equal("/second/lazy-load") - }); -}); diff --git a/tests/e2e-tests/multi-page-routing.js b/tests/e2e-tests/multi-page-routing.js deleted file mode 100644 index 161d58c05..000000000 --- a/tests/e2e-tests/multi-page-routing.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict"; -var nsAppium = require("nativescript-dev-appium"); - -describe("multi page routing", function () { - this.timeout(360000); - var driver; - - before(function () { - driver = nsAppium.createDriver(); - }); - - after(function () { - return driver - .quit() - .finally(function () { - console.log("Driver quit successfully"); - }); - }); - - it("loads default path", function () { - return driver - .waitForElementByAccessibilityId("first-multi-page", 300000) - .elementByAccessibilityId("first-multi-page") - .should.eventually.exist - .text().should.eventually.equal("First: multi-page") - }); - - it("navigates and returns", function () { - var expectedHookLog = [ - "first.init", // <-- load - "second.init", // <-- forward (first component is not destroyed) - "second.destroy"] // <-- back (first component is reused) - .join(","); - return driver - .waitForElementByAccessibilityId("first-navigate-multi-page", 300000) - .elementByAccessibilityId("first-navigate-multi-page") - .should.eventually.exist - .tap() - .elementByAccessibilityId("second-multi-page") - .should.eventually.exist - .text().should.eventually.equal("Second: multi-page") - .elementByAccessibilityId("second-navigate-back-multi-page") - .should.eventually.exist - .tap() - .elementByAccessibilityId("first-multi-page") - .should.eventually.exist - .text().should.eventually.equal("First: multi-page") - .elementByAccessibilityId("hooks-log-multi-page") - .text().should.eventually.equal(expectedHookLog) - }); -}); diff --git a/tests/e2e-tests/single-page-routing.js b/tests/e2e-tests/single-page-routing.js deleted file mode 100644 index c839953da..000000000 --- a/tests/e2e-tests/single-page-routing.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; -var nsAppium = require("nativescript-dev-appium"); - -describe("single page routing", function () { - this.timeout(360000); - var driver; - - before(function () { - driver = nsAppium.createDriver(); - }); - - after(function () { - return driver - .quit() - .finally(function () { - console.log("Driver quit successfully"); - }); - }); - - it("loads default path", function () { - return driver - .waitForElementByAccessibilityId("first-single-page", 300000) - .elementByAccessibilityId("first-single-page") - .should.eventually.exist - .text().should.eventually.equal("First: single-page") - }); - - it("navigates and returns", function () { - var expectedHookLog = [ - "first.init", // <--load - "first.destroy", // <--forward - "second.init", - "second.destroy", // <--back - "first.init"].join(","); - - return driver - .waitForElementByAccessibilityId("first-single-page", 300000) - .elementByAccessibilityId("first-navigate-single-page") - .should.eventually.exist - .tap() - .elementByAccessibilityId("second-single-page") - .should.eventually.exist - .text().should.eventually.equal("Second: single-page") - .elementByAccessibilityId("second-navigate-back-single-page") - .should.eventually.exist - .tap() - .elementByAccessibilityId("first-single-page") - .should.eventually.exist - .text().should.eventually.equal("First: single-page") - .elementByAccessibilityId("hooks-log-single-page") - .text().should.eventually.equal(expectedHookLog) - }); -}); diff --git a/tests/e2e/config/appium.capabilities.json b/tests/e2e/config/appium.capabilities.json new file mode 100644 index 000000000..ee7f5a177 --- /dev/null +++ b/tests/e2e/config/appium.capabilities.json @@ -0,0 +1,101 @@ +{ + "travis-e2e-tests": { + "platformName": "Android", + "platformVersion": "4.4", + "deviceName": "test", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": true, + "fullReset": false, + "app": "" + }, + "android19": { + "platformName": "Android", + "platformVersion": "4.4", + "deviceName": "Emulator-Api19-Default", + "avd": "Emulator-Api19-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": true, + "fullReset": false, + "app": "" + }, + "android21": { + "platformName": "Android", + "platformVersion": "5.0", + "deviceName": "Emulator-Api21-Default", + "avd": "Emulator-Api21-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": true, + "fullReset": false, + "app": "" + }, + "android23": { + "platformName": "Android", + "platformVersion": "6.0", + "deviceName": "Emulator-Api23-Default", + "avd": "Emulator-Api23-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": true, + "fullReset": false, + "app": "" + }, + "android24": { + "platformName": "Android", + "platformVersion": "7.0", + "deviceName": "Emulator-Api24-Default", + "avd": "Emulator-Api24-Default", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": true, + "fullReset": false, + "app": "" + }, + "android25": { + "platformName": "Android", + "platformVersion": "7.1", + "deviceName": "Emulator-Api25-Google", + "avd": "Emulator-Api25-Google", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": true, + "fullReset": false, + "app": "" + }, + "android26": { + "platformName": "Android", + "platformVersion": "8.0", + "deviceName": "Emulator-Api26-Google", + "avd": "Emulator-Api26-Google", + "lt": 60000, + "appActivity": "com.tns.NativeScriptActivity", + "newCommandTimeout": 720, + "noReset": true, + "fullReset": false, + "app": "" + }, + "sim.iPhone7.iOS100": { + "platformName": "iOS", + "platformVersion": "10.0", + "deviceName": "iPhone 7 100", + "noReset": true, + "fullReset": false, + "app": "" + }, + "sim.iPhone7.iOS110": { + "platformName": "iOS", + "platformVersion": "11.0", + "deviceName": "iPhone 7 110", + "noReset": true, + "fullReset": false, + "app": "" + } +} diff --git a/tests/e2e/config/mocha.opts b/tests/e2e/config/mocha.opts new file mode 100644 index 000000000..ea635b0dc --- /dev/null +++ b/tests/e2e/config/mocha.opts @@ -0,0 +1,4 @@ +--timeout 140000 +--recursive e2e +--reporter mocha-multi +--reporter-options spec=-,mocha-junit-reporter=test-results.xml \ No newline at end of file diff --git a/tests/e2e/const.js b/tests/e2e/const.js new file mode 100644 index 000000000..9978b5dac --- /dev/null +++ b/tests/e2e/const.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.initialDisplayName = "initialdisplay"; +//# sourceMappingURL=const.js.map \ No newline at end of file diff --git a/tests/e2e/helpers/image-helper.ts b/tests/e2e/helpers/image-helper.ts new file mode 100644 index 000000000..ff82fb024 --- /dev/null +++ b/tests/e2e/helpers/image-helper.ts @@ -0,0 +1,22 @@ +import { AppiumDriver } from "nativescript-dev-appium"; +import { assert } from "chai"; + +export class ImageHelper { + private _imagesResults = new Map(); + + constructor(private _driver: AppiumDriver) { } + + public async compareScreen(imageName, retryCounts = 1, tollerance = 0.01) { + this._imagesResults.set(imageName, await this._driver.compareScreen(imageName, retryCounts, tollerance)); + } + + public assertImages() { + for (let key in this._imagesResults) { + assert.isTrue(this._imagesResults.get(key), `Image is not correct ${key}`); + } + } + + public reset(){ + this._imagesResults.clear(); + } +} \ No newline at end of file diff --git a/tests/e2e/lazy-load-routing.ts b/tests/e2e/lazy-load-routing.ts new file mode 100644 index 000000000..5348e6879 --- /dev/null +++ b/tests/e2e/lazy-load-routing.ts @@ -0,0 +1,40 @@ +import { AppiumDriver, createDriver } from "nativescript-dev-appium"; +import { assert } from "chai"; +import { ImageHelper } from "./helpers/image-helper" + +describe("lazy load routing", async function () { + let driver: AppiumDriver; + let imageHelper: ImageHelper; + const lazyLoadedDisplay = "lazyLoadedDisplay"; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + imageHelper = new ImageHelper(driver); + }); + + afterEach("clear image results", () => { + imageHelper.reset(); + }); + + it("navigates and returns", async () => { + await (await driver.findElementByAccessibilityId("first-navigate-lazy-load")).tap(); + imageHelper.compareScreen("first-navigate-lazy-load-screen"); + + const btn = await driver.findElementByAccessibilityId("second-navigate-back-lazy-load"); + btn.tap(); + imageHelper.compareScreen("second-navigate-back-lazy-load-screen"); + + imageHelper.assertImages(); + }); + + it("navigates and clear history", async () => { + await (await driver.findElementByAccessibilityId("first-navigate-clear-history-lazy-load")).tap(); + imageHelper.compareScreen("first-navigate-clear-history-lazy-load-screen"); + + await driver.navBack(); + imageHelper.compareScreen("first-navigate-clear-history-lazy-load-nav-back-sceen"); + + imageHelper.assertImages(); + }); +}); diff --git a/tests/e2e/multi-page-routing.ts b/tests/e2e/multi-page-routing.ts new file mode 100644 index 000000000..db4a86719 --- /dev/null +++ b/tests/e2e/multi-page-routing.ts @@ -0,0 +1,29 @@ +import { AppiumDriver, createDriver } from "nativescript-dev-appium"; +import { assert } from "chai"; +import { ImageHelper } from "./helpers/image-helper"; + +describe("multi page routing", async () => { + let driver: AppiumDriver; + let imageHelper: ImageHelper; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + imageHelper = new ImageHelper(driver); + }); + + afterEach(()=>{ + imageHelper.reset(); + }) + + it("navigates and returns", async () => { + let btn = await driver.findElementByAccessibilityId("first-navigate-multi-page"); + await btn.tap(); + await imageHelper.compareScreen("first-navigate-multi-page-screen", 3); + + btn = await driver.findElementByAccessibilityId("second-navigate-back-multi-page"); + await btn.tap(); + await imageHelper.compareScreen("second-navigate-back-multi-page-screen", 1, 0.01); + imageHelper.assertImages(); + }); +}); diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-clear-history-lazy-load-nav-back-sceen.png b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-clear-history-lazy-load-nav-back-sceen.png new file mode 100644 index 000000000..4206d382b Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-clear-history-lazy-load-nav-back-sceen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-clear-history-lazy-load-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-clear-history-lazy-load-screen.png new file mode 100644 index 000000000..de5884ed5 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-clear-history-lazy-load-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-clear-history-single-page-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-clear-history-single-page-screen.png new file mode 100644 index 000000000..a0d4d1483 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-clear-history-single-page-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-lazy-load-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-lazy-load-screen.png new file mode 100644 index 000000000..2a469d51f Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-lazy-load-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-multi-page-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-multi-page-screen.png new file mode 100644 index 000000000..363719c75 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-navigate-multi-page-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-single-page-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-single-page-screen.png new file mode 100644 index 000000000..e37fdef33 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/first-single-page-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/second-navigate-back-lazy-load-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/second-navigate-back-lazy-load-screen.png new file mode 100644 index 000000000..1be280d92 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/second-navigate-back-lazy-load-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/second-navigate-back-multi-page-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/second-navigate-back-multi-page-screen.png new file mode 100644 index 000000000..88d27dfe7 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/second-navigate-back-multi-page-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/second-navigate-back-single-page-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/second-navigate-back-single-page-screen.png new file mode 100644 index 000000000..4d1f3be8b Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api19-Default/second-navigate-back-single-page-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-lazy-load-nav-back-sceen.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-lazy-load-nav-back-sceen.png new file mode 100755 index 000000000..7c8653c11 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-lazy-load-nav-back-sceen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-lazy-load-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-lazy-load-screen.png new file mode 100755 index 000000000..7a3014a67 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-lazy-load-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-single-page-screen_actual.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-single-page-screen_actual.png new file mode 100755 index 000000000..c76add36a Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-single-page-screen_actual.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-single-page.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-single-page.png new file mode 100755 index 000000000..e484c2823 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-clear-history-single-page.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-lazy-load-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-lazy-load-screen.png new file mode 100755 index 000000000..c08ba640d Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-lazy-load-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-multi-page-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-multi-page-screen.png new file mode 100755 index 000000000..c96828824 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-navigate-multi-page-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-single-page-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-single-page-screen.png new file mode 100755 index 000000000..191481588 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/first-single-page-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/second-navigate-back-lazy-load-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/second-navigate-back-lazy-load-screen.png new file mode 100755 index 000000000..9e40c0c96 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/second-navigate-back-lazy-load-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/second-navigate-back-multi-page-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/second-navigate-back-multi-page-screen.png new file mode 100755 index 000000000..84d8b7eeb Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/second-navigate-back-multi-page-screen.png differ diff --git a/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/second-navigate-back-single-page-screen.png b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/second-navigate-back-single-page-screen.png new file mode 100755 index 000000000..c29c6e503 Binary files /dev/null and b/tests/e2e/resources/images/ngtests/Emulator-Api24-Default/second-navigate-back-single-page-screen.png differ diff --git a/tests/e2e/setup.ts b/tests/e2e/setup.ts new file mode 100644 index 000000000..64138b8d3 --- /dev/null +++ b/tests/e2e/setup.ts @@ -0,0 +1,20 @@ +import { startServer, stopServer, createDriver, AppiumDriver } from "nativescript-dev-appium"; + +let driver: AppiumDriver; +before("start server", async () => { + await startServer(); + driver = await createDriver(); +}); + +afterEach(async function () { + if (this.currentTest.state === "failed") { + const fullName = await this.currentTest.fullTitle(); + await driver.logScreenshot(fullName); + await driver.logPageSource(fullName); + } +}); + +after("stop server", async () => { + await driver.quit(); + await stopServer(); +}); diff --git a/tests/e2e/single-page-routing.ts b/tests/e2e/single-page-routing.ts new file mode 100644 index 000000000..dc0a75694 --- /dev/null +++ b/tests/e2e/single-page-routing.ts @@ -0,0 +1,42 @@ +import { AppiumDriver, createDriver } from "nativescript-dev-appium"; +import { assert } from "chai"; +import { ImageHelper } from "./helpers/image-helper"; + + +describe("single page routing", function () { + let driver: AppiumDriver; + let imageHelper: ImageHelper; + + before(async () => { + driver = await createDriver(); + await driver.resetApp(); + imageHelper = new ImageHelper(driver); + }); + + afterEach("clear image results", () => { + imageHelper.reset(); + }); + + it("navigates and returns", async () => { + let btn = (await driver.findElementByAccessibilityId("first-navigate-single-page")); + await btn.tap(); + await imageHelper.compareScreen("first-single-page-screen"); + + btn = await driver.findElementByAccessibilityId("second-navigate-back-single-page"); + await btn.tap(); + await imageHelper.compareScreen("second-navigate-back-single-page-screen"); + + imageHelper.assertImages(); + }); + + it("navigates and returns with clear history", async () => { + let btn = (await driver.findElementByAccessibilityId("first-navigate-clear-history-single-page")); + await btn.click(); + await imageHelper.compareScreen("first-navigate-clear-history-single-page-screen"); + + await driver.navBack(); + await imageHelper.compareScreen("second-navigate-back-single-page-screen"); + + imageHelper.assertImages(); + }); +}); diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json new file mode 100644 index 000000000..99f58b8e6 --- /dev/null +++ b/tests/e2e/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "importHelpers": true, + "sourceMap": true, + "types": [ + "node", + "mocha", + "chai" + ], + "lib": [ + "es6", + "dom" + ] + } +} \ No newline at end of file diff --git a/tests/hooks/before-livesync/nativescript-angular-sync.js b/tests/hooks/before-livesync/nativescript-angular-sync.js deleted file mode 100644 index 5a8510cb2..000000000 --- a/tests/hooks/before-livesync/nativescript-angular-sync.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("nativescript-angular/hooks/before-livesync"); diff --git a/tests/package.json b/tests/package.json index d23bf511b..9cfb0bb66 100644 --- a/tests/package.json +++ b/tests/package.json @@ -26,15 +26,15 @@ ], "homepage": "http://nativescript.org", "dependencies": { - "@angular/animations": "~4.2.4", - "@angular/common": "~4.2.4", - "@angular/compiler": "~4.2.4", - "@angular/core": "~4.2.4", - "@angular/forms": "~4.2.4", - "@angular/http": "~4.2.4", - "@angular/platform-browser": "~4.2.4", - "@angular/platform-browser-dynamic": "~4.2.4", - "@angular/router": "~4.2.4", + "@angular/animations": "~4.4.1", + "@angular/common": "~4.4.1", + "@angular/compiler": "~4.4.1", + "@angular/core": "~4.4.1", + "@angular/forms": "~4.4.1", + "@angular/http": "~4.4.1", + "@angular/platform-browser": "~4.4.1", + "@angular/platform-browser-dynamic": "~4.4.1", + "@angular/router": "~4.4.1", "nativescript-angular": "../nativescript-angular", "nativescript-unit-test-runner": "^0.3.4", "rxjs": "^5.2.0", @@ -42,6 +42,9 @@ "zone.js": "^0.8.2" }, "devDependencies": { + "@types/chai": "^4.0.2", + "@types/mocha": "^2.2.41", + "@types/node": "^7.0.5", "babel-traverse": "6.8.0", "babel-types": "6.8.1", "babylon": "6.8.0", @@ -55,16 +58,19 @@ "karma-mocha-reporter": "^1.2.0", "karma-nativescript-launcher": "^0.4.0", "lazy": "1.0.11", - "mocha": "^2.4.5", - "nativescript-dev-appium": "^0.4.0", + "mocha-junit-reporter": "^1.13.0", + "mocha-multi": "^0.11.0", + "nativescript-dev-appium": "next", "nativescript-dev-typescript": "^0.4.0", "socket.io": "1.4.8", "socket.io-client": "1.4.8", + "tslib": "^1.7.1", "tslint": "^4.5.1", - "typescript": "~2.2.0", - "wd": "^1.2.0" + "typescript": "~2.5.0", + "mocha": "~3.5.0" }, "scripts": { - "appium": "nativescript-dev-appium" + "e2e": "tsc -p e2e && mocha --opts ./e2e/config/mocha.opts", + "compile-tests-w": "tsc -p e2e --watch" } } diff --git a/tests/references.d.ts b/tests/references.d.ts new file mode 100644 index 000000000..b14f3837d --- /dev/null +++ b/tests/references.d.ts @@ -0,0 +1 @@ +/// Needed for autocompletion and compilation. \ No newline at end of file diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 7193bf4a1..abbc8d5f9 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -22,6 +22,7 @@ "exclude": [ "node_modules", "platforms", - "**/*.aot" + "**/*.aot", + "e2e" ] } \ No newline at end of file diff --git a/tests/typings/chai.d.ts b/tests/typings/chai.d.ts deleted file mode 100644 index 113158d83..000000000 --- a/tests/typings/chai.d.ts +++ /dev/null @@ -1,283 +0,0 @@ -// Type definitions for chai 1.7.2 -// Project: http://chaijs.com/ -// Definitions by: Jed Hunsaker , Bart van der Schoor -// Definitions: https://github.com/borisyankov/DefinitelyTyped - -declare module chai { - export class AssertionError { - constructor(message: string, _props?: any, ssf?: Function); - name: string; - message: string; - showDiff: boolean; - stack: string; - } - - function expect(target: any, message?: string): Expect; - - export var assert: Assert; - export var config: Config; - - export interface Config { - includeStack: boolean; - } - - // Provides a way to extend the internals of Chai - function use(fn: (chai: any, utils: any) => void): any; - - interface ExpectStatic { - (target: any): Expect; - } - - interface Assertions { - attr(name: string, value?: string): any; - css(name: string, value?: string): any; - data(name: string, value?: string): any; - class(className: string): any; - id(id: string): any; - html(html: string): any; - text(text: string): any; - value(value: string): any; - visible: any; - hidden: any; - selected: any; - checked: any; - disabled: any; - empty: any; - exist: any; - } - - interface Expect extends LanguageChains, NumericComparison, TypeComparison, Assertions { - not: Expect; - deep: Deep; - a: TypeComparison; - an: TypeComparison; - include: Include; - contain: Include; - ok: Expect; - true: Expect; - false: Expect; - null: Expect; - undefined: Expect; - exist: Expect; - empty: Expect; - arguments: Expect; - Arguments: Expect; - equal: Equal; - equals: Equal; - eq: Equal; - eql: Equal; - eqls: Equal; - property: Property; - ownProperty: OwnProperty; - haveOwnProperty: OwnProperty; - length: Length; - lengthOf: Length; - match(RegularExpression: RegExp, message?: string): Expect; - string(string: string, message?: string): Expect; - keys: Keys; - key(string: string): Expect; - throw: Throw; - throws: Throw; - Throw: Throw; - respondTo(method: string, message?: string): Expect; - itself: Expect; - satisfy(matcher: Function, message?: string): Expect; - closeTo(expected: number, delta: number, message?: string): Expect; - members: Members; - } - - interface LanguageChains { - to: Expect; - be: Expect; - been: Expect; - is: Expect; - that: Expect; - and: Expect; - have: Expect; - with: Expect; - at: Expect; - of: Expect; - same: Expect; - } - - interface NumericComparison { - above: NumberComparer; - gt: NumberComparer; - greaterThan: NumberComparer; - least: NumberComparer; - gte: NumberComparer; - below: NumberComparer; - lt: NumberComparer; - lessThan: NumberComparer; - most: NumberComparer; - lte: NumberComparer; - within(start: number, finish: number, message?: string): Expect; - } - - interface NumberComparer { - (value: number, message?: string): Expect; - } - - interface TypeComparison { - (type: string, message?: string): Expect; - instanceof: InstanceOf; - instanceOf: InstanceOf; - } - - interface InstanceOf { - (constructor: Object, message?: string): Expect; - } - - interface Deep { - equal: Equal; - property: Property; - } - - interface Equal { - (value: any, message?: string): Expect; - } - - interface Property { - (name: string, value?: any, message?: string): Expect; - } - - interface OwnProperty { - (name: string, message?: string): Expect; - } - - interface Length extends LanguageChains, NumericComparison { - (length: number, message?: string): Expect; - } - - interface Include { - (value: Object, message?: string): Expect; - (value: string, message?: string): Expect; - (value: number, message?: string): Expect; - keys: Keys; - members: Members; - } - - interface Keys { - (...keys: string[]): Expect; - (keys: any[]): Expect; - } - - interface Members { - (set: any[], message?: string): Expect; - } - - interface Throw { - (): Expect; - (expected: string, message?: string): Expect; - (expected: RegExp, message?: string): Expect; - (constructor: Error, expected?: string, message?: string): Expect; - (constructor: Error, expected?: RegExp, message?: string): Expect; - (constructor: Function, expected?: string, message?: string): Expect; - (constructor: Function, expected?: RegExp, message?: string): Expect; - } - - export interface Assert { - (express: any, msg?: string):void; - - fail(actual?: any, expected?: any, msg?: string, operator?: string):void; - - ok(val: any, msg?: string):void; - notOk(val: any, msg?: string):void; - - equal(act: any, exp: any, msg?: string):void; - notEqual(act: any, exp: any, msg?: string):void; - - strictEqual(act: any, exp: any, msg?: string):void; - notStrictEqual(act: any, exp: any, msg?: string):void; - - deepEqual(act: any, exp: any, msg?: string):void; - notDeepEqual(act: any, exp: any, msg?: string):void; - - isTrue(val: any, msg?: string):void; - isFalse(val: any, msg?: string):void; - - isNull(val: any, msg?: string):void; - isNotNull(val: any, msg?: string):void; - - isUndefined(val: any, msg?: string):void; - isDefined(val: any, msg?: string):void; - - isFunction(val: any, msg?: string):void; - isNotFunction(val: any, msg?: string):void; - - isObject(val: any, msg?: string):void; - isNotObject(val: any, msg?: string):void; - - isArray(val: any, msg?: string):void; - isNotArray(val: any, msg?: string):void; - - isString(val: any, msg?: string):void; - isNotString(val: any, msg?: string):void; - - isNumber(val: any, msg?: string):void; - isNotNumber(val: any, msg?: string):void; - - isBoolean(val: any, msg?: string):void; - isNotBoolean(val: any, msg?: string):void; - - typeOf(val: any, type: string, msg?: string):void; - notTypeOf(val: any, type: string, msg?: string):void; - - instanceOf(val: any, type: Function, msg?: string):void; - notInstanceOf(val: any, type: Function, msg?: string):void; - - include(exp: string, inc: any, msg?: string):void; - include(exp: any[], inc: any, msg?: string):void; - - notInclude(exp: string, inc: any, msg?: string):void; - notInclude(exp: any[], inc: any, msg?: string):void; - - match(exp: any, re: RegExp, msg?: string):void; - notMatch(exp: any, re: RegExp, msg?: string):void; - - property(obj: Object, prop: string, msg?: string):void; - notProperty(obj: Object, prop: string, msg?: string):void; - deepProperty(obj: Object, prop: string, msg?: string):void; - notDeepProperty(obj: Object, prop: string, msg?: string):void; - - propertyVal(obj: Object, prop: string, val: any, msg?: string):void; - propertyNotVal(obj: Object, prop: string, val: any, msg?: string):void; - - deepPropertyVal(obj: Object, prop: string, val: any, msg?: string):void; - deepPropertyNotVal(obj: Object, prop: string, val: any, msg?: string):void; - - lengthOf(exp: any, len: number, msg?: string):void; - //alias frenzy - throw(fn: Function, msg?: string):void; - throw(fn: Function, regExp: RegExp):void; - throw(fn: Function, errType: Function, msg?: string):void; - throw(fn: Function, errType: Function, regExp: RegExp):void; - - throws(fn: Function, msg?: string):void; - throws(fn: Function, regExp: RegExp):void; - throws(fn: Function, errType: Function, msg?: string):void; - throws(fn: Function, errType: Function, regExp: RegExp):void; - - Throw(fn: Function, msg?: string):void; - Throw(fn: Function, regExp: RegExp):void; - Throw(fn: Function, errType: Function, msg?: string):void; - Throw(fn: Function, errType: Function, regExp: RegExp):void; - - doesNotThrow(fn: Function, msg?: string):void; - doesNotThrow(fn: Function, regExp: RegExp):void; - doesNotThrow(fn: Function, errType: Function, msg?: string):void; - doesNotThrow(fn: Function, errType: Function, regExp: RegExp):void; - - operator(val: any, operator: string, val2: any, msg?: string):void; - closeTo(act: number, exp: number, delta: number, msg?: string):void; - - sameMembers(set1: any[], set2: any[], msg?: string):void; - includeMembers(set1: any[], set2: any[], msg?: string):void; - - ifError(val: any, msg?: string):void; - } -} - -declare module "chai" { -export = chai; -} diff --git a/tests/typings/mocha.d.ts b/tests/typings/mocha.d.ts deleted file mode 100644 index 8d49749b7..000000000 --- a/tests/typings/mocha.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Type definitions for mocha 1.9.0 -// Project: http://visionmedia.github.io/mocha/ -// Definitions by: Kazi Manzur Rashid -// DefinitelyTyped: https://github.com/borisyankov/DefinitelyTyped - -interface Mocha { - // Setup mocha with the given setting options. - setup(options: MochaSetupOptions): Mocha; - - //Run tests and invoke `fn()` when complete. - run(callback?: () => void): void; - - // Set reporter as function - reporter(reporter: () => void): Mocha; - - // Set reporter, defaults to "dot" - reporter(reporter: string): Mocha; - - // Enable growl support. - growl(): Mocha -} - -interface MochaSetupOptions { - //milliseconds to wait before considering a test slow - slow?: number; - - // timeout in milliseconds - timeout?: number; - - // ui name "bdd", "tdd", "exports" etc - ui?: string; - - //array of accepted globals - globals?: any[]; - - // reporter instance (function or string), defaults to `mocha.reporters.Dot` - reporter?: any; - - // bail on the first test failure - bail?: Boolean; - - // ignore global leaks - ignoreLeaks?: Boolean; - - // grep string or regexp to filter tests with - grep?: any; -} - -declare module mocha { - interface Done { - (error?: Error): void; - } -} - -declare var describe : { - (description: string, spec: () => void): void; - only(description: string, spec: () => void): void; - skip(description: string, spec: () => void): void; - timeout(ms: number): void; -} - -declare var it: { - (expectation: string, assertion?: () => void): void; - (expectation: string, assertion?: (done: mocha.Done) => void): void; - only(expectation: string, assertion?: () => void): void; - only(expectation: string, assertion?: (done: mocha.Done) => void): void; - skip(expectation: string, assertion?: () => void): void; - skip(expectation: string, assertion?: (done: mocha.Done) => void): void; - timeout(ms: number): void; -}; - -declare function before(action: () => void): void; - -declare function before(action: (done: mocha.Done) => void): void; - -declare function after(action: () => void): void; - -declare function after(action: (done: mocha.Done) => void): void; - -declare function beforeEach(action: () => void): void; - -declare function beforeEach(action: (done: mocha.Done) => void): void; - -declare function afterEach(action: () => void): void; - -declare function afterEach(action: (done: mocha.Done) => void): void;