Compare commits

..

1 Commits

Author SHA1 Message Date
Sean Perkins
395b14317f chore: use dev-build 2023-11-15 12:56:32 -05:00
119 changed files with 1568 additions and 5819 deletions

View File

@@ -3,20 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.5.6](https://github.com/ionic-team/ionic-framework/compare/v7.5.5...v7.5.6) (2023-11-21)
### Bug Fixes
* **alert:** match MD spec on tablet ([#28501](https://github.com/ionic-team/ionic-framework/issues/28501)) ([6a2be9f](https://github.com/ionic-team/ionic-framework/commit/6a2be9fa3c12a893d98dc139a1575a6e7e3c7c26)), closes [#23977](https://github.com/ionic-team/ionic-framework/issues/23977)
* **angular:** ng add @ionic/angular in standalone projects ([#28523](https://github.com/ionic-team/ionic-framework/issues/28523)) ([c07312e](https://github.com/ionic-team/ionic-framework/commit/c07312e5ed931f6f825ccf083c9dead9fa815843)), closes [#28514](https://github.com/ionic-team/ionic-framework/issues/28514)
* **angular:** overlays are defined when using standalone controllers ([#28560](https://github.com/ionic-team/ionic-framework/issues/28560)) ([9453132](https://github.com/ionic-team/ionic-framework/commit/9453132aa8952b4adfa1326e61138b329e254f76)), closes [#28385](https://github.com/ionic-team/ionic-framework/issues/28385)
* **datetime:** updating value with min scrolls to new value ([#28549](https://github.com/ionic-team/ionic-framework/issues/28549)) ([388d19e](https://github.com/ionic-team/ionic-framework/commit/388d19e04f83f85abd4602adb04cc71ac575764a)), closes [#28548](https://github.com/ionic-team/ionic-framework/issues/28548)
## [7.5.5](https://github.com/ionic-team/ionic-framework/compare/v7.5.4...v7.5.5) (2023-11-15)

View File

@@ -3,18 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.5.6](https://github.com/ionic-team/ionic-framework/compare/v7.5.5...v7.5.6) (2023-11-21)
### Bug Fixes
* **alert:** match MD spec on tablet ([#28501](https://github.com/ionic-team/ionic-framework/issues/28501)) ([6a2be9f](https://github.com/ionic-team/ionic-framework/commit/6a2be9fa3c12a893d98dc139a1575a6e7e3c7c26)), closes [#23977](https://github.com/ionic-team/ionic-framework/issues/23977)
* **datetime:** updating value with min scrolls to new value ([#28549](https://github.com/ionic-team/ionic-framework/issues/28549)) ([388d19e](https://github.com/ionic-team/ionic-framework/commit/388d19e04f83f85abd4602adb04cc71ac575764a)), closes [#28548](https://github.com/ionic-team/ionic-framework/issues/28548)
## [7.5.5](https://github.com/ionic-team/ionic-framework/compare/v7.5.4...v7.5.5) (2023-11-15)

1625
core/package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "7.5.6",
"version": "7.5.5",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -31,7 +31,7 @@
"loader/"
],
"dependencies": {
"@stencil/core": "^4.7.2",
"@stencil/core": "^4.6.0-dev.1698410852.c526078",
"ionicons": "^7.2.1",
"tslib": "^2.1.0"
},
@@ -64,7 +64,6 @@
"jest": "^29.7.0",
"jest-cli": "^29.7.0",
"prettier": "^2.6.1",
"puppeteer": "21.1.1",
"rollup": "^2.26.4",
"sass": "^1.33.0",
"serve": "^14.0.1",

View File

@@ -1,8 +1,8 @@
import { newSpecPage } from '@stencil/core/testing';
import { AccordionGroup } from '../../accordion-group/accordion-group';
import { Item } from '../../item/item';
import { Accordion } from '../accordion';
import { AccordionGroup } from '../../accordion-group/accordion-group.tsx';
import { Item } from '../../item/item.tsx';
import { Accordion } from '../accordion.tsx';
it('should open correct accordions when accordion group value is set', async () => {
const page = await newSpecPage({
@@ -25,7 +25,7 @@ it('should open correct accordions when accordion group value is set', async ()
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach((accordion) => {
@@ -61,7 +61,7 @@ it('should open correct accordions when accordion value is set', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach((accordion) => {
@@ -97,7 +97,7 @@ it('should open more than one accordion when multiple="true"', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach((accordion) => {
@@ -133,7 +133,7 @@ it('should render with accordion open', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
@@ -162,7 +162,7 @@ it('should accept a string when multiple="true"', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
@@ -183,8 +183,8 @@ it('should set default values if not provided', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordion = accordionGroup.querySelector('ion-accordion')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordion = accordionGroup.querySelector('ion-accordion');
/**
* ID is determined via an auto incrementing counter

View File

@@ -7,17 +7,10 @@ describe('action sheet: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [ActionSheet],
template: () => (
<ion-action-sheet
htmlAttributes={{
'data-testid': 'basic-action-sheet',
}}
overlayIndex={1}
></ion-action-sheet>
),
template: () => <ion-action-sheet htmlAttributes={{ 'data-testid': 'basic-action-sheet' }}></ion-action-sheet>,
});
const actionSheet = page.body.querySelector('ion-action-sheet')!;
const actionSheet = page.body.querySelector('ion-action-sheet');
await expect(actionSheet.getAttribute('data-testid')).toBe('basic-action-sheet');
});

View File

@@ -52,18 +52,9 @@
}
.alert-message {
font-size: $alert-md-message-font-size;
}
max-height: $alert-md-content-max-height;
/**
* MD Alerts on tablets can expand vertically up to
* a total maximum height. We only want to set a max-height
* on mobile phones.
*/
@include mobile-viewport() {
.alert-message {
max-height: $alert-md-content-max-height;
}
font-size: $alert-md-message-font-size;
}
.alert-message:empty {
@@ -111,24 +102,14 @@
.alert-checkbox-group {
position: relative;
max-height: $alert-md-content-max-height;
border-top: $alert-md-list-border-top;
border-bottom: $alert-md-list-border-bottom;
overflow: auto;
}
/**
* MD Alerts on tablets can expand vertically up to
* a total maximum height. We only want to set a max-height
* on mobile phones.
*/
@include mobile-viewport() {
.alert-radio-group,
.alert-checkbox-group {
max-height: $alert-md-content-max-height;
}
}
.alert-tappable {
position: relative;
@@ -301,14 +282,3 @@
.alert-button-inner {
justify-content: $alert-md-button-group-justify-content;
}
/**
* MD alerts should scale up to 560px x 560px
* on tablet dimensions.
*/
@include tablet-viewport() {
:host {
--max-width: #{$alert-md-max-width-tablet};
--max-height: #{$alert-md-max-height-tablet};
}
}

View File

@@ -10,20 +10,6 @@ $alert-md-font-size: dynamic-font(14px) !default;
/// @prop - Max width of the alert
$alert-md-max-width: 280px !default;
/// @prop - Max width of the alert on a tablet
/**
* Large display requirements for MD Alert:
* 1. Maintain a minimum of 48px distance from the leading and
* trailing edges of the screen. (48px * 2 = 96px)
* 2. The width can increase up to 560px.
* 3. The height can increase up to 560px.
* Source: https://m2.material.io/components/dialogs#behavior
*/
$alert-md-max-width-tablet: min(calc(100vw - 96px), 560px) !default;
/// @prop - Max width of the alert on a tablet
$alert-md-max-height-tablet: min(calc(100vh - 96px), 560px) !default;
/// @prop - Border radius of the alert
$alert-md-border-radius: 4px !default;

View File

@@ -84,15 +84,7 @@
font-weight: normal;
}
/**
* Alert has a maximum height in scenarios
* such as the MD alert on tablet devices.
* As a result, we need to make sure the inner
* containers can scroll otherwise content
* may be cut off.
*/
.alert-message,
.alert-input-group {
.alert-message {
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overflow-y: auto;

View File

@@ -1,7 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { config } from '../../../global/config';
import { Alert } from '../alert';
import { config } from '../../../global/config';
describe('alert: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -10,7 +9,7 @@ describe('alert: custom html', () => {
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message')!;
const content = page.body.querySelector('.alert-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -22,7 +21,7 @@ describe('alert: custom html', () => {
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message')!;
const content = page.body.querySelector('.alert-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
@@ -34,7 +33,7 @@ describe('alert: custom html', () => {
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message')!;
const content = page.body.querySelector('.alert-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});

View File

@@ -1,47 +0,0 @@
import { expect } from '@playwright/test';
import { configs, test, Viewports } from '@utils/test/playwright';
/**
* This behavior does not vary across directions.
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('alert: rendering - tablet'), () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(Viewports.tablet.portrait);
await page.goto('/src/components/alert/test/basic', config);
});
test('should expand width and height on larger displays with text', async ({ page }) => {
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator('#longMessage');
const alert = page.locator('ion-alert');
await button.click();
await ionAlertDidPresent.next();
await expect(alert).toHaveScreenshot(screenshot('alert-tablet-text'));
});
test('should expand width and height on larger displays with checkboxes', async ({ page }) => {
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator('#checkbox');
const alert = page.locator('ion-alert');
await button.click();
await ionAlertDidPresent.next();
await expect(alert).toHaveScreenshot(screenshot('alert-tablet-checkboxes'));
});
test('should expand width and height on larger displays with radios', async ({ page }) => {
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator('#radio');
const alert = page.locator('ion-alert');
await button.click();
await ionAlertDidPresent.next();
await expect(alert).toHaveScreenshot(screenshot('alert-tablet-radios'));
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,7 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { Breadcrumb } from '../../breadcrumb/breadcrumb';
import { Breadcrumbs } from '../breadcrumbs';
import { Breadcrumb } from '../../breadcrumb/breadcrumb.tsx';
import { Breadcrumbs } from '../breadcrumbs.tsx';
it('should correctly provide the collapsed breadcrumbs in the event payload', async () => {
const page = await newSpecPage({
@@ -18,8 +18,8 @@ it('should correctly provide the collapsed breadcrumbs in the event payload', as
});
const onCollapsedClick = jest.fn((ev) => ev);
const breadcrumbs = page.body.querySelector('ion-breadcrumbs')!;
const breadcrumb = page.body.querySelectorAll('ion-breadcrumb')!;
const breadcrumbs = page.body.querySelector('ion-breadcrumbs');
const breadcrumb = page.body.querySelectorAll('ion-breadcrumb');
breadcrumbs.addEventListener('ionCollapsedClick', onCollapsedClick);
@@ -46,8 +46,8 @@ it('should exclude the separator from narrators', async () => {
`,
});
const firstBreadcrumb = page.body.querySelector('ion-breadcrumb:first-of-type')!;
const separator = firstBreadcrumb.shadowRoot!.querySelector('[part="separator"]')!;
const firstBreadcrumb = page.body.querySelector('ion-breadcrumb:first-of-type');
const separator = firstBreadcrumb.shadowRoot.querySelector('[part="separator"]');
expect(separator.getAttribute('aria-hidden')).toBe('true');
});
@@ -62,7 +62,7 @@ it('should have color attribute', async () => {
`,
});
const breadcrumbs = page.body.querySelector('ion-breadcrumbs')!;
const breadcrumbs = page.body.querySelector('ion-breadcrumbs');
expect(breadcrumbs.hasAttribute('color')).toBe(true);
});

View File

@@ -1,5 +1,4 @@
import { newSpecPage } from '@stencil/core/testing';
import { Button } from '../../button';
describe('Button: Hidden Form Button', () => {
@@ -16,7 +15,8 @@ describe('Button: Hidden Form Button', () => {
return page.body.querySelectorAll('form button');
};
const button = page.body.querySelector('ion-button')!;
const form = page.body.querySelectorAll('form');
const button = page.body.querySelector('ion-button');
await page.waitForChanges();

View File

@@ -11,7 +11,7 @@ describe('ion-checkbox: disabled', () => {
`,
});
const checkbox = page.body.querySelector('ion-checkbox')!;
const checkbox = page.body.querySelector('ion-checkbox');
expect(checkbox.checked).toBe(false);

View File

@@ -1132,7 +1132,7 @@ export class Datetime implements ComponentInterface {
* so we need to re-init behavior with the new elements.
*/
componentDidRender() {
const { presentation, prevPresentation, calendarBodyRef, minParts, preferWheel, forceRenderDate } = this;
const { presentation, prevPresentation, calendarBodyRef, minParts, preferWheel } = this;
/**
* TODO(FW-2165)
@@ -1150,20 +1150,7 @@ export class Datetime implements ComponentInterface {
const hasCalendarGrid = !preferWheel && ['date-time', 'time-date', 'date'].includes(presentation);
if (minParts !== undefined && hasCalendarGrid && calendarBodyRef) {
const workingMonth = calendarBodyRef.querySelector('.calendar-month:nth-of-type(1)');
/**
* We need to make sure the datetime is not in the process
* of scrolling to a new datetime value if the value
* is updated programmatically.
* Otherwise, the datetime will appear to not scroll at all because
* we are resetting the scroll position to the center of the view.
* Prior to the datetime's value being updated programmatically,
* the calendarBodyRef is scrolled such that the middle month is centered
* in the view. The below code updates the scroll position so the middle
* month is also centered in the view. Since the scroll position did not change,
* the scroll callback in this file does not fire,
* and the resolveForceDateScrolling promise never resolves.
*/
if (workingMonth && forceRenderDate === undefined) {
if (workingMonth) {
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
}
}

View File

@@ -1,22 +1,21 @@
import type { DatetimeParts } from '../datetime-interface';
import { isSameDay, isBefore, isAfter } from '../utils/comparison';
describe('isSameDay()', () => {
it('should return correct results for month, day, and year', () => {
const reference: DatetimeParts = { month: 1, day: 1, year: 2021 };
const reference = { month: 1, day: 1, year: 2021 };
expect(isSameDay(reference, { month: 1, day: 1, year: 2021 })).toEqual(true);
expect(isSameDay(reference, { month: 2, day: 1, year: 2021 })).toEqual(false);
expect(isSameDay(reference, { month: 1, day: 2, year: 2021 })).toEqual(false);
expect(isSameDay(reference, { month: 1, day: 1, year: 2022 })).toEqual(false);
expect(isSameDay(reference, { month: 0, day: 0, year: 0 })).toEqual(false);
expect(isSameDay(reference, { month: null, day: null, year: null } as any)).toEqual(false);
expect(isSameDay(reference, { month: null, day: null, year: null })).toEqual(false);
});
});
describe('isBefore()', () => {
it('should return correct results for month, day, and year', () => {
const reference: DatetimeParts = { month: 1, day: 1, year: 2021 };
const reference = { month: 1, day: 1, year: 2021 };
expect(isBefore(reference, { month: 1, day: 1, year: 2021 })).toEqual(false);
expect(isBefore(reference, { month: 2, day: 1, year: 2021 })).toEqual(true);
@@ -24,13 +23,13 @@ describe('isBefore()', () => {
expect(isBefore(reference, { month: 1, day: 1, year: 2022 })).toEqual(true);
expect(isBefore(reference, { month: 1, day: 1, year: 2020 })).toEqual(false);
expect(isBefore(reference, { month: 0, day: 0, year: 0 })).toEqual(false);
expect(isBefore(reference, { month: null, day: null, year: null } as any)).toEqual(false);
expect(isBefore(reference, { month: null, day: null, year: null })).toEqual(false);
});
});
describe('isAfter()', () => {
it('should return correct results for month, day, and year', () => {
const reference: DatetimeParts = { month: 2, day: 2, year: 2021 };
const reference = { month: 2, day: 2, year: 2021 };
expect(isAfter(reference, { month: 2, day: 2, year: 2021 })).toEqual(false);
expect(isAfter(reference, { month: 2, day: 1, year: 2021 })).toEqual(true);
@@ -43,6 +42,6 @@ describe('isAfter()', () => {
* 2021 > undefined === false
* 2021 > null === true
*/
expect(isAfter(reference, { month: null, day: null, year: null } as any)).toEqual(true);
expect(isAfter(reference, { month: null, day: null, year: null })).toEqual(true);
});
});

View File

@@ -1,4 +1,3 @@
import type { DatetimeParts } from '../datetime-interface';
import {
generateMonths,
getDaysOfWeek,
@@ -365,7 +364,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 50,
} as unknown as DatetimeParts;
};
const minParts = {
day: undefined,
@@ -373,7 +372,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 50,
} as unknown as DatetimeParts;
};
const { hours } = generateTime('en-US', refValue, 'h23', minParts);
@@ -388,7 +387,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 20,
minute: 22,
} as unknown as DatetimeParts;
};
const minParts = {
day: undefined,
@@ -396,7 +395,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 30,
} as unknown as DatetimeParts;
};
const { hours, minutes } = generateTime('en-US', refValue, 'h23', minParts);
@@ -412,7 +411,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 20,
minute: 30,
} as unknown as DatetimeParts;
};
const minParts = {
day: undefined,
@@ -420,7 +419,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 30,
} as unknown as DatetimeParts;
};
const maxParts = {
day: undefined,
@@ -428,7 +427,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 20,
minute: 40,
} as unknown as DatetimeParts;
};
const { hours } = generateTime('en-US', refValue, 'h23', minParts, maxParts);
@@ -442,7 +441,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 13,
minute: 0,
} as unknown as DatetimeParts;
};
const maxParts = {
day: undefined,
@@ -450,7 +449,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 13,
minute: 2,
} as unknown as DatetimeParts;
};
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
@@ -464,7 +463,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 12,
minute: 0,
} as unknown as DatetimeParts;
};
const maxParts = {
day: undefined,
@@ -472,7 +471,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 13,
minute: 2,
} as unknown as DatetimeParts;
};
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
@@ -483,7 +482,7 @@ describe('generateTime()', () => {
describe('getToday', () => {
beforeAll(() => {
jest.useFakeTimers();
jest.useFakeTimers('modern');
// System time is zero based, 1 = February
jest.setSystemTime(new Date(2022, 1, 21, 18, 30));
});

View File

@@ -1,4 +1,3 @@
import type { DatetimeParts } from '../datetime-interface';
import {
generateDayAriaLabel,
getMonthAndDay,
@@ -110,7 +109,7 @@ describe('getLocalizedDayPeriod', () => {
describe('getLocalizedTime', () => {
it('should localize the time to PM', () => {
const datetimeParts: DatetimeParts = {
const datetimeParts = {
day: 1,
month: 1,
year: 2022,
@@ -122,7 +121,7 @@ describe('getLocalizedTime', () => {
});
it('should localize the time to AM', () => {
const datetimeParts: DatetimeParts = {
const datetimeParts = {
day: 1,
month: 1,
year: 2022,
@@ -134,7 +133,7 @@ describe('getLocalizedTime', () => {
});
it('should avoid Chromium bug when using 12 hour time in a 24 hour locale', () => {
const datetimeParts: DatetimeParts = {
const datetimeParts = {
day: 1,
month: 1,
year: 2022,
@@ -145,12 +144,12 @@ describe('getLocalizedTime', () => {
expect(getLocalizedTime('en-GB', datetimeParts, 'h12')).toEqual('12:00 am');
});
it('should parse time-only values correctly', () => {
const datetimeParts: Partial<DatetimeParts> = {
const datetimeParts = {
hour: 22,
minute: 40,
};
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h12')).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h23')).toEqual('22:40');
expect(getLocalizedTime('en-US', datetimeParts, 'h12')).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts, 'h23')).toEqual('22:40');
});
});

View File

@@ -1,4 +1,3 @@
import type { DatetimeParts } from '../datetime-interface';
import {
getPreviousYear,
getNextYear,
@@ -104,31 +103,31 @@ describe('getInternalHourValue()', () => {
describe('calculateHourFromAMPM()', () => {
it('should correctly convert from AM to PM', () => {
expect(calculateHourFromAMPM({ hour: 12, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(12);
expect(calculateHourFromAMPM({ hour: 1, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(13);
expect(calculateHourFromAMPM({ hour: 2, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(14);
expect(calculateHourFromAMPM({ hour: 3, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(15);
expect(calculateHourFromAMPM({ hour: 4, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(16);
expect(calculateHourFromAMPM({ hour: 5, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(17);
expect(calculateHourFromAMPM({ hour: 6, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(18);
expect(calculateHourFromAMPM({ hour: 7, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(19);
expect(calculateHourFromAMPM({ hour: 8, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(20);
expect(calculateHourFromAMPM({ hour: 9, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(21);
expect(calculateHourFromAMPM({ hour: 10, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(22);
expect(calculateHourFromAMPM({ hour: 11, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(23);
expect(calculateHourFromAMPM({ hour: 12, ampm: 'am' }, 'pm')).toEqual(12);
expect(calculateHourFromAMPM({ hour: 1, ampm: 'am' }, 'pm')).toEqual(13);
expect(calculateHourFromAMPM({ hour: 2, ampm: 'am' }, 'pm')).toEqual(14);
expect(calculateHourFromAMPM({ hour: 3, ampm: 'am' }, 'pm')).toEqual(15);
expect(calculateHourFromAMPM({ hour: 4, ampm: 'am' }, 'pm')).toEqual(16);
expect(calculateHourFromAMPM({ hour: 5, ampm: 'am' }, 'pm')).toEqual(17);
expect(calculateHourFromAMPM({ hour: 6, ampm: 'am' }, 'pm')).toEqual(18);
expect(calculateHourFromAMPM({ hour: 7, ampm: 'am' }, 'pm')).toEqual(19);
expect(calculateHourFromAMPM({ hour: 8, ampm: 'am' }, 'pm')).toEqual(20);
expect(calculateHourFromAMPM({ hour: 9, ampm: 'am' }, 'pm')).toEqual(21);
expect(calculateHourFromAMPM({ hour: 10, ampm: 'am' }, 'pm')).toEqual(22);
expect(calculateHourFromAMPM({ hour: 11, ampm: 'am' }, 'pm')).toEqual(23);
expect(calculateHourFromAMPM({ hour: 13, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(1);
expect(calculateHourFromAMPM({ hour: 14, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(2);
expect(calculateHourFromAMPM({ hour: 15, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(3);
expect(calculateHourFromAMPM({ hour: 16, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(4);
expect(calculateHourFromAMPM({ hour: 17, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(5);
expect(calculateHourFromAMPM({ hour: 18, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(6);
expect(calculateHourFromAMPM({ hour: 19, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(7);
expect(calculateHourFromAMPM({ hour: 20, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(8);
expect(calculateHourFromAMPM({ hour: 21, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(9);
expect(calculateHourFromAMPM({ hour: 22, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(10);
expect(calculateHourFromAMPM({ hour: 23, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(11);
expect(calculateHourFromAMPM({ hour: 0, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(12);
expect(calculateHourFromAMPM({ hour: 13, ampm: 'pm' }, 'am')).toEqual(1);
expect(calculateHourFromAMPM({ hour: 14, ampm: 'pm' }, 'am')).toEqual(2);
expect(calculateHourFromAMPM({ hour: 15, ampm: 'pm' }, 'am')).toEqual(3);
expect(calculateHourFromAMPM({ hour: 16, ampm: 'pm' }, 'am')).toEqual(4);
expect(calculateHourFromAMPM({ hour: 17, ampm: 'pm' }, 'am')).toEqual(5);
expect(calculateHourFromAMPM({ hour: 18, ampm: 'pm' }, 'am')).toEqual(6);
expect(calculateHourFromAMPM({ hour: 19, ampm: 'pm' }, 'am')).toEqual(7);
expect(calculateHourFromAMPM({ hour: 20, ampm: 'pm' }, 'am')).toEqual(8);
expect(calculateHourFromAMPM({ hour: 21, ampm: 'pm' }, 'am')).toEqual(9);
expect(calculateHourFromAMPM({ hour: 22, ampm: 'pm' }, 'am')).toEqual(10);
expect(calculateHourFromAMPM({ hour: 23, ampm: 'pm' }, 'am')).toEqual(11);
expect(calculateHourFromAMPM({ hour: 0, ampm: 'pm' }, 'am')).toEqual(12);
});
});

View File

@@ -42,8 +42,7 @@ describe('parseDate()', () => {
* See https://github.com/ionic-team/ionic-framework/commit/3fb4caf21ffac12f765c4c80bf1850e05d211c6a
*/
it('should return the correct time zone offset', () => {
// Casting as any since `tzOffset` does not exist on DatetimeParts
expect((parseDate('2022-12-15T13:47:30-02:00') as any)?.tzOffset).toEqual(undefined);
expect(parseDate('2022-12-15T13:47:30-02:00').tzOffset).toEqual(undefined);
});
it('should parse an array of dates', () => {
@@ -163,8 +162,8 @@ describe('parseMinParts()', () => {
minute: 4,
hour: 2,
};
expect(parseMinParts(undefined as any, today)).toEqual(undefined);
expect(parseMinParts(null as any, today)).toEqual(undefined);
expect(parseMinParts(undefined, today)).toEqual(undefined);
expect(parseMinParts(null, today)).toEqual(undefined);
expect(parseMinParts('foo', today)).toEqual(undefined);
});
});
@@ -226,8 +225,8 @@ describe('parseMaxParts()', () => {
minute: 4,
hour: 2,
};
expect(parseMaxParts(undefined as any, today)).toEqual(undefined);
expect(parseMaxParts(null as any, today)).toEqual(undefined);
expect(parseMaxParts(undefined, today)).toEqual(undefined);
expect(parseMaxParts(null, today)).toEqual(undefined);
expect(parseMaxParts('foo', today)).toEqual(undefined);
});
});

View File

@@ -84,13 +84,13 @@ describe('isPrevMonthDisabled()', () => {
// Date month and year is the same as min month and year
expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { month: 1, year: 2021, day: null })).toEqual(true);
// Date year is the same as min year (month not provided)
expect(
isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { year: 2021, month: null, day: null } as any)
).toEqual(true);
expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(
true
);
// Date year is less than the min year (month not provided)
expect(
isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { year: 2022, month: null, day: null } as any)
).toEqual(true);
expect(isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { year: 2022, month: null, day: null })).toEqual(
true
);
// Date is above the maximum bounds and the previous month does not does not fall within the
// min-max range.
@@ -118,12 +118,12 @@ describe('isPrevMonthDisabled()', () => {
expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null })).toEqual(false);
// Date year is the same as min year,
// but can navigate to a previous month without reducing the year.
expect(
isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { year: 2021, month: null, day: null } as any)
).toEqual(false);
expect(
isPrevMonthDisabled({ month: 2, year: 2021, day: null }, { year: 2021, month: null, day: null } as any)
).toEqual(false);
expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(
false
);
expect(isPrevMonthDisabled({ month: 2, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(
false
);
});
});

View File

@@ -1,7 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { config } from '../../../global/config';
import { InfiniteScrollContent } from '../infinite-scroll-content';
import { config } from '../../../global/config';
describe('infinite-scroll-content: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -10,7 +9,7 @@ describe('infinite-scroll-content: custom html', () => {
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text')!;
const content = page.body.querySelector('.infinite-loading-text');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -22,7 +21,7 @@ describe('infinite-scroll-content: custom html', () => {
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text')!;
const content = page.body.querySelector('.infinite-loading-text');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
@@ -34,7 +33,7 @@ describe('infinite-scroll-content: custom html', () => {
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text2</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text')!;
const content = page.body.querySelector('.infinite-loading-text');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});

View File

@@ -1,5 +1,4 @@
import { newSpecPage } from '@stencil/core/testing';
import { Input } from '../input';
describe('input: rendering', () => {
@@ -9,7 +8,7 @@ describe('input: rendering', () => {
html: '<ion-input title="my title" tabindex="-1" data-form-type="password"></ion-input>',
});
const nativeEl = page.body.querySelector('ion-input input')!;
const nativeEl = page.body.querySelector('ion-input input');
expect(nativeEl.getAttribute('title')).toBe('my title');
expect(nativeEl.getAttribute('tabindex')).toBe('-1');
expect(nativeEl.getAttribute('data-form-type')).toBe('password');
@@ -64,9 +63,9 @@ describe('input: label rendering', () => {
`,
});
const input = page.body.querySelector('ion-input')!;
const input = page.body.querySelector('ion-input');
const labelText = input.querySelector('.label-text-wrapper')!;
const labelText = input.querySelector('.label-text-wrapper');
expect(labelText.textContent).toBe('Label Prop Text');
});
@@ -78,9 +77,9 @@ describe('input: label rendering', () => {
`,
});
const input = page.body.querySelector('ion-input')!;
const input = page.body.querySelector('ion-input');
const labelText = input.querySelector('.label-text-wrapper')!;
const labelText = input.querySelector('.label-text-wrapper');
expect(labelText.textContent).toBe('Label Slot Text');
});
@@ -92,9 +91,9 @@ describe('input: label rendering', () => {
`,
});
const input = page.body.querySelector('ion-input')!;
const input = page.body.querySelector('ion-input');
const labelText = input.querySelector('.label-text-wrapper')!;
const labelText = input.querySelector('.label-text-wrapper');
expect(labelText.textContent).toBe('Label Prop Text');
});

View File

@@ -1,7 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { Item } from '../../../item/item';
import { Input } from '../../input';
import { Item } from '../../../item/item';
it('should render as modern when label is set asynchronously', async () => {
const page = await newSpecPage({
@@ -13,7 +12,7 @@ it('should render as modern when label is set asynchronously', async () => {
`,
});
const input = page.body.querySelector('ion-input')!;
const input = page.body.querySelector('ion-input');
// Template should be modern
expect(input.classList.contains('legacy-input')).toBe(false);

View File

@@ -1,10 +1,9 @@
import { Radio } from '../../../radio/radio.tsx';
import { RadioGroup } from '../../../radio-group/radio-group.tsx';
import { Item } from '../../item.tsx';
import { List } from '../../../list/list.tsx';
import { newSpecPage } from '@stencil/core/testing';
import { List } from '../../../list/list';
import { RadioGroup } from '../../../radio-group/radio-group';
import { Radio } from '../../../radio/radio';
import { Item } from '../../item';
describe('ion-item', () => {
it('should not have a role when used without list', async () => {
const page = await newSpecPage({
@@ -12,7 +11,7 @@ describe('ion-item', () => {
html: `<ion-item>Hello World</ion-item>`,
});
const item = page.body.querySelector('ion-item')!;
const item = page.body.querySelector('ion-item');
expect(item.getAttribute('role')).toBe(null);
});
@@ -28,7 +27,7 @@ describe('ion-item', () => {
`,
});
const item = page.body.querySelector('ion-item')!;
const item = page.body.querySelector('ion-item');
expect(item.getAttribute('role')).toBe('listitem');
});
@@ -46,7 +45,7 @@ describe('ion-item', () => {
`,
});
const item = page.body.querySelector('ion-item')!;
const item = page.body.querySelector('ion-item');
expect(item.getAttribute('role')).toBe(null);
});
});

View File

@@ -7,10 +7,10 @@ describe('loading: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Loading],
template: () => <ion-loading overlayIndex={1} htmlAttributes={{ 'data-testid': 'basic-loading' }}></ion-loading>,
template: () => <ion-loading htmlAttributes={{ 'data-testid': 'basic-loading' }}></ion-loading>,
});
const loading = page.body.querySelector('ion-loading')!;
const loading = page.body.querySelector('ion-loading');
await expect(loading.getAttribute('data-testid')).toBe('basic-loading');
});

View File

@@ -27,7 +27,7 @@
// iOS Card Modal
// --------------------------------------------------
@include mobile-viewport() {
@media screen and (max-width: 767px) {
@supports (width: max(0px, 1px)) {
:host(.modal-card) {
--height: calc(100% - max(30px, var(--ion-safe-area-top)) - 10px);
@@ -60,7 +60,7 @@
}
}
@include tablet-viewport() {
@media screen and (min-width: 768px) {
:host(.modal-card) {
--width: calc(100% - 120px);
--height: calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));

View File

@@ -15,8 +15,8 @@ describe('modal: a11y', () => {
`,
});
const modal = page.body.querySelector('ion-modal')!;
const modalWrapper = modal.shadowRoot!.querySelector('.modal-wrapper')!;
const modal = page.body.querySelector('ion-modal');
const modalWrapper = modal.shadowRoot.querySelector('.modal-wrapper');
await expect(modalWrapper.getAttribute('role')).toBe('alertdialog');
});

View File

@@ -7,10 +7,10 @@ describe('modal: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal htmlAttributes={{ 'data-testid': 'basic-modal' }} overlayIndex={1}></ion-modal>,
template: () => <ion-modal htmlAttributes={{ 'data-testid': 'basic-modal' }}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await expect(modal.getAttribute('data-testid')).toBe('basic-modal');
});

View File

@@ -1,18 +1,19 @@
import { h, setMode } from '@stencil/core';
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { setMode } from '@stencil/core';
import { Content } from '../../../content/content';
import { Modal } from '../../modal';
import { Content } from '../../../content/content';
describe('modal: canDismiss', () => {
describe('modal: regular modal', () => {
it('should dismiss when canDismiss is true', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal overlayIndex={1} animated={false} canDismiss={true}></ion-modal>,
template: () => <ion-modal animated={false} canDismiss={true}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -25,10 +26,10 @@ describe('modal: canDismiss', () => {
it('should not dismiss when canDismiss is false', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal overlayIndex={1} animated={false} canDismiss={false}></ion-modal>,
template: () => <ion-modal animated={false} canDismiss={false}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -43,7 +44,6 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
animated={false}
canDismiss={() => {
return new Promise((resolve) => {
@@ -54,7 +54,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -69,7 +69,6 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
animated={false}
canDismiss={() => {
return new Promise((resolve) => {
@@ -80,7 +79,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -95,24 +94,19 @@ describe('modal: canDismiss', () => {
/**
* Card modal is only available on iOS
*/
setMode(() => 'ios');
setMode((elm) => 'ios');
});
it('should dismiss when canDismiss is true', async () => {
const page = await newSpecPage({
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={true}
>
<ion-modal presentingElement={document.createElement('div')} animated={false} canDismiss={true}>
<ion-content>Test Content</ion-content>
</ion-modal>
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -126,18 +120,13 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={false}
>
<ion-modal presentingElement={document.createElement('div')} animated={false} canDismiss={false}>
<ion-content>Test Content</ion-content>
</ion-modal>
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -152,7 +141,6 @@ describe('modal: canDismiss', () => {
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={() => {
@@ -166,7 +154,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -181,7 +169,6 @@ describe('modal: canDismiss', () => {
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={() => {
@@ -195,7 +182,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -210,17 +197,11 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
canDismiss={true}
></ion-modal>
<ion-modal breakpoints={[0, 1]} initialBreakpoint={1} animated={false} canDismiss={true}></ion-modal>
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -234,17 +215,11 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
canDismiss={false}
></ion-modal>
<ion-modal breakpoints={[0, 1]} initialBreakpoint={1} animated={false} canDismiss={false}></ion-modal>
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -259,7 +234,6 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
@@ -272,7 +246,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -287,7 +261,6 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
@@ -300,7 +273,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -315,15 +288,15 @@ describe('modal: canDismiss', () => {
const canDismiss = jest.fn();
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal overlayIndex={1} animated={false} canDismiss={canDismiss}></ion-modal>,
template: () => <ion-modal animated={false} canDismiss={canDismiss}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
await modal.dismiss('my data', 'my role');
const returnValue = await modal.dismiss('my data', 'my role');
expect(canDismiss).toHaveBeenCalledWith('my data', 'my role');
});

View File

@@ -1,5 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { Config } from '../../../global/config';
import type { ComponentProps } from '../../../interface';
import { Nav } from '../nav';
import type { NavOptions } from '../nav-interface';
@@ -196,6 +197,7 @@ describe('NavController', () => {
.insert(-1, null as any, null, null, trnsDone)
.then(() => {
fail('it should not succeed');
done();
})
.catch((err: Error) => {
const hasCompleted = false;
@@ -250,6 +252,7 @@ describe('NavController', () => {
.pop(null, trnsDone)
.then(() => {
fail('it should not succeed');
done();
})
.catch((err: any) => {
const hasCompleted = false;
@@ -816,10 +819,15 @@ describe('NavController', () => {
beforeEach(async () => {
trnsDone = jest.fn();
const config = new Config();
config.reset({ animated: false });
const page = await newSpecPage({
components: [Nav],
html: `<ion-nav></ion-nav>`,
autoApplyChanges: true,
context: {
config,
},
});
nav = page.rootInstance;
});
@@ -840,7 +848,7 @@ describe('NavController', () => {
pause: jest.fn(),
cancel: jest.fn(),
onfinish: undefined,
} as any;
};
animation.play = () => {
if (animation.onfinish) {

View File

@@ -19,7 +19,6 @@ describe('picker-column', () => {
text: 'Java',
},
],
name: 'programmingLanguages',
};
const page = await newSpecPage({
@@ -27,8 +26,8 @@ describe('picker-column', () => {
template: () => <ion-picker-column col={col}></ion-picker-column>,
});
const firstOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(1)')!;
const secondOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(2)')!;
const firstOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(1)');
const secondOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(2)');
expect(firstOption.getAttribute('aria-label')).toBe('C Sharp');
expect(secondOption.getAttribute('aria-label')).toBe(null);

View File

@@ -15,19 +15,18 @@ describe('picker-column: dynamic options', () => {
const page = await newSpecPage({
components: [PickerColumnCmp],
template: () => <ion-picker-column col={{ options: defaultOptions, name: 'animals' }}></ion-picker-column>,
template: () => <ion-picker-column col={{ options: defaultOptions }}></ion-picker-column>,
});
const pickerCol = page.body.querySelector('ion-picker-column')!;
const pickerCol = page.body.querySelector('ion-picker-column');
pickerCol.col = {
options: [...defaultOptions, { text: 'Carrot', value: 'carrot' }],
name: 'vegetables',
};
await page.waitForChanges();
const pickerOpt = pickerCol.querySelector('.picker-opt:nth(2)')!;
const pickerOpt = pickerCol.querySelector('.picker-opt:nth(2)');
expect(pickerOpt.getAttribute('style')).toContain('transform');
});
});

View File

@@ -5,14 +5,14 @@ import { PickerColumnCmp } from '../picker-column';
describe('picker-column', () => {
it('should add class to host of component', async () => {
const col = { cssClass: 'test-class', options: [], name: 'col' };
const col = { cssClass: 'test-class', options: [] };
const page = await newSpecPage({
components: [PickerColumnCmp],
template: () => <ion-picker-column col={col}></ion-picker-column>,
});
const pickerCol = page.body.querySelector('ion-picker-column')!;
const pickerCol = page.body.querySelector('ion-picker-column');
expect(pickerCol.classList.contains('test-class')).toBe(true);
});
});

View File

@@ -7,10 +7,10 @@ describe('popover: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Popover],
template: () => <ion-popover overlayIndex={1} htmlAttributes={{ 'data-testid': 'basic-popover' }}></ion-popover>,
template: () => <ion-popover htmlAttributes={{ 'data-testid': 'basic-popover' }}></ion-popover>,
});
const popover = page.body.querySelector('ion-popover')!;
const popover = page.body.querySelector('ion-popover');
await expect(popover.getAttribute('data-testid')).toBe('basic-popover');
});

View File

@@ -17,20 +17,20 @@ describe('isTriggerElement', () => {
describe('getIndexOfItem', () => {
it('should return the correct index in an array of ion-items', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
expect(getIndexOfItem(array, array[1])).toEqual(1);
});
it('should return -1 when ion-item not found', () => {
const el = document.createElement('ion-item');
const array = createArrayOfElements(['ion-item', 'ion-item']) as HTMLIonItemElement[];
const array = createArrayOfElements(['ion-item', 'ion-item']);
expect(getIndexOfItem(array, el)).toEqual(-1);
});
it('should return -1 if a non-ion-item is passed in', () => {
const array = createArrayOfElements(['ion-item', 'div', 'ion-item']) as HTMLIonItemElement[];
const array = createArrayOfElements(['ion-item', 'div', 'ion-item']);
expect(getIndexOfItem(array, array[1])).toEqual(-1);
});
@@ -38,24 +38,24 @@ describe('getIndexOfItem', () => {
describe('getNextItem', () => {
it('should get the next item in an array of ion-items', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
expect(getNextItem(array, array[1])).toEqual(array[2]);
});
it('should return undefined if there is no next item', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
expect(getNextItem(array, array[2])).toEqual(undefined);
});
});
describe('getPrevItem', () => {
it('should get the previous item in an array of ion-items', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
expect(getPrevItem(array, array[1])).toEqual(array[0]);
});
it('should return undefined if there is no previous item', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
expect(getPrevItem(array, array[0])).toEqual(undefined);
});
});

View File

@@ -1,8 +1,7 @@
import { Radio } from '../radio.tsx';
import { RadioGroup } from '../../radio-group/radio-group.tsx';
import { newSpecPage } from '@stencil/core/testing';
import { RadioGroup } from '../../radio-group/radio-group';
import { Radio } from '../radio';
describe('ion-radio', () => {
it('should set a default value', async () => {
const radio = new Radio();
@@ -22,7 +21,7 @@ describe('ion-radio', () => {
`,
});
const radio = page.body.querySelector('ion-radio')!;
const radio = page.root.querySelector('ion-radio');
expect(radio.classList.contains('radio-checked')).toBe(false);
radio.value = 'a';
@@ -44,8 +43,8 @@ describe('ion-radio: disabled', () => {
`,
});
const radio = page.body.querySelector('ion-radio')!;
const radioGroup = page.body.querySelector('ion-radio-group')!;
const radio = page.body.querySelector('ion-radio');
const radioGroup = page.body.querySelector('ion-radio-group');
expect(radioGroup.value).toBe(undefined);

View File

@@ -1,10 +1,8 @@
import { newSpecPage } from '@stencil/core/testing';
import { Item } from '../../item/item';
import { Range } from '../range';
import { Item } from '../../item/item';
let sharedRange: Range;
let sharedRange;
describe('Range', () => {
beforeEach(() => {
sharedRange = new Range();
@@ -23,8 +21,7 @@ describe('Range', () => {
];
valueTests.forEach((test) => {
// Casting as any since we are accessing a private API on the range component
expect((sharedRange as any).ensureValueInBounds(test[0])).toBe(test[1]);
expect(sharedRange.ensureValueInBounds(test[0])).toBe(test[1]);
});
});
@@ -61,8 +58,7 @@ describe('Range', () => {
];
valueTests.forEach((test) => {
// Casting as any since we are accessing a private API on the range component
expect((sharedRange as any).ensureValueInBounds(test[0])).toEqual(test[1]);
expect(sharedRange.ensureValueInBounds(test[0])).toEqual(test[1]);
});
});
});
@@ -77,7 +73,7 @@ describe('range id', () => {
</ion-range>`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.getAttribute('id')).toBe('my-custom-range');
});
});
@@ -93,7 +89,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(true);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -108,7 +104,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(true);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});
@@ -123,7 +119,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -138,7 +134,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -153,7 +149,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(true);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -168,7 +164,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(true);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -181,7 +177,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});
@@ -195,7 +191,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});
@@ -210,7 +206,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});
@@ -225,7 +221,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range')!;
const range = page.body.querySelector('ion-range');
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});

View File

@@ -1,7 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { config } from '../../../global/config';
import { RefresherContent } from '../refresher-content';
import { config } from '../../../global/config';
describe('refresher-content: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -10,11 +9,11 @@ describe('refresher-content: custom html', () => {
html: `<ion-refresher-content pulling-text="<button class='custom-pulling-html'>Custom Pulling Text</button>" refreshing-text="<button class='custom-refreshing-html'>Custom Refreshing Text</button>"></ion-refresher-content>`,
});
const pullingContent = page.body.querySelector('.refresher-pulling-text')!;
const pullingContent = page.body.querySelector('.refresher-pulling-text');
expect(pullingContent.textContent).toContain('Custom Pulling Text');
expect(pullingContent.querySelector('button.custom-pulling-html')).toBe(null);
const refreshingContent = page.body.querySelector('.refresher-refreshing-text')!;
const refreshingContent = page.body.querySelector('.refresher-refreshing-text');
expect(refreshingContent.textContent).toContain('Custom Refreshing Text');
expect(refreshingContent.querySelector('button.custom-refreshing-html')).toBe(null);
});
@@ -26,11 +25,11 @@ describe('refresher-content: custom html', () => {
html: `<ion-refresher-content pulling-text="<button class='custom-pulling-html'>Custom Pulling Text</button>" refreshing-text="<button class='custom-refreshing-html'>Custom Refreshing Text</button>"></ion-refresher-content>`,
});
const pullingContent = page.body.querySelector('.refresher-pulling-text')!;
const pullingContent = page.body.querySelector('.refresher-pulling-text');
expect(pullingContent.textContent).toContain('Custom Pulling Text');
expect(pullingContent.querySelector('button.custom-pulling-html')).not.toBe(null);
const refreshingContent = page.body.querySelector('.refresher-refreshing-text')!;
const refreshingContent = page.body.querySelector('.refresher-refreshing-text');
expect(refreshingContent.textContent).toContain('Custom Refreshing Text');
expect(refreshingContent.querySelector('button.custom-refreshing-html')).not.toBe(null);
});
@@ -42,11 +41,11 @@ describe('refresher-content: custom html', () => {
html: `<ion-refresher-content pulling-text="<button class='custom-pulling-html'>Custom Pulling Text</button>" refreshing-text="<button class='custom-html'>Custom Refreshing Text</button>"></ion-refresher-content>`,
});
const pullingContent = page.body.querySelector('.refresher-pulling-text')!;
const pullingContent = page.body.querySelector('.refresher-pulling-text');
expect(pullingContent.textContent).toContain('Custom Pulling Text');
expect(pullingContent.querySelector('button.custom-pulling-html')).toBe(null);
const refreshingContent = page.body.querySelector('.refresher-refreshing-text')!;
const refreshingContent = page.body.querySelector('.refresher-refreshing-text');
expect(refreshingContent.textContent).toContain('Custom Refreshing Text');
expect(refreshingContent.querySelector('button.custom-refreshing-html')).toBe(null);
});

View File

@@ -203,17 +203,17 @@ describe('findChainForSegments', () => {
describe('mergeParams', () => {
it('should merge undefined', () => {
expect(mergeParams(undefined, undefined)).toBeUndefined();
expect(mergeParams(null as any, undefined)).toBeUndefined();
expect(mergeParams(undefined, null as any)).toBeUndefined();
expect(mergeParams(null as any, null as any)).toBeUndefined();
expect(mergeParams(null, undefined)).toBeUndefined();
expect(mergeParams(undefined, null)).toBeUndefined();
expect(mergeParams(null, null)).toBeUndefined();
});
it('should merge undefined with params', () => {
const params = { data: '1' };
expect(mergeParams(undefined, params)).toEqual(params);
expect(mergeParams(null as any, params)).toEqual(params);
expect(mergeParams(null, params)).toEqual(params);
expect(mergeParams(params, undefined)).toEqual(params);
expect(mergeParams(params, null as any)).toEqual(params);
expect(mergeParams(params, null)).toEqual(params);
});
it('should merge params with params', () => {
@@ -253,44 +253,36 @@ describe('RouterSegments', () => {
describe('matchesRedirect', () => {
it('should match empty redirect', () => {
expect(matchesRedirect([''], { from: [''], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect([''], { from: ['*'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect([''], { from: [''], to: [''] })).toBeTruthy();
expect(matchesRedirect([''], { from: ['*'], to: [''] })).toBeTruthy();
expect(matchesRedirect([''], { from: ['hola'], to: { segments: [] } })).toBeFalsy();
expect(matchesRedirect([''], { from: ['hola', '*'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect([''], { from: ['hola'], to: [''] })).toBeFalsy();
expect(matchesRedirect([''], { from: ['hola', '*'], to: [''] })).toBeFalsy();
});
it('should match simple segment redirect', () => {
expect(matchesRedirect(['workouts'], { from: ['workouts'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect(['workouts'], { from: ['*'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', '*'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', 'hola'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect(['workouts'], { from: ['workouts'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts'], { from: ['*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', '*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', 'hola'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts'], { from: ['workouts', '*'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', 'adios'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts'], { from: ['workouts', '*'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', 'adios'], to: [''] })).toBeFalsy();
});
it('should match long route', () => {
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['*'], to: { segments: [''] } })).toBeTruthy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', '*'], to: { segments: [''] } })
).toBeTruthy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', '*'], to: { segments: [''] } })
).toBeTruthy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to'], to: { segments: [''] } })
).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', '*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', '*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['login'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['login', '*'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['login'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['login', '*'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path'], to: [''] })).toBeFalsy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path'], to: { segments: [''] } })
).toBeFalsy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to', '*'], to: { segments: [''] } })
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to', '*'], to: [''] })
).toBeFalsy();
});

View File

@@ -18,21 +18,19 @@ describe('ionic-conference-app', () => {
expect(getRouteIDs('/about', routes)).toEqual(['page-tabs', 'page-about']);
expect(getRouteIDs('/tutorial', routes)).toEqual(['page-tutorial']);
expect(
getRoutePath([{ id: 'PAGE-TABS' }, { id: 'tab-schedule' }, { id: 'page-schedule' }] as RouteID[], routes)
).toEqual('/');
expect(getRoutePath([{ id: 'PAGE-TABS' }, { id: 'tab-schedule' }, { id: 'page-schedule' }], routes)).toEqual('/');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }] as RouteID[], routes)).toEqual('/speaker');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }], routes)).toEqual('/speaker');
expect(
getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }, { id: 'page-speaker-list' }] as RouteID[], routes)
).toEqual('/speaker');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }, { id: 'page-speaker-list' }], routes)).toEqual(
'/speaker'
);
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'PAGE-MAP' }] as RouteID[], routes)).toEqual('/map');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'PAGE-MAP' }], routes)).toEqual('/map');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'page-about' }] as RouteID[], routes)).toEqual('/about');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'page-about' }], routes)).toEqual('/about');
expect(getRoutePath([{ id: 'page-tutorial' }] as RouteID[], routes)).toEqual('/tutorial');
expect(getRoutePath([{ id: 'page-tutorial' }], routes)).toEqual('/tutorial');
});
let win: Window;

View File

@@ -9,7 +9,7 @@ describe('searchbar: rendering', () => {
html: '<ion-searchbar name="search"></ion-searchbar>',
});
const nativeEl = page.body.querySelector('ion-searchbar input')!;
const nativeEl = page.body.querySelector('ion-searchbar input');
expect(nativeEl.getAttribute('name')).toBe('search');
});
});

View File

@@ -9,14 +9,14 @@ it('should disable segment buttons added to disabled segment async', async () =>
html: `<ion-segment disabled="true"></ion-segment>`,
});
const segment = page.body.querySelector('ion-segment')!;
const segment = page.body.querySelector('ion-segment');
segment.innerHTML = `
<ion-segment-button>
<ion-label>Segment Button</ion-label>
</ion-segment-button>`;
await page.waitForChanges();
const segmentButton = page.body.querySelector('ion-segment-button')!;
const segmentButton = page.body.querySelector('ion-segment-button');
expect(segmentButton.disabled).toBe(true);
});
@@ -32,7 +32,7 @@ it('should set checked state when value is set asynchronously', async () => {
`,
});
const segmentButton = page.body.querySelector('ion-segment-button')!;
const segmentButton = page.root.querySelector('ion-segment-button');
expect(segmentButton.classList.contains('segment-button-checked')).toBe(false);

View File

@@ -10,9 +10,9 @@ describe('ion-select', () => {
template: () => <ion-select value="my value" name="my name" disabled={true}></ion-select>,
});
const select = page.body.querySelector('ion-select')!;
const select = page.body.querySelector('ion-select');
const hiddenInput = select.querySelector<HTMLInputElement>('input[type="hidden"]')!;
const hiddenInput = select.querySelector('input[type="hidden"]');
expect(hiddenInput).not.toBe(null);
expect(hiddenInput.value).toBe('my value');
@@ -28,10 +28,10 @@ describe('ion-select', () => {
`,
});
const select = page.body.querySelector('ion-select')!;
const select = page.body.querySelector('ion-select');
const propEl = select.shadowRoot!.querySelector('.label-text');
const slotEl = select.shadowRoot!.querySelector('slot[name="label"]');
const propEl = select.shadowRoot.querySelector('.label-text');
const slotEl = select.shadowRoot.querySelector('slot[name="label"]');
expect(propEl).not.toBe(null);
expect(slotEl).toBe(null);
@@ -44,10 +44,10 @@ describe('ion-select', () => {
`,
});
const select = page.body.querySelector('ion-select')!;
const select = page.body.querySelector('ion-select');
const propEl = select.shadowRoot!.querySelector('.label-text');
const slotEl = select.shadowRoot!.querySelector('slot[name="label"]');
const propEl = select.shadowRoot.querySelector('.label-text');
const slotEl = select.shadowRoot.querySelector('slot[name="label"]');
expect(propEl).toBe(null);
expect(slotEl).not.toBe(null);
@@ -60,10 +60,10 @@ describe('ion-select', () => {
`,
});
const select = page.body.querySelector('ion-select')!;
const select = page.body.querySelector('ion-select');
const propEl = select.shadowRoot!.querySelector('.label-text');
const slotEl = select.shadowRoot!.querySelector('slot[name="label"]');
const propEl = select.shadowRoot.querySelector('.label-text');
const slotEl = select.shadowRoot.querySelector('slot[name="label"]');
expect(propEl).not.toBe(null);
expect(slotEl).toBe(null);

View File

@@ -1,5 +1,4 @@
import { newSpecPage } from '@stencil/core/testing';
import { Textarea } from '../textarea';
it('should inherit attributes', async () => {
@@ -8,7 +7,7 @@ it('should inherit attributes', async () => {
html: '<ion-textarea title="my title" tabindex="-1" data-form-type="password"></ion-textarea>',
});
const nativeEl = page.body.querySelector('ion-textarea textarea')!;
const nativeEl = page.body.querySelector('ion-textarea textarea');
expect(nativeEl.getAttribute('title')).toBe('my title');
expect(nativeEl.getAttribute('tabindex')).toBe('-1');
expect(nativeEl.getAttribute('data-form-type')).toBe('password');
@@ -32,9 +31,9 @@ describe('textarea: label rendering', () => {
`,
});
const textarea = page.body.querySelector('ion-textarea')!;
const textarea = page.body.querySelector('ion-textarea');
const labelText = textarea.querySelector('.label-text-wrapper')!;
const labelText = textarea.querySelector('.label-text-wrapper');
expect(labelText.textContent).toBe('Label Prop Text');
});
@@ -46,9 +45,9 @@ describe('textarea: label rendering', () => {
`,
});
const textarea = page.body.querySelector('ion-textarea')!;
const textarea = page.body.querySelector('ion-textarea');
const labelText = textarea.querySelector('.label-text-wrapper')!;
const labelText = textarea.querySelector('.label-text-wrapper');
expect(labelText.textContent).toBe('Label Prop Slot');
});
@@ -60,9 +59,9 @@ describe('textarea: label rendering', () => {
`,
});
const textarea = page.body.querySelector('ion-textarea')!;
const textarea = page.body.querySelector('ion-textarea');
const labelText = textarea.querySelector('.label-text-wrapper')!;
const labelText = textarea.querySelector('.label-text-wrapper');
expect(labelText.textContent).toBe('Label Prop Text');
});

View File

@@ -1,8 +1,8 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { config } from '../../../global/config';
import { Toast } from '../toast';
import { config } from '../../../global/config';
import { toastController } from '../../../utils/overlays';
describe('toast: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -11,8 +11,8 @@ describe('toast: custom html', () => {
html: `<ion-toast message="<button class='custom-html'>Custom Text</button>"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast')!;
const content = toast.shadowRoot!.querySelector('.toast-message')!;
const toast = page.body.querySelector('ion-toast');
const content = toast.shadowRoot.querySelector('.toast-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -24,8 +24,8 @@ describe('toast: custom html', () => {
html: `<ion-toast message="<button class='custom-html'>Custom Text</button>"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast')!;
const content = toast.shadowRoot!.querySelector('.toast-message')!;
const toast = page.body.querySelector('ion-toast');
const content = toast.shadowRoot.querySelector('.toast-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
@@ -37,8 +37,8 @@ describe('toast: custom html', () => {
html: `<ion-toast message="<button class='custom-html'>Custom Text</button>"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast')!;
const content = toast.shadowRoot!.querySelector('.toast-message')!;
const toast = page.body.querySelector('ion-toast');
const content = toast.shadowRoot.querySelector('.toast-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -56,9 +56,9 @@ describe('toast: a11y smoke test', () => {
html: `<ion-toast message="Message" header="Header"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast')!;
const header = toast.shadowRoot!.querySelector('.toast-header')!;
const message = toast.shadowRoot!.querySelector('.toast-message')!;
const toast = page.body.querySelector('ion-toast');
const header = toast.shadowRoot.querySelector('.toast-header');
const message = toast.shadowRoot.querySelector('.toast-message');
expect(header.getAttribute('aria-hidden')).toBe('true');
expect(message.getAttribute('aria-hidden')).toBe('true');
@@ -74,7 +74,7 @@ describe('toast: a11y smoke test', () => {
`,
});
const toast = page.body.querySelector('ion-toast')!;
const toast = page.body.querySelector('ion-toast');
/**
* Wait for present method to resolve
@@ -83,8 +83,8 @@ describe('toast: a11y smoke test', () => {
await toast.present();
await page.waitForChanges();
const header = toast.shadowRoot!.querySelector('.toast-header')!;
const message = toast.shadowRoot!.querySelector('.toast-message')!;
const header = toast.shadowRoot.querySelector('.toast-header');
const message = toast.shadowRoot.querySelector('.toast-message');
expect(header.getAttribute('aria-hidden')).toBe(null);
expect(message.getAttribute('aria-hidden')).toBe(null);
@@ -98,7 +98,7 @@ describe('toast: duration config', () => {
html: `<ion-toast></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast')!;
const toast = page.body.querySelector('ion-toast');
expect(toast.duration).toBe(0);
});
@@ -111,7 +111,7 @@ describe('toast: duration config', () => {
html: `<ion-toast></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast')!;
const toast = page.body.querySelector('ion-toast');
expect(toast.duration).toBe(5000);
});
@@ -121,10 +121,10 @@ describe('toast: htmlAttributes', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Toast],
template: () => <ion-toast overlayIndex={1} htmlAttributes={{ 'data-testid': 'basic-toast' }}></ion-toast>,
template: () => <ion-toast htmlAttributes={{ 'data-testid': 'basic-toast' }}></ion-toast>,
});
const toast = page.body.querySelector('ion-toast')!;
const toast = page.body.querySelector('ion-toast');
await expect(toast.getAttribute('data-testid')).toBe('basic-toast');
});
@@ -134,12 +134,12 @@ describe('toast: button cancel', () => {
it('should render the cancel button with part button-cancel', async () => {
const page = await newSpecPage({
components: [Toast],
template: () => <ion-toast overlayIndex={1} buttons={[{ text: 'Cancel', role: 'cancel' }]}></ion-toast>,
template: () => <ion-toast buttons={[{ text: 'Cancel', role: 'cancel' }]}></ion-toast>,
});
const toast = page.body.querySelector('ion-toast')!;
const toast = page.body.querySelector('ion-toast');
const buttonCancel = toast.shadowRoot!.querySelector('.toast-button-cancel')!;
const buttonCancel = toast?.shadowRoot?.querySelector('.toast-button-cancel');
expect(buttonCancel.getAttribute('part')).toBe('button cancel');
});

View File

@@ -51,7 +51,7 @@ describe('ion-toggle: disabled', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle')!;
const toggle = page.body.querySelector('ion-toggle');
expect(toggle.checked).toBe(false);

View File

@@ -1,12 +1,11 @@
import type { IonicConfig } from '../../interface';
import { Config } from '../config';
describe('Config', () => {
it('should get a value from the config', () => {
const config = new Config();
config.reset({ mode: 'ios' } as IonicConfig);
expect(config.get('mode')).toEqual('ios');
expect(config.getBoolean('mode')).toBe(false);
config.reset({ name: 'Doc Brown' });
expect(config.get('name')).toEqual('Doc Brown');
expect(config.getBoolean('name')).toBe(false);
});
it('should get a boolean value', () => {
@@ -19,14 +18,14 @@ describe('Config', () => {
bool4: 'hola',
bool5: 0,
bool6: 1,
} as any);
expect(config.getBoolean('bool0' as any)).toEqual(false);
expect(config.getBoolean('bool1' as any)).toEqual(false);
expect(config.getBoolean('bool2' as any)).toEqual(true);
expect(config.getBoolean('bool3' as any)).toEqual(true);
expect(config.getBoolean('bool4' as any)).toEqual(false);
expect(config.getBoolean('bool5' as any)).toEqual(false);
expect(config.getBoolean('bool6' as any)).toEqual(true);
});
expect(config.getBoolean('bool0')).toEqual(false);
expect(config.getBoolean('bool1')).toEqual(false);
expect(config.getBoolean('bool2')).toEqual(true);
expect(config.getBoolean('bool3')).toEqual(true);
expect(config.getBoolean('bool4')).toEqual(false);
expect(config.getBoolean('bool5')).toEqual(false);
expect(config.getBoolean('bool6')).toEqual(true);
});
it('should get a number value', () => {
@@ -37,12 +36,12 @@ describe('Config', () => {
nu2: '200',
nu3: '2.3',
nu4: -100.2,
} as any);
expect(config.getNumber('nu0' as any)).toEqual(0);
expect(config.getNumber('nu1' as any)).toEqual(-1);
expect(config.getNumber('nu2' as any)).toEqual(200);
expect(config.getNumber('nu3' as any)).toEqual(2.3);
expect(config.getNumber('nu4' as any)).toEqual(-100.2);
});
expect(config.getNumber('nu0')).toEqual(0);
expect(config.getNumber('nu1')).toEqual(-1);
expect(config.getNumber('nu2')).toEqual(200);
expect(config.getNumber('nu3')).toEqual(2.3);
expect(config.getNumber('nu4')).toEqual(-100.2);
});
it('should not get fallback', () => {
@@ -57,29 +56,29 @@ describe('Config', () => {
nu0: '0',
nu1: 0,
nu2: 10,
} as any);
expect(config.get('text0' as any, 'HEY')).toEqual('');
expect(config.get('text1' as any, 'HEY')).toEqual('hola');
});
expect(config.get('text0', 'HEY')).toEqual('');
expect(config.get('text1', 'HEY')).toEqual('hola');
expect(config.getBoolean('bool0' as any, true)).toEqual(false);
expect(config.getBoolean('bool1' as any, true)).toEqual(false);
expect(config.getBoolean('bool0', true)).toEqual(false);
expect(config.getBoolean('bool1', true)).toEqual(false);
expect(config.getNumber('nu0' as any, 100)).toEqual(0);
expect(config.getNumber('nu1' as any, 100)).toEqual(0);
expect(config.getNumber('nu2' as any, 100)).toEqual(10);
expect(config.getNumber('nu0', 100)).toEqual(0);
expect(config.getNumber('nu1', 100)).toEqual(0);
expect(config.getNumber('nu2', 100)).toEqual(10);
});
it('should get fallback', () => {
const config = new Config();
expect(config.get('text0' as any, 'HEY')).toEqual('HEY');
expect(config.getBoolean('bool0' as any, true)).toEqual(true);
expect(config.getNumber('nu0' as any, 100)).toEqual(100);
expect(config.get('text0', 'HEY')).toEqual('HEY');
expect(config.getBoolean('bool0', true)).toEqual(true);
expect(config.getNumber('nu0', 100)).toEqual(100);
});
it('should set value', () => {
const config = new Config();
expect(config.get('text0' as any, 'HEY')).toEqual('HEY');
config.set('text0' as any, 'hola');
expect(config.get('text0' as any, 'HEY')).toEqual('hola');
expect(config.get('text0', 'HEY')).toEqual('HEY');
config.set('text0', 'hola');
expect(config.get('text0', 'HEY')).toEqual('hola');
});
});

View File

@@ -1,38 +1,3 @@
/**
* A heuristic that applies CSS to tablet
* viewports.
*
* Usage:
* @include tablet-viewport() {
* :host {
* background-color: green;
* }
* }
*/
@mixin tablet-viewport() {
@media screen and (min-width: 768px) {
@content;
}
}
/**
* A heuristic that applies CSS to mobile
* viewports (i.e. phones, not tablets).
*
* Usage:
* @include mobile-viewport() {
* :host {
* background-color: blue;
* }
* }
*/
@mixin mobile-viewport() {
@media screen and (max-width: 767px) {
@content;
}
}
@mixin input-cover() {
@include position(0, null, null, 0);
@include margin(0);
@@ -252,7 +217,7 @@
$restSelectors: append($restSelectors, $selector, comma);
}
}
// Supported by Chrome.
@if length($hostContextSelectors) > 0 {
@at-root #{$hostContextSelectors} {

View File

@@ -108,7 +108,7 @@ describe('Animation Class', () => {
animation.play();
animation.progressStart();
animation.progressEnd(1, 0);
animation.progressEnd(1);
expect(animation.isRunning()).toEqual(true);
});
@@ -125,9 +125,9 @@ describe('Animation Class', () => {
await animation.play();
animation.progressStart();
animation.progressEnd(0, 0);
animation.progressEnd(0);
await new Promise<void>((resolve) => {
await new Promise((resolve) => {
animation.onFinish(() => {
expect(animation.isRunning()).toEqual(false);
resolve();
@@ -161,8 +161,8 @@ describe('Animation Class', () => {
const el = document.createElement('p');
animation.addElement(el);
animation.addElement(null as any);
animation.addElement(undefined as any);
animation.addElement(null);
animation.addElement(undefined);
expect(animation.elements.length).toEqual(1);
});
@@ -188,8 +188,8 @@ describe('Animation Class', () => {
});
it('should not error when trying to add null or undefined', () => {
animation.addAnimation(null as any);
animation.addAnimation(undefined as any);
animation.addAnimation(null);
animation.addAnimation(undefined);
expect(animation.childAnimations.length).toEqual(0);
});
@@ -312,7 +312,7 @@ describe('Animation Class', () => {
animation.progressStart(true);
expect(animation.getEasing()).toEqual('linear');
animation.progressEnd(0, 0);
animation.progressEnd();
expect(animation.getEasing()).toEqual('ease-in-out');
});
@@ -428,15 +428,9 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.5), [
0.16,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.97), [
0.56,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.33), [
0.11,
]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), [0.16]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.97), [0.56]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.33), [0.11]);
});
it('cubic-bezier(1, 0, 0.68, 0.28)', () => {
@@ -447,15 +441,9 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.08), [
0.6,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.5), [
0.84,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.94), [
0.98,
]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.08), [0.6]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), [0.84]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.94), [0.98]);
});
it('cubic-bezier(0.4, 0, 0.6, 1)', () => {
@@ -466,15 +454,9 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.39), [
0.43,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.03), [
0.11,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.89), [
0.78,
]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.39), [0.43]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.03), [0.11]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.89), [0.78]);
});
it('cubic-bezier(0, 0, 0.2, 1)', () => {
@@ -485,15 +467,9 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.95), [
0.71,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.1), [
0.03,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.7), [
0.35,
]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.95), [0.71]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.1), [0.03]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.7), [0.35]);
});
it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => {
@@ -504,8 +480,8 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
expect(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 1.32)[0]).toBeUndefined();
expect(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], -0.32)[0]).toBeUndefined();
expect(getTimeGivenProgression(...equation, 1.32)[0]).toBeUndefined();
expect(getTimeGivenProgression(...equation, -0.32)[0]).toBeUndefined();
});
it('cubic-bezier(0.21, 1.71, 0.88, 0.9) (multiple solutions)', () => {
@@ -516,10 +492,7 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(
getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 1.02),
[0.35, 0.87]
);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 1.02), [0.35, 0.87]);
});
it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => {
@@ -530,8 +503,8 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
expect(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 1.32)).toEqual([]);
expect(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], -0.32)).toEqual([]);
expect(getTimeGivenProgression(...equation, 1.32)).toEqual([]);
expect(getTimeGivenProgression(...equation, -0.32)).toEqual([]);
});
});
});

View File

@@ -16,8 +16,8 @@ const mockVisualViewport = (
win: Window,
visualViewport: any = { width: 320, height: 568 },
layoutViewport = { innerWidth: 320, innerHeight: 568 }
) => {
(win as any).visualViewport = {
): any => {
win.visualViewport = {
width: 320,
height: 568,
offsetTop: 0,
@@ -29,32 +29,26 @@ const mockVisualViewport = (
onscroll: undefined,
};
(win as any).visualViewport = Object.assign(win.visualViewport!, visualViewport);
win.visualViewport = Object.assign(win.visualViewport, visualViewport);
win = Object.assign(win, layoutViewport);
const mockDispatchEvent = jest.fn();
win.dispatchEvent = mockDispatchEvent;
win.dispatchEvent = jest.fn();
trackViewportChanges(win);
return {
win,
mockDispatchEvent,
};
return win;
};
const mockCapacitor = (win: Window) => {
(win as any).Capacitor = {
win.Capacitor = {
isPluginAvailable: () => false,
};
};
const resizeVisualViewport = (win: Window, visualViewport: any = {}) => {
(win as any).visualViewport = Object.assign((win as any).visualViewport, visualViewport);
win.visualViewport = Object.assign(win.visualViewport, visualViewport);
if (win.visualViewport!.onresize) {
win.visualViewport!.onresize({} as any);
if (win.visualViewport.onresize) {
win.visualViewport.onresize();
} else {
trackViewportChanges(win);
}
@@ -93,64 +87,62 @@ describe('Keyboard Assist Tests', () => {
describe('setKeyboardOpen()', () => {
it('should dispatch the keyboard open event on the window', () => {
const mockDispatchEvent = jest.fn();
window.dispatchEvent = mockDispatchEvent;
window.dispatchEvent = jest.fn();
setKeyboardOpen(window);
expect(mockDispatchEvent.mock.calls.length).toEqual(1);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
});
});
describe('setKeyboardClose()', () => {
it('should dispatch the keyboard close event on the window', () => {
const mockDispatchEvent = jest.fn();
window.dispatchEvent = mockDispatchEvent;
window.dispatchEvent = jest.fn();
setKeyboardClose(window);
expect(mockDispatchEvent.mock.calls.length).toEqual(1);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_CLOSE);
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_CLOSE);
});
});
describe('keyboardDidOpen()', () => {
beforeEach(() => {
resetKeyboardAssist();
resetKeyboardAssist(window);
mockVisualViewport(window);
});
it('should return true when visual viewport height < layout viewport height and meets or exceeds the keyboard threshold', () => {
resizeVisualViewport(window, { height: 200 });
expect(keyboardDidOpen()).toEqual(true);
expect(keyboardDidOpen(window)).toEqual(true);
});
it('should return true if the layout and visual viewports resize', () => {
resizeLayoutViewport(window, { width: 320, height: 300 });
resizeVisualViewport(window, { width: 320, height: 300 });
expect(keyboardDidOpen()).toEqual(true);
expect(keyboardDidOpen(window)).toEqual(true);
});
it('should return false when visual viewport height < layout viewport heigh but does not meet the keyboard threshold', () => {
resizeVisualViewport(window, { height: 500 });
expect(keyboardDidOpen()).toEqual(false);
expect(keyboardDidOpen(window)).toEqual(false);
});
it('should return false on orientation change', () => {
resizeVisualViewport(window, { width: 320, height: 250 });
resizeVisualViewport(window, { width: 250, height: 320 });
expect(keyboardDidOpen()).toEqual(false);
expect(keyboardDidOpen(window)).toEqual(false);
});
it('should return false when both the visual and layout viewports change', () => {
resizeVisualViewport(window, { width: 250, height: 320 });
resizeVisualViewport(window, { width: 250, height: 320 }, { innerWidth: 250, innerHeight: 320 });
expect(keyboardDidOpen()).toEqual(false);
expect(keyboardDidOpen(window)).toEqual(false);
});
it('should return true when the keyboard shows even if the user is zoomed in', () => {
@@ -160,13 +152,13 @@ describe('Keyboard Assist Tests', () => {
// User taps input and keyboard appears
resizeVisualViewport(window, { width: 160, height: 184, scale: 2 });
expect(keyboardDidOpen()).toEqual(true);
expect(keyboardDidOpen(window)).toEqual(true);
});
});
describe('keyboardDidClose()', () => {
beforeEach(() => {
resetKeyboardAssist();
resetKeyboardAssist(window);
mockVisualViewport(window);
});
@@ -230,66 +222,54 @@ describe('Keyboard Assist Tests', () => {
});
describe('Keyboard Assist Integration', () => {
let mockDispatchEvent: jest.Mock<any, any>;
beforeEach(() => {
resetKeyboardAssist();
mockDispatchEvent = mockVisualViewport(window).mockDispatchEvent;
resetKeyboardAssist(window);
mockVisualViewport(window);
startKeyboardAssist(window);
});
afterEach(() => {
mockDispatchEvent.mockReset();
});
it('should properly set the keyboard to be open', () => {
resizeVisualViewport(window, { width: 320, height: 350 });
expect(mockDispatchEvent.mock.calls.length).toEqual(1);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
});
it('should properly set the keyboard to be closed', () => {
resizeVisualViewport(window, { width: 320, height: 350 });
resizeVisualViewport(window, { width: 320, height: 568 });
expect(mockDispatchEvent.mock.calls.length).toEqual(2);
expect(mockDispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_CLOSE);
expect(window.dispatchEvent.mock.calls.length).toEqual(2);
expect(window.dispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_CLOSE);
});
it('should properly set the keyboard to be resized', () => {
resizeVisualViewport(window, { width: 320, height: 350 });
resizeVisualViewport(window, { width: 320, height: 360 });
expect(mockDispatchEvent.mock.calls.length).toEqual(2);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(mockDispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(window.dispatchEvent.mock.calls.length).toEqual(2);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(window.dispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_OPEN);
});
it('should not set keyboard open on orientation change', () => {
resizeVisualViewport(window, { width: 568, height: 320 });
expect(mockDispatchEvent.mock.calls.length).toEqual(0);
expect(window.dispatchEvent.mock.calls.length).toEqual(0);
});
});
describe('Keyboard Assist with Capacitor', () => {
let mockDispatchEvent: jest.Mock<any, any>;
beforeEach(() => {
resetKeyboardAssist();
resetKeyboardAssist(window);
mockCapacitor(window);
mockDispatchEvent = mockVisualViewport(window).mockDispatchEvent;
mockVisualViewport(window);
startKeyboardAssist(window);
});
afterEach(() => {
mockDispatchEvent.mockReset();
});
it('should attach visual viewport listeners when Capacitor is available but the Keyboard plugin is not', () => {
resizeVisualViewport(window, { width: 320, height: 350 });
expect(mockDispatchEvent.mock.calls.length).toEqual(1);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
});
});

View File

@@ -65,7 +65,7 @@ describe('sanitizeDOMString', () => {
});
const enableSanitizer = (enable = true) => {
(window as any).Ionic = {};
(window as any).Ionic.config = {};
(window as any).Ionic.config.sanitizerEnabled = enable;
window.Ionic = {};
window.Ionic.config = {};
window.Ionic.config.sanitizerEnabled = enable;
};

View File

@@ -1,8 +1,8 @@
import { newSpecPage } from '@stencil/core/testing';
import { Item } from '../../components/item/item';
import { Label } from '../../components/label/label';
import { Toggle } from '../../components/toggle/toggle';
import { Item } from '../../components/item/item.tsx';
import { Label } from '../../components/label/label.tsx';
import { Toggle } from '../../components/toggle/toggle.tsx';
import { getAriaLabel } from '../helpers';
describe('getAriaLabel()', () => {
@@ -17,7 +17,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle')!;
const toggle = page.body.querySelector('ion-toggle');
const { label, labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');
@@ -35,7 +35,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle')!;
const toggle = page.body.querySelector('ion-toggle');
const { label, labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');
@@ -53,7 +53,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle')!;
const toggle = page.body.querySelector('ion-toggle');
const { labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');
@@ -70,7 +70,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle')!;
const toggle = page.body.querySelector('ion-toggle');
const { labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');
@@ -87,7 +87,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle')!;
const toggle = page.body.querySelector('ion-toggle');
const { labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');

View File

@@ -1,4 +1,3 @@
import type { BackButtonEvent } from '../../../src/interface';
import { startHardwareBackButton } from '../hardware-back-button';
describe('Hardware Back Button', () => {
@@ -6,7 +5,7 @@ describe('Hardware Back Button', () => {
it('should call handler', () => {
const cbSpy = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
(ev as BackButtonEvent).detail.register(0, cbSpy);
ev.detail.register(0, cbSpy);
});
dispatchBackButtonEvent();
@@ -17,8 +16,8 @@ describe('Hardware Back Button', () => {
const cbSpy = jest.fn();
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
(ev as BackButtonEvent).detail.register(100, cbSpy);
(ev as BackButtonEvent).detail.register(99, cbSpyTwo);
ev.detail.register(100, cbSpy);
ev.detail.register(99, cbSpyTwo);
});
dispatchBackButtonEvent();
@@ -30,8 +29,8 @@ describe('Hardware Back Button', () => {
const cbSpy = jest.fn();
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
(ev as BackButtonEvent).detail.register(100, cbSpy);
(ev as BackButtonEvent).detail.register(100, cbSpyTwo);
ev.detail.register(100, cbSpy);
ev.detail.register(100, cbSpyTwo);
});
dispatchBackButtonEvent();
@@ -40,13 +39,13 @@ describe('Hardware Back Button', () => {
});
it('should call multiple callbacks', () => {
const cbSpy = (processNextHandler: () => void) => {
const cbSpy = (processNextHandler) => {
processNextHandler();
};
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
(ev as BackButtonEvent).detail.register(100, cbSpy);
(ev as BackButtonEvent).detail.register(99, cbSpyTwo);
ev.detail.register(100, cbSpy);
ev.detail.register(99, cbSpyTwo);
});
dispatchBackButtonEvent();

View File

@@ -1,8 +1,9 @@
import { newSpecPage } from '@stencil/core/testing';
import { Modal } from '../../../components/modal/modal';
import { Nav } from '../../../components/nav/nav';
import { RouterOutlet } from '../../../components/router-outlet/router-outlet';
import { Modal } from '../../../components/modal/modal';
import { setRootAriaHidden } from '../../overlays';
describe('setRootAriaHidden()', () => {
@@ -14,7 +15,7 @@ describe('setRootAriaHidden()', () => {
`,
});
const routerOutlet = page.body.querySelector('ion-router-outlet')!;
const routerOutlet = page.body.querySelector('ion-router-outlet');
expect(routerOutlet.hasAttribute('aria-hidden')).toEqual(false);
@@ -33,7 +34,7 @@ describe('setRootAriaHidden()', () => {
`,
});
const nav = page.body.querySelector('ion-nav')!;
const nav = page.body.querySelector('ion-nav');
expect(nav.hasAttribute('aria-hidden')).toEqual(false);
@@ -53,8 +54,8 @@ describe('setRootAriaHidden()', () => {
`,
});
const containerRoot = page.body.querySelector('#ion-view-container-root')!;
const notContainerRoot = page.body.querySelector('#not-container-root')!;
const containerRoot = page.body.querySelector('#ion-view-container-root');
const notContainerRoot = page.body.querySelector('#not-container-root');
expect(containerRoot.hasAttribute('aria-hidden')).toEqual(false);
expect(notContainerRoot.hasAttribute('aria-hidden')).toEqual(false);
@@ -89,8 +90,8 @@ describe('setRootAriaHidden()', () => {
`,
});
const routerOutlet = page.body.querySelector('ion-router-outlet')!;
const modal = page.body.querySelector('ion-modal')!;
const routerOutlet = page.body.querySelector('ion-router-outlet');
const modal = page.body.querySelector('ion-modal');
await modal.present();
@@ -108,9 +109,9 @@ describe('setRootAriaHidden()', () => {
`,
});
const routerOutlet = page.body.querySelector('ion-router-outlet')!;
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#one')!;
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#two')!;
const routerOutlet = page.body.querySelector('ion-router-outlet');
const modalOne = page.body.querySelector('ion-modal#one');
const modalTwo = page.body.querySelector('ion-modal#two');
await modalOne.present();

View File

@@ -12,7 +12,7 @@ describe('componentOnReady()', () => {
);
const component = document.createElement('hello-world');
componentOnReady(component, (el: HTMLElement) => {
componentOnReady(component, (el) => {
expect(el).toBe(component);
done();
});
@@ -39,7 +39,7 @@ describe('componentOnReady()', () => {
);
const component = document.createElement('hello-world');
componentOnReady(component, (el: HTMLElement) => {
componentOnReady(component, (el) => {
expect(el).toBe(component);
expect(cb).toHaveBeenCalledTimes(1);
done();

View File

@@ -37,6 +37,9 @@
"src",
],
"exclude": [
"node_modules"
"node_modules",
"**/test/**/*.spec.ts",
"**/test/**/*.spec.tsx",
"**/test/**/e2e.ts"
]
}

View File

@@ -3,14 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.5.6](https://github.com/ionic-team/ionic-framework/compare/v7.5.5...v7.5.6) (2023-11-21)
**Note:** Version bump only for package @ionic/docs
## [7.5.5](https://github.com/ionic-team/ionic-framework/compare/v7.5.4...v7.5.5) (2023-11-15)
**Note:** Version bump only for package @ionic/docs

View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/docs",
"version": "7.5.6",
"version": "7.5.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/docs",
"version": "7.5.6",
"version": "7.5.5",
"license": "MIT"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/docs",
"version": "7.5.6",
"version": "7.5.5",
"description": "Pre-packaged API documentation for the Ionic docs.",
"main": "core.json",
"types": "core.d.ts",

View File

@@ -4,5 +4,5 @@
"docs",
"packages/*"
],
"version": "7.5.6"
"version": "7.5.5"
}

View File

@@ -3,14 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.5.6](https://github.com/ionic-team/ionic-framework/compare/v7.5.5...v7.5.6) (2023-11-21)
**Note:** Version bump only for package @ionic/angular-server
## [7.5.5](https://github.com/ionic-team/ionic-framework/compare/v7.5.4...v7.5.5) (2023-11-15)
**Note:** Version bump only for package @ionic/angular-server

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular-server",
"version": "7.5.6",
"version": "7.5.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular-server",
"version": "7.5.6",
"version": "7.5.5",
"license": "MIT",
"dependencies": {
"@ionic/core": "^7.5.6"
"@ionic/core": "^7.5.5"
},
"devDependencies": {
"@angular-eslint/eslint-plugin": "^14.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular-server",
"version": "7.5.6",
"version": "7.5.5",
"description": "Angular SSR Module for Ionic",
"keywords": [
"ionic",
@@ -62,6 +62,6 @@
},
"prettier": "@ionic/prettier-config",
"dependencies": {
"@ionic/core": "^7.5.6"
"@ionic/core": "^7.5.5"
}
}

View File

@@ -3,18 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.5.6](https://github.com/ionic-team/ionic-framework/compare/v7.5.5...v7.5.6) (2023-11-21)
### Bug Fixes
* **angular:** ng add @ionic/angular in standalone projects ([#28523](https://github.com/ionic-team/ionic-framework/issues/28523)) ([c07312e](https://github.com/ionic-team/ionic-framework/commit/c07312e5ed931f6f825ccf083c9dead9fa815843)), closes [#28514](https://github.com/ionic-team/ionic-framework/issues/28514)
* **angular:** overlays are defined when using standalone controllers ([#28560](https://github.com/ionic-team/ionic-framework/issues/28560)) ([9453132](https://github.com/ionic-team/ionic-framework/commit/9453132aa8952b4adfa1326e61138b329e254f76)), closes [#28385](https://github.com/ionic-team/ionic-framework/issues/28385)
## [7.5.5](https://github.com/ionic-team/ionic-framework/compare/v7.5.4...v7.5.5) (2023-11-15)
**Note:** Version bump only for package @ionic/angular

View File

@@ -1,4 +1,7 @@
export { AlertController } from './providers/alert-controller';
export { LoadingController } from './providers/loading-controller';
export { MenuController } from './providers/menu-controller';
export { PickerController } from './providers/picker-controller';
export { DomController } from './providers/dom-controller';
export { NavController } from './providers/nav-controller';

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { OverlayBaseController } from '@ionic/angular/common';
import type { AlertOptions } from '@ionic/core/components';
import { alertController } from '@ionic/core/components';
import { defineCustomElement } from '@ionic/core/components/ion-alert.js';
import { OverlayBaseController } from '../utils/overlay';
@Injectable({
providedIn: 'root',
@@ -10,6 +10,5 @@ import { defineCustomElement } from '@ionic/core/components/ion-alert.js';
export class AlertController extends OverlayBaseController<AlertOptions, HTMLIonAlertElement> {
constructor() {
super(alertController);
defineCustomElement();
}
}

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { OverlayBaseController } from '@ionic/angular/common';
import type { LoadingOptions } from '@ionic/core/components';
import { loadingController } from '@ionic/core/components';
import { defineCustomElement } from '@ionic/core/components/ion-loading.js';
import { OverlayBaseController } from '../utils/overlay';
@Injectable({
providedIn: 'root',
@@ -10,6 +10,5 @@ import { defineCustomElement } from '@ionic/core/components/ion-loading.js';
export class LoadingController extends OverlayBaseController<LoadingOptions, HTMLIonLoadingElement> {
constructor() {
super(loadingController);
defineCustomElement();
}
}

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { OverlayBaseController } from '@ionic/angular/common';
import type { PickerOptions } from '@ionic/core/components';
import { pickerController } from '@ionic/core/components';
import { defineCustomElement } from '@ionic/core/components/ion-picker.js';
import { OverlayBaseController } from '../utils/overlay';
@Injectable({
providedIn: 'root',
@@ -10,6 +10,5 @@ import { defineCustomElement } from '@ionic/core/components/ion-picker.js';
export class PickerController extends OverlayBaseController<PickerOptions, HTMLIonPickerElement> {
constructor() {
super(pickerController);
defineCustomElement();
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "7.5.6",
"version": "7.5.5",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@@ -48,7 +48,7 @@
}
},
"dependencies": {
"@ionic/core": "^7.5.6",
"@ionic/core": "^7.5.5",
"ionicons": "^7.0.0",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
@@ -61,12 +61,11 @@
"zone.js": ">=0.11.0"
},
"devDependencies": {
"@angular-devkit/core": "^17.0.0",
"@angular-devkit/schematics": "^17.0.0",
"@angular-devkit/core": "^14.0.0",
"@angular-devkit/schematics": "^14.0.0",
"@angular-eslint/eslint-plugin": "^14.0.0",
"@angular-eslint/eslint-plugin-template": "^14.0.0",
"@angular-eslint/template-parser": "^14.0.0",
"@angular/cli": "^14.0.0",
"@angular/common": "^14.0.0",
"@angular/compiler": "^14.0.0",
"@angular/compiler-cli": "^14.0.0",
@@ -77,7 +76,7 @@
"@angular/router": "^14.0.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@schematics/angular": "^17.0.0",
"@schematics/angular": "^14.0.0",
"@types/node": "12.12.5",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",

View File

@@ -20,6 +20,9 @@ export * from './directives/validators';
// PROVIDERS
export {
AlertController,
LoadingController,
PickerController,
DomController,
NavController,
Config,
@@ -32,14 +35,11 @@ export {
ViewDidEnter,
ViewDidLeave,
} from '@ionic/angular/common';
export { AlertController } from './providers/alert-controller';
export { AnimationController } from './providers/animation-controller';
export { ActionSheetController } from './providers/action-sheet-controller';
export { GestureController } from './providers/gesture-controller';
export { LoadingController } from './providers/loading-controller';
export { MenuController } from './providers/menu-controller';
export { ModalController } from './providers/modal-controller';
export { PickerController } from './providers/picker-controller';
export { PopoverController } from './providers/popover-controller';
export { ToastController } from './providers/toast-controller';

View File

@@ -1,13 +0,0 @@
import { Injectable } from '@angular/core';
import { OverlayBaseController } from '@ionic/angular/common';
import type { AlertOptions } from '@ionic/core';
import { alertController } from '@ionic/core';
@Injectable({
providedIn: 'root',
})
export class AlertController extends OverlayBaseController<AlertOptions, HTMLIonAlertElement> {
constructor() {
super(alertController);
}
}

View File

@@ -1,13 +0,0 @@
import { Injectable } from '@angular/core';
import { OverlayBaseController } from '@ionic/angular/common';
import type { LoadingOptions } from '@ionic/core';
import { loadingController } from '@ionic/core';
@Injectable({
providedIn: 'root',
})
export class LoadingController extends OverlayBaseController<LoadingOptions, HTMLIonLoadingElement> {
constructor() {
super(loadingController);
}
}

View File

@@ -1,13 +0,0 @@
import { Injectable } from '@angular/core';
import { OverlayBaseController } from '@ionic/angular/common';
import type { PickerOptions } from '@ionic/core';
import { pickerController } from '@ionic/core';
@Injectable({
providedIn: 'root',
})
export class PickerController extends OverlayBaseController<PickerOptions, HTMLIonPickerElement> {
constructor() {
super(pickerController);
}
}

View File

@@ -4,244 +4,3 @@
/* To quickly generate your own theme, check out the color generator */
/* https://ionicframework.com/docs/theming/color-generator */
/** Ionic CSS Variables **/
:root {
/** primary **/
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
/** secondary **/
--ion-color-secondary: #3dc2ff;
--ion-color-secondary-rgb: 61, 194, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #36abe0;
--ion-color-secondary-tint: #50c8ff;
/** tertiary **/
--ion-color-tertiary: #5260ff;
--ion-color-tertiary-rgb: 82, 96, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #4854e0;
--ion-color-tertiary-tint: #6370ff;
/** success **/
--ion-color-success: #2dd36f;
--ion-color-success-rgb: 45, 211, 111;
--ion-color-success-contrast: #ffffff;
--ion-color-success-contrast-rgb: 255, 255, 255;
--ion-color-success-shade: #28ba62;
--ion-color-success-tint: #42d77d;
/** warning **/
--ion-color-warning: #ffc409;
--ion-color-warning-rgb: 255, 196, 9;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0ac08;
--ion-color-warning-tint: #ffca22;
/** danger **/
--ion-color-danger: #eb445a;
--ion-color-danger-rgb: 235, 68, 90;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #cf3c4f;
--ion-color-danger-tint: #ed576b;
/** dark **/
--ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 36, 40;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255, 255, 255;
--ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e;
/** medium **/
--ion-color-medium: #92949c;
--ion-color-medium-rgb: 146, 148, 156;
--ion-color-medium-contrast: #ffffff;
--ion-color-medium-contrast-rgb: 255, 255, 255;
--ion-color-medium-shade: #808289;
--ion-color-medium-tint: #9d9fa6;
/** light **/
--ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 245, 248;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
}
@media (prefers-color-scheme: dark) {
/*
* Dark Colors
* -------------------------------------------
*/
body {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66, 140, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80, 200, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106, 100, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47, 223, 117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0, 0, 0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255, 213, 52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255, 73, 97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244, 245, 248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0, 0, 0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152, 154, 162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0, 0, 0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34, 36, 40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255, 255, 255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* -------------------------------------------
*/
.ios body {
--ion-background-color: #000000;
--ion-background-color-rgb: 0, 0, 0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255, 255, 255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-item-background: #000000;
--ion-card-background: #1c1c1d;
}
.ios ion-modal {
--ion-background-color: var(--ion-color-step-100);
--ion-toolbar-background: var(--ion-color-step-150);
--ion-toolbar-border-color: var(--ion-color-step-250);
}
/*
* Material Design Dark Theme
* -------------------------------------------
*/
.md body {
--ion-background-color: #121212;
--ion-background-color-rgb: 18, 18, 18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255, 255, 255;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1e1e1e;
--ion-toolbar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
--ion-card-background: #1e1e1e;
}
}
html {
/*
* For more information on dynamic font scaling, visit the documentation:
* https://ionicframework.com/docs/layout/dynamic-font-scaling
*/
--ion-dynamic-font: var(--ion-default-dynamic-font);
}

View File

@@ -12,19 +12,10 @@ import {
url,
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { addRootProvider } from '@schematics/angular/utility';
import { getWorkspace } from '@schematics/angular/utility/workspace';
import { addIonicModuleImportToNgModule } from '../utils/ast';
import {
addArchitectBuilder,
addAsset,
addCli,
addSchematics,
addStyle,
getDefaultAngularAppName,
} from './../utils/config';
import { addModuleImportToRootModule } from './../utils/ast';
import { addArchitectBuilder, addAsset, addStyle, getDefaultAngularAppName } from './../utils/config';
import { addPackageToPackageJson } from './../utils/package';
import { Schema as IonAddOptions } from './schema';
@@ -42,53 +33,9 @@ function addIonicAngularToolkitToPackageJson(): Rule {
};
}
/**
* Adds the @ionic/angular-toolkit schematics and cli configuration to the project's `angular.json` file.
* @param projectName The name of the project.
*/
function addIonicAngularToolkitToAngularJson(): Rule {
return (host: Tree) => {
addCli(host, '@ionic/angular-toolkit');
addSchematics(host, '@ionic/angular-toolkit:component', {
styleext: 'scss',
});
addSchematics(host, '@ionic/angular-toolkit:page', {
styleext: 'scss',
});
return host;
};
}
/**
* Adds the `IonicModule.forRoot()` usage to the project's `AppModule`.
* If the project does not use modules this will operate as a noop.
* @param projectSourceRoot The source root path of the project.
*/
function addIonicAngularModuleToAppModule(projectSourceRoot: Path): Rule {
return (host: Tree) => {
const appModulePath = `${projectSourceRoot}/app/app.module.ts`;
if (host.exists(appModulePath)) {
addIonicModuleImportToNgModule(host, appModulePath);
}
return host;
};
}
/**
* Adds the `provideIonicAngular` usage to the project's app config.
* If the project does not use an app config this will operate as a noop.
* @param projectName The name of the project.
* @param projectSourceRoot The source root path of the project.
*/
function addProvideIonicAngular(projectName: string, projectSourceRoot: Path): Rule {
return (host: Tree) => {
const appConfig = `${projectSourceRoot}/app/app.config.ts`;
if (host.exists(appConfig)) {
return addRootProvider(
projectName,
({ code, external }) => code`${external('provideIonicAngular', '@ionic/angular/standalone')}({})`
);
}
addModuleImportToRootModule(host, projectSourceRoot, 'IonicModule.forRoot()', '@ionic/angular');
return host;
};
}
@@ -116,49 +63,15 @@ function addIonicStyles(projectName: string, projectSourceRoot: Path): Rule {
};
}
function addIonicons(projectName: string, projectSourceRoot: Path): Rule {
function addIonicons(projectName: string): Rule {
return (host: Tree) => {
const hasAppModule = host.exists(`${projectSourceRoot}/app/app.module.ts`);
if (hasAppModule) {
/**
* Add Ionicons to the `angular.json` file only if the project
* is using the lazy build of `@ionic/angular` with modules.
*/
const ioniconsGlob = {
glob: '**/*.svg',
input: 'node_modules/ionicons/dist/ionicons/svg',
output: './svg',
};
addAsset(host, projectName, 'build', ioniconsGlob);
addAsset(host, projectName, 'test', ioniconsGlob);
}
return host;
};
}
function addIonicConfig(projectSourceRoot: string): Rule {
return (host: Tree) => {
const ionicConfig = 'ionic.config.json';
if (!host.exists(ionicConfig)) {
const hasAppModule = host.exists(`${projectSourceRoot}/app/app.module.ts`);
const type = hasAppModule ? 'angular' : 'angular-standalone';
host.create(
ionicConfig,
JSON.stringify(
{
name: 'ionic-app',
app_id: '',
type,
integrations: {},
},
null,
2
)
);
}
const ioniconsGlob = {
glob: '**/*.svg',
input: 'node_modules/ionicons/dist/ionicons/svg',
output: './svg',
};
addAsset(host, projectName, 'build', ioniconsGlob);
addAsset(host, projectName, 'test', ioniconsGlob);
return host;
};
}
@@ -216,13 +129,10 @@ export default function ngAdd(options: IonAddOptions): Rule {
// @ionic/angular
addIonicAngularToPackageJson(),
addIonicAngularToolkitToPackageJson(),
addIonicAngularToolkitToAngularJson(),
addIonicAngularModuleToAppModule(sourcePath),
addProvideIonicAngular(options.project, sourcePath),
addIonicBuilder(options.project),
addIonicStyles(options.project, sourcePath),
addIonicons(options.project, sourcePath),
addIonicConfig(sourcePath),
addIonicons(options.project),
mergeWith(rootTemplateSource),
// install freshly added dependencies
installNodeDeps(),

View File

@@ -1,13 +1,14 @@
import type { Tree } from '@angular-devkit/schematics';
import { SchematicsException } from '@angular-devkit/schematics';
import { addSymbolToNgModuleMetadata, insertImport } from '@schematics/angular/utility/ast-utils';
import { applyToUpdateRecorder } from '@schematics/angular/utility/change';
import { normalize } from '@angular-devkit/core';
import { Tree, SchematicsException } from '@angular-devkit/schematics';
import * as ts from 'typescript';
import { addImportToModule } from './devkit-utils/ast-utils';
import { InsertChange } from './devkit-utils/change';
/**
* Reads file given path and returns TypeScript source file.
*/
function getSourceFile(host: Tree, path: string): ts.SourceFile {
export function getSourceFile(host: Tree, path: string): ts.SourceFile {
const buffer = host.read(path);
if (!buffer) {
throw new SchematicsException(`Could not find file for path: ${path}`);
@@ -20,17 +21,32 @@ function getSourceFile(host: Tree, path: string): ts.SourceFile {
/**
* Import and add module to root app module.
*/
export function addIonicModuleImportToNgModule(host: Tree, modulePath: string): void {
export function addModuleImportToRootModule(
host: Tree,
projectSourceRoot: string,
moduleName: string,
importSrc: string
): void {
addModuleImportToModule(host, normalize(`${projectSourceRoot}/app/app.module.ts`), moduleName, importSrc);
}
/**
* Import and add module to specific module path.
* @param host the tree we are updating
* @param modulePath src location of the module to import
* @param moduleName name of module to import
* @param src src location to import
*/
export function addModuleImportToModule(host: Tree, modulePath: string, moduleName: string, src: string): void {
const moduleSource = getSourceFile(host, modulePath);
const changes = addImportToModule(moduleSource, modulePath, moduleName, src);
const recorder = host.beginUpdate(modulePath);
const moduleSource = getSourceFile(host, modulePath) as any;
const ionicModuleChange = insertImport(moduleSource, modulePath, 'IonicModule', '@ionic/angular');
applyToUpdateRecorder(recorder, [ionicModuleChange]);
const metadataChange = addSymbolToNgModuleMetadata(moduleSource, modulePath, 'imports', 'IonicModule.forRoot({})');
applyToUpdateRecorder(recorder, metadataChange);
changes.forEach((change) => {
if (change instanceof InsertChange) {
recorder.insertLeft(change.pos, change.toAdd);
}
});
host.commitUpdate(recorder);
}

View File

@@ -1,28 +1,24 @@
import type { JsonObject } from '@angular-devkit/core';
import { WorkspaceDefinition } from '@angular-devkit/core/src/workspace';
import { Tree, SchematicsException } from '@angular-devkit/schematics';
import type { SchematicOptions } from '@angular/cli/lib/config/workspace-schema';
import { parse } from 'jsonc-parser';
const ANGULAR_JSON_PATH = 'angular.json';
const CONFIG_PATH = 'angular.json';
export function readConfig<T extends JsonObject = JsonObject>(host: Tree): T {
return host.readJson(ANGULAR_JSON_PATH) as T;
// TODO(FW-2827): types
export function readConfig(host: Tree): any {
const sourceText = host.read(CONFIG_PATH)?.toString('utf-8');
return JSON.parse(sourceText);
}
export function writeConfig(host: Tree, config: JsonObject): void {
host.overwrite(ANGULAR_JSON_PATH, JSON.stringify(config, null, 2));
export function writeConfig(host: Tree, config: JSON): void {
host.overwrite(CONFIG_PATH, JSON.stringify(config, null, 2));
}
function isAngularBrowserProject(projectConfig: any): boolean {
if (projectConfig.projectType === 'application') {
const buildConfig = projectConfig.architect.build;
// Angular 16 and lower
const legacyAngularBuilder = buildConfig.builder === '@angular-devkit/build-angular:browser';
// Angular 17+
const modernAngularBuilder = buildConfig.builder === '@angular-devkit/build-angular:application';
return legacyAngularBuilder || modernAngularBuilder;
return buildConfig.builder === '@angular-devkit/build-angular:browser';
}
return false;
@@ -42,7 +38,7 @@ export function getDefaultAngularAppName(config: any): string {
return projectNames[0];
}
function getAngularJson(config: any, projectName: string): any | never {
export function getAngularAppConfig(config: any, projectName: string): any | never {
// eslint-disable-next-line no-prototype-builtins
if (!config.projects.hasOwnProperty(projectName)) {
throw new SchematicsException(`Could not find project: ${projectName}`);
@@ -63,8 +59,8 @@ function getAngularJson(config: any, projectName: string): any | never {
export function addStyle(host: Tree, projectName: string, stylePath: string): void {
const config = readConfig(host);
const angularJson = getAngularJson(config, projectName);
angularJson.architect.build.options.styles.push({
const appConfig = getAngularAppConfig(config, projectName);
appConfig.architect.build.options.styles.push({
input: stylePath,
});
writeConfig(host, config);
@@ -77,8 +73,8 @@ export function addAsset(
asset: string | { glob: string; input: string; output: string }
): void {
const config = readConfig(host);
const angularJson = getAngularJson(config, projectName);
const target = angularJson.architect[architect];
const appConfig = getAngularAppConfig(config, projectName);
const target = appConfig.architect[architect];
if (target) {
target.options.assets.push(asset);
writeConfig(host, config);
@@ -92,48 +88,11 @@ export function addArchitectBuilder(
builderOpts: any
): void | never {
const config = readConfig(host);
const angularJson = getAngularJson(config, projectName);
angularJson.architect[builderName] = builderOpts;
const appConfig = getAngularAppConfig(config, projectName);
appConfig.architect[builderName] = builderOpts;
writeConfig(host, config);
}
/**
* Updates the angular.json to add an additional schematic collection
* to the CLI configuration.
*/
export function addCli(host: Tree, collectionName: string): void | never {
const angularJson = readConfig<any>(host);
if (angularJson.cli === undefined) {
angularJson.cli = {};
}
if (angularJson.cli.schematicCollections === undefined) {
angularJson.cli.schematicCollections = [];
}
angularJson.cli.schematicCollections.push(collectionName);
writeConfig(host, angularJson);
}
// TODO(FW-5639): can remove [property: string]: any; when upgrading @angular/cli dev-dep to v16 or later
export function addSchematics(
host: Tree,
schematicName: string,
schematicOpts: SchematicOptions & { [property: string]: any }
): void | never {
const angularJson = readConfig<any>(host);
if (angularJson.schematics === undefined) {
angularJson.schematics = {};
}
angularJson.schematics[schematicName] = schematicOpts;
writeConfig(host, angularJson);
}
export function getWorkspacePath(host: Tree): string {
const possibleFiles = ['/angular.json', '/.angular.json'];
const path = possibleFiles.filter((path) => host.exists(path))[0];

View File

@@ -0,0 +1,579 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import { Change, InsertChange, NoopChange } from './change';
/**
* Add Import `import { symbolName } from fileName` if the import doesn't exit
* already. Assumes fileToEdit can be resolved and accessed.
* @param fileToEdit (file we want to add import to)
* @param symbolName (item to import)
* @param fileName (path to the file)
* @param isDefault (if true, import follows style for importing default exports)
* @return Change
*/
export function insertImport(
source: ts.SourceFile,
fileToEdit: string,
symbolName: string,
fileName: string,
isDefault = false
): Change {
const rootNode = source;
const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
// get nodes that map to import statements from the file fileName
const relevantImports = allImports.filter((node) => {
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
const importFiles = node
.getChildren()
.filter((child) => child.kind === ts.SyntaxKind.StringLiteral)
.map((n) => (n as ts.StringLiteral).text);
return importFiles.filter((file) => file === fileName).length === 1;
});
if (relevantImports.length > 0) {
let importsAsterisk = false;
// imports from import file
const imports: ts.Node[] = [];
relevantImports.forEach((n) => {
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
importsAsterisk = true;
}
});
// if imports * from fileName, don't add symbolName
if (importsAsterisk) {
return new NoopChange();
}
const importTextNodes = imports.filter((n) => (n as ts.Identifier).text === symbolName);
// insert import if it's not there
if (importTextNodes.length === 0) {
const fallbackPos =
findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() ||
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos);
}
return new NoopChange();
}
// no such import declaration exists
const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter(
(n: ts.StringLiteral) => n.text === 'use strict'
);
let fallbackPos = 0;
if (useStrict.length > 0) {
fallbackPos = useStrict[0].end;
}
const open = isDefault ? '' : '{ ';
const close = isDefault ? '' : ' }';
// if there are no imports or 'use strict' statement, insert import at beginning of file
const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
const separator = insertAtBeginning ? '' : ';\n';
const toInsert =
`${separator}import ${open}${symbolName}${close}` + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
return insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
}
/**
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
* @param node
* @param kind
* @param max The maximum number of items to return.
* @return all nodes of kind, or [] if none is found
*/
export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max = Infinity): ts.Node[] {
if (!node || max == 0) {
return [];
}
const arr: ts.Node[] = [];
if (node.kind === kind) {
arr.push(node);
max--;
}
if (max > 0) {
for (const child of node.getChildren()) {
findNodes(child, kind, max).forEach((node) => {
if (max > 0) {
arr.push(node);
}
max--;
});
if (max <= 0) {
break;
}
}
}
return arr;
}
/**
* Get all the nodes from a source.
* @param sourceFile The source file object.
* @returns {Observable<ts.Node>} An observable of all the nodes in the source.
*/
export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] {
const nodes: ts.Node[] = [sourceFile];
const result = [];
while (nodes.length > 0) {
const node = nodes.shift();
if (node) {
result.push(node);
if (node.getChildCount(sourceFile) >= 0) {
nodes.unshift(...node.getChildren());
}
}
}
return result;
}
export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.Node | null {
if (node.kind === kind && node.getText() === text) {
// throw new Error(node.getText());
return node;
}
let foundNode: ts.Node | null = null;
ts.forEachChild(node, (childNode) => {
foundNode = foundNode || findNode(childNode, kind, text);
});
return foundNode;
}
/**
* Helper for sorting nodes.
* @return function to sort nodes in increasing order of position in sourceFile
*/
function nodesByPosition(first: ts.Node, second: ts.Node): number {
return first.getStart() - second.getStart();
}
/**
* Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
* or after the last of occurence of `syntaxKind` if the last occurence is a sub child
* of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
*
* @param nodes insert after the last occurence of nodes
* @param toInsert string to insert
* @param file file to insert changes into
* @param fallbackPos position to insert if toInsert happens to be the first occurence
* @param syntaxKind the ts.SyntaxKind of the subchildren to insert after
* @return Change instance
* @throw Error if toInsert is first occurence but fall back is not set
*/
export function insertAfterLastOccurrence(
nodes: ts.Node[],
toInsert: string,
file: string,
fallbackPos: number,
syntaxKind?: ts.SyntaxKind
): Change {
// sort() has a side effect, so make a copy so that we won't overwrite the parent's object.
let lastItem = [...nodes].sort(nodesByPosition).pop();
if (!lastItem) {
throw new Error();
}
if (syntaxKind) {
lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
}
if (!lastItem && fallbackPos == undefined) {
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`);
}
const lastItemPosition: number = lastItem ? lastItem.getEnd() : fallbackPos;
return new InsertChange(file, lastItemPosition, toInsert);
}
export function getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string | null {
if (node.kind == ts.SyntaxKind.Identifier) {
return (node as ts.Identifier).text;
} else if (node.kind == ts.SyntaxKind.StringLiteral) {
return (node as ts.StringLiteral).text;
} else {
return null;
}
}
function _angularImportsFromNode(
node: ts.ImportDeclaration,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_sourceFile: ts.SourceFile
): { [name: string]: string } {
const ms = node.moduleSpecifier;
let modulePath: string;
switch (ms.kind) {
case ts.SyntaxKind.StringLiteral:
modulePath = (ms as ts.StringLiteral).text;
break;
default:
return {};
}
if (!modulePath.startsWith('@angular/')) {
return {};
}
if (node.importClause) {
if (node.importClause.name) {
// This is of the form `import Name from 'path'`. Ignore.
return {};
} else if (node.importClause.namedBindings) {
const nb = node.importClause.namedBindings;
if (nb.kind == ts.SyntaxKind.NamespaceImport) {
// This is of the form `import * as name from 'path'`. Return `name.`.
return {
[(nb as ts.NamespaceImport).name.text + '.']: modulePath,
};
} else {
// This is of the form `import {a,b,c} from 'path'`
const namedImports = nb as ts.NamedImports;
return namedImports.elements
.map((is: ts.ImportSpecifier) => (is.propertyName ? is.propertyName.text : is.name.text))
.reduce((acc: { [name: string]: string }, curr: string) => {
acc[curr] = modulePath;
return acc;
}, {});
}
}
return {};
} else {
// This is of the form `import 'path';`. Nothing to do.
return {};
}
}
export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, module: string): ts.Node[] {
const angularImports: { [name: string]: string } = findNodes(source, ts.SyntaxKind.ImportDeclaration)
.map((node: ts.ImportDeclaration) => _angularImportsFromNode(node, source))
.reduce((acc: { [name: string]: string }, current: { [name: string]: string }) => {
for (const key of Object.keys(current)) {
acc[key] = current[key];
}
return acc;
}, {});
return getSourceNodes(source)
.filter((node) => {
return (
node.kind == ts.SyntaxKind.Decorator && (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression
);
})
.map((node) => (node as ts.Decorator).expression as ts.CallExpression)
.filter((expr) => {
if (expr.expression.kind == ts.SyntaxKind.Identifier) {
const id = expr.expression as ts.Identifier;
return id.getFullText(source) == identifier && angularImports[id.getFullText(source)] === module;
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
// This covers foo.NgModule when importing * as foo.
const paExpr = expr.expression as ts.PropertyAccessExpression;
// If the left expression is not an identifier, just give up at that point.
if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
return false;
}
const id = paExpr.name.text;
const moduleId = (paExpr.expression as ts.Identifier).getText(source);
return id === identifier && angularImports[moduleId + '.'] === module;
}
return false;
})
.filter((expr) => expr.arguments[0] && expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
.map((expr) => expr.arguments[0] as ts.ObjectLiteralExpression);
}
function findClassDeclarationParent(node: ts.Node): ts.ClassDeclaration | undefined {
if (ts.isClassDeclaration(node)) {
return node;
}
return node.parent && findClassDeclarationParent(node.parent);
}
/**
* Given a source file with @NgModule class(es), find the name of the first @NgModule class.
*
* @param source source file containing one or more @NgModule
* @returns the name of the first @NgModule, or `undefined` if none is found
*/
export function getFirstNgModuleName(source: ts.SourceFile): string | undefined {
// First, find the @NgModule decorators.
const ngModulesMetadata = getDecoratorMetadata(source, 'NgModule', '@angular/core');
if (ngModulesMetadata.length === 0) {
return undefined;
}
// Then walk parent pointers up the AST, looking for the ClassDeclaration parent of the NgModule
// metadata.
const moduleClass = findClassDeclarationParent(ngModulesMetadata[0]);
if (!moduleClass?.name) {
return undefined;
}
// Get the class name of the module ClassDeclaration.
return moduleClass.name.text;
}
export function addSymbolToNgModuleMetadata(
source: ts.SourceFile,
ngModulePath: string,
metadataField: string,
symbolName: string,
importPath: string | null = null
): Change[] {
const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core');
let node: any = nodes[0]; // tslint:disable-line:no-any
// Find the decorator declaration.
if (!node) {
return [];
}
// Get all the children property assignment of object literals.
const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties
.filter((prop) => prop.kind == ts.SyntaxKind.PropertyAssignment)
// Filter out every fields that's not "metadataField". Also handles string literals
// (but not expressions).
.filter((prop: ts.PropertyAssignment) => {
const name = prop.name;
switch (name.kind) {
case ts.SyntaxKind.Identifier:
return (name as ts.Identifier).getText(source) == metadataField;
case ts.SyntaxKind.StringLiteral:
return (name as ts.StringLiteral).text == metadataField;
}
return false;
});
// Get the last node of the array literal.
if (!matchingProperties) {
return [];
}
if (matchingProperties.length == 0) {
// We haven't found the field in the metadata declaration. Insert a new field.
const expr = node as ts.ObjectLiteralExpression;
let position: number;
let toInsert: string;
if (expr.properties.length == 0) {
position = expr.getEnd() - 1;
toInsert = ` ${metadataField}: [${symbolName}]\n`;
} else {
node = expr.properties[expr.properties.length - 1];
position = node.getEnd();
// Get the indentation of the last element, if any.
const text = node.getFullText(source);
const matches = text.match(/^\r?\n\s*/);
if (matches.length > 0) {
toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`;
} else {
toInsert = `, ${metadataField}: [${symbolName}]`;
}
}
if (importPath !== null) {
return [
new InsertChange(ngModulePath, position, toInsert),
insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath),
];
} else {
return [new InsertChange(ngModulePath, position, toInsert)];
}
}
const assignment = matchingProperties[0] as ts.PropertyAssignment;
// If it's not an array, nothing we can do really.
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
return [];
}
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
if (arrLiteral.elements.length == 0) {
// Forward the property.
node = arrLiteral;
} else {
node = arrLiteral.elements;
}
if (!node) {
console.log('No app module found. Please add your new class to your component.');
return [];
}
if (Array.isArray(node)) {
// eslint-disable-next-line @typescript-eslint/ban-types
const nodeArray = node as {} as ts.Node[];
const symbolsArray = nodeArray.map((node) => node.getText());
if (symbolsArray.includes(symbolName)) {
return [];
}
node = node[node.length - 1];
}
let toInsert: string;
let position = node.getEnd();
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
// We haven't found the field in the metadata declaration. Insert a new
// field.
const expr = node as ts.ObjectLiteralExpression;
if (expr.properties.length == 0) {
position = expr.getEnd() - 1;
toInsert = ` ${metadataField}: [${symbolName}]\n`;
} else {
node = expr.properties[expr.properties.length - 1];
position = node.getEnd();
// Get the indentation of the last element, if any.
const text = node.getFullText(source);
if (text.match('^\r?\r?\n')) {
toInsert = `,${text.match(/^\r?\n\s+/)[0]}${metadataField}: [${symbolName}]`;
} else {
toInsert = `, ${metadataField}: [${symbolName}]`;
}
}
} else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
// We found the field but it's empty. Insert it just before the `]`.
position--;
toInsert = `${symbolName}`;
} else {
// Get the indentation of the last element, if any.
const text = node.getFullText(source);
if (text.match(/^\r?\n/)) {
toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`;
} else {
toInsert = `, ${symbolName}`;
}
}
if (importPath !== null) {
return [
new InsertChange(ngModulePath, position, toInsert),
insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath),
];
}
return [new InsertChange(ngModulePath, position, toInsert)];
}
/**
* Custom function to insert a declaration (component, pipe, directive)
* into NgModule declarations. It also imports the component.
*/
export function addDeclarationToModule(
source: ts.SourceFile,
modulePath: string,
classifiedName: string,
importPath: string
): Change[] {
return addSymbolToNgModuleMetadata(source, modulePath, 'declarations', classifiedName, importPath);
}
/**
* Custom function to insert an NgModule into NgModule imports. It also imports the module.
*/
export function addImportToModule(
source: ts.SourceFile,
modulePath: string,
classifiedName: string,
importPath: string
): Change[] {
return addSymbolToNgModuleMetadata(source, modulePath, 'imports', classifiedName, importPath);
}
/**
* Custom function to insert a provider into NgModule. It also imports it.
*/
export function addProviderToModule(
source: ts.SourceFile,
modulePath: string,
classifiedName: string,
importPath: string
): Change[] {
return addSymbolToNgModuleMetadata(source, modulePath, 'providers', classifiedName, importPath);
}
/**
* Custom function to insert an export into NgModule. It also imports it.
*/
export function addExportToModule(
source: ts.SourceFile,
modulePath: string,
classifiedName: string,
importPath: string
): Change[] {
return addSymbolToNgModuleMetadata(source, modulePath, 'exports', classifiedName, importPath);
}
/**
* Custom function to insert an export into NgModule. It also imports it.
*/
export function addBootstrapToModule(
source: ts.SourceFile,
modulePath: string,
classifiedName: string,
importPath: string
): Change[] {
return addSymbolToNgModuleMetadata(source, modulePath, 'bootstrap', classifiedName, importPath);
}
/**
* Custom function to insert an entryComponent into NgModule. It also imports it.
*/
export function addEntryComponentToModule(
source: ts.SourceFile,
modulePath: string,
classifiedName: string,
importPath: string
): Change[] {
return addSymbolToNgModuleMetadata(source, modulePath, 'entryComponents', classifiedName, importPath);
}
/**
* Determine if an import already exists.
*/
export function isImported(source: ts.SourceFile, classifiedName: string, importPath: string): boolean {
const allNodes = getSourceNodes(source);
const matchingNodes = allNodes
.filter((node) => node.kind === ts.SyntaxKind.ImportDeclaration)
.filter((imp: ts.ImportDeclaration) => imp.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral)
.filter((imp: ts.ImportDeclaration) => {
return (imp.moduleSpecifier as ts.StringLiteral).text === importPath;
})
.filter((imp: ts.ImportDeclaration) => {
if (!imp.importClause) {
return false;
}
const nodes = findNodes(imp.importClause, ts.SyntaxKind.ImportSpecifier).filter(
(n) => n.getText() === classifiedName
);
return nodes.length > 0;
});
return matchingNodes.length > 0;
}

View File

@@ -0,0 +1,123 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export interface Host {
write(path: string, content: string): Promise<void>;
read(path: string): Promise<string>;
}
export interface Change {
apply(host: Host): Promise<void>;
// The file this change should be applied to. Some changes might not apply to
// a file (maybe the config).
readonly path: string | null;
// The order this change should be applied. Normally the position inside the file.
// Changes are applied from the bottom of a file to the top.
readonly order: number;
// The description of this change. This will be outputted in a dry or verbose run.
readonly description: string;
}
/**
* An operation that does nothing.
*/
export class NoopChange implements Change {
description = 'No operation.';
order = Infinity;
path = null;
apply(): Promise<void> {
return Promise.resolve();
}
}
/**
* Will add text to the source code.
*/
export class InsertChange implements Change {
order: number;
description: string;
constructor(public path: string, public pos: number, public toAdd: string) {
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
this.order = pos;
}
/**
* This method does not insert spaces if there is none in the original string.
*/
apply(host: Host): Promise<void> {
return host.read(this.path).then((content) => {
const prefix = content.substring(0, this.pos);
const suffix = content.substring(this.pos);
return host.write(this.path, `${prefix}${this.toAdd}${suffix}`);
});
}
}
/**
* Will remove text from the source code.
*/
export class RemoveChange implements Change {
order: number;
description: string;
constructor(public path: string, private pos: number, private toRemove: string) {
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = `Removed ${toRemove} into position ${pos} of ${path}`;
this.order = pos;
}
apply(host: Host): Promise<void> {
return host.read(this.path).then((content) => {
const prefix = content.substring(0, this.pos);
const suffix = content.substring(this.pos + this.toRemove.length);
// TODO: throw error if toRemove doesn't match removed string.
return host.write(this.path, `${prefix}${suffix}`);
});
}
}
/**
* Will replace text from the source code.
*/
export class ReplaceChange implements Change {
order: number;
description: string;
constructor(public path: string, private pos: number, private oldText: string, private newText: string) {
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
this.order = pos;
}
apply(host: Host): Promise<void> {
return host.read(this.path).then((content) => {
const prefix = content.substring(0, this.pos);
const suffix = content.substring(this.pos + this.oldText.length);
const text = content.substring(this.pos, this.pos + this.oldText.length);
if (text !== this.oldText) {
return Promise.reject(new Error(`Invalid replace: "${text}" != "${this.oldText}".`));
}
// TODO: throw error if oldText doesn't match removed string.
return host.write(this.path, `${prefix}${this.newText}${suffix}`);
});
}
}

View File

@@ -0,0 +1,5 @@
### Devkit Utils
These are utility files copied over from `@angular-devkit`.
They are not exported so they need to be manually copied over.
Please do not edit directly.

View File

@@ -0,0 +1,91 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import { findNodes, insertAfterLastOccurrence } from './ast-utils';
import { Change, NoopChange } from './change';
/**
* Add Import `import { symbolName } from fileName` if the import doesn't exit
* already. Assumes fileToEdit can be resolved and accessed.
* @param fileToEdit (file we want to add import to)
* @param symbolName (item to import)
* @param fileName (path to the file)
* @param isDefault (if true, import follows style for importing default exports)
* @return Change
*/
export function insertImport(
source: ts.SourceFile,
fileToEdit: string,
symbolName: string,
fileName: string,
isDefault = false
): Change {
const rootNode = source;
const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
// get nodes that map to import statements from the file fileName
const relevantImports = allImports.filter((node) => {
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
const importFiles = node
.getChildren()
.filter((child) => child.kind === ts.SyntaxKind.StringLiteral)
.map((n) => (n as ts.StringLiteral).text);
return importFiles.filter((file) => file === fileName).length === 1;
});
if (relevantImports.length > 0) {
let importsAsterisk = false;
// imports from import file
const imports: ts.Node[] = [];
relevantImports.forEach((n) => {
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
importsAsterisk = true;
}
});
// if imports * from fileName, don't add symbolName
if (importsAsterisk) {
return new NoopChange();
}
const importTextNodes = imports.filter((n) => (n as ts.Identifier).text === symbolName);
// insert import if it's not there
if (importTextNodes.length === 0) {
const fallbackPos =
findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() ||
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos);
}
return new NoopChange();
}
// no such import declaration exists
const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter(
(n: ts.StringLiteral) => n.text === 'use strict'
);
let fallbackPos = 0;
if (useStrict.length > 0) {
fallbackPos = useStrict[0].end;
}
const open = isDefault ? '' : '{ ';
const close = isDefault ? '' : ' }';
// if there are no imports or 'use strict' statement, insert import at beginning of file
const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
const separator = insertAtBeginning ? '' : ';\n';
const toInsert =
`${separator}import ${open}${symbolName}${close}` + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
return insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
}

View File

@@ -6,16 +6,16 @@ export { IonRouterLink, IonRouterLinkWithHref } from './navigation/router-link-d
export { IonTabs } from './navigation/tabs';
export { provideIonicAngular } from './providers/ionic-angular';
export { ActionSheetController } from './providers/action-sheet-controller';
export { AlertController } from './providers/alert-controller';
export { AnimationController } from './providers/animation-controller';
export { GestureController } from './providers/gesture-controller';
export { LoadingController } from './providers/loading-controller';
export { MenuController } from './providers/menu-controller';
export { ModalController } from './providers/modal-controller';
export { PickerController } from './providers/picker-controller';
export { PopoverController } from './providers/popover-controller';
export { ToastController } from './providers/toast-controller';
export {
AlertController,
LoadingController,
PickerController,
DomController,
NavController,
Config,

View File

@@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
import { OverlayBaseController } from '@ionic/angular/common';
import type { ActionSheetOptions } from '@ionic/core/components';
import { actionSheetController } from '@ionic/core/components';
import { defineCustomElement } from '@ionic/core/components/ion-action-sheet.js';
@Injectable({
providedIn: 'root',
@@ -10,6 +9,5 @@ import { defineCustomElement } from '@ionic/core/components/ion-action-sheet.js'
export class ActionSheetController extends OverlayBaseController<ActionSheetOptions, HTMLIonActionSheetElement> {
constructor() {
super(actionSheetController);
defineCustomElement();
}
}

View File

@@ -2,7 +2,6 @@ import { Injector, Injectable, EnvironmentInjector, inject } from '@angular/core
import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common';
import type { ModalOptions } from '@ionic/core/components';
import { modalController } from '@ionic/core/components';
import { defineCustomElement } from '@ionic/core/components/ion-modal.js';
@Injectable()
export class ModalController extends OverlayBaseController<ModalOptions, HTMLIonModalElement> {
@@ -12,7 +11,6 @@ export class ModalController extends OverlayBaseController<ModalOptions, HTMLIon
constructor() {
super(modalController);
defineCustomElement();
}
create(opts: ModalOptions): Promise<HTMLIonModalElement> {

View File

@@ -2,7 +2,6 @@ import { Injector, inject, EnvironmentInjector } from '@angular/core';
import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common';
import type { PopoverOptions } from '@ionic/core/components';
import { popoverController } from '@ionic/core/components';
import { defineCustomElement } from '@ionic/core/components/ion-popover.js';
export class PopoverController extends OverlayBaseController<PopoverOptions, HTMLIonPopoverElement> {
private angularDelegate = inject(AngularDelegate);
@@ -11,7 +10,6 @@ export class PopoverController extends OverlayBaseController<PopoverOptions, HTM
constructor() {
super(popoverController);
defineCustomElement();
}
create(opts: PopoverOptions): Promise<HTMLIonPopoverElement> {

View File

@@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
import { OverlayBaseController } from '@ionic/angular/common';
import type { ToastOptions } from '@ionic/core/components';
import { toastController } from '@ionic/core/components';
import { defineCustomElement } from '@ionic/core/components/ion-toast.js';
@Injectable({
providedIn: 'root',
@@ -10,6 +9,5 @@ import { defineCustomElement } from '@ionic/core/components/ion-toast.js';
export class ToastController extends OverlayBaseController<ToastOptions, HTMLIonToastElement> {
constructor() {
super(toastController);
defineCustomElement();
}
}

View File

@@ -1,7 +1,7 @@
describe('Providers', () => {
beforeEach(() => {
cy.visit('/lazy/providers');
});
})
it('should load all providers', () => {
cy.get('#is-loaded').should('have.text', 'true');
@@ -26,7 +26,7 @@ describe('Providers', () => {
cy.visit('/lazy/providers?firstParam=abc&secondParam=true');
cy.get('#query-params').should('have.text', 'firstParam: abc, firstParam: true');
});
})
// https://github.com/ionic-team/ionic-framework/issues/28337
it('should register menus correctly', () => {
@@ -39,22 +39,5 @@ describe('Providers', () => {
cy.get('ion-action-sheet').should('be.visible');
});
it('should open an alert', () => {
cy.get('button#open-alert').click();
cy.get('ion-alert').should('be.visible');
});
it('should open a loading-indicator', () => {
cy.get('button#open-loading').click();
cy.get('ion-loading').should('be.visible');
});
it('should open a picker', () => {
cy.get('button#open-picker').click();
cy.get('ion-picker').should('be.visible');
});
});

Some files were not shown because too many files have changed in this diff Show More