Compare commits

...

11 Commits

Author SHA1 Message Date
ionitron
f6a740dce5 v7.5.6 2023-11-21 14:31:40 +00:00
Liam DeBeasi
9453132aa8 fix(angular): overlays are defined when using standalone controllers (#28560)
Issue number: resolves #28385

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Overlay controllers do not register their respective overlays
components. This results in the overlay not appearing.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Each standalone overlay controller manually calls
`defineCustomElement` for their respective overlay component to ensure
the component is loaded/registered.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->


Dev build: `7.5.6-dev.11700492285.1581ed02`
2023-11-20 22:45:51 +00:00
Sean Perkins
c07312e5ed fix(angular): ng add @ionic/angular in standalone projects (#28523)
Issue number: Resolves #28514

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

When using the `@ionic/angular` schematic in an Angular 17 project (`ng
add @ionic/angular`), developers will receive an error preventing the
schematic from running.

Additionally, the previous implementations of the schematic are out of
sync with the current state of the Ionic starters:
- `variables.css` is empty and missing Ionic's defaults
- `ionic.config.json` is not created
- Schematic does not have support for module vs. standalone projects.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- `ng add @ionic/angular` works with Angular 17 projects
- `ng add @ionic/angular` has fallback behavior for Angular 16 projects
using `AppModule`
- Schematics now includes the proper `variables.css` from Ionic starters
- Ionicons assets will no longer be copied when being added to a
standalone project
- Refactors a majority of the implementation to use the utilities that
come directly from `@angular-devkit/schematics` and
`@schematics/angular`.
- Sets the `@ionic/angular-toolkit` CLI configuration and schematics
configuration in the `angular.json`
- Creates missing `ionic.config.json`

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev-build: `7.5.5-dev.11700239837.1925bbdb`

To test this PR:

1. Install Angular CLI v17 - `npm install -g @angular/cli@17`
2. Create a new project - `ng new angular-17`
3. Use the dev-build: - `ng add
@ionic/angular@7.5.5-dev.11700239837.1925bbdb`
4. Confirm the prompts
5. Validate that `provideIonicAngular({})` is added to the
`app.config.ts`
6. Validate that `ionic.config.json` was created
7. Validate that `angular.json` was updated with the
`@ionic/angular-devkit` configurations

Now verify legacy behavior:

1. Install Angular CLI v16 - `npm install -g @angular/cli@16`
2. Create a new project - `ng new angular-16`
3. Use the dev-build - `ng add
@ionic/angular@7.5.5-dev.11700239837.1925bbdb`
4. Confirm the prompts
5. Validate that `IonicModule.forRoot({})` is added to the
`app.module.ts`
8. Validate the ionicons glob pattern is added to the `angular.json`
9. Validate the `ionic.config.json` was created
10. Validate the `angular.json` was updated with the
`@ionic/angular-devkit` configurations
2023-11-20 22:20:20 +00:00
Liam DeBeasi
388d19e04f fix(datetime): updating value with min scrolls to new value (#28549)
Issue number: resolves #28548

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Datetime was not scrolling at all when the `value` prop was changed
programmatically. This was due to some logic we had in
`componentDidRender` to work around a WebKit bug which was causing the
scroll position to be moved back to where it was prior to setting the
`value` prop. This caused the scroll position to never move.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Datetime scrolls to the new value when `value` is updated
programmatically even if `min` is set.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `7.5.6-dev.11700169088.140f3e6a`

Co-authored-by: amandaejohnston
<amandaejohnston@users.noreply.github.com>
2023-11-20 14:19:44 +00:00
Shawn Taylor
adb01e2516 refactor(angular): loading controller uses correct core instance (#28543)
Issue number: Internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
As a takeaway from our learning session about a menuController bug in
Ionic Angular, the team would like to update our other providers to use
the same architecture as the menuController to prevent this kind of
issue from happening again in the future.

We also noticed that the common provider does not provide much value and
it's easier to just have two separate implementations in `src` and
`standalone`. (There wasn't much code we could de-duplicate)

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Removed the common loading provider in favor of separate ones in
src/standalone

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-17 21:16:56 +00:00
Sean Perkins
4f1b4cdc29 chore(core): type checking for unit tests (#28529)
Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Type checking inside of the Stencil unit tests have been disabled for a
long time. This has resulted in a difficult developer experience and
numerous issues (both types and implementation) within our unit tests.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Type checking is now enabled for all Stencil unit tests
- Tests have been updated to resolve type errors and implementation
errors
- Many `as any` casts were introduced, as many legacy tests test invalid
configurations of functions that require it (for example passing
`undefined` to an argument that cannot be `undefined`).

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

To test this PR you can checkout the branch locally. Install
dependencies in the `/core` directory to make sure you are on at least
`@stencil/core@4.7.2`.

Opening either a `.spec.ts` or `.spec.tsx` file, validate that your IDE
detects types and can provide auto completions for jest global types.

If you desire, you can provide an invalid type and try building the
project - you will observe the build will fail due to the invalid type.
2023-11-17 16:47:34 +00:00
Shawn Taylor
1a135ebd76 refactor(angular): alert controller uses correct core instance (#28538)
Issue number: Internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
As a takeaway from our learning session about a menuController bug in
Ionic Angular, the team would like to update our other providers to use
the same architecture as the menuController to prevent this kind of
issue from happening again in the future.

We also noticed that the common provider does not provide much value and
it's easier to just have two separate implementations in `src` and
`standalone`. (There wasn't much code we could de-duplicate)

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Removed the common alert provider in favor of separate ones in
src/standalone

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-16 16:50:43 +00:00
Liam DeBeasi
6a2be9fa3c fix(alert): match MD spec on tablet (#28501)
Issue number: resolves #23977

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

The MD Alert on tablet dimensions does not match the MD spec

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- MD Alert now follows the MD spec for tablet dimensions
- Added tablet and mobile viewport mixins for alert and the card modal.
(There should be no visual diffs for the card modal)

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

This supersedes https://github.com/ionic-team/ionic-framework/pull/27462
since I needed to add new screenshot tests. The author of that PR has
been given co-author credit here.

---------

Co-authored-by: GlenOttley <GlenOttley@users.noreply.github.com>
Co-authored-by: ionitron <hi@ionicframework.com>
Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
2023-11-16 16:28:36 +00:00
Shawn Taylor
9d57758e3e refactor(angular): picker controller uses correct core instance (#28521)
Issue number: Internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
As a takeaway from our learning session about a menuController bug in
Ionic Angular, the team would like to update our other providers to use
the same architecture as the menuController to prevent this kind of
issue from happening again in the future.

We also noticed that the common provider does not provide much value and
it's easier to just have two separate implementations in `src` and
`standalone`. (There wasn't much code we could de-duplicate)

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Removed the common picker provider in favor of separate ones in
src/standalone

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-16 12:57:02 +00:00
Sean Perkins
f143bd0a11 chore(angular): remove tslint (#28528)
Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Ionic Framework migrated to using `eslint` for linting all of our
projects awhile ago. However we left around an unused tslint config file
in the Angular project by accident.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Removes the unused tslint configuration file

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-15 19:08:13 +00:00
Sean Perkins
1b6f15dee1 chore(angular): type checking for standalone directory (#28531)
Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Type checking is disabled in the `standalone/` directory of the angular
project.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Enables type checking in the standalone directories. 

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-15 18:58:27 +00:00
119 changed files with 5819 additions and 1549 deletions

View File

@@ -3,6 +3,20 @@
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,6 +3,18 @@
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)

1594
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.5",
"version": "7.5.6",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -64,6 +64,7 @@
"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.tsx';
import { Item } from '../../item/item.tsx';
import { Accordion } from '../accordion.tsx';
import { AccordionGroup } from '../../accordion-group/accordion-group';
import { Item } from '../../item/item';
import { Accordion } from '../accordion';
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,10 +7,17 @@ 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' }}></ion-action-sheet>,
template: () => (
<ion-action-sheet
htmlAttributes={{
'data-testid': 'basic-action-sheet',
}}
overlayIndex={1}
></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,11 +52,20 @@
}
.alert-message {
max-height: $alert-md-content-max-height;
font-size: $alert-md-message-font-size;
}
/**
* 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;
}
}
.alert-message:empty {
@include padding($alert-md-message-empty-padding-top, $alert-md-message-empty-padding-end, $alert-md-message-empty-padding-bottom, $alert-md-message-empty-padding-start);
}
@@ -102,14 +111,24 @@
.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;
@@ -282,3 +301,14 @@
.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,6 +10,20 @@ $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,7 +84,15 @@
font-weight: normal;
}
.alert-message {
/**
* 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 {
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overflow-y: auto;

View File

@@ -1,6 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { Alert } from '../alert';
import { config } from '../../../global/config';
import { Alert } from '../alert';
describe('alert: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -9,7 +10,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);
});
@@ -21,7 +22,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);
});
@@ -33,7 +34,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

@@ -0,0 +1,47 @@
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.

After

Width:  |  Height:  |  Size: 44 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,7 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { Breadcrumb } from '../../breadcrumb/breadcrumb.tsx';
import { Breadcrumbs } from '../breadcrumbs.tsx';
import { Breadcrumb } from '../../breadcrumb/breadcrumb';
import { Breadcrumbs } from '../breadcrumbs';
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,4 +1,5 @@
import { newSpecPage } from '@stencil/core/testing';
import { Button } from '../../button';
describe('Button: Hidden Form Button', () => {
@@ -15,8 +16,7 @@ describe('Button: Hidden Form Button', () => {
return page.body.querySelectorAll('form button');
};
const form = page.body.querySelectorAll('form');
const button = page.body.querySelector('ion-button');
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 } = this;
const { presentation, prevPresentation, calendarBodyRef, minParts, preferWheel, forceRenderDate } = this;
/**
* TODO(FW-2165)
@@ -1150,7 +1150,20 @@ 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)');
if (workingMonth) {
/**
* 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) {
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
}
}

View File

@@ -1,21 +1,22 @@
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 = { month: 1, day: 1, year: 2021 };
const reference: DatetimeParts = { 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 })).toEqual(false);
expect(isSameDay(reference, { month: null, day: null, year: null } as any)).toEqual(false);
});
});
describe('isBefore()', () => {
it('should return correct results for month, day, and year', () => {
const reference = { month: 1, day: 1, year: 2021 };
const reference: DatetimeParts = { 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);
@@ -23,13 +24,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 })).toEqual(false);
expect(isBefore(reference, { month: null, day: null, year: null } as any)).toEqual(false);
});
});
describe('isAfter()', () => {
it('should return correct results for month, day, and year', () => {
const reference = { month: 2, day: 2, year: 2021 };
const reference: DatetimeParts = { 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);
@@ -42,6 +43,6 @@ describe('isAfter()', () => {
* 2021 > undefined === false
* 2021 > null === true
*/
expect(isAfter(reference, { month: null, day: null, year: null })).toEqual(true);
expect(isAfter(reference, { month: null, day: null, year: null } as any)).toEqual(true);
});
});

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import type { DatetimeParts } from '../datetime-interface';
import {
getPreviousYear,
getNextYear,
@@ -103,31 +104,31 @@ describe('getInternalHourValue()', () => {
describe('calculateHourFromAMPM()', () => {
it('should correctly convert from AM to PM', () => {
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: 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: 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);
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);
});
});

View File

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

View File

@@ -1,6 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { InfiniteScrollContent } from '../infinite-scroll-content';
import { config } from '../../../global/config';
import { InfiniteScrollContent } from '../infinite-scroll-content';
describe('infinite-scroll-content: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -9,7 +10,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);
});
@@ -21,7 +22,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);
});
@@ -33,7 +34,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,4 +1,5 @@
import { newSpecPage } from '@stencil/core/testing';
import { Input } from '../input';
describe('input: rendering', () => {
@@ -8,7 +9,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');
@@ -63,9 +64,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');
});
@@ -77,9 +78,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');
});
@@ -91,9 +92,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,6 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { Input } from '../../input';
import { Item } from '../../../item/item';
import { Input } from '../../input';
it('should render as modern when label is set asynchronously', async () => {
const page = await newSpecPage({
@@ -12,7 +13,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,9 +1,10 @@
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({
@@ -11,7 +12,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);
});
@@ -27,7 +28,7 @@ describe('ion-item', () => {
`,
});
const item = page.body.querySelector('ion-item');
const item = page.body.querySelector('ion-item')!;
expect(item.getAttribute('role')).toBe('listitem');
});
@@ -45,7 +46,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 htmlAttributes={{ 'data-testid': 'basic-loading' }}></ion-loading>,
template: () => <ion-loading overlayIndex={1} 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
// --------------------------------------------------
@media screen and (max-width: 767px) {
@include mobile-viewport() {
@supports (width: max(0px, 1px)) {
:host(.modal-card) {
--height: calc(100% - max(30px, var(--ion-safe-area-top)) - 10px);
@@ -60,7 +60,7 @@
}
}
@media screen and (min-width: 768px) {
@include tablet-viewport() {
: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' }}></ion-modal>,
template: () => <ion-modal htmlAttributes={{ 'data-testid': 'basic-modal' }} overlayIndex={1}></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,19 +1,18 @@
import { h } from '@stencil/core';
import { h, setMode } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { setMode } from '@stencil/core';
import { Modal } from '../../modal';
import { Content } from '../../../content/content';
import { Modal } from '../../modal';
describe('modal: canDismiss', () => {
describe('modal: regular modal', () => {
it('should dismiss when canDismiss is true', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal animated={false} canDismiss={true}></ion-modal>,
template: () => <ion-modal overlayIndex={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();
@@ -26,10 +25,10 @@ describe('modal: canDismiss', () => {
it('should not dismiss when canDismiss is false', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal animated={false} canDismiss={false}></ion-modal>,
template: () => <ion-modal overlayIndex={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();
@@ -44,6 +43,7 @@ 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,6 +69,7 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
animated={false}
canDismiss={() => {
return new Promise((resolve) => {
@@ -79,7 +80,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -94,19 +95,24 @@ describe('modal: canDismiss', () => {
/**
* Card modal is only available on iOS
*/
setMode((elm) => 'ios');
setMode(() => 'ios');
});
it('should dismiss when canDismiss is true', async () => {
const page = await newSpecPage({
components: [Content, Modal],
template: () => (
<ion-modal presentingElement={document.createElement('div')} animated={false} canDismiss={true}>
<ion-modal
overlayIndex={1}
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();
@@ -120,13 +126,18 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Content, Modal],
template: () => (
<ion-modal presentingElement={document.createElement('div')} animated={false} canDismiss={false}>
<ion-modal
overlayIndex={1}
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();
@@ -141,6 +152,7 @@ describe('modal: canDismiss', () => {
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={() => {
@@ -154,7 +166,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -169,6 +181,7 @@ describe('modal: canDismiss', () => {
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={() => {
@@ -182,7 +195,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -197,11 +210,17 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Modal],
template: () => (
<ion-modal breakpoints={[0, 1]} initialBreakpoint={1} animated={false} canDismiss={true}></ion-modal>
<ion-modal
overlayIndex={1}
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();
@@ -215,11 +234,17 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Modal],
template: () => (
<ion-modal breakpoints={[0, 1]} initialBreakpoint={1} animated={false} canDismiss={false}></ion-modal>
<ion-modal
overlayIndex={1}
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();
@@ -234,6 +259,7 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
@@ -246,7 +272,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -261,6 +287,7 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
@@ -273,7 +300,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -288,15 +315,15 @@ describe('modal: canDismiss', () => {
const canDismiss = jest.fn();
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal animated={false} canDismiss={canDismiss}></ion-modal>,
template: () => <ion-modal overlayIndex={1} 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();
const returnValue = await modal.dismiss('my data', 'my role');
await modal.dismiss('my data', 'my role');
expect(canDismiss).toHaveBeenCalledWith('my data', 'my role');
});

View File

@@ -1,6 +1,5 @@
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';
@@ -197,7 +196,6 @@ describe('NavController', () => {
.insert(-1, null as any, null, null, trnsDone)
.then(() => {
fail('it should not succeed');
done();
})
.catch((err: Error) => {
const hasCompleted = false;
@@ -252,7 +250,6 @@ describe('NavController', () => {
.pop(null, trnsDone)
.then(() => {
fail('it should not succeed');
done();
})
.catch((err: any) => {
const hasCompleted = false;
@@ -819,15 +816,10 @@ 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;
});
@@ -848,7 +840,7 @@ describe('NavController', () => {
pause: jest.fn(),
cancel: jest.fn(),
onfinish: undefined,
};
} as any;
animation.play = () => {
if (animation.onfinish) {

View File

@@ -19,6 +19,7 @@ describe('picker-column', () => {
text: 'Java',
},
],
name: 'programmingLanguages',
};
const page = await newSpecPage({
@@ -26,8 +27,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,18 +15,19 @@ describe('picker-column: dynamic options', () => {
const page = await newSpecPage({
components: [PickerColumnCmp],
template: () => <ion-picker-column col={{ options: defaultOptions }}></ion-picker-column>,
template: () => <ion-picker-column col={{ options: defaultOptions, name: 'animals' }}></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: [] };
const col = { cssClass: 'test-class', options: [], name: 'col' };
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 htmlAttributes={{ 'data-testid': 'basic-popover' }}></ion-popover>,
template: () => <ion-popover overlayIndex={1} 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']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
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']);
const array = createArrayOfElements(['ion-item', 'ion-item']) as HTMLIonItemElement[];
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']);
const array = createArrayOfElements(['ion-item', 'div', 'ion-item']) as HTMLIonItemElement[];
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']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
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']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
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']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
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']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
expect(getPrevItem(array, array[0])).toEqual(undefined);
});
});

View File

@@ -1,7 +1,8 @@
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();
@@ -21,7 +22,7 @@ describe('ion-radio', () => {
`,
});
const radio = page.root.querySelector('ion-radio');
const radio = page.body.querySelector('ion-radio')!;
expect(radio.classList.contains('radio-checked')).toBe(false);
radio.value = 'a';
@@ -43,8 +44,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,8 +1,10 @@
import { newSpecPage } from '@stencil/core/testing';
import { Range } from '../range';
import { Item } from '../../item/item';
let sharedRange;
import { Item } from '../../item/item';
import { Range } from '../range';
let sharedRange: Range;
describe('Range', () => {
beforeEach(() => {
sharedRange = new Range();
@@ -21,7 +23,8 @@ describe('Range', () => {
];
valueTests.forEach((test) => {
expect(sharedRange.ensureValueInBounds(test[0])).toBe(test[1]);
// Casting as any since we are accessing a private API on the range component
expect((sharedRange as any).ensureValueInBounds(test[0])).toBe(test[1]);
});
});
@@ -58,7 +61,8 @@ describe('Range', () => {
];
valueTests.forEach((test) => {
expect(sharedRange.ensureValueInBounds(test[0])).toEqual(test[1]);
// Casting as any since we are accessing a private API on the range component
expect((sharedRange as any).ensureValueInBounds(test[0])).toEqual(test[1]);
});
});
});
@@ -73,7 +77,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');
});
});
@@ -89,7 +93,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);
});
@@ -104,7 +108,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);
});
@@ -119,7 +123,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);
});
@@ -134,7 +138,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);
});
@@ -149,7 +153,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);
});
@@ -164,7 +168,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);
});
@@ -177,7 +181,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);
});
@@ -191,7 +195,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);
});
@@ -206,7 +210,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);
});
@@ -221,7 +225,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,6 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { RefresherContent } from '../refresher-content';
import { config } from '../../../global/config';
import { RefresherContent } from '../refresher-content';
describe('refresher-content: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -9,11 +10,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);
});
@@ -25,11 +26,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);
});
@@ -41,11 +42,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, undefined)).toBeUndefined();
expect(mergeParams(undefined, null)).toBeUndefined();
expect(mergeParams(null, null)).toBeUndefined();
expect(mergeParams(null as any, undefined)).toBeUndefined();
expect(mergeParams(undefined, null as any)).toBeUndefined();
expect(mergeParams(null as any, null as any)).toBeUndefined();
});
it('should merge undefined with params', () => {
const params = { data: '1' };
expect(mergeParams(undefined, params)).toEqual(params);
expect(mergeParams(null, params)).toEqual(params);
expect(mergeParams(null as any, params)).toEqual(params);
expect(mergeParams(params, undefined)).toEqual(params);
expect(mergeParams(params, null)).toEqual(params);
expect(mergeParams(params, null as any)).toEqual(params);
});
it('should merge params with params', () => {
@@ -253,36 +253,44 @@ describe('RouterSegments', () => {
describe('matchesRedirect', () => {
it('should match empty redirect', () => {
expect(matchesRedirect([''], { from: [''], to: [''] })).toBeTruthy();
expect(matchesRedirect([''], { from: ['*'], to: [''] })).toBeTruthy();
expect(matchesRedirect([''], { from: [''], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect([''], { from: ['*'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect([''], { from: ['hola'], to: [''] })).toBeFalsy();
expect(matchesRedirect([''], { from: ['hola', '*'], to: [''] })).toBeFalsy();
expect(matchesRedirect([''], { from: ['hola'], to: { segments: [] } })).toBeFalsy();
expect(matchesRedirect([''], { from: ['hola', '*'], to: { segments: [''] } })).toBeFalsy();
});
it('should match simple segment redirect', () => {
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: [''] } })).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: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', 'adios'], to: [''] })).toBeFalsy();
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();
});
it('should match long route', () => {
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: [''] })).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: ['*'], to: { segments: [''] } })).toBeTruthy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to', '*'], to: [''] })
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: ['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: ['workouts', 'path'], to: { segments: [''] } })
).toBeFalsy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to', '*'], to: { segments: [''] } })
).toBeFalsy();
});

