27 | The Ionic Conference is a one-day conference on {{ conferenceDate | date: 'mediumDate' }} featuring talks from the Ionic team. It is focused on Ionic applications being built with Ionic Framework. This includes migrating apps to the latest version of the framework, Angular concepts, Webpack, Sass, and many other technologies used in Ionic 2. Tickets are completely sold out, and we’re expecting more than 1000 developers – making this the largest Ionic conference ever!
28 |
18 | The
19 | ionic conference app is a practical preview of the ionic framework in action, and a demonstration of proper code
20 | use.
21 |
22 |
23 |
24 |
25 |
26 |
What is Ionic?
27 |
28 | Ionic Framework is an open source SDK that enables developers to build high quality mobile apps with web technologies
29 | like HTML, CSS, and JavaScript.
30 |
31 |
32 |
33 |
34 |
What is Ionic Appflow?
35 |
36 | Ionic Appflow is a powerful set of services and features built on top of Ionic Framework that brings a totally new
37 | level of app development agility to mobile dev teams.
38 |
39 |
40 |
41 |
42 |
Ready to Play?
43 |
44 | Continue
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/app/pages/tutorial/tutorial.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { IonicModule } from '@ionic/angular';
4 | import { SwiperModule } from 'swiper/angular';
5 |
6 | import { TutorialPage } from './tutorial';
7 | import { TutorialPageRoutingModule } from './tutorial-routing.module';
8 |
9 | @NgModule({
10 | imports: [
11 | CommonModule,
12 | IonicModule,
13 | TutorialPageRoutingModule,
14 | SwiperModule
15 | ],
16 | declarations: [TutorialPage],
17 | entryComponents: [TutorialPage],
18 | })
19 | export class TutorialModule {}
20 |
--------------------------------------------------------------------------------
/src/app/pages/tutorial/tutorial.scss:
--------------------------------------------------------------------------------
1 | ion-toolbar {
2 | --background: transparent;
3 | --border-color: transparent;
4 | }
5 |
6 | .slide-title {
7 | margin-top: 2.8rem;
8 | }
9 |
10 | .slide-image {
11 | max-height: 50%;
12 | max-width: 60%;
13 | margin: 36px 0;
14 | pointer-events: none;
15 | }
16 |
17 | b {
18 | font-weight: 500;
19 | }
20 |
21 | p {
22 | padding: 0 40px;
23 | font-size: 14px;
24 | line-height: 1.5;
25 | color: var(--ion-color-step-600, #60646b);
26 |
27 | b {
28 | color: var(--ion-text-color, #000000);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/pages/tutorial/tutorial.spec.ts:
--------------------------------------------------------------------------------
1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import { TestBed, waitForAsync } from '@angular/core/testing';
4 | import { MenuController } from '@ionic/angular';
5 |
6 | import { TutorialPage } from './tutorial';
7 |
8 | import { IonicStorageModule } from '@ionic/storage';
9 | describe('TutorialPage', () => {
10 | let fixture, app;
11 | beforeEach(waitForAsync(() => {
12 | const menuSpy = jasmine.createSpyObj('MenuController', [
13 | 'toggle',
14 | 'enable'
15 | ]);
16 | const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
17 |
18 | TestBed.configureTestingModule({
19 | declarations: [TutorialPage],
20 | schemas: [CUSTOM_ELEMENTS_SCHEMA],
21 | imports: [IonicStorageModule.forRoot()],
22 | providers: [
23 | { provide: MenuController, useValue: menuSpy },
24 | { provide: Router, useValue: routerSpy }
25 | ]
26 | }).compileComponents();
27 | }));
28 |
29 | beforeEach(() => {
30 | fixture = TestBed.createComponent(TutorialPage);
31 | app = fixture.debugElement.componentInstance;
32 | });
33 | it('should create the tutorial page', () => {
34 | expect(app).toBeTruthy();
35 | });
36 |
37 | it('should check the tutorial status', async () => {
38 | const didTuts = await app.storage.get('ion_did_tutorial');
39 | expect(didTuts).toBeFalsy();
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/app/pages/tutorial/tutorial.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectorRef } from '@angular/core';
2 | import { Router } from '@angular/router';
3 |
4 | import { MenuController } from '@ionic/angular';
5 |
6 | import { Storage } from '@ionic/storage';
7 | import Swiper from 'swiper';
8 |
9 | @Component({
10 | selector: 'page-tutorial',
11 | templateUrl: 'tutorial.html',
12 | styleUrls: ['./tutorial.scss'],
13 | })
14 | export class TutorialPage {
15 | showSkip = true;
16 | private slides: Swiper;
17 |
18 | constructor(
19 | public menu: MenuController,
20 | public router: Router,
21 | public storage: Storage,
22 | private cd: ChangeDetectorRef
23 | ) {}
24 |
25 | startApp() {
26 | this.router
27 | .navigateByUrl('/app/tabs/schedule', { replaceUrl: true })
28 | .then(() => this.storage.set('ion_did_tutorial', true));
29 | }
30 |
31 | setSwiperInstance(swiper: Swiper) {
32 | this.slides = swiper;
33 | }
34 |
35 | onSlideChangeStart() {
36 | this.showSkip = !this.slides.isEnd;
37 | this.cd.detectChanges();
38 | }
39 |
40 | ionViewWillEnter() {
41 | this.storage.get('ion_did_tutorial').then(res => {
42 | if (res === true) {
43 | this.router.navigateByUrl('/app/tabs/schedule', { replaceUrl: true });
44 | }
45 | });
46 |
47 | this.menu.enable(false);
48 | }
49 |
50 | ionViewDidLeave() {
51 | // enable the root left menu when leaving the tutorial page
52 | this.menu.enable(true);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/providers/check-tutorial.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { CanLoad, Router } from '@angular/router';
3 | import { Storage } from '@ionic/storage';
4 | @Injectable({
5 | providedIn: 'root'
6 | })
7 | export class CheckTutorial implements CanLoad {
8 | constructor(private storage: Storage, private router: Router) {}
9 |
10 | canLoad() {
11 | return this.storage.get('ion_did_tutorial').then(res => {
12 | if (res) {
13 | this.router.navigate(['/app', 'tabs', 'schedule']);
14 | return false;
15 | } else {
16 | return true;
17 | }
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/providers/user-data.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Storage } from '@ionic/storage';
3 |
4 |
5 | @Injectable({
6 | providedIn: 'root'
7 | })
8 | export class UserData {
9 | favorites: string[] = [];
10 | HAS_LOGGED_IN = 'hasLoggedIn';
11 | HAS_SEEN_TUTORIAL = 'hasSeenTutorial';
12 |
13 | constructor(
14 | public storage: Storage
15 | ) { }
16 |
17 | hasFavorite(sessionName: string): boolean {
18 | return (this.favorites.indexOf(sessionName) > -1);
19 | }
20 |
21 | addFavorite(sessionName: string): void {
22 | this.favorites.push(sessionName);
23 | }
24 |
25 | removeFavorite(sessionName: string): void {
26 | const index = this.favorites.indexOf(sessionName);
27 | if (index > -1) {
28 | this.favorites.splice(index, 1);
29 | }
30 | }
31 |
32 | login(username: string): Promise {
33 | return this.storage.set(this.HAS_LOGGED_IN, true).then(() => {
34 | this.setUsername(username);
35 | return window.dispatchEvent(new CustomEvent('user:login'));
36 | });
37 | }
38 |
39 | signup(username: string): Promise {
40 | return this.storage.set(this.HAS_LOGGED_IN, true).then(() => {
41 | this.setUsername(username);
42 | return window.dispatchEvent(new CustomEvent('user:signup'));
43 | });
44 | }
45 |
46 | logout(): Promise {
47 | return this.storage.remove(this.HAS_LOGGED_IN).then(() => {
48 | return this.storage.remove('username');
49 | }).then(() => {
50 | window.dispatchEvent(new CustomEvent('user:logout'));
51 | });
52 | }
53 |
54 | setUsername(username: string): Promise {
55 | return this.storage.set('username', username);
56 | }
57 |
58 | getUsername(): Promise {
59 | return this.storage.get('username').then((value) => {
60 | return value;
61 | });
62 | }
63 |
64 | isLoggedIn(): Promise {
65 | return this.storage.get(this.HAS_LOGGED_IN).then((value) => {
66 | return value === true;
67 | });
68 | }
69 |
70 | checkHasSeenTutorial(): Promise {
71 | return this.storage.get(this.HAS_SEEN_TUTORIAL).then((value) => {
72 | return value;
73 | });
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/assets/img/about/Archive.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/about/Archive.zip
--------------------------------------------------------------------------------
/src/assets/img/about/austin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/about/austin.jpg
--------------------------------------------------------------------------------
/src/assets/img/about/chicago.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/about/chicago.jpg
--------------------------------------------------------------------------------
/src/assets/img/about/madison.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/about/madison.jpg
--------------------------------------------------------------------------------
/src/assets/img/about/seattle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/about/seattle.jpg
--------------------------------------------------------------------------------
/src/assets/img/appicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/appicon.png
--------------------------------------------------------------------------------
/src/assets/img/appicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/img/ica-slidebox-img-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/ica-slidebox-img-1.png
--------------------------------------------------------------------------------
/src/assets/img/ica-slidebox-img-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/ica-slidebox-img-2.png
--------------------------------------------------------------------------------
/src/assets/img/ica-slidebox-img-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/ica-slidebox-img-3.png
--------------------------------------------------------------------------------
/src/assets/img/ica-slidebox-img-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/ica-slidebox-img-4.png
--------------------------------------------------------------------------------
/src/assets/img/ionic-logo-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/speaker-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speaker-background.png
--------------------------------------------------------------------------------
/src/assets/img/speakers/bear.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/bear.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/cheetah.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/cheetah.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/duck.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/duck.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/eagle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/eagle.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/elephant.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/elephant.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/giraffe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/giraffe.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/iguana.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/iguana.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/kitten.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/kitten.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/lion.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/lion.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/mouse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/mouse.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/puppy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/puppy.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/rabbit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/rabbit.jpg
--------------------------------------------------------------------------------
/src/assets/img/speakers/turtle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/ionic-e2e-example/fffed0d77fad002953ec108d2b40416e8d1e1766/src/assets/img/speakers/turtle.jpg
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * In development mode, to ignore zone related error stack frames such as
11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can
12 | * import the following file, but please comment it out in production mode
13 | * because it will have performance impact when throw error
14 | */
15 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
16 |
--------------------------------------------------------------------------------
/src/global.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * App Global CSS
3 | * ----------------------------------------------------------------------------
4 | * Put style rules here that you want to apply globally. These styles are for
5 | * the entire app and not just one component. Additionally, this file can be
6 | * used as an entry point to import other CSS/Sass files to be included in the
7 | * output CSS.
8 | * For more information on global stylesheets, visit the documentation:
9 | * https://ionicframework.com/docs/layout/global-stylesheets
10 | */
11 |
12 | /* Core CSS required for Ionic components to work properly */
13 | @import "~@ionic/angular/css/core.css";
14 |
15 | /* Basic CSS for apps built with Ionic */
16 | @import "~@ionic/angular/css/normalize.css";
17 | @import "~@ionic/angular/css/structure.css";
18 | @import "~@ionic/angular/css/typography.css";
19 |
20 | /* Optional CSS utils that can be commented out */
21 | @import "~@ionic/angular/css/padding.css";
22 | @import "~@ionic/angular/css/float-elements.css";
23 | @import "~@ionic/angular/css/text-alignment.css";
24 | @import "~@ionic/angular/css/text-transformation.css";
25 | @import "~@ionic/angular/css/flex-utils.css";
26 |
27 |
28 | /*
29 | * App CSS
30 | * ----------------------------------------------------------------------------
31 | * Imports a file that can contain Sass/CSS that should be used throughout
32 | * the entire app.
33 | */
34 |
35 | @import "./app/app.scss";
36 |
37 | @import '~swiper/scss';
38 | @import '~@ionic/angular/css/ionic-swiper';
39 |
40 | // placed here instead of tutorial.scss due to slide els not getting ng scoping attribute
41 | .swiper-slide {
42 | display: block;
43 | }
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Ionic Conference App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | process.env.CHROME_BIN = require('puppeteer').executablePath();
5 |
6 | module.exports = function (config) {
7 | config.set({
8 | basePath: '',
9 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
10 | plugins: [
11 | require('karma-jasmine'),
12 | require('karma-chrome-launcher'),
13 | require('karma-jasmine-html-reporter'),
14 | require('karma-coverage-istanbul-reporter'),
15 | require('@angular-devkit/build-angular/plugins/karma')
16 | ],
17 | client: {
18 | clearContext: false // leave Jasmine Spec Runner output visible in browser
19 | },
20 | coverageIstanbulReporter: {
21 | dir: require('path').join(__dirname, 'coverage'),
22 | reports: ['html', 'lcovonly'],
23 | fixWebpackSourcePaths: true
24 | },
25 | reporters: ['progress', 'kjhtml'],
26 | port: 9876,
27 | colors: true,
28 | logLevel: config.LOG_INFO,
29 | autoWatch: true,
30 | browsers: ['Chrome', 'ChromeHeadless'],
31 | customLaunchers: {
32 | ChromeHeadlessCI: {
33 | base: 'ChromeHeadless',
34 | flags: ['--no-sandbox', '--disable-gpu']
35 | }
36 | },
37 | singleRun: false
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import '@angular/compiler';
2 |
3 | import { enableProdMode } from '@angular/core';
4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
5 |
6 | import { AppModule } from './app/app.module';
7 | import { environment } from './environments/environment';
8 |
9 | if (environment.production) {
10 | enableProdMode();
11 | }
12 |
13 | platformBrowserDynamic().bootstrapModule(AppModule)
14 | .catch(err => console.log(err));
15 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Ionic Conference",
3 | "short_name": "Ionic Conference",
4 | "start_url": "index.html",
5 | "display": "standalone",
6 | "icons": [
7 | {
8 | "src": "assets/img/appicon.png",
9 | "sizes": "512x512",
10 | "type": "image/png"
11 | }
12 | ],
13 | "background_color": "#387ef5",
14 | "theme_color": "#387ef5"
15 | }
16 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | // BROWSER POLYFILLS
18 | // ---------------------------------------------------------------------------------
19 |
20 | // IE9, IE10 and IE11 requires all of the following polyfills.
21 | // import 'core-js/es6/symbol';
22 | // import 'core-js/es6/object';
23 | // import 'core-js/es6/function';
24 | // import 'core-js/es6/parse-int';
25 | // import 'core-js/es6/parse-float';
26 | // import 'core-js/es6/number';
27 | // import 'core-js/es6/math';
28 | // import 'core-js/es6/string';
29 | // import 'core-js/es6/date';
30 | // import 'core-js/es6/array';
31 | // import 'core-js/es6/regexp';
32 | // import 'core-js/es6/map';
33 | // import 'core-js/es6/weak-map';
34 | // import 'core-js/es6/set';
35 |
36 | /** IE10 and IE11 requires the following for the Reflect API. */
37 | // import 'core-js/es6/reflect';
38 |
39 | // Evergreen browsers require these.
40 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
41 |
42 |
43 | // Required to support Web Animations `@angular/platform-browser/animations`.
44 |
45 | // Zone JS is required by Angular itself.
46 | import 'zone.js'; // Included with Angular CLI.
47 |
48 | // APPLICATION IMPORTS
49 | // ------------------------------------------------------------------------
50 |
51 | /**
52 | * Date, currency, decimal and percent pipes.
53 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
54 | */
55 | // import 'intl'; // Run `npm install --save intl`.
56 | /**
57 | * Need to import at least one locale-data with intl.
58 | */
59 | // import 'intl/locale-data/jsonp/en';
60 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting(), {
16 | teardown: { destroyAfterEach: false }
17 | }
18 | );
19 | // Then we find all the tests.
20 | const context = require.context('./', true, /\.spec\.ts$/);
21 | // And load the modules.
22 | context.keys().map(context);
23 |
--------------------------------------------------------------------------------
/src/zone-flags.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Prevents Angular change detection from
3 | * running with certain Web Component callbacks
4 | */
5 | (window as any).__Zone_disable_customElements = true;
6 |
--------------------------------------------------------------------------------
/tests/config/wdio.android.config.ts:
--------------------------------------------------------------------------------
1 | import config from './wdio.shared.appium.config';
2 |
3 | // ============
4 | // Capabilities
5 | // ============
6 | //
7 | // For all capabilities please check
8 | // http://appium.io/docs/en/writing-running-appium/caps/#general-capabilities
9 | //
10 | config.capabilities = [
11 | {
12 | // The defaults you need to have in your config
13 | platformName: 'Android',
14 | maxInstances: 1,
15 | // For W3C the appium capabilities need to have an extension prefix
16 | // http://appium.io/docs/en/writing-running-appium/caps/
17 | // This is `appium:` for all Appium Capabilities which can be found here
18 | 'appium:deviceName': 'Pixel_3_11.0',
19 | 'appium:platformVersion': '11',
20 | 'appium:orientation': 'PORTRAIT',
21 | 'appium:automationName': 'UiAutomator2',
22 | // The path to the app
23 | 'appium:app': 'platforms/android/app/build/outputs/apk/debug/app-debug.apk',
24 | 'appium:appWaitActivity': 'com.ionicframework.conferenceapp.MainActivity',
25 | 'appium:newCommandTimeout': 240,
26 | // This will automatically start the iOS app in a webview context,
27 | // if your app starts in a native context then please put this to false and handle your own
28 | // context switching
29 | 'appium:autoWebview': true,
30 | // Read the reset strategies very well, they differ per platform, see
31 | // http://appium.io/docs/en/writing-running-appium/other/reset-strategies/
32 | // When enabling the noReset the App will NOT be re-installed between sessions
33 | // This means that every test needs to maintain it's own state
34 | // `"appium:noReset":false` means that the app is removed and installed
35 | // between each test
36 | 'appium:noReset': true,
37 | // This will prevent appium to restart the app between the sessions,
38 | // meaning between each spec file
39 | // @ts-ignore
40 | 'appium:dontStopAppOnReset': true,
41 | },
42 | ];
43 |
44 | exports.config = config;
45 |
--------------------------------------------------------------------------------
/tests/config/wdio.ios.config.ts:
--------------------------------------------------------------------------------
1 | import config from './wdio.shared.appium.config';
2 |
3 | // ============
4 | // Capabilities
5 | // ============
6 | //
7 | // For all capabilities please check
8 | // http://appium.io/docs/en/writing-running-appium/caps/#general-capabilities
9 | //
10 | config.capabilities = [
11 | {
12 | platformName: 'iOS',
13 | maxInstances: 1,
14 | // For W3C the appium capabilities need to have an extension prefix
15 | // This is `appium:` for all Appium Capabilities which can be found here
16 | // http://appium.io/docs/en/writing-running-appium/caps/
17 | 'appium:deviceName': 'iPhone 13 Pro Max',
18 | 'appium:platformVersion': '15.2',
19 | 'appium:orientation': 'PORTRAIT',
20 | 'appium:automationName': 'XCUITest',
21 | // The path to the app
22 | 'appium:app': './platforms/ios/build/emulator/Ionic Conference App.app',
23 | 'appium:newCommandTimeout': 240,
24 | // This will automatically start the iOS app in a webview context,
25 | // if your app starts in a native context then please put this to false and handle your own
26 | // context switching
27 | 'appium:autoWebview': true,
28 | // Read the reset strategies very well, they differ per platform, see
29 | // http://appium.io/docs/en/writing-running-appium/other/reset-strategies/
30 | // When enabling the noReset the App will NOT be re-installed between sessions
31 | // This means that every test needs to maintain it's own state
32 | // `"appium:noReset":false` means that the app is removed and installed
33 | // between each test
34 | 'appium:noReset': true,
35 | },
36 | ];
37 | config.maxInstances = 1;
38 |
39 | exports.config = config;
40 |
--------------------------------------------------------------------------------
/tests/config/wdio.shared.appium.config.ts:
--------------------------------------------------------------------------------
1 | import { config } from './wdio.shared.config';
2 |
3 | //
4 | // =====================
5 | // Server Configurations
6 | // =====================
7 | //
8 | // The server port Appium is running on
9 | //
10 | config.port = 4723;
11 |
12 | //
13 | // ================
14 | // Services: Appium
15 | // ================
16 | //
17 | config.services = (config.services ? config.services : []).concat([
18 | [
19 | 'appium',
20 | {
21 | // This will use the globally installed version of Appium
22 | command: 'appium',
23 | args: {
24 | // This is needed to tell Appium that we can execute local ADB commands
25 | // and to automatically download the latest version of ChromeDriver
26 | relaxedSecurity: true,
27 | // Only log Appium logs in verbose mode
28 | ...(process.env.VERBOSE === 'true' ? { log: './appium.log' } : {}),
29 | },
30 | },
31 | ],
32 | ]);
33 |
34 | export default config;
35 |
--------------------------------------------------------------------------------
/tests/config/wdio.web.config.ts:
--------------------------------------------------------------------------------
1 | import { config } from './wdio.shared.config';
2 |
3 | //
4 | // =====
5 | // Specs
6 | // =====
7 | //
8 | // Normally the specs are written like
9 | // specs: ["./tests/**/*.spec.ts"],
10 | // but for watch mode we want to run all specs in 1 instance so
11 | // the specs are an array in an array
12 | config.specs = [['./tests/**/*.spec.ts']];
13 | config.filesToWatch = ['./tests/**/*.spec.ts'];
14 |
15 | //
16 | // ================
17 | // Services: Chrome
18 | // ================
19 | //
20 | config.services = (config.services ? config.services : []).concat([
21 | [
22 | 'chromedriver',
23 | {
24 | args: [
25 | '--use-fake-ui-for-media-stream',
26 | '--use-fake-device-for-media-stream',
27 | ],
28 | },
29 | ],
30 | ]);
31 |
32 | // ============
33 | // Capabilities
34 | // ============
35 | //
36 | // For all capabilities please check
37 | // http://appium.io/docs/en/writing-running-appium/caps/#general-capabilities
38 | //
39 | config.capabilities = [
40 | {
41 | maxInstances: 1,
42 | browserName: 'chrome',
43 | 'goog:chromeOptions': {
44 | args: ['--window-size=500,1000'],
45 | // See https://chromedriver.chromium.org/mobile-emulation
46 | // For more details
47 | mobileEmulation: {
48 | deviceMetrics: { width: 393, height: 851, pixelRatio: 3 },
49 | userAgent:
50 | 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Mobile Safari/537.36',
51 | },
52 | prefs: {
53 | 'profile.default_content_setting_values.media_stream_camera': 1,
54 | 'profile.default_content_setting_values.media_stream_mic': 1,
55 | 'profile.default_content_setting_values.notifications': 1,
56 | },
57 | },
58 | },
59 | ];
60 |
61 | exports.config = config;
62 |
--------------------------------------------------------------------------------
/tests/helpers/browser.ts:
--------------------------------------------------------------------------------
1 | export async function getUrl(): Promise {
2 | return new URL(await browser.getUrl());
3 | }
4 |
--------------------------------------------------------------------------------
/tests/helpers/definitions.ts:
--------------------------------------------------------------------------------
1 | export interface ElementActionOptions {
2 | /**
3 | * How long to wait (in ms) for the element to be visible before
4 | * the test fails. Default: 5000 ms
5 | */
6 | visibilityTimeout?: number;
7 | }
8 |
9 | export interface ElementSelector {
10 | text?: string;
11 | }
12 |
--------------------------------------------------------------------------------
/tests/helpers/element.ts:
--------------------------------------------------------------------------------
1 | import { ElementActionOptions } from './definitions';
2 |
3 | export async function waitForElement(selector: string, { visibilityTimeout = 5000 }: ElementActionOptions = {}) {
4 | const el = await $(selector);
5 | await el.waitForDisplayed({ timeout: visibilityTimeout });
6 | return el;
7 | }
8 |
9 | export async function blur(selector: string, { visibilityTimeout = 5000 }: ElementActionOptions = {}) {
10 | return browser.execute((sel) => {
11 | const el = document.querySelector(sel);
12 | if (el) {
13 | (el as any).blur();
14 | }
15 | }, selector);
16 | }
17 |
18 | export async function tryAcceptAlert() {
19 | try {
20 | return driver.acceptAlert();
21 | } catch (e) {
22 | console.warn('No alert to accept');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/helpers/index.ts:
--------------------------------------------------------------------------------
1 | import type { Expect } from 'expect-webdriverio';
2 | export { Expect };
3 |
4 | export * from './definitions';
5 | export * from './platform/index';
6 | export * from './element';
7 | export * from './gestures';
8 | export * from './browser';
9 | export * from './storage';
10 |
11 | export * from './ionic';
12 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/alert.ts:
--------------------------------------------------------------------------------
1 | import { IonicComponent } from './component';
2 |
3 | export class IonicAlert extends IonicComponent {
4 | constructor(selector?: string | WebdriverIO.Element) {
5 | super(selector ?? 'ion-alert');
6 | }
7 |
8 | get input() {
9 | return $(this.selector).$(`.alert-input`);
10 | }
11 |
12 | async button(buttonTitle: string) {
13 | return $(this.selector).$(`button=${buttonTitle}`);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/button.ts:
--------------------------------------------------------------------------------
1 | import { Ionic$ } from '..';
2 | import { ElementActionOptions } from '../..';
3 | import { IonicComponent } from './component';
4 |
5 | export interface TapButtonOptions extends ElementActionOptions {
6 | /**
7 | * Whether to scroll the element into view first. Default: true
8 | */
9 | scroll?: boolean;
10 | }
11 |
12 | export class IonicButton extends IonicComponent {
13 | constructor(selector: string) {
14 | super(selector);
15 | }
16 |
17 | static withTitle(buttonTitle: string): IonicButton {
18 | return new IonicButton(`ion-button=${buttonTitle}`);
19 | }
20 |
21 | async tap({
22 | visibilityTimeout = 5000,
23 | scroll = true,
24 | }: TapButtonOptions = {}) {
25 | const button = await Ionic$.$(this.selector as string);
26 | await button.waitForDisplayed({ timeout: visibilityTimeout });
27 | if (scroll) {
28 | await button.scrollIntoView();
29 | }
30 | return button.click();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/component.ts:
--------------------------------------------------------------------------------
1 | export class IonicComponent {
2 | constructor(public selector: string | WebdriverIO.Element) {
3 | }
4 |
5 | get $() {
6 | return import('./page').then(async ({ IonicPage }) => {
7 | if (typeof this.selector === 'string') {
8 | const activePage = await IonicPage.active();
9 | return activePage.$(this.selector);
10 | }
11 |
12 | return this.selector;
13 | });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/content.ts:
--------------------------------------------------------------------------------
1 | import { IonicComponent } from './component';
2 |
3 | export class IonicContent extends IonicComponent {
4 | constructor(selector: string) {
5 | super(selector);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './button';
2 | export * from './content';
3 | export * from './input';
4 | export * from './item';
5 | export * from './menu';
6 | export * from './page';
7 | export * from './segment';
8 | export * from './select';
9 | export * from './slides';
10 | export * from './textarea';
11 | export * from './toast';
12 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/input.ts:
--------------------------------------------------------------------------------
1 | import { IonicComponent } from './component';
2 | import { Ionic$ } from '..';
3 | import { ElementActionOptions } from '../..';
4 |
5 | export class IonicInput extends IonicComponent {
6 | constructor(selector: string) {
7 | super(selector);
8 | }
9 |
10 | async setValue(
11 | value: string,
12 | { visibilityTimeout = 5000 }: ElementActionOptions = {}
13 | ) {
14 | const el = await Ionic$.$(this.selector as string);
15 | await el.waitForDisplayed({ timeout: visibilityTimeout });
16 |
17 | const ionTags = ['ion-input', 'ion-textarea'];
18 | if (ionTags.indexOf(await el.getTagName()) >= 0) {
19 | const input = await el.$('input,textarea');
20 | await input.setValue(value);
21 | } else {
22 | return el.setValue(value);
23 | }
24 | }
25 |
26 | async getValue({ visibilityTimeout = 5000 }: ElementActionOptions = {}) {
27 | const el = await Ionic$.$(this.selector as string);
28 | await el.waitForDisplayed({ timeout: visibilityTimeout });
29 |
30 | const ionTags = ['ion-input', 'ion-textarea'];
31 | if (ionTags.indexOf(await el.getTagName()) >= 0) {
32 | const input = await el.$('input,textarea');
33 | return input.getValue();
34 | } else {
35 | return el.getValue();
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/item.ts:
--------------------------------------------------------------------------------
1 | import { TapButtonOptions } from '.';
2 | import { Ionic$ } from '..';
3 | import { IonicComponent } from './component';
4 |
5 | export class IonicItem extends IonicComponent {
6 | constructor(selector: string) {
7 | super(selector);
8 | }
9 |
10 | static withTitle(buttonTitle: string): IonicItem {
11 | return new IonicItem(`ion-item=${buttonTitle}`);
12 | }
13 |
14 | async tap({
15 | visibilityTimeout = 5000,
16 | scroll = true,
17 | }: TapButtonOptions = {}) {
18 | const button = await Ionic$.$(this.selector as string);
19 | await button.waitForDisplayed({ timeout: visibilityTimeout });
20 | if (scroll) {
21 | await button.scrollIntoView();
22 | }
23 | return button.click();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/menu.ts:
--------------------------------------------------------------------------------
1 | import { Ionic$ } from '..';
2 | import { ElementActionOptions } from '../..';
3 | import { IonicComponent } from './component';
4 |
5 | export interface OpenMenuOptions extends ElementActionOptions {
6 | delayForAnimation?: boolean;
7 | }
8 |
9 | export class IonicMenu extends IonicComponent {
10 | constructor(selector?: string) {
11 | super(selector || 'ion-menu');
12 | }
13 |
14 | get menuButton() {
15 | return Ionic$.$('.ion-page:not(.ion-page-hidden) ion-menu-button');
16 | }
17 |
18 | async open({
19 | delayForAnimation = true,
20 | visibilityTimeout = 5000,
21 | }: OpenMenuOptions = {}) {
22 | await (
23 | await this.menuButton
24 | ).waitForDisplayed({ timeout: visibilityTimeout });
25 | await (await this.menuButton).click();
26 |
27 | // Let the menu animate open/closed
28 | if (delayForAnimation) {
29 | return driver.pause(500);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/page.ts:
--------------------------------------------------------------------------------
1 | import { IonicComponent } from './component';
2 |
3 | export class IonicPage extends IonicComponent {
4 | static async active() {
5 | await driver.waitUntil(
6 | async () => {
7 | const currentPages = await $$('.ion-page:not(.ion-page-hidden)');
8 | for (const page of currentPages) {
9 | if ((await page.isDisplayed())) {
10 | return true;
11 | }
12 | }
13 | return false;
14 | }, {
15 | timeout: 10000,
16 | timeoutMsg: 'Unable to find any visible pages',
17 | interval: 500,
18 | }
19 | );
20 |
21 | const allPages = await $$('.ion-page:not(.ion-page-hidden)');
22 |
23 | const pages: WebdriverIO.Element[] = [];
24 |
25 | // Collect visible pages
26 | for (const page of allPages) {
27 | if ((await page.isDisplayed())) {
28 | pages.push(page);
29 | }
30 | }
31 |
32 | // Collect all the visible pages in the app
33 | const pagesAndParents: WebdriverIO.Element[][] = [];
34 | for (const page of pages) {
35 | const path = await this.getPath(page);
36 | pagesAndParents.push(path);
37 | }
38 |
39 | // Reverse sort the pages by the ones that have the most parent elements first, since
40 | // we assume pages deeper in the tree are more likely to be "active" than ones higher up
41 | const activePage = pagesAndParents.sort((a, b) => b.length - a.length)[0][0];
42 |
43 | return activePage;
44 | }
45 |
46 | static async getPath(el: WebdriverIO.Element) {
47 | const path = [el];
48 |
49 | let p = el;
50 | while (p) {
51 | p = await p.parentElement();
52 | if (p.error) {
53 | break;
54 | }
55 | path.push(p);
56 | }
57 |
58 | return path;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/segment.ts:
--------------------------------------------------------------------------------
1 | import { IonicComponent } from './component';
2 |
3 | import { TapButtonOptions } from './button';
4 | import { Ionic$ } from '..';
5 |
6 | export class IonicSegment extends IonicComponent {
7 | constructor(selector: string | WebdriverIO.Element) {
8 | super(selector);
9 | }
10 |
11 | async button(buttonTitle: string) {
12 | const segmentButtons = await (await this.$).$$('ion-segment-button');
13 | for (const button of segmentButtons) {
14 | if (
15 | (await button.getText()).toLocaleLowerCase() ===
16 | buttonTitle.toLocaleLowerCase()
17 | ) {
18 | return new IonicSegmentButton(button);
19 | }
20 | }
21 | return Promise.resolve(null);
22 | }
23 | }
24 |
25 | export class IonicSegmentButton extends IonicComponent {
26 | async tap({
27 | visibilityTimeout = 5000,
28 | scroll = true,
29 | }: TapButtonOptions = {}) {
30 | const button = await Ionic$.$(this.selector as string);
31 | await button.waitForDisplayed({ timeout: visibilityTimeout });
32 | if (scroll) {
33 | await button.scrollIntoView();
34 | }
35 | return button.click();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/select.ts:
--------------------------------------------------------------------------------
1 | import { pause, waitForElement } from '../..';
2 | import { IonicComponent } from './component';
3 |
4 | export class IonicSelect extends IonicComponent {
5 | constructor(selector: string) {
6 | super(selector);
7 | }
8 |
9 | async open() {
10 | await (await this.$).click();
11 | // Wait for the alert to popup
12 | return pause(1000);
13 | }
14 |
15 | async select(n: number) {
16 | const options = await $$('.select-interface-option');
17 |
18 | return options[n]?.click();
19 | }
20 |
21 | async cancel() {
22 | const cancel = await waitForElement('.alert-button-role-cancel');
23 | await cancel.click();
24 | // Allow alert to close
25 | return cancel.waitForDisplayed({ reverse: true });
26 | }
27 |
28 | async ok() {
29 | const ok = await waitForElement(
30 | '.alert-button:not(.alert-button-role-cancel)'
31 | );
32 | await ok.click();
33 | // Allow alert to close
34 | return ok.waitForDisplayed({ reverse: true });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/slides.ts:
--------------------------------------------------------------------------------
1 | import type { RectReturn } from '@wdio/protocols/build/types';
2 |
3 | import { IonicComponent } from './component';
4 | import { Ionic$ } from '..';
5 | import { Gestures } from '../..';
6 |
7 | export class IonicSlides extends IonicComponent {
8 | rects: RectReturn | null = null;
9 |
10 | constructor(selector: string) {
11 | super(selector);
12 | }
13 |
14 | /**
15 | * Swipe the Swiper to the LEFT (from right to left)
16 | */
17 | async swipeLeft() {
18 | // Determine the rectangles of the Swiper
19 | const SwiperRectangles = await this.getSwiperRectangles();
20 | // We need to determine the center position of the Swiper on the screen. This can be done by taking the
21 | // starting position (SwiperRectangles.y) and add half of the height of the Swiper to it.
22 | const y = Math.round(SwiperRectangles.y + SwiperRectangles.height / 2);
23 |
24 | // Execute the gesture by providing a starting position and an end position
25 | return Gestures.swipe(
26 | // Here we start on the right of the Swiper. To make sure that we don't touch the outer most right
27 | // part of the screen we take 10% of the x-position. The y-position has already been determined.
28 | {
29 | x: Math.round(SwiperRectangles.width - SwiperRectangles.width * 0.1),
30 | y,
31 | },
32 | // Here we end on the left of the Swiper. To make sure that we don't touch the outer most left
33 | // part of the screen we add 10% to the x-position. The y-position has already been determined.
34 | { x: Math.round(SwiperRectangles.x + SwiperRectangles.width * 0.1), y }
35 | );
36 | }
37 |
38 | /**
39 | * Swipe the Swiper to the RIGHT (from left to right)
40 | */
41 | async swipeRight() {
42 | // Determine the rectangles of the Swiper
43 | const SwiperRectangles = await this.getSwiperRectangles();
44 | // We need to determine the center position of the Swiper on the screen. This can be done by taking the
45 | // starting position (SwiperRectangles.y) and add half of the height of the Swiper to it.
46 | const y = Math.round(SwiperRectangles.y + SwiperRectangles.height / 2);
47 |
48 | // Execute the gesture by providing a starting position and an end position
49 | return Gestures.swipe(
50 | // Here we start on the left of the Swiper. To make sure that we don't touch the outer most left
51 | // part of the screen we add 10% to the x-position. The y-position has already been determined.
52 | { x: Math.round(SwiperRectangles.x + SwiperRectangles.width * 0.1), y },
53 | // Here we end on the right of the Swiper. To make sure that we don't touch the outer most right
54 | // part of the screen we take 10% of the x-position. The y-position has already been determined.
55 | {
56 | x: Math.round(SwiperRectangles.width - SwiperRectangles.width * 0.1),
57 | y,
58 | }
59 | );
60 | }
61 |
62 | /**
63 | * Get the Swiper position and size
64 | */
65 | async getSwiperRectangles(): Promise {
66 | const slides2 = await Ionic$.$(this.selector as string);
67 | // Get the rectangles of the Swiper and store it in a global that will be used for a next call.
68 | // We dont want ask for the rectangles of the Swiper if we already know them.
69 | // This will save unneeded webdriver calls.
70 | this.rects = this.rects || (await driver.getElementRect(slides2.elementId));
71 |
72 | return this.rects;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/textarea.ts:
--------------------------------------------------------------------------------
1 | import { IonicComponent } from './component';
2 |
3 | export class IonicTextarea extends IonicComponent {
4 | constructor(selector: string) {
5 | super(selector);
6 | }
7 |
8 | setValue(value: string) {
9 | return browser.execute(
10 | (selector: string, valueString: string) => {
11 | const el = document.querySelector(selector);
12 | if (el) {
13 | (el as any).value = valueString;
14 | }
15 | },
16 | this.selector,
17 | value
18 | );
19 | }
20 |
21 | getValue() {
22 | return browser.execute((selector: string) => {
23 | const el = document.querySelector(selector);
24 | if (el) {
25 | return (el as any).value;
26 | }
27 | return null;
28 | }, this.selector);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/components/toast.ts:
--------------------------------------------------------------------------------
1 | import { IonicComponent } from './component';
2 |
3 | export class IonicToast extends IonicComponent {
4 | constructor() {
5 | super('ion-toast');
6 | }
7 |
8 | getText() {
9 | return $(this.selector).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/tests/helpers/ionic/index.ts:
--------------------------------------------------------------------------------
1 | import { IonicPage } from './components';
2 |
3 |
4 | export class Ionic$ {
5 | static async $(selector: string): Promise {
6 | const activePage = await IonicPage.active();
7 | return activePage.$(selector);
8 | }
9 |
10 | static async $$(selector: string): Promise {
11 | const activePage = await IonicPage.active();
12 | return activePage.$$(selector);
13 | }
14 | }
15 |
16 | export * from './components';
17 |
--------------------------------------------------------------------------------
/tests/helpers/platform/android.ts:
--------------------------------------------------------------------------------
1 | import { ElementSelector } from '../definitions';
2 |
3 | export function findElementAndroid({ text }: ElementSelector) {
4 | if (text) {
5 | return $(`android=new UiSelector().text("${text}")`);
6 | } else {
7 | throw new Error('Unknown selector strategy');
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/helpers/platform/index.ts:
--------------------------------------------------------------------------------
1 | import WebView, { CONTEXT_REF } from '../webview';
2 |
3 | export * from './android';
4 | export * from './ios';
5 |
6 | export async function waitForLoad() {
7 | if (isWeb()) {
8 | return Promise.resolve();
9 | }
10 | return WebView.waitForWebsiteLoaded();
11 | }
12 |
13 | export async function switchToNative() {
14 | if (isWeb()) {
15 | return Promise.resolve();
16 | }
17 |
18 | return WebView.switchToContext(CONTEXT_REF.NATIVE);
19 | }
20 |
21 | export async function switchToWeb() {
22 | if (isWeb()) {
23 | return Promise.resolve();
24 | }
25 |
26 | return WebView.switchToContext(CONTEXT_REF.WEBVIEW);
27 | }
28 |
29 | export async function getContexts() {
30 | if (isWeb()) {
31 | return Promise.resolve(['WEBVIEW']);
32 | }
33 |
34 | return driver.getContexts();
35 | }
36 |
37 | export function getContext() {
38 | if (isWeb()) {
39 | return Promise.resolve('WEBVIEW');
40 | }
41 |
42 | return driver.getContext();
43 | }
44 |
45 | export async function url(newUrl: string) {
46 | const currentUrl = await browser.getUrl();
47 |
48 | if (newUrl[0] === '/') {
49 | // Simulate baseUrl by grabbing the current url and navigating relative
50 | // to that
51 | try {
52 | const fullUrl = new URL(newUrl, currentUrl);
53 | return browser.url(fullUrl.href);
54 | } catch (e) {}
55 | }
56 |
57 | return browser.url(newUrl);
58 | }
59 |
60 | export function pause(ms: number) {
61 | return driver.pause(ms);
62 | }
63 |
64 | export function hideKeyboard() {
65 | return driver.hideKeyboard();
66 | }
67 |
68 | export function onWeb(fn: () => Promise) {
69 | if (isWeb()) {
70 | return fn();
71 | }
72 | }
73 |
74 | export function onIOS(fn: () => Promise) {
75 | if (isIOS()) {
76 | return fn();
77 | }
78 | }
79 | export function onAndroid(fn: () => Promise) {
80 | if (isAndroid()) {
81 | return fn();
82 | }
83 | }
84 |
85 | export function isIOS() {
86 | return driver.isIOS;
87 | }
88 |
89 | export function isAndroid() {
90 | return driver.isAndroid;
91 | }
92 |
93 | export function isWeb() {
94 | return !driver.isMobile;
95 | }
96 |
97 | export async function setLocation(lat: number, lng: number) {
98 | if (isWeb()) {
99 | // Not available on web
100 | return Promise.resolve();
101 | }
102 |
103 | return driver.setGeoLocation({
104 | latitude: '' + lat,
105 | longitude: '' + lat,
106 | altitude: '94.23',
107 | });
108 | }
109 |
110 | export async function restartApp(urlPath: string) {
111 | // this is needed to set the "default" url on web so the DB can be cleared
112 | if (isWeb()) {
113 | return url(urlPath);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/tests/helpers/platform/ios.ts:
--------------------------------------------------------------------------------
1 | import { ElementSelector } from '../definitions';
2 |
3 | export function findElementIOS({ text }: ElementSelector) {
4 | if (text) {
5 | return $(
6 | `-ios class chain:**/XCUIElementTypeAny[\`label == "${text}" OR value == "${text}"\`]`
7 | );
8 | } else {
9 | throw new Error('Unknown selector strategy');
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/tests/helpers/storage.ts:
--------------------------------------------------------------------------------
1 | import { pause } from '.';
2 |
3 | export async function clearIndexedDB(dbName: string) {
4 | await browser.execute((name) => {
5 | indexedDB.deleteDatabase(name);
6 | // Needed to reload the page for the DB to be reloaded
7 | // for mobile devices
8 | document.location.reload();
9 | }, dbName);
10 |
11 | return pause(500);
12 | }
13 |
--------------------------------------------------------------------------------
/tests/helpers/webview.ts:
--------------------------------------------------------------------------------
1 | export const CONTEXT_REF = {
2 | NATIVE: 'native',
3 | WEBVIEW: 'webview',
4 | };
5 | const DOCUMENT_READY_STATE = {
6 | COMPLETE: 'complete',
7 | INTERACTIVE: 'interactive',
8 | LOADING: 'loading',
9 | };
10 |
11 | class WebView {
12 | /**
13 | * Wait for the webview context to be loaded
14 | *
15 | * By default you have `NATIVE_APP` as the current context. If a webview is loaded it will be
16 | * added to the current contexts and will looks something like this for iOS
17 | * `["NATIVE_APP","WEBVIEW_28158.2"]`
18 | * The number behind `WEBVIEW` will be a random number in random order.
19 | *
20 | * For Android you can get something like
21 | * ["NATIVE_APP","WEBVIEW_com.wdiodemoapp", "WEBVIEW_com.chrome"]`.
22 | * The string behind `WEBVIEW` will the package name of the app that holds
23 | * the webview
24 | */
25 | async waitForWebViewContextLoaded() {
26 | const context = await driver.waitUntil(
27 | async () => {
28 | const currentContexts = await this.getCurrentContexts();
29 |
30 | return (
31 | currentContexts.length > 1 &&
32 | currentContexts.find((currentContext) =>
33 | currentContext.toLowerCase().includes(CONTEXT_REF.WEBVIEW)
34 | ) !== 'undefined'
35 | );
36 | },
37 | {
38 | // Wait a max of 45 seconds. Reason for this high amount is that loading
39 | // a webview for iOS might take longer
40 | timeout: 45000,
41 | timeoutMsg: 'Webview context not loaded',
42 | interval: 100,
43 | }
44 | );
45 |
46 | return context;
47 | }
48 |
49 | /**
50 | * Switch to native or webview context
51 | */
52 | async switchToContext(context: string) {
53 | // The first context will always be the NATIVE_APP,
54 | // the second one will always be the WebdriverIO web page
55 | const currentContexts = await this.getCurrentContexts();
56 | return driver.switchContext(
57 | currentContexts[context === CONTEXT_REF.NATIVE ? 0 : 1]
58 | );
59 | }
60 |
61 | /**
62 | * Returns an object with the list of all available contexts
63 | */
64 | getCurrentContexts(): Promise {
65 | return Promise.resolve(driver.getContexts());
66 | }
67 |
68 | /**
69 | * Wait for the document to be fully loaded
70 | */
71 | waitForDocumentFullyLoaded() {
72 | return driver.waitUntil(
73 | // A webpage can have multiple states, the ready state is the one we need to have.
74 | // This looks like the same implementation as for the w3c implementation for `browser.url('https://webdriver.io')`
75 | // That command also waits for the readiness of the page, see also the w3c specs
76 | // https://www.w3.org/TR/webdriver/#dfn-waiting-for-the-navigation-to-complete
77 | async () =>
78 | (await driver.execute(() => document.readyState)) ===
79 | DOCUMENT_READY_STATE.COMPLETE,
80 | {
81 | timeout: 15000,
82 | timeoutMsg: 'Website not loaded',
83 | interval: 100,
84 | }
85 | );
86 | }
87 |
88 | /**
89 | * Wait for the website in the webview to be loaded
90 | */
91 | async waitForWebsiteLoaded() {
92 | await this.waitForWebViewContextLoaded();
93 | await this.switchToContext(CONTEXT_REF.WEBVIEW);
94 | await this.waitForDocumentFullyLoaded();
95 | await this.switchToContext(CONTEXT_REF.NATIVE);
96 | }
97 |
98 | async waitForWebViewIsDisplayedByXpath(
99 | isShown = true
100 | ): Promise {
101 | const selector = browser.isAndroid
102 | ? '*//android.webkit.WebView'
103 | : '*//XCUIElementTypeWebView';
104 | (await $(selector)).waitForDisplayed({
105 | timeout: 45000,
106 | reverse: !isShown,
107 | });
108 | }
109 | }
110 |
111 | export default new WebView();
112 |
--------------------------------------------------------------------------------
/tests/pageobjects/about.page.ts:
--------------------------------------------------------------------------------
1 | import { Ionic$, IonicButton, IonicSelect } from '../helpers';
2 |
3 | import Page from './page';
4 |
5 | class About extends Page {
6 | get popoverButton() {
7 | return new IonicButton('ion-buttons > ion-button');
8 | }
9 | get headerImage() {
10 | return Ionic$.$('.about-header');
11 | }
12 | get madisonImage() {
13 | return Ionic$.$('.madison');
14 | }
15 | get austinImage() {
16 | return Ionic$.$('.austin');
17 | }
18 | get chicagoImage() {
19 | return Ionic$.$('.chicago');
20 | }
21 | get seattleImage() {
22 | return Ionic$.$('.seattle');
23 | }
24 | get locationSelect() {
25 | return new IonicSelect('ion-select');
26 | }
27 | }
28 |
29 | export default new About();
30 |
--------------------------------------------------------------------------------
/tests/pageobjects/account.page.ts:
--------------------------------------------------------------------------------
1 | import { Ionic$, IonicItem } from '../helpers';
2 | import { IonicAlert } from '../helpers/ionic/components/alert';
3 | import Page from './page';
4 |
5 | class Account extends Page {
6 | get updatePictureButton() {
7 | return IonicItem.withTitle('Update Picture');
8 | }
9 | get changeUsernameButton() {
10 | return IonicItem.withTitle('Change Username');
11 | }
12 | get changePasswordButton() {
13 | return IonicItem.withTitle('Change Password');
14 | }
15 | get supportButton() {
16 | return IonicItem.withTitle('Support');
17 | }
18 | get logoutButton() {
19 | return IonicItem.withTitle('Logout');
20 | }
21 | get changeUsernameAlert() {
22 | return new IonicAlert();
23 | }
24 | get usernameLabel() {
25 | return Ionic$.$('h2');
26 | }
27 | }
28 |
29 | export default new Account();
30 |
--------------------------------------------------------------------------------
/tests/pageobjects/login.page.ts:
--------------------------------------------------------------------------------
1 | import { IonicButton, IonicInput } from '../helpers';
2 | import Page from './page';
3 |
4 | class Login extends Page {
5 | get username() {
6 | return new IonicInput('ion-input [name="username"]');
7 | }
8 | get password() {
9 | return new IonicInput('ion-input [name="password"]');
10 | }
11 | get loginButton() {
12 | return IonicButton.withTitle('Login');
13 | }
14 |
15 | async login(username: string, password: string) {
16 | await this.username.setValue(username);
17 | await this.password.setValue(password);
18 | return this.loginButton.tap();
19 | }
20 | }
21 |
22 | export default new Login();
23 |
--------------------------------------------------------------------------------
/tests/pageobjects/map.page.ts:
--------------------------------------------------------------------------------
1 | import { Ionic$ } from '../helpers';
2 | import Page from './page';
3 |
4 | class Map extends Page {
5 | get map() {
6 | return Ionic$.$('.map-canvas');
7 | }
8 | }
9 |
10 | export default new Map();
11 |
--------------------------------------------------------------------------------
/tests/pageobjects/page.ts:
--------------------------------------------------------------------------------
1 | export default class Page {}
2 |
--------------------------------------------------------------------------------
/tests/pageobjects/schedule-filter.page.ts:
--------------------------------------------------------------------------------
1 | import Page from './page';
2 |
3 | class ScheduleFilter extends Page {}
4 |
5 | export default new ScheduleFilter();
6 |
--------------------------------------------------------------------------------
/tests/pageobjects/schedule.page.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IonicButton,
3 | IonicContent,
4 | IonicMenu,
5 | IonicSegment,
6 | isIOS,
7 | } from '../helpers';
8 | import Page from './page';
9 |
10 | class Schedule extends Page {
11 | get menu() {
12 | return new IonicMenu();
13 | }
14 | get filterButton() {
15 | return new IonicButton(
16 | `ion-buttons[slot="end"] > ion-button:nth-child(${isIOS() ? 1 : 2})`
17 | );
18 | }
19 | get segment() {
20 | return new IonicSegment('ion-segment');
21 | }
22 | get content() {
23 | return new IonicContent('ion-content');
24 | }
25 |
26 | async openMenu() {
27 | return this.menu.open();
28 | }
29 | }
30 |
31 | export default new Schedule();
32 |
--------------------------------------------------------------------------------
/tests/pageobjects/session-detail.page.ts:
--------------------------------------------------------------------------------
1 | import { Ionic$, IonicButton } from '../helpers';
2 | import Page from './page';
3 |
4 | class SessionDetail extends Page {
5 | get sessionTitle() {
6 | return Ionic$.$('h1');
7 | }
8 | get backButton() {
9 | return new IonicButton('ion-back-button');
10 | }
11 | get favoriteButton() {
12 | return new IonicButton('ion-buttons > ion-button:nth-child(1)');
13 | }
14 | get shareButton() {
15 | return new IonicButton('ion-buttons > ion-button:nth-child(2)');
16 | }
17 | }
18 |
19 | export default new SessionDetail();
20 |
--------------------------------------------------------------------------------
/tests/pageobjects/signup.page.ts:
--------------------------------------------------------------------------------
1 | import { IonicButton, IonicInput } from '../helpers';
2 | import Page from './page';
3 |
4 | class Signup extends Page {
5 | get username() {
6 | return new IonicInput('ion-input [name="username"]');
7 | }
8 | get password() {
9 | return new IonicInput('ion-input [name="password"]');
10 | }
11 | get signupButton() {
12 | return IonicButton.withTitle('Create');
13 | }
14 |
15 | async signup(username: string, password: string) {
16 | await this.username.setValue(username);
17 | await this.password.setValue(password);
18 | return this.signupButton.tap();
19 | }
20 | }
21 |
22 | export default new Signup();
23 |
--------------------------------------------------------------------------------
/tests/pageobjects/speaker-detail.page.ts:
--------------------------------------------------------------------------------
1 | import { Ionic$ } from '../helpers';
2 | import Page from './page';
3 |
4 | class SpeakerDetail extends Page {
5 | get speakerName() {
6 | return Ionic$.$('h2');
7 | }
8 | }
9 |
10 | export default new SpeakerDetail();
11 |
--------------------------------------------------------------------------------
/tests/pageobjects/speaker-list.page.ts:
--------------------------------------------------------------------------------
1 | import { Ionic$ } from '../helpers';
2 | import Page from './page';
3 |
4 | class SpeakerList extends Page {
5 | get speakers() {
6 | return Ionic$.$$('ion-col');
7 | }
8 | }
9 |
10 | export default new SpeakerList();
11 |
--------------------------------------------------------------------------------
/tests/pageobjects/support.page.ts:
--------------------------------------------------------------------------------
1 | import { IonicButton, IonicTextarea, IonicToast } from '../helpers';
2 | import Page from './page';
3 |
4 | class Support extends Page {
5 | get messageInput() { return new IonicTextarea('ion-textarea[name="supportQuestion"]'); }
6 | get submitButton() { return IonicButton.withTitle('Submit'); }
7 | get toast() { return new IonicToast(); }
8 |
9 | submitMessage() {
10 | return this.submitButton.tap();
11 | }
12 | }
13 |
14 | export default new Support();
15 |
--------------------------------------------------------------------------------
/tests/pageobjects/tutorial.page.ts:
--------------------------------------------------------------------------------
1 | import { IonicButton, IonicSlides } from '../helpers';
2 | import Page from './page';
3 |
4 | class Tutorial extends Page {
5 | get slides() {
6 | return new IonicSlides('swiper');
7 | }
8 | get skipButton() {
9 | return IonicButton.withTitle('Skip');
10 | }
11 | get continueButton() {
12 | return IonicButton.withTitle('Continue');
13 | }
14 |
15 | async swipeLeft() {
16 | return this.slides.swipeLeft();
17 | }
18 |
19 | async swipeRight() {
20 | return this.slides.swipeRight();
21 | }
22 |
23 | async skip() {
24 | return this.skipButton.tap();
25 | }
26 |
27 | async continue() {
28 | return this.continueButton.tap();
29 | }
30 | }
31 |
32 | export default new Tutorial();
33 |
--------------------------------------------------------------------------------
/tests/specs/app.about.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, restartApp, url } from '../helpers';
2 |
3 | import About from '../pageobjects/about.page';
4 |
5 | describe('About', () => {
6 | beforeEach(async () => {
7 | await restartApp('/app/tabs/about');
8 | await clearIndexedDB('_ionicstorage');
9 | await url('/app/tabs/about');
10 | await pause(500);
11 | });
12 |
13 | it('Should switch location', async () => {
14 | const location = await About.locationSelect;
15 |
16 | await location.open();
17 | await location.select(1);
18 | await location.ok();
19 | const austinImage = await About.austinImage;
20 | await pause(500);
21 | await expect((await austinImage.getCSSProperty('opacity')).value).toEqual(
22 | 1
23 | );
24 |
25 | await location.open();
26 | await location.select(2);
27 | await location.ok();
28 | const chicagoImage = await About.chicagoImage;
29 | await pause(500);
30 | await expect((await chicagoImage.getCSSProperty('opacity')).value).toEqual(
31 | 1
32 | );
33 |
34 | await location.open();
35 | await location.select(3);
36 | await location.ok();
37 | const seattleImage = await About.seattleImage;
38 | await pause(500);
39 | await expect((await seattleImage.getCSSProperty('opacity')).value).toEqual(
40 | 1
41 | );
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/tests/specs/app.account.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, restartApp, url } from '../helpers';
2 |
3 | import Account from '../pageobjects/account.page';
4 | import Login from '../pageobjects/login.page';
5 |
6 | describe('Account', () => {
7 | beforeEach(async () => {
8 | await restartApp('/login');
9 | await clearIndexedDB('_ionicstorage');
10 | await url('/login');
11 | await pause(500);
12 | await Login.login('test', 'test');
13 | await pause(500);
14 | await url('/account');
15 | });
16 |
17 | it('Should open change username alert', async () => {
18 | await Account.changeUsernameButton.tap();
19 | await pause(500);
20 | const input = await Account.changeUsernameAlert.input;
21 | await input.setValue('newusername');
22 | const okButton = await Account.changeUsernameAlert.button('Ok');
23 | await okButton.click();
24 | await url('/login');
25 | await pause(500);
26 | await url('/account');
27 | const usernameLabel = await Account.usernameLabel;
28 | expect(await usernameLabel.getText()).toBe('newusername');
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/tests/specs/app.login.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, restartApp, url } from '../helpers';
2 |
3 | import Login from '../pageobjects/login.page';
4 |
5 | describe('Login', () => {
6 | beforeEach(async () => {
7 | await restartApp('/login');
8 | await clearIndexedDB('_ionicstorage');
9 | await url('/login');
10 | await pause(500);
11 | });
12 |
13 | it('Should login', async () => {
14 | await Login.login('test', 'test');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/tests/specs/app.map.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, restartApp, url } from '../helpers';
2 |
3 | import Map from '../pageobjects/map.page';
4 |
5 | describe('Map', () => {
6 | beforeEach(async () => {
7 | await restartApp('/app/tabs/map');
8 | await clearIndexedDB('_ionicstorage');
9 | await url('/app/tabs/map');
10 | await pause(500);
11 | });
12 |
13 | it('Should load map', async () => {
14 | await expect(Map.map).toBeDisplayed();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/tests/specs/app.schedule-filter.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, restartApp, url } from '../helpers';
2 |
3 | import schedulePage from '../pageobjects/schedule.page';
4 |
5 | describe('Schedule Filter', () => {
6 | beforeEach(async () => {
7 | await restartApp('/app/tabs/schedule');
8 | await clearIndexedDB('_ionicstorage');
9 | await url('/app/tabs/schedule');
10 | await pause(200);
11 | await schedulePage.filterButton.tap();
12 | await pause(400);
13 | });
14 |
15 | it('Should load filters', async () => {
16 | const items = await $$('page-schedule-filter ion-item');
17 | expect(items.length).toBe(10);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/tests/specs/app.schedule.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, restartApp, url } from '../helpers';
2 |
3 | import Schedule from '../pageobjects/schedule.page';
4 |
5 | describe('Schedule', () => {
6 | beforeEach(async () => {
7 | await restartApp('/app/tabs/schedule');
8 | await clearIndexedDB('_ionicstorage');
9 | await url('/app/tabs/schedule');
10 | await pause(500);
11 | });
12 |
13 | it('Should switch to favorites', async () => {
14 | const favoritesButton = await Schedule.segment.button('Favorites');
15 | if (favoritesButton) {
16 | await (await favoritesButton.$).waitForDisplayed();
17 | await favoritesButton.tap();
18 | } else {
19 | throw new Error('Favorites button not found');
20 | }
21 | const listHeader = await (await Schedule.content.$).$('ion-list-header');
22 | await listHeader.waitForDisplayed();
23 | await expect(listHeader).toHaveText('No Sessions Found');
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/tests/specs/app.session-detail.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, restartApp, url } from '../helpers';
2 |
3 | import Schedule from '../pageobjects/schedule.page';
4 | import SessionDetail from '../pageobjects/session-detail.page';
5 |
6 | describe('Session Detail', () => {
7 | beforeEach(async () => {
8 | await restartApp('/app/tabs/schedule/session/1');
9 | await clearIndexedDB('_ionicstorage');
10 | await url('/app/tabs/schedule/session/1');
11 | await pause(500);
12 | });
13 |
14 | it('Should load first session', async () => {
15 | const title = await SessionDetail.sessionTitle;
16 | await expect(await title.getText()).toBe('Breakfast');
17 | });
18 |
19 | it('Should favorite session', async () => {
20 | const button = await SessionDetail.favoriteButton;
21 | await button.tap();
22 | const backButton = await SessionDetail.backButton;
23 | await backButton.tap();
24 |
25 | const favoritesButton = await Schedule.segment.button('Favorites');
26 | if (favoritesButton) {
27 | await (await favoritesButton.$).waitForDisplayed();
28 | await favoritesButton.tap();
29 | } else {
30 | throw new Error('Favorites button not found');
31 | }
32 | const item = await (await Schedule.content.$).$('ion-item');
33 | const h3 = await item.$('h3');
34 | await expect(h3).toHaveText('Breakfast');
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/tests/specs/app.signup.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, getUrl, pause, restartApp, url } from '../helpers';
2 |
3 | import Signup from '../pageobjects/signup.page';
4 |
5 | describe('Signup', () => {
6 | beforeEach(async () => {
7 | await restartApp('/signup');
8 | await clearIndexedDB('_ionicstorage');
9 | await url('/signup');
10 | await pause(500);
11 | });
12 |
13 | it('Should signup', async () => {
14 | await Signup.signup('test', 'test');
15 | await pause(500);
16 | await expect((await getUrl()).pathname).toBe('/app/tabs/schedule');
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/tests/specs/app.speaker-detail.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, restartApp, url } from '../helpers';
2 |
3 | import SpeakerDetail from '../pageobjects/speaker-detail.page';
4 |
5 | describe('Speaker Detail', () => {
6 | beforeEach(async () => {
7 | await restartApp('/app/tabs/speakers/speaker-details/3');
8 | await clearIndexedDB('_ionicstorage');
9 | await url('/app/tabs/speakers/speaker-details/3');
10 | await pause(500);
11 | });
12 |
13 | it('Should load speaker', async () => {
14 | const speakerName = await SpeakerDetail.speakerName;
15 | await expect(await speakerName.getText()).toBe('Donald Duck');
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/tests/specs/app.speaker-list.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | clearIndexedDB,
3 | pause,
4 | restartApp,
5 | url,
6 | } from '../helpers';
7 |
8 | import SpeakerList from '../pageobjects/speaker-list.page';
9 |
10 | describe('Speaker List', () => {
11 | beforeEach(async () => {
12 | await restartApp('/app/tabs/speakers');
13 | await clearIndexedDB('_ionicstorage');
14 | await url('/app/tabs/speakers');
15 | await pause(500);
16 | });
17 |
18 | it('Should load speakers', async () => {
19 | const speakers = await SpeakerList.speakers;
20 |
21 | expect(speakers.length).toBe(13);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/tests/specs/app.support.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, restartApp, url } from '../helpers';
2 |
3 | import Support from '../pageobjects/support.page';
4 |
5 | describe('Support', () => {
6 | beforeEach(async () => {
7 | await restartApp('/support');
8 | await clearIndexedDB('_ionicstorage');
9 | await url('/support');
10 | await pause(500);
11 | });
12 |
13 | it('Should submit support request', async () => {
14 | await pause(5000);
15 | const input = Support.messageInput;
16 | await input.setValue('I am very happy with the app');
17 | await Support.submitMessage();
18 |
19 | const toast = await Support.toast;
20 | await expect(await toast.getText()).toBe(
21 | 'Your support request has been sent.'
22 | );
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/tests/specs/app.tutorial.spec.ts:
--------------------------------------------------------------------------------
1 | import { clearIndexedDB, pause, getUrl, restartApp, url } from '../helpers';
2 |
3 | import Tutorial from '../pageobjects/tutorial.page';
4 |
5 | describe('Tutorial', () => {
6 | beforeEach(async () => {
7 | await restartApp('/tutorial');
8 | await clearIndexedDB('_ionicstorage');
9 | await url('/tutorial');
10 | });
11 |
12 | it('Should load swiper', async () => {
13 | await expect(await Tutorial.slides.$).toBeDisplayed();
14 | });
15 |
16 | it('Should get to schedule', async () => {
17 | await Tutorial.slides.swipeLeft();
18 | await Tutorial.slides.swipeLeft();
19 | await Tutorial.slides.swipeLeft();
20 |
21 | await Tutorial.continue();
22 |
23 | await pause(1000);
24 |
25 | await expect((await getUrl()).pathname).toBe('/app/tabs/schedule');
26 | });
27 |
28 | it('Should skip to schedule', async () => {
29 | await Tutorial.skip();
30 |
31 | await pause(1000);
32 |
33 | await expect((await getUrl()).pathname).toBe('/app/tabs/schedule');
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../.tsbuild/",
5 | "sourceMap": false,
6 | "target": "es2019",
7 | "module": "commonjs",
8 | "removeComments": true,
9 | "noImplicitAny": true,
10 | "esModuleInterop": true,
11 | "strictPropertyInitialization": true,
12 | "strictNullChecks": true,
13 | "types": [
14 | "node",
15 | "webdriverio/async",
16 | "@wdio/mocha-framework",
17 | "expect-webdriverio"
18 | ]
19 | }
20 | }
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "src/main.ts",
9 | "src/polyfills.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "downlevelIteration": true,
4 | "importHelpers": true,
5 | "module": "es2020",
6 | "outDir": "./dist/out-tsc",
7 | "sourceMap": true,
8 | "declaration": false,
9 | "moduleResolution": "node",
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "target": "ES5",
13 | "skipLibCheck": true,
14 | "lib": [
15 | "es2017",
16 | "dom"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts",
12 | "src/zone-flags.ts",
13 | "src/polyfills.ts"
14 | ],
15 | "include": [
16 | "src/**/*.spec.ts",
17 | "src/**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rulesDirectory": [
4 | "codelyzer"
5 | ],
6 | "rules": {
7 | "align": {
8 | "options": [
9 | "parameters",
10 | "statements"
11 | ]
12 | },
13 | "array-type": false,
14 | "arrow-parens": false,
15 | "arrow-return-shorthand": true,
16 | "deprecation": {
17 | "severity": "warn"
18 | },
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "curly": true,
24 | "interface-name": false,
25 | "max-classes-per-file": false,
26 | "max-line-length": [
27 | true,
28 | 140
29 | ],
30 | "eofline": true,
31 | "member-access": false,
32 | "import-spacing": true,
33 | "indent": {
34 | "options": [
35 | "spaces"
36 | ]
37 | },
38 | "member-ordering": [
39 | true,
40 | {
41 | "order": [
42 | "static-field",
43 | "instance-field",
44 | "static-method",
45 | "instance-method"
46 | ]
47 | }
48 | ],
49 | "no-consecutive-blank-lines": false,
50 | "no-console": [
51 | true,
52 | "debug",
53 | "info",
54 | "time",
55 | "timeEnd",
56 | "trace"
57 | ],
58 | "no-empty": false,
59 | "no-inferrable-types": [
60 | true,
61 | "ignore-params"
62 | ],
63 | "no-non-null-assertion": true,
64 | "no-redundant-jsdoc": true,
65 | "no-switch-case-fall-through": true,
66 | "no-use-before-define": true,
67 | "no-var-requires": false,
68 | "object-literal-key-quotes": [
69 | true,
70 | "as-needed"
71 | ],
72 | "object-literal-sort-keys": false,
73 | "ordered-imports": false,
74 | "quotemark": [
75 | true,
76 | "single"
77 | ],
78 | "trailing-comma": false,
79 | "no-output-on-prefix": true,
80 | "no-inputs-metadata-property": true,
81 | "no-host-metadata-property": true,
82 | "no-input-rename": true,
83 | "no-output-rename": true,
84 | "use-lifecycle-interface": true,
85 | "use-pipe-transform-interface": true,
86 | "one-variable-per-declaration": false,
87 | "component-class-suffix": [
88 | true,
89 | "Page",
90 | "Component"
91 | ],
92 | "semicolon": {
93 | "options": [
94 | "always"
95 | ]
96 | },
97 | "space-before-function-paren": {
98 | "options": {
99 | "anonymous": "never",
100 | "asyncArrow": "always",
101 | "constructor": "never",
102 | "method": "never",
103 | "named": "never"
104 | }
105 | },
106 | "directive-class-suffix": true,
107 | "typedef-whitespace": {
108 | "options": [
109 | {
110 | "call-signature": "nospace",
111 | "index-signature": "nospace",
112 | "parameter": "nospace",
113 | "property-declaration": "nospace",
114 | "variable-declaration": "nospace"
115 | },
116 | {
117 | "call-signature": "onespace",
118 | "index-signature": "onespace",
119 | "parameter": "onespace",
120 | "property-declaration": "onespace",
121 | "variable-declaration": "onespace"
122 | }
123 | ]
124 | },
125 | "directive-selector": [
126 | true,
127 | "attribute",
128 | "app",
129 | "camelCase"
130 | ],
131 | "component-selector": [
132 | true,
133 | "element",
134 | "app",
135 | "page",
136 | "kebab-case"
137 | ],
138 | "variable-name": {
139 | "options": [
140 | "ban-keywords",
141 | "check-format",
142 | "allow-pascal-case"
143 | ]
144 | },
145 | "whitespace": {
146 | "options": [
147 | "check-branch",
148 | "check-decl",
149 | "check-operator",
150 | "check-separator",
151 | "check-type",
152 | "check-typecast"
153 | ]
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------