mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-14 00:21:31 +08:00
Merge remote-tracking branch 'origin/main' into sp/sync-feature-8.0-with-main-04-08
This commit is contained in:
497
docs/CONTRIBUTING.md
Normal file
497
docs/CONTRIBUTING.md
Normal file
@ -0,0 +1,497 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest in contributing to the Ionic Framework! 🎉
|
||||
|
||||
- [Contributing Etiquette](#contributing-etiquette)
|
||||
- [Creating an Issue](#creating-an-issue)
|
||||
* [Creating a Good Code Reproduction](#creating-a-good-code-reproduction)
|
||||
- [Using VS Code on Windows](#using-vs-code-on-windows)
|
||||
- [Creating a Pull Request](#creating-a-pull-request)
|
||||
* [Requirements](#requirements)
|
||||
* [Setup](#setup)
|
||||
* [Core](#core)
|
||||
+ [Modifying Components](#modifying-components)
|
||||
+ [Preview Changes](#preview-changes)
|
||||
+ [Lint Changes](#lint-changes)
|
||||
+ [Modifying Documentation](#modifying-documentation)
|
||||
+ [Modifying Tests](#modifying-tests)
|
||||
- [Screenshot Tests](#screenshot-tests)
|
||||
+ [Building Changes](#building-changes)
|
||||
* [Angular, React, and Vue](#angular-react-and-vue)
|
||||
+ [Modifying Files](#modifying-files)
|
||||
+ [Preview Changes](#preview-changes-1)
|
||||
+ [Lint Changes](#lint-changes-1)
|
||||
+ [Modifying Tests](#modifying-tests-1)
|
||||
+ [Building Changes](#building-changes-1)
|
||||
* [Submit Pull Request](#submit-pull-request)
|
||||
- [Commit Message Guidelines](#commit-message-guidelines)
|
||||
* [Commit Message Format](#commit-message-format)
|
||||
* [Revert](#revert)
|
||||
* [Type](#type)
|
||||
* [Scope](#scope)
|
||||
* [Subject](#subject)
|
||||
* [Body](#body)
|
||||
* [Footer](#footer)
|
||||
* [Examples](#examples)
|
||||
- [License](#license)
|
||||
|
||||
|
||||
## Contributing Etiquette
|
||||
|
||||
Please see our [Contributor Code of Conduct](https://github.com/ionic-team/ionic-framework/blob/main/CODE_OF_CONDUCT.md) for information on our rules of conduct.
|
||||
|
||||
|
||||
## Creating an Issue
|
||||
|
||||
* If you have a question about using the framework, please ask on the [Ionic Forum](http://forum.ionicframework.com/) or in the [Ionic Discord](https://ionic.link/discord).
|
||||
|
||||
* It is required that you clearly describe the steps necessary to reproduce the issue you are running into. Although we would love to help our users as much as possible, diagnosing issues without clear reproduction steps is extremely time-consuming and simply not sustainable.
|
||||
|
||||
* The issue list of this repository is exclusively for bug reports and feature requests. Non-conforming issues will be closed immediately.
|
||||
|
||||
* Issues with no clear steps to reproduce will not be triaged. If an issue is labeled with "needs: reply" and receives no further replies from the author of the issue for more than 14 days, it will be closed.
|
||||
|
||||
* If you think you have found a bug, or have a new feature idea, please start by making sure it hasn't already been [reported](https://github.com/ionic-team/ionic-framework/issues?utf8=%E2%9C%93&q=is%3Aissue). You can search through existing issues to see if there is a similar one reported. Include closed issues as it may have been closed with a solution.
|
||||
|
||||
* Next, [create a new issue](https://github.com/ionic-team/ionic-framework/issues/new/choose) that thoroughly explains the problem. Please fill out the populated issue form before submitting the issue.
|
||||
|
||||
|
||||
## Creating a Good Code Reproduction
|
||||
|
||||
### What is a Code Reproduction?
|
||||
|
||||
A code reproduction is a small application that is built to demonstrate a particular issue. The code reproduction should contain the minimum amount of code needed to recreate the issue and should focus on a single issue.
|
||||
|
||||
### Why Should You Create a Reproduction?
|
||||
|
||||
A code reproduction of the issue you are experiencing helps us better isolate the cause of the problem. This is an important first step to getting any bug fixed!
|
||||
|
||||
Without a reliable code reproduction, it is unlikely we will be able to resolve the issue, leading to it being closed. In other words, creating a code reproduction of the issue helps us help you.
|
||||
|
||||
### How to Create a Reproduction
|
||||
|
||||
* Create a new Ionic application using one of our starter templates. The `blank` starter application is a great choice for this. You can create one using the following Ionic CLI command: `ionic start myApp blank`
|
||||
* Add the minimum amount of code needed to recreate the issue you are experiencing. Do not include anything that is not required to reproduce the issue. This includes any 3rd party plugins you have installed.
|
||||
* Publish the application on GitHub and include a link to it when [creating an issue](#creating-an-issue).
|
||||
* Be sure to include steps to reproduce the issue. These steps should be clear and easy to follow.
|
||||
|
||||
### Benefits of Creating a Reproduction
|
||||
|
||||
* **Uses the latest version of Ionic:** By creating a new Ionic application, you are ensuring that you are testing against the latest version of the framework. Sometimes the issues you are experiencing have already been resolved in a newer version of the framework!
|
||||
* **Minimal surface area:** By removing code that is not needed in order to reproduce the issue, it makes it easier to identify the cause of the issue.
|
||||
* **No secret code needed:** Creating a minimal reproduction of the issue prevents you from having to publish any proprietary code used in your project.
|
||||
* **Get help fixing the issue:** If we can reliably reproduce an issue, there is a good chance we will be able to address it.
|
||||
|
||||
## Using VS Code on Windows
|
||||
|
||||
To contribute on Windows, do the following:
|
||||
|
||||
- Configure VS Code to read/save files using line breaks (LF) instead of carriage returns (CRLF). Set it globally by navigating to: Settings -> Text Editor -> Files -> Eol. Set to `\n`.
|
||||
|
||||
- You can optionally use the following settings in your `.vscode/settings.json`:
|
||||
```json
|
||||
{ "files.eol": "\n" }
|
||||
```
|
||||
|
||||
- Check that the Git setting `core.autocrlf` is set to `false`: run `git config -l | grep autocrlf`. Switch it to false using: `git config --global core.autocrlf false`.
|
||||
- If you've already cloned the `ionic-framework` repo, the files may already be cached as LF. To undo this, you need to clean the cache files of the repository. Run the following (make sure you stage or commit your changes first): `git rm --cached -r .` then `git reset --hard`.
|
||||
|
||||
## Creating a Pull Request
|
||||
|
||||
Before creating a pull request, please read our requirements that explains the minimal details to have your PR considered and merged into the codebase.
|
||||
|
||||
### Requirements
|
||||
1. PRs must reference an existing issue that describes the issue or feature being submitted.
|
||||
2. PRs must have a reproduction app or the issue must include a reproduction app to verify changes against.
|
||||
3. PRs must include tests covering the changed behavior or a description of why tests cannot be written.
|
||||
|
||||
> Note: We appreciate you taking the time to contribute! Before submitting a pull request, please take the time to comment on the issue you are wanting to resolve. This helps us prevent duplicate effort or advise if the team is already addressing the issue.
|
||||
|
||||
* Looking for an issue to fix? Look through our issues with the [help wanted](https://github.com/ionic-team/ionic-framework/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) label!
|
||||
|
||||
### Setup
|
||||
|
||||
1. [Download the installer](https://nodejs.org/) for the LTS version of Node.js. This is the best way to also [install npm](https://blog.npmjs.org/post/85484771375/how-to-install-npm#_=_).
|
||||
2. Fork this repository.
|
||||
3. Clone your fork.
|
||||
4. Create a new branch from main for your change.
|
||||
5. Navigate into the directory of the package you wish to modify (core, angular, etc.).
|
||||
6. Run `npm install` to install dependencies for this package.
|
||||
7. Follow the steps for the specific package below.
|
||||
|
||||
|
||||
### Core
|
||||
|
||||
#### Modifying Components
|
||||
|
||||
1. Locate the component(s) to modify inside `/core/src/components/`.
|
||||
2. Take a look at the [Stencil Documentation](https://stenciljs.com/docs/introduction/) and other components to understand the implementation of these components.
|
||||
3. Make your changes to the component. If the change is overly complex or out of the ordinary, add comments so we can understand the changes.
|
||||
4. [Preview your changes](#preview-changes) locally.
|
||||
5. [Modify the documentation](#modifying-documentation) if needed.
|
||||
6. [Run lint](#lint-changes) on the directory and make sure there are no errors.
|
||||
7. [Build the project](#building-changes).
|
||||
8. After the build is finished, commit the changes. Please follow the [commit message format](#commit-message-format) for every commit.
|
||||
9. [Submit a Pull Request](#submit-pull-request) of your changes.
|
||||
|
||||
|
||||
#### Preview Changes
|
||||
|
||||
##### Previewing in this repository
|
||||
|
||||
1. Run `npm start` from within the `core` directory.
|
||||
2. A browser should open at `http://localhost:3333/`.
|
||||
3. From here, navigate to one of the component's tests to preview your changes.
|
||||
4. If a test showing your change doesn't exist, [add a new test or update an existing one](#modifying-tests).
|
||||
5. To test in RTL mode, once you are in the desired component's test, add `?rtl=true` at the end of the url; for example: `http://localhost:3333/src/components/alert/test/basic?rtl=true`.
|
||||
|
||||
##### Previewing in an external app
|
||||
|
||||
We can use `npm pack` to test Ionic Framework changes in an external app outside of this repository. Follow the below steps based on your framework.
|
||||
|
||||
###### JavaScript
|
||||
|
||||
Run the following commands to build the core directory and pack the changes:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
npm i
|
||||
npm run build
|
||||
npm pack --pack-destination ~
|
||||
```
|
||||
|
||||
Then, in your Ionic Framework JavaScript app, run the following command to use the built package with the `.tgz` file that was created:
|
||||
|
||||
```bash
|
||||
npm install file:/~/ionic-core-7.0.1.tgz
|
||||
```
|
||||
|
||||
|
||||
###### Angular
|
||||
|
||||
Run the following commands to build the core & angular directories and pack the changes:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
npm i
|
||||
npm run build
|
||||
npm pack --pack-destination ~
|
||||
|
||||
cd ../packages/angular
|
||||
npm i
|
||||
npm run sync
|
||||
npm run build
|
||||
cd dist/
|
||||
npm pack --pack-destination ~
|
||||
```
|
||||
|
||||
Then, in your Ionic Framework Angular app, run the following commands to use the built packages with the `.tgz` files that were created:
|
||||
|
||||
```bash
|
||||
rm -rf .angular/
|
||||
npm install file:/~/ionic-core-7.0.1.tgz
|
||||
npm install file:/~/ionic-angular-7.0.1.tgz
|
||||
```
|
||||
|
||||
|
||||
###### React
|
||||
|
||||
Run the following commands to build the core & react directories and pack the changes:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
npm i
|
||||
npm run build
|
||||
npm pack --pack-destination ~
|
||||
|
||||
cd ../packages/react
|
||||
npm i
|
||||
npm run sync
|
||||
npm run build
|
||||
npm pack --pack-destination ~
|
||||
|
||||
cd ../react-router
|
||||
npm i
|
||||
npm run sync
|
||||
npm run build
|
||||
npm pack --pack-destination ~
|
||||
```
|
||||
|
||||
Then, in your Ionic Framework React app, run the following commands to use the built packages with the `.tgz` files that were created:
|
||||
|
||||
```bash
|
||||
npm install file:/~/ionic-core-7.0.1.tgz
|
||||
npm install file:/~/ionic-react-7.0.1.tgz
|
||||
npm install file:/~/ionic-react-router-7.0.1.tgz
|
||||
```
|
||||
|
||||
|
||||
##### Vue
|
||||
|
||||
Run the following commands to build the core & vue directories and pack the changes:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
npm i
|
||||
npm run build
|
||||
npm pack --pack-destination ~
|
||||
|
||||
cd ../packages/vue
|
||||
npm i
|
||||
npm run sync
|
||||
npm run build
|
||||
npm pack --pack-destination ~
|
||||
|
||||
cd ../vue-router
|
||||
npm i
|
||||
npm run sync
|
||||
npm run build
|
||||
npm pack --pack-destination ~
|
||||
```
|
||||
|
||||
Then, in your Ionic Framework Vue app, run the following commands to use the built packages with the `.tgz` files that were created:
|
||||
|
||||
```bash
|
||||
npm install file:/~/ionic-core-7.0.1.tgz
|
||||
npm install file:/~/ionic-vue-7.0.1.tgz
|
||||
npm install file:/~/ionic-vue-router-7.0.1.tgz
|
||||
```
|
||||
|
||||
|
||||
#### Lint Changes
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If you are using a Windows machine, you will need to configure your local development environment to use the correct line endings.
|
||||
> - Check that the Git setting `core.autocrlf` is set to `false`: run `git config -l | grep autocrlf`. Switch it to false using: `git config --global core.autocrlf false`.
|
||||
> - If you've already cloned the `ionic-docs` repo, the files may already be cached as LF. To undo this, you need to clean the cache files of the repository. Run the following (make sure you stage or commit your changes first): `git rm --cached -r .` then `git reset --hard`.
|
||||
|
||||
|
||||
|
||||
|
||||
1. Run `npm run lint` to lint the TypeScript and Sass.
|
||||
2. If there are lint errors, run `npm run lint.fix` to automatically fix any errors. Repeat step 1 to ensure the errors have been fixed, and manually fix them if not.
|
||||
3. To lint and fix only TypeScript errors, run `npm run lint.ts` and `npm run lint.ts.fix`, respectively.
|
||||
4. To lint and fix only Sass errors, run `npm run lint.sass` and `npm run lint.sass.fix`, respectively.
|
||||
|
||||
|
||||
#### Modifying Documentation
|
||||
|
||||
- Changes to manually written documentation should be made in the `ionic-docs` repo: https://github.com/ionic-team/ionic-docs/tree/main/docs
|
||||
- In your `ionic-docs` PR, please add a link back to the related `ionic-framework` PR.
|
||||
- Changes to auto generated documentation should be made in the `ionic-framework` repo. These can be done in the same PR as your fix or feature.
|
||||
- Run `npm run build` and commit all updates to ensure your changes make it into the generated documentation.
|
||||
- `Usage`: update the component's usage examples in the component's `usage/` directory.
|
||||
- `Properties`, `Events`, or `Methods`: update the component's TypeScript file (`*.tsx`).
|
||||
- `CSS Custom Properties`: update the component's main Sass file (`*.scss`).
|
||||
|
||||
|
||||
#### Modifying Tests
|
||||
|
||||
1. Locate the test to modify inside the `test/` folder in the component's directory.
|
||||
2. If a test exists, modify the test by adding an example to reproduce the problem fixed or feature added.
|
||||
3. If a new test is needed, the easiest way is to copy the `basic/` directory from the component's `test/` directory, rename it, and edit the content in both the `index.html` and `e2e.ts` file (see [Screenshot Tests](#screenshot-tests) for more information on this file).
|
||||
4. The `preview/` directory is used in the documentation as a demo. Only update this test if there is a bug in the test or if the API has a change that hasn't been updated in the test.
|
||||
|
||||
See [Ionic's E2E testing guide](../core/src/utils/test/playwright/docs/README.md) for information regarding the tools you can use to test Ionic.
|
||||
|
||||
##### Screenshot Tests
|
||||
|
||||
1. If the test exists in screenshot, there will be a file named `e2e.ts` in the directory of the test.
|
||||
2. A screenshot test can be added by including this file and adding one or more `test()` calls that include a call to `page.compareScreenshot()`. See [Stencil end-to-end testing](https://stenciljs.com/docs/end-to-end-testing) and existing tests in `core/` for examples.
|
||||
3. **Important:** each `test()` should have only one screenshot (`page.compareScreenshot()`) call **or** it should check the expect at the end of each test. If there is a mismatch it will fail the test which will prevent the rest of the test from running, i.e. if the first screenshot fails the remaining screenshot calls would not be called _unless_ they are in a separate test or all of the expects are called at the end.
|
||||
4. To run screenshot locally, use the following command: `npm run test.screenshot`.
|
||||
- To run screenshot for a specific test, pass the path to the test or a string to search for.
|
||||
- For example, running all `alert` tests: `npm run test.screenshot alert`.
|
||||
- Or, running the basic `alert` tests: `npm run test.screenshot src/components/alert/test/basic/e2e.ts`.
|
||||
|
||||
|
||||
#### Building Changes
|
||||
|
||||
1. Once all changes have been made and the documentation has been updated, run `npm run build` inside of the `core` directory. This will add your changes to any auto-generated files, if necessary.
|
||||
2. Review the changes and, if everything looks correct, [commit](#commit-message-format) the changes.
|
||||
3. Make sure the build has finished before committing. If you made changes to the documentation, properties, methods, or anything else that requires an update to a generate file, this needs to be committed.
|
||||
4. After the changes have been pushed, publish the branch and [create a pull request](#creating-a-pull-request).
|
||||
|
||||
### Angular, React, and Vue
|
||||
|
||||
#### Modifying Files
|
||||
|
||||
1. Locate the files inside the relevant root directory:
|
||||
- Angular: [`/packages/angular/src`](/packages/angular/src)
|
||||
- React: [`/packages/react/src`](/packages/react/src)
|
||||
- Vue: [`/packages/vue/src`](/packages/vue/src)
|
||||
2. Make your changes to the files. If the change is overly complex or out of the ordinary, add comments so we can understand the changes.
|
||||
3. Run lint on the directory and make sure there are no errors.
|
||||
4. Build the project.
|
||||
5. After the build is finished, commit the changes. Please follow the [commit message format](#commit-message-format) for every commit.
|
||||
6. [Submit a Pull Request](#submit-pull-request) of your changes.
|
||||
|
||||
|
||||
|
||||
#### Preview Changes
|
||||
|
||||
##### Previewing in this repository
|
||||
|
||||
Follow the steps in the test directory for each framework:
|
||||
- Angular: [`/packages/angular/test`](/packages/angular/test/README.md)
|
||||
- React: [`/packages/react/test`](/packages/react/test/README.md)
|
||||
- Vue: [`/packages/vue/test`](/packages/vue/test/README.md)
|
||||
|
||||
##### Previewing in an external app
|
||||
|
||||
Follow the steps to [preview changes in core](#preview-changes).
|
||||
|
||||
#### Lint Changes
|
||||
|
||||
1. Run `npm run lint` to lint the TypeScript in the relevant directory:
|
||||
- Angular: [`/packages/angular/src`](/packages/angular/src)
|
||||
- React: [`/packages/react/src`](/packages/react/src)
|
||||
- Vue: [`/packages/vue/src`](/packages/vue/src)
|
||||
2. If there are lint errors, run `npm run lint.fix` to automatically fix any errors. Repeat step 1 to ensure the errors have been fixed, and manually fix them if not.
|
||||
|
||||
#### Modifying Tests
|
||||
|
||||
1. Locate the e2e test to modify inside the relevant test app directory:
|
||||
- Angular: [`/packages/angular/test/base/e2e/src`](/packages/angular/test/base/e2e/src)
|
||||
- React: [`/packages/react/test/base/tests/e2e/specs`](/packages/react/test/base/tests/e2e/specs)
|
||||
- Vue: [`/packages/vue/test/base/tests/e2e/specs`](/packages/vue/test/base/tests/e2e/specs)
|
||||
2. If a test exists, modify the test by adding an example to reproduce the problem fixed or feature added.
|
||||
3. If a new test is needed, copy an existing test, rename it, and edit the content in the test file.
|
||||
4. Run `npm run test` to run your tests.
|
||||
|
||||
#### Building Changes
|
||||
|
||||
1. Once all changes have been made, run `npm run build` inside of the package's root directory. This will add your changes to any auto-generated files, if necessary.
|
||||
2. Review the changes and, if everything looks correct, [commit](#commit-message-format) the changes.
|
||||
3. Make sure the build has finished before committing. If you made changes to the documentation, properties, methods, or anything else that requires an update to a generate file, this needs to be committed.
|
||||
4. After the changes have been pushed, publish the branch and [create a pull request](#creating-a-pull-request).
|
||||
|
||||
### Submit Pull Request
|
||||
|
||||
1. [Create a new pull request](https://github.com/ionic-team/ionic-framework/compare) with the `main` branch as the `base`. You may need to click on `compare across forks` to find your changes.
|
||||
2. See the [Creating a pull request from a fork](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) GitHub help article for more information.
|
||||
3. Please fill out the provided Pull Request template to the best of your ability and include any issues that are related.
|
||||
|
||||
### Review Process for Feature PRs
|
||||
|
||||
The team has an internal design process for new Ionic features, which must be completed before the PR can be reviewed or merged. As a result of the design process, community feature PRs are subject to large changes. In some cases, the team may instead create a separate PR using pieces of the community PR. Either way, you will always receive co-author commit credit when the feature is merged.
|
||||
|
||||
To expedite the process, please ensure that all feature PRs have an associated issue created, with a clear use case for why the feature should be added to Ionic.
|
||||
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
We have very precise rules over how our git commit messages should be formatted. This leads to readable messages that are easy to follow when looking through the project history. We also use the git commit messages to generate our [changelog](https://github.com/ionic-team/ionic-framework/blob/main/CHANGELOG.md). Our format closely resembles Angular's [commit message guidelines](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit).
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
We follow the [Conventional Commits specification](https://www.conventionalcommits.org/). A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The **header** is mandatory and the **scope** of the header is optional.
|
||||
|
||||
### Revert
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
|
||||
|
||||
### Type
|
||||
|
||||
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **docs**: Documentation only changes
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
* **perf**: A code change that improves performance
|
||||
* **test**: Adding missing tests
|
||||
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation
|
||||
|
||||
### Scope
|
||||
|
||||
The scope can be anything specifying place of the commit change. Usually it will refer to a component but it can also refer to a utility. For example `action-sheet`, `button`, `css`, `menu`, `nav`, etc. If you make multiple commits for the same component, please keep the naming of this component consistent. For example, if you make a change to navigation and the first commit is `fix(nav)`, you should continue to use `nav` for any more commits related to navigation. As a general rule, if you're modifying a component use the name of the folder.
|
||||
|
||||
### Subject
|
||||
|
||||
The subject contains succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* do not capitalize first letter
|
||||
* do not place a period `.` at the end
|
||||
* entire length of the commit message must not go over 50 characters
|
||||
* describe what the commit does, not what issue it relates to or fixes
|
||||
* **be brief, yet descriptive** - we should have a good understanding of what the commit does by reading the subject
|
||||
|
||||
### Body
|
||||
|
||||
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
|
||||
The body should include the motivation for the change and contrast this with previous behavior.
|
||||
|
||||
### Footer
|
||||
|
||||
The footer should contain any information about **Breaking Changes** and is also the place to
|
||||
reference GitHub issues that this commit **Closes**.
|
||||
|
||||
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
|
||||
|
||||
### Examples
|
||||
|
||||
Does not appear in the generated changelog:
|
||||
|
||||
```
|
||||
docs(changelog): update steps to update
|
||||
```
|
||||
|
||||
Appears under "Features" header, toast subheader:
|
||||
|
||||
```
|
||||
feat(toast): add 'buttons' property
|
||||
```
|
||||
|
||||
Appears under "Bug Fixes" header, skeleton-text subheader, with a link to issue #28:
|
||||
|
||||
```
|
||||
fix(skeleton-text): use proper color when animated
|
||||
|
||||
closes #28
|
||||
```
|
||||
|
||||
Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
|
||||
|
||||
```
|
||||
perf(css): remove all css utility attributes
|
||||
|
||||
BREAKING CHANGE: The CSS utility attributes have been removed. Use CSS classes instead.
|
||||
```
|
||||
|
||||
Appears under "Breaking Changes" with the breaking change explanation:
|
||||
|
||||
```
|
||||
refactor(animations): update to new animation system
|
||||
|
||||
BREAKING CHANGE:
|
||||
|
||||
Removes the old animation system to use the new Ionic animations.
|
||||
```
|
||||
|
||||
The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
|
||||
|
||||
```
|
||||
revert: feat(skeleton-text): add animated property
|
||||
|
||||
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
|
||||
```
|
||||
|
||||
|
||||
## License
|
||||
|
||||
By contributing your code to the ionic-team/ionic GitHub Repository, you agree to license your contribution under the MIT license.
|
36
docs/README.md
Normal file
36
docs/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
<p align="center">
|
||||
<img alt="Ionic Logo" src="https://github.com/ionic-team/ionic-framework/blob/main/.github/assets/logo.png?raw=true" width="60" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">
|
||||
Ionic Framework Developer Resources
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
This documentation includes guidelines on contributing, coding conventions, best practices, testing steps, and more. It should serve as a collective resource for all documentation related to developing Ionic Framework.
|
||||
</p>
|
||||
|
||||
## What is Ionic Framework?
|
||||
|
||||
Ionic Framework is an open source app development toolkit for building modern, fast, top-quality cross-platform native and Progressive Web Apps from a single codebase with JavaScript and the Web.
|
||||
|
||||
It is based on <a href="https://www.webcomponents.org/introduction">Web Components</a>, which enables significant performance, usability, and feature improvements alongside support for popular web frameworks like <a href="https://angular.io/">Angular</a>, <a href="https://reactjs.com/">React</a>, and <a href="https://vuejs.org/">Vue</a>.
|
||||
|
||||
## Guides
|
||||
|
||||
| Guide | Description |
|
||||
| ----------------------------------------| ---------------------------------------------------------------------------------------- |
|
||||
| [Contributing](./CONTRIBUTING.md) | How to contribute including creating pull requests, commit message guidelines, and more. |
|
||||
| [Component Guide](./component-guide.md) | Guidelines for implementing component states, accessibility, and more. |
|
||||
| [Sass Guidelines](./sass-guidelines.md) | Outlines scenarios where Sass members and comments should be used. |
|
||||
|
||||
## Packages
|
||||
|
||||
| Project | Package | Documentation | Guides |
|
||||
| ---------------- | -------------------------------------------------------------------------- | ---------------------------------| ----------------------------------------------------------------- |
|
||||
| **Core** | [`@ionic/core`](https://www.npmjs.com/package/@ionic/core) | [Readme](core/README.md) | [Testing](core/testing/README.md) |
|
||||
| **Angular** | [`@ionic/angular`](https://www.npmjs.com/package/@ionic/angular) | [Readme](angular/README.md) | [Testing](angular/testing.md) |
|
||||
| **React** | [`@ionic/react`](https://www.npmjs.com/package/@ionic/react) | [Readme](react/README.md) | [Testing](react/testing.md) |
|
||||
| **React Router** | [`@ionic/react-router`](https://www.npmjs.com/package/@ionic/react-router) | [Readme](react-router/README.md) | [Testing](react-router/testing.md) |
|
||||
| **Vue** | [`@ionic/vue`](https://www.npmjs.com/package/@ionic/vue) | [Readme](vue/README.md) | [Testing](vue/testing.md) |
|
||||
| **Vue Router** | [`@ionic/vue-router`](https://www.npmjs.com/package/@ionic/vue-router) | [Readme](vue-router/README.md) | [Testing](vue-router/testing.md) |
|
2
docs/_config.yml
Normal file
2
docs/_config.yml
Normal file
@ -0,0 +1,2 @@
|
||||
name: Ionic Framework
|
||||
include: [ CONTRIBUTING.md ]
|
11
docs/angular/README.md
Normal file
11
docs/angular/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Ionic Angular
|
||||
|
||||
The [@ionic/angular](https://www.npmjs.com/package/@ionic/angular) package builds on top of [@ionic/core](https://www.npmjs.com/package/@ionic/core) components.
|
||||
|
||||
## Contributing
|
||||
|
||||
See our [Contributing Guide](/docs/CONTRIBUTING.md).
|
||||
|
||||
## Testing
|
||||
|
||||
Refer to the [Angular Testing documentation](./testing.md) for testing the Angular package.
|
118
docs/angular/testing.md
Normal file
118
docs/angular/testing.md
Normal file
@ -0,0 +1,118 @@
|
||||
# Angular Testing
|
||||
|
||||
Ionic Framework supports multiple versions of Angular. As a result, we need to verify that Ionic works correctly with each of these Angular versions.
|
||||
|
||||
## Syncing Local Changes
|
||||
|
||||
The Angular test app supports syncing your locally built changes for validation.
|
||||
|
||||
1. Build the `core` and `packages/angular` directories using `npm run build`.
|
||||
2. [Build the Angular test app](#test-app-build-structure).
|
||||
3. Navigate to the built test app directory (e.g. `packages/angular/test/build/ng14`).
|
||||
4. Install dependencies using `npm install`.
|
||||
5. Sync your local changes using `npm run sync`.
|
||||
|
||||
From here you can either build the application or start a local dev server. When re-syncing changes, you will need to [wipe or disable the application cache](#application-cache).
|
||||
|
||||
## Application Cache
|
||||
|
||||
Angular CLI creates a cache of several files on disk by default in the `.angular` directory. This decreases the time taken to build the test application. However, the cache makes it difficult to quickly sync and check local changes of Ionic. As a result, the `.angular` cache is disabled by default in the test app projects.
|
||||
|
||||
See https://angular.io/cli/cache for more information.
|
||||
|
||||
### Disable Cache
|
||||
|
||||
```shell
|
||||
ng cache disable
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> You may need to manually remove the `.angular` directory once after running this command.
|
||||
|
||||
### Enable Cache
|
||||
|
||||
```shell
|
||||
ng cache enable
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> You will need to delete the `.angular` cache and restart the dev server every time you want to sync local changes of Ionic.
|
||||
|
||||
## Test App Build Structure
|
||||
|
||||
> [!NOTE]
|
||||
> Please confirm your current directory as `packages/angular/test` before proceeding with any of the following commands.
|
||||
|
||||
Unlike other test applications, these test apps are broken up into multiple directories. These directories are then combined to create a single application. This allows us to share common application code, tests, etc so that each app is being tested the same way. Below details the different pieces that help create a single test application.
|
||||
|
||||
**apps** - This directory contains partial applications for each version of Angular we want to test. Typically these directories contain new `package.json` files, `angular.json` files, and more. If you have code that is specific to a particular version of Angular, put it in this directory.
|
||||
|
||||
**base** - This directory contains the base application that each test app will use. This is where tests, application logic, and more live. If you have code that needs to be run on every test app, put it in this directory.
|
||||
|
||||
**build** - When the `apps` and `base` directories are merged, the final result is put in this directory. The `build` directory should never be committed to git.
|
||||
|
||||
**build.sh** - This is the script that merges the `apps` and `base` directories and places the built application in the `build` directory.
|
||||
|
||||
Usage:
|
||||
|
||||
```shell
|
||||
# Build a test app using apps/ng14 as a reference
|
||||
./build.sh ng14
|
||||
```
|
||||
|
||||
## How to modify test apps
|
||||
|
||||
To add new tests, components, or pages, modify the `base` project. This ensures that tests are run for every tested version.
|
||||
|
||||
If you want to add a version-specific change, add the change inside of the appropriate projects in `apps`. Be sure to replicate the directory structure. For example, if you are adding a new E2E test file called `test.spec.ts` in `apps/ng14`, make sure you place the file in `apps/ng14/e2e/src/test.spec.ts`.
|
||||
|
||||
### Version-specific tests
|
||||
|
||||
If you need to add E2E tests that are only run on a specific version of the JS Framework, replicate the `VersionTest` component on each partial application. This ensures that tests for framework version X do not get run for framework version Y.
|
||||
|
||||
### Testing Lazy Loaded Ionic Components
|
||||
|
||||
Tests for lazy loaded Ionic UI components should only be added under the `/lazy` route. This ensures the `IonicModule` is added.
|
||||
|
||||
### Testing Standalone Ionic Components
|
||||
|
||||
Tests for standalone Ionic UI components should only be added under the `/standalone` route. This allows for an isolated environment where the lazy loaded `IonicModule` is not initialized. The standalone components use Stencil's custom element bundle instead of the lazy loaded bundle. If `IonicModule` is initialized then the Stencil components will fall back to using the lazy loaded implementation instead of the custom elements bundle implementation.
|
||||
|
||||
## Adding New Test Apps
|
||||
|
||||
As we add support for new versions of Angular, we will also need to update this directory to test against new applications. The following steps can serve as a guide for adding new apps:
|
||||
|
||||
1. Navigate to the built app for the most recent version of Angular that Ionic tests.
|
||||
2. Update the application by following the steps on https://update.angular.io/.
|
||||
3. Make note of any files that changed during the upgrade (`package.json`, `package-lock.json`, `angular.json`, etc).
|
||||
4. Copy the changed files to a new directory in `apps`.
|
||||
5. Add a new entry to the matrix for `test-core-angular` in `./github/workflows/build.yml`. This will allow the new test app to run against all PRs.
|
||||
6. Commit these changes and push.
|
||||
|
||||
Example:
|
||||
|
||||
In this example, we are going to add the Angular 14 test app.
|
||||
|
||||
1. Build the Angular 13 test app using `./build.sh ng13`.
|
||||
2. Navigate to `build/ng13`.
|
||||
3. Perform the upgrade steps on https://update.angular.io/. The "From" field should say "13.0" and the "To" field should say "14.0".
|
||||
|
||||
Note: You may encounter some other peer dependency issues not covered by the Angular Upgrade Guide. These peer dependency issues can be resolved manually by updating the installed version of each dependency.
|
||||
|
||||
4. Observe that the output of the Angular upgrade indicates that the following files were modified:
|
||||
|
||||
`angular.json`
|
||||
`package-lock.json`
|
||||
`package.json`
|
||||
`tsconfig.json`
|
||||
`src/app/form/form.component.ts`
|
||||
`src/app/modal-example/modal-example.component.ts`
|
||||
|
||||
5. Create a directory in `apps` named `ng14`.
|
||||
6. Copy the modified files to the `apps/ng14` directory.
|
||||
7. Open `./github/workflows/build.yml` and find the `test-angular-e2e` job.
|
||||
8. Find the `apps` field under `matrix`.
|
||||
9. Add "ng14" to the `apps` field.
|
||||
10. Open `./github/workflows/stencil-nightly.yml` and find the `test-angular-e2e` job.
|
||||
11. Repeat steps 8 and 9.
|
||||
12. Commit these changes and push.
|
745
docs/component-guide.md
Normal file
745
docs/component-guide.md
Normal file
@ -0,0 +1,745 @@
|
||||
# Ionic Component Implementation Guide
|
||||
|
||||
- [Button States](#button-states)
|
||||
* [Component Structure](#component-structure)
|
||||
* [Disabled](#disabled)
|
||||
* [Focused](#focused)
|
||||
* [Hover](#hover)
|
||||
* [Activated](#activated)
|
||||
* [Ripple Effect](#ripple-effect)
|
||||
* [Example Components](#example-components)
|
||||
* [References](#references)
|
||||
- [Accessibility](#accessibility)
|
||||
* [Checkbox](#checkbox)
|
||||
* [Switch](#switch)
|
||||
* [Accordion](#accordion)
|
||||
- [Rendering Anchor or Button](#rendering-anchor-or-button)
|
||||
* [Example Components](#example-components-1)
|
||||
* [Component Structure](#component-structure-1)
|
||||
- [Converting Scoped to Shadow](#converting-scoped-to-shadow)
|
||||
- [RTL](#rtl)
|
||||
|
||||
## Button States
|
||||
|
||||
Any component that renders a button should have the following states: [`disabled`](#disabled), [`focused`](#focused), [`hover`](#hover), [`activated`](#activated). It should also have a [Ripple Effect](#ripple-effect) component added for Material Design.
|
||||
|
||||
### Component Structure
|
||||
|
||||
#### JavaScript
|
||||
|
||||
A component that renders a native button should use the following structure:
|
||||
|
||||
```jsx
|
||||
<Host>
|
||||
<button class="button-native">
|
||||
<span class="button-inner">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</button>
|
||||
</Host>
|
||||
```
|
||||
|
||||
Any other attributes and classes that are included are irrelevant to the button states, but it's important that this structure is followed and the classes above exist. In some cases they may be named something else that makes more sense, such as in item.
|
||||
|
||||
|
||||
#### CSS
|
||||
|
||||
A mixin called `button-state()` has been added to make it easier to setup the states in each component.
|
||||
|
||||
```scss
|
||||
@mixin button-state() {
|
||||
@include position(0, 0, 0, 0);
|
||||
|
||||
position: absolute;
|
||||
|
||||
content: "";
|
||||
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The following styles should be set for the CSS to work properly. Note that the `button-state()` mixin is included in the `::after` pseudo element of the native button.
|
||||
|
||||
```scss
|
||||
.button-native {
|
||||
/**
|
||||
* All other CSS in this selector is irrelevant to button states
|
||||
* but the following are required styles
|
||||
*/
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button-native::after {
|
||||
@include button-state();
|
||||
}
|
||||
|
||||
.button-inner {
|
||||
/**
|
||||
* All other CSS in this selector is irrelevant to button states
|
||||
* but the following are required styles
|
||||
*/
|
||||
|
||||
position: relative;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Disabled
|
||||
|
||||
The disabled state should be set via prop on all components that render a native button. Setting a disabled state will change the opacity or color of the button and remove click events from firing.
|
||||
|
||||
#### JavaScript
|
||||
|
||||
The `disabled` property should be set on the component:
|
||||
|
||||
```jsx
|
||||
/**
|
||||
* If `true`, the user cannot interact with the button.
|
||||
*/
|
||||
@Prop({ reflectToAttr: true }) disabled = false;
|
||||
```
|
||||
|
||||
Then, the render function should add the [`aria-disabled`](https://www.w3.org/WAI/PF/aria/states_and_properties#aria-disabled) role to the host, a class that is the element tag name followed by `disabled`, and pass the `disabled` attribute to the native button:
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
const { disabled } = this;
|
||||
|
||||
return (
|
||||
<Host
|
||||
aria-disabled={disabled ? 'true' : null}
|
||||
class={{
|
||||
'button-disabled': disabled
|
||||
}}
|
||||
>
|
||||
<button disabled={disabled}>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If the class being added was for `ion-back-button` it would be `back-button-disabled`.
|
||||
|
||||
#### CSS
|
||||
|
||||
The following CSS _at the bare minimum_ should be added for the disabled class, but it should be styled to match the spec:
|
||||
|
||||
```css
|
||||
:host(.button-disabled) {
|
||||
cursor: default;
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
```
|
||||
|
||||
#### User Customization
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
### Focused
|
||||
|
||||
The focused state should be enabled for elements with actions when tabbed to via the keyboard. This will only work inside of an `ion-app`. It usually changes the opacity or background of an element.
|
||||
|
||||
> [!WARNING]
|
||||
> Do not use `:focus` because that will cause the focus to apply even when an element is tapped (because the element is now focused). Instead, we only want the focus state to be shown when it makes sense which is what the `.ion-focusable` utility mentioned below does.
|
||||
|
||||
> [!NOTE]
|
||||
> The [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) pseudo-class mostly does the same thing as our JavaScript-driven utility. However, it does not work well with Shadow DOM components as the element that receives focus is typically inside of the Shadow DOM, but we usually want to set the `:focus-visible` state on the host so we can style other parts of the component. Using other combinations such as `:has(:focus-visible)` does not work because `:has` does not pierce the Shadow DOM (as that would leak implementation details about the Shadow DOM contents). `:focus-within` does work with the Shadow DOM, but that has the same problem as `:focus` that was mentioned before. Unfortunately, a [`:focus-visible-within` pseudo-class does not exist yet](https://github.com/WICG/focus-visible/issues/151).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Make sure the component has the correct [component structure](#component-structure) before continuing.
|
||||
|
||||
#### JavaScript
|
||||
|
||||
The `ion-focusable` class needs to be set on an element that can be focused:
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
return (
|
||||
<Host class="ion-focusable">
|
||||
<slot></slot>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Once that is done, the element will get the `ion-focused` class added when the element is tabbed to.
|
||||
|
||||
#### CSS
|
||||
|
||||
Components should be written to include the following focused variables for styling:
|
||||
|
||||
```css
|
||||
/**
|
||||
* @prop --color-focused: Color of the button when tabbed to with the keyboard
|
||||
* @prop --background-focused: Background of the button when tabbed to with the keyboard
|
||||
* @prop --background-focused-opacity: Opacity of the background when tabbed to with the keyboard
|
||||
*/
|
||||
```
|
||||
|
||||
Style the `ion-focused` class based on the spec for that element:
|
||||
|
||||
```scss
|
||||
:host(.ion-focused) .button-native {
|
||||
color: var(--color-focused);
|
||||
|
||||
&::after {
|
||||
background: var(--background-focused);
|
||||
|
||||
opacity: var(--background-focused-opacity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Order matters! Focused should be **before** the activated and hover states.
|
||||
|
||||
|
||||
#### User Customization
|
||||
|
||||
Setting the focused state on the `::after` pseudo-element allows the user to customize the focused state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on focus, or they can leave out `--background-focused-opacity` and the button will use the default focus opacity to match the spec.
|
||||
|
||||
```css
|
||||
ion-button {
|
||||
--background-focused: red;
|
||||
--background-focused-opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Hover
|
||||
|
||||
The [hover state](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) happens when a user moves their cursor on top of an element without pressing on it. It should not happen on mobile, only on desktop devices that support hover.
|
||||
|
||||
> [!NOTE]
|
||||
> Some Android devices [incorrectly report their inputs](https://issues.chromium.org/issues/40855702) which can result in certain devices receiving hover events when they should not.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Make sure the component has the correct [component structure](#component-structure) before continuing.
|
||||
|
||||
#### CSS
|
||||
|
||||
Components should be written to include the following hover variables for styling:
|
||||
|
||||
```css
|
||||
/**
|
||||
* @prop --color-hover: Color of the button on hover
|
||||
* @prop --background-hover: Background of the button on hover
|
||||
* @prop --background-hover-opacity: Opacity of the background on hover
|
||||
*/
|
||||
```
|
||||
|
||||
Style the `:hover` based on the spec for that element:
|
||||
|
||||
```scss
|
||||
@media (any-hover: hover) {
|
||||
:host(:hover) .button-native {
|
||||
color: var(--color-hover);
|
||||
|
||||
&::after {
|
||||
background: var(--background-hover);
|
||||
|
||||
opacity: var(--background-hover-opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Order matters! Hover should be **before** the activated state.
|
||||
|
||||
|
||||
#### User Customization
|
||||
|
||||
Setting the hover state on the `::after` pseudo-element allows the user to customize the hover state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on hover, or they can leave out `--background-hover-opacity` and the button will use the default hover opacity to match the spec.
|
||||
|
||||
```css
|
||||
ion-button {
|
||||
--background-hover: red;
|
||||
--background-hover-opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Activated
|
||||
|
||||
The activated state should be enabled for elements with actions on "press". It usually changes the opacity or background of an element.
|
||||
|
||||
> [!WARNING]
|
||||
>`:active` should not be used here as it is not received on mobile Safari unless the element has a `touchstart` listener (which we don't necessarily want to have to add to every element). From [Safari Web Content Guide](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/AdjustingtheTextSize/AdjustingtheTextSize.html):
|
||||
>
|
||||
>> On iOS, mouse events are sent so quickly that the down or active state is never received. Therefore, the `:active` pseudo state is triggered only when there is a touch event set on the HTML element
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Make sure the component has the correct [component structure](#component-structure) before continuing.
|
||||
|
||||
#### JavaScript
|
||||
|
||||
The `ion-activatable` class needs to be set on an element that can be activated:
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
return (
|
||||
<Host class="ion-activatable">
|
||||
<slot></slot>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Once that is done, the element will get the `ion-activated` class added on press after a small delay. This delay exists so that the active state does not show up when an activatable element is tapped while scrolling.
|
||||
|
||||
In addition to setting that class, `ion-activatable-instant` can be set in order to have an instant press with no delay:
|
||||
|
||||
```jsx
|
||||
<Host class="ion-activatable ion-activatable-instant">
|
||||
```
|
||||
|
||||
#### CSS
|
||||
|
||||
```css
|
||||
/**
|
||||
* @prop --color-activated: Color of the button when pressed
|
||||
* @prop --background-activated: Background of the button when pressed
|
||||
* @prop --background-activated-opacity: Opacity of the background when pressed
|
||||
*/
|
||||
```
|
||||
|
||||
Style the `ion-activated` class based on the spec for that element:
|
||||
|
||||
```scss
|
||||
:host(.ion-activated) .button-native {
|
||||
color: var(--color-activated);
|
||||
|
||||
&::after {
|
||||
background: var(--background-activated);
|
||||
|
||||
opacity: var(--background-activated-opacity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Order matters! Activated should be **after** the focused & hover states.
|
||||
|
||||
#### User Customization
|
||||
|
||||
Setting the activated state on the `::after` pseudo-element allows the user to customize the activated state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on press, or they can leave out `--background-activated-opacity` and the button will use the default activated opacity to match the spec.
|
||||
|
||||
```css
|
||||
ion-button {
|
||||
--background-activated: red;
|
||||
--background-activated-opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Ripple Effect
|
||||
|
||||
The ripple effect should be added to elements for Material Design. It *requires* the `ion-activatable` class to be set on the parent element to work, and relative positioning on the parent.
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
const mode = getIonMode(this);
|
||||
|
||||
return (
|
||||
<Host
|
||||
class={{
|
||||
'ion-activatable': true,
|
||||
}}
|
||||
>
|
||||
<button>
|
||||
<slot></slot>
|
||||
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
||||
</button>
|
||||
</Host>
|
||||
);
|
||||
```
|
||||
|
||||
The ripple effect can also accept a different `type`. By default it is `"bounded"` which will expand the ripple effect from the click position outwards. To add a ripple effect that always starts in the center of the element and expands in a circle, set the type to `"unbounded"`. An unbounded ripple will exceed the container, so add `overflow: hidden` to the parent to prevent this.
|
||||
|
||||
Make sure to style the ripple effect for that component to accept a color:
|
||||
|
||||
```css
|
||||
ion-ripple-effect {
|
||||
color: var(--ripple-color);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Example Components
|
||||
|
||||
- [ion-button](https://github.com/ionic-team/ionic/tree/main/core/src/components/button)
|
||||
- [ion-back-button](https://github.com/ionic-team/ionic/tree/main/core/src/components/back-button)
|
||||
- [ion-menu-button](https://github.com/ionic-team/ionic/tree/main/core/src/components/menu-button)
|
||||
|
||||
### References
|
||||
|
||||
- [Material Design States](https://material.io/design/interaction/states.html)
|
||||
- [iOS Buttons](https://developer.apple.com/design/human-interface-guidelines/ios/controls/buttons/)
|
||||
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Checkbox
|
||||
|
||||
#### Example Components
|
||||
|
||||
- [ion-checkbox](https://github.com/ionic-team/ionic/tree/main/core/src/components/checkbox)
|
||||
|
||||
#### VoiceOver
|
||||
|
||||
In order for VoiceOver to work properly with a checkbox component there must be a native `input` with `type="checkbox"`, and `aria-checked` and `role="checkbox"` **must** be on the host element. The `aria-hidden` attribute needs to be added if the checkbox is disabled, preventing iOS users from selecting it:
|
||||
|
||||
```tsx
|
||||
render() {
|
||||
const { checked, disabled } = this;
|
||||
|
||||
return (
|
||||
<Host
|
||||
aria-checked={`${checked}`}
|
||||
aria-hidden={disabled ? 'true' : null}
|
||||
role="checkbox"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
/>
|
||||
...
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### NVDA
|
||||
|
||||
It is required to have `aria-checked` on the native input for checked to read properly and `disabled` to prevent tabbing to the input:
|
||||
|
||||
```tsx
|
||||
render() {
|
||||
const { checked, disabled } = this;
|
||||
|
||||
return (
|
||||
<Host
|
||||
aria-checked={`${checked}`}
|
||||
aria-hidden={disabled ? 'true' : null}
|
||||
role="checkbox"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-checked={`${checked}`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
...
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Labels
|
||||
|
||||
Labels should be passed directly to the component in the form of either visible text or an `aria-label`. The visible text can be set inside of a `label` element, and the `aria-label` can be set directly on the interactive element.
|
||||
|
||||
In the following example the `aria-label` can be inherited from the Host using the `inheritAttributes` or `inheritAriaAttributes` utilities. This allows developers to set `aria-label` on the host element since they do not have access to inside the shadow root.
|
||||
|
||||
> [!NOTE]
|
||||
> Use `inheritAttributes` to specify which attributes should be inherited or `inheritAriaAttributes` to inherit all of the possible `aria` attributes.
|
||||
|
||||
```tsx
|
||||
import { Prop } from '@stencil/core';
|
||||
import { inheritAttributes } from '@utils/helpers';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
|
||||
...
|
||||
|
||||
private inheritedAttributes: Attributes = {};
|
||||
|
||||
@Prop() labelText?: string;
|
||||
|
||||
componentWillLoad() {
|
||||
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Host>
|
||||
<label>
|
||||
{this.labelText}
|
||||
<input type="checkbox" {...this.inheritedAttributes} />
|
||||
</label>
|
||||
</Host>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Hidden Input
|
||||
|
||||
A helper function to render a hidden input has been added, it can be added in the `render`:
|
||||
|
||||
```tsx
|
||||
renderHiddenInput(true, el, name, (checked ? value : ''), disabled);
|
||||
```
|
||||
|
||||
> This is required for the checkbox to work with forms.
|
||||
|
||||
#### Known Issues
|
||||
|
||||
When using VoiceOver on macOS, Chrome will announce the following when you are focused on a checkbox:
|
||||
|
||||
```
|
||||
currently on a checkbox inside of a checkbox
|
||||
```
|
||||
|
||||
This is a compromise we have to make in order for it to work with the other screen readers & Safari.
|
||||
|
||||
|
||||
### Switch
|
||||
|
||||
#### Example Components
|
||||
|
||||
- [ion-toggle](https://github.com/ionic-team/ionic/tree/main/core/src/components/toggle)
|
||||
|
||||
#### Voiceover
|
||||
|
||||
In order for VoiceOver to work properly with a switch component there must be a native `input` with `type="checkbox"` and `role="switch"`, and `aria-checked` and `role="switch"` **must** be on the host element. The `aria-hidden` attribute needs to be added if the switch is disabled, preventing iOS users from selecting it:
|
||||
|
||||
```tsx
|
||||
render() {
|
||||
const { checked, disabled } = this;
|
||||
|
||||
return (
|
||||
<Host
|
||||
aria-checked={`${checked}`}
|
||||
aria-hidden={disabled ? 'true' : null}
|
||||
role="switch"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
...
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### NVDA
|
||||
|
||||
It is required to have `aria-checked` on the native input for checked to read properly and `disabled` to prevent tabbing to the input:
|
||||
|
||||
```tsx
|
||||
render() {
|
||||
const { checked, disabled } = this;
|
||||
|
||||
return (
|
||||
<Host
|
||||
aria-checked={`${checked}`}
|
||||
aria-hidden={disabled ? 'true' : null}
|
||||
role="switch"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-checked={`${checked}`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
...
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Labels
|
||||
|
||||
Labels should be passed directly to the component in the form of either visible text or an `aria-label`. The visible text can be set inside of a `label` element, and the `aria-label` can be set directly on the interactive element.
|
||||
|
||||
In the following example the `aria-label` can be inherited from the Host using the `inheritAttributes` or `inheritAriaAttributes` utilities. This allows developers to set `aria-label` on the host element since they do not have access to inside the shadow root.
|
||||
|
||||
> [!NOTE]
|
||||
> Use `inheritAttributes` to specify which attributes should be inherited or `inheritAriaAttributes` to inherit all of the possible `aria` attributes.
|
||||
|
||||
```tsx
|
||||
import { Prop } from '@stencil/core';
|
||||
import { inheritAttributes } from '@utils/helpers';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
|
||||
...
|
||||
|
||||
private inheritedAttributes: Attributes = {};
|
||||
|
||||
@Prop() labelText?: string;
|
||||
|
||||
componentWillLoad() {
|
||||
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Host>
|
||||
<label>
|
||||
{this.labelText}
|
||||
<input type="checkbox" role="switch" {...this.inheritedAttributes} />
|
||||
</label>
|
||||
</Host>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Hidden Input
|
||||
|
||||
A helper function to render a hidden input has been added, it can be added in the `render`:
|
||||
|
||||
```tsx
|
||||
renderHiddenInput(true, el, name, (checked ? value : ''), disabled);
|
||||
```
|
||||
|
||||
> This is required for the switch to work with forms.
|
||||
|
||||
|
||||
#### Known Issues
|
||||
|
||||
When using VoiceOver on macOS or iOS, Chrome will announce the switch as a checked or unchecked `checkbox`:
|
||||
|
||||
```
|
||||
You are currently on a switch. To select or deselect this checkbox, press Control-Option-Space.
|
||||
```
|
||||
|
||||
There is a WebKit bug open for this: https://bugs.webkit.org/show_bug.cgi?id=196354
|
||||
|
||||
### Accordion
|
||||
|
||||
#### Example Components
|
||||
|
||||
- [ion-accordion](https://github.com/ionic-team/ionic/tree/master/core/src/components/accordion)
|
||||
- [ion-accordion-group](https://github.com/ionic-team/ionic/tree/master/core/src/components/accordion-group)
|
||||
|
||||
#### NVDA
|
||||
|
||||
In order to use the arrow keys to navigate the accordions, users must be in "Focus Mode". Typically, NVDA automatically switches between Browse and Focus modes when inside of a form, but not every accordion needs a form.
|
||||
|
||||
You can either wrap your `ion-accordion-group` in a form, or manually toggle Focus Mode using NVDA's keyboard shortcut.
|
||||
|
||||
|
||||
## Rendering Anchor or Button
|
||||
|
||||
Certain components can render an `<a>` or a `<button>` depending on the presence of an `href` attribute.
|
||||
|
||||
### Example Components
|
||||
|
||||
- [ion-button](https://github.com/ionic-team/ionic/tree/main/core/src/components/button)
|
||||
- [ion-card](https://github.com/ionic-team/ionic/tree/main/core/src/components/card)
|
||||
- [ion-fab-button](https://github.com/ionic-team/ionic/tree/main/core/src/components/fab-button)
|
||||
- [ion-item-option](https://github.com/ionic-team/ionic/tree/main/core/src/components/item-option)
|
||||
- [ion-item](https://github.com/ionic-team/ionic/tree/main/core/src/components/item)
|
||||
|
||||
### Component Structure
|
||||
|
||||
#### JavaScript
|
||||
|
||||
In order to implement a component with a dynamic tag type, set the property that it uses to switch between them, we use `href`:
|
||||
|
||||
```jsx
|
||||
/**
|
||||
* Contains a URL or a URL fragment that the hyperlink points to.
|
||||
* If this property is set, an anchor tag will be rendered.
|
||||
*/
|
||||
@Prop() href: string | undefined;
|
||||
```
|
||||
|
||||
Then use that in order to render the tag:
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
const TagType = href === undefined ? 'button' : 'a' as any;
|
||||
|
||||
return (
|
||||
<Host>
|
||||
<TagType>
|
||||
<slot></slot>
|
||||
</TagType>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
If the component can render an `<a>`, `<button>` or a `<div>` add in more properties such as a `button` attribute in order to check if it should render a button.
|
||||
|
||||
## Converting Scoped to Shadow
|
||||
|
||||
### CSS
|
||||
|
||||
There will be some CSS issues when converting to shadow. Below are some of the differences.
|
||||
|
||||
**Targeting host + slotted child**
|
||||
|
||||
```css
|
||||
/* IN SCOPED */
|
||||
:host(.ion-color)::slotted(ion-segment-button)
|
||||
|
||||
/* IN SHADOW*/
|
||||
:host(.ion-color) ::slotted(ion-segment-button)
|
||||
```
|
||||
|
||||
**Targeting host-context + host (with a :not)**
|
||||
|
||||
```css
|
||||
/* IN SCOPED */
|
||||
:host-context(ion-toolbar.ion-color):not(.ion-color) {
|
||||
|
||||
/* IN SHADOW */
|
||||
:host-context(ion-toolbar.ion-color):host(:not(.ion-color)) {
|
||||
```
|
||||
|
||||
**Targeting host-context + host (with a :not) > slotted child**
|
||||
|
||||
```css
|
||||
/* IN SCOPED */
|
||||
:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(ion-segment-button) {
|
||||
|
||||
/* IN SHADOW*/
|
||||
:host-context(ion-toolbar:not(.ion-color)):host(:not(.ion-color)) ::slotted(ion-segment-button) {
|
||||
```
|
||||
|
||||
## RTL
|
||||
|
||||
When you need to support both LTR and RTL modes, try to avoid using values such as `left` and `right`. For certain CSS properties, you can use the appropriate mixin to have this handled for you automatically.
|
||||
|
||||
For example, if you wanted `transform-origin` to be RTL-aware, you would use the `transform-origin` mixin:
|
||||
|
||||
```css
|
||||
@include transform-origin(start, center);
|
||||
```
|
||||
|
||||
This would output `transform-origin: left center` in LTR mode and `transform-origin: right center` in RTL mode.
|
||||
|
||||
These mixins depend on the `:host-context` pseudo-class when used inside of shadow components, which is not supported in WebKit. As a result, these mixins will not work in Safari for macOS and iOS when applied to shadow components.
|
||||
|
||||
To work around this, you should set an RTL class on the host of your component and set your RTL styles by targeting that class:
|
||||
|
||||
```tsx
|
||||
<Host
|
||||
class={{
|
||||
'my-cmp-rtl': document.dir === 'rtl'
|
||||
}}
|
||||
>
|
||||
...
|
||||
</Host>
|
||||
```
|
||||
|
||||
```css
|
||||
:host {
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
:host(.my-cmp-rtl) {
|
||||
transform-origin: right center;
|
||||
}
|
||||
```
|
11
docs/core/README.md
Normal file
11
docs/core/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Ionic Core
|
||||
|
||||
The [@ionic/core](https://www.npmjs.com/package/@ionic/core) package contains the Web Components that make up the reusable UI building blocks of Ionic Framework. These components are designed to be used in traditional frontend view libraries/frameworks (such as React, Angular, or Vue), or on their own through traditional JavaScript in the browser.
|
||||
|
||||
## Contributing
|
||||
|
||||
See our [Contributing Guide](/docs/CONTRIBUTING.md).
|
||||
|
||||
## Testing
|
||||
|
||||
Refer to the [Core Testing documentation](./testing/README.md) for testing the Core package.
|
13
docs/core/testing/README.md
Normal file
13
docs/core/testing/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
# Core Testing
|
||||
|
||||
## Directory
|
||||
|
||||
| Directory | Description |
|
||||
| --------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| [Preview Changes](./preview-changes.md) | Steps on building core and previewing changes |
|
||||
| [Usage Instructions](./usage-instructions.md) | How to run tests and update screenshots |
|
||||
| [Best Practices](./best-practices.md) | Contains information on conventions to follow as well as pitfalls to avoid when writing tests |
|
||||
| [API](./api.md) | Documents the custom functionality that has been built on top of Playwright |
|
||||
|
||||
|
458
docs/core/testing/api.md
Normal file
458
docs/core/testing/api.md
Normal file
@ -0,0 +1,458 @@
|
||||
# Playwright Test Utils
|
||||
|
||||
The testing directory within Ionic's codebase contains utilities that can be used to more easily test Stencil projects with Playwright.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [`test` function](#test-function)
|
||||
- [`page` fixture](#page-fixture)
|
||||
- [Generators](#generators)
|
||||
- [Matchers](#matchers)
|
||||
|
||||
## `test` Function
|
||||
|
||||
The default [`test` function](https://playwright.dev/docs/api/class-test) has been extended to provide two custom options.
|
||||
|
||||
| Fixture | Type | Description |
|
||||
| ------- | ---- | ----------- |
|
||||
| page | [E2EPage](https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/test/playwright/playwright-declarations.ts) | An extension of the base `page` test fixture within Playwright |
|
||||
| skip | [E2ESkip](https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/test/playwright/playwright-declarations.ts) | Used to skip tests based on text direction, mode, or browser |
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Usage</summary>
|
||||
|
||||
**`page`**
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.goto('path/to/file', config);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**`skip.mode` (DEPRECATED)**
|
||||
|
||||
Deprecated: Use a [generator](#generators) instead.
|
||||
|
||||
```typescript
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test('my custom test', ({ page, skip }) => {
|
||||
skip.mode('md', 'This test is iOS-specific.');
|
||||
|
||||
await page.goto('path/to/file');
|
||||
});
|
||||
```
|
||||
|
||||
**`skip.rtl` (DEPRECATED)**
|
||||
|
||||
Deprecated: Use a [generator](#generators) instead.
|
||||
|
||||
```typescript
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test('my custom test', ({ page, skip }) => {
|
||||
skip.rtl('This test does not have RTL-specific behaviors.');
|
||||
|
||||
await page.goto('path/to/file');
|
||||
});
|
||||
```
|
||||
|
||||
**`skip.browser`**
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page, skip }) => {
|
||||
skip.browser('webkit', 'This test does not work in WebKit yet.');
|
||||
|
||||
await page.goto('path/to/file', config);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**`skip.browser` with callback**
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page, skip }) => {
|
||||
skip.browser((browserName: string) => browserName !== 'webkit', 'This tests a WebKit-specific behavior.');
|
||||
|
||||
await page.goto('path/to/file', config);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## `page` fixture
|
||||
|
||||
The [page fixture](https://playwright.dev/docs/test-fixtures) has been extended to provide additional methods:
|
||||
|
||||
| Method | Description |
|
||||
| - | - |
|
||||
| `goto` | The [page.goto](https://playwright.dev/docs/api/class-page#page-goto) method extended to support a config from a [generator](#generators) and to automatically wait for Stencil components to initialize. |
|
||||
| `setContent` | The [page.setContent](https://playwright.dev/docs/api/class-page#page-set-content) method extended to support a config from a [generator](#generators) and to automatically wait for Stencil components to initialize. |
|
||||
| `locator` | The [page.locator](https://playwright.dev/docs/api/class-page#page-locator) method extended to support `spyOnEvent`. |
|
||||
| `setIonViewport` | Resizes the browser window to fit the entire height of `ion-content` on screen. Only needed when taking fullsize screenshots with `ion-content`. |
|
||||
| `waitForChanges` | Waits for Stencil to re-render before proceeeding. This is typically only needed when you update a property on a component. |
|
||||
| `spyOnEvent` | Creates an event spy that can be used to wait for a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) to be emitted. |
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Usage</summary>
|
||||
|
||||
### Using `goto`
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.goto('src/components/test/alert/test/basic', config);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Using `setContent`
|
||||
|
||||
`setContent` should be used when you only need to render a small amount of markup.
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-button>My Button</ion-button>
|
||||
<style>
|
||||
ion-button {
|
||||
--background: green;
|
||||
}
|
||||
</style>
|
||||
`, config);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Using `locator`
|
||||
|
||||
Locators can be used even if the target element is not in the DOM yet.
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.goto('src/components/test/alert/test/basic', config);
|
||||
|
||||
// Alert is not in the DOM yet
|
||||
const alert = page.locator('ion-alert');
|
||||
|
||||
await page.click('#open-alert');
|
||||
|
||||
// Alert is in the DOM
|
||||
await expect(alert).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Using `setIonViewport`
|
||||
|
||||
`setIonViewport` is only needed when a) you are using `ion-content` and b) you need to take a screenshot of the full page (including content that may overflow offscreen).
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, screenshot, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.goto('src/components/test/alert/test/basic', config);
|
||||
|
||||
await page.setIonViewport();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot('alert'));
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Using `waitForChanges`
|
||||
|
||||
`waitForChanges` is only needed when you must wait for Stencil to re-render before proceeding. This is commonly used when manually updating properties on Stencil components.
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.goto('src/components/test/modal/test/basic', config);
|
||||
|
||||
const modal = page.locator('ion-modal');
|
||||
|
||||
await modal.evaluate((el: HTMLIonModalElement) => el.canDismiss = false);
|
||||
|
||||
// Wait for Stencil to re-render with the canDismiss changes
|
||||
await page.waitForChanges();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Using `spyOnEvent`
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, screenshot, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.goto('src/components/test/modal/test/basic', config);
|
||||
|
||||
// Create spy to listen for event
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#present-modal');
|
||||
|
||||
// Wait for the next emission of `ionModalDidPresent`
|
||||
await ionModalDidPresent.next();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Generators
|
||||
|
||||
Ionic generates tests to test different modes (iOS or MD), layouts (LTR or RTL), and themes (default or dark).
|
||||
|
||||
### Customizing the test configs
|
||||
|
||||
The `configs` function accepts an object containing all the configurations you want to test. It then returns an array of each individual configuration combination. This result is iterated over and one or more tests are generated in each iteration.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Usage</summary>
|
||||
|
||||
**Example 1: Default config**
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This will generate the following test configs
|
||||
* iOS, LTR
|
||||
* iOS, RTL
|
||||
* Material Design, LTR
|
||||
* Material Design, RTL
|
||||
*/
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
...
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Example 2: Configuring the mode**
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This will generate the following test configs
|
||||
* iOS, LTR
|
||||
* iOS, RTL
|
||||
*/
|
||||
configs({ mode: ['ios'] }).forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
...
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Example 3: Configuring the direction**
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This will generate the following test configs
|
||||
* Material Design, RTL
|
||||
* iOS, RTL
|
||||
*/
|
||||
configs({ directions: ['rtl'] }).forEach(({ config, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
...
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Using the return value from each configuration
|
||||
|
||||
Each value in the array returns by `configs` contains the following information:
|
||||
|
||||
| Name | Description |
|
||||
| - | - |
|
||||
| `config` | An object containing a single test configuration. This gets passed to `page.goto` or `page.setContent`. |
|
||||
| `screenshot` | A helper function that generates a unique screenshot name based on the test configuration. |
|
||||
| `title` | A helper function that generates a unique test title based on the test configuration. Playwright requires that each test has a unique title since it uses that to generate a test ID. |
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Usage</summary>
|
||||
|
||||
**Example**
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
/**
|
||||
* Use the "title" function to generate
|
||||
* a "my test block" title with the test
|
||||
* config appended to make it unique.
|
||||
* Example: my test block ios/ltr
|
||||
* Using "title" on the describe block
|
||||
* avoids the need to use "title" on each
|
||||
* inner test block.
|
||||
*/
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
|
||||
/**
|
||||
* Pass a single config object to
|
||||
* load the page with the correct mode,
|
||||
* text direction, and theme.
|
||||
*/
|
||||
await page.goto('/src/components/alert/test/basic', config);
|
||||
|
||||
/**
|
||||
* Use the "screenshot" function to generate
|
||||
* a "alert" screenshot title with the test
|
||||
* config appended to make it unique. Playwright
|
||||
* will also append the browser and platform.
|
||||
* Example: alert-ios-ltr-chrome-linux.png
|
||||
*/
|
||||
await expect(page).toHaveScreenshot(screenshot('alert'));
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Matchers
|
||||
|
||||
Playwright comes with [a set of matchers to do test assertions](https://playwright.dev/docs/test-assertions). However, Ionic has additional custom assertions.
|
||||
|
||||
| Assertion | Description |
|
||||
| - | - |
|
||||
| `toHaveReceivedEvent` | Ensures an event has received an event at least once. |
|
||||
| `toHaveReceviedEventDetail` | Ensures an event has been received with a specified [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) payload. |
|
||||
| `toHaveReceivedEventTimes` | Ensures an event has been received a certain number of times. |
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Usage</summary>
|
||||
|
||||
### Using `toHaveReceivedEvent`
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, screenshot, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-input label="Email"></ion-input>
|
||||
`, config);
|
||||
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
const input = page.locator('ion-input');
|
||||
|
||||
await input.type('hi@ionic.io');
|
||||
|
||||
// In this case you can also use await ionChange.next();
|
||||
await expect(ionChange).toHaveReceivedEvent();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Using `toHaveReceivedEventDetail`
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, screenshot, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-input label="Email"></ion-input>
|
||||
`, config);
|
||||
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
const input = page.locator('ion-input');
|
||||
|
||||
await input.type('hi@ionic.io');
|
||||
|
||||
await ionChange.next();
|
||||
await expect(ionChange).toHaveReceivedEventDetail({ value: 'hi@ionic.io' });
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Using `toHaveReceivedEventTimes`
|
||||
|
||||
```typescript
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, screenshot, title }) => {
|
||||
test.describe(title('my test block'), () => {
|
||||
test('my custom test', ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-input label="Email"></ion-input>
|
||||
`, config);
|
||||
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
const input = page.locator('ion-input');
|
||||
|
||||
await input.type('hi@ionic.io');
|
||||
|
||||
await ionChange.next();
|
||||
|
||||
await input.type('goodbye@ionic.io');
|
||||
|
||||
await ionChange.next();
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
266
docs/core/testing/best-practices.md
Normal file
266
docs/core/testing/best-practices.md
Normal file
@ -0,0 +1,266 @@
|
||||
# Best Practices
|
||||
|
||||
This guide details best practices that should be followed when writing E2E tests.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Use the customized `test` function](#practice-test)
|
||||
- [Break up test directories per-feature](#practice-per-feature)
|
||||
- [Follow the file format](#practice-file-format)
|
||||
- [Ensure each component has a `basic` test directory with an `index.html` file](#practice-basic)
|
||||
- [Use `test.describe` blocks to describe groups of tests](#practice-test-describe)
|
||||
- [Place `configs` generator outside the `test.describe` block](#practice-config-describe)
|
||||
- [Test rendering and functionality in separate `test.describe` blocks](#practice-describe-type)
|
||||
- [Break up large or slow-running tests across multiple files](#practice-slow-tests)
|
||||
- [Use standard viewport sizes](#practice-viewport)
|
||||
- [Avoid using screenshots as a way of verifying functionality](#practice-screenshot-functionality)
|
||||
- [Avoid tests that compare computed values](#practice-test-computed)
|
||||
- [Test for positive and negative cases](#practice-positive-negative)
|
||||
- [Start your test with the configuration or layout in place if possible](#practice-test-config)
|
||||
- [Place your test closest to the fix or feature](#practice-test-close)
|
||||
- [Account for different locales when writing tests](#practice-locales)
|
||||
|
||||
<h2 id="practice-test">Use the customized `test` function</h2>
|
||||
|
||||
Do not import the `test` fixture from `@playwright/test`. Instead, use the `test` fixture defined in `@utils/test/playwright`. This is a custom Playwright fixture that extends the built-in `test` fixture and has logic to wait for the Stencil app to load before proceeding with the test. If you do not use this test fixture, your screenshots will likely be blank.
|
||||
|
||||
Since this fixture extends the built in `test` fixture, all of the normal methods found in the Playwright documentation still apply.
|
||||
|
||||
This is the only custom fixture you need. All of the other fixtures such as `expect` can be imported from `@playwright/test`.
|
||||
|
||||
**Note**: `@utils` is an alias defined in `tsconfig.json` that points to `/src/utils`. This lets us avoid doing `../../../../` if we are several folders deep when importing.
|
||||
|
||||
<h2 id="practice-per-feature">Break up test directories per-feature</h2>
|
||||
|
||||
Tests should be broken up per-feature. This makes it easy for team members to quickly find tests for a particular feature. Additionally, the names of test directories should use kebab-case.
|
||||
|
||||
```diff
|
||||
basic/component.e2e.ts
|
||||
feature-a/component.e2e.ts
|
||||
feature-b/component.e2e.ts
|
||||
feature-c/component.e2e.ts
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
[Datetime Tests](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/datetime/test)
|
||||
|
||||
The `first-day-of-week` directory has all the tests for the `firstDayOfWeek` property on `ion-datetime`, and the `color` directory has all the tests for the `color` property usage.
|
||||
|
||||
Some features can be combined together, and so it is acceptable in this instance to test features together in a single directory.
|
||||
|
||||
<h2 id="practice-file-format">Follow the file format</h2>
|
||||
|
||||
E2E test files should follow this format:
|
||||
|
||||
```tsx
|
||||
[component name].e2e.ts
|
||||
```
|
||||
|
||||
It is recommended to have one E2E test file per directory.
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
/basic
|
||||
button.e2e.ts
|
||||
/anchor
|
||||
button.e2e.ts
|
||||
/form
|
||||
button.e2e.ts
|
||||
```
|
||||
|
||||
In the event you need multiple E2E files per directory, add a modifier to the file that makes it unique.
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
|
||||
/basic
|
||||
modal-controller.e2e.ts // E2E tests for ion-modal via modalController
|
||||
modal-inline.e2e.ts // E2E tests for ion-modal via <ion-modal>
|
||||
```
|
||||
|
||||
<h2 id="practice-basic">Ensure each component has a `basic` test directory with an `index.html` file</h2>
|
||||
|
||||
At a minimum, each component with a `tests` directory must also have a `basic` directory with an `index.html` file. This is done so team members can easily paste usage examples to test out when developing or reviewing PRs. The `basic` directory may have E2E tests, but they should be limited to testing the default (or basic) behavior of a component.
|
||||
|
||||
<h2 id="practice-test-describe">Use `test.describe` blocks to describe groups of tests</h2>
|
||||
|
||||
Each E2E test file should have at least 1 `test.describe` block which defines the component and the feature you are testing.
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
// src/components/button/test/basic/button.e2e.ts
|
||||
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title }) => {
|
||||
test.describe(title('button: disabled state'), () => {
|
||||
...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
There should be one or more `test` blocks which have individual tests. The test name describes what the test should do.
|
||||
|
||||
**Example:**
|
||||
|
||||
```jsx
|
||||
// src/components/button/test/basic/button.e2e.ts
|
||||
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title }) => {
|
||||
test.describe(title('button: disabled state'), () => {
|
||||
test('should not have any visual regressions', async ({ page }) => {
|
||||
...
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
<h2 id="practice-config-describe">Place `configs` generator outside the `test.describe` block</h2>
|
||||
|
||||
The `configs()` generator should be done outside of the `test.describe` block. The benefit of this is it lets you use `test.beforeEach` to run a common `page.goto` or `page.setContent` while passing in the correct config. It also lets you use the `title` function just on the `test.describe` block instead of each `test` inside the block.
|
||||
|
||||
❌ Incorrect
|
||||
|
||||
```typescript
|
||||
import { configs test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('button: disabled state', () => {
|
||||
configs().forEach(({ config, title }) => {
|
||||
/**
|
||||
* This will generate a `test.beforeEach` for each test
|
||||
* config, and all of the generated `test.beforeEach` blocks
|
||||
* will be run on each test.
|
||||
*/
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/button/test', config);
|
||||
});
|
||||
|
||||
test(title('should not have any visual regressions'), async ({ page }) => {
|
||||
...
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
✅ Correct
|
||||
|
||||
```typescript
|
||||
import { configs test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('button: disabled state'), () => {
|
||||
/**
|
||||
* This will generate one `test.beforeEach` for
|
||||
* each `test.describe` block rather than for
|
||||
* each `test` inside of the `test.describe` block.
|
||||
*/
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/button/test', config);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test title is still unique because of our use of title()
|
||||
* on the test.describe block.
|
||||
*/
|
||||
test('should not have any visual regressions', async ({ page }) => {
|
||||
...
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
<h2 id="practice-describe-type">Test rendering and functionality in separate `test.describe` blocks</h2>
|
||||
|
||||
Avoid mixing tests that take screenshots with tests that check functionality. These types of tests often have different requirements that can make a single `test.describe` block hard to understand. For example, a screenshot test might check both iOS and MD modes with LTR and RTL text directions, but a functionality test may not need that if the functionality is consistent across modes and directions.
|
||||
|
||||
If using multiple `test.describe` blocks creates a large test file, consider breaking up these tests across multiple files.
|
||||
|
||||
<h2 id="practice-slow-tests">Break up large or slow-running tests across multiple files</h2>
|
||||
|
||||
Tests are distributed across many test runners on continuous integration (CI) to improve performance. However, Playwright [distributes tests](https://playwright.dev/docs/test-parallel) based on test file, not individual test. This means test files that are particularly slow will negatively impact the overall CI performance.
|
||||
|
||||
<h2 id="practice-viewport">Use standard viewport sizes</h2>
|
||||
|
||||
By default, we run tests on mobile viewports only (think iPhone sized viewports). However, there are some components that have different layouts on tablet viewports. Two examples are `ion-split-pane` and the card variant of `ion-modal`.
|
||||
|
||||
For this scenario, developers must write tests that target the tablet viewport. This can be done by using [page.setViewportSize](https://playwright.dev/docs/api/class-page#page-set-viewport-size). The Playwright test utils directory also contains a `Viewports` constant which contains some common viewport presets. Developers should feel free to add new viewports to this as is applicable.
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
import { configs, test, Viewports } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('thing: rendering'), () => {
|
||||
test('it should do a thing on tablet viewports', async ({ page }) => {
|
||||
await page.setViewportSize(Viewports.tablet.portrait);
|
||||
|
||||
...
|
||||
|
||||
// test logic goes here
|
||||
});
|
||||
});
|
||||
});
|
||||
````
|
||||
|
||||
<h2 id="practice-screenshot-functionality">Avoid using screenshots as a way of verifying functionality</h2>
|
||||
|
||||
Screenshots are great for verifying visual differences, but using it to verify functionality can lead to issues. Instead, try testing for the existence of elements in the DOM, events emitted, etc.
|
||||
|
||||
❌ Incorrect
|
||||
|
||||
This test ensures that the `isOpen` property opens a modal when `true`. It does not verify that the inner contents of a modal are rendered as expected as that is outside the scope of this test. As a result, using a screenshot would not be appropriate here.
|
||||
|
||||
```typescript
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('modal: rendering') => {
|
||||
test('it should open a modal', async ({ page }) => {
|
||||
await page.setContent('<ion-modal is-open="true">...</ion-modal>', config);
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot('modal-open'));
|
||||
});
|
||||
});
|
||||
});
|
||||
````
|
||||
|
||||
✅ Correct
|
||||
|
||||
Instead, we can use a `toBeVisible` assertion to verify that `isOpen` does present a modal when `true`.
|
||||
|
||||
```typescript
|
||||
configs().forEach(({ config, title }) => {
|
||||
test.describe(title('modal: rendering') => {
|
||||
test('it should open a modal', async ({ page }) => {
|
||||
await page.setContent('<ion-modal is-open="true">...</ion-modal>', config);
|
||||
|
||||
const modal = page.locator('ion-modal');
|
||||
await expect(modal).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
````
|
||||
|
||||
<h2 id="practice-test-computed">Avoid tests that compare computed values</h2>
|
||||
|
||||
All browsers render web content in slightly different manners. Instead of testing computed values such as exact pixel values, screenshots are a great way to ensure that elements are being rendered in a consistent manner across browsers.
|
||||
|
||||
<h2 id="practice-positive-negative">Test for positive and negative cases</h2>
|
||||
|
||||
It’s important to test that your code works when the API is used as intended, but what happens if someone makes a mistake? While some errors are fine, incorrect usage should not cause catastrophic failures such as data loss. While TypeScript helps catch incorrect usages, not everyone uses TypeScript in Ionic apps. Additionally, developers will sometimes typecast as `any`.
|
||||
|
||||
<h2 id="practice-test-config">Start your test with the configuration or layout in place if possible</h2>
|
||||
|
||||
This allows tests to remain fast on CI as we can focus on the test itself instead of navigating to the state where the test begins. For example, a simple [scrollIntoViewIfNeeded in Playwright](https://playwright.dev/docs/api/class-locator#locator-scroll-into-view-if-needed) can take around 300ms on CI. Since we run a single test for multiple configurations, that 300ms can add up quickly. Consider setting up your test in a way that the element you want to test is already in view when the test starts.
|
||||
|
||||
<h2 id="practice-test-close">Place your test closest to the fix or feature</h2>
|
||||
|
||||
Tests should be placed closest to where the fix or feature was implemented. This means that if a fix was written for `ion-button`, then the test should be placed in `src/components/button/tests`.
|
||||
|
||||
<h2 id="practice-locales">Account for different locales when writing tests</h2>
|
||||
|
||||
Tests ran on CI may not run on the same locale as your local machine. It's always a good idea to apply locale considerations to components that support it, when writing tests (i.e. `ion-datetime` should specify `locale="en-US"`).
|
29
docs/core/testing/preview-changes.md
Normal file
29
docs/core/testing/preview-changes.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Preview Changes
|
||||
|
||||
## Build
|
||||
|
||||
### 1. Clone ionic
|
||||
|
||||
git clone https://github.com/ionic-team/ionic-framework.git
|
||||
cd ionic-framework
|
||||
|
||||
### 2. Run `npm install`
|
||||
|
||||
cd core
|
||||
npm install
|
||||
|
||||
Notice that `@ionic/core` lives in `core`.
|
||||
|
||||
### 3. Run `npm start`
|
||||
|
||||
Make sure you are inside the `core` directory.
|
||||
|
||||
npm start
|
||||
|
||||
With the `start` command, Ionic components will be built with [Stencil](https://stenciljs.com/), changes to source files are watched, a local http server will startup, and [http://localhost:3333/](http://localhost:3333/) will open in a browser.
|
||||
|
||||
### 4. Preview
|
||||
|
||||
Navigate to [http://localhost:3333/src/components/](http://localhost:3333/src/components/). Each component has small e2e apps found in the `test` directory, for example: [http://localhost:3333/src/components/button/test/basic](http://localhost:3333/src/components/button/test/basic)
|
||||
|
||||
As changes are made in an editor to source files, the e2e app will live-reload.
|
214
docs/core/testing/usage-instructions.md
Normal file
214
docs/core/testing/usage-instructions.md
Normal file
@ -0,0 +1,214 @@
|
||||
# Usage Instructions
|
||||
|
||||
E2E tests verify Ionic components in a real browser. This is useful for testing user interaction and catching visual regressions. We use Playwright as it allows us to test in multiple browsers. Tests can be written and run using Playwright's public API.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installing Dependencies](#installing-dependencies)
|
||||
- [Configuring Docker](#configuring-docker)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Managing Screenshots](#managing-screenshots)
|
||||
- [Further Reading](#further-reading)
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
Follow these steps to install Playwright dependencies. These steps must also be run whenever the installed version of Playwright changes to ensure that you are testing with the correct browser binaries.
|
||||
|
||||
1. Install the Playwright dependency in the `core` directory: `npm ci`
|
||||
2. Download the correct browsers: `npx playwright install`
|
||||
|
||||
## Configuring Docker
|
||||
|
||||
Ionic uses [Docker](https://www.docker.com) to provide a way to run tests locally in the same environment that is used on CI. Using Docker is **optional** as all tests can be run locally on your host machine, but there are a few reasons why you might want to use Docker to run tests locally:
|
||||
|
||||
1. You want to run screenshot tests against the same ground truths used on CI to test your work. Screenshots must be tested in a consistent environment otherwise there will be screenshot mismatches. Without Docker, you would first need to generate ground truths in your local environment.
|
||||
2. You want to update ground truths locally. Ground truths can be updated using a GitHub Action on CI, but this can take ~15 minutes.
|
||||
3. You are a community contributor and you do not have access to the GitHub Action to update ground truths.
|
||||
4. You want to debug an issue that only happen on CI. While this is rare, there are sometimes Linux-specific issues that pop up during development.
|
||||
|
||||
The [Running Tests](#running-tests) and [Managing Screenshots](#managing-screenshots) sections show how to perform various tasks in Playwright with Docker. The section below shows how to configure your environment to get set up with Docker.
|
||||
|
||||
### Installing Docker
|
||||
|
||||
Docker can be installed by [following the steps on the Docker website](https://docs.docker.com/get-docker/).
|
||||
|
||||
### Configuring Docker for Headed Tests (Optional)
|
||||
|
||||
Additional software is needed to run headed tests inside of Docker. The Docker-specific test commands such as `npm run test.e2e.docker` are configured to use this additional software, but it is up to the developer to ensure that the software is installed and running.
|
||||
|
||||
Playwright relies on [XServer](https://www.x.org/wiki/XServer/), a windowing system used to draw and move windows on a display, in order to run tests in headed mode. Follow the steps below to install XServer on your computer.
|
||||
|
||||
> [!NOTE]
|
||||
> The following instructions are based off https://www.oddbird.net/2022/11/30/headed-playwright-in-docker/
|
||||
|
||||
#### macOS
|
||||
|
||||
macOS uses [XQuartz](https://www.xquartz.org) to use XServer on macOS.
|
||||
|
||||
1. Install [Homebrew](https://brew.sh) if not already installed. You can run `brew --version` to check if Homebrew is installed.
|
||||
2. Install XQuartz: `brew install --cask xquartz`
|
||||
3. Open XQuartz, go to `Preferences > Security`, and check “Allow connections from network clients”.
|
||||
4. Restart your computer.
|
||||
5. Start XQuartz from the command line: `xhost +localhost`
|
||||
6. Open Docker Desktop and edit settings to give access to `/tmp/.X11-unix` in `Preferences > Resources > File sharing`.
|
||||
7. In the `core` directory run `echo host.docker.internal:0 > docker-display.txt`. This information is used to set the `DISPLAY` environment variable which tells Playwright how to render a headed UI from the Docker container.
|
||||
8. In the `core` directory run `echo /tmp/.X11-unix:/tmp/.X11-unix > docker-display-volume.txt`. This information is used to make XServer available inside of the Docker container.
|
||||
|
||||
#### Windows
|
||||
|
||||
Windows has a native XServer called [WSLg](https://github.com/microsoft/wslg#readme) that is included as part of the [Windows Subsystem for Linux (WSL)](https://apps.microsoft.com/store/detail/9P9TQF7MRM4R?hl=en-us&gl=US). If you are running Docker Desktop on Windows 10 or 11 you likely already have both WSL and WSLg installed. The following steps show how to verify the WSL and WSLg are installed as well as how to configure your environment for headed tests in Docker.
|
||||
|
||||
If either of the below verification checks fail, then developers should [download the latest version of WSL](https://apps.microsoft.com/store/detail/9P9TQF7MRM4R?hl=en-us&gl=US).
|
||||
|
||||
1. To verify WSL is installed, launch "WSL" from the start menu. If "WSL" does not show up in the start menu then you do not have WSL installed.
|
||||
2. With WSL open, verify that WSLg is installed: `ls -a -w 1 /mnt/wslg`. If the command fails with `No such file or directory` then your system is either missing WSL or running an old version.
|
||||
3. In the `core` directory run `echo :0 > docker-display.txt`. This information is used to set the `DISPLAY` environment variable which tells Playwright how to render a headed UI from the Docker container.
|
||||
4. In the `core` directory run `echo /tmp/.X11-unix:/tmp/.X11-unix > docker-display-volume.txt`. This information is used to make XServer available inside of the Docker container.
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Running All Test Files
|
||||
|
||||
All E2E tests can be run using the following command:
|
||||
|
||||
```shell
|
||||
npm run test.e2e
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> This command is a wrapper for `npx playwright test`. All data passed to `npm run test.e2e` can also be passed to `npx playwright test`.
|
||||
|
||||
### Running Specific Test Files
|
||||
|
||||
Specific test files can be run by passing the file paths or a directory that contains multiple test files. See [Managing Screenshots](#managing-screenshots) for generating ground truths before running screenshot tests.
|
||||
|
||||
**Specific Test Files**
|
||||
|
||||
```shell
|
||||
npm run test.e2e src/components/button/test/basic/button.e2e.ts src/components/button/test/a11y/button.e2e.ts
|
||||
```
|
||||
|
||||
**Test Directory with Multiple Files**
|
||||
|
||||
```shell
|
||||
# Will run all the test files in the `test` directory
|
||||
npm run test.e2e src/components/button/test
|
||||
```
|
||||
|
||||
### Running Tests Inside Docker
|
||||
|
||||
While `npm run test.e2e` can be used to run tests in the same environment that you are developing in, `npm run test.e2e.docker` can be used to run tests in a Docker environment provided by the Ionic team. This command supports all the same features as `npm run test.e2e` detailed in the previous section.
|
||||
|
||||
This command builds a Docker image before tests run. It will also re-build the Docker image in the event that a Playwright update was merged into the repo.
|
||||
|
||||
Note that the Playwright report will not automatically open in your web browser when tests are complete because the tests were run in Docker. Run `npm run test.report` outside of Docker to open the most recent test report.
|
||||
|
||||
> [!NOTE]
|
||||
> Additional setup is needed to run Playwright tests with headed mode in Docker. See [Configuring Docker for Headed Tests](#configuring-docker-for-headed-tests-optional) for more information.
|
||||
|
||||
### Headed vs. Headless Tests
|
||||
|
||||
Playwright tests in Ionic are run in headless mode by default. This means that a visual representation of the browser does not appear on your computer while running.
|
||||
|
||||
No additional steps are needed in order to run the tests in headless mode:
|
||||
|
||||
```shell
|
||||
# Will run tests in headless mode
|
||||
npm run test.e2e src/components/chip
|
||||
```
|
||||
|
||||
Playwright supports the `--headed` flag to run in headed mode which causes the visual representation of the browser to appear:
|
||||
|
||||
```shell
|
||||
# Will run tests in headed mode
|
||||
npm run test.e2e src/components/chip -- --headed
|
||||
```
|
||||
|
||||
## Managing Screenshots
|
||||
|
||||
If you are running a test that takes a screenshot, you must first generate the reference screenshot from your reference branch. This is known as generating a "ground truth screenshot". All other screenshots will be compared to this ground truth.
|
||||
|
||||
### Generating or Updating Ground Truths With Docker (Local Development)
|
||||
|
||||
We recommend generating ground truths inside of [Docker](https://www.docker.com). This allows anyone contributing to Ionic Framework to create or update ground truths.
|
||||
|
||||
To create or update ground truths, run the following command:
|
||||
|
||||
```shell
|
||||
npm run test.e2e.docker.update-snapshots
|
||||
```
|
||||
|
||||
Optionally, you can pass a directory to only update the ground truths for that directory & subdirectories. This is useful when working on a specific component.
|
||||
|
||||
```shell
|
||||
npm run test.e2e.docker.update-snapshots src/components/alert/
|
||||
```
|
||||
|
||||
The resulting screenshots should be committed and pushed to your branch.
|
||||
|
||||
### Generating or Updating Ground Truths Without Docker (Local Development)
|
||||
|
||||
While we recommend generating ground truths inside of Docker it is possible to generate ground truths without it. Note that these generated ground truths can only be used for local testing and will not update the ground truths stored in the repo.
|
||||
|
||||
If the reference branch has changed since the last time you generated ground truths you may need to update your local ground truths.
|
||||
|
||||
For most types of work the reference branch is typically `main`. Features are merged into a different branch, so developers should use that as the reference branch. For example, if branch `foo` will be merged into `bar`, then the reference branch is `bar`.
|
||||
|
||||
The examples provided in the [Running Tests](#running-tests) section also apply here, allowing you to update screenshots for a specific test file.
|
||||
|
||||
Note that since you are generating the reference branch ground truth screenshots, you must be on the reference branch locally. Don't forget to pull the latest reference branch changes and then re-build using `npm run build`.
|
||||
|
||||
```shell
|
||||
npm run test.e2e.update-snapshots
|
||||
```
|
||||
|
||||
Optionally, you can pass a directory to only update the ground truths for that directory & subdirectories. This is useful when working on a specific component.
|
||||
|
||||
```shell
|
||||
npm run test.e2e.update-snapshots src/components/alert/
|
||||
```
|
||||
|
||||
From here, you can switch back to your branch and run the tests.
|
||||
|
||||
> [!NOTE]
|
||||
> Locally generated ground truths should not be committed to the repo. The `.gitignore` file prevents this from accidentally happening.
|
||||
|
||||
### Generating or Updating Ground Truths (CI)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Only Ionic Team members can update ground truths on the main repo. Ground truths cannot be updated on forked versions of the repo. Instead, we recommend generating ground truths in Docker.
|
||||
|
||||
When making an intentional visual change, you will need to update the ground truth screenshots or add new ones. It is important that the ground truth and comparison screenshots are taken in the same environment, so do not update the ground truth screenshots locally and commit them to the repo.
|
||||
|
||||
Instead, use the [Update Reference Screenshots GitHub Action](https://github.com/ionic-team/ionic-framework/actions/workflows/update-screenshots.yml).
|
||||
|
||||
1. Click the **Run workflow** dropdown.
|
||||
2. Select your branch.
|
||||
3. Click **Run workflow**.
|
||||
|
||||
This workflow will re-run the screenshot tests. Instead of failing any tests with mismatched screenshots, it will take new ground truth screenshots. These ground truth screenshots will be pushed as a single commit to your branch once the workflow is completed.
|
||||
|
||||
### Verifying Screenshot Differences
|
||||
|
||||
When any of the screenshot tests fail, it means a potential regression was caught. Developers must manually verify the difference in the Playwright test report.
|
||||
|
||||
If the screenshots fail on CI then developers must download the build artifact. On the **Summary** page for a particular workflow, find the **Artifacts** section. Screenshot tests are currently parallelized across several test runners, and the results from each of those runners is included in an artifact with the following naming scheme:
|
||||
|
||||
```
|
||||
test-results-[current shard]-[total shards]
|
||||
|
||||
Example:
|
||||
|
||||
test-results-2-5 --> Test results from job runner 2 out of 5.
|
||||
```
|
||||
|
||||
Download the appropriate artifact and unzip the file.
|
||||
|
||||
In the newly created directory, open the `playwright-report/index.html` in your browser. From here, you will be able to see the tests that failed as well as the expected screenshot, the actual screenshot, and the pixel differences.
|
||||
|
||||
> [!WARNING]
|
||||
> It is recommended to verify the screenshot difference within the Playwright test report first. If you choose to try and reproduce the difference in a browser manually, make sure you are using the **exact** same browser version that Playwright is using.
|
||||
|
||||
## Further Reading
|
||||
|
||||
For more info on how to use Playwright, please see the [Playwright documentation](https://playwright.dev/docs/intro).
|
11
docs/react-router/README.md
Normal file
11
docs/react-router/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Ionic React Router
|
||||
|
||||
The [@ionic/react-router](https://www.npmjs.com/package/@ionic/react-router) package is the routing integration for [@ionic/react](https://www.npmjs.com/package/@ionic/react). It uses the [React Router](https://github.com/remix-run/react-router) library beneath the surface.
|
||||
|
||||
## Contributing
|
||||
|
||||
See our [Contributing Guide](/docs/CONTRIBUTING.md).
|
||||
|
||||
## Testing
|
||||
|
||||
Refer to the [React Router Testing documentation](./testing.md) for testing the React Router package.
|
55
docs/react-router/testing.md
Normal file
55
docs/react-router/testing.md
Normal file
@ -0,0 +1,55 @@
|
||||
# React Router Testing
|
||||
|
||||
Ionic Framework supports multiple versions of React Router. As a result, we need to verify that Ionic works correctly with each of these React Router versions.
|
||||
|
||||
## Syncing Local Changes
|
||||
|
||||
The React test app supports syncing your locally built changes for validation.
|
||||
|
||||
1. Build the `@ionic/core`, `@ionic/react`, and `@ionic/react-router` projects using `npm run build`.
|
||||
2. [Build the React test app](#test-app-build-structure).
|
||||
3. Navigate to the built test app.
|
||||
4. Install dependencies using `npm install`.
|
||||
5. Sync your local changes using `npm run sync`.
|
||||
|
||||
From here you can either build the application or start a local dev server. When re-syncing changes, you will need to wipe the build cache in `node_modules/.cache` and restart the dev server/re-build.
|
||||
|
||||
## Test App Build Structure
|
||||
|
||||
Unlike other test applications, these test apps are broken up into multiple directories. These directories are then combined to create a single application. This allows us to share common application code, tests, etc so that each app is being tested the same way. Below details the different pieces that help create a single test application.
|
||||
|
||||
**apps** - This directory contains partial applications for each version of React we want to test. Typically these directories contain new `package.json` files, `cypress.config.ts` files, and more. If you have code that is specific to a particular version of React, put it in this directory.
|
||||
|
||||
**base** - This directory contains the base application that each test app will use. This is where tests, application logic, and more live. If you have code that needs to be run on every test app, put it in this directory.
|
||||
|
||||
**build** - When the `apps` and `base` directories are merged, the final result is put in this directory. The `build` directory should never be committed to git.
|
||||
|
||||
**build.sh** - This is the script that merges the `apps` and `base` directories and places the built application in the `build` directory.
|
||||
|
||||
Usage:
|
||||
|
||||
```shell
|
||||
# Build a test app using apps/reactrouter5 as a reference
|
||||
./build.sh reactrouter5
|
||||
```
|
||||
|
||||
## How to modify test apps
|
||||
|
||||
To add new tests, components, or pages, modify the `base` project. This ensures that tests are run for every tested version.
|
||||
|
||||
If you want to add a version-specific change, add the change inside of the appropriate projects in `apps`. Be sure to replicate the directory structure. For example, if you are adding a new E2E test file called `test.e2e.ts` in `apps/reactrouter5`, make sure you place the file in `apps/react17/tests/e2e/test.e2e.ts`.
|
||||
|
||||
### Version-specific tests
|
||||
|
||||
If you need to add E2E tests that are only run on a specific version of the JS Framework, replicate the `VersionTest` component on each partial application. This ensures that tests for framework version X do not get run for framework version Y.
|
||||
|
||||
## Adding New Test Apps
|
||||
|
||||
As we add support for new versions of React Router, we will also need to update this directory to test against new applications. The following steps can serve as a guide for adding new apps:
|
||||
|
||||
1. Navigate to the built app for the most recent version of React Router that Ionic tests.
|
||||
2. Update the application to the latest version of React Router.
|
||||
3. Make note of any files that changed during the upgrade (`package.json`, `package-lock.json`, etc).
|
||||
4. Copy the changed files to a new directory in `apps`.
|
||||
5. Add a new entry to the matrix for `test-react-router-e2e` in `./github/workflows/build.yml`. This will allow the new test app to run against all PRs.
|
||||
6. Commit these changes and push.
|
11
docs/react/README.md
Normal file
11
docs/react/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Ionic React
|
||||
|
||||
The [@ionic/react](https://www.npmjs.com/package/@ionic/react) package builds on top of [@ionic/core](https://www.npmjs.com/package/@ionic/core) components.
|
||||
|
||||
## Contributing
|
||||
|
||||
See our [Contributing Guide](/docs/CONTRIBUTING.md).
|
||||
|
||||
## Testing
|
||||
|
||||
Refer to the [React Testing documentation](./testing.md) for testing the React package.
|
55
docs/react/testing.md
Normal file
55
docs/react/testing.md
Normal file
@ -0,0 +1,55 @@
|
||||
# React Testing
|
||||
|
||||
Ionic Framework supports multiple versions of React. As a result, we need to verify that Ionic works correctly with each of these React versions.
|
||||
|
||||
## Syncing Local Changes
|
||||
|
||||
The React test app supports syncing your locally built changes for validation.
|
||||
|
||||
1. Build the `core`, `packages/react`, and `packages/react-router` directories using `npm run build`.
|
||||
2. [Build the React test app](#test-app-build-structure).
|
||||
3. Navigate to the built test app.
|
||||
4. Install dependencies using `npm install`.
|
||||
5. Sync your local changes using `npm run sync`.
|
||||
|
||||
From here you can either build the application or start a local dev server. When re-syncing changes, you will need to wipe the build cache in `node_modules/.cache` and restart the dev server/re-build.
|
||||
|
||||
## Test App Build Structure
|
||||
|
||||
Unlike other test applications, these test apps are broken up into multiple directories. These directories are then combined to create a single application. This allows us to share common application code, tests, etc so that each app is being tested the same way. Below details the different pieces that help create a single test application.
|
||||
|
||||
**apps** - This directory contains partial applications for each version of React we want to test. Typically these directories contain new `package.json` files, `cypress.config.ts` files, and more. If you have code that is specific to a particular version of React, put it in this directory.
|
||||
|
||||
**base** - This directory contains the base application that each test app will use. This is where tests, application logic, and more live. If you have code that needs to be run on every test app, put it in this directory.
|
||||
|
||||
**build** - When the `apps` and `base` directories are merged, the final result is put in this directory. The `build` directory should never be committed to git.
|
||||
|
||||
**build.sh** - This is the script that merges the `apps` and `base` directories and places the built application in the `build` directory.
|
||||
|
||||
Usage:
|
||||
|
||||
```shell
|
||||
# Build a test app using apps/react17 as a reference
|
||||
./build.sh react17
|
||||
```
|
||||
|
||||
## How to modify test apps
|
||||
|
||||
To add new tests, components, or pages, modify the `base` project. This ensures that tests are run for every tested version.
|
||||
|
||||
If you want to add a version-specific change, add the change inside of the appropriate projects in `apps`. Be sure to replicate the directory structure. For example, if you are adding a new E2E test file called `test.e2e.ts` in `apps/react17`, make sure you place the file in `apps/react17/tests/e2e/test.e2e.ts`.
|
||||
|
||||
### Version-specific tests
|
||||
|
||||
If you need to add E2E tests that are only run on a specific version of the JS Framework, replicate the `VersionTest` component on each partial application. This ensures that tests for framework version X do not get run for framework version Y.
|
||||
|
||||
## Adding New Test Apps
|
||||
|
||||
As we add support for new versions of React, we will also need to update this directory to test against new applications. The following steps can serve as a guide for adding new apps:
|
||||
|
||||
1. Navigate to the built app for the most recent version of React that Ionic tests.
|
||||
2. Update the application to the latest version of React.
|
||||
3. Make note of any files that changed during the upgrade (`package.json`, `package-lock.json`, etc).
|
||||
4. Copy the changed files to a new directory in `apps`.
|
||||
5. Add a new entry to the matrix for `test-react-e2e` in `./github/workflows/build.yml`. This will allow the new test app to run against all PRs.
|
||||
6. Commit these changes and push.
|
685
docs/sass-guidelines.md
Normal file
685
docs/sass-guidelines.md
Normal file
@ -0,0 +1,685 @@
|
||||
# Sass Guidelines
|
||||
|
||||
- [Definitions](#definitions)
|
||||
- [Scope](#scope)
|
||||
- [Historical Usage](#historical-usage)
|
||||
- [Current Usage](#current-usage)
|
||||
- [Recommended Usage](#recommended-usage)
|
||||
* [Comments](#comments)
|
||||
* [Variables](#variables)
|
||||
+ [✅ Global](#-global)
|
||||
+ [✅ Theming](#-theming)
|
||||
+ [✅ Reusable values](#-reusable-values)
|
||||
+ [✅ Media queries](#-media-queries)
|
||||
+ [✅ Dynamic calculations](#-dynamic-calculations)
|
||||
+ [🚫 Consistency](#-consistency)
|
||||
+ [🚫 Text Alignment](#-text-alignment)
|
||||
+ [🚫 Structural Changes](#-structural-changes)
|
||||
+ [🚫 Font Properties](#-font-properties)
|
||||
|
||||
## Definitions
|
||||
|
||||
**Sass:** An extension to CSS that reduces the repetition in CSS and allows developers to use shared functions, mixins and variables. [^1]
|
||||
|
||||
**Members:** Refers to variables, functions and mixins in Sass.
|
||||
|
||||
## Scope
|
||||
|
||||
Sass provides members that make it easier to reuse code throughout the Ionic Framework repository. Variables hold values that can be used by other stylesheets. Mixins define reusable blocks of styles that can be included in other selectors. Functions allow the manipulation of values and can perform calculations.
|
||||
|
||||
The purpose of this document is to identify the scenarios in which Sass variables should be used.
|
||||
|
||||
## Historical Usage
|
||||
|
||||
In Ionic Framework v1 through v3, the project was built with Sass variables that developers could change at runtime. While the default values were provided by Ionic Framework, anyone developing with it could override these values to rebuild the Ionic Framework CSS with their own values. [^2]
|
||||
|
||||
Due to this, Ionic Framework documented the Sass variables as part of the public API using `@prop` comments as early as [v2.0.0](https://github.com/ionic-team/ionic-framework/blob/v2.0.0/src/components/alert/alert.ios.scss):
|
||||
|
||||
```scss
|
||||
// alert.ios.scss
|
||||
|
||||
/// @prop - Max width of the alert
|
||||
$alert-ios-max-width: 270px !default;
|
||||
|
||||
/// @prop - Border radius of the alert
|
||||
$alert-ios-border-radius: 13px !default;
|
||||
```
|
||||
|
||||
If a Sass variable was deprecated or hidden from the public API, the `@prop` comment would be removed, or it would never be added, as seen in [v3.9.2](https://github.com/ionic-team/ionic-framework/blob/v3.9.2/src/components/alert/alert.ios.scss#L18-L19):
|
||||
|
||||
```scss
|
||||
// alert.ios.scss
|
||||
|
||||
// deprecated
|
||||
$alert-ios-head-padding: null !default;
|
||||
```
|
||||
|
||||
To ensure proper documentation of variables for customizing Ionic Framework, Sass variables were added for components even if they were not used multiple times within the same component or elsewhere:
|
||||
|
||||
```scss
|
||||
// alert.ios.scss
|
||||
|
||||
/// @prop - Text color of the label for the checked radio alert
|
||||
$alert-ios-radio-label-text-color-checked: $alert-ios-button-text-color !default;
|
||||
|
||||
.alert-ios [aria-checked=true] .alert-radio-label {
|
||||
color: $alert-ios-radio-label-text-color-checked;
|
||||
}
|
||||
```
|
||||
|
||||
## Current Usage
|
||||
|
||||
The abundance of Sass variables currently in Ionic Framework is a result of their historical usage, being used to rebuild the CSS and customize Ionic Framework components.
|
||||
|
||||
The comments for Sass variables are also still visible today in [v7.7.0](https://github.com/ionic-team/ionic-framework/blob/v7.7.0/core/src/components/alert/alert.ios.vars.scss), even though they are no longer used by any documentation generators:
|
||||
|
||||
```scss
|
||||
// alert.ios.vars.scss
|
||||
|
||||
/// @prop - Max width of the alert
|
||||
$alert-ios-max-width: dynamic-font-clamp(1, 270px, 1.2) !default;
|
||||
|
||||
/// @prop - Border radius of the alert
|
||||
$alert-ios-border-radius: 13px !default;
|
||||
```
|
||||
|
||||
These comments aren't necessary when the naming describes its use thoroughly. The comments for the variables above do not need to be there, as it is fairly obvious what they are used for.
|
||||
|
||||
However, the comment for the following variable might be helpful in explaining where it is used because on first glance it reads like it could be used for a sub title inside of a title:
|
||||
|
||||
```scss
|
||||
// action-sheet.ios.vars.scss
|
||||
|
||||
/// @prop - Font weight of the action sheet title when it has a sub title
|
||||
$action-sheet-ios-title-with-sub-title-font-weight: 600 !default;
|
||||
```
|
||||
|
||||
It could be argued though that the comment doesn't really help, as seeing the variable in use will explain its purpose the best. Additionally, this is an example of a variable that isn't necessary, given it is only used in one place, which is why it is so specific in the first place.
|
||||
|
||||
## Recommended Usage
|
||||
|
||||
There are two things that need to be outlined here: when we should use comments and when we should use variables. The sections below detail the recommended usage for each of these.
|
||||
|
||||
### Comments
|
||||
|
||||
We should update the comments for Sass variables in one of the following ways:
|
||||
|
||||
1. If we don't intend to ever publicly document the Sass variables again, we should update the comments to remove the syntax that was added for documentation generation:
|
||||
```diff
|
||||
// alert.ios.vars.scss
|
||||
|
||||
-/// @prop - Border radius of the alert
|
||||
+// Border radius of the alert
|
||||
$alert-ios-border-radius: 13px !default;
|
||||
```
|
||||
|
||||
2. If we don't find the comments to be helpful, and want to stick with keeping the variable names specific, we should remove the comments entirely:
|
||||
```diff
|
||||
// alert.ios.vars.scss
|
||||
|
||||
-/// @prop - Border radius of the alert
|
||||
$alert-ios-border-radius: 13px !default;
|
||||
```
|
||||
|
||||
3. If we find the comments to be helpful for certain variables or situations, like when there are math calculations involved, we should keep only the comments that are necessary to explain what is going on:
|
||||
```diff
|
||||
-/// @prop - Height of the alert button
|
||||
/**
|
||||
* We want the height of the button to
|
||||
* scale with the text so the next never runs
|
||||
* into the edge of the button. We change the height
|
||||
* instead of adding padding because we would need to offset
|
||||
* the height the padding and the border. Since the border uses
|
||||
* a hairline (<1px) width, this will cause subpixel rendering
|
||||
* differences across browsers.
|
||||
*/
|
||||
$alert-ios-button-height: dynamic-font-min(1, 44px) !default;
|
||||
```
|
||||
|
||||
### Variables
|
||||
|
||||
The table below outlines the recommended approach for when to use Sass variables. Each scenario links to a section that explains it in more detail.
|
||||
|
||||
| | Scenario |
|
||||
| ---| ---------------------------------------------------------------|
|
||||
| ✅ | [Global](#white_check_mark-global) |
|
||||
| ✅ | [Theming](#white_check_mark-theming) |
|
||||
| ✅ | [Reusable values](#white_check_mark-reusable-values) |
|
||||
| ✅ | [Media queries](#white_check_mark-media-queries) |
|
||||
| ✅ | [Dynamic calculations](#white_check_mark-dynamic-calculations) |
|
||||
| 🚫 | [Consistency](#no_entry_sign-consistency) |
|
||||
| 🚫 | [Text Alignment](#no_entry_sign-text-alignment) |
|
||||
| 🚫 | [Structural Changes](#no_entry_sign-structural-changes) |
|
||||
| 🚫 | [Font Properties](#no_entry_sign-font-properties) |
|
||||
|
||||
#### ✅ Global
|
||||
|
||||
Global variables that are used in multiple places include `font-family`, `z-index`, and `opacity`. These should continue to be set in variables as they affect multiple components that use them.
|
||||
|
||||
Example of global variables:
|
||||
|
||||
```scss
|
||||
// ionic.globals.scss
|
||||
|
||||
$font-family-base: var(--ion-font-family, inherit) !default;
|
||||
|
||||
$hairlines-width: .55px !default;
|
||||
|
||||
$placeholder-opacity: 0.6 !default;
|
||||
```
|
||||
|
||||
#### ✅ Theming
|
||||
|
||||
Storing colors and other design-related values makes it easy to update an entire theme by modifying a few variables.
|
||||
|
||||
Example of theme variables:
|
||||
|
||||
```scss
|
||||
// ionic.theme.default.scss
|
||||
|
||||
$background-color-value: #fff !default;
|
||||
$background-color-rgb-value: 255, 255, 255 !default;
|
||||
|
||||
$text-color-value: #000 !default;
|
||||
$text-color-rgb-value: 0, 0, 0 !default;
|
||||
|
||||
$background-color: var(--ion-background-color, $background-color-value) !default;
|
||||
$background-color-rgb: var(--ion-background-color-rgb, $background-color-rgb-value) !default;
|
||||
$text-color: var(--ion-text-color, $text-color-value) !default;
|
||||
$text-color-rgb: var(--ion-text-color-rgb, $text-color-rgb-value) !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// ionic.theme.default.ios.scss
|
||||
|
||||
$backdrop-ios-color: var(--ion-backdrop-color, #000) !default;
|
||||
$overlay-ios-background-color: var(--ion-overlay-background-color, var(--ion-color-step-100, #f9f9f9)) !default;
|
||||
```
|
||||
|
||||
#### ✅ Reusable values
|
||||
|
||||
Use variables for values that are repeated throughout stylesheets, such as spacing, `border-radius`, `font-size`, or any other value used in multiple places. A value should only be considered reusable if it is used more than once and related among the elements it is being applied to in some way. For instance, a value is not considered related if it changes a common property, such as border style. While many components use `border-style: solid`, it does not need to be stored unless these components will require updates with design changes. Currently, the border styles have consistently been set to `solid`, with the exception of `none` for a CSS reset.
|
||||
|
||||
Example of reusable values:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Do ✅</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// alert.ios.vars.scss
|
||||
|
||||
/// @prop - Padding end of the alert head
|
||||
$alert-ios-head-padding-end: 16px !default;
|
||||
|
||||
/// @prop - Padding start of the alert head
|
||||
$alert-ios-head-padding-start: $alert-ios-head-padding-end !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// alert.ios.scss
|
||||
|
||||
.alert-head {
|
||||
padding-top: 12px;
|
||||
padding-inline-end: $alert-ios-head-padding-end;
|
||||
padding-bottom: 7px;
|
||||
padding-inline-start: $alert-ios-head-padding-start;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<th>Don't :x:</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// alert.ios.vars.scss
|
||||
|
||||
/// @prop - Padding top of the alert head
|
||||
$alert-ios-head-padding-top: 12px !default;
|
||||
|
||||
/// @prop - Padding bottom of the alert head
|
||||
$alert-ios-head-padding-bottom: 7px !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// alert.ios.scss
|
||||
|
||||
.alert-head {
|
||||
padding-top: $alert-ios-head-padding-top;
|
||||
padding-bottom: $alert-ios-head-padding-bottom;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
If a value is shared among multiple components, it should be made into a [global variable](#white_check_mark-global) instead of importing the variable from a specific component. For example, variables that are shared between list components (item, item divider, list header) should be defined in a global theme file:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Do ✅</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// ionic.theme.default.md.scss
|
||||
|
||||
$global-md-item-padding-end: 16px;
|
||||
$global-md-item-padding-start: $global-md-item-padding-end;
|
||||
```
|
||||
|
||||
```scss
|
||||
// item.md.vars.scss
|
||||
|
||||
@import "../../themes/ionic.globals.md";
|
||||
|
||||
/// @prop - Padding end for the item content
|
||||
$item-md-padding-end: $global-md-item-padding-end !default;
|
||||
|
||||
/// @prop - Padding start for the item content
|
||||
$item-md-padding-start: $global-md-item-padding-start !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// item-divider.md.vars.scss
|
||||
|
||||
@import "../../themes/ionic.globals.md";
|
||||
|
||||
/// @prop - Padding start for the divider
|
||||
$item-divider-md-padding-start: $global-md-item-padding-start !default;
|
||||
|
||||
/// @prop - Padding end for the divider
|
||||
$item-divider-md-padding-end: $global-md-item-padding-end !default;
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<th>Don't :x:</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// item.md.vars.scss
|
||||
|
||||
@import "../../themes/ionic.globals.md";
|
||||
|
||||
/// @prop - Padding end for the item content
|
||||
$item-md-padding-end: 16px !default;
|
||||
|
||||
/// @prop - Padding start for the item content
|
||||
$item-md-padding-start: 16px !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// item-divider.md.vars.scss
|
||||
|
||||
@import "../../themes/ionic.globals.md";
|
||||
@import "../item/item.md.vars";
|
||||
|
||||
/// @prop - Padding start for the divider
|
||||
$item-divider-md-padding-start: $item-md-padding-start !default;
|
||||
|
||||
/// @prop - Padding end for the divider
|
||||
$item-divider-md-padding-end: $item-md-padding-end !default;
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
> [!TIP]
|
||||
> The names of the global variables are just an example. We do not currently have a naming convention for global variables.
|
||||
|
||||
#### ✅ Media queries
|
||||
|
||||
Define breakpoints for responsive design to allow easy adjustments as needed.
|
||||
|
||||
Example of breakpoints used by media queries:
|
||||
|
||||
```scss
|
||||
// ionic.globals.scss
|
||||
|
||||
// The minimum dimensions at which your layout will change,
|
||||
// adapting to different screen sizes, for use in media queries
|
||||
$screen-breakpoints: (
|
||||
xs: 0,
|
||||
sm: 576px,
|
||||
md: 768px,
|
||||
lg: 992px,
|
||||
xl: 1200px
|
||||
) !default;
|
||||
```
|
||||
|
||||
#### ✅ Dynamic calculations
|
||||
|
||||
Variables can be useful for dynamic calculations, such as storing a base font size in a variable and then using it in calculations for other font sizes or spacing values. Variables should not be used for storing a function call, even if the function itself has dynamic calculations.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Do ✅</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// chip.vars.scss
|
||||
|
||||
/// @prop - Unitless font size of the chip before scaling
|
||||
$chip-base-font-size: 14;
|
||||
|
||||
/// @prop - Font size of the chip in rem before scaling
|
||||
$chip-base-font-size-rem: #{math.div($chip-base-font-size, 16)}rem;
|
||||
|
||||
/// @prop - Size of an icon within a chip (in em to scale as the font size of the chip scales)
|
||||
$chip-icon-size: math.div(20em, $chip-base-font-size);
|
||||
|
||||
/// @prop - Size of an avatar within a chip (in em to scale as the font size of the chip scales)
|
||||
$chip-avatar-size: math.div(24em, $chip-base-font-size);
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<th>Don't :x:</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// alert.vars.scss
|
||||
|
||||
/// @prop - Font size of the alert button
|
||||
$alert-button-font-size: dynamic-font(14px) !default;
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### 🚫 Consistency
|
||||
|
||||
While we usually aim for consistency across different modes, this isn't always necessary when dealing with Sass variables. If certain styles are present in one mode but absent in another, there's no need to include a Sass variable for the mode lacking those styles.
|
||||
|
||||
For example, the color of the label changes when focused in `md` mode. However, in `ios`, the label does not receive different styling when focused, therefore it does not require the same styles or a Sass variable defined:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Do ✅</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// label.md.vars.scss
|
||||
|
||||
/// @prop - Text color of the stacked/floating label when it is focused
|
||||
$label-md-text-color-focused: ion-color(primary, base) !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// label.md.scss
|
||||
|
||||
:host-context(.ion-focused).label-stacked:not(.ion-color),
|
||||
:host-context(.ion-focused).label-floating:not(.ion-color),
|
||||
:host-context(.item-has-focus).label-stacked:not(.ion-color),
|
||||
:host-context(.item-has-focus).label-floating:not(.ion-color) {
|
||||
color: $label-md-text-color-focused;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<th>Don't :x:</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// label.ios.vars.scss
|
||||
|
||||
/// @prop - Text color of the stacked/floating label when it is focused
|
||||
$label-ios-text-color-focused: null !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// label.ios.scss
|
||||
|
||||
:host-context(.ion-focused).label-stacked:not(.ion-color),
|
||||
:host-context(.ion-focused).label-floating:not(.ion-color),
|
||||
:host-context(.item-has-focus).label-stacked:not(.ion-color),
|
||||
:host-context(.item-has-focus).label-floating:not(.ion-color) {
|
||||
color: $label-ios-text-color-focused;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### 🚫 Text Alignment
|
||||
|
||||
A text alignment property should not be stored in a Sass variable, even if it is used in multiple places. This is because the alignment may be tied to a specific design, and the design may change, causing them to become disconnected.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Do ✅</th>
|
||||
<th>Don't :x:</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// action-sheet.ios.scss
|
||||
|
||||
:host {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-sheet-title {
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// action-sheet.ios.vars.scss
|
||||
|
||||
/// @prop - Text align of the action sheet
|
||||
$action-sheet-ios-text-align: center !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// action-sheet.ios.scss
|
||||
|
||||
:host {
|
||||
text-align: $action-sheet-ios-text-align;
|
||||
}
|
||||
|
||||
.action-sheet-title {
|
||||
text-align: $action-sheet-ios-text-align;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
#### 🚫 Structural Changes
|
||||
|
||||
Variables should not be used when they are structural changes of an element. This includes `display` properties, `flex` properties, `grid` properties, and more.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Do ✅</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// alert.ios.scss
|
||||
|
||||
.alert-button-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.alert-button {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<th>Don't :x:</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
|
||||
```scss
|
||||
// alert.ios.vars.scss
|
||||
|
||||
/// @prop - Flex wrap of the alert button group
|
||||
$alert-ios-button-group-flex-wrap: wrap !default;
|
||||
|
||||
/// @prop - Flex of the alert button
|
||||
$alert-ios-button-flex: 1 1 auto !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// alert.ios.scss
|
||||
|
||||
.alert-button-group {
|
||||
flex-wrap: $alert-ios-button-group-flex-wrap;
|
||||
}
|
||||
|
||||
.alert-button {
|
||||
flex: $alert-ios-button-flex;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### 🚫 Font Properties
|
||||
|
||||
We shouldn't use variables for changing things such as `font-size` or `font-weight`, as these are not changed based on a theme and do not need to be updated globally. When updating the `font-size` and `font-weight` for these elements, it will always be done on a case-by-case basis:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Do ✅</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// action-sheet.ios.scss
|
||||
|
||||
.action-sheet-title {
|
||||
font-size: dynamic-font-min(1, 13px);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.action-sheet-sub-title {
|
||||
font-size: dynamic-font-min(1, 13px);
|
||||
font-weight: 400;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<th>Don't :x:</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
|
||||
```scss
|
||||
// action-sheet.ios.vars.scss
|
||||
|
||||
/// @prop - Font size of the action sheet title
|
||||
$action-sheet-ios-title-font-size: dynamic-font-min(1, 13px) !default;
|
||||
|
||||
/// @prop - Font weight of the action sheet title
|
||||
$action-sheet-ios-title-font-weight: 400 !default;
|
||||
|
||||
/// @prop - Font size of the action sheet sub title
|
||||
$action-sheet-ios-sub-title-font-size: dynamic-font-min(1, 13px) !default;
|
||||
```
|
||||
|
||||
```scss
|
||||
// action-sheet.ios.scss
|
||||
|
||||
.action-sheet-title {
|
||||
font-size: $action-sheet-ios-title-font-size;
|
||||
font-weight: $action-sheet-ios-title-font-weight;
|
||||
}
|
||||
|
||||
.action-sheet-sub-title {
|
||||
font-size: $action-sheet-ios-sub-title-font-size;
|
||||
font-weight: $action-sheet-ios-title-font-weight;
|
||||
}
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
[^1]: Sass Documentation, https://sass-lang.com/documentation/
|
||||
|
||||
[^2]: Ionic Framework v3 Documentation - Theming - Overriding Ionic Variables, https://ionicframework.com/docs/v3/theming/overriding-ionic-variables/
|
11
docs/vue-router/README.md
Normal file
11
docs/vue-router/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Ionic Vue Router
|
||||
|
||||
The [@ionic/vue-router](https://www.npmjs.com/package/@ionic/vue-router) package is the routing integration for [@ionic/vue](https://www.npmjs.com/package/@ionic/vue). It uses the [Vue Router](https://router.vuejs.org/) library beneath the surface.
|
||||
|
||||
## Contributing
|
||||
|
||||
See our [Contributing Guide](/docs/CONTRIBUTING.md).
|
||||
|
||||
## Testing
|
||||
|
||||
Refer to the [Vue Router Testing documentation](./testing.md) for testing the Vue Router package.
|
7
docs/vue-router/testing.md
Normal file
7
docs/vue-router/testing.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Vue Router Testing
|
||||
|
||||
## Tests
|
||||
|
||||
* Tests are found in the `__tests__` directory and use Jest.
|
||||
* Tests can be run using `npm run test.spec`
|
||||
* Bug fix and feature PRs should have new tests verifying the PR functionality.
|
11
docs/vue/README.md
Normal file
11
docs/vue/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Ionic Vue
|
||||
|
||||
The [@ionic/vue](https://www.npmjs.com/package/@ionic/vue) package builds on top of [@ionic/core](https://www.npmjs.com/package/@ionic/core) components.
|
||||
|
||||
## Contributing
|
||||
|
||||
See our [Contributing Guide](/docs/CONTRIBUTING.md).
|
||||
|
||||
## Testing
|
||||
|
||||
Refer to the [Vue Testing documentation](./testing.md) for testing the Vue package.
|
58
docs/vue/testing.md
Normal file
58
docs/vue/testing.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Vue Testing
|
||||
|
||||
Ionic Framework supports multiple versions of Vue. As a result, we need to verify that Ionic works correctly with each of these Vue versions.
|
||||
|
||||
## Syncing Local Changes
|
||||
|
||||
The Vue test app supports syncing your locally built changes for validation.
|
||||
|
||||
1. [Build](../README.md#building) the `core`, `packages/vue`, and `packages/vue-router` projects using `npm run build`.
|
||||
2. [Build the Vue test app](#test-app-build-structure).
|
||||
3. Navigate to the built test app directory (e.g. `packages/vue/test/build/vue3`).
|
||||
4. Install dependencies using `npm install`.
|
||||
5. Sync your local changes using `npm run sync`.
|
||||
|
||||
From here you can either build the application or start a local dev server. When re-syncing changes, you will need to wipe the build cache in `node_modules/.cache` and restart the dev server/re-build.
|
||||
|
||||
## Test App Build Structure
|
||||
|
||||
> [!NOTE]
|
||||
> Please confirm your current directory as `packages/vue/test` before proceeding with any of the following commands.
|
||||
|
||||
Unlike other test applications, these test apps are broken up into multiple directories. These directories are then combined to create a single application. This allows us to share common application code, tests, etc so that each app is being tested the same way. Below details the different pieces that help create a single test application.
|
||||
|
||||
**apps** - This directory contains partial applications for each version of Vue we want to test. Typically these directories contain new `package.json` files, `jest.config.js` files, and more. If you have code that is specific to a particular version of Vue, put it in this directory.
|
||||
|
||||
**base** - This directory contains the base application that each test app will use. This is where tests, application logic, and more live. If you have code that needs to be run on every test app, put it in this directory.
|
||||
|
||||
**build** - When the `apps` and `base` directories are merged, the final result is put in this directory. The `build` directory should never be committed to git.
|
||||
|
||||
**build.sh** - This is the script that merges the `apps` and `base` directories and places the built application in the `build` directory.
|
||||
|
||||
Usage:
|
||||
|
||||
```shell
|
||||
# Build a test app using apps/vue3 as a reference
|
||||
./build.sh vue3
|
||||
```
|
||||
|
||||
## How to modify test apps
|
||||
|
||||
To add new tests, components, or pages, modify the `base` project. This ensures that tests are run for every tested version.
|
||||
|
||||
If you want to add a version-specific change, add the change inside of the appropriate projects in `apps`. Be sure to replicate the directory structure. For example, if you are adding a new E2E test file called `test.e2e.ts` in `apps/vue3`, make sure you place the file in `apps/vue3/tests/e2e/test.e2e.ts`.
|
||||
|
||||
### Version-specific tests
|
||||
|
||||
If you need to add E2E tests that are only run on a specific version of the JS Framework, replicate the `VersionTest` component on each partial application. This ensures that tests for framework version X do not get run for framework version Y.
|
||||
|
||||
## Adding New Test Apps
|
||||
|
||||
As we add support for new versions of Vue, we will also need to update this directory to test against new applications. The following steps can serve as a guide for adding new apps:
|
||||
|
||||
1. Navigate to the built app for the most recent version of Vue that Ionic tests.
|
||||
2. Update the application to the latest version of Vue.
|
||||
3. Make note of any files that changed during the upgrade (`package.json`, `package-lock.json`, etc).
|
||||
4. Copy the changed files to a new directory in `apps`.
|
||||
5. Add a new entry to the matrix for `test-core-vue` in `./github/workflows/build.yml`. This will allow the new test app to run against all PRs.
|
||||
6. Commit these changes and push.
|
Reference in New Issue
Block a user