├── .eslintrc
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── LICENSE
├── README.md
├── README_CN.md
├── dist
├── url-change-event.js
└── url-change-event.min.js
├── index.d.ts
├── karma.conf.js
├── package-lock.json
├── package.json
├── rollup.config.mjs
├── src
├── UrlChangeEvent.js
├── index.js
├── override.js
└── stateCache.js
└── test
├── UrlChangeEvent.test.js
├── index.test.js
├── override.test.js
├── reset.js
├── stateCache.test.js
└── utils.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": { "es6": true, "browser": true },
3 | "rules": {
4 | "semi": ["error", "never"]
5 | },
6 | "parserOptions": {
7 | "sourceType": "module",
8 | "ecmaVersion": 2020
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | branches: [ main ]
7 |
8 | jobs:
9 | test:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v3
15 |
16 | - name: Install dependencies
17 | run: npm install
18 |
19 | - name: Run test
20 | run: npm run test
21 |
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .log/
3 | example/
4 | node_modules/
5 | src/
6 | dist/url-change-event.min.js
7 | test/
8 | .babelrc
9 | .eslintrc
10 | .prettierrc
11 | karma.conf.js
12 | rollup.config.mjs
13 | README*
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 jinrui
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # url-change-event
2 | a wrapper event that listen & control URL changes
3 | [中文](README_CN.md)
4 |
5 | ## Installation
6 | you can install with ```npm install url-change-event```
7 | ```javascript
8 | /* in ES 5 */
9 | require('url-change-event')
10 | /* in ES 6 */
11 | import 'url-change-event'
12 | ```
13 | or
14 | ```html
15 |
16 | ```
17 | > Due to override some history method, you should import this lib before your code.
18 |
19 | ## Usage
20 | ```javascript
21 | window.addEventListener('urlchangeevent', function(e) {
22 | // your code here
23 | })
24 | ```
25 | ### UrlChangeEvent instance
26 | Properties
27 | * ```oldURL``` {__URL__} - the url before change.
28 | * ```newURL``` {__URL__ | __null__} - the url after change. __WARNING:__ when event.action is __beforeunload__, this value is null.
29 | * ```action``` {[pushState|replaceState|popstate|beforeunload]} - the action that causes the url to change.
30 |
31 | Method
32 | * ```preventDefault``` - prevent url change
33 |
34 | ## License
35 | MIT licensed
36 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | # url-change-event
2 | 监听和控制URL的更改事件
3 |
4 | ## 安装
5 | 你可以通过 ```npm install url-change-event``` 引入
6 | ```javascript
7 | /* in ES 5 */
8 | require('url-change-event')
9 | /* in ES 6 */
10 | import 'url-change-event'
11 | ```
12 | 或者
13 | ```html
14 |
15 | ```
16 | > 因为复写了部分history函数,所以你应该在你的代码前引入此库。
17 |
18 | ## 使用
19 | ```javascript
20 | window.addEventListener('urlchangeevent', function(e) {
21 | // your code here
22 | })
23 | ```
24 | ### UrlChangeEvent 实例
25 | 属性
26 | * ```oldURL``` {__URL__} - 变化前的URL。
27 | * ```newURL``` {__URL__ | __null__} - 变化后的URL。 __WARNING:__ 当event.action为 __beforeunload__ 时,此项值为null。
28 | * ```action``` {[pushState|replaceState|popstate|beforeunload]} - 导致URL改变的操作。
29 |
30 | 方法
31 | * ```preventDefault``` - 阻止URL改变
32 |
33 | ## License
34 | MIT licensed
35 |
--------------------------------------------------------------------------------
/dist/url-change-event.js:
--------------------------------------------------------------------------------
1 | class UrlChangeEvent extends Event {
2 | constructor(option = {}) {
3 | super('urlchangeevent', { cancelable: true, ...option });
4 | this.newURL = option.newURL;
5 | this.oldURL = option.oldURL;
6 | this.action = option.action;
7 | }
8 |
9 | get [Symbol.toStringTag]() {
10 | return 'UrlChangeEvent'
11 | }
12 | }
13 |
14 | const originPushState = window.history.pushState.bind(window.history);
15 | window.history.pushState = function (state, title, url) {
16 | const nowURL = new URL(url || '', window.location.href);
17 | const notCanceled = window.dispatchEvent(
18 | new UrlChangeEvent({
19 | newURL: nowURL,
20 | oldURL: cacheURL,
21 | action: 'pushState',
22 | })
23 | );
24 |
25 | if (notCanceled) {
26 | originPushState({ _index: cacheIndex + 1, ...state }, title, url);
27 | updateCacheState();
28 | }
29 | };
30 |
31 | const originReplaceState = window.history.replaceState.bind(
32 | window.history
33 | );
34 | window.history.replaceState = function (state, title, url) {
35 | const nowURL = new URL(url || '', window.location.href);
36 | const notCanceled = window.dispatchEvent(
37 | new UrlChangeEvent({
38 | newURL: nowURL,
39 | oldURL: cacheURL,
40 | action: 'replaceState',
41 | })
42 | );
43 |
44 | if (notCanceled) {
45 | originReplaceState({ _index: cacheIndex, ...state }, title, url);
46 | updateCacheState();
47 | }
48 | };
49 |
50 | let cacheURL;
51 | let cacheIndex;
52 |
53 | function initState() {
54 | const state = window.history.state;
55 | if (!state || typeof state._index !== 'number') {
56 | originReplaceState({ _index: window.history.length, ...state }, null, null);
57 | }
58 | }
59 |
60 | function updateCacheState() {
61 | cacheURL = new URL(window.location.href);
62 | cacheIndex = window.history.state._index;
63 | }
64 |
65 | initState();
66 | updateCacheState();
67 |
68 | window.addEventListener('popstate', function (e) {
69 | initState();
70 | const nowIndex = window.history.state._index;
71 | const nowURL = new URL(window.location);
72 | if (nowIndex === cacheIndex) {
73 | e.stopImmediatePropagation();
74 | return
75 | }
76 |
77 | const notCanceled = window.dispatchEvent(
78 | new UrlChangeEvent({
79 | oldURL: cacheURL,
80 | newURL: nowURL,
81 | action: 'popstate',
82 | })
83 | );
84 |
85 | if (!notCanceled) {
86 | e.stopImmediatePropagation();
87 | window.history.go(cacheIndex - nowIndex);
88 | return
89 | }
90 | updateCacheState();
91 | });
92 |
93 | window.addEventListener('beforeunload', function (e) {
94 | const notCanceled = window.dispatchEvent(
95 | new UrlChangeEvent({
96 | oldURL: cacheURL,
97 | newURL: null,
98 | action: 'beforeunload',
99 | })
100 | );
101 |
102 | if (!notCanceled) {
103 | e.preventDefault();
104 | const confirmationMessage = 'o/';
105 | e.returnValue = confirmationMessage;
106 | return confirmationMessage
107 | }
108 | });
109 |
--------------------------------------------------------------------------------
/dist/url-change-event.min.js:
--------------------------------------------------------------------------------
1 | !function(t){"function"==typeof define&&define.amd?define(t):t()}((function(){"use strict";function t(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,r)}return n}function e(e){for(var n=1;n0&&void 0!==arguments[0]?arguments[0]:{};return n(this,y),(t=d.call(this,"urlchangeevent",e({cancelable:!0},r))).newURL=r.newURL,t.oldURL=r.oldURL,t.action=r.action,t}return p=y,(s=[{key:o,get:function(){return"UrlChangeEvent"}}])&&r(p.prototype,s),w&&r(p,w),Object.defineProperty(p,"prototype",{writable:!1}),y}(f(Event),Symbol.toStringTag),s=window.history.pushState.bind(window.history);window.history.pushState=function(t,n,r){var o=new URL(r||"",window.location.href);window.dispatchEvent(new p({newURL:o,oldURL:w,action:"pushState"}))&&(s(e({_index:d+1},t),n,r),h())};var w,d,y=window.history.replaceState.bind(window.history);function b(){var t=window.history.state;t&&"number"==typeof t._index||y(e({_index:window.history.length},t),null,null)}function h(){w=new URL(window.location.href),d=window.history.state._index}window.history.replaceState=function(t,n,r){var o=new URL(r||"",window.location.href);window.dispatchEvent(new p({newURL:o,oldURL:w,action:"replaceState"}))&&(y(e({_index:d},t),n,r),h())},b(),h(),window.addEventListener("popstate",(function(t){b();var e=window.history.state._index,n=new URL(window.location);if(e!==d){if(!window.dispatchEvent(new p({oldURL:w,newURL:n,action:"popstate"})))return t.stopImmediatePropagation(),void window.history.go(d-e);h()}else t.stopImmediatePropagation()})),window.addEventListener("beforeunload",(function(t){if(!window.dispatchEvent(new p({oldURL:w,newURL:null,action:"beforeunload"}))){t.preventDefault();return t.returnValue="o/","o/"}}))}));
2 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | interface IOption {
2 | oldURL: URL
3 | newURL: URL | null
4 | action: 'pushState' | 'replaceState' | 'popstate' | 'beforeunload'
5 | }
6 |
7 | export type UrlChangeEvent = Event & IOption
8 |
9 | declare global {
10 | interface Window {
11 | addEventListener(
12 | type: 'urlchangeevent',
13 | callback: (event: UrlChangeEvent) => void,
14 | options?: boolean | AddEventListenerOptions
15 | ): void
16 |
17 | removeEventListener(
18 | type: 'urlchangeevent',
19 | callback: (event: UrlChangeEvent) => void,
20 | options?: boolean | AddEventListenerOptions
21 | ): void
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | config.set({
3 | // base path that will be used to resolve all patterns (eg. files, exclude)
4 | basePath: '',
5 |
6 | // frameworks to use
7 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
8 | frameworks: ['mocha', 'chai'],
9 |
10 | // list of files / patterns to load in the browser
11 | // files: ['./test/*.test.js', './src/*.js'],
12 | files: ['./test/index.test.js'],
13 |
14 | // list of files / patterns to exclude
15 | // exclude: ['node_modules'],
16 |
17 | // preprocess matching files before serving them to the browser
18 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
19 | preprocessors: {
20 | './test/index.test.js': ['rollup']
21 | },
22 |
23 | rollupPreprocessor: {
24 | output: {
25 | name: 'url-change-event',
26 | format: 'es'
27 | }
28 | },
29 |
30 | // test results reporter to use
31 | // possible values: 'dots', 'progress'
32 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
33 | reporters: ['progress'],
34 |
35 | // web server port
36 | port: 9876,
37 |
38 | // enable / disable colors in the output (reporters and logs)
39 | colors: true,
40 |
41 | // level of logging
42 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
43 | logLevel: config.LOG_INFO,
44 |
45 | // enable / disable watching file and executing tests whenever any file changes
46 | autoWatch: false,
47 |
48 | // start these browsers
49 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
50 | browsers: ['ChromeHeadless'],
51 |
52 | // Continuous Integration mode
53 | // if true, Karma captures browsers, runs the tests and exits
54 | singleRun: true,
55 |
56 | // Concurrency level
57 | // how many browser should be started simultaneous
58 | concurrency: Infinity
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "url-change-event",
3 | "version": "0.1.7",
4 | "description": "a wrapper event that listen & control URL changes",
5 | "main": "dist/url-change-event.js",
6 | "typings": "./index.d.ts",
7 | "sideEffects": true,
8 | "scripts": {
9 | "lint": "prettier -l 'src/**/*.js' && eslint src",
10 | "build": "rollup -c",
11 | "karma": "karma start",
12 | "test": "npm run lint && npm run karma",
13 | "prepublishOnly": "npm run build"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/jerrykingxyz/url-change-event.git"
18 | },
19 | "author": "jinrui",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/jerrykingxyz/url-change-event/issues"
23 | },
24 | "homepage": "https://github.com/jerrykingxyz/url-change-event#readme",
25 | "devDependencies": {
26 | "@babel/core": "^7.20.2",
27 | "@babel/preset-env": "^7.20.2",
28 | "chai": "^4.3.7",
29 | "eslint": "^8.28.0",
30 | "karma": "^6.4.1",
31 | "karma-chai": "^0.1.0",
32 | "karma-chrome-launcher": "^3.1.1",
33 | "karma-mocha": "^2.0.1",
34 | "karma-rollup-preprocessor": "^7.0.8",
35 | "mocha": "^10.1.0",
36 | "prettier": "^2.7.1",
37 | "rollup": "^3.3.0",
38 | "@rollup/plugin-babel": "^6.0.2",
39 | "@rollup/plugin-terser": "^0.1.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import { babel } from '@rollup/plugin-babel'
2 | import terser from '@rollup/plugin-terser'
3 | import pkg from './package.json' assert { type: 'json' }
4 |
5 | export default [
6 | // cjs & esm & browser
7 | // because of no import and export in this lib, we can use esm for cjs & browser
8 | {
9 | input: 'src/index.js',
10 | output: {
11 | file: pkg.main,
12 | format: 'es',
13 | },
14 | plugins: [],
15 | },
16 | // browser minify
17 | {
18 | input: 'src/index.js',
19 | output: {
20 | name: 'url-change-event',
21 | file: pkg.main.replace('.js', '.min.js'),
22 | format: 'umd',
23 | },
24 | plugins: [
25 | babel({
26 | presets: ['@babel/preset-env'],
27 | }),
28 | terser(),
29 | ],
30 | },
31 | ]
32 |
--------------------------------------------------------------------------------
/src/UrlChangeEvent.js:
--------------------------------------------------------------------------------
1 | export default class UrlChangeEvent extends Event {
2 | constructor(option = {}) {
3 | super('urlchangeevent', { cancelable: true, ...option })
4 | this.newURL = option.newURL
5 | this.oldURL = option.oldURL
6 | this.action = option.action
7 | }
8 |
9 | get [Symbol.toStringTag]() {
10 | return 'UrlChangeEvent'
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import UrlChangeEvent from './UrlChangeEvent'
2 | import { initState, cacheURL, cacheIndex, updateCacheState } from './stateCache'
3 | import './override'
4 |
5 | window.addEventListener('popstate', function (e) {
6 | initState()
7 | const nowIndex = window.history.state._index
8 | const nowURL = new URL(window.location)
9 | if (nowIndex === cacheIndex) {
10 | e.stopImmediatePropagation()
11 | return
12 | }
13 |
14 | const notCanceled = window.dispatchEvent(
15 | new UrlChangeEvent({
16 | oldURL: cacheURL,
17 | newURL: nowURL,
18 | action: 'popstate',
19 | })
20 | )
21 |
22 | if (!notCanceled) {
23 | e.stopImmediatePropagation()
24 | window.history.go(cacheIndex - nowIndex)
25 | return
26 | }
27 | updateCacheState()
28 | })
29 |
30 | window.addEventListener('beforeunload', function (e) {
31 | const notCanceled = window.dispatchEvent(
32 | new UrlChangeEvent({
33 | oldURL: cacheURL,
34 | newURL: null,
35 | action: 'beforeunload',
36 | })
37 | )
38 |
39 | if (!notCanceled) {
40 | e.preventDefault()
41 | const confirmationMessage = 'o/'
42 | e.returnValue = confirmationMessage
43 | return confirmationMessage
44 | }
45 | })
46 |
--------------------------------------------------------------------------------
/src/override.js:
--------------------------------------------------------------------------------
1 | import UrlChangeEvent from './UrlChangeEvent'
2 | import { cacheURL, cacheIndex, updateCacheState } from './stateCache'
3 |
4 | export const originPushState = window.history.pushState.bind(window.history)
5 | window.history.pushState = function (state, title, url) {
6 | const nowURL = new URL(url || '', window.location.href)
7 | const notCanceled = window.dispatchEvent(
8 | new UrlChangeEvent({
9 | newURL: nowURL,
10 | oldURL: cacheURL,
11 | action: 'pushState',
12 | })
13 | )
14 |
15 | if (notCanceled) {
16 | originPushState({ _index: cacheIndex + 1, ...state }, title, url)
17 | updateCacheState()
18 | }
19 | }
20 |
21 | export const originReplaceState = window.history.replaceState.bind(
22 | window.history
23 | )
24 | window.history.replaceState = function (state, title, url) {
25 | const nowURL = new URL(url || '', window.location.href)
26 | const notCanceled = window.dispatchEvent(
27 | new UrlChangeEvent({
28 | newURL: nowURL,
29 | oldURL: cacheURL,
30 | action: 'replaceState',
31 | })
32 | )
33 |
34 | if (notCanceled) {
35 | originReplaceState({ _index: cacheIndex, ...state }, title, url)
36 | updateCacheState()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/stateCache.js:
--------------------------------------------------------------------------------
1 | import { originReplaceState } from './override'
2 |
3 | export let cacheURL
4 | export let cacheIndex
5 |
6 | export function initState() {
7 | const state = window.history.state
8 | if (!state || typeof state._index !== 'number') {
9 | originReplaceState({ _index: window.history.length, ...state }, null, null)
10 | }
11 | }
12 |
13 | export function updateCacheState() {
14 | cacheURL = new URL(window.location.href)
15 | cacheIndex = window.history.state._index
16 | }
17 |
18 | initState()
19 | updateCacheState()
20 |
--------------------------------------------------------------------------------
/test/UrlChangeEvent.test.js:
--------------------------------------------------------------------------------
1 | import UrlChangeEvent from '../src/UrlChangeEvent'
2 |
3 | describe('UrlChangeEvent test', function () {
4 | it('event struct', function () {
5 | const oldURL = {}
6 | const newURL = {}
7 | const action = {}
8 | const event = new UrlChangeEvent({
9 | oldURL,
10 | newURL,
11 | action,
12 | })
13 |
14 | expect(event.oldURL).to.equal(oldURL)
15 | expect(event.newURL).to.equal(newURL)
16 | expect(event.action).to.equal(action)
17 | })
18 |
19 | it('event default cancelable is true', function () {
20 | const event = new UrlChangeEvent()
21 | expect(event.cancelable).to.equal(true)
22 | })
23 |
24 | it('event set cancelable', function () {
25 | const event = new UrlChangeEvent({ cancelable: false })
26 | expect(event.cancelable).to.equal(false)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | import './reset.js'
2 | import '../src/index'
3 | import './UrlChangeEvent.test'
4 | import './stateCache.test'
5 | import './override.test'
6 |
7 | import { cacheIndex, cacheURL } from '../src/stateCache'
8 | import {
9 | sleep,
10 | expectURLEqual,
11 | waitForUrlChange,
12 | waitForPopstate,
13 | } from './utils'
14 |
15 | const initURL = cacheURL
16 | const initIndex = cacheIndex
17 |
18 | describe('popstate test', function () {
19 | it('history back test', async function () {
20 | const nextPath = '/nextState'
21 | const nextURL = new URL(nextPath, window.location.href)
22 | window.history.pushState(null, null, nextPath)
23 |
24 | let flag = false
25 | await waitForUrlChange(
26 | function () {
27 | window.history.back()
28 | },
29 | function (event) {
30 | flag = true
31 | expectURLEqual(event.oldURL, nextURL)
32 | expectURLEqual(event.newURL, initURL)
33 | expect(event.action).to.equal('popstate')
34 | }
35 | )
36 |
37 | expect(flag).to.equal(true)
38 | expect(cacheIndex).to.equal(initIndex)
39 | expectURLEqual(cacheURL, initURL)
40 | })
41 |
42 | it('history forward test', async function () {
43 | const nextPath = '/nextState'
44 | const nextURL = new URL(nextPath, window.location.href)
45 | window.history.pushState(null, null, nextPath)
46 | await waitForPopstate(function () {
47 | window.history.back()
48 | })
49 |
50 | let flag = false
51 | await waitForUrlChange(
52 | function () {
53 | window.history.forward()
54 | },
55 | function (event) {
56 | flag = true
57 | expectURLEqual(event.oldURL, initURL)
58 | expectURLEqual(event.newURL, nextURL)
59 | expect(event.action).to.equal('popstate')
60 | }
61 | )
62 |
63 | expect(flag).to.equal(true)
64 | expect(cacheIndex).to.equal(initIndex + 1)
65 | expectURLEqual(cacheURL, nextURL)
66 |
67 | await waitForPopstate(function () {
68 | window.history.back()
69 | })
70 |
71 | expect(cacheIndex).to.equal(initIndex)
72 | expectURLEqual(cacheURL, initURL)
73 | })
74 |
75 | it('prevent history change test', async function () {
76 | const nextPath = '/nextState'
77 | const nextURL = new URL(nextPath, window.location.href)
78 | window.history.pushState(null, null, nextPath)
79 | await waitForPopstate(function () {
80 | window.history.back()
81 | })
82 |
83 | let flag = false
84 | await waitForUrlChange(
85 | function () {
86 | window.history.forward()
87 | },
88 | function (event) {
89 | flag = true
90 | expectURLEqual(event.oldURL, initURL)
91 | expectURLEqual(event.newURL, nextURL)
92 | expect(event.action).to.equal('popstate')
93 | event.preventDefault()
94 | }
95 | )
96 |
97 | expect(flag).to.equal(true)
98 | expect(cacheIndex).to.equal(initIndex)
99 | expectURLEqual(cacheURL, initURL)
100 | })
101 |
102 | it('prevent history change without popstate test', async function () {
103 | const nextPath = '/nextState'
104 | const nextURL = new URL(nextPath, window.location.href)
105 | window.history.pushState(null, null, nextPath)
106 |
107 | let popstateTimes = 0
108 | const onPopstate = function () {
109 | popstateTimes++
110 | }
111 | window.addEventListener('popstate', onPopstate)
112 |
113 | let flag = false
114 | await waitForUrlChange(
115 | function () {
116 | window.history.back()
117 | },
118 | function (event) {
119 | flag = true
120 | expectURLEqual(event.oldURL, nextURL)
121 | expectURLEqual(event.newURL, initURL)
122 | expect(event.action).to.equal('popstate')
123 | event.preventDefault()
124 | }
125 | )
126 | expect(flag).to.equal(true)
127 |
128 | // wait the popstate run finish
129 | await sleep(400)
130 | await waitForPopstate(function () {
131 | window.history.back()
132 | })
133 |
134 | expect(popstateTimes).to.equal(1)
135 | expect(cacheIndex).to.equal(initIndex)
136 | expectURLEqual(cacheURL, initURL)
137 | window.removeEventListener('popstate', onPopstate)
138 | })
139 | })
140 |
141 | describe('before unload test', function () {
142 | it('event test', async function () {
143 | let flag = false
144 | let notCanceled = false
145 | await waitForUrlChange(
146 | function () {
147 | notCanceled = window.dispatchEvent(
148 | new Event('beforeunload', { cancelable: true })
149 | )
150 | },
151 | function (event) {
152 | flag = true
153 | expectURLEqual(event.oldURL, initURL)
154 | expect(event.newURL).to.equal(null)
155 | expect(event.action).to.equal('beforeunload')
156 | }
157 | )
158 |
159 | expect(flag).to.equal(true)
160 | expect(notCanceled).to.equal(true)
161 | })
162 |
163 | it('prevent event test', async function () {
164 | let flag = false
165 | let notCanceled = true
166 |
167 | await waitForUrlChange(
168 | function () {
169 | notCanceled = window.dispatchEvent(
170 | new Event('beforeunload', { cancelable: true })
171 | )
172 | },
173 | function (event) {
174 | flag = true
175 | expectURLEqual(event.oldURL, initURL)
176 | expect(event.newURL).to.equal(null)
177 | expect(event.action).to.equal('beforeunload')
178 | event.preventDefault()
179 | }
180 | )
181 |
182 | expect(flag).to.equal(true)
183 | expect(notCanceled).to.equal(false)
184 | })
185 | })
186 |
--------------------------------------------------------------------------------
/test/override.test.js:
--------------------------------------------------------------------------------
1 | import { cacheIndex, cacheURL } from '../src/stateCache'
2 | import { expectURLEqual, waitForUrlChange, waitForPopstate } from './utils'
3 |
4 | const initURL = cacheURL
5 | const initIndex = cacheIndex
6 |
7 | describe('override push state method test', function () {
8 | it('absolute path test', async function () {
9 | const pushPath = '/pushPath'
10 | const pushURL = new URL(pushPath, window.location.href)
11 | let flag = false
12 |
13 | await waitForUrlChange(
14 | function () {
15 | window.history.pushState(null, null, pushPath)
16 | },
17 | function (event) {
18 | flag = true
19 | expectURLEqual(event.oldURL, initURL)
20 | expectURLEqual(event.newURL, pushURL)
21 | expect(event.action).to.equal('pushState')
22 | }
23 | )
24 |
25 | expect(flag).to.equal(true)
26 | expect(cacheIndex).to.equal(initIndex + 1)
27 | expectURLEqual(cacheURL, pushURL)
28 |
29 | await waitForPopstate(function () {
30 | window.history.back()
31 | })
32 |
33 | expect(cacheIndex).to.equal(initIndex)
34 | expectURLEqual(cacheURL, initURL)
35 | })
36 |
37 | it('relative path test', async function () {
38 | const pushPath = 'pushPath'
39 | const pushURL = new URL(pushPath, window.location.href)
40 | let flag = false
41 | await waitForUrlChange(
42 | function () {
43 | window.history.pushState(null, null, pushPath)
44 | },
45 | function (event) {
46 | flag = true
47 | expectURLEqual(event.oldURL, initURL)
48 | expectURLEqual(event.newURL, pushURL)
49 | expect(event.action).to.equal('pushState')
50 | }
51 | )
52 |
53 | expect(flag).to.equal(true)
54 | expect(cacheIndex).to.equal(initIndex + 1)
55 | expectURLEqual(cacheURL, pushURL)
56 |
57 | await waitForPopstate(function () {
58 | window.history.back()
59 | })
60 |
61 | expect(cacheIndex).to.equal(initIndex)
62 | expectURLEqual(cacheURL, initURL)
63 | })
64 |
65 | it('prevent absolute path change test', async function () {
66 | const pushPath = '/pushPath'
67 | const pushURL = new URL(pushPath, window.location.href)
68 | let flag = false
69 | await waitForUrlChange(
70 | function () {
71 | window.history.pushState(null, null, pushPath)
72 | },
73 | function (event) {
74 | flag = true
75 | expectURLEqual(event.oldURL, initURL)
76 | expectURLEqual(event.newURL, pushURL)
77 | expect(event.action).to.equal('pushState')
78 | event.preventDefault()
79 | }
80 | )
81 |
82 | expect(flag).to.equal(true)
83 | expect(cacheIndex).to.equal(initIndex)
84 | expectURLEqual(cacheURL, initURL)
85 | })
86 |
87 | it('prevent relative path change test', async function () {
88 | const pushPath = 'pushPath'
89 | const pushURL = new URL(pushPath, window.location.href)
90 | let flag = false
91 |
92 | await waitForUrlChange(
93 | function () {
94 | window.history.pushState(null, null, pushPath)
95 | },
96 | function (event) {
97 | flag = true
98 | expectURLEqual(event.oldURL, initURL)
99 | expectURLEqual(event.newURL, pushURL)
100 | expect(event.action).to.equal('pushState')
101 | event.preventDefault()
102 | }
103 | )
104 |
105 | expect(flag).to.equal(true)
106 | expect(cacheIndex).to.equal(initIndex)
107 | expectURLEqual(cacheURL, initURL)
108 | })
109 | })
110 |
111 | describe('override replace state method test', function () {
112 | it('absolute path test', async function () {
113 | const replacePath = '/replacePath'
114 | const replaceURL = new URL(replacePath, window.location.href)
115 | let flag = false
116 |
117 | await waitForUrlChange(
118 | function () {
119 | window.history.replaceState(null, null, replacePath)
120 | },
121 | function (event) {
122 | flag = true
123 | expectURLEqual(event.oldURL, initURL)
124 | expectURLEqual(event.newURL, replaceURL)
125 | expect(event.action).to.equal('replaceState')
126 | }
127 | )
128 |
129 | expect(flag).to.equal(true)
130 | expect(cacheIndex).to.equal(initIndex)
131 | expectURLEqual(cacheURL, replaceURL)
132 |
133 | await waitForUrlChange(function () {
134 | window.history.replaceState(null, null, initURL.href)
135 | })
136 |
137 | expect(cacheIndex).to.equal(initIndex)
138 | expectURLEqual(cacheURL, initURL)
139 | })
140 |
141 | it('relative path test', async function () {
142 | const replacePath = 'replacePath'
143 | const replaceURL = new URL(replacePath, window.location.href)
144 | let flag = false
145 |
146 | await waitForUrlChange(
147 | function () {
148 | window.history.replaceState(null, null, replacePath)
149 | },
150 | function (event) {
151 | flag = true
152 | expectURLEqual(event.oldURL, initURL)
153 | expectURLEqual(event.newURL, replaceURL)
154 | expect(event.action).to.equal('replaceState')
155 | }
156 | )
157 |
158 | expect(flag).to.equal(true)
159 | expect(cacheIndex).to.equal(initIndex)
160 | expectURLEqual(cacheURL, replaceURL)
161 |
162 | await waitForUrlChange(function () {
163 | window.history.replaceState(null, null, initURL.href)
164 | })
165 |
166 | expect(cacheIndex).to.equal(initIndex)
167 | expectURLEqual(cacheURL, initURL)
168 | })
169 |
170 | it('prevent absolute path change test', async function () {
171 | const replacePath = '/replacePath'
172 | const replaceURL = new URL(replacePath, window.location.href)
173 | let flag = false
174 |
175 | await waitForUrlChange(
176 | function () {
177 | window.history.replaceState(null, null, replacePath)
178 | },
179 | function (event) {
180 | flag = true
181 | expectURLEqual(event.oldURL, initURL)
182 | expectURLEqual(event.newURL, replaceURL)
183 | expect(event.action).to.equal('replaceState')
184 | event.preventDefault()
185 | }
186 | )
187 |
188 | expect(flag).to.equal(true)
189 | expect(cacheIndex).to.equal(initIndex)
190 | expectURLEqual(cacheURL, initURL)
191 | })
192 |
193 | it('prevent relative path change test', async function () {
194 | const replacePath = 'replacePath'
195 | const replaceURL = new URL(replacePath, window.location.href)
196 | let flag = false
197 |
198 | await waitForUrlChange(
199 | function () {
200 | window.history.replaceState(null, null, replacePath)
201 | },
202 | function (event) {
203 | flag = true
204 | expectURLEqual(event.oldURL, initURL)
205 | expectURLEqual(event.newURL, replaceURL)
206 | expect(event.action).to.equal('replaceState')
207 | event.preventDefault()
208 | }
209 | )
210 |
211 | expect(flag).to.equal(true)
212 | expect(cacheIndex).to.equal(initIndex)
213 | expectURLEqual(cacheURL, initURL)
214 | })
215 | })
216 |
--------------------------------------------------------------------------------
/test/reset.js:
--------------------------------------------------------------------------------
1 | window.onbeforeunload = null
2 |
--------------------------------------------------------------------------------
/test/stateCache.test.js:
--------------------------------------------------------------------------------
1 | import { cacheIndex, cacheURL, updateCacheState } from '../src/stateCache'
2 |
3 | describe('state cache test', function () {
4 | it('index & path', function () {
5 | updateCacheState()
6 | expect(cacheIndex).to.equal(window.history.length)
7 | expect(cacheURL.href).to.equal(window.location.href)
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | export function sleep(time) {
2 | return new Promise((res) => setTimeout(res, time))
3 | }
4 |
5 | export function expectURLEqual(url1, url2) {
6 | expect(url1.href).to.equal(url2.href)
7 | }
8 |
9 | export function waitForUrlChange(trigger, onChange) {
10 | return new Promise((res) => {
11 | const callback = function (event) {
12 | if (typeof onChange === 'function') {
13 | onChange(event)
14 | }
15 | window.removeEventListener('urlchangeevent', callback)
16 | res()
17 | }
18 | window.addEventListener('urlchangeevent', callback)
19 | if (typeof trigger === 'function') {
20 | trigger()
21 | }
22 | })
23 | }
24 |
25 | export function waitForPopstate(trigger, onChange) {
26 | return new Promise((res) => {
27 | const callback = function (event) {
28 | if (typeof onChange === 'function') {
29 | onChange(event)
30 | }
31 | window.removeEventListener('popstate', callback)
32 | res()
33 | }
34 | window.addEventListener('popstate', callback)
35 | if (typeof trigger === 'function') {
36 | trigger()
37 | }
38 | })
39 | }
40 |
--------------------------------------------------------------------------------