diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index e10c85f4f..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: "\U0001F41E Bug report" -description: Report an issue with Vue Router -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this bug report! - - type: input - id: reproduction - attributes: - label: Reproduction - description: "If possible, provide a boiled down editable reproduction with the [Vue.js Playground](https://play.vuejs.org/#eNqlVG1P2zAQ/iu3bFKLRuNSEB+yUMEQGpv2gti0L8s+pInbGhLbsp1SVPW/72znrVD4MlVp7Lvn7p67x/EmKFPGwzsdRAErpVAGNpApmhp6ISVsYa5ECYNVRQcJbwFKVIaq1hsSb+ggNrbxpVKGPj7hbeYhPgcJBwgrTYc+3O9LUXEzHLzFsMFBcBjU4cgvNrSUBcZPLTBeHk2vaVEIW+xNTHDrzNK9cKGNEnwxvayUotx4ziBTs4xiUvtgs4F3zhHOq6K4QSdsty4N8Xlinq6ahLeO5VfG78GIsyQgSTD9JHAN16KkMen8L0akM7S0YRd29ywuJk3N2Gqzm+s3ow9Aalztjkk7GJyXn2Vo9ij6jZZCPV4zbfB1WBt93r7So1bNVk/boCvdiNoYamUb2W1DO7jW0h4BunbQTHBdi6LgbIfLcGPbW3qa0T7uw4NDC3HROoI/fkQbry4MyACbE0iJo/BRR37rovpAp8cuuuvBw//i/xaPJo6237Q9jzpTTBrQ1FQSipQvUGKjUV6riXfiEjdPDu5k2mTCYzt5puDOzP6vTptqbyGv2qhMJV4AgmMpN/mkdmCFCJzF2rqDYc1JsDRG6oiQisv7RYgDJB3i/CQ8DsckR7V61pDqcjRT4kHj+g6z13IkwTmCSE5XRohCj1LJXirxDHh+Gp6GR6RgM4LZCeM5XbvcNjV+yVts02g8bHO2eNKk1ZwVVP2QhuFh3Gk2xXvl4YuzGVXRlmi2pNn9HvudXnvKN4pidyvaa86kakHxo7fuq5/f6RrXrbMUeVUg+hXnLdWiqCxHD/tY8Rxp93CO7WenGeOLX/pqbSjXTVOWqJuGwzshL19pvaN7HJ70pqjNY0F1mGl7seANdQj2+vFxM6FyqiKYyDUgWZbD2/F4/MG6SkzH+GgmjBFlBEdjuXZ2meY5km0tWCXhmBamkMJ7fFziOrqgc/wy+8jlpF+5S98RyLKsRyCCMf4mdYZg+w+goli8), or an **editable** and **up to date** [CodeSandbox](https://codesandbox.io/s/vue-router-4-reproduction-s1sqc), Stackblitz, or a GitHub repository. A failing unit test is even better! Otherwise provide as much information as possible to reproduce the problem. If we can't reproduce the problem, we won't be able to give it a look **and the issue will be converted into a question and moved to discussions**." - placeholder: Reproduction - validations: - required: true - - type: textarea - id: steps - attributes: - label: Steps to reproduce the bug - description: | - 1. Click on ... - 2. Check logs - validations: - required: true - - type: textarea - id: expected-behavior - attributes: - label: Expected behavior - description: A clear and concise description of what you expected to happen. - validations: - required: true - - type: textarea - id: actual-behavior - attributes: - label: Actual behavior - description: 'A clear and concise description of what actually happens.' - validations: - required: true - - type: textarea - id: other-info - attributes: - label: Additional information - description: Add any other context about the problem here. - - type: markdown - attributes: - value: | - ## Before creating an issue make sure that: - - This hasn't been [reported before](https://github.com/vuejs/router/issues). - - The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug **with no external dependencies (e.g. Vuetify or Nuxt)** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 53668b5df..94af3fbf4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,17 +1,11 @@ blank_issues_enabled: false contact_links: - - name: 👨‍💻 Support - url: https://cal.com/posva/consultancy - about: Get direct help from the author of Vue Router with your project - - name: ❓ Help & Questions - url: https://github.com/vuejs/router/discussions/new?category=help-and-questions - about: Ask a question or discuss about Vue Router - - name: 💡 Ideas - url: https://github.com/vuejs/router/discussions/new?category=ideas - about: Start a discussion to improve Vue Router - - name: 🌟 GitHub Sponsors + - name: Create new issue + url: https://new-issue.vuejs.org/?repo=vuejs/vue-router-next + about: Please use the following link to create a new issue. + - name: Github Sponsors url: https://github.com/sponsors/posva - about: Like this project? Please consider supporting the author. + about: Love Vue.js? Please consider supporting us via Github Sponsors. - name: Open Collective url: https://opencollective.com/vuejs/donate about: Love Vue.js? Please consider supporting us via Open Collective. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index 3dfe01e75..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: "\U0001F680 New feature proposal" -description: Suggest an idea for Vue Router -labels: ['feature request'] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in the project and taking the time to fill out this feature report! - - type: textarea - id: feature-description - attributes: - label: What problem is this solving - description: 'A clear and concise description of what the problem is. Ex. when using the function X we cannot do Y.' - validations: - required: true - - type: textarea - id: proposed-solution - attributes: - label: Proposed solution - description: 'A clear and concise description of what you want to happen with an API proposal when applicable' - validations: - required: true - - type: textarea - id: alternative - attributes: - label: Describe alternatives you've considered - description: A clear and concise description of any alternative solutions or features you've considered. - - type: markdown - attributes: - value: | - ## Before creating a feature request make sure that: - - This hasn't been [requested before](https://github.com/vuejs/router/issues). diff --git a/.github/contributing.md b/.github/contributing.md index 1bdb74077..c2ed3d7d2 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -17,7 +17,7 @@ Hi! I'm really excited that you are interested in contributing to Vue Router. Be ## Pull Request Guidelines -- Check out a topic branch from a base branch, e.g. `main`, and merge back against that branch. +- Checkout a topic branch from a base branch, e.g. `master`, and merge back against that branch. - If adding a new feature: @@ -26,52 +26,52 @@ Hi! I'm really excited that you are interested in contributing to Vue Router. Be - If fixing bug: - - If you are resolving a particular issue, add `(fix #xxxx[,#xxxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`. + - If you are resolving a special issue, add `(fix #xxxx[,#xxxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`. - Provide a detailed description of the bug in the PR. Live demo preferred. - - Add appropriate test coverage if applicable. You can check the coverage of your code addition by running `pnpm test --coverage`. + - Add appropriate test coverage if applicable. You can check the coverage of your code addition by running `yarn test --coverage`. - It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging. - Make sure tests pass! -- Commit messages must follow the [commit message convention](./commit-convention.md) so that the changelog can be automatically generated. Commit messages are automatically validated before commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)). +- Commit messages must follow the [commit message convention](./commit-convention.md) so that the changelog can be automatically generated. Commit messages are automatically validated before commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [yorkie](https://github.com/yyx990803/yorkie)). -- No need to worry about code style as long as you have installed the dev dependencies - modified files are automatically formatted with Prettier on commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks)). +- No need to worry about code style as long as you have installed the dev dependencies - modified files are automatically formatted with Prettier on commit (by invoking [Git Hooks](https://git-scm.com/docs/githooks) via [yorkie](https://github.com/yyx990803/yorkie)). ## Development Setup -You will need [Node.js](http://nodejs.org) **version 10+**, and [Pnpm](https://pnpm.io/installation). +You will need [Node.js](http://nodejs.org) **version 10+**, and [Yarn](https://classic.yarnpkg.com/en/docs/install). After cloning the repo, run: ```bash -pnpm install # install the dependencies of the project +$ yarn # install the dependencies of the project ``` -A high-level overview of tools used: +A high level overview of tools used: - [TypeScript](https://www.typescriptlang.org/) as the development language - [Rollup](https://rollupjs.org) for bundling -- [Vitest](https://vitest.dev/) for unit testing +- [Jest](https://jestjs.io/) for unit testing - [Prettier](https://prettier.io/) for code formatting ## Scripts -### `pnpm build` +### `yarn build` The `build` script builds vue-router -### `pnpm play` +### `yarn dev` -The `play` script starts a playground project located at `playground/`, allowing you to test things on a browser. +The `dev` scripts starts a playground project located at `playground/` that allows you to test things on a browser. ```bash -pnpm play +$ yarn dev ``` -### `pnpm test` +### `yarn test` -The `pnpm test` script runs all checks: +The `yarn test` script runs all checks: - _Typings_: `test:types` - _Linting_: `test:lint` @@ -80,10 +80,10 @@ The `pnpm test` script runs all checks: ```bash # run all tests -$ pnpm test +$ yarn test # run unit tests in watch mode -$ pnpm test:unit --watch +$ yarn jest --watch ``` ## Project Structure @@ -91,82 +91,26 @@ $ pnpm test:unit --watch Vue Router source code can be found in the `src` directory: - `src/history`: history implementations that are instantiable with `create*History()`. This folder contains code related to using the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API). -- `src/matcher`: RouteMatcher implementation. Contains the code that transforms paths like `/users/:id` into regexps and handles the transformation of locations like `{ name: 'UserDetail', params: { id: '2' } }` to strings. It contains path ranking logic and the part of dynamic routing that concerns matching URLs in the correct order. -- `src/utils`: contains small utility functions used across other router -- `src/router`: contains the router creation, navigation execution, matcher use, and history implementation. It runs navigation guards. -- `src/location`: helpers related to route location and URLs -- `src/encoding`: helpers related to URL encoding +- `src/matcher`: RouteMatcher implementation. Contains the code that transforms paths like `/users/:id` into regexps and handle the transformation of locations like `{ name: 'UserDetail', params: { id: '2' } }` to strings. It contains path ranking logic and the part of dynamic routing that concerns matching urls in the right order. +- `src/utils`: contains small utility functions that are used across other sections of the router but are not contained by them. +- `src/router`: contains the router creation, navigation execution, using the matcher, the history implementation. It runs navigation guards. +- `src/location`: helpers related to route location and urls +- `src/encoding`: helpers related to url encoding - `src/errors`: different internal and external errors with their messages -- `src/index` contains all public APIs as exports. -- `src/types`: contains global types used across multiple router sections. +- `src/index`: contains all public API as exports. +- `src/types`: contains global types that are used across multiple sections of the router. ## Contributing Tests -Unit tests are located inside `__tests__`. Consult the [Vitest docs](https://vitest.dev/guide/) and existing test cases for how to write new test specs. Here are some additional guidelines: +Unit tests are located inside `__tests__`. Consult the [Jest docs](https://jestjs.io/docs/en/using-matchers) and existing test cases for how to write new test specs. Here are some additional guidelines: - Use the minimal API needed for a test case. For example, if a test can be written without involving the reactivity system or a component, it should be written so. This limits the test's exposure to changes in unrelated parts and makes it more stable. -- Use the minimal API needed for a test case. For example, if a test concerns the `router-link` component, don't create a router instance, mock the required properties instead. +- Use the minimal API needed for a test case. For example, if a test concerns the `router-link` component, don't create a router instance, mock the needed properties instead. - Write a unit test whenever possible -- If a test is specific to a browser, create an e2e (end-to-end) test and make sure to indicate it on the test - -## Contributing Docs - -All the documentation files can be found in `packages/docs`. It contains the English markdown files while translation(s) are stored in their corresponding `` sub-folder(s): - -- [`zh`](https://github.com/vuejs/router/tree/main/packages/docs/zh): Chinese translation. - -Besides that, the `.vitepress` sub-folder contains the config and theme, including the i18n information. - -Contributing to the English docs is the same as contributing to the source code. You can create a pull request to our GitHub repo. However, if you would like to contribute to the translations, there are two options and some extra steps to follow: - -### Translate in a `` sub-folder and host it on our official repo - -If you want to start translating the docs in a _new_ language: - -1. Create the corresponding `` sub-folder for your translation. -2. Modify the i18n configuration in the `.vitepress` sub-folder. -3. Translate the docs and run the doc site to self-test locally. -4. Create a checkpoint for your language by running `pnpm run docs:translation:update []`. A checkpoint is the hash and date of the latest commit when you do the translation. The checkpoint information is stored in the status file `packages/docs/.vitepress/translation-status.json`. _It's crucial for long-term maintenance since all the further translation sync-ups are based on their previous checkpoints._ Usually, you can skip the commit argument because the default value is `main`. -5. Commit all the changes and create a pull request to our GitHub repo. - -We will have a paragraph at the top of each translation page that shows the translation status. That way, users can quickly determine if the translation is up-to-date or lags behind the English version. - -Speaking of the up-to-date translation, we also need good long-term maintenance for every language. If you want to _update_ an existing translation: - -1. See what translation you need to sync up with the original docs. There are two popular ways: - 1. Via the [GitHub Compare](https://github.com/vuejs/router/compare/) page, only see the changes in `packages/docs/*` from the checkpoint hash to `main` branch. You can find the checkpoint hash for your language via the translation status file `packages/docs/.vitepress/translation-status.json`. The compare page can be directly opened with the hash as part of the URL, e.g. https://github.com/vuejs/router/compare/e008551...main - 2. Via a local command: `pnpm run docs:translation:compare []`. -2. Create your own branch and start the translation update, following the previous comparison. -3. Create a checkpoint for your language by running `pnpm run docs:translation:update []`. -4. Commit all the changes and create a pull request to our GitHub repo. - - - -### Self-host the translation - -You can also host the translation on your own. To create one, fork our GitHub repo and change the content and site config in `packages/docs`. To long-term maintain it, we _highly recommend_ a similar way that we do above for our officially hosted translations: - -- Ensure you maintain the _checkpoint_ properly. Also, ensure the _translation status_ is well-displayed on the top of each translation page. -- Utilize the diff result between the latest official repository and your own checkpoint to guide your translation. - -Tip: you can add the official repo as a remote to your forked repo. This way, you can still run `pnpm run docs:translation:update []` and `npm run docs:translation:compare []` to get the checkpoint and diff result: - -```bash -# prepare the upstream remote -git remote add upstream git@github.com:vuejs/router.git -git fetch upstream main - -# set the checkpoint -pnpm run docs:translation:update upstream/main - -# get the diff result -pnpm run docs:translation:compare upstream/main -``` - - +- If a test is specific to a browser, create an e2e (end to end) test and make sure to indicate it on the test ## Credits Thank you to all the people who have already contributed to Vue Router! - + diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml deleted file mode 100644 index 47abd6026..000000000 --- a/.github/workflows/pkg.pr.new.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Publish Any Commit - -on: - pull_request: - branches: main - paths-ignore: - - 'packages/docs/**' - - 'packages/playground/**' - - push: - branches: - - '**' - tags: - - '!**' - paths-ignore: - - 'packages/docs/**' - - 'packages/playground/**' - -jobs: - build: - if: github.repository == 'vuejs/router' - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - cache: pnpm - - - name: Install - run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm -C packages/router build - - - name: Build DTS - run: pnpm -C packages/router build:dts - - - name: Release - run: pnpm dlx pkg-pr-new publish --compact --pnpm './packages/*' diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 8c49e9ca1..7fdbae077 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -20,4 +20,4 @@ jobs: with: tag_name: ${{ github.ref }} body: | - Please refer to [CHANGELOG.md](https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md) for details. + Please refer to [CHANGELOG.md](https://github.com/vuejs/vue-router-next/blob/master/CHANGELOG.md) for details. diff --git a/.github/workflows/size-check.yml b/.github/workflows/size-check.yml new file mode 100644 index 000000000..6915abea0 --- /dev/null +++ b/.github/workflows/size-check.yml @@ -0,0 +1,22 @@ +name: 'size' +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + size: + runs-on: ubuntu-latest + env: + CI_JOB_NUMBER: 1 + steps: + - uses: actions/checkout@v1 + - uses: bahmutov/npm-install@v1 + + - uses: posva/size-check-action@v1.1.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + build_script: build:size + files: dist/vue-router.global.prod.js size-checks/dist/webRouter.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 5bbdc52fc..000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: test - -on: - push: - branches: - - main - paths-ignore: - - 'packages/docs/**' - - 'packages/playground/**' - pull_request: - branches: - - main - paths-ignore: - - 'packages/docs/**' - - 'packages/playground/**' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - cache: pnpm - - name: 'BrowserStack Env Setup' - uses: 'browserstack/github-actions/setup-env@master' - # forks do not have access to secrets so just skip this - if: ${{ github.repository == 'vuejs/router' && !github.event.pull_request.head.repo.fork }} - with: - username: ${{ secrets.BROWSERSTACK_USERNAME }} - access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - - - run: pnpm install - - run: pnpm run lint - - run: pnpm run -r build - - run: pnpm run -r build:dts - - run: pnpm run -r test:types - - run: pnpm run -r test:unit - - # e2e tests that that run locally - - run: pnpm run -r test:e2e:ci - - - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - - # - name: 'Start BrowserStackLocal Tunnel' - # uses: 'browserstack/github-actions/setup-local@master' - # with: - # local-testing: 'start' - # local-logging-level: 'all-logs' - # local-identifier: 'random' - - # - run: pnpm run -r test:e2e:bs-test - - # - name: 'Stop BrowserStackLocal' - # uses: 'browserstack/github-actions/setup-local@master' - # with: - # local-testing: 'stop' diff --git a/.gitignore b/.gitignore index faa347817..ed70eac5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,19 @@ dist +examples_dist node_modules coverage .nyc_output .rpt2_cache .env +local.log .DS_Store +e2e/reports +e2e/screenshots +__build__ +playground_dist +yarn-error.log temp +markdown explorations selenium-server.log browserstack.err -.idea -debug.log -yalc.lock -.yalc -local.log -_selenium-server.log -packages/*/LICENSE -tracing_output diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 9b4409dda..000000000 --- a/.npmrc +++ /dev/null @@ -1,3 +0,0 @@ -shamefully-hoist=true -detect_chromedriver_version=true -strict-peer-dependencies=false diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index dd4104bc7..000000000 --- a/.prettierignore +++ /dev/null @@ -1,5 +0,0 @@ -__build__ -dist -coverage -tests_output -packages/docs/.vitepress/cache diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..d9cdf6dee --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,603 @@ +## [4.0.1](https://github.com/vuejs/vue-router-next/compare/v4.0.0...v4.0.1) (2020-12-07) + +### Bug Fixes + +- **build:** rollback rollup plugin commonjs ([9486950](https://github.com/vuejs/vue-router-next/commit/9486950f3399bda34ab2840b83fd123ac5ce7ce9)) + +# [4.0.0](https://github.com/vuejs/vue-router-next/compare/v4.0.0-rc.6...v4.0.0) (2020-12-07) + +### Bug Fixes + +- **router-view:** properly use route prop when nested ([b74051a](https://github.com/vuejs/vue-router-next/commit/b74051a6bde7524d1a7cc6cc1daacb213987faa0)) +- **router-view:** return one node when possible ([d18e500](https://github.com/vuejs/vue-router-next/commit/d18e500da2ed017be30871628a5cc59324bec15c)), closes [#537](https://github.com/vuejs/vue-router-next/issues/537) + +### Features + +- expose routerViewLocationKey as internal ([f498646](https://github.com/vuejs/vue-router-next/commit/f498646c3bc2ad480be7a3d0f11aa11710729911)) + +# [4.0.0-rc.6](https://github.com/vuejs/vue-router-next/compare/v4.0.0-rc.5...v4.0.0-rc.6) (2020-11-30) + +### Bug Fixes + +- **guards:** correctly reuse guards ([#616](https://github.com/vuejs/vue-router-next/issues/616)) ([95d44c8](https://github.com/vuejs/vue-router-next/commit/95d44c8ff2a961e052fd67b2160b87fb32d0ffb4)), closes [#614](https://github.com/vuejs/vue-router-next/issues/614) + +### Features + +- **devtools:** improve active + match in routes inspector ([9f59489](https://github.com/vuejs/vue-router-next/commit/9f59489f04cedfca5ba55da019b2dc790e926fd7)) +- **types:** expose `LocationQueryValueRaw` as internal ([dc02850](https://github.com/vuejs/vue-router-next/commit/dc028500c3e931ed5fd6beedf58b5425f5115b52)) + +# [4.0.0-rc.5](https://github.com/vuejs/vue-router-next/compare/v4.0.0-rc.4...v4.0.0-rc.5) (2020-11-21) + +### Features + +- **scroll:** allow modifying scrollBehavior in options ([#602](https://github.com/vuejs/vue-router-next/issues/602)) ([d6651f5](https://github.com/vuejs/vue-router-next/commit/d6651f5f954c8ecaf1a77ec209d5aba06343e867)) + +# [4.0.0-rc.4](https://github.com/vuejs/vue-router-next/compare/v4.0.0-rc.3...v4.0.0-rc.4) (2020-11-20) + +### Features + +- expose symbols as internals ([ef62d96](https://github.com/vuejs/vue-router-next/commit/ef62d9645c456f069699480ae3f2c3dd97b9d30d)) + +# [4.0.0-rc.3](https://github.com/vuejs/vue-router-next/compare/v4.0.0-rc.2...v4.0.0-rc.3) (2020-11-14) + +### Bug Fixes + +- trigger redirect on popstate ([#592](https://github.com/vuejs/vue-router-next/issues/592)) ([18dbdc2](https://github.com/vuejs/vue-router-next/commit/18dbdc2745cf7bd2516d4576a8d6a21de78516ec)) +- **query:** encode space as + ([4d3dd5f](https://github.com/vuejs/vue-router-next/commit/4d3dd5fd523cefc675aa7e61ed9b06b66e42b80c)), closes [#561](https://github.com/vuejs/vue-router-next/issues/561) + +# [4.0.0-rc.2](https://github.com/vuejs/vue-router-next/compare/v4.0.0-rc.1...v4.0.0-rc.2) (2020-11-05) + +### Features + +- expose injection symbols as internals ([0056aca](https://github.com/vuejs/vue-router-next/commit/0056aca5b251df2a18bab79e18874a18e0204b4d)) +- **devtools:** add devtools plugin ([894d50d](https://github.com/vuejs/vue-router-next/commit/894d50d351a40df95a3227840f5485f7e8b90432)) +- **devtools:** add more ([ee07302](https://github.com/vuejs/vue-router-next/commit/ee0730254522d6162114968e4d62b93e8b6f7f93)) +- **devtools:** better search ([5d68a29](https://github.com/vuejs/vue-router-next/commit/5d68a29386f34363b38c4138fbeae01ec538285e)) +- **devtools:** support multiple router instances ([2e5d0d4](https://github.com/vuejs/vue-router-next/commit/2e5d0d4d726ee6329745f34ca463a74820c5aa29)) + +# [4.0.0-rc.1](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.13...v4.0.0-rc.1) (2020-10-23) + +### Features + +- **warn:** improve warning for invalid components ([5985b65](https://github.com/vuejs/vue-router-next/commit/5985b6560d40412d67311df10343ee6a119a0535)), closes [#517](https://github.com/vuejs/vue-router-next/issues/517) + +# [4.0.0-beta.13](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.12...v4.0.0-beta.13) (2020-10-02) + +### Bug Fixes + +- **encoding:** decode hash in string location ([11acb3d](https://github.com/vuejs/vue-router-next/commit/11acb3dea072592f00a23b912d39c3fcf72dc6c3)) +- **encoding:** differentiate keys and values in query ([a967e42](https://github.com/vuejs/vue-router-next/commit/a967e427ab3bc5c1e6236b01f484a87b74a92be1)) +- **encoding:** keep decoded hash when resolving ([1a8ffc1](https://github.com/vuejs/vue-router-next/commit/1a8ffc19b0d2bfc17daec4cb04b96d174c73dd9d)) +- **hash:** only pushState the hash part ([2a14c19](https://github.com/vuejs/vue-router-next/commit/2a14c19e4f0313996fd075a6821f85d30c5cad66)), closes [#495](https://github.com/vuejs/vue-router-next/issues/495) + +### Features + +- **warn:** help migrating catch all routes ([14e1eb9](https://github.com/vuejs/vue-router-next/commit/14e1eb96485f74669f582a87f522d3b13b567c9c)) +- print errors from lazy loading ([f6db91a](https://github.com/vuejs/vue-router-next/commit/f6db91aaf496b85c80e74727575cc1c2b1d06282)), closes [#497](https://github.com/vuejs/vue-router-next/issues/497) + +# [4.0.0-beta.12](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.11...v4.0.0-beta.12) (2020-09-25) + +### Bug Fixes + +- **types:** extend @vue/runtime-core module ([#473](https://github.com/vuejs/vue-router-next/issues/473)) ([556cd4b](https://github.com/vuejs/vue-router-next/commit/556cd4b4af3d7ac1aa1c66848f5ab1bc33d13153)) + +# [4.0.0-beta.11](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.10...v4.0.0-beta.11) (2020-09-20) + +### Bug Fixes + +- use post flush in modal example ([2024281](https://github.com/vuejs/vue-router-next/commit/2024281902d62454d9159c87d4288d691cd0bce8)) +- **guards:** use post watcher for instances ([3234c59](https://github.com/vuejs/vue-router-next/commit/3234c5924f39fd9497866bfd160407256dc91bfe)) + +# [4.0.0-beta.10](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.9...v4.0.0-beta.10) (2020-09-18) + +### Bug Fixes + +- **history:** gracefully handle empty state ([cbcf2a9](https://github.com/vuejs/vue-router-next/commit/cbcf2a95a2af001c8aea96f3c76c4c4ef139219f)), closes [#366](https://github.com/vuejs/vue-router-next/issues/366) +- **types:** better type for navigate ([0384cb0](https://github.com/vuejs/vue-router-next/commit/0384cb062d50f6be37512410b4c2d170896dc9cb)) +- **types:** explicit types on navigate ([36d218c](https://github.com/vuejs/vue-router-next/commit/36d218c15268d0d3d15d4ed3adc75c8cb09ed68b)) +- **types:** fix types for redirect records ([a77f148](https://github.com/vuejs/vue-router-next/commit/a77f1485323ef3b654077ecb227fd5a0373d3a2f)) +- **warn:** correctly warn against unused next ([47cd7b9](https://github.com/vuejs/vue-router-next/commit/47cd7b97bb7a3999178a26a4ca1af955178ea5d6)) + +### Code Refactoring + +- **types:** Rename ScrollBehavior to RouterScrollBehavior ([9fc0996](https://github.com/vuejs/vue-router-next/commit/9fc09969db854bc0201454fbecd546637b76213a)) + +### Features + +- **router:** remove partial Promise from router.go ([6ed6eee](https://github.com/vuejs/vue-router-next/commit/6ed6eee38b59eb0b6dec0bcb7d73e24203e20ba4)) +- **types:** allow extending meta fields ([#407](https://github.com/vuejs/vue-router-next/issues/407)) ([706e84f](https://github.com/vuejs/vue-router-next/commit/706e84f0099a2a04485dfa98449fdc875442bb49)) +- **warn:** point to scrollBehavior in message ([70ce7fe](https://github.com/vuejs/vue-router-next/commit/70ce7feefac3fddd2a9641fcc2ccc66b4b108775)) + +### BREAKING CHANGES + +- **router:** The `router.go()` methods doesn't return anything + (like in Vue Router 3) anymore. The existing implementation was wrong as it + would resolve the promise for the following navigation if `router.go()` + was called with something that wasn't possible e.g. `router.go(-20)` + right after entering the application would not do anything. Even worse, + the promise returned by that call would resolve **after the next + navigation**. There is no proper native API to implement this + promise-based api properly, but one can write a version that should work + in most scenarios by setting up multiple hooks right before calling + `router.go()`: + +```js +export function go(delta) { + return new Promise((resolve, reject) => { + function popStateListener() { + clearTimeout(timeout) + } + window.addEventListener('popstate', popStateListener) + + function clearHooks() { + removeAfterEach() + removeOnError() + window.removeEventListener('popstate', popStateListener) + } + + // if the popstate event is not called, consider this a failure + const timeout = setTimeout(() => { + clearHooks() + reject(new Error('Failed to use router.go()')) + // It's unclear of what value would always work here + }, 10) + + setImmediate + + const removeAfterEach = router.afterEach((_to, _from, failure) => { + clearHooks() + resolve(failure) + }) + const removeOnError = router.onError(err => { + clearHooks() + reject(err) + }) + + router.go(delta) + }) +} +``` + +- **types:** there is already an existing type named `ScrollBehavior`, + so we are renaming our type to avoid any confusions and allow the user + to use both types at the same type (which given what the existing + `ScrollBehavior` type is designed for, will likely happen). + +# [4.0.0-beta.9](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.8...v4.0.0-beta.9) (2020-09-01) + +Build related fixes + +# [4.0.0-beta.8](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.7...v4.0.0-beta.8) (2020-09-01) + +### Bug Fixes + +- **router-view:** reuse saved instances in different records ([#446](https://github.com/vuejs/vue-router-next/issues/446)) ([6554171](https://github.com/vuejs/vue-router-next/commit/65541718b0d5af665fd87dc0e48770cba832a2bb)) +- **types:** add HTML attributes for JSX ([06f3f8f](https://github.com/vuejs/vue-router-next/commit/06f3f8fd7c3a32da331802fe5d3d19ced17200a3)), closes [#435](https://github.com/vuejs/vue-router-next/issues/435) +- **types:** allow components defined via defineComponent ([#421](https://github.com/vuejs/vue-router-next/issues/421)) ([e47c84c](https://github.com/vuejs/vue-router-next/commit/e47c84c74a97ae7bb9095ea75f98a6fa8a216532)) + +### BREAKING CHANGES + +- **router-view:** `onBeforeRouteLeave` and `onBeforeRouteUpdate` used to + have access to the component instance through `instance.proxy` but given + that: + 1. It has been marked as `internal` (https://github.com/vuejs/vue-next/pull/1849) + 2. When using `setup`, all variables are accessible on the scope (and + should be accessed that way because the code minimizes better) + It has been removed to prevent wrong usage and lighten Vue Router + +# [4.0.0-beta.7](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.6...v4.0.0-beta.7) (2020-08-19) + +### Bug Fixes + +- **encoding:** encode partial params ([eb04117](https://github.com/vuejs/vue-router-next/commit/eb041175c02ab0dac093823574a85bbbbf2056eb)) +- **matcher:** avoid trailing slash with optional params ([faf0aab](https://github.com/vuejs/vue-router-next/commit/faf0aab6451848e5b4330e1d01033137a0c42a5a)) +- **types:** append declare module ([50ad404](https://github.com/vuejs/vue-router-next/commit/50ad404ae45086f051b01ac552e4a3ab98535633)), closes [#419](https://github.com/vuejs/vue-router-next/issues/419) +- **vetur:** update tags/attributes definition ([#408](https://github.com/vuejs/vue-router-next/issues/408)) ([df8b2b1](https://github.com/vuejs/vue-router-next/commit/df8b2b140155d1e4ad5d00cd17d57ab2046a75e2)) + +### Features + +- **warn:** warn against infinite redirections ([e3dcc8d](https://github.com/vuejs/vue-router-next/commit/e3dcc8d9477e17f9b92e22787b750edc4658b77a)) + +# [4.0.0-beta.6](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.5...v4.0.0-beta.6) (2020-08-05) + +### Bug Fixes + +- **router:** stack overflow with redirect ([3594011](https://github.com/vuejs/vue-router-next/commit/359401107078348f0410abbd36cffb3b8d4d8f85)), closes [#404](https://github.com/vuejs/vue-router-next/issues/404) + +### Features + +- **router-link:** add ariaCurrentValue prop ([23e6e9c](https://github.com/vuejs/vue-router-next/commit/23e6e9c10b4f9cb9f074ebb4f56d2d99acac9097)) +- add Vetur support ([1f1189f](https://github.com/vuejs/vue-router-next/commit/1f1189fd23dc6ec318edd5d7e8f225b467d4d386)), closes [#381](https://github.com/vuejs/vue-router-next/issues/381) + +# [4.0.0-beta.5](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.4...v4.0.0-beta.5) (2020-08-03) + +### Features + +- resolve simple relative links ([af1deaa](https://github.com/vuejs/vue-router-next/commit/af1deaab5e0fd1597a7cf7ee9a6d01cac507970d)) +- **url:** simple resolve relative location ([69c44db](https://github.com/vuejs/vue-router-next/commit/69c44db3fd5363a833675b4b0ef14f97ac691af6)) +- **warn:** warn if guard returns without calling next ([6e16bdd](https://github.com/vuejs/vue-router-next/commit/6e16bdd6338ea3b7da1f8a0b3000ec880be840d6)) + +# [4.0.0-beta.4](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.3...v4.0.0-beta.4) (2020-07-25) + +### Bug Fixes + +- **router-view:** render the slot when there is no match ([bae42d4](https://github.com/vuejs/vue-router-next/commit/bae42d41c2240947e5b649e568cad274214c6346)), closes [#385](https://github.com/vuejs/vue-router-next/issues/385) +- work on Edge by adding an argument to catch ([#383](https://github.com/vuejs/vue-router-next/issues/383)) ([9580bea](https://github.com/vuejs/vue-router-next/commit/9580bead1f03f1be95473e965daa1f1ee78921f3)) + +# [4.0.0-beta.3](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.2...v4.0.0-beta.3) (2020-07-21) + +### Bug Fixes + +- **guards:** call beforeRouteEnter once per named view ([f2846ff](https://github.com/vuejs/vue-router-next/commit/f2846ff2a0796e58a9b04593909f7a30b7b68bb1)) +- **guards:** remove registered update guards after leaving ([41bffda](https://github.com/vuejs/vue-router-next/commit/41bffda49c24d560cfe555aa88bcebbbd1d03d68)) +- **guards:** skip update and leave guards of unmounted views ([f22e70a](https://github.com/vuejs/vue-router-next/commit/f22e70a6d15ce9834c9eb841d9fe9547c5d21e24)) +- **hash:** allow url to contain search params before hash ([ae8b289](https://github.com/vuejs/vue-router-next/commit/ae8b28934b1c9a092174ebd6fb5aa10aefe1de44)), closes [#378](https://github.com/vuejs/vue-router-next/issues/378) + +### Features + +- **errors:** export isNavigationFailure ([28a9b25](https://github.com/vuejs/vue-router-next/commit/28a9b25d976c325d3193cada8034a6e42297e665)) +- **guards:** allow guards to return a value instead of calling next ([#343](https://github.com/vuejs/vue-router-next/issues/343)) ([5cb209f](https://github.com/vuejs/vue-router-next/commit/5cb209f3bb53ac0ddf62152f695da610facf4724)) +- **guards:** wip context support in multi apps ([34d7390](https://github.com/vuejs/vue-router-next/commit/34d7390b946644a128ab6fd03fd821a91fd4782c)) + +# [4.0.0-beta.2](https://github.com/vuejs/vue-router-next/compare/v4.0.0-beta.1...v4.0.0-beta.2) (2020-07-07) + +Fix build cache issues + +# [4.0.0-beta.1](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.14...v4.0.0-beta.1) (2020-07-03) + +### Bug Fixes + +- **hash:** manual changes should trigger a navigation ([93891ab](https://github.com/vuejs/vue-router-next/commit/93891abf02fc24d66c6f43926a28f275560fb714)), closes [#346](https://github.com/vuejs/vue-router-next/issues/346) +- **router-link:** add missing prop custom in jsx ([c6274ae](https://github.com/vuejs/vue-router-next/commit/c6274aeaf5ad4ba4f97c82aad3e1819ef20f5d69)) +- **router-view:** preserve keep-alive route guard this context ([#344](https://github.com/vuejs/vue-router-next/issues/344)) ([994c073](https://github.com/vuejs/vue-router-next/commit/994c073fd90add30bf16b5268332277f8b082a74)) +- **warn:** warn when RouterView is wrapped with transition ([e4b3fbe](https://github.com/vuejs/vue-router-next/commit/e4b3fbe8b799b6621537afe365267a18eab9d3cd)) + +### Code Refactoring + +- **history:** simplify location as a string ([10a071c](https://github.com/vuejs/vue-router-next/commit/10a071c85c62b6674929162aa36220bd8c167f27)) +- **router:** remove history property ([aba3a3f](https://github.com/vuejs/vue-router-next/commit/aba3a3f3a0d860f76d75938ae09616a329c7c13c)) + +### Features + +- **guards:** next callback beforeRouteEnter ([d9dad0b](https://github.com/vuejs/vue-router-next/commit/d9dad0b9467fee9478406899043ee35f30cdf1fb)) + +### BREAKING CHANGES + +- **router:** the history property was marked as internal already. Since we + need to pass the history instance to the router, we always have access to it, + differently from Vue Router 3 where the history was instantiated internally. + The history API was also internal (it wasn't documented), so this change + shouldn't be a problem as people shouldn't be relying on `router.history` in + their apps. If you think this property is needed, please open an issue to + discuss the use case. Note it's already accessible as you have to create it: + +```js +export const history = createWebHistory() +export const router = createRouter({ history, routes: [] }) +``` + +- **history:** HistoryLocation is just a string now. It was pretty much an + internal property but it could be used inside `history.state`. It used to be an + object `{ fullPath: '/the-url' }`. And it's now just the `fullPath` property. + +# [4.0.0-alpha.14](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.13...v4.0.0-alpha.14) (2020-07-01) + +### Bug Fixes + +- **hash:** use relative links in hash mode ([32c9590](https://github.com/vuejs/vue-router-next/commit/32c9590db89e69c8f7c61905a5eaf19df2054e42)), closes [#342](https://github.com/vuejs/vue-router-next/issues/342) +- **query:** do not normalize query with custom stringifyQuery ([ea65066](https://github.com/vuejs/vue-router-next/commit/ea65066e8511d8320ad8de37b32ea9a8028fa9d5)), closes [#328](https://github.com/vuejs/vue-router-next/issues/328) +- **query:** isSameRouteLocation compares queries by string ([6e1f0ea](https://github.com/vuejs/vue-router-next/commit/6e1f0eacf60c7e3d465dd0af68f79dc649269b17)), closes [#328](https://github.com/vuejs/vue-router-next/issues/328) + +### Features + +- **redirect:** allow redirect on routes witch children ([e57b875](https://github.com/vuejs/vue-router-next/commit/e57b875dd9d375778a847627434803f4ec79a818)) +- **router:** support multiple apps at the same time ([565ec9d](https://github.com/vuejs/vue-router-next/commit/565ec9d489b4aad347ee466b781ca85aff76bf2d)) + +# [4.0.0-alpha.13](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.12...v4.0.0-alpha.13) (2020-06-18) + +### Bug Fixes + +- allow arbitrary selectors starting with # ([14b859d](https://github.com/vuejs/vue-router-next/commit/14b859dfa6fa5ccefe42c6f834ddd24dd9921a1b)) +- use assign to align with Vue browser support ([#311](https://github.com/vuejs/vue-router-next/issues/311)) ([f80b670](https://github.com/vuejs/vue-router-next/commit/f80b670d4dac30323221fcb2f93137ffd874c51b)), closes [#304](https://github.com/vuejs/vue-router-next/issues/304) +- **hash:** use location.pathname ([0078147](https://github.com/vuejs/vue-router-next/commit/007814745dd98bb8cfa53f44d5c308193b2fbb60)), closes [#261](https://github.com/vuejs/vue-router-next/issues/261) +- **matcher:** correct check when removing existing records on add ([2c267f5](https://github.com/vuejs/vue-router-next/commit/2c267f5aceec899c84514571e4fa75dc61441ed4)) +- **matcher:** override records by name when adding ([07100fc](https://github.com/vuejs/vue-router-next/commit/07100fc1386fb636da3eb1c8196a36f6538eb91f)) +- **scroll:** avoid reusing scroll position ([dfc1fb3](https://github.com/vuejs/vue-router-next/commit/dfc1fb34a761138a3390ccd5a8a042863018222a)) + +### Features + +- **scroll:** allow passing behavior option ([12e9209](https://github.com/vuejs/vue-router-next/commit/12e92094df46129ddf75d0fa8e3d9816644200de)) +- **scroll:** replace selector with el ([ab8a01c](https://github.com/vuejs/vue-router-next/commit/ab8a01c0a6eda1bafc293b39cb6c77ed10fb359e)) +- **warn:** warn if component is a promise ([4b2bfa8](https://github.com/vuejs/vue-router-next/commit/4b2bfa80cd3440441d71e690ca85d0532a4b8428)) +- **warn:** warn when routes are not found ([#279](https://github.com/vuejs/vue-router-next/issues/279)) ([d125356](https://github.com/vuejs/vue-router-next/commit/d125356e0f67f906f5f602f0b485f9e1e4f5bf51)) +- allow props for named views ([dbe2344](https://github.com/vuejs/vue-router-next/commit/dbe2344af5fed39aa4aa8fbfe48b195580d9538b)) +- **warn:** warn multiple params with same name ([5c8cd6e](https://github.com/vuejs/vue-router-next/commit/5c8cd6e8ae1223e9871252cc617b19424f01c5c2)) + +### BREAKING CHANGES + +- **scroll:** this change follows the RFC at + https://github.com/vuejs/rfcs/pull/176: + +* `selector` is renamed into `el` +* `el` also accepts an `Element` +* `left` and `top` are passed along `el` instead of inside an object + passed as `offset` + +- **scroll:** `scrollBehavior` doesn't accept an object with `x` and `y` + coordinates anymore. Instead it accepts an object like + [`ScrollToOptions`](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions) + with `left` and `top` properties. You can now also pass the + [`behavior`](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions/behavior) + property to enable smooth scrolling in most browsers. +- It is now necessary to escape id selectors like + explained at https://mathiasbynens.be/notes/css-escapes. This was + necessary to allow selectors like `#container > child`. + +# [4.0.0-alpha.12](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.11...v4.0.0-alpha.12) (2020-05-19) + +### Bug Fixes + +- **hash:** allow base with non trailing slash ([f5cc050](https://github.com/vuejs/vue-router-next/commit/f5cc0505f9e0cc30ff94e362ceb24d300afd684d)), closes [#247](https://github.com/vuejs/vue-router-next/issues/247) +- prevent error on initial navigation to //invalid ([e72e4ba](https://github.com/vuejs/vue-router-next/commit/e72e4ba1cc7b80aa44d3958db259d9e3a351d0fd)) + +### Features + +- **warn:** warn multiple leading slashes ([87c5e53](https://github.com/vuejs/vue-router-next/commit/87c5e53b43c218c83f9db986ac7538d74525ea5b)) + +### BREAKING CHANGES + +- **hash:** When providing a base for hash histories, it is now necessary + to include a trailing slash to create a url that starts with `/#/`, otherwise it + will result in a url starting with `#/`. This allows users to use the routing + system directly in simple files without needing to configure a server at all: + - `https://example.com/file.html` + `base: 'file.html` will produce a final + url of `https://example.com/file.html#/` + - `https://example.com/folder` + `base: 'folder` will produce a final url of + `https://example.com/folder#/` + - `https://example.com/folder` + `base: 'folder/` will produce a final url of + `https://example.com/folder/#/` + +# [4.0.0-alpha.11](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.10...v4.0.0-alpha.11) (2020-05-12) + +### Bug Fixes + +- **scroll:** change scrollRestoration if scrollBehavior is provided ([5cf2e61](https://github.com/vuejs/vue-router-next/commit/5cf2e611de2477e92699121573cb162ff98a7b8d)) +- match base in a non-sensitive way ([7087bbc](https://github.com/vuejs/vue-router-next/commit/7087bbc9c479f2955381d8a823a3ef8f9eed7b5a)) +- **router:** allow multiple router instance ([24d3d49](https://github.com/vuejs/vue-router-next/commit/24d3d49babcdea751f4c4e7e9a87625f8744a122)) +- **router:** unique first navigation with multi app ([33172af](https://github.com/vuejs/vue-router-next/commit/33172aff03b7c302699753a8abe5750094bdde26)) + +### Features + +- **types:** export NavigationGuardNext ([#229](https://github.com/vuejs/vue-router-next/issues/229)) ([888bf4d](https://github.com/vuejs/vue-router-next/commit/888bf4df33d718d74e5835e99d0f1ac4ce3a0ccf)) +- explicit injection symbols in dev mode ([#228](https://github.com/vuejs/vue-router-next/issues/228)) ([fab88ee](https://github.com/vuejs/vue-router-next/commit/fab88ee261c49b739545918deab583757aab561e)) +- support jsx and tsx for RouterLink and RouterView ([1d3dce3](https://github.com/vuejs/vue-router-next/commit/1d3dce3106af700fc95a403f1c229644fe8d85b8)), closes [#226](https://github.com/vuejs/vue-router-next/issues/226) +- **router:** allow functional components for routes ([096d864](https://github.com/vuejs/vue-router-next/commit/096d86498e954345c6bd4d8e82fe54c37d3f869b)) +- **scroll:** scroll to the same location like regular links ([5f22d4f](https://github.com/vuejs/vue-router-next/commit/5f22d4fa39171906802cc20ada00ec57bdfce880)) +- **warn:** warn if next was called multiple times ([dce2612](https://github.com/vuejs/vue-router-next/commit/dce2612e495b1d5789cd993a54d24599967a8cf4)) + +# [4.0.0-alpha.10](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.9...v4.0.0-alpha.10) (2020-05-05) + +### Bug Fixes + +- **scroll:** do not restore on push ([3f79195](https://github.com/vuejs/vue-router-next/commit/3f7919585117048c379b6dee8af1cc1de5996af0)) + +### Features + +- **warn:** warn invalid hash ([fcf2365](https://github.com/vuejs/vue-router-next/commit/fcf2365556dffa87153c13d31a684070f123ea0e)) +- allow numbers as params ([ef0920a](https://github.com/vuejs/vue-router-next/commit/ef0920a86574bca10836214015c2317ed11a29b7)), closes [#206](https://github.com/vuejs/vue-router-next/issues/206) +- **router:** allow global router classes ([388735b](https://github.com/vuejs/vue-router-next/commit/388735bc752852e2a9a24f971207fd81fae45fcf)) +- **router:** go, back and forward can be awaited ([eb87757](https://github.com/vuejs/vue-router-next/commit/eb87757ed189958c8c9955a10ece9306fa99f6d8)) +- **warn:** detect missing param in nested absolute paths ([f5b5949](https://github.com/vuejs/vue-router-next/commit/f5b59493a4e27bf07bd5a0d2e109bc6750f6f1a9)) +- **warn:** warn for invalid path+params and redirect ([91f4de9](https://github.com/vuejs/vue-router-next/commit/91f4de9aab99231fb39ed4cc5b4052979afda216)) +- **warn:** warn missing params in alias ([186e275](https://github.com/vuejs/vue-router-next/commit/186e2755ec0488ff80bdde11a53b0ddc9ee9fc03)) +- **warn:** warn when params are provided alongside path ([8a8ddf1](https://github.com/vuejs/vue-router-next/commit/8a8ddf1a5e5f2d29733da4fe25e4ddb447b0df30)) + +# [4.0.0-alpha.9](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.8...v4.0.0-alpha.9) (2020-04-29) + +- Removed sourcemaps from build + +# [4.0.0-alpha.8](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.7...v4.0.0-alpha.8) (2020-04-29) + +### Bug Fixes + +- default matcher options ([cea397b](https://github.com/vuejs/vue-router-next/commit/cea397b7402cd27ff06013f846bf35966aff6952)) +- **guards:** preserve navigation options when redirecting ([9effd81](https://github.com/vuejs/vue-router-next/commit/9effd816c51b58cb1103d878799aed6992f78454)) +- **html5:** correctly preserve current history.state ([0586394](https://github.com/vuejs/vue-router-next/commit/05863948ee86e0f1c9c9ec31c02ad7af17923743)), closes [#180](https://github.com/vuejs/vue-router-next/issues/180) +- **link:** make alias of empty child active ([cfe5993](https://github.com/vuejs/vue-router-next/commit/cfe5993332cc7dc94c5de2f2edb7f2e15c9b7049)) +- encode hash ([85bb7e1](https://github.com/vuejs/vue-router-next/commit/85bb7e11b1a4326f5048a823ae7d49654b308cdd)) +- **link:** preserve the alias path ([fffa585](https://github.com/vuejs/vue-router-next/commit/fffa58585ac89e9fb6b648e61e499a9ee3a9e217)) +- **matcher:** merge params ([d8a6b25](https://github.com/vuejs/vue-router-next/commit/d8a6b2591ac2e37388fb7f4ce8c70922389cedb5)), closes [#189](https://github.com/vuejs/vue-router-next/issues/189) +- **router:** make redirect relative to target location ([e878e91](https://github.com/vuejs/vue-router-next/commit/e878e91af217fde6d2e934857ce895e7abbd5920)) +- **router:** preserve navigation options with redirects ([9732758](https://github.com/vuejs/vue-router-next/commit/9732758d076eef252f2940ffa44e44fa94e794a0)) +- **view:** render slot with no match ([5873296](https://github.com/vuejs/vue-router-next/commit/5873296ec96df15f13b0cf02b685ebb36f4e0a41)) + +### Code Refactoring + +- Link and View renamed to RouterLink and RouterView ([030bbc4](https://github.com/vuejs/vue-router-next/commit/030bbc4c3f68d29a9e9d23ee01603394427427a3)) + +### Features + +- **link:** make empty child active with adjacent children ([4b813b1](https://github.com/vuejs/vue-router-next/commit/4b813b1ec387f8be9506f1400b7e83fd5794c7af)) +- **router:** add global pathOptions ([7383564](https://github.com/vuejs/vue-router-next/commit/73835649f450ffc378b906c72aa5ae8a6a03feb2)) +- add navigation duplicated failure ([9570416](https://github.com/vuejs/vue-router-next/commit/9570416c75f904a172af07bcf10956fe3385ec13)) +- add onBeforeRouteUpdate ([96c9503](https://github.com/vuejs/vue-router-next/commit/96c95035653a52f94781808fccbf262a02a3cd79)) +- resolve relative paths ([eae833e](https://github.com/vuejs/vue-router-next/commit/eae833e0fc1c8e549f2b4cd47b3dcb90484d17d5)) +- **router:** add back,forward,go ([5e927b5](https://github.com/vuejs/vue-router-next/commit/5e927b5ab8a09c2941edbec7c6af145323c6d3eb)) +- **router:** add beforeResolve ([9697134](https://github.com/vuejs/vue-router-next/commit/9697134c05f0f4c6fde48a773880946074e95666)) +- **scroll:** handle scroll on reload ([617f131](https://github.com/vuejs/vue-router-next/commit/617f131d2473952072f345000c3d43556dfe9761)) + +### Performance Improvements + +- use index access for strings ([971fea4](https://github.com/vuejs/vue-router-next/commit/971fea415fcce84ce86d8ace67b65115af3b7ac2)) + +### BREAKING CHANGES + +- exported components Link and View have been renamed to be + include the _Router_ prefix and to have the same export name as their component + name + +# [4.0.0-alpha.7](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.6...v4.0.0-alpha.7) (2020-04-17) + +### Features + +- add `$route` and `$router` types ([a4f80aa](https://github.com/vuejs/vue-router-next/commit/a4f80aaaafb1bf29a3f4d992e8c6a2bec0f70d62)) +- add guards types ([c7ccd5a](https://github.com/vuejs/vue-router-next/commit/c7ccd5a0e67d88467fc661474308fbdf55b947ec)) +- refactor navigation to comply with vuejs/rfcs[#150](https://github.com/vuejs/vue-router-next/issues/150) ([290c3be](https://github.com/vuejs/vue-router-next/commit/290c3be1f6cb476016f23b77d6fc49987dd84751)) + +### BREAKING CHANGES + +- This follows the RFC at https://github.com/vuejs/rfcs/pull/150 + Summary: `router.afterEach` and `router.onError` are now the global equivalent of + `router.push`/`router.replace` as well as navigation through the interface + (`history.go()`). A navigation only rejects if there was an unexpected error. + A navigation failure will still resolve the promise returned by `router.push` + and be exposed as the resolved value. + +# [4.0.0-alpha.6](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.5...v4.0.0-alpha.6) (2020-04-17) + +### Bug Fixes + +- **history:** allow base with / and base tag ([d7c71b5](https://github.com/vuejs/vue-router-next/commit/d7c71b55ee4a11ecaf3a72f25eb126d118829d3f)), closes [#164](https://github.com/vuejs/vue-router-next/issues/164) +- **history:** allow hash history with no origin ([760d216](https://github.com/vuejs/vue-router-next/commit/760d21672051b6338d40f2cdfdac80dc16209e13)), closes [#163](https://github.com/vuejs/vue-router-next/issues/163) +- **scroll:** only apply on browser ([cf53192](https://github.com/vuejs/vue-router-next/commit/cf53192b77d619b1e43c8decda76d4083d9c17ea)) +- revert history navigation if navigation is cancelled ([d8a0d11](https://github.com/vuejs/vue-router-next/commit/d8a0d117dbede9b177f06c8ebab201d12dfca0c0)) + +### Code Refactoring + +- **router:** merge createHref into resolve ([66b2db9](https://github.com/vuejs/vue-router-next/commit/66b2db95b6b73433dc3abbe6c6f7f07959429d78)) + +### Features + +- add this.\$route ([92dc18d](https://github.com/vuejs/vue-router-next/commit/92dc18d448ffeb57d9b3f3b303b8ec2991175eb5)) +- add this.\$router ([1807f30](https://github.com/vuejs/vue-router-next/commit/1807f301053ac93db1e50991f67dcf532990d5c9)) +- **scroll:** handle scroll on popstate ([181efe9](https://github.com/vuejs/vue-router-next/commit/181efe9f29a200b03e2d8f4759e7854047936824)) +- merge meta fields ([72a052f](https://github.com/vuejs/vue-router-next/commit/72a052fdf4a198e3ac72779f1b7b8b80d0ac018d)) +- **guards:** support errors in navigation guards ([23ed08d](https://github.com/vuejs/vue-router-next/commit/23ed08d983f308b7b118f2a235e58d29bf1994ec)) +- **router:** hasRoute ([ca02444](https://github.com/vuejs/vue-router-next/commit/ca02444c91c8f6b21caf6a71dee5d0f2e3f7e51b)) + +### Reverts + +- Revert "test: only call browser.end on the last test" ([d3221f1](https://github.com/vuejs/vue-router-next/commit/d3221f16978186b09531f7ea0cb5b92b20147181)) + +### BREAKING CHANGES + +- **router:** createHref is removed from the router. Instead, resolve + returns a location object with the corresponding `href` property + +# [4.0.0-alpha.5](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.4...v4.0.0-alpha.5) (2020-04-08) + +### Bug Fixes + +- **link:** not active when matched is empty ([acd644d](https://github.com/vuejs/vue-router-next/commit/acd644db70793da7719b321b2dcdd537ec358f9c)) +- check query and hash when navigating ([3862ad9](https://github.com/vuejs/vue-router-next/commit/3862ad924bbc734a835577c3a3c71bc3550db29c)) +- ignore order of keys in query and params ([643bd15](https://github.com/vuejs/vue-router-next/commit/643bd15ceaf9d6314434b15b169171b599b58e1c)) +- skip initial guards with static redirect ([c76bb93](https://github.com/vuejs/vue-router-next/commit/c76bb938a2c9a1790be98b6ce44ccd153a342141)) +- **types:** add missing exported types ([ec241f7](https://github.com/vuejs/vue-router-next/commit/ec241f7a93107815d9ffd25d36cbf00b47cb7318)), closes [#147](https://github.com/vuejs/vue-router-next/issues/147) + +### Features + +- allow symbols as route record name ([f42ab3f](https://github.com/vuejs/vue-router-next/commit/f42ab3fecfaecddcef0ccf8bb0f7f44ca24d6160)) +- **link:** activeClass and exactActiveClass props ([d53b383](https://github.com/vuejs/vue-router-next/commit/d53b3832b50131cb83b8c567015780e60addb6c8)) +- **link:** allow `custom` prop ([874510b](https://github.com/vuejs/vue-router-next/commit/874510be69c3b068970e8a90ae251cf487d6acf9)) + +### BREAKING CHANGES + +- Renamed types by removing suffix Normalized and using Raw instead + - `RouteLocation` -> `RouteLocationRaw` + - `RouteLocationNormalized` -> `RouteLocation` + - `RouteLocationNormalized` is now a location that can be displayed (not a static redirect) + - `RouteLocationNormalizedResolved` -> `RouteLocationNormalizedLoaded` + - `RouteRecord` -> `RouteRecordRaw` + - `RouteRecordNormalized` -> `RouteRecord` + - `RouteRecordNormalized` is now a record that is not a static redirect + +# [4.0.0-alpha.4](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.3...v4.0.0-alpha.4) (2020-03-28) + +### Bug Fixes + +- **history:** use current history state when replacing ([5d80209](https://github.com/vuejs/vue-router-next/commit/5d802094923851102557bfb2583835cc135e16b8)) +- export more types ([1583d48](https://github.com/vuejs/vue-router-next/commit/1583d480fff2da1caa35c2dd7892c36b57dad734)), closes [#137](https://github.com/vuejs/vue-router-next/issues/137) +- **guards:** free instances only if navigation is confirmed ([d0514e1](https://github.com/vuejs/vue-router-next/commit/d0514e192839c54c4181f80286602e9d37459f4d)) +- **hash:** fix base position for hash routing ([ba40b8f](https://github.com/vuejs/vue-router-next/commit/ba40b8f0cf2d6d85533e0e7e7daaadd088298f19)) +- initial location with base ([d05208b](https://github.com/vuejs/vue-router-next/commit/d05208b6c9457931bda8205ba6d9f1d5e39a54c7)) +- **router:** prevent duplicated navigation on aliases ([e825586](https://github.com/vuejs/vue-router-next/commit/e82558684c0b6b688065032df65604b2c245d395)) + +### Features + +- allow passing state to history ([ac1c96f](https://github.com/vuejs/vue-router-next/commit/ac1c96f176dcad8aac03a86a1dccfbaab4b66520)) +- improve route access ([baf266c](https://github.com/vuejs/vue-router-next/commit/baf266cd1bd6cafd32d244f185e340bee10af32c)) +- **history:** expose state on html5 ([3f83607](https://github.com/vuejs/vue-router-next/commit/3f83607c8798960f49cdb5eed8fdfe8adc52fabf)) +- **matcher:** remove aliases alongside the original record ([26b71b2](https://github.com/vuejs/vue-router-next/commit/26b71b285b743ab8af94b9297fa7037872ae0de6)) +- **router:** support custom parseQuery and stringifyQuery ([#136](https://github.com/vuejs/vue-router-next/issues/136)) ([5dce7bc](https://github.com/vuejs/vue-router-next/commit/5dce7bcbfbb4a80bd1edbe061a250fa646f2afd7)) +- **view:** add props option as boolean ([7fe1e7d](https://github.com/vuejs/vue-router-next/commit/7fe1e7dc7406bddd0924bf7f01709b9113582472)) +- **view:** allow passing props as a function ([494fc5e](https://github.com/vuejs/vue-router-next/commit/494fc5efb6add93c68ed467bb9a8dc7b3b149fff)) +- **view:** useView to customize router-view ([06b0c34](https://github.com/vuejs/vue-router-next/commit/06b0c34ee5018aa9d76c0bfcd32ff2c12cd94277)) +- allow true in `next` ([d76c6aa](https://github.com/vuejs/vue-router-next/commit/d76c6aae115110e2d9c4c072748bd9403080c8bd)) +- invoke guards with the right context ([7053413](https://github.com/vuejs/vue-router-next/commit/7053413c93bc715d5c2179378367dc12f60a118d)) +- lazy loading ([6ecdc70](https://github.com/vuejs/vue-router-next/commit/6ecdc70baa6361b8614368196ff2652560b6a0ba)) +- **view:** allow props as object in record ([fd4dc06](https://github.com/vuejs/vue-router-next/commit/fd4dc0630bdf856f972ed6e9020b70a70ac582b4)) + +### BREAKING CHANGES + +- `useRoute` now retrieves a reactive RouteLocationNormalized instead of a Ref. + This means there is no need to use `.value` when accessing the route. You still need to wrap it with `toRefs` if you want to expose parts of the route: + ```js + setup () { + return { params: toRefs(useRoute()).params } + } + ``` + +# [4.0.0-alpha.3](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.2...v4.0.0-alpha.3) (2020-03-14) + +### Bug Fixes + +- add missing type definitions + +# [4.0.0-alpha.2](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.1...v4.0.0-alpha.2) (2020-03-14) + +### Bug Fixes + +- **history:** correct url when replacing current location ([704b45e](https://github.com/vuejs/vue-router-next/commit/704b45ea52b10099a765c93ced37d03393a72d17)) +- **link:** allow attrs to override behavior ([4cae9db](https://github.com/vuejs/vue-router-next/commit/4cae9dbede993a79577691e1df4444a8fe5ca3a0)) +- **link:** allow custom classes ([#134](https://github.com/vuejs/vue-router-next/issues/134)) ([392c295](https://github.com/vuejs/vue-router-next/commit/392c295552e5b7dbe1d494c1c3168571e3339153)), closes [#133](https://github.com/vuejs/vue-router-next/issues/133) +- **link:** navigate to the alias path ([3284110](https://github.com/vuejs/vue-router-next/commit/328411079e1aa8a5dc3903ae76a55d634946d9fd)) +- **link:** non active repeatable params ([0ccbc1e](https://github.com/vuejs/vue-router-next/commit/0ccbc1e9af07a30a149ab14c007f63cbc35a8126)) + +### Features + +- add aliasOf to normalized records ([d9f3174](https://github.com/vuejs/vue-router-next/commit/d9f31748802c39572254691108b0667cfd40e911)) +- handle active/exact in Link ([6f49dce](https://github.com/vuejs/vue-router-next/commit/6f49dcea35a63785ae08d08787913ab8391cae67)) +- **matcher:** link aliases to their original record ([e9eb648](https://github.com/vuejs/vue-router-next/commit/e9eb6481e21de61080a96f66fbd8640157d0fd27)) + +# [4.0.0-alpha.1](https://github.com/vuejs/vue-router-next/compare/v4.0.0-alpha.0...v4.0.0-alpha.1) (2020-02-26) + +### Code Refactoring + +- rename createHistory and createHashHistory ([7dbebb6](https://github.com/vuejs/vue-router-next/commit/7dbebb6e2d75ab4aa77019712f2ed251ad62464f)) + +### Features + +- add dynamic routing at router level ([a7943c6](https://github.com/vuejs/vue-router-next/commit/a7943c64383bced7ff90ae92c0498827acdb71f6)) + +### BREAKING CHANGES + +- `createHistory` is now named `createWebHistory`. + `createHashHistory` is now named `createWebHashHistory`. + + Both createHistory and createHashHistory are renamed to + better reflect that they must be used in a browser environment while + createMemoryHistory doesn't. + +# [4.0.0-alpha.0](https://github.com/vuejs/vue-router-next/compare/v0.0.11...v4.0.0-alpha.0) (2020-02-26) + +## Known issues + +### Breaking changes compared to vue-router@3.x + +- `mode: 'history'` -> `history: createHistory()` +- Catch all routes (`/*`) must now be defined using a parameter with a custom regex: `/:catchAll(.*)` + +### Missing features + +- `keep-alive` is not yet supported +- Partial support of per-component navigation guards. No `beforeRouteEnter` yet diff --git a/LICENSE b/LICENSE index 0c77562be..2d297f23e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019-present Eduardo San Martin Morote +Copyright (c) 2020 Eduardo San Martin Morote Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 25eef7b6c..db0ad8a58 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,23 @@ -# vue-router [![release candidate](https://img.shields.io/npm/v/vue-router.svg)](https://www.npmjs.com/package/vue-router) [![test](https://github.com/vuejs/router/actions/workflows/test.yml/badge.svg)](https://github.com/vuejs/router/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/vuejs/router/graph/badge.svg?token=azNM3FI0d1)](https://codecov.io/gh/vuejs/router) +# vue-router-next [![release candidate](https://img.shields.io/npm/v/vue-router/next.svg)](https://www.npmjs.com/package/vue-router/v/next) [![CircleCI](https://circleci.com/gh/vuejs/vue-router-next.svg?style=svg)](https://circleci.com/gh/vuejs/vue-router-next) -> - This is the repository for Vue Router 4 (for Vue 3) -> - For Vue Router 3 (for Vue 2) see [vuejs/vue-router](https://github.com/vuejs/vue-router). - -

