mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 10:41:13 +08:00
chore(): resolve merge conflicts
This commit is contained in:
@ -458,7 +458,7 @@ jobs:
|
|||||||
command: npm install --legacy-peer-deps
|
command: npm install --legacy-peer-deps
|
||||||
working_directory: /tmp/workspace/angular/test/test-app
|
working_directory: /tmp/workspace/angular/test/test-app
|
||||||
- run:
|
- run:
|
||||||
command: npm run test -- --protractor-config=e2e/protractor-ci.conf.js
|
command: npm run test
|
||||||
working_directory: /tmp/workspace/angular/test/test-app
|
working_directory: /tmp/workspace/angular/test/test-app
|
||||||
|
|
||||||
install-vue-test-app:
|
install-vue-test-app:
|
||||||
|
30
CHANGELOG.md
30
CHANGELOG.md
@ -1,3 +1,33 @@
|
|||||||
|
## [5.6.5](https://github.com/ionic-team/ionic/compare/v5.6.4...v5.6.5) (2021-04-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **content:** only render a main element when content is being used in primary view ([#23160](https://github.com/ionic-team/ionic/issues/23160)) ([2d07d82](https://github.com/ionic-team/ionic/commit/2d07d8216af908b181c5e7e438e79a049bb6d8c2))
|
||||||
|
* **datetime, input, textarea:** only add aria-labelledby if there is an adjacent label ([#23211](https://github.com/ionic-team/ionic/issues/23211)) ([a31fb55](https://github.com/ionic-team/ionic/commit/a31fb55bac1ef03e014f3d7f6c22c24eff20feb5))
|
||||||
|
* **radio-group:** pressing spacebar correctly unselects radio with allow-empty-selection ([#23194](https://github.com/ionic-team/ionic/issues/23194)) ([7139b3f](https://github.com/ionic-team/ionic/commit/7139b3f39e8eeef07ff7c595940fc5dafe062956)), closes [#22734](https://github.com/ionic-team/ionic/issues/22734)
|
||||||
|
* **react:** callback refs now work correctly with ionic components ([#23152](https://github.com/ionic-team/ionic/issues/23152)) ([0dd189e](https://github.com/ionic-team/ionic/commit/0dd189e2c05012659894a4c15cd3a9d407fe0a63)), closes [#23153](https://github.com/ionic-team/ionic/issues/23153)
|
||||||
|
* **segment, segment-button:** use tablist and tab roles ([#23145](https://github.com/ionic-team/ionic/issues/23145)) ([91ac340](https://github.com/ionic-team/ionic/commit/91ac340ae7e8928f7b0972a093dd9dd7fa727671))
|
||||||
|
* **vue:** dynamic tabs are now correctly recognized ([#23212](https://github.com/ionic-team/ionic/issues/23212)) ([004885b](https://github.com/ionic-team/ionic/commit/004885bfd4446487e6386876c868532a2795347f)), closes [#22847](https://github.com/ionic-team/ionic/issues/22847)
|
||||||
|
* **vue:** update props when navigating to new parameterized route ([#23189](https://github.com/ionic-team/ionic/issues/23189)) ([35c8802](https://github.com/ionic-team/ionic/commit/35c8802c22c1f4bf213a01e1c28398ad62d1b7ac))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.6.4](https://github.com/ionic-team/ionic/compare/v5.6.3...v5.6.4) (2021-04-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **angular:** swiping back quickly no longer causes app to get stuck ([#23125](https://github.com/ionic-team/ionic/issues/23125)) ([28c52fd](https://github.com/ionic-team/ionic/commit/28c52fd4e3df3d96b4ec83075a322e110e938a1a)), closes [#15154](https://github.com/ionic-team/ionic/issues/15154)
|
||||||
|
* **input:** inherit aria-label to input ([#23159](https://github.com/ionic-team/ionic/issues/23159)) ([61f094d](https://github.com/ionic-team/ionic/commit/61f094d30665c9afec428028883a5d9a085892d8))
|
||||||
|
* **react:** overlays now correctly unmount any child components after dismissing ([#23149](https://github.com/ionic-team/ionic/issues/23149)) ([dee6eb3](https://github.com/ionic-team/ionic/commit/dee6eb30df370047bbc872b00ab6d801dd11fa81)), closes [#23140](https://github.com/ionic-team/ionic/issues/23140)
|
||||||
|
* **react, vue:** correct view now chosen when going back inside tabs ([#23154](https://github.com/ionic-team/ionic/issues/23154)) ([7203190](https://github.com/ionic-team/ionic/commit/72031902347dc279045e2e099f69852a23dd8436)), closes [#23087](https://github.com/ionic-team/ionic/issues/23087) [#23101](https://github.com/ionic-team/ionic/issues/23101)
|
||||||
|
* **toggle:** prevent click event from firing twice ([#23146](https://github.com/ionic-team/ionic/issues/23146)) ([42e6c90](https://github.com/ionic-team/ionic/commit/42e6c90c4632423386b165dddc4b94a55c075e2e)), closes [#23041](https://github.com/ionic-team/ionic/issues/23041)
|
||||||
|
* **vue:** account for event name changes in vue 3.0.6+ for overlay components ([#23100](https://github.com/ionic-team/ionic/issues/23100)) ([27318cf](https://github.com/ionic-team/ionic/commit/27318cf58563c4b38d0b7045fb61451f45954a8f))
|
||||||
|
* **vue:** components now integrate properly with vee-validate ([#23114](https://github.com/ionic-team/ionic/issues/23114)) ([ba51daf](https://github.com/ionic-team/ionic/commit/ba51daf17c4438aea6826882f82a04ebf8d6a5d8)), closes [#22886](https://github.com/ionic-team/ionic/issues/22886)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.6.3](https://github.com/ionic-team/ionic/compare/v5.6.2...v5.6.3) (2021-03-23)
|
## [5.6.3](https://github.com/ionic-team/ionic/compare/v5.6.2...v5.6.3) (2021-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
34
angular/package-lock.json
generated
34
angular/package-lock.json
generated
@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@ionic/angular",
|
"name": "@ionic/angular",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@ionic/angular",
|
"name": "@ionic/angular",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ionic/core": "5.6.2",
|
"@ionic/core": "5.6.4",
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -204,15 +204,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ionic/core": {
|
"node_modules/@ionic/core": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.6.4.tgz",
|
||||||
"integrity": "sha512-hnwd6ln0IZUVfFu2ilZK03b6EdQFqEWiTkL5kayq2gjB3BK/u1IEtV3C9fdwc8NJKopGwdbdQnujj6VhYPzV3Q==",
|
"integrity": "sha512-fxCV/+0ibiaiBn1dsrrOmlLGJlTkqiG6IVXdLpPKimGdFLjy56olDvB5trlz9J5C/nHc7vR5MIiYC0qdTyX7og==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stencil/core": "^2.4.0",
|
"@stencil/core": "^2.4.0",
|
||||||
"ionicons": "^5.5.1",
|
"ionicons": "^5.5.1",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ionic/core/node_modules/tslib": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||||
|
},
|
||||||
"node_modules/@rollup/plugin-commonjs": {
|
"node_modules/@rollup/plugin-commonjs": {
|
||||||
"version": "11.1.0",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.1.0.tgz",
|
||||||
@ -5151,13 +5156,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ionic/core": {
|
"@ionic/core": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.6.4.tgz",
|
||||||
"integrity": "sha512-hnwd6ln0IZUVfFu2ilZK03b6EdQFqEWiTkL5kayq2gjB3BK/u1IEtV3C9fdwc8NJKopGwdbdQnujj6VhYPzV3Q==",
|
"integrity": "sha512-fxCV/+0ibiaiBn1dsrrOmlLGJlTkqiG6IVXdLpPKimGdFLjy56olDvB5trlz9J5C/nHc7vR5MIiYC0qdTyX7og==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@stencil/core": "^2.4.0",
|
"@stencil/core": "^2.4.0",
|
||||||
"ionicons": "^5.5.1",
|
"ionicons": "^5.5.1",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^2.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@rollup/plugin-commonjs": {
|
"@rollup/plugin-commonjs": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ionic/angular",
|
"name": "@ionic/angular",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"description": "Angular specific wrappers for @ionic/core",
|
"description": "Angular specific wrappers for @ionic/core",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ionic",
|
"ionic",
|
||||||
@ -42,7 +42,7 @@
|
|||||||
"validate": "npm i && npm run lint && npm run test && npm run build"
|
"validate": "npm i && npm run lint && npm run test && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ionic/core": "5.6.3",
|
"@ionic/core": "5.6.5",
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -85,33 +85,6 @@
|
|||||||
"browserTarget": "test-app:build"
|
"browserTarget": "test-app:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
|
||||||
"options": {
|
|
||||||
"main": "src/test.ts",
|
|
||||||
"polyfills": "src/polyfills.ts",
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
|
||||||
"karmaConfig": "karma.conf.js",
|
|
||||||
"styles": ["src/styles.css"],
|
|
||||||
"scripts": [],
|
|
||||||
"assets": ["src/favicon.ico", "src/assets"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"e2e": {
|
|
||||||
"builder": "@angular-devkit/build-angular:protractor",
|
|
||||||
"options": {
|
|
||||||
"protractorConfig": "e2e/protractor.conf.js",
|
|
||||||
"devServerTarget": "test-app:serve"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"devServerTarget": "test-app:serve:production"
|
|
||||||
},
|
|
||||||
"ci": {
|
|
||||||
"devServerTarget": "test-app:serve:ci"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
"lint": {
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
"options": {
|
"options": {
|
||||||
|
8
angular/test/test-app/cypress.json
Normal file
8
angular/test/test-app/cypress.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"integrationFolder": "./e2e",
|
||||||
|
"testFiles": "**/*.spec.ts",
|
||||||
|
"baseUrl": "http://localhost:4200/",
|
||||||
|
"ignoreTestFiles": "**/examples/*",
|
||||||
|
"video": false,
|
||||||
|
"screenshotOnRunFailure": false
|
||||||
|
}
|
22
angular/test/test-app/cypress/plugins/index.js
vendored
Normal file
22
angular/test/test-app/cypress/plugins/index.js
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
}
|
79
angular/test/test-app/cypress/support/commands.js
vendored
Normal file
79
angular/test/test-app/cypress/support/commands.js
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
|
Cypress.Commands.add('ionSwipeToGoBack', (complete = false, selector = 'ion-router-outlet') => {
|
||||||
|
const increment = (complete) ? 60 : 25;
|
||||||
|
cy.get(selector)
|
||||||
|
.first()
|
||||||
|
.trigger('mousedown', 0, 275, { which: 1, force: true })
|
||||||
|
.trigger('mousemove', increment * 1, 275, { which: 1, force: true })
|
||||||
|
.wait(50)
|
||||||
|
.trigger('mousemove', increment * 2, 275, { which: 1, force: true })
|
||||||
|
.wait(50)
|
||||||
|
.trigger('mousemove', increment * 3, 275, { which: 1, force: true })
|
||||||
|
.wait(50)
|
||||||
|
.trigger('mousemove', increment * 4, 275, { which: 1, force: true })
|
||||||
|
.wait(50)
|
||||||
|
.trigger('mouseup', increment * 4, 275, { which: 1, force: true })
|
||||||
|
cy.wait(150);
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('testStack', (selector, expected) => {
|
||||||
|
cy.document().then((doc) => {
|
||||||
|
const children = Array.from(
|
||||||
|
doc.querySelector(selector).children
|
||||||
|
).map(el => el.tagName.toLowerCase());
|
||||||
|
expect(children).to.deep.equal(expected);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('testLifeCycle', (selector, expected) => {
|
||||||
|
cy.get(`${selector} #ngOnInit`).invoke('text').should('equal', '1');
|
||||||
|
cy.get(`${selector} #ionViewWillEnter`).invoke('text').should('equal', expected.ionViewWillEnter.toString());
|
||||||
|
cy.get(`${selector} #ionViewDidEnter`).invoke('text').should('equal', expected.ionViewDidEnter.toString());
|
||||||
|
cy.get(`${selector} #ionViewWillLeave`).invoke('text').should('equal', expected.ionViewWillLeave.toString());
|
||||||
|
cy.get(`${selector} #ionViewDidLeave`).invoke('text').should('equal', expected.ionViewDidLeave.toString());
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('ionPageVisible', (selector) => {
|
||||||
|
cy.get(selector)
|
||||||
|
.should('have.class', 'ion-page')
|
||||||
|
.should('not.have.class', 'ion-page-hidden')
|
||||||
|
.should('not.have.class', 'ion-page-invisible')
|
||||||
|
.should('have.length', 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('ionPageHidden', (selector) => {
|
||||||
|
cy.get(selector)
|
||||||
|
.should('have.class', 'ion-page')
|
||||||
|
.should('have.class', 'ion-page-hidden')
|
||||||
|
.should('have.length', 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('ionPageDoesNotExist', (selector) => {
|
||||||
|
cy.get(selector)
|
||||||
|
.should('not.exist')
|
||||||
|
});
|
69
angular/test/test-app/cypress/support/index.d.ts
vendored
Normal file
69
angular/test/test-app/cypress/support/index.d.ts
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
declare namespace Cypress {
|
||||||
|
interface Chainable<Subject> {
|
||||||
|
/**
|
||||||
|
* Swipe to go back on the current selector or router outlet
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* cy.ionSwipeToGoBack();
|
||||||
|
* cy.ionSwipeToGoBack(true);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
ionSwipeToGoBack(complete: boolean, selector: string): Chainable<any>
|
||||||
|
/**
|
||||||
|
* Test that the proper pages are in the navigation stack
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* cy.testStack('ion-router-outlet', ['app-navigation-page2', 'app-navigation-page1']);
|
||||||
|
* cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
testStack(selector: string, expected: string[]): Chainable<any>
|
||||||
|
/**
|
||||||
|
* Test whether or not the lifecycle events fired
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* cy.testLifeCycle('app-router-link-page', {
|
||||||
|
* ionViewWillEnter: 1,
|
||||||
|
* ionViewDidEnter: 1,
|
||||||
|
* ionViewWillLeave: 0,
|
||||||
|
* ionViewDidLeave: 0,
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
testLifeCycle(selector: string, expected: any): Chainable<any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether or not an .ion-page element is visible.
|
||||||
|
* Use this to test a page after navigating to it.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* cy.ionPageVisible('app-my-page');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
ionPageVisible(selector: string): Chainable<any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether or not an .ion-page element is hidden
|
||||||
|
* Use this to test a page after navigating away from it.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* cy.ionPageHidden('app-my-page');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
ionPageHidden(selector: string): Chainable<any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether or not an .ion-page element exists.
|
||||||
|
* Use this to test a page after popping it off the stack.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* cy.ionPageDoesNotExist('app-my-page');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
ionPageDoesNotExist(selector: string): Chainable<any>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
20
angular/test/test-app/cypress/support/index.js
vendored
Normal file
20
angular/test/test-app/cypress/support/index.js
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands'
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
// Protractor CI configuration file, see link for more information
|
|
||||||
// https://angular.io/guide/testing#configure-cli-for-ci-testing-in-chrome
|
|
||||||
|
|
||||||
const config = require('./protractor.conf').config;
|
|
||||||
|
|
||||||
config.capabilities = {
|
|
||||||
browserName: 'chrome',
|
|
||||||
chromeOptions: {
|
|
||||||
args: ['--headless', '--no-sandbox', '--window-size=1920,1080']
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.config = config;
|
|
@ -1,35 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
// Protractor configuration file, see link for more information
|
|
||||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
|
||||||
|
|
||||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type { import("protractor").Config }
|
|
||||||
*/
|
|
||||||
exports.config = {
|
|
||||||
allScriptsTimeout: 11000,
|
|
||||||
specs: [
|
|
||||||
'./src/**/*.e2e-spec.ts'
|
|
||||||
],
|
|
||||||
capabilities: {
|
|
||||||
'browserName': 'chrome'
|
|
||||||
},
|
|
||||||
chromeOptions: {
|
|
||||||
args: [ "--headless", "--disable-gpu", "--window-size=400,1000", "--start-maximized" ]
|
|
||||||
},
|
|
||||||
directConnect: true,
|
|
||||||
baseUrl: 'http://localhost:4200/',
|
|
||||||
framework: 'jasmine',
|
|
||||||
jasmineNodeOpts: {
|
|
||||||
showColors: true,
|
|
||||||
defaultTimeoutInterval: 100000,
|
|
||||||
print: function() {}
|
|
||||||
},
|
|
||||||
onPrepare() {
|
|
||||||
require('ts-node').register({
|
|
||||||
project: require('path').join(__dirname, './tsconfig.json')
|
|
||||||
});
|
|
||||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,133 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
import { handleErrorMessages, getProperty, setProperty, getText, waitTime } from './utils';
|
|
||||||
|
|
||||||
describe('form', () => {
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('status updates', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/form');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update Ionic form classes when calling form methods programatically', async () => {
|
|
||||||
await element(by.css('form #input-touched')).click();
|
|
||||||
await waitTime(100);
|
|
||||||
const classList = (await getProperty('#touched-input-test', 'classList')) as string[];
|
|
||||||
expect(classList.includes('ion-touched')).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('change', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/form');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have default values', async () => {
|
|
||||||
await testStatus('INVALID');
|
|
||||||
expect(await getText('#submit')).toEqual('false');
|
|
||||||
await testData({
|
|
||||||
datetime: '2010-08-20',
|
|
||||||
select: null,
|
|
||||||
toggle: false,
|
|
||||||
input: '',
|
|
||||||
input2: 'Default Value',
|
|
||||||
checkbox: false,
|
|
||||||
range: 5
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should become valid', async () => {
|
|
||||||
await setProperty('ion-input.required', 'value', 'Some value');
|
|
||||||
await testStatus('INVALID');
|
|
||||||
await setProperty('ion-select', 'value', 'nes');
|
|
||||||
await testStatus('INVALID');
|
|
||||||
await setProperty('ion-range', 'value', 40);
|
|
||||||
await testStatus('VALID');
|
|
||||||
await testData({
|
|
||||||
datetime: '2010-08-20',
|
|
||||||
select: 'nes',
|
|
||||||
toggle: false,
|
|
||||||
input: 'Some value',
|
|
||||||
input2: 'Default Value',
|
|
||||||
checkbox: false,
|
|
||||||
range: 40
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ion-toggle should change', async () => {
|
|
||||||
await element(by.css('form ion-toggle')).click();
|
|
||||||
await testData({
|
|
||||||
datetime: '2010-08-20',
|
|
||||||
select: null,
|
|
||||||
toggle: true,
|
|
||||||
input: '',
|
|
||||||
input2: 'Default Value',
|
|
||||||
checkbox: false,
|
|
||||||
range: 5
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ion-checkbox should change', async () => {
|
|
||||||
await element(by.css('ion-checkbox')).click();
|
|
||||||
await testData({
|
|
||||||
datetime: '2010-08-20',
|
|
||||||
select: null,
|
|
||||||
toggle: false,
|
|
||||||
input: '',
|
|
||||||
input2: 'Default Value',
|
|
||||||
checkbox: true,
|
|
||||||
range: 5
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should submit', async () => {
|
|
||||||
await element(by.css('#set-values')).click();
|
|
||||||
await waitTime(100);
|
|
||||||
await element(by.css('#submit-button')).click();
|
|
||||||
expect(await getText('#submit')).toEqual('true');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('blur', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/form#blur');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ion-toggle should change only after blur', async () => {
|
|
||||||
await element(by.css('form ion-toggle')).click();
|
|
||||||
await testData({
|
|
||||||
datetime: '2010-08-20',
|
|
||||||
select: null,
|
|
||||||
toggle: false,
|
|
||||||
input: '',
|
|
||||||
input2: 'Default Value',
|
|
||||||
checkbox: false,
|
|
||||||
range: 5
|
|
||||||
});
|
|
||||||
await element(by.css('ion-checkbox')).click();
|
|
||||||
await testData({
|
|
||||||
datetime: '2010-08-20',
|
|
||||||
select: null,
|
|
||||||
toggle: true,
|
|
||||||
input: '',
|
|
||||||
input2: 'Default Value',
|
|
||||||
checkbox: false,
|
|
||||||
range: 5
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function testStatus(status: string) {
|
|
||||||
expect(await element(by.css('#status')).getText()).toEqual(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testData(data: any) {
|
|
||||||
expect(JSON.parse(await element(by.css('#data')).getText())).toEqual(data);
|
|
||||||
}
|
|
118
angular/test/test-app/e2e/src/form.spec.ts
Normal file
118
angular/test/test-app/e2e/src/form.spec.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
describe('Form', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/form');
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('status updates', () => {
|
||||||
|
it('should update Ionic form classes when calling form methods programmatically', async () => {
|
||||||
|
cy.get('#input-touched').click();
|
||||||
|
cy.get('#touched-input-test').should('have.class', 'ion-touched');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change', () => {
|
||||||
|
it('should have default values', () => {
|
||||||
|
testStatus('INVALID');
|
||||||
|
cy.get('#submit').should('have.text', 'false');
|
||||||
|
testData({
|
||||||
|
datetime: '2010-08-20',
|
||||||
|
select: null,
|
||||||
|
toggle: false,
|
||||||
|
input: '',
|
||||||
|
input2: 'Default Value',
|
||||||
|
checkbox: false,
|
||||||
|
range: 5
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should become valid', () => {
|
||||||
|
cy.get('ion-input.required').invoke('prop', 'value', 'Some value');
|
||||||
|
testStatus('INVALID');
|
||||||
|
|
||||||
|
cy.get('ion-select').invoke('prop', 'value', 'nes');
|
||||||
|
testStatus('INVALID');
|
||||||
|
|
||||||
|
cy.get('ion-range').invoke('prop', 'value', 40);
|
||||||
|
testStatus('VALID');
|
||||||
|
|
||||||
|
testData({
|
||||||
|
datetime: '2010-08-20',
|
||||||
|
select: 'nes',
|
||||||
|
toggle: false,
|
||||||
|
input: 'Some value',
|
||||||
|
input2: 'Default Value',
|
||||||
|
checkbox: false,
|
||||||
|
range: 40
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ion-toggle should change', () => {
|
||||||
|
cy.get('form ion-toggle').click();
|
||||||
|
testData({
|
||||||
|
datetime: '2010-08-20',
|
||||||
|
select: null,
|
||||||
|
toggle: true,
|
||||||
|
input: '',
|
||||||
|
input2: 'Default Value',
|
||||||
|
checkbox: false,
|
||||||
|
range: 5
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ion-checkbox should change', () => {
|
||||||
|
cy.get('ion-checkbox').click();
|
||||||
|
testData({
|
||||||
|
datetime: '2010-08-20',
|
||||||
|
select: null,
|
||||||
|
toggle: false,
|
||||||
|
input: '',
|
||||||
|
input2: 'Default Value',
|
||||||
|
checkbox: true,
|
||||||
|
range: 5
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should submit', () => {
|
||||||
|
cy.get('#set-values').click();
|
||||||
|
cy.get('#submit-button').click();
|
||||||
|
cy.get('#submit').should('have.text', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('blur', () => {
|
||||||
|
it('ion-toggle should change only after blur', () => {
|
||||||
|
cy.get('form ion-toggle').click();
|
||||||
|
testData({
|
||||||
|
datetime: '2010-08-20',
|
||||||
|
select: null,
|
||||||
|
toggle: true,
|
||||||
|
input: '',
|
||||||
|
input2: 'Default Value',
|
||||||
|
checkbox: false,
|
||||||
|
range: 5
|
||||||
|
});
|
||||||
|
cy.get('ion-checkbox').click();
|
||||||
|
testData({
|
||||||
|
datetime: '2010-08-20',
|
||||||
|
select: null,
|
||||||
|
toggle: true,
|
||||||
|
input: '',
|
||||||
|
input2: 'Default Value',
|
||||||
|
checkbox: true,
|
||||||
|
range: 5
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function testStatus(status) {
|
||||||
|
cy.get('#status').should('have.text', status);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testData(data) {
|
||||||
|
cy.get('#data').invoke('text').then(text => {
|
||||||
|
const value = JSON.parse(text);
|
||||||
|
console.log(value, data);
|
||||||
|
expect(value).to.deep.equal(data);
|
||||||
|
})
|
||||||
|
}
|
@ -1,69 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
import { getProperty, setProperty, handleErrorMessages, waitTime } from './utils';
|
|
||||||
|
|
||||||
describe('inputs', () => {
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/inputs');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have default value', async () => {
|
|
||||||
expect(await getProperty('ion-checkbox', 'checked')).toEqual(true);
|
|
||||||
expect(await getProperty('ion-toggle', 'checked')).toEqual(true);
|
|
||||||
expect(await getProperty('ion-input', 'value')).toEqual('some text');
|
|
||||||
expect(await getProperty('ion-datetime', 'value')).toEqual('1994-03-15');
|
|
||||||
expect(await getProperty('ion-select', 'value')).toEqual('nes');
|
|
||||||
expect(await getProperty('ion-range', 'value')).toEqual(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have reset value', async () => {
|
|
||||||
await element(by.css('#reset-button')).click();
|
|
||||||
|
|
||||||
expect(await getProperty('ion-checkbox', 'checked')).toEqual(false);
|
|
||||||
expect(await getProperty('ion-toggle', 'checked')).toEqual(false);
|
|
||||||
expect(await getProperty('ion-input', 'value')).toEqual('');
|
|
||||||
expect(await getProperty('ion-datetime', 'value')).toEqual('');
|
|
||||||
expect(await getProperty('ion-select', 'value')).toEqual('');
|
|
||||||
expect(await getProperty('ion-range', 'value')).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get some value', async () => {
|
|
||||||
await element(by.css('#reset-button')).click();
|
|
||||||
await element(by.css('#set-button')).click();
|
|
||||||
|
|
||||||
expect(await getProperty('ion-checkbox', 'checked')).toEqual(true);
|
|
||||||
expect(await getProperty('ion-toggle', 'checked')).toEqual(true);
|
|
||||||
expect(await getProperty('ion-input', 'value')).toEqual('some text');
|
|
||||||
expect(await getProperty('ion-datetime', 'value')).toEqual('1994-03-15');
|
|
||||||
expect(await getProperty('ion-select', 'value')).toEqual('nes');
|
|
||||||
expect(await getProperty('ion-range', 'value')).toEqual(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('change values should update angular', async () => {
|
|
||||||
await element(by.css('#reset-button')).click();
|
|
||||||
|
|
||||||
await setProperty('ion-checkbox', 'checked', true);
|
|
||||||
await setProperty('ion-toggle', 'checked', true);
|
|
||||||
await setProperty('ion-input', 'value', 'hola');
|
|
||||||
await setProperty('ion-datetime', 'value', '1996-03-15');
|
|
||||||
await setProperty('ion-select', 'value', 'playstation');
|
|
||||||
await setProperty('ion-range', 'value', 20);
|
|
||||||
|
|
||||||
expect(await element(by.css('#checkbox-note')).getText()).toEqual('true');
|
|
||||||
expect(await element(by.css('#toggle-note')).getText()).toEqual('true');
|
|
||||||
expect(await element(by.css('#input-note')).getText()).toEqual('hola');
|
|
||||||
expect(await element(by.css('#datetime-note')).getText()).toEqual('1996-03-15');
|
|
||||||
expect(await element(by.css('#select-note')).getText()).toEqual('playstation');
|
|
||||||
expect(await element(by.css('#range-note')).getText()).toEqual('20');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('nested components should not interfere with NgModel', async () => {
|
|
||||||
expect(await element(by.css('#range-note')).getText()).toEqual('10');
|
|
||||||
await element(by.css('#nested-toggle')).click();
|
|
||||||
expect(await element(by.css('#range-note')).getText()).toEqual('10');
|
|
||||||
});
|
|
||||||
});
|
|
61
angular/test/test-app/e2e/src/inputs.spec.ts
Normal file
61
angular/test/test-app/e2e/src/inputs.spec.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
describe('Inputs', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/inputs');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have default value', () => {
|
||||||
|
cy.get('ion-checkbox').should('have.prop', 'checked').and('equal', true);
|
||||||
|
cy.get('ion-toggle').should('have.prop', 'checked').and('equal', true);
|
||||||
|
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some text');
|
||||||
|
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
|
||||||
|
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
|
||||||
|
cy.get('ion-range').should('have.prop', 'value').and('equal', 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have reset value', () => {
|
||||||
|
cy.get('#reset-button').click();
|
||||||
|
|
||||||
|
cy.get('ion-checkbox').should('have.prop', 'checked').and('equal', false);
|
||||||
|
cy.get('ion-toggle').should('have.prop', 'checked').and('equal', false);
|
||||||
|
cy.get('ion-input').should('have.prop', 'value').and('equal', '');
|
||||||
|
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '');
|
||||||
|
cy.get('ion-select').should('have.prop', 'value').and('equal', '');
|
||||||
|
cy.get('ion-range').should('have.prop', 'value').and('be.NaN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get some value', () => {
|
||||||
|
cy.get('#reset-button').click();
|
||||||
|
cy.get('#set-button').click();
|
||||||
|
|
||||||
|
cy.get('ion-checkbox').should('have.prop', 'checked').and('equal', true);
|
||||||
|
cy.get('ion-toggle').should('have.prop', 'checked').and('equal', true);
|
||||||
|
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some text');
|
||||||
|
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
|
||||||
|
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
|
||||||
|
cy.get('ion-range').should('have.prop', 'value').and('equal', 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('change values should update angular', () => {
|
||||||
|
cy.get('#reset-button').click();
|
||||||
|
|
||||||
|
cy.get('ion-checkbox').invoke('prop', 'checked', true);
|
||||||
|
cy.get('ion-toggle').invoke('prop', 'checked', true);
|
||||||
|
cy.get('ion-input').invoke('prop', 'value', 'hola');
|
||||||
|
cy.get('ion-datetime').invoke('prop', 'value', '1996-03-15');
|
||||||
|
cy.get('ion-select').invoke('prop', 'value', 'playstation');
|
||||||
|
cy.get('ion-range').invoke('prop', 'value', 20);
|
||||||
|
|
||||||
|
cy.get('#checkbox-note').should('have.text', 'true');
|
||||||
|
cy.get('#toggle-note').should('have.text', 'true');
|
||||||
|
cy.get('#input-note').should('have.text', 'hola');
|
||||||
|
cy.get('#datetime-note').should('have.text', '1996-03-15');
|
||||||
|
cy.get('#select-note').should('have.text', 'playstation');
|
||||||
|
cy.get('#range-note').should('have.text', '20');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nested components should not interfere with NgModel', () => {
|
||||||
|
cy.get('#range-note').should('have.text', '10');
|
||||||
|
cy.get('#nested-toggle').click();
|
||||||
|
cy.get('#range-note').should('have.text', '10');
|
||||||
|
});
|
||||||
|
})
|
@ -1,55 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
import { waitTime, getText, handleErrorMessages } from './utils';
|
|
||||||
|
|
||||||
describe('modals', () => {
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/modals');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should open standalone modal and close', async () => {
|
|
||||||
await element(by.css('#action-button')).click();
|
|
||||||
|
|
||||||
await waitTime(800);
|
|
||||||
|
|
||||||
const modal = element(by.css('app-modal-example'));
|
|
||||||
expect(await modal.$('h2').getText()).toEqual('123');
|
|
||||||
expect(await modal.$('h3').getText()).toEqual('321');
|
|
||||||
|
|
||||||
expect(await getText('#onWillDismiss')).toEqual('false');
|
|
||||||
expect(await getText('#onDidDismiss')).toEqual('false');
|
|
||||||
|
|
||||||
await modal.$('#close-modal').click();
|
|
||||||
await waitTime(800);
|
|
||||||
|
|
||||||
expect(await getText('#onWillDismiss')).toEqual('true');
|
|
||||||
expect(await getText('#onDidDismiss')).toEqual('true');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should open nav modal and close', async () => {
|
|
||||||
await element(by.css('#action-button-2')).click();
|
|
||||||
|
|
||||||
await waitTime(800);
|
|
||||||
|
|
||||||
let page = element(by.css('ion-nav > *:last-child'));
|
|
||||||
expect(await page.$('h2').getText()).toEqual('123');
|
|
||||||
expect(await page.$('h3').getText()).toEqual('321');
|
|
||||||
|
|
||||||
await page.$('.push-page').click();
|
|
||||||
await waitTime(800);
|
|
||||||
|
|
||||||
page = element(by.css('ion-nav > *:last-child'));
|
|
||||||
expect(await page.$('h2').getText()).toEqual('pushed!');
|
|
||||||
expect(await page.$('h3').getText()).toEqual('');
|
|
||||||
|
|
||||||
await page.$('.pop-page').click();
|
|
||||||
await waitTime(800);
|
|
||||||
|
|
||||||
page = element(by.css('ion-nav > *:last-child'));
|
|
||||||
expect(await page.$('h2').getText()).toEqual('123');
|
|
||||||
});
|
|
||||||
});
|
|
43
angular/test/test-app/e2e/src/modal.spec.ts
Normal file
43
angular/test/test-app/e2e/src/modal.spec.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
describe('Modals', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/modals');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open standalone modal and close', () => {
|
||||||
|
cy.get('#action-button').click();
|
||||||
|
|
||||||
|
cy.get('ion-modal').should('exist').should('be.visible');
|
||||||
|
|
||||||
|
cy.get('app-modal-example h2').should('have.text', '123');
|
||||||
|
cy.get('app-modal-example h3').should('have.text', '321');
|
||||||
|
|
||||||
|
cy.get('#onWillDismiss').should('have.text', 'false');
|
||||||
|
cy.get('#onDidDismiss').should('have.text', 'false');
|
||||||
|
|
||||||
|
cy.get('#close-modal').click();
|
||||||
|
|
||||||
|
cy.get('ion-modal').should('not.exist');
|
||||||
|
|
||||||
|
cy.get('#onWillDismiss').should('have.text', 'true');
|
||||||
|
cy.get('#onDidDismiss').should('have.text', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open nav modal and close', () => {
|
||||||
|
cy.get('#action-button-2').click();
|
||||||
|
|
||||||
|
cy.get('ion-modal').should('exist').should('be.visible');
|
||||||
|
|
||||||
|
cy.get('ion-nav > *:last-child h2').should('have.text', '123');
|
||||||
|
cy.get('ion-nav > *:last-child h3').should('have.text', '321');
|
||||||
|
|
||||||
|
cy.get('ion-nav > *:last-child .push-page').click();
|
||||||
|
|
||||||
|
cy.get('ion-nav > *:last-child h2').should('have.text', 'pushed!');
|
||||||
|
cy.get('ion-nav > *:last-child h3').should('have.text', '');
|
||||||
|
|
||||||
|
cy.get('ion-nav > *:last-child .pop-page').click();
|
||||||
|
|
||||||
|
cy.get('ion-nav > *:last-child h2').should('have.text', '123');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -1,73 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
import { handleErrorMessages, waitTime, testStack } from './utils';
|
|
||||||
|
|
||||||
describe('navigation', () => {
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Fix flaky tests
|
|
||||||
xit ('should swipe and abort', async () => {
|
|
||||||
await browser.get('/router-link?ionic:mode=ios');
|
|
||||||
await waitTime(500);
|
|
||||||
await element(by.css('#routerLink')).click();
|
|
||||||
await waitTime(500);
|
|
||||||
await swipeLeft(5);
|
|
||||||
await waitTime(500);
|
|
||||||
|
|
||||||
const pageHidden = element(by.css('app-router-link'));
|
|
||||||
expect(await pageHidden.getAttribute('aria-hidden')).toEqual('true');
|
|
||||||
expect(await pageHidden.getAttribute('class')).toEqual('ion-page ion-page-hidden');
|
|
||||||
|
|
||||||
const pageVisible = element(by.css('app-router-link-page'));
|
|
||||||
expect(await pageVisible.getAttribute('aria-hidden')).toEqual(null);
|
|
||||||
expect(await pageVisible.getAttribute('class')).toEqual('ion-page can-go-back');
|
|
||||||
});
|
|
||||||
|
|
||||||
xit ('should swipe and go back', async () => {
|
|
||||||
await browser.get('/router-link?ionic:mode=ios');
|
|
||||||
await waitTime(500);
|
|
||||||
await element(by.css('#routerLink')).click();
|
|
||||||
await waitTime(500);
|
|
||||||
await testStack('ion-router-outlet', ['app-router-link', 'app-router-link-page']);
|
|
||||||
|
|
||||||
await swipeLeft(300);
|
|
||||||
|
|
||||||
await waitTime(1000);
|
|
||||||
await testStack('ion-router-outlet', ['app-router-link']);
|
|
||||||
|
|
||||||
const page = element(by.css('app-router-link'));
|
|
||||||
expect(await page.getAttribute('aria-hidden')).toEqual(null);
|
|
||||||
expect(await page.getAttribute('class')).toEqual('ion-page');
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should navigate correctly', async () => {
|
|
||||||
await browser.get('/navigation/page1');
|
|
||||||
await waitTime(2000);
|
|
||||||
await testStack('ion-router-outlet', ['app-navigation-page2', 'app-navigation-page1']);
|
|
||||||
|
|
||||||
const pageHidden = element(by.css('app-navigation-page2'));
|
|
||||||
expect(await pageHidden.getAttribute('aria-hidden')).toEqual('true');
|
|
||||||
expect(await pageHidden.getAttribute('class')).toEqual('ion-page ion-page-hidden');
|
|
||||||
|
|
||||||
const pageVisible = element(by.css('app-navigation-page1'));
|
|
||||||
expect(await pageVisible.getAttribute('aria-hidden')).toEqual(null);
|
|
||||||
expect(await pageVisible.getAttribute('class')).toEqual('ion-page can-go-back');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
function swipeLeft(end: number) {
|
|
||||||
return browser.driver.touchActions()
|
|
||||||
.tapAndHold({x: 5, y: 1})
|
|
||||||
.move({x: 6, y: 1})
|
|
||||||
.move({x: 7, y: 1})
|
|
||||||
.move({x: 8, y: 1})
|
|
||||||
.move({x: 30, y: 1})
|
|
||||||
.move({x: 300, y: 1})
|
|
||||||
.move({x: end, y: 1})
|
|
||||||
.move({x: end, y: 1})
|
|
||||||
.release({x: end, y: 1})
|
|
||||||
.perform();
|
|
||||||
}
|
|
18
angular/test/test-app/e2e/src/navigation.spec.ts
Normal file
18
angular/test/test-app/e2e/src/navigation.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
describe('Navigation', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/navigation');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate correctly', () => {
|
||||||
|
cy.visit('/navigation/page1');
|
||||||
|
cy.wait(2000);
|
||||||
|
cy.testStack('ion-router-outlet', ['app-navigation-page2', 'app-navigation-page1']);
|
||||||
|
|
||||||
|
cy.get('app-navigation-page2').should('have.attr', 'aria-hidden').and('equal', 'true');
|
||||||
|
cy.get('app-navigation-page2').should('have.attr', 'class').and('equal', 'ion-page ion-page-hidden');
|
||||||
|
|
||||||
|
cy.get('app-navigation-page1').should('not.have.attr', 'aria-hidden');
|
||||||
|
cy.get('app-navigation-page1').should('have.attr', 'class').and('equal', 'ion-page can-go-back');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
@ -1,23 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
import { waitTime, handleErrorMessages, goBack } from './utils';
|
|
||||||
|
|
||||||
describe('nested-outlet', () => {
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should navigate correctly', async () => {
|
|
||||||
await browser.get('/nested-outlet/page');
|
|
||||||
expect(await element(by.css('ion-router-outlet ion-router-outlet app-nested-outlet-page h1')).getText()).toEqual('Nested page 1');
|
|
||||||
|
|
||||||
await element(by.css('#goto-tabs')).click();
|
|
||||||
await waitTime(500);
|
|
||||||
await element(by.css('#goto-nested-page1')).click();
|
|
||||||
await waitTime(500);
|
|
||||||
await element(by.css('#goto-nested-page2')).click();
|
|
||||||
await waitTime(500);
|
|
||||||
expect(await element(by.css('ion-router-outlet ion-router-outlet app-nested-outlet-page2 h1')).getText()).toEqual('Nested page 2');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
25
angular/test/test-app/e2e/src/nested-outlet.spec.ts
Normal file
25
angular/test/test-app/e2e/src/nested-outlet.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
describe('Nested Outlet', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/nested-outlet/page');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate correctly', () => {
|
||||||
|
cy.get('ion-router-outlet ion-router-outlet app-nested-outlet-page h1').should('have.text', 'Nested page 1');
|
||||||
|
|
||||||
|
cy.get('#goto-tabs').click();
|
||||||
|
|
||||||
|
cy.ionPageVisible('app-tabs');
|
||||||
|
cy.ionPageVisible('app-tabs-tab1');
|
||||||
|
|
||||||
|
cy.get('#goto-nested-page1').click();
|
||||||
|
|
||||||
|
cy.ionPageVisible('app-nested-outlet-page');
|
||||||
|
cy.ionPageDoesNotExist('app-tabs');
|
||||||
|
|
||||||
|
cy.get('#goto-nested-page2').click();
|
||||||
|
cy.ionPageVisible('app-nested-outlet-page2');
|
||||||
|
|
||||||
|
cy.get('ion-router-outlet ion-router-outlet app-nested-outlet-page2 h1').should('have.text', 'Nested page 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,29 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
import { handleErrorMessages, waitTime } from './utils';
|
|
||||||
|
|
||||||
describe('providers', () => {
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load all providers', async () => {
|
|
||||||
await browser.get('/providers');
|
|
||||||
|
|
||||||
expect(await element(by.css('#is-loaded')).getText()).toEqual('true');
|
|
||||||
expect(await element(by.css('#is-ready')).getText()).toEqual('true');
|
|
||||||
expect(await element(by.css('#is-paused')).getText()).toEqual('true');
|
|
||||||
expect(await element(by.css('#is-resumed')).getText()).toEqual('true');
|
|
||||||
expect(await element(by.css('#is-resized')).getText()).toEqual('true');
|
|
||||||
expect(await element(by.css('#is-testing')).getText()).toEqual('false');
|
|
||||||
expect(await element(by.css('#is-desktop')).getText()).toEqual('true');
|
|
||||||
expect(await element(by.css('#is-mobile')).getText()).toEqual('false');
|
|
||||||
expect(await element(by.css('#keyboard-height')).getText()).toEqual('12345');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect testing mode', async () => {
|
|
||||||
await browser.get('/providers?ionic:_testing=true');
|
|
||||||
|
|
||||||
expect(await element(by.css('#is-testing')).getText()).toEqual('true');
|
|
||||||
});
|
|
||||||
});
|
|
24
angular/test/test-app/e2e/src/providers.spec.ts
Normal file
24
angular/test/test-app/e2e/src/providers.spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
describe('Providers', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/providers');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load all providers', () => {
|
||||||
|
cy.get('#is-loaded').should('have.text', 'true');
|
||||||
|
cy.get('#is-ready').should('have.text', 'true');
|
||||||
|
cy.get('#is-paused').should('have.text', 'true');
|
||||||
|
cy.get('#is-resumed').should('have.text', 'true');
|
||||||
|
cy.get('#is-resized').should('have.text', 'true');
|
||||||
|
cy.get('#is-testing').should('have.text', 'false');
|
||||||
|
cy.get('#is-desktop').should('have.text', 'true');
|
||||||
|
cy.get('#is-mobile').should('have.text', 'false');
|
||||||
|
cy.get('#keyboard-height').should('have.text', '12345');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect testing mode', () => {
|
||||||
|
cy.visit('/providers?ionic:_testing=true');
|
||||||
|
|
||||||
|
cy.get('#is-testing').should('have.text', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,194 +0,0 @@
|
|||||||
import { browser, element, by, protractor } from 'protractor';
|
|
||||||
import { waitTime, testStack, testLifeCycle, handleErrorMessages, getText } from './utils';
|
|
||||||
|
|
||||||
const EC = protractor.ExpectedConditions;
|
|
||||||
|
|
||||||
describe('router-link params and fragments', () => {
|
|
||||||
const queryParam = 'A&=#Y';
|
|
||||||
const fragment = 'myDiv1';
|
|
||||||
const id = 'MyPageID==';
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go to a page with properly encoded values', async () => {
|
|
||||||
await browser.get('/router-link?ionic:_testing=true');
|
|
||||||
await element(by.css('#queryParamsFragment')).click();
|
|
||||||
|
|
||||||
const expectedRoute = `${encodeURIComponent(id)}?token=${encodeURIComponent(queryParam)}#${encodeURIComponent(fragment)}`;
|
|
||||||
|
|
||||||
browser.wait(EC.urlContains(expectedRoute), 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return to a page with preserved query param and fragment', async () => {
|
|
||||||
await browser.get('/router-link?ionic:_testing=true');
|
|
||||||
await waitTime(30);
|
|
||||||
await element(by.css('#queryParamsFragment')).click();
|
|
||||||
await waitTime(400);
|
|
||||||
await element(by.css('#goToPage3')).click();
|
|
||||||
|
|
||||||
browser.wait(EC.urlContains('router-link-page3'), 5000);
|
|
||||||
await waitTime(400);
|
|
||||||
|
|
||||||
await element(by.css('#goBackFromPage3')).click();
|
|
||||||
|
|
||||||
const expectedRoute = `${encodeURIComponent(id)}?token=${encodeURIComponent(queryParam)}#${encodeURIComponent(fragment)}`;
|
|
||||||
browser.wait(EC.urlContains(expectedRoute), 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should preserve query param and fragment with defaultHref string', async () => {
|
|
||||||
await browser.get('/router-link-page3?ionic:_testing=true');
|
|
||||||
await waitTime(30);
|
|
||||||
|
|
||||||
await element(by.css('#goBackFromPage3')).click();
|
|
||||||
|
|
||||||
const expectedRoute = '?token=ABC#fragment';
|
|
||||||
browser.wait(EC.urlContains(expectedRoute), 5000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('router-link', () => {
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/router-link');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should have correct lifecycle counts', async () => {
|
|
||||||
await testLifeCycle('app-router-link', {
|
|
||||||
ionViewWillEnter: 1,
|
|
||||||
ionViewDidEnter: 1,
|
|
||||||
ionViewWillLeave: 0,
|
|
||||||
ionViewDidLeave: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('forward', () => {
|
|
||||||
|
|
||||||
it('should go forward with ion-button[routerLink]', async () => {
|
|
||||||
await element(by.css('#routerLink')).click();
|
|
||||||
await testForward();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go forward with a[routerLink]', async () => {
|
|
||||||
await element(by.css('#a')).click();
|
|
||||||
await testForward();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go forward with button + navigateByUrl()', async () => {
|
|
||||||
await element(by.css('#button')).click();
|
|
||||||
await testForward();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go forward with button + navigateForward()', async () => {
|
|
||||||
await element(by.css('#button-forward')).click();
|
|
||||||
await testForward();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('root', () => {
|
|
||||||
|
|
||||||
it('should go root with ion-button[routerLink][routerDirection=root]', async () => {
|
|
||||||
await element(by.css('#routerLink-root')).click();
|
|
||||||
await testRoot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go root with a[routerLink][routerDirection=root]', async () => {
|
|
||||||
await element(by.css('#a-root')).click();
|
|
||||||
await testRoot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go root with button + navigateRoot', async () => {
|
|
||||||
await element(by.css('#button-root')).click();
|
|
||||||
await testRoot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('back', () => {
|
|
||||||
|
|
||||||
it('should go back with ion-button[routerLink][routerDirection=back]', async () => {
|
|
||||||
await element(by.css('#routerLink-back')).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go back with a[routerLink][routerDirection=back]', async () => {
|
|
||||||
await element(by.css('#a-back')).click();
|
|
||||||
await testBack();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go back with button + navigateBack', async () => {
|
|
||||||
await element(by.css('#button-back')).click();
|
|
||||||
await testBack();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function testForward() {
|
|
||||||
await waitTime(2500);
|
|
||||||
await testStack('ion-router-outlet', ['app-router-link', 'app-router-link-page']);
|
|
||||||
await testLifeCycle('app-router-link-page', {
|
|
||||||
ionViewWillEnter: 1,
|
|
||||||
ionViewDidEnter: 1,
|
|
||||||
ionViewWillLeave: 0,
|
|
||||||
ionViewDidLeave: 0,
|
|
||||||
});
|
|
||||||
expect(await getText(`app-router-link-page #canGoBack`)).toEqual('true');
|
|
||||||
|
|
||||||
await browser.navigate().back();
|
|
||||||
await waitTime(100);
|
|
||||||
await testStack('ion-router-outlet', ['app-router-link']);
|
|
||||||
await testLifeCycle('app-router-link', {
|
|
||||||
ionViewWillEnter: 2,
|
|
||||||
ionViewDidEnter: 2,
|
|
||||||
ionViewWillLeave: 1,
|
|
||||||
ionViewDidLeave: 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testRoot() {
|
|
||||||
await waitTime(200);
|
|
||||||
await testStack('ion-router-outlet', ['app-router-link-page']);
|
|
||||||
await testLifeCycle('app-router-link-page', {
|
|
||||||
ionViewWillEnter: 1,
|
|
||||||
ionViewDidEnter: 1,
|
|
||||||
ionViewWillLeave: 0,
|
|
||||||
ionViewDidLeave: 0,
|
|
||||||
});
|
|
||||||
expect(await getText(`app-router-link-page #canGoBack`)).toEqual('false');
|
|
||||||
|
|
||||||
await browser.navigate().back();
|
|
||||||
await waitTime(100);
|
|
||||||
await testStack('ion-router-outlet', ['app-router-link']);
|
|
||||||
await testLifeCycle('app-router-link', {
|
|
||||||
ionViewWillEnter: 1,
|
|
||||||
ionViewDidEnter: 1,
|
|
||||||
ionViewWillLeave: 0,
|
|
||||||
ionViewDidLeave: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testBack() {
|
|
||||||
await waitTime(500);
|
|
||||||
await testStack('ion-router-outlet', ['app-router-link-page']);
|
|
||||||
await testLifeCycle('app-router-link-page', {
|
|
||||||
ionViewWillEnter: 1,
|
|
||||||
ionViewDidEnter: 1,
|
|
||||||
ionViewWillLeave: 0,
|
|
||||||
ionViewDidLeave: 0,
|
|
||||||
});
|
|
||||||
expect(await getText(`app-router-link-page #canGoBack`)).toEqual('false');
|
|
||||||
|
|
||||||
await browser.navigate().back();
|
|
||||||
await waitTime(100);
|
|
||||||
await testStack('ion-router-outlet', ['app-router-link']);
|
|
||||||
await testLifeCycle('app-router-link', {
|
|
||||||
ionViewWillEnter: 1,
|
|
||||||
ionViewDidEnter: 1,
|
|
||||||
ionViewWillLeave: 0,
|
|
||||||
ionViewDidLeave: 0,
|
|
||||||
});
|
|
||||||
}
|
|
192
angular/test/test-app/e2e/src/router-link.spec.ts
Normal file
192
angular/test/test-app/e2e/src/router-link.spec.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
describe('Router Link', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/router-link');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('router-link params and fragments', () => {
|
||||||
|
const queryParam = 'A&=#Y';
|
||||||
|
const fragment = 'myDiv1';
|
||||||
|
const id = 'MyPageID==';
|
||||||
|
|
||||||
|
it('should go to a page with properly encoded values', () => {
|
||||||
|
cy.visit('/router-link?ionic:_testing=true');
|
||||||
|
cy.get('#queryParamsFragment').click();
|
||||||
|
|
||||||
|
const expectedPath = `${encodeURIComponent(id)}`;
|
||||||
|
const expectedSearch = `?token=${encodeURIComponent(queryParam)}`;
|
||||||
|
const expectedHash = `#${encodeURIComponent(fragment)}`;
|
||||||
|
|
||||||
|
cy.location().should((location) => {
|
||||||
|
expect(location.pathname).to.contain(expectedPath);
|
||||||
|
expect(location.search).to.eq(expectedSearch);
|
||||||
|
expect(location.hash).to.eq(expectedHash);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return to a page with preserved query param and fragment', () => {
|
||||||
|
cy.visit('/router-link?ionic:_testing=true');
|
||||||
|
cy.get('#queryParamsFragment').click();
|
||||||
|
cy.get('#goToPage3').click();
|
||||||
|
|
||||||
|
cy.location().should((location) => {
|
||||||
|
expect(location.pathname).to.contain('router-link-page3');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('#goBackFromPage3').click();
|
||||||
|
|
||||||
|
const expectedPath = `${encodeURIComponent(id)}`;
|
||||||
|
const expectedSearch = `?token=${encodeURIComponent(queryParam)}`;
|
||||||
|
const expectedHash = `#${encodeURIComponent(fragment)}`;
|
||||||
|
|
||||||
|
cy.location().should((location) => {
|
||||||
|
expect(location.pathname).to.contain(expectedPath);
|
||||||
|
expect(location.search).to.eq(expectedSearch);
|
||||||
|
expect(location.hash).to.eq(expectedHash);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve query param and fragment with defaultHref string', () => {
|
||||||
|
cy.visit('/router-link-page3?ionic:_testing=true');
|
||||||
|
|
||||||
|
cy.get('#goBackFromPage3').click();
|
||||||
|
|
||||||
|
const expectedSearch = '?token=ABC';
|
||||||
|
const expectedHash = '#fragment';
|
||||||
|
|
||||||
|
cy.location().should((location) => {
|
||||||
|
expect(location.search).to.eq(expectedSearch);
|
||||||
|
expect(location.hash).to.eq(expectedHash);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('router-link', () => {
|
||||||
|
it('should have correct lifecycle counts', () => {
|
||||||
|
cy.testLifeCycle('app-router-link', {
|
||||||
|
ionViewWillEnter: 1,
|
||||||
|
ionViewDidEnter: 1,
|
||||||
|
ionViewWillLeave: 0,
|
||||||
|
ionViewDidLeave: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('forward', () => {
|
||||||
|
it('should go forward with ion-button[routerLink]', () => {
|
||||||
|
cy.get('#routerLink').click();
|
||||||
|
testForward();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go forward with a[routerLink]', () => {
|
||||||
|
cy.get('#a').click();
|
||||||
|
testForward();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go forward with button + navigateByUrl()', () => {
|
||||||
|
cy.get('#button').click();
|
||||||
|
testForward();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go forward with button + navigateForward()', () => {
|
||||||
|
cy.get('#button-forward').click();
|
||||||
|
testForward();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should go root with ion-button[routerLink][routerDirection=root]', () => {
|
||||||
|
cy.get('#routerLink-root').click();
|
||||||
|
testRoot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go root with a[routerLink][routerDirection=root]', () => {
|
||||||
|
cy.get('#a-root').click();
|
||||||
|
testRoot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go root with button + navigateRoot', () => {
|
||||||
|
cy.get('#button-root').click();
|
||||||
|
testRoot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('back', () => {
|
||||||
|
it('should go back with ion-button[routerLink][routerDirection=back]', () => {
|
||||||
|
cy.get('#routerLink-back').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go back with a[routerLink][routerDirection=back]', () => {
|
||||||
|
cy.get('#a-back').click();
|
||||||
|
testBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go back with button + navigateBack', () => {
|
||||||
|
cy.get('#button-back').click();
|
||||||
|
testBack();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function testForward() {
|
||||||
|
cy.testStack('ion-router-outlet', ['app-router-link', 'app-router-link-page']);
|
||||||
|
cy.testLifeCycle('app-router-link-page', {
|
||||||
|
ionViewWillEnter: 1,
|
||||||
|
ionViewDidEnter: 1,
|
||||||
|
ionViewWillLeave: 0,
|
||||||
|
ionViewDidLeave: 0,
|
||||||
|
});
|
||||||
|
cy.get('app-router-link-page #canGoBack').should('have.text', 'true');
|
||||||
|
|
||||||
|
cy.go('back');
|
||||||
|
cy.testStack('ion-router-outlet', ['app-router-link']);
|
||||||
|
cy.testLifeCycle('app-router-link', {
|
||||||
|
ionViewWillEnter: 2,
|
||||||
|
ionViewDidEnter: 2,
|
||||||
|
ionViewWillLeave: 1,
|
||||||
|
ionViewDidLeave: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testRoot() {
|
||||||
|
cy.wait(200);
|
||||||
|
cy.testStack('ion-router-outlet', ['app-router-link-page']);
|
||||||
|
cy.testLifeCycle('app-router-link-page', {
|
||||||
|
ionViewWillEnter: 1,
|
||||||
|
ionViewDidEnter: 1,
|
||||||
|
ionViewWillLeave: 0,
|
||||||
|
ionViewDidLeave: 0,
|
||||||
|
});
|
||||||
|
cy.get('app-router-link-page #canGoBack').should('have.text', 'false');
|
||||||
|
|
||||||
|
cy.go('back');
|
||||||
|
cy.wait(100);
|
||||||
|
cy.testStack('ion-router-outlet', ['app-router-link']);
|
||||||
|
cy.testLifeCycle('app-router-link', {
|
||||||
|
ionViewWillEnter: 1,
|
||||||
|
ionViewDidEnter: 1,
|
||||||
|
ionViewWillLeave: 0,
|
||||||
|
ionViewDidLeave: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testBack() {
|
||||||
|
cy.wait(500);
|
||||||
|
cy.testStack('ion-router-outlet', ['app-router-link-page']);
|
||||||
|
cy.testLifeCycle('app-router-link-page', {
|
||||||
|
ionViewWillEnter: 1,
|
||||||
|
ionViewDidEnter: 1,
|
||||||
|
ionViewWillLeave: 0,
|
||||||
|
ionViewDidLeave: 0,
|
||||||
|
});
|
||||||
|
cy.get('app-router-link-page #canGoBack').should('have.text', 'false');
|
||||||
|
|
||||||
|
cy.go('back');
|
||||||
|
cy.wait(100);
|
||||||
|
cy.testStack('ion-router-outlet', ['app-router-link']);
|
||||||
|
cy.testLifeCycle('app-router-link', {
|
||||||
|
ionViewWillEnter: 1,
|
||||||
|
ionViewDidEnter: 1,
|
||||||
|
ionViewWillLeave: 0,
|
||||||
|
ionViewDidLeave: 0,
|
||||||
|
});
|
||||||
|
}
|
36
angular/test/test-app/e2e/src/routing.spec.ts
Normal file
36
angular/test/test-app/e2e/src/routing.spec.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
describe('Routing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/router-link?ionic:mode=ios');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should swipe and abort', () => {
|
||||||
|
cy.get('#routerLink').click();
|
||||||
|
|
||||||
|
cy.ionSwipeToGoBack();
|
||||||
|
|
||||||
|
cy.get('app-router-link').should('have.attr', 'aria-hidden').and('equal', 'true');
|
||||||
|
cy.get('app-router-link').should('have.attr', 'class').and('equal', 'ion-page ion-page-hidden');
|
||||||
|
|
||||||
|
cy.get('app-router-link-page').should('not.have.attr', 'aria-hidden');
|
||||||
|
cy.get('app-router-link-page').should('have.attr', 'class').and('equal', 'ion-page can-go-back');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should swipe and go back', () => {
|
||||||
|
cy.get('#routerLink').click();
|
||||||
|
|
||||||
|
cy.ionPageHidden('app-router-link');
|
||||||
|
cy.ionPageVisible('app-router-link-page');
|
||||||
|
|
||||||
|
cy.testStack('ion-router-outlet', ['app-router-link', 'app-router-link-page']);
|
||||||
|
|
||||||
|
cy.ionSwipeToGoBack(true);
|
||||||
|
|
||||||
|
cy.ionPageVisible('app-router-link');
|
||||||
|
cy.ionPageDoesNotExist('app-router-link-page');
|
||||||
|
|
||||||
|
cy.testStack('ion-router-outlet', ['app-router-link']);
|
||||||
|
|
||||||
|
cy.get('app-router-link').should('not.have.attr', 'aria-hidden');
|
||||||
|
cy.get('app-router-link').should('have.attr', 'class').and('equal', 'ion-page');
|
||||||
|
});
|
||||||
|
})
|
@ -1,51 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
import { handleErrorMessages, waitTime } from './utils';
|
|
||||||
|
|
||||||
describe('slides', () => {
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/slides');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should change index on slide change', async () => {
|
|
||||||
expect(await element.all(by.css('ion-slide')).count()).toEqual(0);
|
|
||||||
await addSlides();
|
|
||||||
expect(await element.all(by.css('ion-slide')).count()).toEqual(3);
|
|
||||||
|
|
||||||
await checkIndex('0');
|
|
||||||
|
|
||||||
await nextSlide();
|
|
||||||
await checkIndex('1');
|
|
||||||
|
|
||||||
await nextSlide();
|
|
||||||
await checkIndex('2');
|
|
||||||
|
|
||||||
await prevSlide();
|
|
||||||
await checkIndex('1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function checkIndex(index: string) {
|
|
||||||
expect(await element(by.css('#slide-index')).getText()).toEqual(index);
|
|
||||||
expect(await element(by.css('#slide-index-2')).getText()).toEqual(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addSlides() {
|
|
||||||
await element(by.css('#add-slides')).click();
|
|
||||||
await waitTime(800);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function nextSlide() {
|
|
||||||
await element(by.css('#btn-next')).click();
|
|
||||||
await waitTime(800);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function prevSlide() {
|
|
||||||
await element(by.css('#btn-prev')).click();
|
|
||||||
await waitTime(800);
|
|
||||||
}
|
|
44
angular/test/test-app/e2e/src/slides.spec.ts
Normal file
44
angular/test/test-app/e2e/src/slides.spec.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
describe('Slides', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/slides');
|
||||||
|
cy.wait(30);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should change index on slide change', () => {
|
||||||
|
cy.get('ion-slide').should('have.length', 0);
|
||||||
|
|
||||||
|
cy.get('#add-slides').click();
|
||||||
|
|
||||||
|
cy.get('ion-slide').should('have.length', 3);
|
||||||
|
|
||||||
|
// Should be on the first slide
|
||||||
|
checkIndex('0');
|
||||||
|
|
||||||
|
// Swipe to the second slide
|
||||||
|
nextSlide();
|
||||||
|
checkIndex('1');
|
||||||
|
|
||||||
|
// Swipe to the third slide
|
||||||
|
nextSlide();
|
||||||
|
checkIndex('2');
|
||||||
|
|
||||||
|
// Go back to the second slide
|
||||||
|
prevSlide();
|
||||||
|
checkIndex('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkIndex(index) {
|
||||||
|
cy.get('#slide-index').should('have.text', index);
|
||||||
|
cy.get('#slide-index-2').should('have.text', index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextSlide() {
|
||||||
|
cy.get('#btn-next').click();
|
||||||
|
cy.wait(800);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevSlide() {
|
||||||
|
cy.get('#btn-prev').click();
|
||||||
|
cy.wait(800);
|
||||||
|
}
|
@ -1,334 +0,0 @@
|
|||||||
import { browser, by, element, ElementFinder, ExpectedConditions } from 'protractor';
|
|
||||||
import { handleErrorMessages, testStack, waitTime } from './utils';
|
|
||||||
|
|
||||||
describe('tabs', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
describe('entry url - /tabs', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/tabs');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect and load tab-account', async () => {
|
|
||||||
await testTabTitle('Tab 1 - Page 1');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1']);
|
|
||||||
await testState(1, 'account');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should navigate between tabs and ionChange events should be dispatched ', async () => {
|
|
||||||
let tab = await testTabTitle('Tab 1 - Page 1');
|
|
||||||
expect(await tab.$('.segment-changed').getText()).toEqual('false');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-contact')).click();
|
|
||||||
tab = await testTabTitle('Tab 2 - Page 1');
|
|
||||||
expect(await tab.$('.segment-changed').getText()).toEqual('false');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should simulate stack + double tab click', async () => {
|
|
||||||
let tab = await getSelectedTab() as ElementFinder;
|
|
||||||
await tab.$('#goto-tab1-page2').click();
|
|
||||||
await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested']);
|
|
||||||
await testState(1, 'account');
|
|
||||||
expect(await tab.$('ion-back-button').isDisplayed()).toBe(true);
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-contact')).click();
|
|
||||||
await testTabTitle('Tab 2 - Page 1');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
|
||||||
await testState(2, 'contact');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
|
||||||
await testState(3, 'account');
|
|
||||||
expect(await tab.$('ion-back-button').isDisplayed()).toBe(true);
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 1');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
|
||||||
await testState(3, 'account');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should simulate stack + back button click', async () => {
|
|
||||||
const tab = await getSelectedTab();
|
|
||||||
await tab.$('#goto-tab1-page2').click();
|
|
||||||
await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testState(1, 'account');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-contact')).click();
|
|
||||||
await testTabTitle('Tab 2 - Page 1');
|
|
||||||
await testState(2, 'contact');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testState(3, 'account');
|
|
||||||
|
|
||||||
await element(by.css('ion-back-button')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 1');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
|
||||||
await testState(3, 'account');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should navigate deep then go home', async () => {
|
|
||||||
let tab = await getSelectedTab();
|
|
||||||
await tab.$('#goto-tab1-page2').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
|
|
||||||
await tab.$('#goto-next').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (2)');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-contact')).click();
|
|
||||||
tab = await testTabTitle('Tab 2 - Page 1');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 2 (2)');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', [
|
|
||||||
'app-tabs-tab1',
|
|
||||||
'app-tabs-tab1-nested',
|
|
||||||
'app-tabs-tab1-nested',
|
|
||||||
'app-tabs-tab2'
|
|
||||||
]);
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 1');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', [
|
|
||||||
'app-tabs-tab1',
|
|
||||||
'app-tabs-tab2'
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch tabs and go back', async () => {
|
|
||||||
await element(by.css('#tab-button-contact')).click();
|
|
||||||
const tab = await testTabTitle('Tab 2 - Page 1');
|
|
||||||
|
|
||||||
await tab.$('#goto-tab1-page1').click();
|
|
||||||
await testTabTitle('Tab 1 - Page 1');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch tabs and go to nested', async () => {
|
|
||||||
await element(by.css('#tab-button-contact')).click();
|
|
||||||
const tab = await testTabTitle('Tab 2 - Page 1');
|
|
||||||
|
|
||||||
await tab.$('#goto-tab1-page2').click();
|
|
||||||
await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab2', 'app-tabs-tab1-nested']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load lazy loaded tab', async () => {
|
|
||||||
await element(by.css('#tab-button-lazy')).click();
|
|
||||||
await testTabTitle('Tab 3 - Page 1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use ion-back-button defaultHref', async () => {
|
|
||||||
let tab = await getSelectedTab() as ElementFinder;
|
|
||||||
await tab.$('#goto-tab3-page2').click();
|
|
||||||
tab = await testTabTitle('Tab 3 - Page 2');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3-nested']);
|
|
||||||
|
|
||||||
await tab.$('ion-back-button').click();
|
|
||||||
await testTabTitle('Tab 3 - Page 1');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should preserve navigation extras when switching tabs', async () => {
|
|
||||||
const expectUrlToContain = 'search=hello#fragment';
|
|
||||||
let tab = await getSelectedTab() as ElementFinder;
|
|
||||||
await tab.$('#goto-nested-page1-with-query-params').click();
|
|
||||||
await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testUrlContains(expectUrlToContain);
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-contact')).click();
|
|
||||||
await testTabTitle('Tab 2 - Page 1');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testUrlContains(expectUrlToContain);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set root when clicking on an active tab to navigate to the root', async () => {
|
|
||||||
const expectNestedTabUrlToContain = 'search=hello#fragment';
|
|
||||||
const tab = await getSelectedTab() as ElementFinder;
|
|
||||||
const initialUrl = await browser.getCurrentUrl();
|
|
||||||
await tab.$('#goto-nested-page1-with-query-params').click();
|
|
||||||
await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testUrlContains(expectNestedTabUrlToContain);
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 1');
|
|
||||||
|
|
||||||
await testUrlEquals(initialUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('entry tab contains navigation extras', () => {
|
|
||||||
const expectNestedTabUrlToContain = 'search=hello#fragment';
|
|
||||||
const rootUrlParams = 'test=123#rootFragment';
|
|
||||||
const rootUrl = `/tabs/account?${rootUrlParams}`;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get(rootUrl);
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should preserve root url navigation extras when clicking on an active tab to navigate to the root', async () => {
|
|
||||||
await browser.get(rootUrl);
|
|
||||||
|
|
||||||
const tab = await getSelectedTab() as ElementFinder;
|
|
||||||
await tab.$('#goto-nested-page1-with-query-params').click();
|
|
||||||
await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testUrlContains(expectNestedTabUrlToContain);
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 1');
|
|
||||||
|
|
||||||
await testUrlContains(rootUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should preserve root url navigation extras when changing tabs', async () => {
|
|
||||||
await browser.get(rootUrl);
|
|
||||||
|
|
||||||
let tab = await getSelectedTab() as ElementFinder;
|
|
||||||
await element(by.css('#tab-button-contact')).click();
|
|
||||||
tab = await testTabTitle('Tab 2 - Page 1');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 1');
|
|
||||||
|
|
||||||
await testUrlContains(rootUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should navigate deep then go home and preserve navigation extras', async () => {
|
|
||||||
let tab = await getSelectedTab();
|
|
||||||
await tab.$('#goto-tab1-page2').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
|
|
||||||
await tab.$('#goto-next').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (2)');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-contact')).click();
|
|
||||||
tab = await testTabTitle('Tab 2 - Page 1');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 2 (2)');
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
await testTabTitle('Tab 1 - Page 1');
|
|
||||||
|
|
||||||
await testUrlContains(rootUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('entry url - /tabs/account/nested/1', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/tabs/account/nested/1');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should only display the back-button when there is a page in the stack', async () => {
|
|
||||||
let tab = await testTabTitle('Tab 1 - Page 2 (1)') as ElementFinder;
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1-nested']);
|
|
||||||
expect(await tab.$('ion-back-button').isDisplayed()).toBe(false);
|
|
||||||
|
|
||||||
await element(by.css('#tab-button-account')).click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 1');
|
|
||||||
|
|
||||||
await tab.$('#goto-tab1-page2').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
expect(await tab.$('ion-back-button').isDisplayed()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not reuse the same page', async () => {
|
|
||||||
let tab = await testTabTitle('Tab 1 - Page 2 (1)') as ElementFinder;
|
|
||||||
await tab.$('#goto-next').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (2)');
|
|
||||||
await tab.$('#goto-next').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (3)');
|
|
||||||
|
|
||||||
await testStack('ion-tabs ion-router-outlet', [
|
|
||||||
'app-tabs-tab1-nested',
|
|
||||||
'app-tabs-tab1-nested',
|
|
||||||
'app-tabs-tab1-nested'
|
|
||||||
]);
|
|
||||||
|
|
||||||
await tab.$('ion-back-button').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (2)');
|
|
||||||
await tab.$('ion-back-button').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
|
|
||||||
expect(await tab.$('ion-back-button').isDisplayed()).toBe(false);
|
|
||||||
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1-nested']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('entry url - /tabs/lazy', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/tabs/lazy');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not display the back-button if coming from a different stack', async () => {
|
|
||||||
let tab = await testTabTitle('Tab 3 - Page 1') as ElementFinder;
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab3']);
|
|
||||||
|
|
||||||
await tab.$('#goto-tab1-page2').click();
|
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (1)');
|
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab3', 'app-tabs-tab1-nested']);
|
|
||||||
expect(await tab.$('ion-back-button').isDisplayed()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('enter url - /tabs/contact/one', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/tabs/contact/one');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return to correct tab after going to page in different outlet', async () => {
|
|
||||||
const tab = await getSelectedTab();
|
|
||||||
await tab.$('#goto-nested-page1').click();
|
|
||||||
|
|
||||||
await waitTime(600);
|
|
||||||
await testStack('app-nested-outlet ion-router-outlet', ['app-nested-outlet-page']);
|
|
||||||
|
|
||||||
const nestedOutlet = await element(by.css('app-nested-outlet'));
|
|
||||||
const backButton = await nestedOutlet.$('ion-back-button');
|
|
||||||
await backButton.click();
|
|
||||||
|
|
||||||
await testTabTitle('Tab 2 - Page 1');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
async function testState(count: number, tab: string) {
|
|
||||||
expect(await element(by.css('#tabs-state')).getText()).toEqual(`${count}.${tab}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testTabTitle(title: string) {
|
|
||||||
await waitTime(1000);
|
|
||||||
const tab = await getSelectedTab();
|
|
||||||
expect(await tab.$('ion-title').getText()).toEqual(title);
|
|
||||||
return tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testUrlContains(urlFragment: string) {
|
|
||||||
await browser.wait(ExpectedConditions.urlContains(urlFragment),
|
|
||||||
5000,
|
|
||||||
`expected ${browser.getCurrentUrl()} to contain ${urlFragment}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testUrlEquals(url: string) {
|
|
||||||
await browser.wait(ExpectedConditions.urlIs(url),
|
|
||||||
5000,
|
|
||||||
`expected ${browser.getCurrentUrl()} to equal ${url}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSelectedTab(): Promise<ElementFinder> {
|
|
||||||
const tabs = element.all(by.css('ion-tabs ion-router-outlet > *:not(.ion-page-hidden)'));
|
|
||||||
expect(await tabs.count()).toEqual(1);
|
|
||||||
const tab = tabs.first();
|
|
||||||
return tab;
|
|
||||||
}
|
|
329
angular/test/test-app/e2e/src/tabs.spec.ts
Normal file
329
angular/test/test-app/e2e/src/tabs.spec.ts
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
describe('Tabs', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/tabs');
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('entry url - /tabs', () => {
|
||||||
|
it('should redirect and load tab-account', () => {
|
||||||
|
testTabTitle('Tab 1 - Page 1');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1']);
|
||||||
|
testState(1, 'account');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate between tabs and ionChange events should be dispatched', () => {
|
||||||
|
let tab = testTabTitle('Tab 1 - Page 1');
|
||||||
|
tab.find('.segment-changed').should('have.text', 'false');
|
||||||
|
|
||||||
|
cy.get('#tab-button-contact').click();
|
||||||
|
tab = testTabTitle('Tab 2 - Page 1');
|
||||||
|
tab.find('.segment-changed').should('have.text', 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should simulate stack + double tab click', () => {
|
||||||
|
let tab = getSelectedTab();
|
||||||
|
tab.find('#goto-tab1-page2').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested']);
|
||||||
|
testState(1, 'account');
|
||||||
|
|
||||||
|
// When you call find on tab above it changes the value of tab
|
||||||
|
// so we need to redefine it
|
||||||
|
tab = getSelectedTab();
|
||||||
|
tab.find('ion-back-button').should('be.visible');
|
||||||
|
|
||||||
|
cy.get('#tab-button-contact').click();
|
||||||
|
testTabTitle('Tab 2 - Page 1');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||||
|
testState(2, 'contact');
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab1-nested', 'app-tabs-tab2']);
|
||||||
|
testState(3, 'account');
|
||||||
|
|
||||||
|
tab = getSelectedTab();
|
||||||
|
tab.find('ion-back-button').should('be.visible');
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 1');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||||
|
testState(3, 'account');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should simulate stack + back button click', () => {
|
||||||
|
const tab = getSelectedTab();
|
||||||
|
tab.find('#goto-tab1-page2').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
testState(1, 'account');
|
||||||
|
|
||||||
|
cy.get('#tab-button-contact').click();
|
||||||
|
testTabTitle('Tab 2 - Page 1');
|
||||||
|
testState(2, 'contact');
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
testState(3, 'account');
|
||||||
|
|
||||||
|
cy.get('ion-back-button').click();
|
||||||
|
testTabTitle('Tab 1 - Page 1');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||||
|
testState(3, 'account');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate deep then go home', () => {
|
||||||
|
const tab = getSelectedTab();
|
||||||
|
tab.find('#goto-tab1-page2').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
|
||||||
|
cy.get('#goto-next').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (2)');
|
||||||
|
|
||||||
|
cy.get('#tab-button-contact').click();
|
||||||
|
testTabTitle('Tab 2 - Page 1');
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (2)');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', [
|
||||||
|
'app-tabs-tab1',
|
||||||
|
'app-tabs-tab1-nested',
|
||||||
|
'app-tabs-tab1-nested',
|
||||||
|
'app-tabs-tab2'
|
||||||
|
]);
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 1');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', [
|
||||||
|
'app-tabs-tab1',
|
||||||
|
'app-tabs-tab2'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch tabs and go back', () => {
|
||||||
|
cy.get('#tab-button-contact').click();
|
||||||
|
const tab = testTabTitle('Tab 2 - Page 1');
|
||||||
|
|
||||||
|
tab.find('#goto-tab1-page1').click();
|
||||||
|
testTabTitle('Tab 1 - Page 1');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch tabs and go to nested', () => {
|
||||||
|
cy.get('#tab-button-contact').click();
|
||||||
|
const tab = testTabTitle('Tab 2 - Page 1');
|
||||||
|
|
||||||
|
tab.find('#goto-tab1-page2').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab2', 'app-tabs-tab1-nested']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load lazy loaded tab', () => {
|
||||||
|
cy.get('#tab-button-lazy').click();
|
||||||
|
cy.ionPageVisible('app-tabs-tab3');
|
||||||
|
testTabTitle('Tab 3 - Page 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use ion-back-button defaultHref', () => {
|
||||||
|
let tab = getSelectedTab();
|
||||||
|
tab.find('#goto-tab3-page2').click();
|
||||||
|
testTabTitle('Tab 3 - Page 2');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3-nested']);
|
||||||
|
|
||||||
|
tab = getSelectedTab();
|
||||||
|
tab.find('ion-back-button').click();
|
||||||
|
testTabTitle('Tab 3 - Page 1');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve navigation extras when switching tabs', () => {
|
||||||
|
const expectUrlToContain = 'search=hello#fragment';
|
||||||
|
let tab = getSelectedTab();
|
||||||
|
tab.find('#goto-nested-page1-with-query-params').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
testUrlContains(expectUrlToContain);
|
||||||
|
|
||||||
|
cy.get('#tab-button-contact').click();
|
||||||
|
testTabTitle('Tab 2 - Page 1');
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
testUrlContains(expectUrlToContain);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set root when clicking on an active tab to navigate to the root', () => {
|
||||||
|
const expectNestedTabUrlToContain = 'search=hello#fragment';
|
||||||
|
cy.url().then(url => {
|
||||||
|
const tab = getSelectedTab();
|
||||||
|
tab.find('#goto-nested-page1-with-query-params').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
testUrlContains(expectNestedTabUrlToContain);
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 1');
|
||||||
|
|
||||||
|
testUrlEquals(url);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('entry tab contains navigation extras', () => {
|
||||||
|
const expectNestedTabUrlToContain = 'search=hello#fragment';
|
||||||
|
const rootUrlParams = 'test=123#rootFragment';
|
||||||
|
const rootUrl = `/tabs/account?${rootUrlParams}`;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit(rootUrl);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve root url navigation extras when clicking on an active tab to navigate to the root', () => {
|
||||||
|
const tab = getSelectedTab();
|
||||||
|
tab.find('#goto-nested-page1-with-query-params').click();
|
||||||
|
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
testUrlContains(expectNestedTabUrlToContain);
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 1');
|
||||||
|
|
||||||
|
testUrlContains(rootUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve root url navigation extras when changing tabs', () => {
|
||||||
|
getSelectedTab();
|
||||||
|
cy.get('#tab-button-contact').click();
|
||||||
|
testTabTitle('Tab 2 - Page 1');
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 1');
|
||||||
|
|
||||||
|
testUrlContains(rootUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate deep then go home and preserve navigation extras', () => {
|
||||||
|
let tab = getSelectedTab();
|
||||||
|
tab.find('#goto-tab1-page2').click();
|
||||||
|
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
|
||||||
|
tab.find('#goto-next').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (2)');
|
||||||
|
|
||||||
|
cy.get('#tab-button-contact').click();
|
||||||
|
testTabTitle('Tab 2 - Page 1');
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 2 (2)');
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
testTabTitle('Tab 1 - Page 1');
|
||||||
|
|
||||||
|
testUrlContains(rootUrl);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('entry url - /tabs/account/nested/1', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/tabs/account/nested/1');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should only display the back-button when there is a page in the stack', () => {
|
||||||
|
let tab = getSelectedTab();
|
||||||
|
tab.find('ion-back-button').should('not.be.visible');
|
||||||
|
testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1-nested']);
|
||||||
|
|
||||||
|
cy.get('#tab-button-account').click();
|
||||||
|
tab = testTabTitle('Tab 1 - Page 1');
|
||||||
|
|
||||||
|
tab.find('#goto-tab1-page2').click();
|
||||||
|
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
tab.find('ion-back-button').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not reuse the same page', () => {
|
||||||
|
let tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
tab.find('#goto-next').click();
|
||||||
|
tab = testTabTitle('Tab 1 - Page 2 (2)');
|
||||||
|
|
||||||
|
tab.find('#goto-next').click();
|
||||||
|
tab = testTabTitle('Tab 1 - Page 2 (3)');
|
||||||
|
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', [
|
||||||
|
'app-tabs-tab1-nested',
|
||||||
|
'app-tabs-tab1-nested',
|
||||||
|
'app-tabs-tab1-nested'
|
||||||
|
]);
|
||||||
|
|
||||||
|
tab = getSelectedTab();
|
||||||
|
tab.find('ion-back-button').click();
|
||||||
|
tab = testTabTitle('Tab 1 - Page 2 (2)');
|
||||||
|
tab.find('ion-back-button').click();
|
||||||
|
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
|
||||||
|
tab.find('ion-back-button').should('not.be.visible');
|
||||||
|
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1-nested']);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('entry url - /tabs/lazy', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/tabs/lazy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display the back-button if coming from a different stack', () => {
|
||||||
|
let tab = testTabTitle('Tab 3 - Page 1');
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab3']);
|
||||||
|
|
||||||
|
tab = getSelectedTab();
|
||||||
|
tab.find('#goto-tab1-page2').click();
|
||||||
|
cy.testStack('ion-tabs ion-router-outlet', ['app-tabs-tab3', 'app-tabs-tab1-nested']);
|
||||||
|
|
||||||
|
tab = testTabTitle('Tab 1 - Page 2 (1)');
|
||||||
|
tab.find('ion-back-button').should('not.be.visible');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('enter url - /tabs/contact/one', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/tabs/contact/one');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return to correct tab after going to page in different outlet', () => {
|
||||||
|
const tab = getSelectedTab();
|
||||||
|
tab.find('#goto-nested-page1').click();
|
||||||
|
cy.testStack('app-nested-outlet ion-router-outlet', ['app-nested-outlet-page']);
|
||||||
|
|
||||||
|
const nestedOutlet = cy.get('app-nested-outlet');
|
||||||
|
nestedOutlet.find('ion-back-button').click();
|
||||||
|
|
||||||
|
testTabTitle('Tab 2 - Page 1');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function testTabTitle(title) {
|
||||||
|
const tab = getSelectedTab();
|
||||||
|
|
||||||
|
// Find is used to get a direct descendant instead of get
|
||||||
|
tab.find('ion-title').should('have.text', title);
|
||||||
|
return getSelectedTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedTab() {
|
||||||
|
cy.get('ion-tabs ion-router-outlet > *:not(.ion-page-hidden)').should('have.length', 1);
|
||||||
|
return cy.get('ion-tabs ion-router-outlet > *:not(.ion-page-hidden)').first();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testState(count, tab) {
|
||||||
|
cy.get('#tabs-state').should('have.text', `${count}.${tab}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testUrlContains(urlFragment) {
|
||||||
|
cy.location().should((location) => {
|
||||||
|
expect(location.href).to.contain(urlFragment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testUrlEquals(url) {
|
||||||
|
cy.url().should('eq', url);
|
||||||
|
}
|
@ -1,73 +0,0 @@
|
|||||||
import { browser } from 'protractor';
|
|
||||||
|
|
||||||
export function goBack() {
|
|
||||||
return browser.executeScript(`return window.history.back()`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProperty(selector: string, property: string) {
|
|
||||||
return browser.executeScript(`
|
|
||||||
return document.querySelector('${selector}')['${property}'];
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getText(selector: string) {
|
|
||||||
return browser.executeScript(`
|
|
||||||
return document.querySelector('${selector}').textContent;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setProperty(selector: string, property: string, value: any) {
|
|
||||||
const text = JSON.stringify(value);
|
|
||||||
return browser.executeScript(`
|
|
||||||
document.querySelector('${selector}')['${property}'] = ${text};
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function waitTime(time: number) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
setTimeout(resolve, time);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LifeCycleCount {
|
|
||||||
ionViewWillEnter: number;
|
|
||||||
ionViewDidEnter: number;
|
|
||||||
ionViewWillLeave: number;
|
|
||||||
ionViewDidLeave: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleErrorMessages() {
|
|
||||||
return browser.manage().logs().get('browser').then(browserLog => {
|
|
||||||
for (let i = 0; i <= browserLog.length - 1; i++) {
|
|
||||||
if (browserLog[i].level.name_ === 'SEVERE') {
|
|
||||||
fail(browserLog[i].message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function testLifeCycle(selector: string, expected: LifeCycleCount) {
|
|
||||||
await waitTime(50);
|
|
||||||
const results = await Promise.all([
|
|
||||||
getText(`${selector} #ngOnInit`),
|
|
||||||
getText(`${selector} #ionViewWillEnter`),
|
|
||||||
getText(`${selector} #ionViewDidEnter`),
|
|
||||||
getText(`${selector} #ionViewWillLeave`),
|
|
||||||
getText(`${selector} #ionViewDidLeave`),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(results[0]).toEqual('1');
|
|
||||||
expect(results[1]).toEqual(expected.ionViewWillEnter.toString());
|
|
||||||
expect(results[2]).toEqual(expected.ionViewDidEnter.toString());
|
|
||||||
expect(results[3]).toEqual(expected.ionViewWillLeave.toString());
|
|
||||||
expect(results[4]).toEqual(expected.ionViewDidLeave.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function testStack(selector: string, expected: string[]) {
|
|
||||||
const children = await browser.executeScript(`
|
|
||||||
return Array.from(
|
|
||||||
document.querySelector('${selector}').children
|
|
||||||
).map(el => el.tagName.toLowerCase());
|
|
||||||
`);
|
|
||||||
expect(children).toEqual(expected);
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
import { handleErrorMessages, waitTime } from './utils';
|
|
||||||
|
|
||||||
describe('view-child', () => {
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/view-child');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get a reference to all children', async () => {
|
|
||||||
// button should be red
|
|
||||||
expect(await element(by.css('#color-button.ion-color-danger')).isPresent()).toBeTruthy();
|
|
||||||
|
|
||||||
// tabs should be found
|
|
||||||
expect(await element(by.css('#tabs-result')).getText()).toEqual('all found');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
14
angular/test/test-app/e2e/src/view-child.spec.ts
Normal file
14
angular/test/test-app/e2e/src/view-child.spec.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
describe('View Child', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/view-child');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get a reference to all children', () => {
|
||||||
|
// button should be red
|
||||||
|
cy.get('#color-button').should('have.class', 'ion-color-danger');
|
||||||
|
|
||||||
|
// tabs should be found
|
||||||
|
cy.get('#tabs-result').should('have.text', 'all found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,18 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
import { waitTime, handleErrorMessages } from './utils';
|
|
||||||
|
|
||||||
describe('virtual-scroll', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
return handleErrorMessages();
|
|
||||||
});
|
|
||||||
beforeEach(async () => {
|
|
||||||
await browser.get('/virtual-scroll');
|
|
||||||
await waitTime(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should open virtual-scroll', () => {
|
|
||||||
const virtualElements = element.all(by.css('ion-virtual-scroll > *'));
|
|
||||||
expect(virtualElements.count()).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
14
angular/test/test-app/e2e/src/virtual-scroll.spec.ts
Normal file
14
angular/test/test-app/e2e/src/virtual-scroll.spec.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
describe('Virtual Scroll', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/virtual-scroll');
|
||||||
|
cy.wait(30);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open virtual-scroll', () => {
|
||||||
|
cy.document().then((doc) => {
|
||||||
|
const virtualElements = doc.querySelectorAll('ion-virtual-scroll > *');
|
||||||
|
expect(virtualElements.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,14 +4,17 @@
|
|||||||
"strictMetadataEmit" : true
|
"strictMetadataEmit" : true
|
||||||
},
|
},
|
||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"src/**spec.ts",
|
||||||
|
"../cypress/support/index.d.ts"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "../out-tsc/app",
|
"outDir": "../out-tsc/app",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"types": [
|
"types": [
|
||||||
"jasmine",
|
"cypress",
|
||||||
"jasminewd2",
|
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
// Karma configuration file, see link for more information
|
|
||||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
basePath: '',
|
|
||||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
|
||||||
plugins: [
|
|
||||||
require('karma-jasmine'),
|
|
||||||
require('karma-chrome-launcher'),
|
|
||||||
require('karma-jasmine-html-reporter'),
|
|
||||||
require('karma-coverage-istanbul-reporter'),
|
|
||||||
require('@angular-devkit/build-angular/plugins/karma')
|
|
||||||
],
|
|
||||||
client: {
|
|
||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
|
||||||
},
|
|
||||||
coverageIstanbulReporter: {
|
|
||||||
dir: require('path').join(__dirname, '../coverage'),
|
|
||||||
reports: ['html', 'lcovonly', 'text-summary'],
|
|
||||||
fixWebpackSourcePaths: true
|
|
||||||
},
|
|
||||||
reporters: ['progress', 'kjhtml'],
|
|
||||||
port: 9876,
|
|
||||||
colors: true,
|
|
||||||
logLevel: config.LOG_INFO,
|
|
||||||
autoWatch: true,
|
|
||||||
browsers: ['Chrome'],
|
|
||||||
customLaunchers: {
|
|
||||||
ChromeHeadlessCI: {
|
|
||||||
base: 'ChromeHeadless',
|
|
||||||
flags: ['--no-sandbox']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
singleRun: false,
|
|
||||||
restartOnFileChange: true
|
|
||||||
});
|
|
||||||
};
|
|
20057
angular/test/test-app/package-lock.json
generated
20057
angular/test/test-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,15 +8,16 @@
|
|||||||
"sync:build": "sh scripts/build-ionic.sh",
|
"sync:build": "sh scripts/build-ionic.sh",
|
||||||
"sync": "sh scripts/sync.sh",
|
"sync": "sh scripts/sync.sh",
|
||||||
"build": "npm run sync && ng build --prod --no-progress",
|
"build": "npm run sync && ng build --prod --no-progress",
|
||||||
"pretest": "webdriver-manager update --versions.chrome 89.0.4389.23",
|
|
||||||
"test": "ng e2e --prod --webdriver-update=false",
|
|
||||||
"test.dev": "npm run sync && ng e2e",
|
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"postinstall": "npm run sync && ngcc",
|
"postinstall": "npm run sync && ngcc",
|
||||||
"serve:ssr": "node dist/test-app/server/main.js",
|
"serve:ssr": "node dist/test-app/server/main.js",
|
||||||
"build:ssr": "ng build --prod && ng run test-app:server:production",
|
"build:ssr": "ng build --prod && ng run test-app:server:production",
|
||||||
"dev:ssr": "ng run test-app:serve-ssr",
|
"dev:ssr": "ng run test-app:serve-ssr",
|
||||||
"prerender": "ng run test-app:prerender"
|
"prerender": "ng run test-app:prerender",
|
||||||
|
"cy.open": "cypress open",
|
||||||
|
"cy.run": "cypress run",
|
||||||
|
"test": "concurrently \"npm run start\" \"wait-on http-get://localhost:4200 && npm run cy.run\" --kill-others --success first",
|
||||||
|
"test.watch": "concurrently \"npm run start\" \"wait-on http-get://localhost:4200 && npm run cy.open\" --kill-others --success first"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^9.1.12",
|
"@angular/animations": "^9.1.12",
|
||||||
@ -34,7 +35,6 @@
|
|||||||
"angular-in-memory-web-api": "^0.11.0",
|
"angular-in-memory-web-api": "^0.11.0",
|
||||||
"core-js": "^2.6.11",
|
"core-js": "^2.6.11",
|
||||||
"express": "^4.15.2",
|
"express": "^4.15.2",
|
||||||
"jasmine-marbles": "^0.6.0",
|
|
||||||
"rxjs": "^6.5.5",
|
"rxjs": "^6.5.5",
|
||||||
"tslib": "^1.13.0",
|
"tslib": "^1.13.0",
|
||||||
"zone.js": "^0.10.3"
|
"zone.js": "^0.10.3"
|
||||||
@ -46,22 +46,15 @@
|
|||||||
"@angular/language-service": "^9.1.12",
|
"@angular/language-service": "^9.1.12",
|
||||||
"@nguniversal/builders": "9.0.0-next.9",
|
"@nguniversal/builders": "9.0.0-next.9",
|
||||||
"@types/express": "^4.17.7",
|
"@types/express": "^4.17.7",
|
||||||
"@types/jasmine": "^3.5.13",
|
|
||||||
"@types/jasminewd2": "^2.0.8",
|
|
||||||
"@types/node": "^12.12.54",
|
"@types/node": "^12.12.54",
|
||||||
"codelyzer": "^5.2.2",
|
"codelyzer": "^5.2.2",
|
||||||
"jasmine-core": "^3.5.0",
|
"concurrently": "^6.0.0",
|
||||||
"jasmine-spec-reporter": "^4.2.1",
|
"cypress": "^6.7.1",
|
||||||
"karma": "^4.4.1",
|
|
||||||
"karma-chrome-launcher": "^3.1.0",
|
|
||||||
"karma-coverage-istanbul-reporter": "^2.1.1",
|
|
||||||
"karma-jasmine": "^3.0.3",
|
|
||||||
"karma-jasmine-html-reporter": "^1.5.4",
|
|
||||||
"protractor": "^5.4.4",
|
|
||||||
"ts-loader": "^6.2.2",
|
"ts-loader": "^6.2.2",
|
||||||
"ts-node": "^8.3.0",
|
"ts-node": "^8.3.0",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"typescript": "^3.8.3",
|
"typescript": "^3.8.3",
|
||||||
|
"wait-on": "^5.2.1",
|
||||||
"webpack-cli": "^3.3.12"
|
"webpack-cli": "^3.3.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ export class ModalComponent {
|
|||||||
async open(TheModalComponent: any) {
|
async open(TheModalComponent: any) {
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
component: TheModalComponent,
|
component: TheModalComponent,
|
||||||
|
animated: false,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
value: '123',
|
value: '123',
|
||||||
prop: '321'
|
prop: '321'
|
||||||
@ -40,7 +41,7 @@ export class ModalComponent {
|
|||||||
modal.onDidDismiss().then(() => {
|
modal.onDidDismiss().then(() => {
|
||||||
NgZone.assertInAngularZone();
|
NgZone.assertInAngularZone();
|
||||||
if (!this.onWillDismiss) {
|
if (!this.onWillDismiss) {
|
||||||
throw new Error('onWillDismiss should be emited first');
|
throw new Error('onWillDismiss should be emitted first');
|
||||||
}
|
}
|
||||||
this.onDidDismiss = true;
|
this.onDidDismiss = true;
|
||||||
});
|
});
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
|
||||||
|
|
||||||
import 'zone.js/dist/zone-testing';
|
|
||||||
import { getTestBed } from '@angular/core/testing';
|
|
||||||
import {
|
|
||||||
BrowserDynamicTestingModule,
|
|
||||||
platformBrowserDynamicTesting
|
|
||||||
} from '@angular/platform-browser-dynamic/testing';
|
|
||||||
|
|
||||||
declare const require: any;
|
|
||||||
|
|
||||||
// First, initialize the Angular testing environment.
|
|
||||||
getTestBed().initTestEnvironment(
|
|
||||||
BrowserDynamicTestingModule,
|
|
||||||
platformBrowserDynamicTesting()
|
|
||||||
);
|
|
||||||
// Then we find all the tests.
|
|
||||||
const context = require.context('./', true, /\.spec\.ts$/);
|
|
||||||
// And load the modules.
|
|
||||||
context.keys().map(context);
|
|
@ -3,7 +3,6 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/spec",
|
"outDir": "./out-tsc/spec",
|
||||||
"types": [
|
"types": [
|
||||||
"jasmine",
|
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
4
core/package-lock.json
generated
4
core/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@ionic/core",
|
"name": "@ionic/core",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@ionic/core",
|
"name": "@ionic/core",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stencil/core": "^2.4.0",
|
"@stencil/core": "^2.4.0",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ionic/core",
|
"name": "@ionic/core",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"description": "Base components for Ionic",
|
"description": "Base components for Ionic",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ionic",
|
"ionic",
|
||||||
|
@ -418,7 +418,7 @@ Developers can also use this component directly in their template:
|
|||||||
header="Albums"
|
header="Albums"
|
||||||
css-class="my-custom-class"
|
css-class="my-custom-class"
|
||||||
:buttons="buttons"
|
:buttons="buttons"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
</ion-action-sheet>
|
</ion-action-sheet>
|
||||||
</template>
|
</template>
|
||||||
|
@ -76,7 +76,7 @@ Developers can also use this component directly in their template:
|
|||||||
header="Albums"
|
header="Albums"
|
||||||
css-class="my-custom-class"
|
css-class="my-custom-class"
|
||||||
:buttons="buttons"
|
:buttons="buttons"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
</ion-action-sheet>
|
</ion-action-sheet>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1664,7 +1664,7 @@ Developers can also use this component directly in their template:
|
|||||||
message="This is an alert message."
|
message="This is an alert message."
|
||||||
css-class="my-custom-class"
|
css-class="my-custom-class"
|
||||||
:buttons="buttons"
|
:buttons="buttons"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
</ion-alert>
|
</ion-alert>
|
||||||
</template>
|
</template>
|
||||||
|
@ -315,7 +315,7 @@ Developers can also use this component directly in their template:
|
|||||||
message="This is an alert message."
|
message="This is an alert message."
|
||||||
css-class="my-custom-class"
|
css-class="my-custom-class"
|
||||||
:buttons="buttons"
|
:buttons="buttons"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
</ion-alert>
|
</ion-alert>
|
||||||
</template>
|
</template>
|
||||||
|
@ -26,6 +26,7 @@ export class Content implements ComponentInterface {
|
|||||||
private cTop = -1;
|
private cTop = -1;
|
||||||
private cBottom = -1;
|
private cBottom = -1;
|
||||||
private scrollEl!: HTMLElement;
|
private scrollEl!: HTMLElement;
|
||||||
|
private isMainContent = true;
|
||||||
|
|
||||||
// Detail is used in a hot loop in the scroll event, by allocating it here
|
// Detail is used in a hot loop in the scroll event, by allocating it here
|
||||||
// V8 will be able to inline any read/write to it since it's a monomorphic class.
|
// V8 will be able to inline any read/write to it since it's a monomorphic class.
|
||||||
@ -104,6 +105,10 @@ export class Content implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
|
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
|
||||||
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this.onScrollEnd();
|
this.onScrollEnd();
|
||||||
}
|
}
|
||||||
@ -303,10 +308,11 @@ export class Content implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { scrollX, scrollY } = this;
|
const { isMainContent, scrollX, scrollY } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const forceOverscroll = this.shouldForceOverscroll();
|
const forceOverscroll = this.shouldForceOverscroll();
|
||||||
const transitionShadow = mode === 'ios';
|
const transitionShadow = mode === 'ios';
|
||||||
|
const TagType = isMainContent ? 'main' : 'div' as any;
|
||||||
|
|
||||||
this.resize();
|
this.resize();
|
||||||
|
|
||||||
@ -323,19 +329,19 @@ export class Content implements ComponentInterface {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div id="background-content" part="background"></div>
|
<div id="background-content" part="background"></div>
|
||||||
<main
|
<TagType
|
||||||
class={{
|
class={{
|
||||||
'inner-scroll': true,
|
'inner-scroll': true,
|
||||||
'scroll-x': scrollX,
|
'scroll-x': scrollX,
|
||||||
'scroll-y': scrollY,
|
'scroll-y': scrollY,
|
||||||
'overscroll': (scrollX || scrollY) && forceOverscroll
|
'overscroll': (scrollX || scrollY) && forceOverscroll
|
||||||
}}
|
}}
|
||||||
ref={el => this.scrollEl = el!}
|
ref={(el: HTMLElement) => this.scrollEl = el!}
|
||||||
onScroll={(this.scrollEvents) ? ev => this.onScroll(ev) : undefined}
|
onScroll={(this.scrollEvents) ? (ev: UIEvent) => this.onScroll(ev) : undefined}
|
||||||
part="scroll"
|
part="scroll"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</main>
|
</TagType>
|
||||||
|
|
||||||
{transitionShadow ? (
|
{transitionShadow ? (
|
||||||
<div class="transition-effect">
|
<div class="transition-effect">
|
||||||
|
@ -641,7 +641,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
aria-disabled={disabled ? 'true' : null}
|
aria-disabled={disabled ? 'true' : null}
|
||||||
aria-expanded={`${isExpanded}`}
|
aria-expanded={`${isExpanded}`}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-labelledby={labelId}
|
aria-labelledby={label ? labelId : null}
|
||||||
class={{
|
class={{
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'datetime-disabled': disabled,
|
'datetime-disabled': disabled,
|
||||||
|
30
core/src/components/datetime/test/a11y/datetime.spec.ts
Normal file
30
core/src/components/datetime/test/a11y/datetime.spec.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { newSpecPage } from '@stencil/core/testing';
|
||||||
|
import { Datetime } from '../../datetime';
|
||||||
|
import { Item } from '../../../item/item';
|
||||||
|
import { Label } from '../../../label/label';
|
||||||
|
|
||||||
|
describe('Datetime a11y', () => {
|
||||||
|
it('does not set a default aria-labelledby when there is not a neighboring ion-label', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [Datetime, Item, Label],
|
||||||
|
html: `<ion-datetime></ion-datetime>`
|
||||||
|
})
|
||||||
|
|
||||||
|
const ariaLabelledBy = page.root.getAttribute('aria-labelledby');
|
||||||
|
expect(ariaLabelledBy).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set a default aria-labelledby when a neighboring ion-label exists', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [Datetime, Item, Label],
|
||||||
|
html: `<ion-item>
|
||||||
|
<ion-label>A11y Test</ion-label>
|
||||||
|
<ion-datetime></ion-datetime>
|
||||||
|
</ion-item>`
|
||||||
|
})
|
||||||
|
|
||||||
|
const label = page.body.querySelector('ion-label');
|
||||||
|
const ariaLabelledBy = page.body.querySelector('ion-datetime').getAttribute('aria-labelledby');
|
||||||
|
expect(ariaLabelledBy).toBe(label.id);
|
||||||
|
});
|
||||||
|
});
|
@ -12,6 +12,6 @@
|
|||||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head>
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<ion-datetime id="basic" display-format="MMMM" value="2012-12-15T13:47:20.789"></ion-datetime>
|
<ion-datetime id="basic" display-format="MMMM" value="2012-12-15T13:47:20.789" aria-label="datetime picker"></ion-datetime>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -234,7 +234,7 @@ export class Input implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillLoad() {
|
componentWillLoad() {
|
||||||
this.inheritedAttributes = inheritAttributes(this.el, ['tabindex', 'title']);
|
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label', 'tabindex', 'title']);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@ -408,7 +408,7 @@ export class Input implements ComponentInterface {
|
|||||||
<input
|
<input
|
||||||
class="native-input"
|
class="native-input"
|
||||||
ref={input => this.nativeInput = input}
|
ref={input => this.nativeInput = input}
|
||||||
aria-labelledby={labelId}
|
aria-labelledby={label ? labelId : null}
|
||||||
disabled={this.disabled}
|
disabled={this.disabled}
|
||||||
accept={this.accept}
|
accept={this.accept}
|
||||||
autoCapitalize={this.autocapitalize}
|
autoCapitalize={this.autocapitalize}
|
||||||
|
30
core/src/components/input/test/a11y/input.spec.ts
Normal file
30
core/src/components/input/test/a11y/input.spec.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { newSpecPage } from '@stencil/core/testing';
|
||||||
|
import { Input } from '../../input';
|
||||||
|
import { Item } from '../../../item/item';
|
||||||
|
import { Label } from '../../../label/label';
|
||||||
|
|
||||||
|
describe('Input a11y', () => {
|
||||||
|
it('does not set a default aria-labelledby when there is not a neighboring ion-label', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [Input, Item, Label],
|
||||||
|
html: `<ion-input></ion-input>`
|
||||||
|
})
|
||||||
|
|
||||||
|
const ariaLabelledBy = page.body.querySelector('ion-input > input').getAttribute('aria-labelledby');
|
||||||
|
expect(ariaLabelledBy).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set a default aria-labelledby when a neighboring ion-label exists', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [Input, Item, Label],
|
||||||
|
html: `<ion-item>
|
||||||
|
<ion-label>A11y Test</ion-label>
|
||||||
|
<ion-input></ion-input>
|
||||||
|
</ion-item>`
|
||||||
|
})
|
||||||
|
|
||||||
|
const label = page.body.querySelector('ion-label');
|
||||||
|
const ariaLabelledBy = page.body.querySelector('ion-input > input').getAttribute('aria-labelledby');
|
||||||
|
expect(ariaLabelledBy).toBe(label.id);
|
||||||
|
});
|
||||||
|
});
|
@ -46,6 +46,10 @@
|
|||||||
<ion-input id="input3" value="inputs"></ion-input>
|
<ion-input id="input3" value="inputs"></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-input id="input4" placeholder="No Label" aria-label="input4"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
Number Test
|
Number Test
|
||||||
@ -59,6 +63,10 @@
|
|||||||
Default Test
|
Default Test
|
||||||
<span id="defaultInputResult"></span>
|
<span id="defaultInputResult"></span>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
No Label Test
|
||||||
|
<span id="noLabelInputResult"></span>
|
||||||
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
</ion-list>
|
</ion-list>
|
||||||
@ -74,6 +82,9 @@
|
|||||||
var defaultInput = checkInput('input3');
|
var defaultInput = checkInput('input3');
|
||||||
updateResult(defaultInput, 'defaultInputResult');
|
updateResult(defaultInput, 'defaultInputResult');
|
||||||
|
|
||||||
|
var noLabelInput = checkInput('input4');
|
||||||
|
updateResult(noLabelInput, 'noLabelInputResult');
|
||||||
|
|
||||||
// Update results of input
|
// Update results of input
|
||||||
function updateResult(result, resultId) {
|
function updateResult(result, resultId) {
|
||||||
var resultEl = document.getElementById(resultId);
|
var resultEl = document.getElementById(resultId);
|
||||||
@ -122,6 +133,14 @@
|
|||||||
readonly: undefined,
|
readonly: undefined,
|
||||||
disabled: undefined
|
disabled: undefined
|
||||||
});
|
});
|
||||||
|
} else if (id === 'input4') {
|
||||||
|
return testAttributes(el, inputEl, {
|
||||||
|
id: 'input4',
|
||||||
|
type: undefined,
|
||||||
|
readonly: undefined,
|
||||||
|
disabled: undefined,
|
||||||
|
'aria-label': 'input4'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -312,7 +312,7 @@ Developers can also use this component directly in their template:
|
|||||||
cssClass="my-custom-class"
|
cssClass="my-custom-class"
|
||||||
message="Please wait..."
|
message="Please wait..."
|
||||||
:duration="timeout"
|
:duration="timeout"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
</ion-loading>
|
</ion-loading>
|
||||||
</template>
|
</template>
|
||||||
|
@ -63,7 +63,7 @@ Developers can also use this component directly in their template:
|
|||||||
cssClass="my-custom-class"
|
cssClass="my-custom-class"
|
||||||
message="Please wait..."
|
message="Please wait..."
|
||||||
:duration="timeout"
|
:duration="timeout"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
</ion-loading>
|
</ion-loading>
|
||||||
</template>
|
</template>
|
||||||
|
@ -717,7 +717,7 @@ Developers can also use this component directly in their template:
|
|||||||
<ion-modal
|
<ion-modal
|
||||||
:is-open="isOpenRef"
|
:is-open="isOpenRef"
|
||||||
css-class="my-custom-class"
|
css-class="my-custom-class"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
<Modal :data="data"></Modal>
|
<Modal :data="data"></Modal>
|
||||||
</ion-modal>
|
</ion-modal>
|
||||||
|
@ -69,7 +69,7 @@ Developers can also use this component directly in their template:
|
|||||||
<ion-modal
|
<ion-modal
|
||||||
:is-open="isOpenRef"
|
:is-open="isOpenRef"
|
||||||
css-class="my-custom-class"
|
css-class="my-custom-class"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
<Modal :data="data"></Modal>
|
<Modal :data="data"></Modal>
|
||||||
</ion-modal>
|
</ion-modal>
|
||||||
|
@ -330,7 +330,7 @@ Developers can also use this component directly in their template:
|
|||||||
css-class="my-custom-class"
|
css-class="my-custom-class"
|
||||||
:event="event"
|
:event="event"
|
||||||
:translucent="true"
|
:translucent="true"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
<Popover></Popover>
|
<Popover></Popover>
|
||||||
</ion-popover>
|
</ion-popover>
|
||||||
|
@ -60,7 +60,7 @@ Developers can also use this component directly in their template:
|
|||||||
css-class="my-custom-class"
|
css-class="my-custom-class"
|
||||||
:event="event"
|
:event="event"
|
||||||
:translucent="true"
|
:translucent="true"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
<Popover></Popover>
|
<Popover></Popover>
|
||||||
</ion-popover>
|
</ion-popover>
|
||||||
|
@ -141,10 +141,11 @@ export class RadioGroup implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the radio group value when a user presses the
|
// Update the radio group value when a user presses the
|
||||||
// space bar on top of a selected radio (only applies
|
// space bar on top of a selected radio
|
||||||
// to radios in a select popover)
|
|
||||||
if (['Space'].includes(ev.code)) {
|
if (['Space'].includes(ev.code)) {
|
||||||
this.value = current.value;
|
this.value = (this.allowEmptySelection && this.value !== undefined)
|
||||||
|
? undefined
|
||||||
|
: current.value;
|
||||||
|
|
||||||
// Prevent browsers from jumping
|
// Prevent browsers from jumping
|
||||||
// to the bottom of the screen
|
// to the bottom of the screen
|
||||||
|
88
core/src/components/radio-group/test/radio-group.e2e.ts
Normal file
88
core/src/components/radio-group/test/radio-group.e2e.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param page the E2E page that contains the radio button
|
||||||
|
* @param radioButtonId the id of the radio button to focus
|
||||||
|
* @returns the checked property of the focused radio button
|
||||||
|
*/
|
||||||
|
const selectRadio = async (page, radioButtonId: string, selectionMethod: 'keyboard' | 'mouse'): Promise<boolean> => {
|
||||||
|
const selector = `ion-radio#${radioButtonId}`;
|
||||||
|
if (selectionMethod === 'keyboard') {
|
||||||
|
await page.focus(selector);
|
||||||
|
await page.keyboard.press('Space');
|
||||||
|
} else if (selectionMethod === 'mouse') {
|
||||||
|
await page.click(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForChanges();
|
||||||
|
|
||||||
|
const radioGroup = await page.find(`ion-radio#${radioButtonId} >>> input`);
|
||||||
|
const checked = await radioGroup.getProperty('checked');
|
||||||
|
return checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('radio-group', () => {
|
||||||
|
it('Spacebar should not deselect without allowEmptySelection', async () => {
|
||||||
|
const page = await newE2EPage();
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-radio-group value="one" allow-empty-selection="false">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>One</ion-label>
|
||||||
|
<ion-radio id="one" value="one"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const checked = await selectRadio(page, 'one', 'keyboard');
|
||||||
|
|
||||||
|
expect(checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Spacebar should deselect with allowEmptySelection', async () => {
|
||||||
|
const page = await newE2EPage();
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-radio-group value="one" allow-empty-selection="true">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>One</ion-label>
|
||||||
|
<ion-radio id="one" value="one"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const checked = await selectRadio(page, 'one', 'keyboard');
|
||||||
|
|
||||||
|
expect(checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Click should not deselect without allowEmptySelection', async () => {
|
||||||
|
const page = await newE2EPage();
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-radio-group value="one" allow-empty-selection="false">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>One</ion-label>
|
||||||
|
<ion-radio id="one" value="one"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const checked = await selectRadio(page, 'one', 'mouse');
|
||||||
|
|
||||||
|
expect(checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Click should deselect with allowEmptySelection', async () => {
|
||||||
|
const page = await newE2EPage();
|
||||||
|
await page.setContent(`
|
||||||
|
<ion-radio-group value="one" allow-empty-selection="true">
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>One</ion-label>
|
||||||
|
<ion-radio id="one" value="one"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const checked = await selectRadio(page, 'one', 'mouse');
|
||||||
|
|
||||||
|
expect(checked).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@ -166,6 +166,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(:focus) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
// Segment Button: Hover
|
// Segment Button: Hover
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
@ -86,13 +86,28 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get tabIndex() {
|
||||||
|
if (this.disabled) { return -1; }
|
||||||
|
|
||||||
|
const hasTabIndex = this.el.hasAttribute('tabindex');
|
||||||
|
|
||||||
|
if (hasTabIndex) {
|
||||||
|
return this.el.getAttribute('tabindex');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl } = this;
|
const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl, tabIndex } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const hasSegmentColor = () => segmentEl !== null && segmentEl.color !== undefined;
|
const hasSegmentColor = () => segmentEl !== null && segmentEl.color !== undefined;
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
|
role="tab"
|
||||||
|
aria-selected={checked ? 'true' : 'false'}
|
||||||
aria-disabled={disabled ? 'true' : null}
|
aria-disabled={disabled ? 'true' : null}
|
||||||
|
tabIndex={tabIndex}
|
||||||
class={{
|
class={{
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'in-toolbar': hostContext('ion-toolbar', this.el),
|
'in-toolbar': hostContext('ion-toolbar', this.el),
|
||||||
@ -113,7 +128,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
aria-pressed={checked ? 'true' : 'false'}
|
tabIndex={-1}
|
||||||
class="button-native"
|
class="button-native"
|
||||||
part="native"
|
part="native"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -425,6 +425,7 @@ export class Segment implements ComponentInterface {
|
|||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
|
role="tablist"
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
class={createColorClasses(this.color, {
|
class={createColorClasses(this.color, {
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
|
11
core/src/components/segment/test/a11y/e2e.ts
Normal file
11
core/src/components/segment/test/a11y/e2e.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
import { AxePuppeteer } from '@axe-core/puppeteer';
|
||||||
|
|
||||||
|
test('segment: axe', async () => {
|
||||||
|
const page = await newE2EPage({
|
||||||
|
url: '/src/components/segment/test/a11y?ionic:_testing=true'
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await new AxePuppeteer(page).analyze();
|
||||||
|
expect(results.violations.length).toEqual(0);
|
||||||
|
});
|
31
core/src/components/segment/test/a11y/index.html
Normal file
31
core/src/components/segment/test/a11y/index.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Segment - a11y</title>
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||||
|
<link href="../../../../../css/core.css" rel="stylesheet">
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||||
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Segment</h1>
|
||||||
|
<ion-segment aria-label="Tab Options" color="dark" value="reading-list">
|
||||||
|
<ion-segment-button value="bookmarks">
|
||||||
|
<ion-label>Bookmarks</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button value="reading-list">
|
||||||
|
<ion-label>Reading List</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
<ion-segment-button value="shared-links">
|
||||||
|
<ion-label>Shared Links</ion-label>
|
||||||
|
</ion-segment-button>
|
||||||
|
</ion-segment>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
24
core/src/components/segment/test/a11y/screen-readers.md
Normal file
24
core/src/components/segment/test/a11y/screen-readers.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"native" refers to this sample: https://w3c.github.io/aria-practices/examples/tabs/tabs-2/tabs.html
|
||||||
|
|
||||||
|
### Tabbing to Segment Button
|
||||||
|
|
||||||
|
| | native | Ionic |
|
||||||
|
| ------------------------ | ------------------------------------------------ | ------------------------------------------------ |
|
||||||
|
| VoiceOver macOS - Chrome | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 3, Tab Options, tab group |
|
||||||
|
| VoiceOver macOS - Safari | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 3, Tab Options, tab group |
|
||||||
|
| VoiceOver iOS | Bookmarks, tab | Bookmarks, tab |
|
||||||
|
| Android TalkBack | Bookmarks, tab | Bookmarks, tab |
|
||||||
|
| Windows NVDA | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 |
|
||||||
|
|
||||||
|
### Selecting Segment Button
|
||||||
|
|
||||||
|
| | native | Ionic |
|
||||||
|
| ------------------------ | -------------------------------------------------------- | ------------------------ |
|
||||||
|
| VoiceOver macOS - Chrome | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group |
|
||||||
|
| VoiceOver macOS - Safari | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group |
|
||||||
|
| VoiceOver iOS | selected, Bookmarks, tab | selected, Bookmarks, tab |
|
||||||
|
| Android TalkBack | selected | selected |
|
||||||
|
| Windows NVDA | BOOKMARKS, tab, 1 of 3, selected | BOOKMARKS, tab, 1 of 3, selected |
|
||||||
|
|
||||||
|
Note: The `aria-label` for tablist is typically only read on the first interaction.
|
||||||
|
|
@ -148,7 +148,7 @@
|
|||||||
console.log('slide transition start', e)
|
console.log('slide transition start', e)
|
||||||
});
|
});
|
||||||
slides.addEventListener('ionSlideTransitionEnd', function (e) {
|
slides.addEventListener('ionSlideTransitionEnd', function (e) {
|
||||||
console.log('slide transistion end', e)
|
console.log('slide transition end', e)
|
||||||
});
|
});
|
||||||
slides.addEventListener('ionSlideDrag', function (e) {
|
slides.addEventListener('ionSlideDrag', function (e) {
|
||||||
console.log('slide drag', e)
|
console.log('slide drag', e)
|
||||||
|
30
core/src/components/textarea/test/a11y/textarea.spec.ts
Normal file
30
core/src/components/textarea/test/a11y/textarea.spec.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { newSpecPage } from '@stencil/core/testing';
|
||||||
|
import { Textarea } from '../../textarea';
|
||||||
|
import { Item } from '../../../item/item';
|
||||||
|
import { Label } from '../../../label/label';
|
||||||
|
|
||||||
|
describe('Textarea a11y', () => {
|
||||||
|
it('does not set a default aria-labelledby when there is not a neighboring ion-label', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [Textarea, Item, Label],
|
||||||
|
html: `<ion-textarea></ion-textarea>`
|
||||||
|
})
|
||||||
|
|
||||||
|
const ariaLabelledBy = page.body.querySelector('ion-textarea textarea').getAttribute('aria-labelledby');
|
||||||
|
expect(ariaLabelledBy).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set a default aria-labelledby when a neighboring ion-label exists', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [Textarea, Item, Label],
|
||||||
|
html: `<ion-item>
|
||||||
|
<ion-label>A11y Test</ion-label>
|
||||||
|
<ion-textarea></ion-textarea>
|
||||||
|
</ion-item>`
|
||||||
|
})
|
||||||
|
|
||||||
|
const label = page.body.querySelector('ion-label');
|
||||||
|
const ariaLabelledBy = page.body.querySelector('ion-textarea textarea').getAttribute('aria-labelledby');
|
||||||
|
expect(ariaLabelledBy).toBe(label.id);
|
||||||
|
});
|
||||||
|
});
|
@ -363,7 +363,7 @@ export class Textarea implements ComponentInterface {
|
|||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
class="native-textarea"
|
class="native-textarea"
|
||||||
aria-labelledby={labelId}
|
aria-labelledby={label ? labelId : null}
|
||||||
ref={el => this.nativeInput = el}
|
ref={el => this.nativeInput = el}
|
||||||
autoCapitalize={this.autocapitalize}
|
autoCapitalize={this.autocapitalize}
|
||||||
autoFocus={this.autofocus}
|
autoFocus={this.autofocus}
|
||||||
|
@ -334,7 +334,7 @@ Developers can also use this component directly in their template:
|
|||||||
:is-open="isOpenRef"
|
:is-open="isOpenRef"
|
||||||
message="Your settings have been saved."
|
message="Your settings have been saved."
|
||||||
:duration="2000"
|
:duration="2000"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
</ion-toast>
|
</ion-toast>
|
||||||
</template>
|
</template>
|
||||||
|
@ -64,7 +64,7 @@ Developers can also use this component directly in their template:
|
|||||||
:is-open="isOpenRef"
|
:is-open="isOpenRef"
|
||||||
message="Your settings have been saved."
|
message="Your settings have been saved."
|
||||||
:duration="2000"
|
:duration="2000"
|
||||||
@onDidDismiss="setOpen(false)"
|
@didDismiss="setOpen(false)"
|
||||||
>
|
>
|
||||||
</ion-toast>
|
</ion-toast>
|
||||||
</template>
|
</template>
|
||||||
|
@ -76,6 +76,11 @@
|
|||||||
<ion-toggle slot="start" style="--border-radius: 0px;--handle-border-radius: 0px;" checked></ion-toggle>
|
<ion-toggle slot="start" style="--border-radius: 0px;--handle-border-radius: 0px;" checked></ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Stop Immediate Event Propagation</ion-label>
|
||||||
|
<ion-toggle slot="start" checked id="eventPropagation"></ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
|
|
||||||
@ -121,6 +126,11 @@
|
|||||||
var isTrue = el[prop] ? false : true;
|
var isTrue = el[prop] ? false : true;
|
||||||
el[prop] = isTrue;
|
el[prop] = isTrue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById('eventPropagation').addEventListener('click', (evt) => {
|
||||||
|
evt.stopImmediatePropagation();
|
||||||
|
console.log('clicked');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
@ -53,6 +53,8 @@ label {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ionic/docs",
|
"name": "@ionic/docs",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"description": "Pre-packaged API documentation for the Ionic docs.",
|
"description": "Pre-packaged API documentation for the Ionic docs.",
|
||||||
"main": "core.json",
|
"main": "core.json",
|
||||||
"types": "core.d.ts",
|
"types": "core.d.ts",
|
||||||
|
36
packages/angular-server/package-lock.json
generated
36
packages/angular-server/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@ionic/angular-server",
|
"name": "@ionic/angular-server",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@ionic/angular-server",
|
"name": "@ionic/angular-server",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/animations": "8.2.13",
|
"@angular/animations": "8.2.13",
|
||||||
@ -16,7 +16,7 @@
|
|||||||
"@angular/core": "8.2.13",
|
"@angular/core": "8.2.13",
|
||||||
"@angular/platform-browser": "8.2.13",
|
"@angular/platform-browser": "8.2.13",
|
||||||
"@angular/platform-server": "8.2.13",
|
"@angular/platform-server": "8.2.13",
|
||||||
"@ionic/core": "5.6.2",
|
"@ionic/core": "5.6.4",
|
||||||
"ng-packagr": "5.7.1",
|
"ng-packagr": "5.7.1",
|
||||||
"tslint": "^5.12.1",
|
"tslint": "^5.12.1",
|
||||||
"tslint-ionic-rules": "0.0.21",
|
"tslint-ionic-rules": "0.0.21",
|
||||||
@ -137,16 +137,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ionic/core": {
|
"node_modules/@ionic/core": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.6.4.tgz",
|
||||||
"integrity": "sha512-hnwd6ln0IZUVfFu2ilZK03b6EdQFqEWiTkL5kayq2gjB3BK/u1IEtV3C9fdwc8NJKopGwdbdQnujj6VhYPzV3Q==",
|
"integrity": "sha512-fxCV/+0ibiaiBn1dsrrOmlLGJlTkqiG6IVXdLpPKimGdFLjy56olDvB5trlz9J5C/nHc7vR5MIiYC0qdTyX7og==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stencil/core": "^2.4.0",
|
"@stencil/core": "^2.4.0",
|
||||||
"ionicons": "^5.5.1",
|
"ionicons": "^5.5.1",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ionic/core/node_modules/tslib": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@sindresorhus/is": {
|
"node_modules/@sindresorhus/is": {
|
||||||
"version": "0.14.0",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||||
@ -5418,14 +5424,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ionic/core": {
|
"@ionic/core": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.6.4.tgz",
|
||||||
"integrity": "sha512-hnwd6ln0IZUVfFu2ilZK03b6EdQFqEWiTkL5kayq2gjB3BK/u1IEtV3C9fdwc8NJKopGwdbdQnujj6VhYPzV3Q==",
|
"integrity": "sha512-fxCV/+0ibiaiBn1dsrrOmlLGJlTkqiG6IVXdLpPKimGdFLjy56olDvB5trlz9J5C/nHc7vR5MIiYC0qdTyX7og==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@stencil/core": "^2.4.0",
|
"@stencil/core": "^2.4.0",
|
||||||
"ionicons": "^5.5.1",
|
"ionicons": "^5.5.1",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^2.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sindresorhus/is": {
|
"@sindresorhus/is": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ionic/angular-server",
|
"name": "@ionic/angular-server",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"description": "Angular SSR Module for Ionic",
|
"description": "Angular SSR Module for Ionic",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ionic",
|
"ionic",
|
||||||
@ -49,7 +49,7 @@
|
|||||||
"@angular/core": "8.2.13",
|
"@angular/core": "8.2.13",
|
||||||
"@angular/platform-browser": "8.2.13",
|
"@angular/platform-browser": "8.2.13",
|
||||||
"@angular/platform-server": "8.2.13",
|
"@angular/platform-server": "8.2.13",
|
||||||
"@ionic/core": "5.6.3",
|
"@ionic/core": "5.6.5",
|
||||||
"ng-packagr": "5.7.1",
|
"ng-packagr": "5.7.1",
|
||||||
"tslint": "^5.12.1",
|
"tslint": "^5.12.1",
|
||||||
"tslint-ionic-rules": "0.0.21",
|
"tslint-ionic-rules": "0.0.21",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ionic/react-router",
|
"name": "@ionic/react-router",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"description": "React Router wrapper for @ionic/react",
|
"description": "React Router wrapper for @ionic/react",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ionic",
|
"ionic",
|
||||||
@ -39,16 +39,16 @@
|
|||||||
"tslib": "*"
|
"tslib": "*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ionic/core": "5.6.3",
|
"@ionic/core": "5.6.5",
|
||||||
"@ionic/react": "5.6.3",
|
"@ionic/react": "5.6.5",
|
||||||
"react": ">=16.8.6",
|
"react": ">=16.8.6",
|
||||||
"react-dom": ">=16.8.6",
|
"react-dom": ">=16.8.6",
|
||||||
"react-router": "^5.0.1",
|
"react-router": "^5.0.1",
|
||||||
"react-router-dom": "^5.0.1"
|
"react-router-dom": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ionic/core": "5.6.3",
|
"@ionic/core": "5.6.5",
|
||||||
"@ionic/react": "5.6.3",
|
"@ionic/react": "5.6.5",
|
||||||
"@rollup/plugin-node-resolve": "^8.1.0",
|
"@rollup/plugin-node-resolve": "^8.1.0",
|
||||||
"@testing-library/jest-dom": "^5.11.6",
|
"@testing-library/jest-dom": "^5.11.6",
|
||||||
"@testing-library/react": "^11.2.2",
|
"@testing-library/react": "^11.2.2",
|
||||||
|
@ -243,7 +243,19 @@ class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
|
|||||||
routeDirection: 'back',
|
routeDirection: 'back',
|
||||||
routeAnimation: routeAnimation || routeInfo.routeAnimation,
|
routeAnimation: routeAnimation || routeInfo.routeAnimation,
|
||||||
};
|
};
|
||||||
if (routeInfo.lastPathname === routeInfo.pushedByRoute || prevInfo.pathname === routeInfo.pushedByRoute) {
|
if (
|
||||||
|
routeInfo.lastPathname === routeInfo.pushedByRoute ||
|
||||||
|
(
|
||||||
|
/**
|
||||||
|
* We need to exclude tab switches/tab
|
||||||
|
* context changes here because tabbed
|
||||||
|
* navigation is not linear, but router.back()
|
||||||
|
* will go back in a linear fashion.
|
||||||
|
*/
|
||||||
|
prevInfo.pathname === routeInfo.pushedByRoute &&
|
||||||
|
routeInfo.tab === '' && prevInfo.tab === ''
|
||||||
|
)
|
||||||
|
) {
|
||||||
this.props.history.goBack();
|
this.props.history.goBack();
|
||||||
} else {
|
} else {
|
||||||
this.handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back');
|
this.handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back');
|
||||||
|
46
packages/react-router/test-app/cypress/integration/tabs.js
vendored
Normal file
46
packages/react-router/test-app/cypress/integration/tabs.js
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
describe('Tabs', () => {
|
||||||
|
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/23101
|
||||||
|
it('should return to previous tab instance when using the ion-back-button', () => {
|
||||||
|
cy.visit(`http://localhost:${port}/tabs/tab1`);
|
||||||
|
|
||||||
|
cy.get('#tabs-secondary').click();
|
||||||
|
cy.ionPageVisible('tab1-secondary');
|
||||||
|
|
||||||
|
cy.get('ion-tab-button#tab-button-tab2-secondary').click();
|
||||||
|
cy.ionPageHidden('tab1-secondary');
|
||||||
|
cy.ionPageVisible('tab2-secondary');
|
||||||
|
|
||||||
|
cy.get('ion-tab-button#tab-button-tab1-secondary').click();
|
||||||
|
cy.ionPageHidden('tab2-secondary');
|
||||||
|
cy.ionPageVisible('tab1-secondary');
|
||||||
|
|
||||||
|
cy.ionBackClick('tab1-secondary');
|
||||||
|
cy.ionPageDoesNotExist('tabs-secondary');
|
||||||
|
cy.ionPageVisible('tab1');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verifies fix for https://github.com/ionic-team/ionic-framework/issues/23087
|
||||||
|
it('should return to correct view and url when going back from child page after switching tabs', () => {
|
||||||
|
cy.visit(`http://localhost:${port}/tabs/tab1`);
|
||||||
|
|
||||||
|
cy.get('#child-one').click();
|
||||||
|
cy.ionPageHidden('tab1');
|
||||||
|
cy.ionPageVisible('tab1child1');
|
||||||
|
|
||||||
|
cy.get('ion-tab-button#tab-button-tab2').click();
|
||||||
|
cy.ionPageHidden('tab1child1');
|
||||||
|
cy.ionPageVisible('tab2');
|
||||||
|
|
||||||
|
cy.get('ion-tab-button#tab-button-tab1').click();
|
||||||
|
cy.ionPageHidden('tab2');
|
||||||
|
cy.ionPageVisible('tab1child1');
|
||||||
|
|
||||||
|
cy.ionBackClick('tab1child1');
|
||||||
|
cy.ionPageDoesNotExist('tab1child1');
|
||||||
|
cy.ionPageVisible('tab1');
|
||||||
|
|
||||||
|
cy.url().should('include', '/tabs/tab1');
|
||||||
|
});
|
||||||
|
});
|
@ -47,12 +47,6 @@ Cypress.Commands.add('ionPageVisible', (pageId) => {
|
|||||||
// cy.get(`div.ion-page[data-pageid=${pageId}]`).should('have.attr', 'style', 'z-index: 101;')
|
// cy.get(`div.ion-page[data-pageid=${pageId}]`).should('have.attr', 'style', 'z-index: 101;')
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('ionPageInvisible', (pageId) => {
|
|
||||||
cy.get(`div.ion-page[data-pageid=${pageId}]`)
|
|
||||||
.should('have.class', 'ion-page-invisible')
|
|
||||||
.should('have.length', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('ionPageHidden', (pageId) => {
|
Cypress.Commands.add('ionPageHidden', (pageId) => {
|
||||||
cy.get(`div.ion-page[data-pageid=${pageId}]`)
|
cy.get(`div.ion-page[data-pageid=${pageId}]`)
|
||||||
.should('have.class', 'ion-page-hidden')
|
.should('have.class', 'ion-page-hidden')
|
||||||
|
@ -34,6 +34,8 @@ import { OutletRef } from './pages/outlet-ref/OutletRef';
|
|||||||
import { SwipeToGoBack } from './pages/swipe-to-go-back/SwipToGoBack';
|
import { SwipeToGoBack } from './pages/swipe-to-go-back/SwipToGoBack';
|
||||||
import Refs from './pages/refs/Refs';
|
import Refs from './pages/refs/Refs';
|
||||||
import DynamicIonpageClassnames from './pages/dynamic-ionpage-classnames/DynamicIonpageClassnames';
|
import DynamicIonpageClassnames from './pages/dynamic-ionpage-classnames/DynamicIonpageClassnames';
|
||||||
|
import Tabs from './pages/tabs/Tabs';
|
||||||
|
import TabsSecondary from './pages/tabs/TabsSecondary';
|
||||||
debugger;
|
debugger;
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@ -51,6 +53,8 @@ const App: React.FC = () => {
|
|||||||
<Route path="/outlet-ref" component={OutletRef} />
|
<Route path="/outlet-ref" component={OutletRef} />
|
||||||
<Route path="/swipe-to-go-back" component={SwipeToGoBack} />
|
<Route path="/swipe-to-go-back" component={SwipeToGoBack} />
|
||||||
<Route path="/dynamic-ionpage-classnames" component={DynamicIonpageClassnames} />
|
<Route path="/dynamic-ionpage-classnames" component={DynamicIonpageClassnames} />
|
||||||
|
<Route path="/tabs" component={Tabs} />
|
||||||
|
<Route path="/tabs-secondary" component={TabsSecondary} />
|
||||||
<Route path="/refs" component={Refs} />
|
<Route path="/refs" component={Refs} />
|
||||||
</IonReactRouter>
|
</IonReactRouter>
|
||||||
</IonApp>
|
</IonApp>
|
||||||
|
95
packages/react-router/test-app/src/pages/tabs/Tabs.tsx
Normal file
95
packages/react-router/test-app/src/pages/tabs/Tabs.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
IonTabs,
|
||||||
|
IonRouterOutlet,
|
||||||
|
IonTabBar,
|
||||||
|
IonTabButton,
|
||||||
|
IonIcon,
|
||||||
|
IonLabel,
|
||||||
|
IonPage,
|
||||||
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonButtons,
|
||||||
|
IonBackButton,
|
||||||
|
IonTitle,
|
||||||
|
IonContent,
|
||||||
|
IonButton,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import { Route, Redirect } from 'react-router';
|
||||||
|
import { triangle, square } from 'ionicons/icons';
|
||||||
|
|
||||||
|
interface Tabs {}
|
||||||
|
|
||||||
|
const Tabs: React.FC<Tabs> = () => {
|
||||||
|
return (
|
||||||
|
<IonTabs>
|
||||||
|
<IonRouterOutlet id="tabs">
|
||||||
|
<Route path="/tabs/tab1" component={Tab1} exact />
|
||||||
|
<Route path="/tabs/tab2" component={Tab2} exact />
|
||||||
|
<Route path="/tabs/tab1/child" component={Tab1Child1} exact />
|
||||||
|
<Redirect from="/tabs" to="/tabs/tab1" exact />
|
||||||
|
</IonRouterOutlet>
|
||||||
|
<IonTabBar slot="bottom">
|
||||||
|
<IonTabButton tab="tab1" href="/tabs/tab1">
|
||||||
|
<IonIcon icon={triangle} />
|
||||||
|
<IonLabel>Tab1</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
<IonTabButton tab="tab2" href="/tabs/tab2">
|
||||||
|
<IonIcon icon={square} />
|
||||||
|
<IonLabel>Tab2</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
</IonTabBar>
|
||||||
|
</IonTabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tab1 = () => {
|
||||||
|
return (
|
||||||
|
<IonPage data-pageid="tab1">
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Tab1</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
<IonButton routerLink="/tabs/tab1/child" id="child-one">Go to Tab1Child1</IonButton>
|
||||||
|
<IonButton routerLink="/tabs-secondary/tab1" id="tabs-secondary">Go to Secondary Tabs</IonButton>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tab1Child1 = () => {
|
||||||
|
return (
|
||||||
|
<IonPage data-pageid="tab1child1">
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonBackButton />
|
||||||
|
</IonButtons>
|
||||||
|
<IonTitle>Tab1</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tab2 = () => {
|
||||||
|
return (
|
||||||
|
<IonPage data-pageid="tab2">
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Tab2</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
Tab 2
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabs;
|
@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
IonTabs,
|
||||||
|
IonRouterOutlet,
|
||||||
|
IonTabBar,
|
||||||
|
IonTabButton,
|
||||||
|
IonIcon,
|
||||||
|
IonLabel,
|
||||||
|
IonPage,
|
||||||
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonButtons,
|
||||||
|
IonBackButton,
|
||||||
|
IonTitle,
|
||||||
|
IonContent,
|
||||||
|
IonButton,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import { Route, Redirect } from 'react-router';
|
||||||
|
import { triangle, square } from 'ionicons/icons';
|
||||||
|
|
||||||
|
interface TabsSecondary {}
|
||||||
|
|
||||||
|
const TabsSecondary: React.FC<TabsSecondary> = () => {
|
||||||
|
return (
|
||||||
|
<IonTabs>
|
||||||
|
<IonRouterOutlet id="tabs-secondary">
|
||||||
|
<Route path="/tabs-secondary/tab1" component={Tab1} exact />
|
||||||
|
<Route path="/tabs-secondary/tab2" component={Tab2} exact />
|
||||||
|
<Redirect from="/tabs-secondary" to="/tabs-secondary/tab1" exact />
|
||||||
|
</IonRouterOutlet>
|
||||||
|
<IonTabBar slot="bottom">
|
||||||
|
<IonTabButton tab="tab1-secondary" href="/tabs-secondary/tab1">
|
||||||
|
<IonIcon icon={triangle} />
|
||||||
|
<IonLabel>Tab1</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
<IonTabButton tab="tab2-secondary" href="/tabs-secondary/tab2">
|
||||||
|
<IonIcon icon={square} />
|
||||||
|
<IonLabel>Tab2</IonLabel>
|
||||||
|
</IonTabButton>
|
||||||
|
</IonTabBar>
|
||||||
|
</IonTabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tab1 = () => {
|
||||||
|
return (
|
||||||
|
<IonPage data-pageid="tab1-secondary">
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Tab1</IonTitle>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonBackButton />
|
||||||
|
</IonButtons>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
Tab 1
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tab2 = () => {
|
||||||
|
return (
|
||||||
|
<IonPage data-pageid="tab2-secondary">
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Tab2</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
Tab 2
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabsSecondary;
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ionic/react",
|
"name": "@ionic/react",
|
||||||
"version": "5.6.3",
|
"version": "5.6.5",
|
||||||
"description": "React specific wrapper for @ionic/core",
|
"description": "React specific wrapper for @ionic/core",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ionic",
|
"ionic",
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"css/"
|
"css/"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ionic/core": "5.6.3",
|
"@ionic/core": "5.6.5",
|
||||||
"ionicons": "^5.1.2",
|
"ionicons": "^5.1.2",
|
||||||
"tslib": "*"
|
"tslib": "*"
|
||||||
},
|
},
|
||||||
|
@ -22,7 +22,7 @@ interface IonIconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InternalProps = IonIconProps & {
|
type InternalProps = IonIconProps & {
|
||||||
forwardedRef?: React.RefObject<HTMLIonIconElement>;
|
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
class IonIconContainer extends React.PureComponent<InternalProps> {
|
class IonIconContainer extends React.PureComponent<InternalProps> {
|
||||||
|
@ -9,7 +9,7 @@ import { createForwardRef } from './utils';
|
|||||||
interface IonPageProps extends IonicReactProps {}
|
interface IonPageProps extends IonicReactProps {}
|
||||||
|
|
||||||
interface IonPageInternalProps extends IonPageProps {
|
interface IonPageInternalProps extends IonPageProps {
|
||||||
forwardedRef?: React.RefObject<HTMLDivElement>;
|
forwardedRef?: React.ForwardedRef<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class IonPageInternal extends React.Component<IonPageInternalProps> {
|
class IonPageInternal extends React.Component<IonPageInternalProps> {
|
||||||
|
@ -10,12 +10,12 @@ import { createForwardRef } from './utils';
|
|||||||
|
|
||||||
type Props = LocalJSX.IonRouterOutlet & {
|
type Props = LocalJSX.IonRouterOutlet & {
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
ref?: React.RefObject<any>;
|
ref?: React.Ref<any>;
|
||||||
ionPage?: boolean;
|
ionPage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface InternalProps extends Props {
|
interface InternalProps extends Props {
|
||||||
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
|
forwardedRef?: React.ForwardedRef<HTMLIonRouterOutletElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InternalState {}
|
interface InternalState {}
|
||||||
|
@ -27,7 +27,7 @@ describe('createComponent - events', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createComponent - ref', () => {
|
describe('createComponent - ref', () => {
|
||||||
test('should pass ref on to web component instance', () => {
|
test('should pass ref on to web component instance (RefObject)', () => {
|
||||||
const ionButtonRef: React.RefObject<any> = React.createRef();
|
const ionButtonRef: React.RefObject<any> = React.createRef();
|
||||||
const IonButton = createReactComponent<JSX.IonButton, HTMLIonButtonElement>('ion-button');
|
const IonButton = createReactComponent<JSX.IonButton, HTMLIonButtonElement>('ion-button');
|
||||||
|
|
||||||
@ -35,6 +35,16 @@ describe('createComponent - ref', () => {
|
|||||||
const ionButtonItem = getByText('ButtonNameA');
|
const ionButtonItem = getByText('ButtonNameA');
|
||||||
expect(ionButtonRef.current).toEqual(ionButtonItem);
|
expect(ionButtonRef.current).toEqual(ionButtonItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should pass ref on to web component instance (RefCallback)', () => {
|
||||||
|
let current
|
||||||
|
const ionButtonRef: React.RefCallback<any> = value => current = value;
|
||||||
|
const IonButton = createReactComponent<JSX.IonButton, HTMLIonButtonElement>('ion-button');
|
||||||
|
|
||||||
|
const { getByText } = render(<IonButton ref={ionButtonRef}>ButtonNameA</IonButton>);
|
||||||
|
const ionButtonItem = getByText('ButtonNameA');
|
||||||
|
expect(current).toEqual(ionButtonItem);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createComponent - strict mode', () => {
|
describe('createComponent - strict mode', () => {
|
||||||
|
@ -11,10 +11,11 @@ import {
|
|||||||
createForwardRef,
|
createForwardRef,
|
||||||
dashToPascalCase,
|
dashToPascalCase,
|
||||||
isCoveredByReact,
|
isCoveredByReact,
|
||||||
|
mergeRefs,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<ElementType> {
|
interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<ElementType> {
|
||||||
forwardedRef?: React.RefObject<ElementType>;
|
forwardedRef?: React.ForwardedRef<ElementType>;
|
||||||
href?: string;
|
href?: string;
|
||||||
routerLink?: string;
|
routerLink?: string;
|
||||||
ref?: React.Ref<any>;
|
ref?: React.Ref<any>;
|
||||||
@ -31,12 +32,14 @@ export const createReactComponent = <PropType, ElementType>(
|
|||||||
const ReactComponent = class extends React.Component<IonicReactInternalProps<PropType>> {
|
const ReactComponent = class extends React.Component<IonicReactInternalProps<PropType>> {
|
||||||
context!: React.ContextType<typeof NavContext>;
|
context!: React.ContextType<typeof NavContext>;
|
||||||
ref: React.RefObject<HTMLElement>;
|
ref: React.RefObject<HTMLElement>;
|
||||||
|
stableMergedRefs: React.RefCallback<HTMLElement>
|
||||||
|
|
||||||
constructor(props: IonicReactInternalProps<PropType>) {
|
constructor(props: IonicReactInternalProps<PropType>) {
|
||||||
super(props);
|
super(props);
|
||||||
// If we weren't given a ref to forward, we still need one
|
// Create a local ref to to attach props to the wrapped element.
|
||||||
// in order to attach props to the wrapped element.
|
|
||||||
this.ref = React.createRef();
|
this.ref = React.createRef();
|
||||||
|
// React refs must be stable (not created inline).
|
||||||
|
this.stableMergedRefs = mergeRefs(this.ref, this.props.forwardedRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -44,9 +47,7 @@ export const createReactComponent = <PropType, ElementType>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: IonicReactInternalProps<PropType>) {
|
componentDidUpdate(prevProps: IonicReactInternalProps<PropType>) {
|
||||||
// Try to use the forwarded ref to get the child node.
|
const node = this.ref.current! as HTMLElement;
|
||||||
// Otherwise, use the one we created.
|
|
||||||
const node = (this.props.forwardedRef?.current || this.ref.current!) as HTMLElement;
|
|
||||||
attachProps(node, this.props, prevProps);
|
attachProps(node, this.props, prevProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +82,7 @@ export const createReactComponent = <PropType, ElementType>(
|
|||||||
|
|
||||||
const newProps: IonicReactInternalProps<PropType> = {
|
const newProps: IonicReactInternalProps<PropType> = {
|
||||||
...propsToPass,
|
...propsToPass,
|
||||||
ref: forwardedRef || this.ref,
|
ref: this.stableMergedRefs,
|
||||||
style,
|
style,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { OverlayEventDetail } from '@ionic/core';
|
import { OverlayEventDetail } from '@ionic/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { attachProps } from './utils';
|
import { attachProps, setRef } from './utils';
|
||||||
|
|
||||||
interface OverlayBase extends HTMLElement {
|
interface OverlayBase extends HTMLElement {
|
||||||
present: () => Promise<void>;
|
present: () => Promise<void>;
|
||||||
@ -30,7 +30,7 @@ export const createControllerComponent = <
|
|||||||
|
|
||||||
type Props = OptionsType &
|
type Props = OptionsType &
|
||||||
ReactControllerProps & {
|
ReactControllerProps & {
|
||||||
forwardedRef?: React.RefObject<OverlayType>;
|
forwardedRef?: React.ForwardedRef<OverlayType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Overlay extends React.Component<Props> {
|
class Overlay extends React.Component<Props> {
|
||||||
@ -73,9 +73,7 @@ export const createControllerComponent = <
|
|||||||
if (this.props.onDidDismiss) {
|
if (this.props.onDidDismiss) {
|
||||||
this.props.onDidDismiss(event);
|
this.props.onDidDismiss(event);
|
||||||
}
|
}
|
||||||
if (this.props.forwardedRef) {
|
setRef(this.props.forwardedRef, null)
|
||||||
(this.props.forwardedRef as any).current = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async present(prevProps?: Props) {
|
async present(prevProps?: Props) {
|
||||||
@ -106,9 +104,7 @@ export const createControllerComponent = <
|
|||||||
// Check isOpen again since the value could have changed during the async call to controller.create
|
// Check isOpen again since the value could have changed during the async call to controller.create
|
||||||
// It's also possible for the component to have become unmounted.
|
// It's also possible for the component to have become unmounted.
|
||||||
if (this.props.isOpen === true && this.isUnmounted === false) {
|
if (this.props.isOpen === true && this.isUnmounted === false) {
|
||||||
if (this.props.forwardedRef) {
|
setRef(this.props.forwardedRef, this.overlay)
|
||||||
(this.props.forwardedRef as any).current = this.overlay;
|
|
||||||
}
|
|
||||||
await this.overlay.present();
|
await this.overlay.present();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { OverlayEventDetail } from '@ionic/core';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { attachProps } from './utils';
|
import { attachProps, setRef } from './utils';
|
||||||
|
|
||||||
interface OverlayElement extends HTMLElement {
|
interface OverlayElement extends HTMLElement {
|
||||||
present: () => Promise<void>;
|
present: () => Promise<void>;
|
||||||
@ -32,7 +32,7 @@ export const createOverlayComponent = <
|
|||||||
|
|
||||||
type Props = OverlayComponent &
|
type Props = OverlayComponent &
|
||||||
ReactOverlayProps & {
|
ReactOverlayProps & {
|
||||||
forwardedRef?: React.RefObject<OverlayType>;
|
forwardedRef?: React.ForwardedRef<OverlayType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let isDismissing = false;
|
let isDismissing = false;
|
||||||
@ -69,9 +69,7 @@ export const createOverlayComponent = <
|
|||||||
if (this.props.onDidDismiss) {
|
if (this.props.onDidDismiss) {
|
||||||
this.props.onDidDismiss(event);
|
this.props.onDidDismiss(event);
|
||||||
}
|
}
|
||||||
if (this.props.forwardedRef) {
|
setRef(this.props.forwardedRef, null)
|
||||||
(this.props.forwardedRef as any).current = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props) {
|
shouldComponentUpdate(nextProps: Props) {
|
||||||
@ -94,6 +92,13 @@ export const createOverlayComponent = <
|
|||||||
if (this.overlay && prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) {
|
if (this.overlay && prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) {
|
||||||
await this.overlay.dismiss();
|
await this.overlay.dismiss();
|
||||||
isDismissing = false;
|
isDismissing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now that the overlay is dismissed
|
||||||
|
* we need to render again so that any
|
||||||
|
* inner components will be unmounted
|
||||||
|
*/
|
||||||
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,10 +130,7 @@ export const createOverlayComponent = <
|
|||||||
componentProps: {},
|
componentProps: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.props.forwardedRef) {
|
setRef(this.props.forwardedRef, this.overlay);
|
||||||
(this.props.forwardedRef as any).current = this.overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
attachProps(this.overlay, elementProps, prevProps);
|
attachProps(this.overlay, elementProps, prevProps);
|
||||||
|
|
||||||
await this.overlay.present();
|
await this.overlay.present();
|
||||||
|
@ -18,7 +18,7 @@ export const IonBackButtonInner = /*@__PURE__*/ createReactComponent<
|
|||||||
export const IonRouterOutletInner = /*@__PURE__*/ createReactComponent<
|
export const IonRouterOutletInner = /*@__PURE__*/ createReactComponent<
|
||||||
JSX.IonRouterOutlet & {
|
JSX.IonRouterOutlet & {
|
||||||
setRef?: (val: HTMLIonRouterOutletElement) => void;
|
setRef?: (val: HTMLIonRouterOutletElement) => void;
|
||||||
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
|
forwardedRef?: React.ForwardedRef<HTMLIonRouterOutletElement>;
|
||||||
},
|
},
|
||||||
HTMLIonRouterOutletElement
|
HTMLIonRouterOutletElement
|
||||||
>('ion-router-outlet');
|
>('ion-router-outlet');
|
||||||
|
@ -13,7 +13,7 @@ type Props = Omit<LocalJSX.IonBackButton, 'icon'> &
|
|||||||
md: string;
|
md: string;
|
||||||
}
|
}
|
||||||
| string;
|
| string;
|
||||||
ref?: React.RefObject<HTMLIonBackButtonElement>;
|
ref?: React.Ref<HTMLIonBackButtonElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IonBackButton = /*@__PURE__*/ (() =>
|
export const IonBackButton = /*@__PURE__*/ (() =>
|
||||||
|
@ -18,7 +18,7 @@ type IonTabBarProps = LocalJSX.IonTabBar &
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface InternalProps extends IonTabBarProps {
|
interface InternalProps extends IonTabBarProps {
|
||||||
forwardedRef?: React.RefObject<HTMLIonIconElement>;
|
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
|
||||||
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
|
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
|
||||||
routeInfo: RouteInfo;
|
routeInfo: RouteInfo;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { IonTabButtonInner } from '../inner-proxies';
|
|||||||
type Props = LocalJSX.IonTabButton &
|
type Props = LocalJSX.IonTabButton &
|
||||||
IonicReactProps & {
|
IonicReactProps & {
|
||||||
routerOptions?: RouterOptions;
|
routerOptions?: RouterOptions;
|
||||||
ref?: React.RefObject<HTMLIonTabButtonElement>;
|
ref?: React.Ref<HTMLIonTabButtonElement>;
|
||||||
onClick?: (e: any) => void;
|
onClick?: (e: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export const createForwardRef = <PropType, ElementType>(
|
|||||||
) => {
|
) => {
|
||||||
const forwardRef = (
|
const forwardRef = (
|
||||||
props: IonicReactExternalProps<PropType, ElementType>,
|
props: IonicReactExternalProps<PropType, ElementType>,
|
||||||
ref: React.Ref<ElementType>
|
ref: React.ForwardedRef<ElementType>
|
||||||
) => {
|
) => {
|
||||||
return <ReactComponent {...props} forwardedRef={ref} />;
|
return <ReactComponent {...props} forwardedRef={ref} />;
|
||||||
};
|
};
|
||||||
@ -27,6 +27,25 @@ export const createForwardRef = <PropType, ElementType>(
|
|||||||
return React.forwardRef(forwardRef);
|
return React.forwardRef(forwardRef);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setRef = (ref: React.ForwardedRef<any> | React.Ref<any> | undefined, value: any) => {
|
||||||
|
if (typeof ref === 'function') {
|
||||||
|
ref(value)
|
||||||
|
} else if (ref != null) {
|
||||||
|
// Cast as a MutableRef so we can assign current
|
||||||
|
(ref as React.MutableRefObject<any>).current = value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeRefs = (
|
||||||
|
...refs: (React.ForwardedRef<any> | React.Ref<any> | undefined)[]
|
||||||
|
): React.RefCallback<any> => {
|
||||||
|
return (value: any) => {
|
||||||
|
refs.forEach(ref => {
|
||||||
|
setRef(ref, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export * from './attachProps';
|
export * from './attachProps';
|
||||||
export * from './case';
|
export * from './case';
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user