chore(): resolve merge conflicts

This commit is contained in:
Liam DeBeasi
2021-04-23 11:41:46 -04:00
120 changed files with 21206 additions and 2546 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -0,0 +1,8 @@
{
"integrationFolder": "./e2e",
"testFiles": "**/*.spec.ts",
"baseUrl": "http://localhost:4200/",
"ignoreTestFiles": "**/examples/*",
"video": false,
"screenshotOnRunFailure": false
}

View 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
}

View 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')
});

View 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>
}
}

View 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')

View File

@ -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;

View File

@ -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 } }));
}
};

View File

@ -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);
}

View 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);
})
}

View File

@ -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');
});
});

View 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');
});
})

View File

@ -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');
});
});

View 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');
});
});

View File

@ -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();
}

View 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');
});
})

View File

@ -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');
});
});

View 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');
});
});

View File

@ -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');
});
});

View 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');
});
});

View File

@ -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,
});
}

View 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,
});
}

View 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');
});
})

View File

@ -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);
}

View 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);
}

View File

@ -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;
}

View 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);
}

View File

@ -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);
}

View File

@ -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');
});
});

View 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');
});
});

View File

@ -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);
});
});

View 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);
});
});
});

View File

@ -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"
] ]
} }
} }

View File

@ -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
});
};

File diff suppressed because it is too large Load Diff

View File

@ -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"
} }
} }

View File

@ -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;
}); });

View File

@ -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);

View File

@ -3,7 +3,6 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": [
"jasmine",
"node" "node"
] ]
}, },

View File

@ -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",

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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,

View 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);
});
});

View File

@ -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>

View File

@ -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}

View 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);
});
});

View File

@ -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&nbsp; Number Test&nbsp;
@ -59,6 +63,10 @@
Default Test&nbsp; Default Test&nbsp;
<span id="defaultInputResult"></span> <span id="defaultInputResult"></span>
</ion-item> </ion-item>
<ion-item>
No Label Test&nbsp;
<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;
} }

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View 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);
});
});

View File

@ -166,6 +166,10 @@
} }
} }
:host(:focus) {
outline: none;
}
// Segment Button: Hover // Segment Button: Hover
// -------------------------------------------------- // --------------------------------------------------

View File

@ -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}

View File

@ -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,

View 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);
});

View 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>

View 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.

View File

@ -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)

View 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);
});
});

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -53,6 +53,8 @@ label {
align-items: center; align-items: center;
opacity: 0; opacity: 0;
pointer-events: none;
} }
input { input {

View File

@ -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",

View File

@ -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": {

View File

@ -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",

View File

@ -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",

View File

@ -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');

View 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');
});
});

View File

@ -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')

View File

@ -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>

View 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;

View File

@ -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;

View File

@ -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": "*"
}, },

View File

@ -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> {

View File

@ -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> {

View File

@ -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 {}

View File

@ -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', () => {

View File

@ -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,
}; };

View File

@ -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();
} }
} }

View File

@ -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();

View File

@ -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');

View File

@ -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__*/ (() =>

View File

@ -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;
} }

View File

@ -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;
}; };

View File

@ -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