Supporting Vue Router

- -Vue Router is part of the Vue Ecosystem and is an MIT-licensed open source project with its ongoing development made possible entirely by the support of Sponsors. If you would like to become a sponsor, please consider: - -- [Become a Sponsor on GitHub](https://github.com/sponsors/posva) -- [One-time donation via PayPal](https://paypal.me/posva) - - - -

Gold Sponsors

-

- - - - CodeRabbit - - -

- -

Silver Sponsors

-

- - - - VueMastery - - - - - - Prefect - - - - - - Controla - - - - - - Route Optimizer and Route Planner Software - - -

- -

Bronze Sponsors

-

- - - - Storyblok - - - - - - NuxtLabs - - - - - - Stanislas Ormières - - -

- - - ---- - -Get started with the [documentation](https://router.vuejs.org). +This is the repository for Vue Router 4 (for Vue 3) ## Quickstart - Via CDN: `` - In-browser playground on [CodeSandbox](https://codesandbox.io/s/vue-router-4-reproduction-hb9lh) - Add it to an existing Vue Project: - ```bash npm install vue-router@4 ``` ## Changes from Vue Router 3 -Please consult the [Migration Guide](https://router.vuejs.org/guide/migration/). +Please consult the [Migration Guide](https://next.router.vuejs.org/guide/migration/). ## Contributing -See [Contributing Guide](https://github.com/vuejs/router/blob/main/.github/contributing.md). +See [Contributing Guide](https://github.com/vuejs/vue-router-next/blob/master/.github/contributing.md). ## Special Thanks diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index c0ca86836..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,5 +0,0 @@ -# Reporting a Vulnerability - -To report a vulnerability, please email security@vuejs.org. - -While the discovery of new vulnerabilities is rare, we also recommend always using the latest versions of Vue and its official companion libraries to ensure your application remains as secure as possible. diff --git a/packages/router/__tests__/RouterLink.spec.ts b/__tests__/RouterLink.spec.ts similarity index 74% rename from packages/router/__tests__/RouterLink.spec.ts rename to __tests__/RouterLink.spec.ts index 9a0a51457..c2af1951a 100644 --- a/packages/router/__tests__/RouterLink.spec.ts +++ b/__tests__/RouterLink.spec.ts @@ -1,22 +1,19 @@ /** - * @vitest-environment jsdom + * @jest-environment jsdom */ -import { RouterLink } from '../src/RouterLink' -import { RouteQueryAndHash, MatcherLocationRaw } from '../src/types' -import { START_LOCATION_NORMALIZED } from '../src/location' +import { RouterLink, RouterLinkProps } from '../src/RouterLink' import { - createMemoryHistory, - RouterOptions, + START_LOCATION_NORMALIZED, + RouteQueryAndHash, + MatcherLocationRaw, RouteLocationNormalized, - RouteLocationResolved, -} from '../src' -import { createMockedRoute } from './mount' -import { defineComponent, PropType } from 'vue' +} from '../src/types' +import { createMemoryHistory, RouterOptions } from '../src' +import { mount, createMockedRoute } from './mount' +import { defineComponent, nextTick, PropType } from 'vue' import { RouteRecordNormalized } from '../src/matcher/types' import { routerKey } from '../src/injectionSymbols' import { tick } from './utils' -import { mount } from '@vue/test-utils' -import { vi, describe, expect, it } from 'vitest' const records = { home: {} as RouteRecordNormalized, @@ -39,6 +36,8 @@ records.parentAlias = { records.childAlias = { aliasOf: records.child } as RouteRecordNormalized records.childEmptyAlias.aliasOf = records.childEmpty +type RouteLocationResolved = RouteLocationNormalized & { href: string } + function createLocations< T extends Record< string, @@ -47,7 +46,7 @@ function createLocations< normalized: RouteLocationResolved toResolve?: MatcherLocationRaw & Required } - >, + > >(locs: T) { return locs } @@ -358,8 +357,7 @@ async function factory( currentLocation: RouteLocationNormalized, propsData: any, resolvedLocation: RouteLocationResolved, - slotTemplate: string = '', - component: any = RouterLink + slotTemplate: string = '' ) { const route = createMockedRoute(currentLocation) const router = { @@ -368,19 +366,16 @@ async function factory( return this.history.base + to.fullPath }, options: {} as Partial, - resolve: vi.fn(), - push: vi.fn().mockResolvedValue(resolvedLocation), - replace: vi.fn().mockResolvedValue(resolvedLocation), + resolve: jest.fn(), + push: jest.fn().mockResolvedValue(resolvedLocation), } router.resolve.mockReturnValueOnce(resolvedLocation) - const wrapper = mount(component, { + const wrapper = await mount(RouterLink, { propsData, - global: { - provide: { - [routerKey as any]: router, - ...route.provides, - }, + provide: { + [routerKey as any]: router, + ...route.provides, }, slots: { default: slotTemplate }, }) @@ -395,7 +390,7 @@ describe('RouterLink', () => { { to: locations.basic.string }, locations.basic.normalized ) - expect(wrapper.find('a')!.attributes('href')).toBe('/home') + expect(wrapper.find('a')!.getAttribute('href')).toBe('/home') }) it('can change the value', async () => { @@ -404,10 +399,10 @@ describe('RouterLink', () => { { to: locations.basic.string }, locations.basic.normalized ) - expect(wrapper.find('a')!.attributes('href')).toBe('/home') + expect(wrapper.find('a')!.getAttribute('href')).toBe('/home') router.resolve.mockReturnValueOnce(locations.foo.normalized) await wrapper.setProps({ to: locations.foo.string }) - expect(wrapper.find('a')!.attributes('href')).toBe('/foo') + expect(wrapper.find('a')!.getAttribute('href')).toBe('/foo') }) it('displays a link with an object with path prop', async () => { @@ -416,7 +411,7 @@ describe('RouterLink', () => { { to: { path: locations.basic.string } }, locations.basic.normalized ) - expect(wrapper.find('a')!.attributes('href')).toBe('/home') + expect(wrapper.find('a')!.getAttribute('href')).toBe('/home') }) it('can be active', async () => { @@ -425,7 +420,7 @@ describe('RouterLink', () => { { to: locations.basic.string }, locations.basic.normalized ) - expect(wrapper.find('a').classes()).toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('router-link-active') }) it('sets aria-current to page by default when exact active', async () => { @@ -434,10 +429,10 @@ describe('RouterLink', () => { { to: locations.parent.string }, locations.parent.normalized ) - expect(wrapper.find('a')!.attributes('aria-current')).toBe('page') + expect(wrapper.find('a')!.getAttribute('aria-current')).toBe('page') route.set(locations.child.normalized) await tick() - expect(wrapper.find('a')!.attributes('aria-current')).not.toBe('page') + expect(wrapper.find('a')!.getAttribute('aria-current')).not.toBe('page') }) it('can customize aria-current value', async () => { @@ -446,7 +441,7 @@ describe('RouterLink', () => { { to: locations.basic.string, ariaCurrentValue: 'time' }, locations.basic.normalized ) - expect(wrapper.find('a')!.attributes('aria-current')).toBe('time') + expect(wrapper.find('a')!.getAttribute('aria-current')).toBe('time') }) it('can customize active class', async () => { @@ -455,8 +450,8 @@ describe('RouterLink', () => { { to: locations.basic.string, activeClass: 'is-active' }, locations.basic.normalized ) - expect(wrapper.find('a')!.classes()).not.toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('is-active') + expect(wrapper.find('a')!.className).not.toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('is-active') }) it('prop classes take over global', async () => { @@ -475,14 +470,14 @@ describe('RouterLink', () => { // force render because options is not reactive router.resolve.mockReturnValueOnce(locations.basic.normalized) await wrapper.setProps({ to: locations.basic.string }) - expect(wrapper.find('a')!.classes()).not.toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).not.toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) - expect(wrapper.find('a')!.classes()).not.toContain('custom') - expect(wrapper.find('a')!.classes()).not.toContain('custom-exact') - expect(wrapper.find('a')!.classes()).toContain('is-active') - expect(wrapper.find('a')!.classes()).toContain('is-exact') + expect(wrapper.find('a')!.className).not.toContain('custom') + expect(wrapper.find('a')!.className).not.toContain('custom-exact') + expect(wrapper.find('a')!.className).toContain('is-active') + expect(wrapper.find('a')!.className).toContain('is-exact') }) it('can globally customize active class', async () => { @@ -496,8 +491,8 @@ describe('RouterLink', () => { // force render because options is not reactive router.resolve.mockReturnValueOnce(locations.basic.normalized) await wrapper.setProps({ to: locations.basic.string }) - expect(wrapper.find('a')!.classes()).not.toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('custom') + expect(wrapper.find('a')!.className).not.toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('custom') }) it('can globally customize exact active class', async () => { @@ -511,10 +506,10 @@ describe('RouterLink', () => { // force render because options is not reactive router.resolve.mockReturnValueOnce(locations.basic.normalized) await wrapper.setProps({ to: locations.basic.string }) - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) - expect(wrapper.find('a')!.classes()).toContain('custom') + expect(wrapper.find('a')!.className).toContain('custom') }) it('can customize exact active class', async () => { @@ -523,10 +518,10 @@ describe('RouterLink', () => { { to: locations.basic.string, exactActiveClass: 'is-active' }, locations.basic.normalized ) - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) - expect(wrapper.find('a')!.classes()).toContain('is-active') + expect(wrapper.find('a')!.className).toContain('is-active') }) it('can be active with custom class', async () => { @@ -535,8 +530,8 @@ describe('RouterLink', () => { { to: locations.basic.string, class: 'nav-item' }, locations.basic.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('nav-item') + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('nav-item') }) it('is not active on a non matched location', async () => { @@ -545,7 +540,7 @@ describe('RouterLink', () => { { to: locations.basic.string }, locations.basic.normalized ) - expect(wrapper.find('a')!.classes()).toHaveLength(0) + expect(wrapper.find('a')!.className).toBe('') }) it('is not active with different params type', async () => { @@ -554,7 +549,7 @@ describe('RouterLink', () => { { to: locations.singleStringParams.string }, locations.singleStringParams.normalized ) - expect(wrapper.find('a')!.classes()).toHaveLength(0) + expect(wrapper.find('a')!.className).toBe('') }) it('is not active with different repeated params', async () => { @@ -563,7 +558,7 @@ describe('RouterLink', () => { { to: locations.anotherRepeatedParams2.string }, locations.anotherRepeatedParams2.normalized ) - expect(wrapper.find('a')!.classes()).toHaveLength(0) + expect(wrapper.find('a')!.className).toBe('') }) it('is not active with more repeated params', async () => { @@ -572,7 +567,7 @@ describe('RouterLink', () => { { to: locations.repeatedParams3.string }, locations.repeatedParams3.normalized ) - expect(wrapper.find('a')!.classes()).toHaveLength(0) + expect(wrapper.find('a')!.className).toBe('') }) it('is not active with partial repeated params', async () => { @@ -581,7 +576,7 @@ describe('RouterLink', () => { { to: locations.repeatedParams2.string }, locations.repeatedParams2.normalized ) - expect(wrapper.find('a')!.classes()).toHaveLength(0) + expect(wrapper.find('a')!.className).toBe('') }) it('can be active as an alias', async () => { @@ -590,8 +585,8 @@ describe('RouterLink', () => { { to: locations.alias.string }, locations.alias.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('router-link-exact-active') + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('router-link-exact-active') wrapper = ( await factory( locations.alias.normalized, @@ -599,8 +594,8 @@ describe('RouterLink', () => { locations.basic.normalized ) ).wrapper - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('router-link-exact-active') + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('router-link-exact-active') }) it('is active when a child is active', async () => { @@ -609,8 +604,8 @@ describe('RouterLink', () => { { to: locations.parent.string }, locations.parent.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) }) @@ -621,8 +616,8 @@ describe('RouterLink', () => { { to: locations.child.string }, locations.child.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('router-link-exact-active') + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('router-link-exact-active') }) it('child is not active if the parent is active', async () => { @@ -631,8 +626,8 @@ describe('RouterLink', () => { { to: locations.child.string }, locations.child.normalized ) - expect(wrapper.find('a')!.classes()).not.toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).not.toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) }) @@ -643,8 +638,8 @@ describe('RouterLink', () => { { to: locations.parent.string }, locations.parent.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) }) @@ -655,8 +650,8 @@ describe('RouterLink', () => { { to: locations.childEmpty.string }, locations.childEmpty.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) }) @@ -667,8 +662,8 @@ describe('RouterLink', () => { { to: locations.childEmptyAlias.string }, locations.childEmptyAlias.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) }) @@ -679,8 +674,8 @@ describe('RouterLink', () => { { to: locations.childEmpty.string }, locations.childEmpty.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) }) @@ -691,8 +686,8 @@ describe('RouterLink', () => { { to: locations.childEmptyAlias.string }, locations.childEmptyAlias.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) }) @@ -703,8 +698,8 @@ describe('RouterLink', () => { { to: locations.parentAlias.string }, locations.parentAlias.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) }) @@ -715,8 +710,8 @@ describe('RouterLink', () => { { to: locations.parentAlias.string }, locations.parentAlias.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) wrapper = ( @@ -726,8 +721,8 @@ describe('RouterLink', () => { locations.parentAlias.normalized ) ).wrapper - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).not.toContain( + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).not.toContain( 'router-link-exact-active' ) }) @@ -738,8 +733,8 @@ describe('RouterLink', () => { { to: locations.parentAlias.string }, locations.parentAlias.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('router-link-exact-active') + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('router-link-exact-active') wrapper = ( await factory( @@ -748,8 +743,8 @@ describe('RouterLink', () => { locations.parent.normalized ) ).wrapper - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('router-link-exact-active') + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('router-link-exact-active') }) it('child and parent with alias', async () => { @@ -758,8 +753,8 @@ describe('RouterLink', () => { { to: locations.childDoubleAlias.string }, locations.childDoubleAlias.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('router-link-exact-active') + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('router-link-exact-active') wrapper = ( await factory( @@ -768,8 +763,8 @@ describe('RouterLink', () => { locations.childParentAlias.normalized ) ).wrapper - expect(wrapper.find('a')!.classes()).toContain('router-link-active') - expect(wrapper.find('a')!.classes()).toContain('router-link-exact-active') + expect(wrapper.find('a')!.className).toContain('router-link-active') + expect(wrapper.find('a')!.className).toContain('router-link-exact-active') }) it('can be exact-active', async () => { @@ -778,7 +773,7 @@ describe('RouterLink', () => { { to: locations.basic.string }, locations.basic.normalized ) - expect(wrapper.find('a')!.classes()).toContain('router-link-exact-active') + expect(wrapper.find('a')!.className).toContain('router-link-exact-active') }) it('calls ensureLocation', async () => { @@ -797,53 +792,19 @@ describe('RouterLink', () => { { to: locations.basic.string }, locations.basic.normalized ) - wrapper.find('a')!.trigger('click') + wrapper.find('a')!.click() + await nextTick() expect(router.push).toHaveBeenCalledTimes(1) }) - it('allows adding more click listeners', async () => { - const onClick = vi.fn() - const { router, wrapper } = await factory( - START_LOCATION_NORMALIZED, - { to: locations.basic.string, onClick }, - locations.basic.normalized - ) - wrapper.find('a')!.trigger('click') - expect(router.push).toHaveBeenCalledTimes(1) - expect(onClick).toHaveBeenCalledTimes(1) - }) - - it('allows adding custom classes', async () => { - const { wrapper } = await factory( - locations.basic.normalized, - { to: locations.basic.string, class: 'custom class' }, - locations.basic.normalized - ) - expect(wrapper.find('a')!.classes()).toEqual([ - 'router-link-active', - 'router-link-exact-active', - 'custom', - 'class', - ]) - }) - - it('calls router.replace when clicked with replace prop', async () => { - const { router, wrapper } = await factory( - START_LOCATION_NORMALIZED, - { to: locations.basic.string, replace: true }, - locations.basic.normalized - ) - wrapper.find('a')!.trigger('click') - expect(router.replace).toHaveBeenCalledTimes(1) - }) - it('calls router.push with the correct location for aliases', async () => { const { router, wrapper } = await factory( START_LOCATION_NORMALIZED, { to: locations.alias.string }, locations.alias.normalized ) - wrapper.find('a')!.trigger('click') + wrapper.find('a')!.click() + await nextTick() expect(router.push).toHaveBeenCalledTimes(1) expect(router.push).not.toHaveBeenCalledWith( expect.objectContaining({ @@ -856,14 +817,12 @@ describe('RouterLink', () => { describe('v-slot', () => { const slotTemplate = ` - ` it('provides information on v-slot', async () => { @@ -885,8 +844,8 @@ describe('RouterLink', () => { slotTemplate ) - expect(wrapper.element.tagName).toBe('A') - expect(wrapper.element.childElementCount).toBe(1) + expect(wrapper.rootEl.children[0].tagName).toBe('A') + expect(wrapper.rootEl.children).toHaveLength(1) }) it('can customize the rendering and remove the wrapping `a`', async () => { @@ -900,32 +859,6 @@ describe('RouterLink', () => { expect(wrapper.html()).not.toContain('') }) - // #2375 - it('works with custom directive when custom=true', async () => { - const Directive = (el: HTMLElement) => el.setAttribute('data-test', 'x') - const AppLink = defineComponent({ - template: ` - - - - `, - components: { RouterLink }, - directives: { Directive }, - name: 'AppLink', - }) - - const { wrapper } = await factory( - locations.basic.normalized, - { to: locations.basic.string }, - locations.basic.normalized, - undefined, - AppLink - ) - - expect(wrapper.element.tagName).toBe('A') - expect(wrapper.attributes('data-test')).toBe('x') - }) - describe('Extending RouterLink', () => { const AppLink = defineComponent({ template: ` @@ -946,48 +879,75 @@ describe('RouterLink', () => { components: { RouterLink }, name: 'AppLink', + // @ts-ignore props: { - ...(RouterLink as any).props, + ...((RouterLink as any).props as RouterLinkProps), inactiveClass: String as PropType, }, computed: { isExternalLink(): boolean { - // @ts-expect-error + // @ts-ignore return typeof this.to === 'string' && this.to.startsWith('http') }, }, }) + async function factoryCustom( + currentLocation: RouteLocationNormalized, + propsData: any, + resolvedLocation: RouteLocationResolved, + slotTemplate: string = '' + ) { + const route = createMockedRoute(currentLocation) + const router = { + history: createMemoryHistory(), + createHref(to: RouteLocationNormalized): string { + return this.history.base + to.fullPath + }, + options: {} as Partial, + resolve: jest.fn(), + push: jest.fn().mockResolvedValue(resolvedLocation), + } + router.resolve.mockReturnValueOnce(resolvedLocation) + + const wrapper = await mount(AppLink, { + propsData, + provide: { + [routerKey as any]: router, + ...route.provides, + }, + slots: { default: slotTemplate }, + }) + + return { router, wrapper, route } + } + it('can extend RouterLink with inactive class', async () => { - const { wrapper } = await factory( + const { wrapper } = await factoryCustom( locations.basic.normalized, { to: locations.basic.string, inactiveClass: 'inactive', activeClass: 'active', }, - locations.foo.normalized, - undefined, - AppLink + locations.foo.normalized ) - expect(wrapper.find('a')!.classes()).toEqual(['inactive']) + expect(wrapper.find('a')!.className).toEqual('inactive') }) it('can extend RouterLink with external link', async () => { - const { wrapper } = await factory( + const { wrapper } = await factoryCustom( locations.basic.normalized, { to: 'https://esm.dev', }, - locations.foo.normalized, - undefined, - AppLink + locations.foo.normalized ) - expect(wrapper.find('a')!.classes()).toHaveLength(0) - expect(wrapper.find('a')!.attributes('href')).toEqual('https://esm.dev') + expect(wrapper.find('a')!.className).toEqual('') + expect(wrapper.find('a')!.href).toEqual('https://esm.dev/') }) }) }) diff --git a/packages/router/__tests__/RouterView.spec.ts b/__tests__/RouterView.spec.ts similarity index 65% rename from packages/router/__tests__/RouterView.spec.ts rename to __tests__/RouterView.spec.ts index 6f142b9f1..1803d7e92 100644 --- a/packages/router/__tests__/RouterView.spec.ts +++ b/__tests__/RouterView.spec.ts @@ -1,15 +1,15 @@ /** - * @vitest-environment jsdom + * @jest-environment jsdom */ import { RouterView } from '../src/RouterView' import { components, RouteLocationNormalizedLoose } from './utils' -import { START_LOCATION_NORMALIZED } from '../src/location' +import { + START_LOCATION_NORMALIZED, + RouteLocationNormalized, +} from '../src/types' import { markRaw } from 'vue' -import { createMockedRoute } from './mount' -import { mount } from '@vue/test-utils' -import { RouteLocationNormalized } from '../src' -import { describe, expect, it } from 'vitest' -import { mockWarn } from './vitest-mock-warn' +import { mount, createMockedRoute } from './mount' +import { mockWarn } from 'jest-mock-warn' // to have autocompletion function createRoutes>( @@ -19,10 +19,6 @@ function createRoutes>( for (let key in routes) { nonReactiveRoutes[key] = markRaw(routes[key]) - nonReactiveRoutes[key].matched.forEach(record => { - record.leaveGuards ??= new Set() - record.updateGuards ??= new Set() - }) } return nonReactiveRoutes @@ -205,32 +201,6 @@ const routes = createRoutes({ }, ], }, - - passthrough: { - fullPath: '/foo', - name: undefined, - path: '/foo', - query: {}, - params: {}, - hash: '', - meta: {}, - matched: [ - { - components: null, - instances: {}, - enterCallbacks: {}, - path: '/', - props, - }, - { - components: { default: components.Foo }, - instances: {}, - enterCallbacks: {}, - path: 'foo', - props, - }, - ], - }, }) describe('RouterView', () => { @@ -238,15 +208,13 @@ describe('RouterView', () => { async function factory( initialRoute: RouteLocationNormalizedLoose, - props: any = {} + propsData: any = {} ) { const route = createMockedRoute(initialRoute) - const wrapper = mount(RouterView as any, { - props, - global: { - provide: route.provides, - components: { RouterView }, - }, + const wrapper = await mount(RouterView, { + propsData, + provide: route.provides, + components: { RouterView }, }) return { route, wrapper } @@ -266,17 +234,19 @@ describe('RouterView', () => { const { wrapper } = await factory(START_LOCATION_NORMALIZED as any) // NOTE: I wonder if this will stay stable in future releases expect('Router').not.toHaveBeenWarned() - expect(wrapper.element.childNodes).toHaveLength(0) + expect(wrapper.rootEl.childElementCount).toBe(0) }) it('displays nested views', async () => { const { wrapper } = await factory(routes.nested) - expect(wrapper.html()).toMatchSnapshot() + expect(wrapper.html()).toBe(`

Nested

Foo
`) }) it('displays deeply nested views', async () => { const { wrapper } = await factory(routes.nestedNested) - expect(wrapper.html()).toMatchSnapshot() + expect(wrapper.html()).toBe( + `

Nested

Nested

Foo
` + ) }) it('renders when the location changes', async () => { @@ -323,47 +293,35 @@ describe('RouterView', () => { expect(wrapper.html()).toBe(`
id:foo;other:fixed
`) }) - it('inherit attributes', async () => { - const { wrapper } = await factory(routes.withIdAndOther, { - 'data-test': 'true', - }) - expect(wrapper.html()).toBe( - `
id:foo;other:fixed
` - ) - }) - it('can pass a function as props', async () => { const { wrapper } = await factory(routes.withFnProps) expect(wrapper.html()).toBe(`
id:2;other:page
`) }) - it('pass through with empty children', async () => { - const { wrapper } = await factory(routes.passthrough) - expect(wrapper.html()).toBe(`
Foo
`) - }) - describe('warnings', () => { - it('does not warn RouterView is wrapped', () => { + it('does not warn RouterView is wrapped', async () => { const route = createMockedRoute(routes.root) - const wrapper = mount( + const wrapper = await mount( { - template: `
`, + template: ` +
+ +
+ `, }, { - props: {}, - global: { - provide: route.provides, - components: { RouterView }, - }, + propsData: {}, + provide: route.provides, + components: { RouterView }, } ) - expect(wrapper.html()).toMatchSnapshot() + expect(wrapper.html()).toBe(`
Home
`) expect('can no longer be used directly inside').not.toHaveBeenWarned() }) - it('warns if KeepAlive wraps a RouterView', () => { + it('warns if KeepAlive wraps a RouterView', async () => { const route = createMockedRoute(routes.root) - const wrapper = mount( + const wrapper = await mount( { template: ` @@ -372,11 +330,9 @@ describe('RouterView', () => { `, }, { - props: {}, - global: { - provide: route.provides, - components: { RouterView }, - }, + propsData: {}, + provide: route.provides, + components: { RouterView }, } ) expect(wrapper.html()).toBe(`
Home
`) @@ -385,7 +341,7 @@ describe('RouterView', () => { it('warns if KeepAlive and Transition wrap a RouterView', async () => { const route = createMockedRoute(routes.root) - const wrapper = mount( + const wrapper = await mount( { template: ` @@ -396,49 +352,18 @@ describe('RouterView', () => { `, }, { - props: {}, - global: { - stubs: { - transition: false, - }, - provide: route.provides, - components: { RouterView }, - }, + propsData: {}, + provide: route.provides, + components: { RouterView }, } ) expect(wrapper.html()).toBe(`
Home
`) expect('can no longer be used directly inside').toHaveBeenWarned() }) - it('does not warn if RouterView is not a direct-child of transition', async () => { + it('warns if Transition wraps a RouterView', async () => { const route = createMockedRoute(routes.root) - mount( - { - template: ` - -
- -
-
- `, - }, - { - props: {}, - global: { - stubs: { - transition: false, - }, - provide: route.provides, - components: { RouterView }, - }, - } - ) - expect('can no longer be used directly inside').not.toHaveBeenWarned() - }) - - it('warns if Transition wraps a RouterView', () => { - const route = createMockedRoute(routes.root) - const wrapper = mount( + const wrapper = await mount( { template: ` @@ -447,14 +372,9 @@ describe('RouterView', () => { `, }, { - props: {}, - global: { - stubs: { - transition: false, - }, - provide: route.provides, - components: { RouterView }, - }, + propsData: {}, + provide: route.provides, + components: { RouterView }, } ) expect(wrapper.html()).toBe(`
Home
`) @@ -465,22 +385,18 @@ describe('RouterView', () => { describe('v-slot', () => { async function factory( initialRoute: RouteLocationNormalizedLoose, - props: any = {} + propsData: any = {} ) { const route = createMockedRoute(initialRoute) - const wrapper = await mount(RouterView as any, { - props, - global: { - provide: route.provides, - components: { RouterView }, - }, + const wrapper = await mount(RouterView, { + propsData, + provide: route.provides, + components: { RouterView }, slots: { default: ` - - `, + {{ route.name }} + + `, }, }) @@ -489,69 +405,34 @@ describe('RouterView', () => { it('passes a Component and route', async () => { const { wrapper } = await factory(routes.root) - expect(wrapper.html()).toMatchSnapshot() + expect(wrapper.html()).toBe(`home
Home
`) }) }) describe('KeepAlive', () => { async function factory( initialRoute: RouteLocationNormalizedLoose, - props: any = {} - ) { - const route = createMockedRoute(initialRoute) - const wrapper = await mount(RouterView as any, { - props, - global: { - provide: route.provides, - components: { RouterView }, - }, - slots: { - default: ` - `, - }, - }) - - return { route, wrapper } - } - - it('works', async () => { - const { route, wrapper } = await factory(routes.root) - expect(wrapper.html()).toMatchInlineSnapshot(`"
Home
"`) - await route.set(routes.foo) - expect(wrapper.html()).toMatchInlineSnapshot(`"
Foo
"`) - }) - }) - - describe('Suspense', () => { - async function factory( - initialRoute: RouteLocationNormalizedLoose, - props: any = {} + propsData: any = {} ) { const route = createMockedRoute(initialRoute) - const wrapper = await mount(RouterView as any, { - props, - global: { - provide: route.provides, - components: { RouterView }, - }, + const wrapper = await mount(RouterView, { + propsData, + provide: route.provides, + components: { RouterView }, slots: { default: ` - `, + + + + `, }, }) return { route, wrapper } } - it('works', async () => { + // TODO: maybe migrating to VTU 2 to handle this properly + it.skip('works', async () => { const { route, wrapper } = await factory(routes.root) expect(wrapper.html()).toMatchInlineSnapshot(`"
Home
"`) await route.set(routes.foo) diff --git a/__tests__/__snapshots__/RouterLink.spec.ts.snap b/__tests__/__snapshots__/RouterLink.spec.ts.snap new file mode 100644 index 000000000..360a57a37 --- /dev/null +++ b/__tests__/__snapshots__/RouterLink.spec.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RouterLink v-slot provides information on v-slot 1`] = `" route: {\\"href\\":\\"/home\\",\\"fullPath\\":\\"/home\\",\\"path\\":\\"/home\\",\\"params\\":{},\\"meta\\":{},\\"query\\":{},\\"hash\\":\\"\\",\\"matched\\":[{}],\\"name\\":\\"home\\"} href: \\"/home\\" isActive: \\"true\\" isExactActive: \\"true\\" "`; diff --git a/packages/router/__tests__/encoding.spec.ts b/__tests__/encoding.spec.ts similarity index 98% rename from packages/router/__tests__/encoding.spec.ts rename to __tests__/encoding.spec.ts index e4aebb88d..d299ac3a8 100644 --- a/packages/router/__tests__/encoding.spec.ts +++ b/__tests__/encoding.spec.ts @@ -5,7 +5,6 @@ import { encodeQueryValue, // decode, } from '../src/encoding' -import { describe, expect, it } from 'vitest' describe('Encoding', () => { // all ascii chars with a non ascii char at the beginning diff --git a/packages/router/__tests__/errors.spec.ts b/__tests__/errors.spec.ts similarity index 87% rename from packages/router/__tests__/errors.spec.ts rename to __tests__/errors.spec.ts index 5678bf43f..ba85b09ae 100644 --- a/packages/router/__tests__/errors.spec.ts +++ b/__tests__/errors.spec.ts @@ -8,26 +8,23 @@ import { ErrorTypes, } from '../src/errors' import { components, tick } from './utils' -import type { RouteRecordRaw, NavigationGuard } from '../src' -import type { +import { + RouteRecordRaw, + NavigationGuard, RouteLocationRaw, - RouteLocationNormalized, -} from '../src/typed-routes' -import { START_LOCATION_NORMALIZED } from '../src/location' -import { vi, describe, expect, it, beforeEach } from 'vitest' -import { mockWarn } from './vitest-mock-warn' + START_LOCATION_NORMALIZED, +} from '../src/types' -const routes: Readonly[] = [ +const routes: RouteRecordRaw[] = [ { path: '/', component: components.Home }, { path: '/redirect', redirect: '/' }, { path: '/foo', component: components.Foo, name: 'Foo' }, // prevent the log of no match warnings { path: '/:pathMatch(.*)', component: components.Home }, - { path: '/async', component: () => Promise.reject('failed') }, ] -const onError = vi.fn() -const afterEach = vi.fn() +const onError = jest.fn() +const afterEach = jest.fn() function createRouter() { const history = createMemoryHistory() const router = newRouter({ @@ -41,7 +38,6 @@ function createRouter() { } describe('Errors & Navigation failures', () => { - mockWarn() beforeEach(() => { onError.mockReset() afterEach.mockReset() @@ -56,10 +52,6 @@ describe('Errors & Navigation failures', () => { ) }) - it('lazy loading reject', async () => { - await testError(true, 'failed', '/async') - }) - it('Duplicated navigation triggers afterEach', async () => { let expectedFailure = expect.objectContaining({ type: NavigationFailureType.duplicated, @@ -149,25 +141,6 @@ describe('Errors & Navigation failures', () => { }, error) }) - it('triggers onError with to and from', async () => { - const { router } = createRouter() - let expectedTo: RouteLocationNormalized | undefined - let expectedFrom: RouteLocationNormalized | undefined - const error = new Error() - router.beforeEach((to, from) => { - expectedTo = to - expectedFrom = from - throw error - }) - - await router.push('/foo').catch(() => {}) - - expect(afterEach).toHaveBeenCalledTimes(0) - expect(onError).toHaveBeenCalledTimes(1) - - expect(onError).toHaveBeenCalledWith(error, expectedTo, expectedFrom) - }) - it('triggers onError with rejected promises', async () => { let error = new Error() await testError(async () => { @@ -342,7 +315,7 @@ describe('isNavigationFailure', () => { async function testError( nextArgument: any | NavigationGuard, - expectedError: any = undefined, + expectedError: Error | void = undefined, to: RouteLocationRaw = '/foo' ) { const { router } = createRouter() @@ -354,20 +327,12 @@ async function testError( } ) - if (expectedError !== undefined) { - await expect(router.push(to)).rejects.toEqual(expectedError) - } else { - await router.push(to).catch(() => {}) - } + await expect(router.push(to)).rejects.toEqual(expectedError) expect(afterEach).toHaveBeenCalledTimes(0) expect(onError).toHaveBeenCalledTimes(1) - expect(onError).toHaveBeenCalledWith( - expectedError, - expect.any(Object), - expect.any(Object) - ) + expect(onError).toHaveBeenCalledWith(expectedError) } async function testNavigation( @@ -455,9 +420,5 @@ async function testHistoryError( expect(afterEach).toHaveBeenCalledTimes(0) expect(onError).toHaveBeenCalledTimes(1) - expect(onError).toHaveBeenCalledWith( - expectedError, - expect.any(Object), - expect.any(Object) - ) + expect(onError).toHaveBeenCalledWith(expectedError) } diff --git a/packages/router/__tests__/guards/afterEach.spec.ts b/__tests__/guards/afterEach.spec.ts similarity index 75% rename from packages/router/__tests__/guards/afterEach.spec.ts rename to __tests__/guards/afterEach.spec.ts index 8709b76fc..01a9d44f4 100644 --- a/packages/router/__tests__/guards/afterEach.spec.ts +++ b/__tests__/guards/afterEach.spec.ts @@ -1,6 +1,5 @@ import { createDom, newRouter as createRouter } from '../utils' -import { RouteRecordRaw } from '../../src/types' -import { vi, describe, expect, it, beforeAll } from 'vitest' +import { RouteRecordRaw } from 'src/types' const Home = { template: `
Home
` } const Foo = { template: `
Foo
` } @@ -25,7 +24,7 @@ describe('router.afterEach', () => { }) it('calls afterEach guards on push', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) router.afterEach(spy) await router.push('/foo') @@ -38,7 +37,7 @@ describe('router.afterEach', () => { }) it('can be removed', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) const remove = router.afterEach(spy) remove() @@ -47,7 +46,7 @@ describe('router.afterEach', () => { }) it('calls afterEach guards on multiple push', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) await router.push('/nested') router.afterEach(spy) @@ -66,16 +65,4 @@ describe('router.afterEach', () => { ) expect(spy).toHaveBeenCalledTimes(2) }) - - it('removing an afterEach guard within one does not affect others', async () => { - const spy1 = vi.fn() - const spy2 = vi.fn() - const router = createRouter({ routes }) - router.afterEach(spy1) - const remove = router.afterEach(spy2) - spy1.mockImplementationOnce(remove) - await router.push('/foo') - expect(spy1).toHaveBeenCalledTimes(1) - expect(spy2).toHaveBeenCalledTimes(1) - }) }) diff --git a/packages/router/__tests__/guards/beforeEach.spec.ts b/__tests__/guards/beforeEach.spec.ts similarity index 79% rename from packages/router/__tests__/guards/beforeEach.spec.ts rename to __tests__/guards/beforeEach.spec.ts index b4e373606..4cc45e089 100644 --- a/packages/router/__tests__/guards/beforeEach.spec.ts +++ b/__tests__/guards/beforeEach.spec.ts @@ -1,8 +1,6 @@ import fakePromise from 'faked-promise' import { createDom, tick, noGuard, newRouter as createRouter } from '../utils' -import { RouteRecordRaw } from '../../src/types' -import { RouteLocationRaw } from '../../src' -import { vi, describe, expect, it, beforeAll } from 'vitest' +import { RouteRecordRaw, RouteLocationRaw } from '../../src/types' const Home = { template: `
Home
` } const Foo = { template: `
Foo
` } @@ -21,10 +19,6 @@ const routes: RouteRecordRaw[] = [ { path: 'home', name: 'nested-home', component: Home }, ], }, - { - path: '/redirect', - redirect: { path: '/other', state: { fromRecord: true } }, - }, ] describe('router.beforeEach', () => { @@ -33,7 +27,7 @@ describe('router.beforeEach', () => { }) it('calls beforeEach guards on navigation', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) router.beforeEach(spy) spy.mockImplementationOnce(noGuard) @@ -42,7 +36,7 @@ describe('router.beforeEach', () => { }) it('can be removed', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) const remove = router.beforeEach(spy) remove() @@ -52,7 +46,7 @@ describe('router.beforeEach', () => { }) it('does not call beforeEach guard if we were already on the page', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) await router.push('/foo') router.beforeEach(spy) @@ -62,7 +56,7 @@ describe('router.beforeEach', () => { }) it('calls beforeEach guards on navigation between children routes', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) await router.push('/nested') router.beforeEach(spy) @@ -84,7 +78,7 @@ describe('router.beforeEach', () => { }) it('can redirect to a different location', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) await router.push('/foo') spy.mockImplementation((to, from, next) => { @@ -112,52 +106,8 @@ describe('router.beforeEach', () => { expect(router.currentRoute.value.fullPath).toBe('/other') }) - it('can add state when redirecting', async () => { - const router = createRouter({ routes }) - await router.push('/foo') - router.beforeEach((to, from) => { - // only allow going to /other - if (to.fullPath !== '/other') { - return { - path: '/other', - state: { added: 'state' }, - } - } - return - }) - - const spy = vi.spyOn(history, 'pushState') - await router.push({ path: '/', state: { a: 'a' } }) - expect(spy).toHaveBeenCalledTimes(1) - // called before redirect - expect(spy).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ added: 'state', a: 'a' }), - '', - expect.stringMatching(/\/other$/) - ) - spy.mockClear() - }) - - it('can add state to a redirect route', async () => { - const router = createRouter({ routes }) - await router.push('/foo') - - const spy = vi.spyOn(history, 'pushState') - await router.push({ path: '/redirect', state: { a: 'a' } }) - expect(spy).toHaveBeenCalledTimes(1) - // called before redirect - expect(spy).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ fromRecord: true, a: 'a' }), - '', - expect.stringMatching(/\/other$/) - ) - spy.mockClear() - }) - async function assertRedirect(redirectFn: (i: string) => RouteLocationRaw) { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) await router.push('/') spy.mockImplementation((to, from, next) => { @@ -186,7 +136,7 @@ describe('router.beforeEach', () => { }) it('is called when changing params', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes: [...routes] }) await router.push('/n/2') spy.mockImplementation(noGuard) @@ -197,7 +147,7 @@ describe('router.beforeEach', () => { }) it('is not called with same params', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes: [...routes] }) await router.push('/n/2') spy.mockImplementation(noGuard) @@ -225,7 +175,7 @@ describe('router.beforeEach', () => { const [p1, r1] = fakePromise() const [p2, r2] = fakePromise() const router = createRouter({ routes }) - const guard1 = vi.fn() + const guard1 = jest.fn() let order = 0 guard1.mockImplementationOnce(async (to, from, next) => { expect(order++).toBe(0) @@ -233,7 +183,7 @@ describe('router.beforeEach', () => { next() }) router.beforeEach(guard1) - const guard2 = vi.fn() + const guard2 = jest.fn() guard2.mockImplementationOnce(async (to, from, next) => { expect(order++).toBe(1) await p2 @@ -257,7 +207,7 @@ describe('router.beforeEach', () => { }) it('adds meta information', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) router.beforeEach(spy) spy.mockImplementationOnce(noGuard) diff --git a/packages/router/__tests__/guards/beforeEnter.spec.ts b/__tests__/guards/beforeEnter.spec.ts similarity index 94% rename from packages/router/__tests__/guards/beforeEnter.spec.ts rename to __tests__/guards/beforeEnter.spec.ts index 1df476eee..913b0d041 100644 --- a/packages/router/__tests__/guards/beforeEnter.spec.ts +++ b/__tests__/guards/beforeEnter.spec.ts @@ -1,21 +1,20 @@ import fakePromise from 'faked-promise' import { createDom, noGuard, tick, newRouter as createRouter } from '../utils' import { RouteRecordRaw } from '../../src/types' -import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest' const Home = { template: `
Home
` } const Foo = { template: `
Foo
` } -const beforeEnter = vi.fn() -const beforeEnters = [vi.fn(), vi.fn()] +const beforeEnter = jest.fn() +const beforeEnters = [jest.fn(), jest.fn()] const nested = { - parent: vi.fn(), - nestedEmpty: vi.fn(), - nestedA: vi.fn(), - nestedAbs: vi.fn(), - nestedNested: vi.fn(), - nestedNestedFoo: vi.fn(), - nestedNestedParam: vi.fn(), + parent: jest.fn(), + nestedEmpty: jest.fn(), + nestedA: jest.fn(), + nestedAbs: jest.fn(), + nestedNested: jest.fn(), + nestedNestedFoo: jest.fn(), + nestedNestedParam: jest.fn(), } const routes: RouteRecordRaw[] = [ diff --git a/packages/router/__tests__/guards/beforeResolve.spec.ts b/__tests__/guards/beforeResolve.spec.ts similarity index 88% rename from packages/router/__tests__/guards/beforeResolve.spec.ts rename to __tests__/guards/beforeResolve.spec.ts index 9736b520e..a0dbeaed4 100644 --- a/packages/router/__tests__/guards/beforeResolve.spec.ts +++ b/__tests__/guards/beforeResolve.spec.ts @@ -1,6 +1,5 @@ import { createDom, noGuard, newRouter as createRouter } from '../utils' import { RouteRecordRaw } from '../../src/types' -import { vi, describe, expect, it, beforeAll } from 'vitest' const Home = { template: `
Home
` } const Foo = { template: `
Foo
` } @@ -16,7 +15,7 @@ describe('router.beforeEach', () => { }) it('calls beforeEach guards on navigation', async () => { - const spy = vi.fn() + const spy = jest.fn() const router = createRouter({ routes }) router.beforeResolve(spy) spy.mockImplementationOnce(noGuard) diff --git a/packages/router/__tests__/guards/beforeRouteEnter.spec.ts b/__tests__/guards/beforeRouteEnter.spec.ts similarity index 84% rename from packages/router/__tests__/guards/beforeRouteEnter.spec.ts rename to __tests__/guards/beforeRouteEnter.spec.ts index 43268f2b5..1a0a3b7e1 100644 --- a/packages/router/__tests__/guards/beforeRouteEnter.spec.ts +++ b/__tests__/guards/beforeRouteEnter.spec.ts @@ -1,25 +1,27 @@ import fakePromise from 'faked-promise' import { createDom, noGuard, newRouter as createRouter } from '../utils' -import type { RouteRecordRaw, NavigationGuard } from '../../src' -import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest' +import { RouteRecordRaw, NavigationGuard } from '../../src/types' const Home = { template: `
Home
` } const Foo = { template: `
Foo
` } -const beforeRouteEnter = vi.fn() +const beforeRouteEnter = jest.fn< + ReturnType, + Parameters +>() const named = { - default: vi.fn(), - other: vi.fn(), + default: jest.fn(), + other: jest.fn(), } const nested = { - parent: vi.fn(), - nestedEmpty: vi.fn(), - nestedA: vi.fn(), - nestedAbs: vi.fn(), - nestedNested: vi.fn(), - nestedNestedFoo: vi.fn(), - nestedNestedParam: vi.fn(), + parent: jest.fn(), + nestedEmpty: jest.fn(), + nestedA: jest.fn(), + nestedAbs: jest.fn(), + nestedNested: jest.fn(), + nestedNestedFoo: jest.fn(), + nestedNestedParam: jest.fn(), } const routes: RouteRecordRaw[] = [ @@ -27,7 +29,6 @@ const routes: RouteRecordRaw[] = [ { path: '/foo', component: Foo }, { path: '/guard/:n', - alias: '/guard-alias/:n', component: { ...Foo, beforeRouteEnter, @@ -119,20 +120,6 @@ describe('beforeRouteEnter', () => { expect(beforeRouteEnter).toHaveBeenCalledTimes(1) }) - it('does not call beforeRouteEnter guards on navigation between aliases', async () => { - const router = createRouter({ routes }) - const spy = vi.fn() - beforeRouteEnter.mockImplementation(spy) - await router.push('/guard/valid') - expect(beforeRouteEnter).toHaveBeenCalledTimes(1) - await router.push('/guard-alias/valid') - expect(beforeRouteEnter).toHaveBeenCalledTimes(1) - await router.push('/guard-alias/other') - expect(beforeRouteEnter).toHaveBeenCalledTimes(1) - await router.push('/guard/other') - expect(beforeRouteEnter).toHaveBeenCalledTimes(1) - }) - it('calls beforeRouteEnter guards on navigation for nested views', async () => { const router = createRouter({ routes }) await router.push('/nested/nested/foo') diff --git a/packages/router/__tests__/guards/beforeRouteEnterCallback.spec.ts b/__tests__/guards/beforeRouteEnterCallback.spec.ts similarity index 81% rename from packages/router/__tests__/guards/beforeRouteEnterCallback.spec.ts rename to __tests__/guards/beforeRouteEnterCallback.spec.ts index d851c7c3f..b223c7709 100644 --- a/packages/router/__tests__/guards/beforeRouteEnterCallback.spec.ts +++ b/__tests__/guards/beforeRouteEnterCallback.spec.ts @@ -1,14 +1,18 @@ /** - * @vitest-environment jsdom + * @jest-environment jsdom */ import { defineComponent, h } from 'vue' -import { mount } from '@vue/test-utils' -import { createRouter, createMemoryHistory, RouterOptions } from '../../src' -import { vi, describe, expect, it, beforeEach } from 'vitest' +import { mount } from '../mount' +import { + createRouter, + RouterView, + createMemoryHistory, + RouterOptions, +} from '../../src' const nextCallbacks = { - Default: vi.fn(), - Other: vi.fn(), + Default: jest.fn(), + Other: jest.fn(), } const Default = defineComponent({ beforeRouteEnter(to, from, next) { @@ -52,7 +56,7 @@ describe('beforeRouteEnter next callback', () => { ...options, }) - const wrapper = mount( + const wrapper = await mount( { template: `
@@ -60,12 +64,9 @@ describe('beforeRouteEnter next callback', () => {
`, + components: { RouterView }, }, - { - global: { - plugins: [router], - }, - } + { router } ) return { wrapper, router } diff --git a/packages/router/__tests__/guards/beforeRouteLeave.spec.ts b/__tests__/guards/beforeRouteLeave.spec.ts similarity index 95% rename from packages/router/__tests__/guards/beforeRouteLeave.spec.ts rename to __tests__/guards/beforeRouteLeave.spec.ts index 57dd78158..ff5618337 100644 --- a/packages/router/__tests__/guards/beforeRouteLeave.spec.ts +++ b/__tests__/guards/beforeRouteLeave.spec.ts @@ -1,21 +1,20 @@ import { createDom, noGuard, newRouter as createRouter } from '../utils' import { RouteRecordRaw } from '../../src/types' -import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest' const Home = { template: `
Home
` } const Foo = { template: `
Foo
` } const nested = { - parent: vi.fn(), - nestedEmpty: vi.fn(), - nestedA: vi.fn(), - nestedB: vi.fn(), - nestedAbs: vi.fn(), - nestedNested: vi.fn(), - nestedNestedFoo: vi.fn(), - nestedNestedParam: vi.fn(), + parent: jest.fn(), + nestedEmpty: jest.fn(), + nestedA: jest.fn(), + nestedB: jest.fn(), + nestedAbs: jest.fn(), + nestedNested: jest.fn(), + nestedNestedFoo: jest.fn(), + nestedNestedParam: jest.fn(), } -const beforeRouteLeave = vi.fn() +const beforeRouteLeave = jest.fn() const routes: RouteRecordRaw[] = [ { path: '/', component: Home }, diff --git a/packages/router/__tests__/guards/beforeRouteUpdate.spec.ts b/__tests__/guards/beforeRouteUpdate.spec.ts similarity index 76% rename from packages/router/__tests__/guards/beforeRouteUpdate.spec.ts rename to __tests__/guards/beforeRouteUpdate.spec.ts index d4cbf98eb..2b539b6eb 100644 --- a/packages/router/__tests__/guards/beforeRouteUpdate.spec.ts +++ b/__tests__/guards/beforeRouteUpdate.spec.ts @@ -1,18 +1,16 @@ import fakePromise from 'faked-promise' import { createDom, noGuard, newRouter as createRouter } from '../utils' import { RouteRecordRaw } from '../../src/types' -import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest' const Home = { template: `
Home
` } const Foo = { template: `
Foo
` } -const beforeRouteUpdate = vi.fn() +const beforeRouteUpdate = jest.fn() const routes: RouteRecordRaw[] = [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/guard/:go', - alias: '/guard-alias/:go', component: { ...Foo, beforeRouteUpdate, @@ -41,18 +39,6 @@ describe('beforeRouteUpdate', () => { expect(beforeRouteUpdate).toHaveBeenCalledTimes(1) }) - it('calls beforeRouteUpdate guards when changing params with alias', async () => { - const router = createRouter({ routes }) - beforeRouteUpdate.mockImplementationOnce(noGuard) - await router.push('/guard/valid') - // not called on initial navigation - expect(beforeRouteUpdate).not.toHaveBeenCalled() - // simulate a mounted route component - router.currentRoute.value.matched[0].instances.default = {} as any - await router.push('/guard-alias/other') - expect(beforeRouteUpdate).toHaveBeenCalledTimes(1) - }) - it('does not call beforeRouteUpdate guard if the view is not mounted', async () => { const router = createRouter({ routes }) beforeRouteUpdate.mockImplementationOnce(noGuard) diff --git a/packages/router/__tests__/guards/extractComponentsGuards.spec.ts b/__tests__/guards/extractComponentsGuards.spec.ts similarity index 62% rename from packages/router/__tests__/guards/extractComponentsGuards.spec.ts rename to __tests__/guards/extractComponentsGuards.spec.ts index b10a32247..7301ea47d 100644 --- a/packages/router/__tests__/guards/extractComponentsGuards.spec.ts +++ b/__tests__/guards/extractComponentsGuards.spec.ts @@ -1,25 +1,25 @@ import { extractComponentsGuards } from '../../src/navigationGuards' -import type { RouteRecordRaw, RouteRecordNormalized } from '../../src' -import { START_LOCATION_NORMALIZED } from '../../src/location' +import { START_LOCATION_NORMALIZED, RouteRecordRaw } from '../../src/types' import { components } from '../utils' import { normalizeRouteRecord } from '../../src/matcher' -import { mockWarn } from '../vitest-mock-warn' -import { vi, describe, expect, it, beforeEach } from 'vitest' +import { RouteRecordNormalized } from 'src/matcher/types' +import { mockWarn } from 'jest-mock-warn' -const beforeRouteEnter = vi.fn() +const beforeRouteEnter = jest.fn() // stub those two const to = START_LOCATION_NORMALIZED const from = START_LOCATION_NORMALIZED const NoGuard: RouteRecordRaw = { path: '/', component: components.Home } -// @ts-expect-error const InvalidRoute: RouteRecordRaw = { path: '/', + // @ts-ignore: intended error component: null, } const WrongLazyRoute: RouteRecordRaw = { path: '/', + // @ts-ignore: intended error component: Promise.resolve(components.Home), } const SingleGuard: RouteRecordRaw = { @@ -33,10 +33,6 @@ const SingleGuardNamed: RouteRecordRaw = { other: { ...components.Foo, beforeRouteEnter }, }, } -const ErrorLazyLoad: RouteRecordRaw = { - path: '/', - component: () => Promise.reject(new Error('custom')), -} beforeEach(() => { beforeRouteEnter.mockReset() @@ -51,7 +47,7 @@ async function checkGuards( guardsLength: number = n ) { beforeRouteEnter.mockClear() - const guards = extractComponentsGuards( + const guards = await extractComponentsGuards( // type is fine as we excluded RouteRecordRedirect in components argument components.map(normalizeRouteRecord) as RouteRecordNormalized[], 'beforeRouteEnter', @@ -88,46 +84,16 @@ describe('extractComponentsGuards', () => { }) it('throws if component is null', async () => { - // @ts-expect-error - await expect(checkGuards([InvalidRoute], 0)) - expect('either missing a "component(s)" or "children"').toHaveBeenWarned() + // @ts-ignore + await expect(checkGuards([InvalidRoute], 2)).rejects.toHaveProperty( + 'message', + expect.stringMatching('Invalid route component') + ) + expect('is not a valid component').toHaveBeenWarned() }) it('warns wrong lazy component', async () => { await checkGuards([WrongLazyRoute], 0, 1) expect('Promise instead of a function').toHaveBeenWarned() }) - - it('rejects if lazy load fails', async () => { - await expect(checkGuards([ErrorLazyLoad], 0, 1)).rejects.toHaveProperty( - 'message', - 'custom' - ) - }) - - it('preserves resolved modules in mods', async () => { - const mod = { - default: components.Home, - __esModule: true, - custom: true, - } - const mod2 = { - default: components.Bar, - __esModule: true, - custom: true, - } - const record = normalizeRouteRecord({ - path: '/', - components: { default: async () => mod, other: async () => mod2 }, - }) - expect(record.mods).toEqual({}) - const guards = extractComponentsGuards( - [record], - 'beforeRouteEnter', - to, - from - ) - await Promise.all(guards.map(guard => guard())) - expect(record.mods).toEqual({ default: mod, other: mod2 }) - }) }) diff --git a/packages/router/__tests__/guards/guardToPromiseFn.spec.ts b/__tests__/guards/guardToPromiseFn.spec.ts similarity index 97% rename from packages/router/__tests__/guards/guardToPromiseFn.spec.ts rename to __tests__/guards/guardToPromiseFn.spec.ts index d19267793..d2b596f98 100644 --- a/packages/router/__tests__/guards/guardToPromiseFn.spec.ts +++ b/__tests__/guards/guardToPromiseFn.spec.ts @@ -1,8 +1,7 @@ import { guardToPromiseFn } from '../../src/navigationGuards' -import { START_LOCATION_NORMALIZED } from '../../src/location' +import { START_LOCATION_NORMALIZED } from '../../src/types' import { ErrorTypes } from '../../src/errors' -import { mockWarn } from '../vitest-mock-warn' -import { vi, describe, expect, it } from 'vitest' +import { mockWarn } from 'jest-mock-warn' // stub those two const to = START_LOCATION_NORMALIZED @@ -16,7 +15,7 @@ describe('guardToPromiseFn', () => { mockWarn() it('calls the guard with to, from and, next', async () => { expect.assertions(2) - const spy = vi.fn((to, from, next) => next()) + const spy = jest.fn((to, from, next) => next()) await expect(guardToPromiseFn(spy, to, from)()).resolves.toEqual(undefined) expect(spy).toHaveBeenCalledWith(to, from, expect.any(Function)) }) diff --git a/packages/router/__tests__/guards/guardsContext.spec.ts b/__tests__/guards/guardsContext.spec.ts similarity index 77% rename from packages/router/__tests__/guards/guardsContext.spec.ts rename to __tests__/guards/guardsContext.spec.ts index dd8de4819..7900f9f3b 100644 --- a/packages/router/__tests__/guards/guardsContext.spec.ts +++ b/__tests__/guards/guardsContext.spec.ts @@ -1,9 +1,8 @@ /** - * @vitest-environment jsdom + * @jest-environment jsdom */ import { createRouter, createMemoryHistory } from '../../src' import { createApp, defineComponent } from 'vue' -import { vi, describe, expect, it } from 'vitest' const component = { template: '
Generic
', @@ -12,15 +11,12 @@ const component = { describe('beforeRouteLeave', () => { it('invokes with the component context', async () => { expect.assertions(2) - const spy = vi.fn().mockImplementationOnce(function ( - this: any, - to, - from, - next - ) { - expect(typeof this.counter).toBe('number') - next() - }) + const spy = jest + .fn() + .mockImplementationOnce(function (this: any, to, from, next) { + expect(typeof this.counter).toBe('number') + next() + }) const WithLeave = defineComponent({ template: `text`, // we use data to check if the context is the right one because saving `this` in a variable logs a few warnings @@ -57,29 +53,23 @@ describe('beforeRouteLeave', () => { template: `text`, // we use data to check if the context is the right one because saving `this` in a variable logs a few warnings data: () => ({ counter: 0 }), - beforeRouteLeave: vi.fn().mockImplementationOnce(function ( - this: any, - to, - from, - next - ) { - expect(typeof this.counter).toBe('number') - next() - }), + beforeRouteLeave: jest + .fn() + .mockImplementationOnce(function (this: any, to, from, next) { + expect(typeof this.counter).toBe('number') + next() + }), }) const WithLeaveTwo = defineComponent({ template: `text`, // we use data to check if the context is the right one because saving `this` in a variable logs a few warnings data: () => ({ counter: 0 }), - beforeRouteLeave: vi.fn().mockImplementationOnce(function ( - this: any, - to, - from, - next - ) { - expect(typeof this.counter).toBe('number') - next() - }), + beforeRouteLeave: jest + .fn() + .mockImplementationOnce(function (this: any, to, from, next) { + expect(typeof this.counter).toBe('number') + next() + }), }) const router = createRouter({ @@ -117,29 +107,23 @@ describe('beforeRouteLeave', () => { template: ``, // we use data to check if the context is the right one because saving `this` in a variable logs a few warnings data: () => ({ counter: 0 }), - beforeRouteLeave: vi.fn().mockImplementationOnce(function ( - this: any, - to, - from, - next - ) { - expect(typeof this.counter).toBe('number') - next() - }), + beforeRouteLeave: jest + .fn() + .mockImplementationOnce(function (this: any, to, from, next) { + expect(typeof this.counter).toBe('number') + next() + }), }) const WithLeave = defineComponent({ template: `text`, // we use data to check if the context is the right one because saving `this` in a variable logs a few warnings data: () => ({ counter: 0 }), - beforeRouteLeave: vi.fn().mockImplementationOnce(function ( - this: any, - to, - from, - next - ) { - expect(typeof this.counter).toBe('number') - next() - }), + beforeRouteLeave: jest + .fn() + .mockImplementationOnce(function (this: any, to, from, next) { + expect(typeof this.counter).toBe('number') + next() + }), }) const router = createRouter({ @@ -182,43 +166,34 @@ describe('beforeRouteLeave', () => { `, // we use data to check if the context is the right one because saving `this` in a variable logs a few warnings data: () => ({ counter: 0 }), - beforeRouteLeave: vi.fn().mockImplementationOnce(function ( - this: any, - to, - from, - next - ) { - expect(typeof this.counter).toBe('number') - next() - }), + beforeRouteLeave: jest + .fn() + .mockImplementationOnce(function (this: any, to, from, next) { + expect(typeof this.counter).toBe('number') + next() + }), }) const WithLeaveOne = defineComponent({ template: `text`, // we use data to check if the context is the right one because saving `this` in a variable logs a few warnings data: () => ({ counter: 0 }), - beforeRouteLeave: vi.fn().mockImplementationOnce(function ( - this: any, - to, - from, - next - ) { - expect(typeof this.counter).toBe('number') - next() - }), + beforeRouteLeave: jest + .fn() + .mockImplementationOnce(function (this: any, to, from, next) { + expect(typeof this.counter).toBe('number') + next() + }), }) const WithLeaveTwo = defineComponent({ template: `text`, // we use data to check if the context is the right one because saving `this` in a variable logs a few warnings data: () => ({ counter: 0 }), - beforeRouteLeave: vi.fn().mockImplementationOnce(function ( - this: any, - to, - from, - next - ) { - expect(typeof this.counter).toBe('number') - next() - }), + beforeRouteLeave: jest + .fn() + .mockImplementationOnce(function (this: any, to, from, next) { + expect(typeof this.counter).toBe('number') + next() + }), }) const router = createRouter({ @@ -259,15 +234,12 @@ describe('beforeRouteLeave', () => { describe('beforeRouteUpdate', () => { it('invokes with the component context', async () => { expect.assertions(2) - const spy = vi.fn().mockImplementationOnce(function ( - this: any, - to, - from, - next - ) { - expect(typeof this.counter).toBe('number') - next() - }) + const spy = jest + .fn() + .mockImplementationOnce(function (this: any, to, from, next) { + expect(typeof this.counter).toBe('number') + next() + }) const WithParam = defineComponent({ template: `text`, // we use data to check if the context is the right one because saving `this` in a variable logs a few warnings diff --git a/packages/router/__tests__/guards/onBeforeRouteLeave.spec.ts b/__tests__/guards/onBeforeRouteLeave.spec.ts similarity index 91% rename from packages/router/__tests__/guards/onBeforeRouteLeave.spec.ts rename to __tests__/guards/onBeforeRouteLeave.spec.ts index a244874f9..7fd60c632 100644 --- a/packages/router/__tests__/guards/onBeforeRouteLeave.spec.ts +++ b/__tests__/guards/onBeforeRouteLeave.spec.ts @@ -1,5 +1,5 @@ /** - * @vitest-environment jsdom + * @jest-environment jsdom */ import { createRouter, @@ -7,7 +7,6 @@ import { onBeforeRouteLeave, } from '../../src' import { createApp, defineComponent } from 'vue' -import { vi, describe, expect, it } from 'vitest' const component = { template: '
Generic
', @@ -15,7 +14,7 @@ const component = { describe('onBeforeRouteLeave', () => { it('removes guards when leaving the route', async () => { - const spy = vi.fn() + const spy = jest.fn() const WithLeave = defineComponent({ template: `text`, setup() { diff --git a/__tests__/guards/onBeforeRouteUpdate.spec.ts b/__tests__/guards/onBeforeRouteUpdate.spec.ts new file mode 100644 index 000000000..dab3d99a0 --- /dev/null +++ b/__tests__/guards/onBeforeRouteUpdate.spec.ts @@ -0,0 +1,50 @@ +/** + * @jest-environment jsdom + */ +import { + createRouter, + createMemoryHistory, + onBeforeRouteUpdate, +} from '../../src' +import { createApp, defineComponent } from 'vue' + +const component = { + template: '
Generic
', +} + +describe('onBeforeRouteUpdate', () => { + it('removes update guards when leaving', async () => { + const spy = jest.fn() + const WithLeave = defineComponent({ + template: `text`, + setup() { + onBeforeRouteUpdate(spy) + }, + }) + + const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/', component }, + { path: '/foo', component: WithLeave as any }, + ], + }) + const app = createApp({ + template: ` + + `, + }) + app.use(router) + const rootEl = document.createElement('div') + document.body.appendChild(rootEl) + app.mount(rootEl) + + await router.isReady() + await router.push('/foo') + await router.push('/foo?q') + await router.push('/') + await router.push('/foo') + await router.push('/foo?q') + expect(spy).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/router/__tests__/history/hash.spec.ts b/__tests__/history/hash.spec.ts similarity index 91% rename from packages/router/__tests__/history/hash.spec.ts rename to __tests__/history/hash.spec.ts index 65cc6dfdd..43a299c15 100644 --- a/packages/router/__tests__/history/hash.spec.ts +++ b/__tests__/history/hash.spec.ts @@ -2,23 +2,12 @@ import { JSDOM } from 'jsdom' import { createWebHashHistory } from '../../src/history/hash' import { createWebHistory } from '../../src/history/html5' import { createDom } from '../utils' -import { mockWarn } from '../vitest-mock-warn' -import { - vi, - describe, - expect, - it, - beforeAll, - beforeEach, - Mock, - afterAll, - afterEach, -} from 'vitest' - -vi.mock('../../src/history/html5') +import { mockWarn } from 'jest-mock-warn' + +jest.mock('../../src/history/html5') // override the value of isBrowser because the variable is created before JSDOM // is created -vi.mock('../../src/utils/env', () => ({ +jest.mock('../../src/utils/env', () => ({ isBrowser: true, })) @@ -30,7 +19,7 @@ describe('History Hash', () => { mockWarn() beforeEach(() => { - ;(createWebHistory as Mock).mockClear() + ;(createWebHistory as jest.Mock).mockClear() }) afterAll(() => { diff --git a/__tests__/history/html5.spec.ts b/__tests__/history/html5.spec.ts new file mode 100644 index 000000000..c92ce0d62 --- /dev/null +++ b/__tests__/history/html5.spec.ts @@ -0,0 +1,121 @@ +import { JSDOM } from 'jsdom' +import { createWebHistory } from '../../src/history/html5' +import { createDom } from '../utils' + +// override the value of isBrowser because the variable is created before JSDOM +// is created +jest.mock('../../src/utils/env', () => ({ + isBrowser: true, +})) + +// These unit tests are supposed to tests very specific scenarios that are easier to setup +// on a unit test than an e2e tests +describe('History HTMl5', () => { + let dom: JSDOM + beforeAll(() => { + dom = createDom() + }) + + afterAll(() => { + dom.window.close() + }) + + afterEach(() => { + // ensure no base element is left after a test as only the first is + // respected + for (let element of Array.from(document.getElementsByTagName('base'))) + element.remove() + }) + + it('handles a basic base', () => { + expect(createWebHistory().base).toBe('') + expect(createWebHistory('/').base).toBe('') + }) + + it('handles a base tag', () => { + const baseEl = document.createElement('base') + baseEl.href = '/foo/' + document.head.appendChild(baseEl) + expect(createWebHistory().base).toBe('/foo') + }) + + it('handles a base tag with origin', () => { + const baseEl = document.createElement('base') + baseEl.href = 'https://example.com/foo/' + document.head.appendChild(baseEl) + expect(createWebHistory().base).toBe('/foo') + }) + + it('handles a base tag with origin without trailing slash', () => { + const baseEl = document.createElement('base') + baseEl.href = 'https://example.com/bar' + document.head.appendChild(baseEl) + expect(createWebHistory().base).toBe('/bar') + }) + + it('ignores base tag if base is provided', () => { + const baseEl = document.createElement('base') + baseEl.href = '/foo/' + document.head.appendChild(baseEl) + expect(createWebHistory('/bar/').base).toBe('/bar') + }) + + it('handles a non-empty base', () => { + expect(createWebHistory('/foo/').base).toBe('/foo') + expect(createWebHistory('/foo').base).toBe('/foo') + }) + + it('handles a single hash base', () => { + expect(createWebHistory('#').base).toBe('#') + expect(createWebHistory('#/').base).toBe('#') + }) + + it('handles a non-empty hash base', () => { + expect(createWebHistory('#/bar').base).toBe('#/bar') + expect(createWebHistory('#/bar/').base).toBe('#/bar') + }) + + it('prepends the host to support // urls', () => { + let history = createWebHistory() + let spy = jest.spyOn(window.history, 'pushState') + history.push('/foo') + expect(spy).toHaveBeenCalledWith( + expect.anything(), + expect.any(String), + 'https://example.com/foo' + ) + history.push('//foo') + expect(spy).toHaveBeenLastCalledWith( + expect.anything(), + expect.any(String), + 'https://example.com//foo' + ) + spy.mockRestore() + }) + + it('calls push with hash part of the url with a base', () => { + dom.reconfigure({ url: 'file:///usr/etc/index.html' }) + let history = createWebHistory('/usr/etc/index.html#/') + let spy = jest.spyOn(window.history, 'pushState') + history.push('/foo') + expect(spy).toHaveBeenCalledWith( + expect.anything(), + expect.any(String), + '#/foo' + ) + spy.mockRestore() + }) + + it('works with something after the hash in the base', () => { + dom.reconfigure({ url: 'file:///usr/etc/index.html' }) + let history = createWebHistory('#something') + let spy = jest.spyOn(window.history, 'pushState') + history.push('/foo') + expect(spy).toHaveBeenCalledWith( + expect.anything(), + expect.any(String), + '#something/foo' + ) + spy.mockRestore() + }) +}) diff --git a/packages/router/__tests__/history/memory.spec.ts b/__tests__/history/memory.spec.ts similarity index 78% rename from packages/router/__tests__/history/memory.spec.ts rename to __tests__/history/memory.spec.ts index b2de91e6d..4afa14544 100644 --- a/packages/router/__tests__/history/memory.spec.ts +++ b/__tests__/history/memory.spec.ts @@ -1,6 +1,5 @@ import { createMemoryHistory } from '../../src/history/memory' import { START, HistoryLocation } from '../../src/history/common' -import { vi, describe, expect, it } from 'vitest' const loc: HistoryLocation = '/foo' @@ -27,7 +26,7 @@ describe('Memory history', () => { it('does not trigger listeners with push', () => { const history = createMemoryHistory() - const spy = vi.fn() + const spy = jest.fn() history.listen(spy) history.push(loc) expect(spy).not.toHaveBeenCalled() @@ -35,7 +34,7 @@ describe('Memory history', () => { it('does not trigger listeners with replace', () => { const history = createMemoryHistory() - const spy = vi.fn() + const spy = jest.fn() history.listen(spy) history.replace(loc) expect(spy).not.toHaveBeenCalled() @@ -51,16 +50,6 @@ describe('Memory history', () => { expect(history.location).toEqual(START) }) - it('stores a state', () => { - const history = createMemoryHistory() - history.push(loc, { foo: 'bar' }) - expect(history.state).toEqual({ foo: 'bar' }) - history.push(loc, { foo: 'baz' }) - expect(history.state).toEqual({ foo: 'baz' }) - history.go(-1) - expect(history.state).toEqual({ foo: 'bar' }) - }) - it('does nothing with back if queue contains only one element', () => { const history = createMemoryHistory() history.go(-1) @@ -102,7 +91,7 @@ describe('Memory history', () => { it('can listen to navigations', () => { const history = createMemoryHistory() - const spy = vi.fn() + const spy = jest.fn() history.listen(spy) history.push(loc) history.go(-1) @@ -123,8 +112,8 @@ describe('Memory history', () => { it('can stop listening to navigation', () => { const history = createMemoryHistory() - const spy = vi.fn() - const spy2 = vi.fn() + const spy = jest.fn() + const spy2 = jest.fn() // remove right away history.listen(spy)() const remove = history.listen(spy2) @@ -140,8 +129,8 @@ describe('Memory history', () => { it('removing the same listener is a noop', () => { const history = createMemoryHistory() - const spy = vi.fn() - const spy2 = vi.fn() + const spy = jest.fn() + const spy2 = jest.fn() const rem = history.listen(spy) const rem2 = history.listen(spy2) rem() @@ -160,35 +149,16 @@ describe('Memory history', () => { it('removes all listeners with destroy', () => { const history = createMemoryHistory() history.push('/other') - const spy = vi.fn() + const spy = jest.fn() history.listen(spy) history.destroy() - history.push('/2') history.go(-1) expect(spy).not.toHaveBeenCalled() }) - it('can be reused after destroy', () => { - const history = createMemoryHistory() - history.push('/1') - history.push('/2') - history.push('/3') - history.go(-1) - - expect(history.location).toBe('/2') - history.destroy() - history.go(-1) - expect(history.location).toBe(START) - history.push('/4') - history.push('/5') - expect(history.location).toBe('/5') - history.go(-1) - expect(history.location).toBe('/4') - }) - it('can avoid listeners with back and forward', () => { const history = createMemoryHistory() - const spy = vi.fn() + const spy = jest.fn() history.listen(spy) history.push(loc) history.go(-1, false) @@ -196,9 +166,4 @@ describe('Memory history', () => { history.go(1, false) expect(spy).not.toHaveBeenCalled() }) - - it('handles a non-empty base', () => { - expect(createMemoryHistory('/foo/').base).toBe('/foo') - expect(createMemoryHistory('/foo').base).toBe('/foo') - }) }) diff --git a/packages/router/__tests__/initialNavigation.spec.ts b/__tests__/initialNavigation.spec.ts similarity index 92% rename from packages/router/__tests__/initialNavigation.spec.ts rename to __tests__/initialNavigation.spec.ts index c30d7fd34..9625e365b 100644 --- a/packages/router/__tests__/initialNavigation.spec.ts +++ b/__tests__/initialNavigation.spec.ts @@ -2,11 +2,10 @@ import { JSDOM } from 'jsdom' import { createRouter, createWebHistory } from '../src' import { createDom, components, nextNavigation } from './utils' import { RouteRecordRaw } from '../src/types' -import { describe, expect, it, beforeAll, vi, afterAll } from 'vitest' // override the value of isBrowser because the variable is created before JSDOM // is created -vi.mock('../src/utils/env', () => ({ +jest.mock('../src/utils/env', () => ({ isBrowser: true, })) @@ -62,7 +61,7 @@ describe('Initial Navigation', () => { expect(router.currentRoute.value).toMatchObject({ path: '/' }) }) - it('handles initial navigation with beforeEnter', async () => { + it('handles initial navigation with beforEnter', async () => { const { history, router } = newRouter('/home-before') expect(history.location).toBe('/home-before') // this is done automatically on mount but there is no mount here diff --git a/packages/router/__tests__/lazyLoading.spec.ts b/__tests__/lazyLoading.spec.ts similarity index 86% rename from packages/router/__tests__/lazyLoading.spec.ts rename to __tests__/lazyLoading.spec.ts index 1a8210937..62a4c3b92 100644 --- a/packages/router/__tests__/lazyLoading.spec.ts +++ b/__tests__/lazyLoading.spec.ts @@ -4,16 +4,7 @@ import { RouterOptions } from '../src/router' import { RouteComponent } from '../src/types' import { ticks } from './utils' import { FunctionalComponent, h } from 'vue' -import { mockWarn } from './vitest-mock-warn' -import { - vi, - describe, - expect, - it, - beforeEach, - MockInstance, - afterEach, -} from 'vitest' +import { mockWarn } from 'jest-mock-warn' function newRouter(options: Partial = {}) { let history = createMemoryHistory() @@ -26,7 +17,7 @@ function createLazyComponent() { const [promise, resolve, reject] = fakePromise() return { - component: vi.fn(() => promise.then(() => ({}) as RouteComponent)), + component: jest.fn(() => promise.then(() => ({} as RouteComponent))), promise, resolve, reject, @@ -35,15 +26,6 @@ function createLazyComponent() { describe('Lazy Loading', () => { mockWarn() - let consoleErrorSpy: MockInstance - beforeEach(() => { - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - }) - - afterEach(() => { - consoleErrorSpy.mockRestore() - }) - it('works', async () => { const { component, resolve } = createLazyComponent() const { router } = newRouter({ @@ -159,7 +141,7 @@ describe('Lazy Loading', () => { it('avoid fetching async component if navigation is cancelled through beforeEnter', async () => { const { component, resolve } = createLazyComponent() - const spy = vi.fn((to, from, next) => next(false)) + const spy = jest.fn((to, from, next) => next(false)) const { router } = newRouter({ routes: [ { @@ -187,7 +169,7 @@ describe('Lazy Loading', () => { ], }) - const spy = vi.fn((to, from, next) => next(false)) + const spy = jest.fn((to, from, next) => next(false)) router.beforeEach(spy) @@ -199,8 +181,8 @@ describe('Lazy Loading', () => { it('invokes beforeRouteEnter after lazy loading the component', async () => { const { promise, resolve } = createLazyComponent() - const spy = vi.fn((to, from, next) => next()) - const component = vi.fn(() => + const spy = jest.fn((to, from, next) => next()) + const component = jest.fn(() => promise.then(() => ({ beforeRouteEnter: spy })) ) const { router } = newRouter({ @@ -215,8 +197,8 @@ describe('Lazy Loading', () => { it('beforeRouteLeave works on a lazy loaded component', async () => { const { promise, resolve } = createLazyComponent() - const spy = vi.fn((to, from, next) => next()) - const component = vi.fn(() => + const spy = jest.fn((to, from, next) => next()) + const component = jest.fn(() => promise.then(() => ({ beforeRouteLeave: spy })) ) const { router } = newRouter({ @@ -240,8 +222,8 @@ describe('Lazy Loading', () => { it('beforeRouteUpdate works on a lazy loaded component', async () => { const { promise, resolve } = createLazyComponent() - const spy = vi.fn((to, from, next) => next()) - const component = vi.fn(() => + const spy = jest.fn((to, from, next) => next()) + const component = jest.fn(() => promise.then(() => ({ beforeRouteUpdate: spy })) ) const { router } = newRouter({ @@ -266,15 +248,13 @@ describe('Lazy Loading', () => { routes: [{ path: '/foo', component }], }) - const spy = vi.fn() + const spy = jest.fn() - const error = new Error('fail') - reject(error) + reject(new Error('fail')) await router.push('/foo').catch(spy) expect(spy).toHaveBeenCalled() - expect(spy).toHaveBeenLastCalledWith(error) - expect('uncaught error').toHaveBeenWarned() + expect('fail').toHaveBeenWarned() expect(router.currentRoute.value).toMatchObject({ path: '/', @@ -288,13 +268,12 @@ describe('Lazy Loading', () => { routes: [{ path: '/foo', component }], }) - const spy = vi.fn() + const spy = jest.fn() reject() await router.push('/foo').catch(spy) - expect(spy).toHaveBeenCalledTimes(1) - expect('uncaught error').toHaveBeenWarned() + expect(spy).toHaveBeenCalled() expect(router.currentRoute.value).toMatchObject({ path: '/', @@ -315,15 +294,13 @@ describe('Lazy Loading', () => { ], }) - const spy = vi.fn() + const spy = jest.fn() parent.resolve() - const error = new Error() - child.reject(error) + child.reject() await router.push('/foo').catch(spy) - expect(spy).toHaveBeenCalledWith(error) - expect('uncaught error').toHaveBeenWarned() + expect(spy).toHaveBeenCalledWith(expect.any(Error)) expect(router.currentRoute.value).toMatchObject({ path: '/', diff --git a/packages/router/__tests__/location.spec.ts b/__tests__/location.spec.ts similarity index 87% rename from packages/router/__tests__/location.spec.ts rename to __tests__/location.spec.ts index 8ccd8a425..a770e900d 100644 --- a/packages/router/__tests__/location.spec.ts +++ b/__tests__/location.spec.ts @@ -8,8 +8,7 @@ import { resolveRelativePath, } from '../src/location' import { RouteLocationNormalizedLoaded } from 'src' -import { vi, describe, expect, it } from 'vitest' -import { mockWarn } from './vitest-mock-warn' +import { mockWarn } from 'jest-mock-warn' describe('parseURL', () => { let parseURL = originalParseURL.bind(null, parseQuery) @@ -134,23 +133,8 @@ describe('parseURL', () => { }) }) - it('parses ? after the hash', () => { - expect(parseURL('/foo#?a=one')).toEqual({ - fullPath: '/foo#?a=one', - path: '/foo', - hash: '#?a=one', - query: {}, - }) - expect(parseURL('/foo/#?a=one')).toEqual({ - fullPath: '/foo/#?a=one', - path: '/foo/', - hash: '#?a=one', - query: {}, - }) - }) - it('calls parseQuery', () => { - const parseQuery = vi.fn() + const parseQuery = jest.fn() originalParseURL(parseQuery, '/?é=é&é=a') expect(parseQuery).toHaveBeenCalledTimes(1) expect(parseQuery).toHaveBeenCalledWith('é=é&é=a') @@ -215,7 +199,7 @@ describe('stringifyURL', () => { }) it('calls stringifyQuery', () => { - const stringifyQuery = vi.fn() + const stringifyQuery = jest.fn() originalStringifyURL(stringifyQuery, { path: '/', query: { é: 'é', b: 'a' }, @@ -345,28 +329,12 @@ describe('resolveRelativePath', () => { expect(resolveRelativePath('./../add', '/')).toBe('/add') expect(resolveRelativePath('../../add', '/')).toBe('/add') expect(resolveRelativePath('../../../add', '/')).toBe('/add') - expect(resolveRelativePath('a/add', '/')).toBe('/a/add') - expect(resolveRelativePath('./a/add', '/')).toBe('/a/add') - expect(resolveRelativePath('../a/add', '/')).toBe('/a/add') }) it('ignores it location is absolute', () => { expect(resolveRelativePath('/add', '/users/posva')).toBe('/add') }) - it('works without anything after the .', () => { - expect(resolveRelativePath('./', '/users/posva')).toBe('/users/') - expect(resolveRelativePath('.', '/users/posva')).toBe('/users/') - }) - - it('works without anything after the ..', () => { - expect(resolveRelativePath('../', '/users/posva')).toBe('/') - expect(resolveRelativePath('../', '/users/posva/new')).toBe('/users/') - expect(resolveRelativePath('../../', '/users/posva/a/b')).toBe('/users/') - expect(resolveRelativePath('..', '/users/posva/new')).toBe('/users/') - expect(resolveRelativePath('../..', '/users/posva/a/b')).toBe('/users/') - }) - it('resolves empty path', () => { expect(resolveRelativePath('', '/users/posva')).toBe('/users/posva') expect(resolveRelativePath('', '/users')).toBe('/users') diff --git a/__tests__/matcher/__snapshots__/resolve.spec.ts.snap b/__tests__/matcher/__snapshots__/resolve.spec.ts.snap new file mode 100644 index 000000000..d2ffdb9c2 --- /dev/null +++ b/__tests__/matcher/__snapshots__/resolve.spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RouterMatcher.resolve LocationAsName throws if the named route does not exists 1`] = ` +[Error: No match for + {"name":"Home"}] +`; + +exports[`RouterMatcher.resolve LocationAsRelative throws if the current named route does not exists 1`] = ` +[Error: No match for + {"params":{"a":"foo"}} +while being at +{"name":"home","params":{},"path":"/","meta":{}}] +`; diff --git a/packages/router/__tests__/matcher/addingRemoving.spec.ts b/__tests__/matcher/addingRemoving.spec.ts similarity index 64% rename from packages/router/__tests__/matcher/addingRemoving.spec.ts rename to __tests__/matcher/addingRemoving.spec.ts index 9633ae6cc..6130754f4 100644 --- a/packages/router/__tests__/matcher/addingRemoving.spec.ts +++ b/__tests__/matcher/addingRemoving.spec.ts @@ -1,10 +1,9 @@ import { createRouterMatcher } from '../../src/matcher' import { MatcherLocation } from '../../src/types' -import { mockWarn } from '../vitest-mock-warn' -import { describe, expect, it } from 'vitest' +import { mockWarn } from 'jest-mock-warn' const currentLocation = { path: '/' } as MatcherLocation -// @ts-expect-error +// @ts-ignore const component: RouteComponent = null describe('Matcher: adding and removing records', () => { @@ -16,21 +15,6 @@ describe('Matcher: adding and removing records', () => { }) }) - it('can remove all records', () => { - const matcher = createRouterMatcher([], {}) - matcher.addRoute({ path: '/', component }) - matcher.addRoute({ path: '/about', component, name: 'about' }) - matcher.addRoute({ - path: '/with-children', - component, - children: [{ path: 'child', component }], - }) - expect(matcher.getRoutes()).not.toHaveLength(0) - matcher.clearRoutes() - expect(matcher.getRoutes()).toHaveLength(0) - expect(matcher.getRecordMatcher('about')).toBeFalsy() - }) - it('throws when adding *', () => { const matcher = createRouterMatcher([], {}) expect(() => { @@ -404,204 +388,13 @@ describe('Matcher: adding and removing records', () => { }) }) - it('throws if a parent and child have the same name', () => { - expect(() => { - createRouterMatcher( - [ - { - path: '/', - component, - name: 'home', - children: [{ path: '/home', component, name: 'home' }], - }, - ], - {} - ) - }).toThrowError( - 'A route named "home" has been added as a child of a route with the same name' - ) - }) - - it('throws if an ancestor and descendant have the same name', () => { - const name = Symbol('home') - const matcher = createRouterMatcher( - [ - { - path: '/', - name, - children: [ - { - path: 'home', - name: 'other', - component, - }, - ], - }, - ], - {} - ) - - const parent = matcher.getRecordMatcher('other') - - expect(() => { - matcher.addRoute({ path: '', component, name }, parent) - }).toThrowError( - 'A route named "Symbol(home)" has been added as a descendant of a route with the same name' - ) - }) - - it('adds empty paths as children', () => { - const matcher = createRouterMatcher([], {}) - matcher.addRoute({ path: '/', component, name: 'parent' }) - const parent = matcher.getRecordMatcher('parent') - expect(matcher.resolve({ path: '/' }, currentLocation)).toMatchObject({ - name: 'parent', - }) - matcher.addRoute({ path: '', component, name: 'child' }, parent) - expect(matcher.resolve({ path: '/' }, currentLocation)).toMatchObject({ - name: 'child', - }) - }) - - it('adding dynamic child with root path', () => { - const matcher = createRouterMatcher([], {}) - matcher.addRoute({ path: '/parent', component, name: 'parent' }) - const parent = matcher.getRecordMatcher('parent') - expect(matcher.resolve({ path: '/parent' }, currentLocation)).toMatchObject( - { - name: 'parent', - } - ) - matcher.addRoute({ path: '/:id', component, name: 'child' }, parent) - expect(matcher.resolve({ path: '/parent' }, currentLocation)).toMatchObject( - { - name: 'parent', - } - ) - }) - describe('warnings', () => { mockWarn() - it('warns if alias is missing a required param', () => { + // TODO: add warnings for invalid records + it.skip('warns if alias is missing a required param', () => { createRouterMatcher([{ path: '/:id', alias: '/no-id', component }], {}) - expect('same param named "id"').toHaveBeenWarned() - }) - - it('does not warn for optional param on alias', () => { - createRouterMatcher( - [{ path: '/:id', alias: '/:id-:suffix?', component }], - {} - ) - expect('same param named').not.toHaveBeenWarned() - }) - - it('does not warn for optional param on main record', () => { - createRouterMatcher( - [{ alias: '/:id', path: '/:id-:suffix?', component }], - {} - ) - expect('same param named').not.toHaveBeenWarned() - }) - - it('warns if a named route has an empty non-named child route', () => { - createRouterMatcher( - [ - { - name: 'UserRoute', - path: '/user/:id', - component, - children: [{ path: '', component }], - }, - ], - {} - ) - expect('has a child without a name').toHaveBeenWarned() - }) - - it('no warn if both or just the child are named', () => { - createRouterMatcher( - [ - { - name: 'UserRoute', - path: '/user/:id', - component, - children: [{ path: '', name: 'UserHome', component }], - }, - { - path: '/', - component, - children: [{ path: '', name: 'child', component }], - }, - ], - {} - ) - expect('has a child without a name').not.toHaveBeenWarned() - }) - - it('warns if nested child is missing a name', () => { - createRouterMatcher( - [ - { - name: 'parent', - path: '/a', - component, - children: [ - { - path: 'b', - name: 'b', - component, - children: [{ path: '', component }], - }, - ], - }, - ], - {} - ) - expect('has a child without a name').toHaveBeenWarned() - }) - - it('warns if middle nested child is missing a name', () => { - createRouterMatcher( - [ - { - path: '/a', - component, - children: [ - { - path: '', - name: 'parent', - component, - children: [{ path: '', component }], - }, - ], - }, - ], - {} - ) - expect('has a child without a name').toHaveBeenWarned() - }) - - it('no warn if nested child is named', () => { - createRouterMatcher( - [ - { - name: 'parent', - path: '/a', - component, - children: [ - { - path: 'b', - name: 'b', - component, - children: [{ path: '', name: 'child', component }], - }, - ], - }, - ], - {} - ) - expect('has a child without a name').not.toHaveBeenWarned() + expect('TODO').toHaveBeenWarned() }) }) }) diff --git a/packages/router/__tests__/matcher/pathParser.spec.ts b/__tests__/matcher/pathParser.spec.ts similarity index 81% rename from packages/router/__tests__/matcher/pathParser.spec.ts rename to __tests__/matcher/pathParser.spec.ts index b7b9969b1..b897243bb 100644 --- a/packages/router/__tests__/matcher/pathParser.spec.ts +++ b/__tests__/matcher/pathParser.spec.ts @@ -1,6 +1,5 @@ import { tokenizePath, TokenType } from '../../src/matcher/pathTokenizer' import { tokensToParser } from '../../src/matcher/pathParserRanker' -import { describe, expect, it } from 'vitest' describe('Path parser', () => { describe('tokenizer', () => { @@ -148,29 +147,6 @@ describe('Path parser', () => { ]) }) - it('param custom re followed by param without regex', () => { - expect(tokenizePath('/:one(\\d+)/:two')).toEqual([ - [ - { - type: TokenType.Param, - value: 'one', - regexp: '\\d+', - repeatable: false, - optional: false, - }, - ], - [ - { - type: TokenType.Param, - value: 'two', - regexp: '', - repeatable: false, - optional: false, - }, - ], - ]) - }) - it('param custom re?', () => { expect(tokenizePath('/:id(\\d+)?')).toEqual([ [ @@ -619,83 +595,9 @@ describe('Path parser', () => { matchParams('/home', '/other/home', {}, { start: false }) }) - it('defaults to matching the end', () => { - // The default should behave like `end: true` - const optionSets = [{}, { end: true }] - - for (const options of optionSets) { - matchParams('/home', '/home', {}, options) - matchParams('/home', '/home/', {}, options) - matchParams('/home', '/home/other', null, options) - matchParams('/home', '/homepage', null, options) - - matchParams('/home/', '/home', {}, options) - matchParams('/home/', '/home/', {}, options) - matchParams('/home/', '/home/other', null, options) - matchParams('/home/', '/homepage', null, options) - } - }) - it('can not match the end', () => { - const options = { end: false } - - matchParams('/home', '/home', {}, options) - matchParams('/home', '/home/', {}, options) - matchParams('/home', '/home/other', {}, options) - matchParams('/home', '/homepage', {}, options) - - matchParams('/home/:p', '/home', null, options) - matchParams('/home/:p', '/home/', null, options) - matchParams('/home/:p', '/home/a', { p: 'a' }, options) - matchParams('/home/:p', '/home/a/', { p: 'a' }, options) - matchParams('/home/:p', '/home/a/b', { p: 'a' }, options) - matchParams('/home/:p', '/homepage', null, options) - - matchParams('/home/', '/home', {}, options) - matchParams('/home/', '/home/', {}, options) - matchParams('/home/', '/home/other', {}, options) - matchParams('/home/', '/homepage', {}, options) - - matchParams('/home/:p/', '/home', null, options) - matchParams('/home/:p/', '/home/', null, options) - matchParams('/home/:p/', '/home/a', { p: 'a' }, options) - matchParams('/home/:p/', '/home/a/', { p: 'a' }, options) - matchParams('/home/:p/', '/home/a/b', { p: 'a' }, options) - matchParams('/home/:p/', '/homepage', null, options) - }) - - it('can not match the end when strict', () => { - const options = { end: false, strict: true } - - matchParams('/home', '/home', {}, options) - matchParams('/home', '/home/', {}, options) - matchParams('/home', '/home/other', {}, options) - matchParams('/home', '/homepage', null, options) - - matchParams('/home/:p', '/home', null, options) - matchParams('/home/:p', '/home/', null, options) - matchParams('/home/:p', '/home/a', { p: 'a' }, options) - matchParams('/home/:p', '/home/a/', { p: 'a' }, options) - matchParams('/home/:p', '/home/a/b', { p: 'a' }, options) - matchParams('/home/:p', '/homepage', null, options) - - matchParams('/home/', '/home', null, options) - matchParams('/home/', '/home/', {}, options) - matchParams('/home/', '/home/other', {}, options) - matchParams('/home/', '/homepage', null, options) - - matchParams('/home/:p/', '/home', null, options) - matchParams('/home/:p/', '/home/', null, options) - matchParams('/home/:p/', '/home/a', null, options) - matchParams('/home/:p/', '/home/a/', { p: 'a' }, options) - matchParams('/home/:p/', '/home/a/b', { p: 'a' }, options) - matchParams('/home/:p/', '/homepage', null, options) - }) - - it('should not match optional params + static without leading slash', () => { - matchParams('/a/:p?-b', '/a-b', null) - matchParams('/a/:p?-b', '/a/-b', { p: '' }) - matchParams('/a/:p?-b', '/a/e-b', { p: 'e' }) + matchParams('/home', '/home/other', null, { end: true }) + matchParams('/home', '/home/other', {}, { end: false }) }) it('returns an empty object with no keys', () => { @@ -733,9 +635,9 @@ describe('Path parser', () => { }) it('catch all non-greedy', () => { - matchParams('/:rest(.*?)/b/:other(.*)', '/a/b/c/b/d', { + matchParams('/:rest(.*?)/b/:other(.*)', '/a/b/c', { rest: 'a', - other: 'c/b/d', + other: 'c', }) }) @@ -874,12 +776,6 @@ describe('Path parser', () => { matchStringify('/b-:a?/other', {}, '/b-/other') }) - it('starting optional param? with static segment should not drop the initial /', () => { - matchStringify('/a/:a?-other/other', { a: '' }, '/a/-other/other') - matchStringify('/a/:a?-other/other', {}, '/a/-other/other') - matchStringify('/a/:a?-other/other', { a: 'p' }, '/a/p-other/other') - }) - it('optional param*', () => { matchStringify('/:a*/other', { a: '' }, '/other') matchStringify('/:a*/other', { a: [] }, '/other') diff --git a/packages/router/__tests__/matcher/pathRanking.spec.ts b/__tests__/matcher/pathRanking.spec.ts similarity index 95% rename from packages/router/__tests__/matcher/pathRanking.spec.ts rename to __tests__/matcher/pathRanking.spec.ts index 230c3a182..86e9151a1 100644 --- a/packages/router/__tests__/matcher/pathRanking.spec.ts +++ b/__tests__/matcher/pathRanking.spec.ts @@ -3,7 +3,6 @@ import { tokensToParser, comparePathParserScore, } from '../../src/matcher/pathParserRanker' -import { describe, expect, it } from 'vitest' type PathParserOptions = Parameters[1] @@ -14,16 +13,18 @@ describe('Path ranking', () => { { score: a, re: /a/, - // @ts-expect-error + // @ts-ignore stringify: v => v, - // @ts-expect-error + // @ts-ignore parse: v => v, keys: [], }, { score: b, re: /a/, + // @ts-ignore stringify: v => v, + // @ts-ignore parse: v => v, keys: [], } @@ -144,15 +145,6 @@ describe('Path ranking', () => { }) }) - it('puts catchall param after same prefix', () => { - possibleOptions.forEach(options => { - checkPathOrder([ - ['/a', options], - ['/a/:a(.*)*', options], - ]) - }) - }) - it('sensitive should go before non sensitive', () => { checkPathOrder([ ['/Home', { sensitive: true }], diff --git a/packages/router/__tests__/matcher/records.spec.ts b/__tests__/matcher/records.spec.ts similarity index 95% rename from packages/router/__tests__/matcher/records.spec.ts rename to __tests__/matcher/records.spec.ts index 92d459084..698edcdfb 100644 --- a/packages/router/__tests__/matcher/records.spec.ts +++ b/__tests__/matcher/records.spec.ts @@ -1,5 +1,4 @@ import { normalizeRouteRecord } from '../../src/matcher' -import { vi, describe, expect, it } from 'vitest' describe('normalizeRouteRecord', () => { it('transforms a single view into multiple views', () => { @@ -23,7 +22,7 @@ describe('normalizeRouteRecord', () => { }) it('keeps original values in single view', () => { - const beforeEnter = vi.fn() + const beforeEnter = jest.fn() const record = normalizeRouteRecord({ path: '/home', beforeEnter, @@ -65,7 +64,7 @@ describe('normalizeRouteRecord', () => { }) it('keeps original values in multiple views', () => { - const beforeEnter = vi.fn() + const beforeEnter = jest.fn() const record = normalizeRouteRecord({ path: '/home', beforeEnter, diff --git a/packages/router/__tests__/matcher/resolve.spec.ts b/__tests__/matcher/resolve.spec.ts similarity index 89% rename from packages/router/__tests__/matcher/resolve.spec.ts rename to __tests__/matcher/resolve.spec.ts index 2b9c4e7ae..1a5b1edea 100644 --- a/packages/router/__tests__/matcher/resolve.spec.ts +++ b/__tests__/matcher/resolve.spec.ts @@ -1,24 +1,21 @@ import { createRouterMatcher, normalizeRouteRecord } from '../../src/matcher' import { + START_LOCATION_NORMALIZED, RouteComponent, RouteRecordRaw, MatcherLocationRaw, MatcherLocation, } from '../../src/types' import { MatcherLocationNormalizedLoose } from '../utils' -import { defineComponent } from 'vue' -import { START_LOCATION_NORMALIZED } from '../../src/location' -import { mockWarn } from '../vitest-mock-warn' -import { describe, expect, it } from 'vitest' +import { mockWarn } from 'jest-mock-warn' -const component: RouteComponent = defineComponent({}) +// @ts-ignore +const component: RouteComponent = null // for normalized records const components = { default: component } describe('RouterMatcher.resolve', () => { - mockWarn() - function assertRecordMatch( record: RouteRecordRaw | RouteRecordRaw[], location: MatcherLocationRaw, @@ -45,7 +42,9 @@ describe('RouterMatcher.resolve', () => { throw new Error('not handled') } else { // use one single record - if (!resolved.matched) resolved.matched = record.map(normalizeRouteRecord) + if (!resolved.matched) + // @ts-ignore + resolved.matched = record.map(normalizeRouteRecord) // allow passing an expect.any(Array) else if (Array.isArray(resolved.matched)) resolved.matched = resolved.matched.map(m => ({ @@ -76,7 +75,7 @@ describe('RouterMatcher.resolve', () => { /** * * @param record - Record or records we are testing the matcher against - * @param location - location we want to resolve against + * @param location - location we want to reolve against * @param [start] Optional currentLocation used when resolving * @returns error */ @@ -694,7 +693,8 @@ describe('RouterMatcher.resolve', () => { ) }) - it('keeps required trailing slash (strict: true)', () => { + // FIXME: + it.skip('keeps required trailing slash (strict: true)', () => { const record = { path: '/home/', name: 'Home', @@ -778,55 +778,6 @@ describe('RouterMatcher.resolve', () => { ) }) - it('keep optional params from parent record', () => { - const Child_A = { path: 'a', name: 'child_a', components } - const Child_B = { path: 'b', name: 'child_b', components } - const Parent = { - path: '/:optional?/parent', - name: 'parent', - components, - children: [Child_A, Child_B], - } - assertRecordMatch( - Parent, - { name: 'child_b' }, - { - name: 'child_b', - path: '/foo/parent/b', - params: { optional: 'foo' }, - matched: [ - Parent as any, - { - ...Child_B, - path: `${Parent.path}/${Child_B.path}`, - }, - ], - }, - { - params: { optional: 'foo' }, - path: '/foo/parent/a', - matched: [], - meta: {}, - name: undefined, - } - ) - }) - - it('discards non existent params', () => { - assertRecordMatch( - { path: '/', name: 'home', components }, - { name: 'home', params: { a: 'a', b: 'b' } }, - { name: 'home', path: '/', params: {} } - ) - expect('invalid param(s) "a", "b" ').toHaveBeenWarned() - assertRecordMatch( - { path: '/:b', name: 'a', components }, - { name: 'a', params: { a: 'a', b: 'b' } }, - { name: 'a', path: '/b', params: { b: 'b' } } - ) - expect('invalid param(s) "a"').toHaveBeenWarned() - }) - it('drops optional params', () => { assertRecordMatch( { path: '/:a/:b?', name: 'p', components }, @@ -841,37 +792,10 @@ describe('RouterMatcher.resolve', () => { } ) }) - - it('keeps optional params passed as empty strings', () => { - assertRecordMatch( - { path: '/:a/:b?', name: 'p', components }, - { name: 'p', params: { a: 'b', b: '' } }, - { name: 'p', path: '/b', params: { a: 'b', b: '' } }, - { - params: { a: 'a', b: '' }, - path: '/a', - matched: [], - meta: {}, - name: undefined, - } - ) - }) - - it('resolves root path with optional params', () => { - assertRecordMatch( - { path: '/:tab?', name: 'h', components }, - { name: 'h' }, - { name: 'h', path: '/', params: {} } - ) - assertRecordMatch( - { path: '/:tab?/:other?', name: 'h', components }, - { name: 'h' }, - { name: 'h', path: '/', params: {} } - ) - }) }) describe('LocationAsRelative', () => { + mockWarn() it('warns if a path isn not absolute', () => { const record = { path: '/parent', @@ -1054,44 +978,6 @@ describe('RouterMatcher.resolve', () => { ) ).toMatchSnapshot() }) - - it('avoids records with children without a component nor name', () => { - assertErrorMatch( - { - path: '/articles', - children: [{ path: ':id', components }], - }, - { path: '/articles' } - ) - }) - - it('avoid deeply nested records with children without a component nor name', () => { - assertErrorMatch( - { - path: '/app', - components, - children: [ - { - path: '/articles', - children: [{ path: ':id', components }], - }, - ], - }, - { path: '/articles' } - ) - }) - - it('can reach a named route with children and no component if named', () => { - assertRecordMatch( - { - path: '/articles', - name: 'ArticlesParent', - children: [{ path: ':id', components }], - }, - { name: 'ArticlesParent' }, - { name: 'ArticlesParent', path: '/articles' } - ) - }) }) describe('children', () => { @@ -1154,7 +1040,7 @@ describe('RouterMatcher.resolve', () => { name: 'nested', path: '/foo', params: {}, - matched: [Foo as any, { ...Nested, path: `${Foo.path}` }], + matched: [Foo, { ...Nested, path: `${Foo.path}` }], } ) }) @@ -1181,7 +1067,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo', params: {}, matched: [ - Foo as any, + Foo, { ...Nested, path: `${Foo.path}` }, { ...NestedNested, path: `${Foo.path}` }, ], @@ -1204,7 +1090,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/a', params: {}, matched: [ - Foo as any, + Foo, { ...Nested, path: `${Foo.path}/${Nested.path}` }, { ...NestedChildA, @@ -1230,7 +1116,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/a', params: {}, matched: [ - Foo as any, + Foo, { ...Nested, path: `${Foo.path}/${Nested.path}` }, { ...NestedChildA, @@ -1256,7 +1142,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/a', params: {}, matched: [ - Foo as any, + Foo, { ...Nested, path: `${Foo.path}/${Nested.path}` }, { ...NestedChildA, @@ -1289,7 +1175,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/a/b', params: { p: 'b', n: 'a' }, matched: [ - Foo as any, + Foo, { ...NestedWithParam, path: `${Foo.path}/${NestedWithParam.path}`, @@ -1318,7 +1204,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/b/a', params: { p: 'a', n: 'b' }, matched: [ - Foo as any, + Foo, { ...NestedWithParam, path: `${Foo.path}/${NestedWithParam.path}`, @@ -1366,7 +1252,7 @@ describe('RouterMatcher.resolve', () => { name: 'nested', path: '/nested', params: {}, - matched: [Parent as any, { ...Nested, path: `/nested` }], + matched: [Parent, { ...Nested, path: `/nested` }], } ) }) @@ -1386,7 +1272,7 @@ describe('RouterMatcher.resolve', () => { name: 'nested', path: '/parent/nested', params: {}, - matched: [Parent as any, { ...Nested, path: `/parent/nested` }], + matched: [Parent, { ...Nested, path: `/parent/nested` }], } ) }) diff --git a/__tests__/mount.ts b/__tests__/mount.ts new file mode 100644 index 000000000..41976e433 --- /dev/null +++ b/__tests__/mount.ts @@ -0,0 +1,213 @@ +import { + createApp, + defineComponent, + h, + ComponentPublicInstance, + reactive, + nextTick, + ComponentObjectPropsOptions, + ComputedRef, + computed, + App, + VNode, + shallowRef, + ComponentOptions, +} from 'vue' +import { compile } from '@vue/compiler-dom' +import * as runtimeDom from '@vue/runtime-dom' +import { RouteLocationNormalizedLoose } from './utils' +import { + routeLocationKey, + routerViewLocationKey, +} from '../src/injectionSymbols' +import { Router } from '../src' + +export interface MountOptions { + propsData: Record + provide: Record + components: ComponentOptions['components'] + slots: Record + router?: Router +} + +interface Wrapper { + app: App + vm: ComponentPublicInstance + rootEl: HTMLDivElement + setProps(props: MountOptions['propsData']): Promise + html(): string + find: typeof document['querySelector'] +} + +function initialProps

(propsOption: ComponentObjectPropsOptions

) { + let copy = {} as ComponentPublicInstance['$props'] + + for (let key in propsOption) { + const prop = propsOption[key]! + // @ts-ignore + if (!prop.required && prop.default) + // @ts-ignore + copy[key] = prop.default + } + + return copy +} + +// cleanup wrappers after a suite runs +let activeWrapperRemovers: Array<() => void> = [] +afterAll(() => { + activeWrapperRemovers.forEach(remove => remove()) + activeWrapperRemovers = [] +}) + +export function mount( + targetComponent: Parameters[0], + options: Partial = {} +): Promise { + const TargetComponent = targetComponent + return new Promise(resolve => { + // NOTE: only supports props as an object + const propsData = reactive( + Object.assign( + initialProps( + // @ts-ignore + TargetComponent.props || {} + ), + options.propsData + ) + ) + + function setProps(partialProps: Record) { + Object.assign(propsData, partialProps) + return nextTick() + } + + let slots: Record VNode> = {} + + const Wrapper = defineComponent({ + setup(_props, { emit }) { + const componentInstanceRef = shallowRef() + + return () => { + return h( + TargetComponent as any, + { + ref: componentInstanceRef, + onVnodeMounted() { + emit('ready', componentInstanceRef.value) + }, + ...propsData, + }, + slots + ) + } + }, + }) + + const app = createApp(Wrapper, { + onReady: (instance: ComponentPublicInstance) => { + resolve({ app, vm: instance!, rootEl, setProps, html, find }) + }, + }) + + if (options.provide) { + const keys = getKeys(options.provide) + + for (let key of keys) { + app.provide(key, options.provide[key as any]) + } + } + + if (options.components) { + for (let key in options.components) { + app.component(key, options.components[key]) + } + } + + if (options.slots) { + for (let key in options.slots) { + slots[key] = compileSlot(options.slots[key]) + } + } + + const rootEl = document.createElement('div') + document.body.appendChild(rootEl) + + function html() { + return rootEl.innerHTML + } + + function find(selector: string) { + return rootEl.querySelector(selector) + } + + if (options.router) app.use(options.router) + + app.mount(rootEl) + + activeWrapperRemovers.push(() => { + app.unmount(rootEl) + rootEl.remove() + }) + }) +} + +function getKeys(object: Record): Array { + return (Object.getOwnPropertyNames(object) as Array).concat( + Object.getOwnPropertySymbols(object) + ) +} + +export function createMockedRoute(initialValue: RouteLocationNormalizedLoose) { + const route = {} as { + [k in keyof RouteLocationNormalizedLoose]: ComputedRef< + RouteLocationNormalizedLoose[k] + > + } + + const routeRef = shallowRef(initialValue) + + function set(newRoute: RouteLocationNormalizedLoose) { + routeRef.value = newRoute + return nextTick() + } + + for (let key in initialValue) { + // @ts-ignore + route[key] = + // new line to still get errors here + computed(() => routeRef.value[key as keyof RouteLocationNormalizedLoose]) + } + + const value = reactive(route) + + return { + value, + set, + provides: { + [routeLocationKey as symbol]: value, + [routerViewLocationKey as symbol]: routeRef, + }, + } +} + +function compileSlot(template: string) { + const codegen = compile(template, { + mode: 'function', + hoistStatic: true, + prefixIdentifiers: true, + }) + + const render = new Function('Vue', codegen.code)(runtimeDom) + + const ToRender = defineComponent({ + render, + inheritAttrs: false, + + setup(props, { attrs }) { + return { ...attrs } + }, + }) + + return (propsData: any) => h(ToRender, { ...propsData }) +} diff --git a/packages/router/__tests__/multipleApps.spec.ts b/__tests__/multipleApps.spec.ts similarity index 91% rename from packages/router/__tests__/multipleApps.spec.ts rename to __tests__/multipleApps.spec.ts index 664cd08af..1158048e8 100644 --- a/packages/router/__tests__/multipleApps.spec.ts +++ b/__tests__/multipleApps.spec.ts @@ -1,7 +1,7 @@ import { createRouter, createMemoryHistory } from '../src' import { h } from 'vue' import { createDom } from './utils' -import { vi, describe, expect, it, beforeAll } from 'vitest' +// import { mockWarn } from 'jest-mock-warn' const delay = (t: number) => new Promise(resolve => setTimeout(resolve, t)) @@ -34,7 +34,7 @@ describe('Multiple apps', () => { it('does not listen to url changes before being ready', async () => { const { router, history } = newRouter() - const spy = vi.fn((to, from, next) => { + const spy = jest.fn((to, from, next) => { next() }) router.beforeEach(spy) diff --git a/packages/router/__tests__/parseQuery.spec.ts b/__tests__/parseQuery.spec.ts similarity index 95% rename from packages/router/__tests__/parseQuery.spec.ts rename to __tests__/parseQuery.spec.ts index 031c3932c..b16c86245 100644 --- a/packages/router/__tests__/parseQuery.spec.ts +++ b/__tests__/parseQuery.spec.ts @@ -1,6 +1,5 @@ import { parseQuery } from '../src/query' -import { mockWarn } from './vitest-mock-warn' -import { describe, expect, it } from 'vitest' +import { mockWarn } from 'jest-mock-warn' describe('parseQuery', () => { mockWarn() diff --git a/packages/router/__tests__/router.spec.ts b/__tests__/router.spec.ts similarity index 77% rename from packages/router/__tests__/router.spec.ts rename to __tests__/router.spec.ts index bf11f31ba..84d420683 100644 --- a/packages/router/__tests__/router.spec.ts +++ b/__tests__/router.spec.ts @@ -4,15 +4,15 @@ import { createMemoryHistory, createWebHistory, createWebHashHistory, - loadRouteLocation, - RouteLocationRaw, } from '../src' import { NavigationFailureType } from '../src/errors' import { createDom, components, tick, nextNavigation } from './utils' -import { RouteRecordRaw } from '../src/types' -import { START_LOCATION_NORMALIZED } from '../src/location' -import { vi, describe, expect, it, beforeAll } from 'vitest' -import { mockWarn } from './vitest-mock-warn' +import { + RouteRecordRaw, + RouteLocationRaw, + START_LOCATION_NORMALIZED, +} from '../src/types' +import { mockWarn } from 'jest-mock-warn' declare var __DEV__: boolean @@ -31,13 +31,10 @@ const routes: RouteRecordRaw[] = [ { path: '/to-foo', redirect: '/foo' }, { path: '/to-foo-named', redirect: { name: 'Foo' } }, { path: '/to-foo2', redirect: '/to-foo' }, - { path: '/to-foo-query', redirect: '/foo?a=2#b' }, { path: '/to-p/:p', redirect: { name: 'Param' } }, { path: '/p/:p', name: 'Param', component: components.Bar }, - { path: '/optional/:p?', name: 'optional', component: components.Bar }, { path: '/repeat/:r+', name: 'repeat', component: components.Bar }, { path: '/to-p/:p', redirect: to => `/p/${to.params.p}` }, - { path: '/redirect-with-param/:p', redirect: () => `/` }, { path: '/before-leave', component: components.BeforeLeave }, { path: '/parent', @@ -95,13 +92,6 @@ describe('Router', () => { createDom() }) - it('fails if history option is missing', () => { - // @ts-expect-error - expect(() => createRouter({ routes })).toThrowError( - 'Provide the "history" option' - ) - }) - it('starts at START_LOCATION', () => { const history = createMemoryHistory() const router = createRouter({ history, routes }) @@ -110,7 +100,7 @@ describe('Router', () => { it('calls history.push with router.push', async () => { const { router, history } = await newRouter() - vi.spyOn(history, 'push') + jest.spyOn(history, 'push') await router.push('/foo') expect(history.push).toHaveBeenCalledTimes(1) expect(history.push).toHaveBeenCalledWith('/foo', undefined) @@ -119,79 +109,35 @@ describe('Router', () => { it('calls history.replace with router.replace', async () => { const history = createMemoryHistory() const { router } = await newRouter({ history }) - vi.spyOn(history, 'replace') + jest.spyOn(history, 'replace') await router.replace('/foo') expect(history.replace).toHaveBeenCalledTimes(1) expect(history.replace).toHaveBeenCalledWith('/foo', expect.anything()) }) - it('parses query and hash with router.replace', async () => { - const history = createMemoryHistory() - const { router } = await newRouter({ history }) - vi.spyOn(history, 'replace') - await router.replace('/foo?q=2#a') - expect(history.replace).toHaveBeenCalledTimes(1) - expect(history.replace).toHaveBeenCalledWith( - '/foo?q=2#a', - expect.anything() - ) - }) - it('replaces if a guard redirects', async () => { const history = createMemoryHistory() const { router } = await newRouter({ history }) // move somewhere else await router.push('/search') - vi.spyOn(history, 'replace') - vi.spyOn(history, 'push') + jest.spyOn(history, 'replace') await router.replace('/home-before') - expect(history.push).toHaveBeenCalledTimes(0) expect(history.replace).toHaveBeenCalledTimes(1) expect(history.replace).toHaveBeenCalledWith('/', expect.anything()) }) - it('replaces if a guard redirect replaces', async () => { - const history = createMemoryHistory() - const { router } = await newRouter({ history }) - // move somewhere else - router.beforeEach(to => { - if (to.name !== 'Foo') { - return { name: 'Foo', replace: true } - } - return // no warn - }) - vi.spyOn(history, 'replace') - vi.spyOn(history, 'push') - await router.push('/search') - expect(history.location).toBe('/foo') - expect(history.push).toHaveBeenCalledTimes(0) - expect(history.replace).toHaveBeenCalledTimes(1) - expect(history.replace).toHaveBeenCalledWith('/foo', expect.anything()) - }) - it('allows to customize parseQuery', async () => { - const parseQuery = vi.fn(_ => ({})) + const parseQuery = jest.fn() const { router } = await newRouter({ parseQuery }) - const to = router.resolve('/foo?bar=baz') + router.resolve('/foo?bar=baz') expect(parseQuery).toHaveBeenCalledWith('bar=baz') - expect(to.query).toEqual({}) }) it('allows to customize stringifyQuery', async () => { - const stringifyQuery = vi.fn(_ => '') + const stringifyQuery = jest.fn() const { router } = await newRouter({ stringifyQuery }) - const to = router.resolve({ query: { foo: 'bar' } }) + router.resolve({ query: { foo: 'bar' } }) expect(stringifyQuery).toHaveBeenCalledWith({ foo: 'bar' }) - expect(to.query).toEqual({ foo: 'bar' }) - expect(to.fullPath).toBe('/') - }) - - it('creates an empty query with no query', async () => { - const stringifyQuery = vi.fn(_ => '') - const { router } = await newRouter({ stringifyQuery }) - const to = router.resolve({ hash: '#a' }) - expect(stringifyQuery).not.toHaveBeenCalled() - expect(to.query).toEqual({}) }) it('merges meta properties from parent to child', async () => { @@ -204,30 +150,6 @@ describe('Router', () => { }) }) - it('merges meta properties from component-less route records', async () => { - const { router } = await newRouter() - router.addRoute({ - meta: { parent: true }, - path: '/app', - children: [ - { path: '', component: components.Foo, meta: { child: true } }, - { - path: 'nested', - component: components.Foo, - children: [ - { path: 'a', children: [{ path: 'b', component: components.Foo }] }, - ], - }, - ], - }) - expect(router.resolve('/app')).toMatchObject({ - meta: { parent: true, child: true }, - }) - expect(router.resolve('/app/nested/a/b')).toMatchObject({ - meta: { parent: true }, - }) - }) - it('can do initial navigation to /', async () => { const router = createRouter({ history: createMemoryHistory(), @@ -255,7 +177,7 @@ describe('Router', () => { it('can pass replace option to push', async () => { const { router, history } = await newRouter() - vi.spyOn(history, 'replace') + jest.spyOn(history, 'replace') await router.push({ path: '/foo', replace: true }) expect(history.replace).toHaveBeenCalledTimes(1) expect(history.replace).toHaveBeenCalledWith('/foo', expect.anything()) @@ -263,7 +185,7 @@ describe('Router', () => { it('can replaces current location with a string location', async () => { const { router, history } = await newRouter() - vi.spyOn(history, 'replace') + jest.spyOn(history, 'replace') await router.replace('/foo') expect(history.replace).toHaveBeenCalledTimes(1) expect(history.replace).toHaveBeenCalledWith('/foo', expect.anything()) @@ -271,7 +193,7 @@ describe('Router', () => { it('can replaces current location with an object location', async () => { const { router, history } = await newRouter() - vi.spyOn(history, 'replace') + jest.spyOn(history, 'replace') await router.replace({ path: '/foo' }) expect(history.replace).toHaveBeenCalledTimes(1) expect(history.replace).toHaveBeenCalledWith('/foo', expect.anything()) @@ -279,7 +201,7 @@ describe('Router', () => { it('navigates if the location does not exist', async () => { const { router } = await newRouter({ routes: [routes[0]] }) - const spy = vi.fn((to, from, next) => next()) + const spy = jest.fn((to, from, next) => next()) router.beforeEach(spy) await router.push('/idontexist') expect(spy).toHaveBeenCalledTimes(1) @@ -297,73 +219,6 @@ describe('Router', () => { expect(router.currentRoute.value).toMatchObject({ params: { p: '0' } }) }) - it('removes null/undefined params', async () => { - const { router } = await newRouter() - - const route1 = router.resolve({ - name: 'optional', - params: { p: undefined }, - }) - expect(route1.path).toBe('/optional') - expect(route1.params).toEqual({}) - - const route2 = router.resolve({ - name: 'optional', - params: { p: null }, - }) - expect(route2.path).toBe('/optional') - expect(route2.params).toEqual({}) - - await router.push({ name: 'optional', params: { p: null } }) - expect(router.currentRoute.value.params).toEqual({}) - await router.push({ name: 'optional', params: {} }) - }) - - it('handles undefined path', async () => { - const { router } = await newRouter() - - const route1 = router.resolve({ - path: undefined, - params: { p: 'a' }, - }) - expect(route1.path).toBe('/') - expect(route1.params).toEqual({ p: 'a' }) - }) - - it('warns on undefined location during dev', async () => { - const { router } = await newRouter() - - const route1 = router.resolve(undefined as any) - expect('router.resolve() was passed an invalid location').toHaveBeenWarned() - expect(route1.path).toBe('/') - }) - - it('warns on null location during dev', async () => { - const { router } = await newRouter() - - const route1 = router.resolve(null as any) - expect('router.resolve() was passed an invalid location').toHaveBeenWarned() - expect(route1.path).toBe('/') - }) - - it('removes null/undefined optional params when current location has it', async () => { - const { router } = await newRouter() - - await router.push({ name: 'optional', params: { p: 'a' } }) - await router.push({ name: 'optional', params: { p: null } }) - expect(router.currentRoute.value.params).toEqual({}) - - await router.push({ name: 'optional', params: { p: 'a' } }) - await router.push({ name: 'optional', params: { p: undefined } }) - expect(router.currentRoute.value.params).toEqual({}) - }) - - it('keeps empty strings in optional params', async () => { - const { router } = await newRouter() - const route1 = router.resolve({ name: 'optional', params: { p: '' } }) - expect(route1.params).toEqual({ p: '' }) - }) - it('navigates to same route record but different query', async () => { const { router } = await newRouter() await router.push('/?q=1') @@ -460,24 +315,6 @@ describe('Router', () => { }) }) - it('can pass a currentLocation to resolve', async () => { - const { router } = await newRouter() - expect(router.resolve({ params: { p: 1 } })).toMatchObject({ - path: '/', - }) - expect( - router.resolve( - { params: { p: 1 } }, - await loadRouteLocation( - router.resolve({ name: 'Param', params: { p: 2 } }) - ) - ) - ).toMatchObject({ - name: 'Param', - params: { p: '1' }, - }) - }) - it('resolves relative locations', async () => { const { router } = await newRouter() await router.push('/users/posva') @@ -496,15 +333,12 @@ describe('Router', () => { await router.push('/users/posva') await router.push('../../../add') expect(router.currentRoute.value.path).toBe('/add') - await router.push('/users/posva') - await router.push('../') - expect(router.currentRoute.value.path).toBe('/') }) describe('alias', () => { it('does not navigate to alias if already on original record', async () => { const { router } = await newRouter() - const spy = vi.fn((to, from, next) => next()) + const spy = jest.fn((to, from, next) => next()) await router.push('/basic') router.beforeEach(spy) await router.push('/basic-alias') @@ -513,7 +347,7 @@ describe('Router', () => { it('does not navigate to alias with children if already on original record', async () => { const { router } = await newRouter() - const spy = vi.fn((to, from, next) => next()) + const spy = jest.fn((to, from, next) => next()) await router.push('/aliases') router.beforeEach(spy) await router.push('/aliases1') @@ -524,7 +358,7 @@ describe('Router', () => { it('does not navigate to child alias if already on original record', async () => { const { router } = await newRouter() - const spy = vi.fn((to, from, next) => next()) + const spy = jest.fn((to, from, next) => next()) await router.push('/aliases/one') router.beforeEach(spy) await router.push('/aliases1/one') @@ -536,24 +370,6 @@ describe('Router', () => { }) }) - it('should be able to resolve a partially updated location', async () => { - const { router } = await newRouter() - expect( - router.resolve({ - // spread the current location - ...router.currentRoute.value, - // then update some stuff, creating inconsistencies, - query: { a: '1' }, - hash: '#a', - }) - ).toMatchObject({ - query: { a: '1' }, - path: '/', - fullPath: '/?a=1#a', - hash: '#a', - }) - }) - describe('navigation cancelled', () => { async function checkNavigationCancelledOnPush( target?: RouteLocationRaw | false @@ -699,7 +515,7 @@ describe('Router', () => { it('only triggers guards once with a redirect option', async () => { const history = createMemoryHistory() const router = createRouter({ history, routes }) - const spy = vi.fn((to, from, next) => next()) + const spy = jest.fn((to, from, next) => next()) router.beforeEach(spy) await router.push('/to-foo') expect(spy).toHaveBeenCalledTimes(1) @@ -721,22 +537,6 @@ describe('Router', () => { }) }) - it('handles query and hash passed in redirect string', async () => { - const history = createMemoryHistory() - const router = createRouter({ history, routes }) - await expect(router.push('/to-foo-query')).resolves.toEqual(undefined) - expect(router.currentRoute.value).toMatchObject({ - name: 'Foo', - path: '/foo', - params: {}, - query: { a: '2' }, - hash: '#b', - redirectedFrom: expect.objectContaining({ - fullPath: '/to-foo-query', - }), - }) - }) - it('keeps query and hash when redirect is a string', async () => { const history = createMemoryHistory() const router = createRouter({ history, routes }) @@ -772,23 +572,6 @@ describe('Router', () => { }) }) - it('discard params on string redirect', async () => { - const history = createMemoryHistory() - const router = createRouter({ history, routes }) - await expect(router.push('/redirect-with-param/test')).resolves.toEqual( - undefined - ) - expect(router.currentRoute.value).toMatchObject({ - params: {}, - query: {}, - hash: '', - redirectedFrom: expect.objectContaining({ - fullPath: '/redirect-with-param/test', - params: { p: 'test' }, - }), - }) - }) - it('allows object in redirect', async () => { const history = createMemoryHistory() const router = createRouter({ history, routes }) @@ -862,7 +645,7 @@ describe('Router', () => { }) }) - // https://github.com/vuejs/router/issues/404 + // https://github.com/vuejs/vue-router-next/issues/404 it('works with named routes', async () => { const history = createMemoryHistory() const router = createRouter({ @@ -1112,22 +895,5 @@ describe('Router', () => { name: 'Param', }) }) - - it('warns when the parent route is missing', async () => { - const { router } = await newRouter() - router.addRoute('parent-route', { - path: '/p', - component: components.Foo, - }) - expect( - 'Parent route "parent-route" not found when adding child route' - ).toHaveBeenWarned() - }) - - it('warns when removing a missing route', async () => { - const { router } = await newRouter() - router.removeRoute('route-name') - expect('Cannot remove non-existent route "route-name"').toHaveBeenWarned() - }) }) }) diff --git a/packages/router/__tests__/scrollBehavior.spec.ts b/__tests__/scrollBehavior.spec.ts similarity index 89% rename from packages/router/__tests__/scrollBehavior.spec.ts rename to __tests__/scrollBehavior.spec.ts index bdd42a3e5..96b5dd1c1 100644 --- a/packages/router/__tests__/scrollBehavior.spec.ts +++ b/__tests__/scrollBehavior.spec.ts @@ -1,30 +1,20 @@ import { JSDOM } from 'jsdom' import { scrollToPosition } from '../src/scrollBehavior' import { createDom } from './utils' -import { mockWarn } from './vitest-mock-warn' -import { - vi, - describe, - expect, - it, - beforeEach, - afterAll, - beforeAll, - MockInstance, -} from 'vitest' +import { mockWarn } from 'jest-mock-warn' describe('scrollBehavior', () => { mockWarn() let dom: JSDOM - let scrollTo: MockInstance - let getElementById: MockInstance - let querySelector: MockInstance + let scrollTo: jest.SpyInstance + let getElementById: jest.SpyInstance + let querySelector: jest.SpyInstance beforeAll(() => { dom = createDom() - scrollTo = vi.spyOn(window, 'scrollTo').mockImplementation(() => {}) - getElementById = vi.spyOn(document, 'getElementById') - querySelector = vi.spyOn(document, 'querySelector') + scrollTo = jest.spyOn(window, 'scrollTo').mockImplementation(() => {}) + getElementById = jest.spyOn(document, 'getElementById') + querySelector = jest.spyOn(document, 'querySelector') // #text let el = document.createElement('div') @@ -80,7 +70,7 @@ describe('scrollBehavior', () => { it('scrolls to a position', () => { scrollToPosition({ left: 10, top: 100 }) expect(getElementById).not.toHaveBeenCalled() - expect(querySelector).not.toHaveBeenCalled() + expect(getElementById).not.toHaveBeenCalled() expect(scrollTo).toHaveBeenCalledWith({ left: 10, top: 100, @@ -91,7 +81,7 @@ describe('scrollBehavior', () => { it('scrolls to a partial position top', () => { scrollToPosition({ top: 10 }) expect(getElementById).not.toHaveBeenCalled() - expect(querySelector).not.toHaveBeenCalled() + expect(getElementById).not.toHaveBeenCalled() expect(scrollTo).toHaveBeenCalledWith({ top: 10, behavior: undefined, @@ -101,7 +91,7 @@ describe('scrollBehavior', () => { it('scrolls to a partial position left', () => { scrollToPosition({ left: 10 }) expect(getElementById).not.toHaveBeenCalled() - expect(querySelector).not.toHaveBeenCalled() + expect(getElementById).not.toHaveBeenCalled() expect(scrollTo).toHaveBeenCalledWith({ left: 10, behavior: undefined, diff --git a/packages/router/__tests__/ssr.spec.ts b/__tests__/ssr.spec.ts similarity index 96% rename from packages/router/__tests__/ssr.spec.ts rename to __tests__/ssr.spec.ts index 65091620f..660c1ae02 100644 --- a/packages/router/__tests__/ssr.spec.ts +++ b/__tests__/ssr.spec.ts @@ -1,5 +1,5 @@ /** - * @vitest-environment node + * @jest-environment node */ import { createRouter, createMemoryHistory } from '../src' import { createSSRApp, resolveComponent, Component } from 'vue' @@ -8,7 +8,6 @@ import { ssrInterpolate, ssrRenderComponent, } from '@vue/server-renderer' -import { describe, expect, it } from 'vitest' const delay = (t: number) => new Promise(resolve => setTimeout(resolve, t)) diff --git a/packages/router/__tests__/stringifyQuery.spec.ts b/__tests__/stringifyQuery.spec.ts similarity index 72% rename from packages/router/__tests__/stringifyQuery.spec.ts rename to __tests__/stringifyQuery.spec.ts index 16a9703d4..c39d1b4e8 100644 --- a/packages/router/__tests__/stringifyQuery.spec.ts +++ b/__tests__/stringifyQuery.spec.ts @@ -1,6 +1,5 @@ import { stringifyQuery } from '../src/query' -import { mockWarn } from './vitest-mock-warn' -import { describe, expect, it } from 'vitest' +import { mockWarn } from 'jest-mock-warn' describe('stringifyQuery', () => { mockWarn() @@ -29,19 +28,6 @@ describe('stringifyQuery', () => { expect(stringifyQuery({ e: undefined, b: 'a' })).toEqual('b=a') }) - it('avoids trailing &', () => { - expect(stringifyQuery({ a: 'a', b: undefined })).toEqual('a=a') - expect(stringifyQuery({ a: 'a', c: [] })).toEqual('a=a') - }) - - it('skips undefined in arrays', () => { - expect(stringifyQuery({ a: [undefined, '3'] })).toEqual('a=3') - expect(stringifyQuery({ a: [1, undefined, '3'] })).toEqual('a=1&a=3') - expect(stringifyQuery({ a: [1, undefined, '3', undefined] })).toEqual( - 'a=1&a=3' - ) - }) - it('stringifies arrays', () => { expect(stringifyQuery({ e: ['b', 'a'] })).toEqual('e=b&e=a') }) diff --git a/packages/router/__tests__/urlEncoding.spec.ts b/__tests__/urlEncoding.spec.ts similarity index 85% rename from packages/router/__tests__/urlEncoding.spec.ts rename to __tests__/urlEncoding.spec.ts index fd83e1a07..5c34f0bc6 100644 --- a/packages/router/__tests__/urlEncoding.spec.ts +++ b/__tests__/urlEncoding.spec.ts @@ -3,9 +3,8 @@ import { components } from './utils' import { RouteRecordRaw } from '../src/types' import { createMemoryHistory } from '../src' import * as encoding from '../src/encoding' -import { vi, describe, expect, it, beforeEach } from 'vitest' -vi.mock('../src/encoding') +jest.mock('../src/encoding') const routes: RouteRecordRaw[] = [ { path: '/', name: 'home', component: components.Home }, @@ -26,11 +25,11 @@ describe('URL Encoding', () => { beforeEach(() => { // mock all encoding functions for (const key in encoding) { - // @ts-expect-error + // @ts-ignore const value = encoding[key] - // @ts-expect-error - if (typeof value === 'function') encoding[key] = vi.fn((v: string) => v) - // @ts-expect-error + // @ts-ignore + if (typeof value === 'function') encoding[key] = jest.fn((v: string) => v) + // @ts-ignore else if (key === 'PLUS_RE') encoding[key] = /\+/g } }) @@ -69,23 +68,23 @@ describe('URL Encoding', () => { const router = createRouter() await router.push('/p/foo') // one extra time for hash - expect(encoding.decode).toHaveBeenCalledTimes(3) - expect(encoding.decode).toHaveBeenNthCalledWith(2, 'foo') + expect(encoding.decode).toHaveBeenCalledTimes(2) + expect(encoding.decode).toHaveBeenNthCalledWith(1, 'foo') }) it('calls decode with a path with repeatable params', async () => { const router = createRouter() await router.push('/p/foo/bar') // one extra time for hash - expect(encoding.decode).toHaveBeenCalledTimes(4) - expect(encoding.decode).toHaveBeenNthCalledWith(2, 'foo', 0, ['foo', 'bar']) - expect(encoding.decode).toHaveBeenNthCalledWith(3, 'bar', 1, ['foo', 'bar']) + expect(encoding.decode).toHaveBeenCalledTimes(3) + expect(encoding.decode).toHaveBeenNthCalledWith(1, 'foo', 0, ['foo', 'bar']) + expect(encoding.decode).toHaveBeenNthCalledWith(2, 'bar', 1, ['foo', 'bar']) }) it('decodes values in params', async () => { - // @ts-expect-error: override to make the difference + // @ts-ignore: override to make the difference encoding.decode = () => 'd' - // @ts-expect-error + // @ts-ignore encoding.encodeParam = () => 'e' const router = createRouter() await router.push({ name: 'optional', params: { a: 'a%' } }) @@ -109,7 +108,7 @@ describe('URL Encoding', () => { const router = createRouter() await router.push('/?p=foo') // one extra time for hash - expect(encoding.decode).toHaveBeenCalledTimes(4) + expect(encoding.decode).toHaveBeenCalledTimes(3) expect(encoding.decode).toHaveBeenNthCalledWith(1, 'p') expect(encoding.decode).toHaveBeenNthCalledWith(2, 'foo') }) @@ -125,11 +124,11 @@ describe('URL Encoding', () => { }) it('keeps decoded values in query', async () => { - // @ts-expect-error: override to make the difference + // @ts-ignore: override to make the difference encoding.decode = () => 'd' - // @ts-expect-error + // @ts-ignore encoding.encodeQueryValue = () => 'ev' - // @ts-expect-error + // @ts-ignore encoding.encodeQueryKey = () => 'ek' const router = createRouter() await router.push({ name: 'home', query: { p: '%' } }) @@ -140,9 +139,9 @@ describe('URL Encoding', () => { }) it('keeps decoded values in hash', async () => { - // @ts-expect-error: override to make the difference + // @ts-ignore: override to make the difference encoding.decode = () => 'd' - // @ts-expect-error + // @ts-ignore encoding.encodeHash = () => '#e' const router = createRouter() await router.push({ name: 'home', hash: '#%' }) @@ -152,9 +151,9 @@ describe('URL Encoding', () => { }) }) it('decodes hash', async () => { - // @ts-expect-error: override to make the difference + // @ts-ignore: override to make the difference encoding.decode = () => '#d' - // @ts-expect-error + // @ts-ignore encoding.encodeHash = () => '#e' const router = createRouter() await router.push('#%20') diff --git a/packages/router/__tests__/utils.ts b/__tests__/utils.ts similarity index 81% rename from packages/router/__tests__/utils.ts rename to __tests__/utils.ts index a6020fc3a..b51ddbdf2 100644 --- a/packages/router/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -1,9 +1,14 @@ import { JSDOM, ConstructorOptions } from 'jsdom' import { + NavigationGuard, RouteRecordMultipleViews, MatcherLocation, + RouteLocationNormalized, + _RouteRecordBase, RouteComponent, RouteRecordRaw, + RouteRecordName, + _RouteRecordProps, } from '../src/types' import { h, ComponentOptions } from 'vue' import { @@ -12,11 +17,7 @@ import { createRouter, Router, RouterView, - RouteRecordNormalized, - NavigationGuard, - RouteLocationNormalized, } from '../src' -import { _RouteRecordProps } from '../src/typed-routes' export const tick = (time?: number) => new Promise(resolve => { @@ -30,8 +31,6 @@ export async function ticks(n: number) { } } -export const delay = (t: number) => new Promise(r => setTimeout(r, t)) - export function nextNavigation(router: Router) { return new Promise((resolve, reject) => { let removeAfter = router.afterEach((_to, _from, failure) => { @@ -50,21 +49,18 @@ export function nextNavigation(router: Router) { export interface RouteRecordViewLoose extends Pick< RouteRecordMultipleViews, - 'path' | 'name' | 'meta' | 'beforeEnter' + 'path' | 'name' | 'components' | 'children' | 'meta' | 'beforeEnter' > { leaveGuards?: any - updateGuards?: any instances: Record enterCallbacks: Record props: Record - aliasOf: RouteRecordNormalized | RouteRecordViewLoose | undefined - children?: RouteRecordRaw[] - components: Record | null | undefined + aliasOf: RouteRecordViewLoose | undefined } -// @ts-expect-error we are intentionally overriding the type +// @ts-ignore we are intentionally overriding the type export interface RouteLocationNormalizedLoose extends RouteLocationNormalized { - name: string | symbol | null | undefined + name: RouteRecordName | null | undefined path: string // record? params: any @@ -107,15 +103,11 @@ export function createDom(options?: ConstructorOptions) { } ) - try { - // @ts-expect-error: not the same window - global.window = dom.window - global.location = dom.window.location - global.history = dom.window.history - global.document = dom.window.document - } catch (erro) { - // it's okay, some are readonly - } + // @ts-ignore: needed for jsdom + global.window = dom.window + global.location = dom.window.location + global.history = dom.window.history + global.document = dom.window.document return dom } diff --git a/packages/router/__tests__/warnings.spec.ts b/__tests__/warnings.spec.ts similarity index 65% rename from packages/router/__tests__/warnings.spec.ts rename to __tests__/warnings.spec.ts index 7d9d9274e..5273b0fbb 100644 --- a/packages/router/__tests__/warnings.spec.ts +++ b/__tests__/warnings.spec.ts @@ -1,12 +1,6 @@ -import { createMemoryHistory, createRouter, createRouterMatcher } from '../src' -import { - defineAsyncComponent, - defineComponent, - FunctionalComponent, - h, -} from 'vue' -import { describe, expect, it } from 'vitest' -import { mockWarn } from './vitest-mock-warn' +import { mockWarn } from 'jest-mock-warn' +import { createMemoryHistory, createRouter } from '../src' +import { defineComponent, FunctionalComponent, h } from 'vue' let component = defineComponent({}) @@ -33,8 +27,7 @@ describe('warnings', () => { history, routes: [{ path: '/:p', name: 'p', component }], }) - // @ts-expect-error: cannot pass params with a path - router.push({ path: '/p', params: { p: 'p' } }) + router.resolve({ path: '/p', params: { p: 'p' } }) expect('Path "/p" was passed with params').toHaveBeenWarned() }) @@ -44,35 +37,17 @@ describe('warnings', () => { history, routes: [{ path: '/:p', name: 'p', component }], }) - // @ts-expect-error: it would be better if this didn't error but it still an - // invalid location - router.push({ path: '/p', name: 'p', params: { p: 'p' } }) + router.resolve({ path: '/p', name: 'p', params: { p: 'p' } }) expect('Path "/" was passed with params').not.toHaveBeenWarned() }) - it('does not warn when redirecting from params', async () => { - const history = createMemoryHistory() - const router = createRouter({ - history, - routes: [ - { - path: '/p/:p', - redirect: to => ({ path: '/s', query: { p: to.params.p } }), - }, - { path: '/s', component }, - ], - }) - router.push({ path: '/p/abc' }) - expect('was passed with params').not.toHaveBeenWarned() - }) - it('warns if an alias is missing params', async () => { createRouter({ history: createMemoryHistory(), routes: [{ path: '/:p/:c', alias: ['/:p/c'], component }], }) expect( - 'Alias "/:p/c" and the original record: "/:p/:c" must have the exact same param named "c"' + 'Alias "/:p/c" and the original record: "/:p/:c" should have the exact same param named "c"' ).toHaveBeenWarned() }) @@ -94,7 +69,7 @@ describe('warnings', () => { ], }) expect( - `Absolute path "/:a/b" must have the exact same param named "b" as its parent "/:a/:b".` + `Absolute path "/:a/b" should have the exact same param named "b" as its parent "/:a/:b".` ).toHaveBeenWarned() }) @@ -104,7 +79,7 @@ describe('warnings', () => { routes: [{ path: '/:p/:c', alias: ['/:p/:c+'], component }], }) expect( - 'Alias "/:p/:c+" and the original record: "/:p/:c" must have the exact same param named "c"' + 'Alias "/:p/:c+" and the original record: "/:p/:c" should have the exact same param named "c"' ).toHaveBeenWarned() }) @@ -114,11 +89,11 @@ describe('warnings', () => { routes: [{ path: '/:p/c', alias: ['/:p/:c'], component }], }) expect( - 'Alias "/:p/:c" and the original record: "/:p/c" must have the exact same param named "c"' + 'Alias "/:p/:c" and the original record: "/:p/c" should have the exact same param named "c"' ).toHaveBeenWarned() }) - it('warns if next is called multiple times in one navigation guard', async () => { + it('warns if next is called multiple times in one navigation guard', done => { expect.assertions(3) let router = createRouter({ history: createMemoryHistory(), @@ -135,14 +110,15 @@ describe('warnings', () => { expect('called more than once').toHaveBeenWarnedTimes(1) next() expect('called more than once').toHaveBeenWarnedTimes(1) + done() }) - await router.push('/b') + router.push('/b') }) it('warns if a non valid function is passed as a component', async () => { const Functional: FunctionalComponent = () => h('div', 'functional') - // Functional must have a displayName to avoid the warning + // Functional should have a displayName to avoid the warning const router = createRouter({ history: createMemoryHistory(), @@ -201,45 +177,6 @@ describe('warnings', () => { expect('"/foo" is a Promise instead of a function').toHaveBeenWarned() }) - it('warns if use defineAsyncComponent in routes', async () => { - const router = createRouter({ - history: createMemoryHistory(), - // simulates import('./component.vue') - routes: [ - { - path: '/foo', - component: defineAsyncComponent(() => Promise.resolve({})), - }, - ], - }) - await router.push('/foo') - expect(`defined using "defineAsyncComponent()"`).toHaveBeenWarned() - }) - - it('warns if use defineAsyncComponent in routes only once per component', async () => { - const router = createRouter({ - history: createMemoryHistory(), - // simulates import('./component.vue') - routes: [ - { path: '/', component }, - { - path: '/foo', - component: defineAsyncComponent(() => Promise.resolve({})), - }, - { - path: '/bar', - component: defineAsyncComponent(() => Promise.resolve({})), - }, - ], - }) - await router.push('/foo') - await router.push('/') - await router.push('/foo') - expect(`defined using "defineAsyncComponent()"`).toHaveBeenWarnedTimes(1) - await router.push('/bar') - expect(`defined using "defineAsyncComponent()"`).toHaveBeenWarnedTimes(2) - }) - it('warns if no route matched', async () => { const router = createRouter({ history: createMemoryHistory(), @@ -269,7 +206,7 @@ describe('warnings', () => { await router.push('/b').catch(() => {}) expect( - 'Detected a possibly infinite redirection in a navigation guard when going from "/" to "/b"' + 'Detected an infinite redirection in a navigation guard when going from "/" to "/b"' ).toHaveBeenWarned() }) @@ -290,26 +227,4 @@ describe('warnings', () => { 'It should be called exactly one time in each navigation guard' ).toHaveBeenWarned() }) - - it('warns when discarding params', () => { - const record = { - path: '/a', - name: 'a', - components: {}, - } - const matcher = createRouterMatcher([record], {}) - matcher.resolve( - { name: 'a', params: { no: 'a', foo: '35' } }, - { - path: '/parent/one', - name: undefined, - params: { old: 'one' }, - matched: [] as any, - meta: {}, - } - ) - expect('invalid param(s) "no", "foo" ').toHaveBeenWarned() - // from the previous location - expect('"one"').not.toHaveBeenWarned() - }) }) diff --git a/packages/router/api-extractor.json b/api-extractor.json similarity index 100% rename from packages/router/api-extractor.json rename to api-extractor.json diff --git a/circle.yml b/circle.yml new file mode 100644 index 000000000..402562287 --- /dev/null +++ b/circle.yml @@ -0,0 +1,114 @@ +# Javascript Node CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-javascript/ for more details +# +version: 2.1 + +defaults: &defaults + working_directory: ~/project/vue-router + docker: + - image: circleci/node:lts-browsers + +jobs: + install: + <<: *defaults + steps: + - checkout + - restore_cache: + keys: + - v3-dependencies-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} + - v3-dependencies-cache-{{ .Branch }}- + - v3-dependencies-cache- + - run: yarn install --frozen-lockfile + - persist_to_workspace: + root: ~/project + paths: + - vue-router + + build-e2e: + <<: *defaults + steps: + - attach_workspace: + at: ~/project + - run: yarn run build:e2e + - persist_to_workspace: + root: ~/project + paths: + - vue-router/e2e/__build__ + + test-e2e: + <<: *defaults + steps: + - attach_workspace: + at: ~/project + - run: yarn test:e2e:ci + - store_artifacts: + path: e2e/reports + - store_artifacts: + path: e2e/screenshots + - store_test_results: + path: e2e/reports + + test-e2e-bs: + <<: *defaults + steps: + - attach_workspace: + at: ~/project + - run: yarn test:e2e:bs + - store_test_results: + path: e2e/reports + + test-unit: + <<: *defaults + steps: + - attach_workspace: + at: ~/project + - run: yarn test:unit --maxWorkers=2 + - store_artifacts: + path: coverage + + build-lint: + <<: *defaults + steps: + - attach_workspace: + at: ~/project + - run: yarn lint + - run: yarn test:types + - run: yarn build + - run: yarn build:dts + - run: yarn run test:dts + # Save cache after this task. At this point e2e are still running + - save_cache: + key: v3-dependencies-cache-{{ .Branch }}-{{ checksum "yarn.lock" }} + paths: + - node_modules + + coverage: + <<: *defaults + steps: + - attach_workspace: + at: ~/project + - run: + name: Send code coverage + command: yarn run codecov + +workflows: + version: 2 + install-and-parallel-test: + jobs: + - install + - test-e2e: + requires: + - install + - test-e2e-bs: + requires: + - install + - test-unit: + requires: + - install + - build-lint: + requires: + - install + - coverage: + requires: + - test-unit diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 74aac0d3f..000000000 --- a/codecov.yml +++ /dev/null @@ -1,6 +0,0 @@ -coverage: - status: - patch: off - project: - default: - threshold: 2% diff --git a/docs/.vitepress/components/HomeSponsors.vue b/docs/.vitepress/components/HomeSponsors.vue new file mode 100644 index 000000000..eb8a7b317 --- /dev/null +++ b/docs/.vitepress/components/HomeSponsors.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/docs/.vitepress/components/sponsors.json b/docs/.vitepress/components/sponsors.json new file mode 100644 index 000000000..206e892e1 --- /dev/null +++ b/docs/.vitepress/components/sponsors.json @@ -0,0 +1,14 @@ +{ + "silver": [ + { + "href": "https://www.vuemastery.com/", + "alt": "VueMastery", + "imgSrc": "https://www.vuemastery.com/images/vuemastery.svg" + }, + { + "imgSrc": "https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-light-text.svg", + "href": "https://www.vuetifyjs.com/", + "alt": "Vuetify" + } + ] +} diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js new file mode 100644 index 000000000..8b84b0072 --- /dev/null +++ b/docs/.vitepress/config.js @@ -0,0 +1,203 @@ +/** @typedef {import('vitepress').UserConfig} UserConfig */ + +/** @type {UserConfig['head']} */ +const head = [['link', { rel: 'icon', href: `/logo.png` }]] + +if (process.env.NODE_ENV === 'production') { + head.push([ + 'script', + { + src: 'https://unpkg.com/thesemetrics@latest', + async: '', + }, + ]) +} + +/** @type {UserConfig} */ +const config = { + lang: 'en-US', + title: 'Vue Router Next', + description: 'The official router for Vue.js.', + base: '/router4/', + locales: { + '/': { + lang: 'en-US', + title: 'Vue Router', + description: 'The official router for Vue.js.', + }, + // '/es/': { + // lang: 'es-ES', + // title: 'Vue Router', + // description: 'El router oficial par Vue.js', + // }, + }, + head, + // serviceWorker: true, + themeConfig: { + repo: 'vuejs/vue-router-next', + docsRepo: 'vuejs/vue-router-next', + docsDir: 'docs', + docsBranch: 'master', + editLinks: true, + + carbonAds: { + carbon: 'CEBICK3I', + custom: 'CEBICK3M', + placement: 'routervuejsorg', + }, + + algolia: { + apiKey: '07ed552fc16926cc57c9eb0862c1a7f9', + indexName: 'next_router_vuejs', + algoliaOptions: { facetFilters: ['tags:guide,api'] }, + }, + + locales: { + // English + '/': { + nav: [ + { + text: 'Guide', + link: '/guide/', + }, + { + text: 'API Reference', + link: '/api/', + }, + { + text: 'Changelog', + link: + 'https://github.com/vuejs/vue-router-next/blob/master/CHANGELOG.md', + }, + ], + + sidebar: [ + { + text: 'Introduction', + link: '/introduction.html', + }, + { + text: 'Installation', + link: '/installation.html', + }, + { + text: 'Essentials', + collapsable: false, + children: [ + { + text: 'Getting Started', + link: '/guide/', + }, + { + text: 'Dynamic Route Matching', + link: '/guide/essentials/dynamic-matching.html', + }, + { + text: "Routes' Matching Syntax", + link: '/guide/essentials/route-matching-syntax.html', + }, + { + text: 'Nested Routes', + link: '/guide/essentials/nested-routes.html', + }, + { + text: 'Programmatic Navigation', + link: '/guide/essentials/navigation.html', + }, + { + text: 'Named Routes', + link: '/guide/essentials/named-routes.html', + }, + { + text: 'Named Views', + link: '/guide/essentials/named-views.html', + }, + { + text: 'Redirect and Alias', + link: '/guide/essentials/redirect-and-alias.html', + }, + { + text: 'Passing Props to Route Components', + link: '/guide/essentials/passing-props.html', + }, + { + text: 'Different History modes', + link: '/guide/essentials/history-mode.html', + }, + ], + }, + { + text: 'Advanced', + collapsable: false, + children: [ + { + text: 'Navigation guards', + link: '/guide/advanced/navigation-guards.html', + }, + { + text: 'Route Meta Fields', + link: '/guide/advanced/meta.html', + }, + { + text: 'Data Fetching', + link: '/guide/advanced/data-fetching.html', + }, + { + text: 'Composition API', + link: '/guide/advanced/composition-api.html', + }, + { + text: 'Transitions', + link: '/guide/advanced/transitions.html', + }, + { + text: 'Scroll Behavior', + link: '/guide/advanced/scroll-behavior.html', + }, + { + text: 'Lazy Loading Routes', + link: '/guide/advanced/lazy-loading.html', + }, + { + text: 'Extending RouterLink', + link: '/guide/advanced/extending-router-link.html', + }, + { + text: 'Navigation Failures', + link: '/guide/advanced/navigation-failures.html', + }, + { + text: 'Dynamic Routing', + link: '/guide/advanced/dynamic-routing.html', + }, + ], + }, + { + text: 'Migrating from Vue 2', + link: '/guide/migration/index.html', + }, + ], + }, + }, + + // '/es/': { + // nav: [ + // { + // text: 'Guía', + // link: '/guide/', + // }, + // { + // text: 'API', + // link: '/api/', + // }, + // { + // text: 'Cambios', + // link: + // 'https://github.com/vuejs/vue-router-next/blob/master/CHANGELOG.md', + // }, + // ], + // }, + }, +} + +module.exports = config diff --git a/docs/.vitepress/style.styl b/docs/.vitepress/style.styl new file mode 100644 index 000000000..395006696 --- /dev/null +++ b/docs/.vitepress/style.styl @@ -0,0 +1,66 @@ +.bit-sponsor + font-weight 600 + background-color #f3f6f8 + padding 0.6em 1.2em + border-radius 8px + display inline-block + margin 1em 0 !important + + a + color #999 + + img + height 40px + margin-left 15px + + img, span + vertical-align middle + +.vueschool + background-color #e7ecf3 + padding 1em 1.25em + border-radius 2px + color #486491 + position relative + display flex + + a + color #486491 !important + position relative + padding-left 36px + + &:before + content '' + position absolute + display block + width 30px + height 30px + top calc(50% - 15px); + left -4px + border-radius 50% + background-color #73abfe + + &:after + content '' + position absolute + display block + width 0 + height 0 + top calc(50% - 5px) + left 8px + border-top 5px solid transparent + border-bottom 5px solid transparent + border-left 8px solid #fff +#ad + width: 125px; + position: fixed; + z-index: $z-header -1; + right: 10px; + top: 120px; + padding: 10px; + background-color: #fff; + border-radius: 3px; + font-size: 13px; + text-align: center; + img + width: 100%; \ No newline at end of file diff --git a/docs/.vitepress/theme/Layout.vue b/docs/.vitepress/theme/Layout.vue new file mode 100644 index 000000000..6df984891 --- /dev/null +++ b/docs/.vitepress/theme/Layout.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/docs/.vitepress/theme/components/AlgoliaSearchBox.vue b/docs/.vitepress/theme/components/AlgoliaSearchBox.vue new file mode 100644 index 000000000..298e27063 --- /dev/null +++ b/docs/.vitepress/theme/components/AlgoliaSearchBox.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/docs/.vitepress/theme/components/BuySellAds.vue b/docs/.vitepress/theme/components/BuySellAds.vue new file mode 100644 index 000000000..ccd4cfcac --- /dev/null +++ b/docs/.vitepress/theme/components/BuySellAds.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/docs/.vitepress/theme/components/CarbonAds.vue b/docs/.vitepress/theme/components/CarbonAds.vue new file mode 100644 index 000000000..94814564c --- /dev/null +++ b/docs/.vitepress/theme/components/CarbonAds.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js new file mode 100644 index 000000000..6782a554d --- /dev/null +++ b/docs/.vitepress/theme/index.js @@ -0,0 +1,14 @@ +import DefaultTheme from 'vitepress/dist/client/theme-default' +import Layout from './Layout.vue' +import HomeSponsors from '../components/HomeSponsors.vue' + +export default { + ...DefaultTheme, + Layout, + enhanceApp({ app, router, siteData }) { + app.component('HomeSponsors', HomeSponsors) + // app is the Vue 3 app instance from createApp() + // router is VitePress' custom router (see `lib/app/router.js`) + // siteData is a ref of current site-level metadata. + }, +} diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 000000000..2ced3c4ad --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,1085 @@ +--- +sidebar: auto +--- + +# API Reference + +## `` Props + +### to + +- **Type**: [`RouteLocationRaw`](#routelocationraw) +- **Details**: + + Denotes the target route of the link. When clicked, the value of the `to` prop will be passed to `router.push()` internally, so it can either be a `string` or a [route location object](#routelocationraw). + +```html + +Home + +Home + + +Home + + +Home + + +User + + + + Register + +``` + +### replace + +- **Type**: `boolean` +- **Default**: `false` +- **Details**: + + Setting `replace` prop will call `router.replace()` instead of `router.push()` when clicked, so the navigation will not leave a history record. + +```html + +``` + +### active-class + +- **Type**: `string` +- **Default**: `"router-link-active"` (or global [`linkActiveClass`](#linkactiveclass)) +- **Details**: + + Class to apply on the rendered `` when the link is active. + +### aria-current-value + +- **Type**: `'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'` (`string`) +- **Default**: `"page"` +- **Details**: + + Value passed to the attribute `aria-current` when the link is exactly active. + +### custom + +- **Type**: `boolean` +- **Default**: `false` +- **Details**: + + Whether `` should not wrap its content in an `` element. Useful when using [`v-slot`](#router-link-s-v-slot) to create a custom RouterLink. By default, `` will render its content wrapped in an `` element, even when using `v-slot`. Passing the `custom` prop, removes that behavior. + +- **Examples**: + + ```html + + {{ route.fullPath }} + + ``` + + Renders `/home`. + + ```html + + {{ route.fullPath }} + + ``` + + Renders `/home`. + +### exact-active-class + +- **Type**: `string` +- **Default**: `"router-link-exact-active"` (or global [`linkExactActiveClass`](#linkexactactiveclass)) +- **Details**: + + Class to apply on the rendered `` when the link is exact active. + +## ``'s `v-slot` + +`` exposes a low level customization through a [scoped slot](https://v3.vuejs.org/guide/component-slots.html#scoped-slots). This is a more advanced API that primarily targets library authors but can come in handy for developers as well, to build a custom component like a _NavLink_ or other. + +:::tip +Remember to pass the `custom` option to `` to prevent it from wrapping its content inside of an `` element. +::: + +```html + + + {{ route.fullPath }} + + +``` + +- `href`: resolved url. This would be the `href` attribute of an `` element. It contains the `base` if any was provided. +- `route`: resolved normalized location. +- `navigate`: function to trigger the navigation. **It will automatically prevent events when necessary**, the same way `router-link` does, e.g. `ctrl` or `cmd` + click will still be ignored by `navigate`. +- `isActive`: `true` if the [active class](#active-class) should be applied. Allows to apply an arbitrary class. +- `isExactActive`: `true` if the [exact active class](#exact-active-class) should be applied. Allows to apply an arbitrary class. + +### Example: Applying Active Class to Outer Element + +Sometimes we may want the active class to be applied to an outer element rather than the `` element itself, in that case, you can wrap that element inside a `router-link` and use the `v-slot` properties to create your link: + +```html + +

  • + {{ route.fullPath }} +
  • + +``` + +:::tip +If you add a `target="_blank"` to your `a` element, you must omit the `@click="navigate"` handler. +::: + +## `` Props + +### name + +- **Type**: `string` +- **Default**: `"default"` +- **Details**: + + When a `` has a `name`, it will render the component with the corresponding name in the matched route record's `components` option. + +- **See Also**: [Named Views](/guide/essentials/named-views.md) + +### route + +- **Type**: [`RouteLocationNormalized`](#routelocationnormalized) +- **Details**: + + A route location that has all of its component resolved (if any was lazy loaded) so it can be displayed. + +## ``'s `v-slot` + +`` exposes a `v-slot` API mainly to wrap your route components with `` and `` components. + +```html + + + +``` + +- `Component`: VNodes to be passed to a ``'s `is` prop. +- `route`: resolved normalized [route location](#routelocationnormalized). + +## createRouter + +Creates a Router instance that can be used by a Vue app. Check the [`RouterOptions`](#routeroptions) for a list of all the properties that can be passed. + +**Signature:** + +```typescript +export declare function createRouter(options: RouterOptions): Router +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | ------------------------------- | -------------------------------- | +| options | [RouterOptions](#routeroptions) | Options to initialize the router | + +## createWebHistory + +Creates an HTML5 history. Most common history for single page applications. The application must be served through the http protocol. + +**Signature:** + +```typescript +export declare function createWebHistory(base?: string): RouterHistory +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------------------------------------------------------------------- | +| base | `string` | optional base to provide. Useful when the application is hosted inside of a folder like `https://example.com/folder/` | + +### Examples + +```js +createWebHistory() // No base, the app is hosted at the root of the domain `https://example.com` +createWebHistory('/folder/') // gives a url of `https://example.com/folder/` +``` + +## createWebHashHistory + +Creates a hash history. Useful for web applications with no host (e.g. `file://`) or when configuring a server to handle any URL isn't an option. **Note you should use [`createWebHistory`](#createwebhistory) if SEO matters to you**. + +**Signature:** + +```typescript +export declare function createWebHashHistory(base?: string): RouterHistory +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| base | `string` | optional base to provide. Defaults to `location.pathname` or `/` if at root. If there is a `base` tag in the `head`, its value will be **ignored**. | + +### Examples + +```js +// at https://example.com/folder +createWebHashHistory() // gives a url of `https://example.com/folder#` +createWebHashHistory('/folder/') // gives a url of `https://example.com/folder/#` +// if the `#` is provided in the base, it won't be added by `createWebHashHistory` +createWebHashHistory('/folder/#/app/') // gives a url of `https://example.com/folder/#/app/` +// you should avoid doing this because it changes the original url and breaks copying urls +createWebHashHistory('/other-folder/') // gives a url of `https://example.com/other-folder/#` + +// at file:///usr/etc/folder/index.html +// for locations with no `host`, the base is ignored +createWebHashHistory('/iAmIgnored') // gives a url of `file:///usr/etc/folder/index.html#` +``` + +## createMemoryHistory + +Creates a in-memory based history. The main purpose of this history is to handle SSR. It starts in a special location that is nowhere. If the user is not on a browser context, it's up to them to replace that location with the starter location by either calling `router.push()` or `router.replace()`. + +**Signature:** + +```typescript +export declare function createMemoryHistory(base?: string): RouterHistory +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | -------- | ----------------------------------------- | +| base | `string` | Base applied to all urls, defaults to '/' | + +### Returns + +A history object that can be passed to the router constructor + +## NavigationFailureType + +Enumeration with all possible types for navigation failures. Can be passed to [isNavigationFailure](#isnavigationfailure) to check for specific failures. **Never use any of the numerical values**, always use the variables like `NavigationFailureType.aborted`. + +**Signature:** + +```typescript +export declare enum NavigationFailureType +``` + +### Members + +| Member | Value | Description | +| ---------- | ----- | -------------------------------------------------------------------------------------------------------------------------------- | +| aborted | 4 | An aborted navigation is a navigation that failed because a navigation guard returned `false` or called `next(false)` | +| cancelled | 8 | A cancelled navigation is a navigation that failed because a more recent navigation finished started (not necessarily finished). | +| duplicated | 16 | A duplicated navigation is a navigation that failed because it was initiated while already being at the exact same location. | + +## START_LOCATION + +- **Type**: [`RouteLocationNormalized`](#routelocationnormalized) +- **Details**: + + Initial route location where the router is. Can be used in navigation guards to differentiate the initial navigation. + + ```js + import { START_LOCATION } from 'vue-router' + + router.beforeEach((to, from) => { + if (from === START_LOCATION) { + // initial navigation + } + }) + ``` + +## Composition API + +### onBeforeRouteLeave + +Add a navigation guard that triggers whenever the component for the current location is about to be left. Similar to `beforeRouteLeave` but can be used in any component. The guard is removed when the component is unmounted. + +**Signature:** + +```typescript +export declare function onBeforeRouteLeave(leaveGuard: NavigationGuard): void +``` + +#### Parameters + +| Parameter | Type | Description | +| ---------- | ------------------------------------- | ----------------------- | +| leaveGuard | [`NavigationGuard`](#navigationguard) | Navigation guard to add | + +### onBeforeRouteUpdate + +Add a navigation guard that triggers whenever the current location is about to be updated. Similar to `beforeRouteUpdate` but can be used in any component. The guard is removed when the component is unmounted. + +**Signature:** + +```typescript +export declare function onBeforeRouteUpdate(updateGuard: NavigationGuard): void +``` + +#### Parameters + +| Parameter | Type | Description | +| ----------- | ------------------------------------- | ----------------------- | +| updateGuard | [`NavigationGuard`](#navigationguard) | Navigation guard to add | + +### useLink + +Returns everything exposed by the [`v-slot` API](#router-link-s-v-slot). + +**Signature:** + +```typescript +export declare function useLink(props: RouterLinkOptions): { + route: ComputedRef, + href: ComputedRef, + isActive: ComputedRef, + isExactActive: ComputedRef, + navigate: (event?: MouseEvent) => Promise(NavigationFailure | void), +} +``` + +#### Parameters + +| Parameter | Type | Description | +| --------- | ----------------- | ------------------------------------------------------------------------------------- | +| props | RouterLinkOptions | props object that can be passed to ``. Accepts `Ref`s and `ComputedRef`s | + +### useRoute + +Returns the current route location. Equivalent to using `$route` inside templates. Must be called inside of `setup()`. + +**Signature:** + +```typescript +export declare function useRoute(): RouteLocationNormalized +``` + +### useRouter + +Returns the [router](#Router) instance. Equivalent to using `$router` inside templates. Must be called inside of `setup()`. + +**Signature:** + +```typescript +export declare function useRouter(): Router +``` + +## TypeScript + +Here are some of the interfaces and types used by Vue Router. The documentation references them to give you an idea of the existing properties in objects. + +## Router Properties + +### currentRoute + +- **Type**: [`Ref`](#routelocationnormalized) +- **Details**: + + Current route location. Readonly. + +### options + +- **Type**: [`RouterOptions`](#routeroptions) +- **Details**: + + Original options object passed to create the Router. Readonly. + +## Router Methods + +### addRoute + +Add a new [Route Record](#routerecordraw) as the child of an existing route. If the route has a `name` and there is already an existing one with the same one, it removes it first. + +**Signature:** + +```typescript +addRoute(parentName: string | symbol, route: RouteRecordRaw): () => void +``` + +_Parameters_ + +| Parameter | Type | Description | +| ---------- | ----------------------------------- | ------------------- | +| parentName | `string | symbol` | Parent Route Record where `route` should be appended at | +| route | [`RouteRecordRaw`](#routerecordraw) | Route Record to add | + +### addRoute + +Add a new [route record](#routerecordraw) to the router. If the route has a `name` and there is already an existing one with the same one, it removes it first. + +**Signature:** + +```typescript +addRoute(route: RouteRecordRaw): () => void +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | ----------------------------------- | ------------------- | +| route | [`RouteRecordRaw`](#routerecordraw) | Route Record to add | + +:::tip +Note adding routes does not trigger a new navigation, meaning that the added route will not be displayed unless a new navigation is triggered. +::: + +### afterEach + +Add a navigation hook that is executed after every navigation. Returns a function that removes the registered hook. + +**Signature:** + +```typescript +afterEach(guard: NavigationHookAfter): () => void +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | ------------------- | ---------------------- | +| guard | NavigationHookAfter | navigation hook to add | + +#### Examples + +```js +router.afterEach((to, from, failure) => { + if (isNavigationFailure(failure)) { + console.log('failed navigation', failure) + } +}) +``` + +### back + +Go back in history if possible by calling `history.back()`. Equivalent to `router.go(-1)`. + +**Signature:** + +```typescript +back(): void +``` + +### beforeEach + +Add a navigation guard that executes before any navigation. Returns a function that removes the registered guard. + +**Signature:** + +```typescript +beforeEach(guard: NavigationGuard): () => void +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | ------------------------------------- | ----------------------- | +| guard | [`NavigationGuard`](#navigationguard) | navigation guard to add | + +### beforeResolve + +Add a navigation guard that executes before navigation is about to be resolved. At this state all component have been fetched and other navigation guards have been successful. Returns a function that removes the registered guard. + +**Signature:** + +```typescript +beforeResolve(guard: NavigationGuard): () => void +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | ------------------------------------- | ----------------------- | +| guard | [`NavigationGuard`](#navigationguard) | navigation guard to add | + +#### Examples + +```js +router.beforeEach(to => { + if (to.meta.requiresAuth && !isAuthenticated) return false +}) +``` + +### forward + +Go forward in history if possible by calling `history.forward()`. Equivalent to `router.go(1)`. + +**Signature:** + +```typescript +forward(): void +``` + +### getRoutes + +Get a full list of all the [route records](#routerecord). + +**Signature:** + +```typescript +getRoutes(): RouteRecord[] +``` + +### go + +Allows you to move forward or backward through the history. + +**Signature:** + +```typescript +go(delta: number): void +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | -------- | ----------------------------------------------------------------------------------- | +| delta | `number` | The position in the history to which you want to move, relative to the current page | + +### hasRoute + +Checks if a route with a given name exists + +**Signature:** + +```typescript +hasRoute(name: string | symbol): boolean +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | ------- | ----------- | +| name | `string | symbol` | Name of the route to check | + +### isReady + +Returns a Promise that resolves when the router has completed the initial navigation, which means it has resolved all async enter hooks and async components that are associated with the initial route. If the initial navigation already happened, the promise resolves immediately.This is useful in server-side rendering to ensure consistent output on both the server and the client. Note that on server side, you need to manually push the initial location while on client side, the router automatically picks it up from the URL. + +**Signature:** + +```typescript +isReady(): Promise +``` + +### onError + +Adds an error handler that is called every time a non caught error happens during navigation. This includes errors thrown synchronously and asynchronously, errors returned or passed to `next` in any navigation guard, and errors occurred when trying to resolve an async component that is required to render a route. + +**Signature:** + +```typescript +onError(handler: (error: any) => any): () => void +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | --------------------- | ------------------------- | +| handler | `(error: any) => any` | error handler to register | + +### push + +Programmatically navigate to a new URL by pushing an entry in the history stack. + +**Signature:** + +```typescript +push(to: RouteLocationRaw): Promise +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | --------------------------------------- | ----------------------------- | +| to | [`RouteLocationRaw`](#routelocationraw) | Route location to navigate to | + +### removeRoute + +Remove an existing route by its name. + +**Signature:** + +```typescript +removeRoute(name: string | symbol): void +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | ------- | ----------- | +| name | `string | symbol` | Name of the route to remove | + +### replace + +Programmatically navigate to a new URL by replacing the current entry in the history stack. + +**Signature:** + +```typescript +replace(to: RouteLocationRaw): Promise +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | --------------------------------------- | ----------------------------- | +| to | [`RouteLocationRaw`](#routelocationraw) | Route location to navigate to | + +### resolve + +Returns the [normalized version](#routelocation) of a [route location](#routelocationraw). Also includes an `href` property that includes any existing `base`. + +**Signature:** + +```typescript +resolve(to: RouteLocationRaw): RouteLocation & { + href: string +} +``` + +_Parameters_ + +| Parameter | Type | Description | +| --------- | --------------------------------------- | ----------------------------- | +| to | [`RouteLocationRaw`](#routelocationraw) | Raw route location to resolve | + +## RouterOptions + +### history + +History implementation used by the router. Most web applications should use `createWebHistory` but it requires the server to be properly configured. You can also use a _hash_ based history with `createWebHashHistory` that does not require any configuration on the server but isn't handled at all by search engines and does poorly on SEO. + +**Signature:** + +```typescript +history: RouterHistory +``` + +#### Examples + +```js +createRouter({ + history: createWebHistory(), + // other options... +}) +``` + +### linkActiveClass + +Default class applied to active [RouterLink](#router-link-props). If none is provided, `router-link-active` will be applied. + +**Signature:** + +```typescript +linkActiveClass?: string +``` + +### linkExactActiveClass + +Default class applied to exact active [RouterLink](#router-link-props). If none is provided, `router-link-exact-active` will be applied. + +**Signature:** + +```typescript +linkExactActiveClass?: string +``` + +### parseQuery + +Custom implementation to parse a query. Must decode query keys and values. See its counterpart, [stringifyQuery](#stringifyquery). + +**Signature:** + +```typescript +parseQuery?: (searchQuery: string) => Record +``` + +#### Examples + +Let's say you want to use the package [qs](https://github.com/ljharb/qs) to parse queries, you can provide both `parseQuery` and `stringifyQuery`: + +```js +import qs from 'qs' + +createRouter({ + // other options... + parseQuery: qs.parse, + stringifyQuery: qs.stringify, +}) +``` + +### routes + +Initial list of routes that should be added to the router. + +**Signature:** + +```typescript +routes: RouteRecordRaw[] +``` + +### scrollBehavior + +Function to control scrolling when navigating between pages. Can return a Promise to delay scrolling. Check . + +**Signature:** + +```typescript +scrollBehavior?: ScrollBehavior +``` + +#### Examples + +```js +function scrollBehavior(to, from, savedPosition) { + // `to` and `from` are both route locations + // `savedPosition` can be null if there isn't one +} +``` + +### stringifyQuery + +Custom implementation to stringify a query object. Should not prepend a leading `?`. Should properly encode query keys and values. [parseQuery](#parsequery) counterpart to handle query parsing. + +**Signature:** + +```typescript +stringifyQuery?: ( + query: Record< + string | number, + string | number | null | undefined | (string | number | null | undefined)[] + > +) => string +``` + +## RouteRecordRaw + +Route record that can be provided by the user when adding routes via the [`routes` option](#routeroptions) or via [`router.addRoutes()`](#addroutes). There are three different kind of route records: + +- Single views records: have a `component` option +- Multiple views records ([named views](/guide/essentials/named-views.md)): have a `components` option +- Redirect records: cannot have `component` or `components` option because a redirect record is never reached. + +### path + +- **Type**: `string` +- **Details**: + + Path of the record. Should start with `/` unless the record is the child of another record. + Can define parameters: `/users/:id` matches `/users/1` as well as `/users/posva`. + +- **See Also**: [Dynamic Route Matching](/guide/essentials/dynamic-matching.md) + +### redirect + +- **Type**: `RouteLocationRaw | (to: RouteLocationNormalized) => RouteLocationRaw` (Optional) +- **Details**: + + Where to redirect if the route is directly matched. The redirection happens + before any navigation guard and triggers a new navigation with the new target + location. Can also be a function that receives the target route location and + returns the location we should redirect to. + +### children + +- **Type**: Array of [`RouteRecordRaw`](#routerecordraw) (Optional) +- **Details**: + + Nested routes of the current record. + +- **See Also**: [Nested Routes](/guide/essentials/nested-routes.md) + +### alias + +- **Type**: `string | string[]` (Optional) +- **Details**: + + Aliases for the route. Allows defining extra paths that will behave like a + copy of the record. This enables paths shorthands like `/users/:id` and + `/u/:id`. **All `alias` and `path` values must share the same params**. + +### name + +- **Type**: `string | symbol` (Optional) +- **Details**: + + Unique name for the route record. + +### beforeEnter + +- **Type**: [`NavigationGuard | NavigationGuard[]`](#navigationguard) (Optional) +- **Details**: + + Before enter guard specific to this record. Note `beforeEnter` has no effect if the record has a `redirect` property. + +### props + +- **Type**: `boolean | Record | (to: RouteLocationNormalized) => Record` (Optional) +- **Details**: + + Allows passing down params as props to the component rendered by `router-view`. When passed to a _multiple views record_, it should be an object with the same keys as `components` or a `boolean` to be applied to each component. + target location. + +- **See Also**: [Passing props to Route Components](/guide/essentials/passing-props.md) + +### meta + +- **Type**: [`RouteMeta`](#routemeta) (Optional) +- **Details**: + + Custom data attached to the record. + +- **See Also**: [Meta fields](/guide/advanced/meta.md) + +## RouteRecordNormalized + +Normalized version of a [Route Record](#routerecordraw) + +### aliasOf + +- **Type**: `RouteRecordNormalized | undefined` +- **Details**: + + Defines if this record is the alias of another one. This property is `undefined` if the record is the original one. + +### beforeEnter + +- **Type**: [`NavigationGuard`](#navigationguard) +- **Details**: + + Navigation guard applied when entering this record from somewhere else. + +- **See Also**: [Navigation guards](/guide/advanced/navigation-guards.md) + +### children + +- **Type**: Array of normalized [route records](#routerecordnormalized) +- **Details**: + + Children route records of the current route. Empty array if none. + +### components + +- **Type**: `Record` +- **Details**: + + Dictionary of named views, if none, contains an object with the key `default`. + +### meta + +- **Type**: `RouteMeta` +- **Details**: + + Arbitrary data attached to the record. + +- **See also**: [Meta fields](/guide/advanced/meta.md) + +### name + +- **Type**: `string | symbol | undefined` +- **Details**: + + Name for the route record. `undefined` if none was provided. + +### path + +- **Type**: `string` +- **Details**: + + Normalized path of the record. Includes any parent's `path`. + +### props + +- **Type**: `Record>` +- **Details**: + + Dictionary of the [`props` option](#props) for each named view. If none, it will contain only one property named `default`. + +### redirect + +- **Type**: [`RouteLocationRaw`](#routelocationraw) +- **Details**: + + Where to redirect if the route is directly matched. The redirection happens before any navigation guard and triggers a new navigation with the new target location. + +## RouteLocationRaw + +User-level route location that can be passed to `router.push()`, `redirect`, and returned in [Navigation Guards](/guide/advanced/navigation-guards.md). + +A raw location can either be a `string` like `/users/posva#bio` or an object: + +```js +// these three forms are equivalent +router.push('/users/posva#bio') +router.push({ path: '/users/posva', hash: '#bio' }) +router.push({ name: 'users', params: { username: 'posva' }, hash: '#bio' }) +// only change the hash +router.push({ hash: '#bio' }) +// only change query +router.push({ query: { page: '2' } }) +// change one param +router.push({ params: { username: 'jolyne' } }) +``` + +Note `path` must be provided encoded (e.g. `phantom blood` becomes `phantom%20blood`) while `params`, `query` and `hash` must not, they are encoded by the router. + +Raw route locations also support an extra option `replace` to call `router.replace()` instead of `router.push()` in navigation guards. Note this also internally calls `router.replace()` even when calling `router.push()`: + +```js +router.push({ hash: '#bio', replace: true }) +// equivalent to +router.replace({ hash: '#bio' }) +``` + +## RouteLocation + +Resolved [RouteLocationRaw](#routelocationraw) that can contain [redirect records](#routerecordraw). Apart from that it has the same properties as [RouteLocationNormalized](#routelocationnormalized). + +## RouteLocationNormalized + +Normalized route location. Does not have any [redirect records](#routerecordraw). In navigation guards, `to` and `from` are always of this type. + +### fullPath + +- **Type**: `string` +- **Details**: + + Encoded URL associated to the route location. Contains `path`, `query` and `hash`. + +### hash + +- **Type**: `string` +- **Details**: + + Decoded `hash` section of the URL. Always starts with a `#`. Empty string if there is no `hash` in the URL. + +### query + +- **Type**: `Record` +- **Details**: + + Dictionary of decoded query params extracted from the `search` section of the URL. + +### matched + +- **Type**: [`RouteRecordNormalized[]`](#routerecordnormalized) +- **Details**: + + Array of [normalized route records](#routerecord) that were matched with the given route location. + +### meta + +- **Type**: `RouteMeta` +- **Details**: + + Arbitrary data attached to all matched records merged (non recursively) from parent to child. + +- **See also**: [Meta fields](/guide/advanced/meta.md) + +### name + +- **Type**: `string | symbol | undefined | null` +- **Details**: + + Name for the route record. `undefined` if none was provided. + +### params + +- **Type**: `Record` +- **Details**: + + Dictionary of decoded params extracted from `path`. + +### path + +- **Type**: `string` +- **Details**: + + Encoded `pathname` section of the URL associated to the route location. + +### redirectedFrom + +- **Type**: [`RouteLocation`](#routelocation) +- **Details**: + + Route location we were initially trying to access before ending up on the current location when a `redirect` option was found or a navigation guard called `next()` with a route location. `undefined` if there was no redirection. + +## NavigationFailure + +### from + +- **Type**: [`RouteLocationNormalized`](#routelocationnormalized) +- **Details**: + + Route location we were navigating from + +### to + +- **Type**: [`RouteLocationNormalized`](#routelocationnormalized) +- **Details**: + + Route location we were navigating to + +### type + +- **Type**: [`NavigationFailureType`](#navigationfailuretype) +- **Details**: + + Type of the navigation failure. + +- **See Also**: [Navigation Failures](/guide/advanced/navigation-failures.md) + +## NavigationGuard + +- **Arguments**: + + - [`RouteLocationNormalized`](#routelocationnormalized) to - Route location we are navigating to + - [`RouteLocationNormalized`](#routelocationnormalized) from - Route location we are navigating from + - `Function` next (Optional) - Callback to validate the navigation + +- **Details**: + + Function that can be passed to control a router navigation. The `next` callback can be omitted if you return a value (or a Promise) instead, which is encouraged. Possible return values (and parameters for `next`) are: + + - `undefined | void | true`: validates the navigation + - `false`: cancels the navigation + - [`RouteLocationRaw`](#routelocationraw): redirects to a different location + - `(vm: ComponentPublicInstance) => any` **only for `beforeRouteEnter`**: A callback to be executed once the navigation completes. Receives the route component instance as the parameter. + +- **See Also**: [Navigation Guards](/guide/advanced/navigation-guards.md) + +## Component Injections + +### Component Injected Properties + +These properties are injected into every child component by calling `app.use(router)`. + +- **this.\$router** + + The router instance. + +- **this.\$route** + + The current active [route location](#routelocationnormalized). This property is read-only and its properties are immutable, but it can be watched. + +### Component Enabled Options + +- **beforeRouteEnter** +- **beforeRouteUpdate** +- **beforeRouteLeave** + +See [In Component Guards](/guide/advanced/navigation-guards.md#in-component-guards). diff --git a/docs/guide/advanced/composition-api.md b/docs/guide/advanced/composition-api.md new file mode 100644 index 000000000..fcd6e3311 --- /dev/null +++ b/docs/guide/advanced/composition-api.md @@ -0,0 +1,107 @@ +# Vue Router and the Composition API + +The introduction of `setup` and Vue's [Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html), open up new possibilities but to be able to get the full potential out of Vue Router, we will need to use a few new functions to replace access to `this` and in-component navigation guards. + +## Accessing the Router and current Route inside `setup` + +Because we don't have access to `this` inside of `setup`, we cannot directly access `this.$router` or `this.$route` anymore. Instead we use the `useRouter` function: + +```js +export default { + setup() { + const router = useRouter() + const route = useRoute() + + function pushWithQuery(query) { + router.push({ + name: 'search', + query: { + ...route.query, + }, + }) + } + }, +} +``` + +The `route` object is a reactive object, so any of its properties can be watched and you should **avoid watching the whole `route`** object: + +```js +export default { + setup() { + const route = useRoute() + const userData = ref() + + // fetch the user information when params change + watch( + () => route.params, + async newParams => { + userData.value = await fetchUser(newParams.id) + } + ) + }, +} +``` + +Note we still have access to `$router` and `$route` in templates, so there is no need to return `router` or `route` inside of `setup`. + +## Navigation Guards + +While you can still use in-component navigation guards with a `setup` function, Vue Router exposes update and leave guards as Composition API functions: + +```js +import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' + +export default { + setup() { + // same as beforeRouteLeave option with no access to `this` + onBeforeRouteLeave((to, from) => { + const answer = window.confirm( + 'Do you really want to leave? you have unsaved changes!' + ) + // cancel the navigation and stay on the same page + if (!answer) return false + }) + + const userData = ref() + + // same as beforeRouteUpdate option with no access to `this` + onBeforeRouteUpdate(async (to, from) => { + // only fetch the user if the id changed as maybe only the query or the hash changed + if (to.params.id !== from.params.id) { + userData.value = await fetchUser(to.params.id) + } + }) + }, +} +``` + +Composition API guards can also be used in any component rendered by ``, they don't have to be used directly on the route component like in-component guards. + +## `useLink` + +Vue Router exposes the internal behavior of RouterLink as a Composition API function. It gives access the same properties as the [`v-slot` API](/api/#router-link-s-v-slot): + +```js +import { RouterLink, useLink } from 'vue-router' + +export default { + name: 'AppLink', + + props: { + // add @ts-ignore if using TypeScript + ...RouterLink.props, + inactiveClass: String, + }, + + setup(props) { + const { route, href, isActive, isExactActive, navigate } = useLink(props) + + const isExternalLink = computed( + () => typeof props.to === 'string' && props.to.startsWith('http') + ) + + return { isExternalLink, href, navigate, isActive } + }, +} +``` diff --git a/docs/guide/advanced/data-fetching.md b/docs/guide/advanced/data-fetching.md new file mode 100644 index 000000000..8a3882d10 --- /dev/null +++ b/docs/guide/advanced/data-fetching.md @@ -0,0 +1,106 @@ +# Data Fetching + +Sometimes you need to fetch data from the server when a route is activated. For example, before rendering a user profile, you need to fetch the user's data from the server. We can achieve this in two different ways: + +- **Fetching After Navigation**: perform the navigation first, and fetch data in the incoming component's lifecycle hook. Display a loading state while data is being fetched. + +- **Fetching Before Navigation**: Fetch data before navigation in the route enter guard, and perform the navigation after data has been fetched. + +Technically, both are valid choices - it ultimately depends on the user experience you are aiming for. + +## Fetching After Navigation + +When using this approach, we navigate and render the incoming component immediately, and fetch data in the component's `created` hook. It gives us the opportunity to display a loading state while the data is being fetched over the network, and we can also handle loading differently for each view. + +Let's assume we have a `Post` component that needs to fetch the data for a post based on `$route.params.id`: + +```html + +``` + +```js +export default { + data() { + return { + loading: false, + post: null, + error: null, + } + }, + created() { + // watch the params of the route to fetch the data again + this.$watch( + () => this.$route.params, + () => { + this.fetchData() + }, + // fetch the data when the view is created and the data is + // already being observed + { immediate: true } + ) + }, + methods: { + fetchData() { + this.error = this.post = null + this.loading = true + // replace `getPost` with your data fetching util / API wrapper + getPost(this.$route.params.id, (err, post) => { + this.loading = false + if (err) { + this.error = err.toString() + } else { + this.post = post + } + }) + }, + }, +} +``` + +## Fetching Before Navigation + +With this approach we fetch the data before actually navigating to the new +route. We can perform the data fetching in the `beforeRouteEnter` guard in the incoming component, and only call `next` when the fetch is complete: + +```js +export default { + data() { + return { + post: null, + error: null, + } + }, + beforeRouteEnter(to, from, next) { + getPost(to.params.id, (err, post) => { + next(vm => vm.setData(err, post)) + }) + }, + // when route changes and this component is already rendered, + // the logic will be slightly different. + async beforeRouteUpdate(to, from) { + this.post = null + try { + this.post = await getPost(to.params.id) + } catch (error) { + this.error = error.toString() + } + }, +} +``` + +The user will stay on the previous view while the resource is being fetched for the incoming view. It is therefore recommended to display a progress bar or some kind of indicator while the data is being fetched. If the data fetch fails, it's also necessary to display some kind of global warning message. + + + + diff --git a/packages/docs/guide/advanced/dynamic-routing.md b/docs/guide/advanced/dynamic-routing.md similarity index 73% rename from packages/docs/guide/advanced/dynamic-routing.md rename to docs/guide/advanced/dynamic-routing.md index f8de03c13..41f634fee 100644 --- a/packages/docs/guide/advanced/dynamic-routing.md +++ b/docs/guide/advanced/dynamic-routing.md @@ -1,13 +1,8 @@ # Dynamic Routing - +Adding routes to your router is usually done via the [`routes` option](/api/#routes) but in some situations, you might want to add or remove routes while the application is already running. Application with extensible interfaces like [Vue CLI UI](https://cli.vuejs.org/dev-guide/ui-api.html) can use this to make the application grow. -Adding routes to your router is usually done via the `routes` option but in some situations, you might want to add or remove routes while the application is already running. Applications with extensible interfaces like [Vue CLI UI](https://cli.vuejs.org/dev-guide/ui-api.html) can use this to make the application grow. - -## Adding routes +## Adding Routes Dynamic routing is achieved mainly via two functions: `router.addRoute()` and `router.removeRoute()`. They **only** register a new route, meaning that if the newly added route matches the current location, it would require you to **manually navigate** with `router.push()` or `router.replace()` to display that new route. Let's take a look at an example: @@ -20,25 +15,23 @@ const router = createRouter({ }) ``` -Going to any page like `/about`, `/store`, or `/3-tricks-to-improve-your-routing-code` ends up rendering the `Article` component. If we are on `/about` and we add a new route: +Going to any page, `/about`, `/store`, or `/3-tricks-to-improve-your-routing-code` ends up rendering the `Article` component. If we are on `/about` and we are a new route: ```js router.addRoute({ path: '/about', component: About }) ``` -The page will still show the `Article` component. We need to manually call `router.replace()` to change the current location and overwrite where we were (instead of pushing a new entry, ending up in the same location twice in our history): +The page will still show the `Article` component, we need to manually call `router.replace()` to change the current location and overwrite where we were (instead of pushing a new entry, ending up in the same location twice in our history): ```js router.addRoute({ path: '/about', component: About }) -// we could also use this.$route or useRoute() +// we could also use this.$route or route = useRoute() (inside a setup) router.replace(router.currentRoute.value.fullPath) ``` Remember you can `await router.replace()` if you need to wait for the new route to be displayed. - - -## Adding routes inside navigation guards +## Adding Routes inside navigation guards If you decide to add or remove routes inside of a navigation guard, you should not call `router.replace()` but trigger a redirection by returning the new location: @@ -52,7 +45,7 @@ router.beforeEach(to => { }) ``` -The example above assumes two things: first, the newly added route record will match the `to` location, effectively resulting in a different location from the one we were trying to access. Second, `hasNecessaryRoute()` returns `true` after adding the new route to avoid an infinite redirection. +The example above assumes two things: first, the newly added route record will match the `to` location, effectively resulting in a different location from the one we were trying to access. Second, `hasNecessaryRoute()` returns `false` after adding the new route to avoid an infinite redirection. Because we are redirecting, we are replacing the ongoing navigation, effectively behaving like the example shown before. In real world scenarios, adding is more likely to happen outside of navigation guards, e.g. when a view component mounts, it register new routes. @@ -61,37 +54,30 @@ Because we are redirecting, we are replacing the ongoing navigation, effectively There are few different ways to remove existing routes: - By adding a route with a conflicting name. If you add a route that has the same name as an existing route, it will remove the route first and then add the route: - ```js router.addRoute({ path: '/about', name: 'about', component: About }) - // this will remove the previously added route because they have - // the same name and names are unique across all routes + // this will remove the previously added route because they have the same name and names are unique router.addRoute({ path: '/other', name: 'about', component: Other }) ``` - - By calling the callback returned by `router.addRoute()`: - ```js const removeRoute = router.addRoute(routeRecord) removeRoute() // removes the route if it exists ``` - This is useful when the routes do not have a name - By using `router.removeRoute()` to remove a route by its name: - ```js router.addRoute({ path: '/about', name: 'about', component: About }) // remove the route router.removeRoute('about') ``` - Note you can use `Symbol`s for names in routes if you wish to use this function but want to avoid conflicts in names. Whenever a route is removed, **all of its aliases and children** are removed with it. ## Adding nested routes -To add nested routes to an existing route, you can pass the _name_ of the route as its first parameter to `router.addRoute()`. This will effectively add the route as if it was added through `children`: +To add nested routes to an existing route, you can pass the _name_ of the route as its first parameter to `router.addRoute()`, this will effectively add the route as if it was added through `children`: ```js router.addRoute({ name: 'admin', path: '/admin', component: Admin }) @@ -113,5 +99,5 @@ router.addRoute({ Vue Router gives you two functions to look at existing routes: -- [`router.hasRoute()`](/api/interfaces/Router.md#hasRoute): check if a route exists. -- [`router.getRoutes()`](/api/interfaces/Router.md#getRoutes): get an array with all the route records. +- [`router.hasRoute()`](/api/#hasroute): check if a route exists +- [`router.getRoutes()`](/api/#getroutes): get an array with all the route records. diff --git a/packages/docs/guide/advanced/extending-router-link.md b/docs/guide/advanced/extending-router-link.md similarity index 62% rename from packages/docs/guide/advanced/extending-router-link.md rename to docs/guide/advanced/extending-router-link.md index 0e72a24db..46973c5c1 100644 --- a/packages/docs/guide/advanced/extending-router-link.md +++ b/docs/guide/advanced/extending-router-link.md @@ -1,36 +1,10 @@ # Extending RouterLink - - The RouterLink component exposes enough `props` to suffice most basic applications but it doesn't try to cover every possible use case and you will likely find yourself using `v-slot` for some advanced cases. In most medium to large sized applications, it's worth creating one if not multiple custom RouterLink components to reuse them across your application. Some examples are Links in a Navigation Menu, handling external links, adding an `inactive-class`, etc. Let's extend RouterLink to handle external links as well and adding a custom `inactive-class` in an `AppLink.vue` file: -::: code-group - -```vue [Composition API] - - +```vue -``` -```vue [Options API] - - ``` -::: - If you prefer using a render function or create `computed` properties, you can use the `useLink` from the [Composition API](./composition-api.md): ```js @@ -114,25 +62,25 @@ export default { }, setup(props) { - // `props` contains `to` and any other prop that can be passed to - const { navigate, href, route, isActive, isExactActive } = useLink(props) + // toRef allows us to extract one prop and keep it reactive + // https://v3.vuejs.org/api/refs-api.html#toref + const { navigate, href route, isActive, isExactActive } = useLink(toRef(props, 'to')) + // profit! return { isExternalLink } - }, + } } ``` - - In practice, you might want to use your `AppLink` component for different parts of your application. e.g. using [Tailwind CSS](https://tailwindcss.com), you could create a `NavLink.vue` component with all the classes: ```vue