View File

@@ -18,19 +18,21 @@ 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' }], routes)).toEqual('/');
expect(
getRoutePath([{ id: 'PAGE-TABS' }, { id: 'tab-schedule' }, { id: 'page-schedule' }] as RouteID[], routes)
).toEqual('/');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }], routes)).toEqual('/speaker');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }] 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: 'TAB-SPEAKER' }, { id: 'page-speaker-list' }] as RouteID[], routes)
).toEqual('/speaker');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'PAGE-MAP' }], routes)).toEqual('/map');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'PAGE-MAP' }] as RouteID[], routes)).toEqual('/map');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'page-about' }], routes)).toEqual('/about');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'page-about' }] as RouteID[], routes)).toEqual('/about');
expect(getRoutePath([{ id: 'page-tutorial' }], routes)).toEqual('/tutorial');
expect(getRoutePath([{ id: 'page-tutorial' }] as RouteID[], 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.root.querySelector('ion-segment-button');
const segmentButton = page.body.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('input[type="hidden"]');
const hiddenInput = select.querySelector<HTMLInputElement>('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,4 +1,5 @@
import { newSpecPage } from '@stencil/core/testing';
import { Textarea } from '../textarea';
it('should inherit attributes', async () => {
@@ -7,7 +8,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');
@@ -31,9 +32,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');
});
@@ -45,9 +46,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');
});
@@ -59,9 +60,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 { Toast } from '../toast';
import { config } from '../../../global/config';
import { toastController } from '../../../utils/overlays';
import { Toast } from '../toast';
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 htmlAttributes={{ 'data-testid': 'basic-toast' }}></ion-toast>,
template: () => <ion-toast overlayIndex={1} 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 buttons={[{ text: 'Cancel', role: 'cancel' }]}></ion-toast>,
template: () => <ion-toast overlayIndex={1} 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,11 +1,12 @@
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({ name: 'Doc Brown' });
expect(config.get('name')).toEqual('Doc Brown');
expect(config.getBoolean('name')).toBe(false);
config.reset({ mode: 'ios' } as IonicConfig);
expect(config.get('mode')).toEqual('ios');
expect(config.getBoolean('mode')).toBe(false);
});
it('should get a boolean value', () => {
@@ -18,14 +19,14 @@ describe('Config', () => {
bool4: 'hola',
bool5: 0,
bool6: 1,
});
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);
} 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);
});
it('should get a number value', () => {
@@ -36,12 +37,12 @@ describe('Config', () => {
nu2: '200',
nu3: '2.3',
nu4: -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);
} 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);
});
it('should not get fallback', () => {
@@ -56,29 +57,29 @@ describe('Config', () => {
nu0: '0',
nu1: 0,
nu2: 10,
});
expect(config.get('text0', 'HEY')).toEqual('');
expect(config.get('text1', 'HEY')).toEqual('hola');
} as any);
expect(config.get('text0' as any, 'HEY')).toEqual('');
expect(config.get('text1' as any, 'HEY')).toEqual('hola');
expect(config.getBoolean('bool0', true)).toEqual(false);
expect(config.getBoolean('bool1', true)).toEqual(false);
expect(config.getBoolean('bool0' as any, true)).toEqual(false);
expect(config.getBoolean('bool1' as any, true)).toEqual(false);
expect(config.getNumber('nu0', 100)).toEqual(0);
expect(config.getNumber('nu1', 100)).toEqual(0);
expect(config.getNumber('nu2', 100)).toEqual(10);
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);
});
it('should get fallback', () => {
const config = new Config();
expect(config.get('text0', 'HEY')).toEqual('HEY');
expect(config.getBoolean('bool0', true)).toEqual(true);
expect(config.getNumber('nu0', 100)).toEqual(100);
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);
});
it('should set value', () => {
const config = new Config();
expect(config.get('text0', 'HEY')).toEqual('HEY');
config.set('text0', 'hola');
expect(config.get('text0', 'HEY')).toEqual('hola');
expect(config.get('text0' as any, 'HEY')).toEqual('HEY');
config.set('text0' as any, 'hola');
expect(config.get('text0' as any, 'HEY')).toEqual('hola');
});
});

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { newSpecPage } from '@stencil/core/testing';
import { Item } from '../../components/item/item.tsx';
import { Label } from '../../components/label/label.tsx';
import { Toggle } from '../../components/toggle/toggle.tsx';
import { Item } from '../../components/item/item';
import { Label } from '../../components/label/label';
import { Toggle } from '../../components/toggle/toggle';
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,3 +1,4 @@
import type { BackButtonEvent } from '../../../src/interface';
import { startHardwareBackButton } from '../hardware-back-button';
describe('Hardware Back Button', () => {
@@ -5,7 +6,7 @@ describe('Hardware Back Button', () => {
it('should call handler', () => {
const cbSpy = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(0, cbSpy);
(ev as BackButtonEvent).detail.register(0, cbSpy);
});
dispatchBackButtonEvent();
@@ -16,8 +17,8 @@ describe('Hardware Back Button', () => {
const cbSpy = jest.fn();
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(100, cbSpy);
ev.detail.register(99, cbSpyTwo);
(ev as BackButtonEvent).detail.register(100, cbSpy);
(ev as BackButtonEvent).detail.register(99, cbSpyTwo);
});
dispatchBackButtonEvent();
@@ -29,8 +30,8 @@ describe('Hardware Back Button', () => {
const cbSpy = jest.fn();
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(100, cbSpy);
ev.detail.register(100, cbSpyTwo);
(ev as BackButtonEvent).detail.register(100, cbSpy);
(ev as BackButtonEvent).detail.register(100, cbSpyTwo);
});
dispatchBackButtonEvent();
@@ -39,13 +40,13 @@ describe('Hardware Back Button', () => {
});
it('should call multiple callbacks', () => {
const cbSpy = (processNextHandler) => {
const cbSpy = (processNextHandler: () => void) => {
processNextHandler();
};
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(100, cbSpy);
ev.detail.register(99, cbSpyTwo);
(ev as BackButtonEvent).detail.register(100, cbSpy);
(ev as BackButtonEvent).detail.register(99, cbSpyTwo);
});
dispatchBackButtonEvent();

View File

@@ -1,9 +1,8 @@
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()', () => {
@@ -15,7 +14,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);
@@ -34,7 +33,7 @@ describe('setRootAriaHidden()', () => {
`,
});
const nav = page.body.querySelector('ion-nav');
const nav = page.body.querySelector('ion-nav')!;
expect(nav.hasAttribute('aria-hidden')).toEqual(false);
@@ -54,8 +53,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);
@@ -90,8 +89,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();
@@ -109,9 +108,9 @@ describe('setRootAriaHidden()', () => {
`,
});
const routerOutlet = page.body.querySelector('ion-router-outlet');
const modalOne = page.body.querySelector('ion-modal#one');
const modalTwo = page.body.querySelector('ion-modal#two');
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')!;
await modalOne.present();

View File

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

View File

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

View File

@@ -3,6 +3,14 @@
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.5",
"version": "7.5.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/docs",
"version": "7.5.5",
"version": "7.5.6",
"license": "MIT"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/docs",
"version": "7.5.5",
"version": "7.5.6",
"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.5"
"version": "7.5.6"
}

View File

@@ -3,6 +3,14 @@
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.5",
"version": "7.5.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular-server",
"version": "7.5.5",
"version": "7.5.6",
"license": "MIT",
"dependencies": {
"@ionic/core": "^7.5.5"
"@ionic/core": "^7.5.6"
},
"devDependencies": {
"@angular-eslint/eslint-plugin": "^14.0.0",

View File

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

View File

@@ -3,6 +3,18 @@
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,7 +1,4 @@
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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -20,9 +20,6 @@ export * from './directives/validators';
// PROVIDERS
export {
AlertController,
LoadingController,
PickerController,
DomController,
NavController,
Config,
@@ -35,11 +32,14 @@ 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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,13 @@
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,3 +4,244 @@
/* 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,10 +12,19 @@ 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 { addModuleImportToRootModule } from './../utils/ast';
import { addArchitectBuilder, addAsset, addStyle, getDefaultAngularAppName } from './../utils/config';
import { addIonicModuleImportToNgModule } from '../utils/ast';
import {
addArchitectBuilder,
addAsset,
addCli,
addSchematics,
addStyle,
getDefaultAngularAppName,
} from './../utils/config';
import { addPackageToPackageJson } from './../utils/package';
import { Schema as IonAddOptions } from './schema';
@@ -33,9 +42,53 @@ 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) => {
addModuleImportToRootModule(host, projectSourceRoot, 'IonicModule.forRoot()', '@ionic/angular');
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')}({})`
);
}
return host;
};
}
@@ -63,15 +116,49 @@ function addIonicStyles(projectName: string, projectSourceRoot: Path): Rule {
};
}
function addIonicons(projectName: string): Rule {
function addIonicons(projectName: string, projectSourceRoot: Path): Rule {
return (host: Tree) => {
const ioniconsGlob = {
glob: '**/*.svg',
input: 'node_modules/ionicons/dist/ionicons/svg',
output: './svg',
};
addAsset(host, projectName, 'build', ioniconsGlob);
addAsset(host, projectName, 'test', ioniconsGlob);
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
)
);
}
return host;
};
}
@@ -129,10 +216,13 @@ 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),
addIonicons(options.project, sourcePath),
addIonicConfig(sourcePath),
mergeWith(rootTemplateSource),
// install freshly added dependencies
installNodeDeps(),

View File

@@ -1,14 +1,13 @@
import { normalize } from '@angular-devkit/core';
import { Tree, SchematicsException } from '@angular-devkit/schematics';
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 * 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.
*/
export function getSourceFile(host: Tree, path: string): ts.SourceFile {
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}`);
@@ -21,32 +20,17 @@ export function getSourceFile(host: Tree, path: string): ts.SourceFile {
/**
* Import and add module to root app module.
*/
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);
export function addIonicModuleImportToNgModule(host: Tree, modulePath: string): void {
const recorder = host.beginUpdate(modulePath);
const moduleSource = getSourceFile(host, modulePath) as any;
changes.forEach((change) => {
if (change instanceof InsertChange) {
recorder.insertLeft(change.pos, change.toAdd);
}
});
const ionicModuleChange = insertImport(moduleSource, modulePath, 'IonicModule', '@ionic/angular');
applyToUpdateRecorder(recorder, [ionicModuleChange]);
const metadataChange = addSymbolToNgModuleMetadata(moduleSource, modulePath, 'imports', 'IonicModule.forRoot({})');
applyToUpdateRecorder(recorder, metadataChange);
host.commitUpdate(recorder);
}

View File

@@ -1,24 +1,28 @@
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 CONFIG_PATH = 'angular.json';
const ANGULAR_JSON_PATH = 'angular.json';
// 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 readConfig<T extends JsonObject = JsonObject>(host: Tree): T {
return host.readJson(ANGULAR_JSON_PATH) as T;
}
export function writeConfig(host: Tree, config: JSON): void {
host.overwrite(CONFIG_PATH, JSON.stringify(config, null, 2));
export function writeConfig(host: Tree, config: JsonObject): void {
host.overwrite(ANGULAR_JSON_PATH, JSON.stringify(config, null, 2));
}
function isAngularBrowserProject(projectConfig: any): boolean {
if (projectConfig.projectType === 'application') {
const buildConfig = projectConfig.architect.build;
return buildConfig.builder === '@angular-devkit/build-angular:browser';
// 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 false;
@@ -38,7 +42,7 @@ export function getDefaultAngularAppName(config: any): string {
return projectNames[0];
}
export function getAngularAppConfig(config: any, projectName: string): any | never {
function getAngularJson(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}`);
@@ -59,8 +63,8 @@ export function getAngularAppConfig(config: any, projectName: string): any | nev
export function addStyle(host: Tree, projectName: string, stylePath: string): void {
const config = readConfig(host);
const appConfig = getAngularAppConfig(config, projectName);
appConfig.architect.build.options.styles.push({
const angularJson = getAngularJson(config, projectName);
angularJson.architect.build.options.styles.push({
input: stylePath,
});
writeConfig(host, config);
@@ -73,8 +77,8 @@ export function addAsset(
asset: string | { glob: string; input: string; output: string }
): void {
const config = readConfig(host);
const appConfig = getAngularAppConfig(config, projectName);
const target = appConfig.architect[architect];
const angularJson = getAngularJson(config, projectName);
const target = angularJson.architect[architect];
if (target) {
target.options.assets.push(asset);
writeConfig(host, config);
@@ -88,11 +92,48 @@ export function addArchitectBuilder(
builderOpts: any
): void | never {
const config = readConfig(host);
const appConfig = getAngularAppConfig(config, projectName);
appConfig.architect[builderName] = builderOpts;
const angularJson = getAngularJson(config, projectName);
angularJson.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

@@ -1,579 +0,0 @@
/**
* @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

@@ -1,123 +0,0 @@
/**
* @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

@@ -1,5 +0,0 @@
### 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

@@ -1,91 +0,0 @@
/**
* @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,6 +2,7 @@ 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',
@@ -9,5 +10,6 @@ import { actionSheetController } from '@ionic/core/components';
export class ActionSheetController extends OverlayBaseController<ActionSheetOptions, HTMLIonActionSheetElement> {
constructor() {
super(actionSheetController);
defineCustomElement();
}
}

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 { OverlayBaseController } from '../utils/overlay';
import { defineCustomElement } from '@ionic/core/components/ion-alert.js';
@Injectable({
providedIn: 'root',
@@ -10,5 +10,6 @@ import { OverlayBaseController } from '../utils/overlay';
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 { OverlayBaseController } from '../utils/overlay';
import { defineCustomElement } from '@ionic/core/components/ion-loading.js';
@Injectable({
providedIn: 'root',
@@ -10,5 +10,6 @@ import { OverlayBaseController } from '../utils/overlay';
export class LoadingController extends OverlayBaseController<LoadingOptions, HTMLIonLoadingElement> {
constructor() {
super(loadingController);
defineCustomElement();
}
}

View File

@@ -2,6 +2,7 @@ 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> {
@@ -11,6 +12,7 @@ export class ModalController extends OverlayBaseController<ModalOptions, HTMLIon
constructor() {
super(modalController);
defineCustomElement();
}
create(opts: ModalOptions): Promise<HTMLIonModalElement> {

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

View File

@@ -2,6 +2,7 @@ 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);
@@ -10,6 +11,7 @@ export class PopoverController extends OverlayBaseController<PopoverOptions, HTM
constructor() {
super(popoverController);
defineCustomElement();
}
create(opts: PopoverOptions): Promise<HTMLIonPopoverElement> {

View File

@@ -2,6 +2,7 @@ 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',
@@ -9,5 +10,6 @@ import { toastController } from '@ionic/core/components';
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,5 +39,22 @@ 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