├── .eslintrc.js ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── pagesconfig.json ├── rollup.config.js ├── rollup.test.config.js ├── src └── index.ts ├── tests ├── e2e.ts ├── index.html └── src │ └── App.svelte ├── tsconfig.json └── typedoc.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:import/errors", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 2019, 13 | "sourceType": "module" 14 | }, 15 | plugins: [ 16 | "import", 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | "indent": [ 21 | "error", 22 | 4 23 | ], 24 | "linebreak-style": [ 25 | "error", 26 | "unix" 27 | ], 28 | "quotes": [ 29 | "error", 30 | "double" 31 | ], 32 | "semi": [ 33 | "error", 34 | "never" 35 | ], 36 | "import/export": ["error"], 37 | "import/order": ["error", {"newlines-between": "always", "alphabetize": {"order": "asc", "caseInsensitive": true}}], 38 | "import/newline-after-import": ["error"], 39 | "import/no-absolute-path": ["error"] 40 | }, 41 | "settings": { 42 | "import/extensions": [".js"], 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /types/ 4 | /docs/ 5 | /tests/build/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.1.0] 10 | 11 | ### Added 12 | 13 | - Functional tests 14 | 15 | ### Changed 16 | 17 | - Migrate the code to TypeScript 18 | 19 | ## [1.0.0] - 2020-09-12 20 | 21 | First version 22 | 23 | [Unreleased]: https://github.com/MacFJA/svelte-undoable/compare/1.1.0...HEAD 24 | [1.1.0]: https://github.com/MacFJA/svelte-undoable/releases/tag/1.0.0...1.1.0 25 | [1.0.0]: https://github.com/MacFJA/svelte-undoable/releases/tag/1.0.0 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Reporting and improving 4 | 5 | ### Did you find a bug? 6 | 7 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/MacFJA/svelte-undoable/issues). 8 | 9 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/MacFJA/svelte-undoable/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible 10 | 11 | ### Did you write a patch that fixes a bug? 12 | 13 | * Open a new GitHub pull request with the patch. 14 | 15 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 16 | 17 | ### Do you have an idea to improve the application? 18 | 19 | * **Ensure the suggestion was not already ask** by searching on GitHub under [Issues](https://github.com/MacFJA/svelte-undoable/issues). 20 | 21 | * If you're unable to find an open issue about your feature, [open a new one](https://github.com/MacFJA/svelte-undoable/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible 22 | 23 | ### Do you want to contribute to the application documentation? 24 | 25 | * **Ensure the documentation improvement was not already submitted** by searching on GitHub under [Issues](https://github.com/MacFJA/svelte-undoable/issues). 26 | 27 | * If you're unable to find an open issue addressing this, clone the wiki git on your computer 28 | 29 | * [Open a new issue](https://github.com/MacFJA/svelte-undoable/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible and the patch for the documentation 30 | 31 | ## Coding conventions 32 | 33 | Check your code by running the command: 34 | ```sh 35 | npm run lint 36 | npm run test 37 | ``` 38 | The command will output any information worth knowing. No error should be left. 39 | 40 | ---- 41 | 42 | Thanks! -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2020 [MacFJA](https://github.com/MacFJA) 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 13 | > all 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 21 | > THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte Undoable store 2 | 3 | Memento design pattern in Svelte 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm install @macfja/svelte-undoable 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```javascript 14 | import { undoable } from "@macfja/svelte-undoable" 15 | 16 | let name = undoable("John") 17 | 18 | $name = "Jeanne" 19 | $name = "Doe" 20 | 21 | name.undo() 22 | // Now the value of $name is "Jeanne" 23 | 24 | name.undo() 25 | // Now $name is "John" 26 | 27 | name.redo() 28 | // Now $name is "Jeanne" again 29 | ``` 30 | 31 | ## Example 32 | 33 | ```html 34 | 46 | 47 |

Hello {$name}

48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | Only even number as saved in the store history. The maximum number of remembered value is 10. 56 | (If you go to 20, you can only go back to 2) 57 | 58 | 61 | 62 | 63 | ``` 64 | ([REPL](https://svelte.dev/repl/9412d77adca64a668055027e84619090?version=3.25.0)) 65 | 66 | ## Contributing 67 | 68 | Contributions are welcome. Please open up an issue or create PR if you would like to help out. 69 | 70 | Read more in the [Contributing file](CONTRIBUTING.md) 71 | 72 | ## License 73 | 74 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@macfja/svelte-undoable", 3 | "version": "1.1.0", 4 | "description": "Memento design pattern in Svelte", 5 | "module": "dist/index.mjs", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "doc": "typedoc src/index.ts", 9 | "lint": "eslint src/", 10 | "pretest": "rollup -c rollup.test.config.js", 11 | "test": "testcafe all tests/e2e.ts --app 'npx sirv tests'", 12 | "prebuild": "tsc", 13 | "build": "rollup -c", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "files": [ 17 | "src/", 18 | "dist/", 19 | "types/", 20 | "LICENSE.md", 21 | "README.md" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/macfja/svelte-undoable.git" 26 | }, 27 | "keywords": [ 28 | "undo", 29 | "redo", 30 | "store", 31 | "svelte", 32 | "sveltejs", 33 | "memento" 34 | ], 35 | "author": "MacFJA", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/macfja/svelte-undoable/issues" 39 | }, 40 | "homepage": "https://github.com/macfja/svelte-undoable#readme", 41 | "devDependencies": { 42 | "@rollup/plugin-node-resolve": "^9.0.0", 43 | "@rollup/plugin-typescript": "^8.2.0", 44 | "@tsconfig/svelte": "^1.0.10", 45 | "@typescript-eslint/eslint-plugin": "^4.15.1", 46 | "@typescript-eslint/parser": "^4.15.1", 47 | "eslint": "^7.20.0", 48 | "eslint-plugin-import": "^2.22.1", 49 | "rollup": "^2.39.0", 50 | "rollup-plugin-svelte": "^6.1.1", 51 | "sirv-cli": "^1.0.11", 52 | "svelte": "^3.32.3", 53 | "svelte-check": "^1.1.35", 54 | "svelte-preprocess": "^4.6.9", 55 | "testcafe": "^1.11.0", 56 | "tslib": "^2.1.0", 57 | "typedoc": "^0.20.26", 58 | "typedoc-plugin-pages": "^1.1.0", 59 | "typescript": "^4.1.5" 60 | }, 61 | "dependencies": {}, 62 | "types": "types/index.d.ts" 63 | } 64 | -------------------------------------------------------------------------------- /pagesconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": [ 3 | { 4 | "title": "Svelte Undoable store", 5 | "pages": [ 6 | {"title": "Changelog", "source": "CHANGELOG.md"}, 7 | {"title": "Contributing", "source": "CONTRIBUTING.md"}, 8 | {"title": "License", "source": "LICENSE.md"} 9 | ] 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import typescript from "@rollup/plugin-typescript"; 4 | import autoPreprocess from 'svelte-preprocess'; 5 | import pkg from './package.json'; 6 | 7 | const name = pkg.name 8 | .replace(/^(@\S+\/)?(svelte-)?(\S+)/, '$3') 9 | .replace(/^\w/, m => m.toUpperCase()) 10 | .replace(/-\w/g, m => m[1].toUpperCase()); 11 | 12 | export default { 13 | input: 'src/index.ts', 14 | output: [ 15 | { file: pkg.module, 'format': 'es' }, 16 | { file: pkg.main, 'format': 'umd', name } 17 | ], 18 | plugins: [ 19 | svelte({ 20 | preprocess: autoPreprocess() 21 | }), 22 | typescript(), 23 | resolve() 24 | ] 25 | }; -------------------------------------------------------------------------------- /rollup.test.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import typescript from "@rollup/plugin-typescript"; 4 | import autoPreprocess from 'svelte-preprocess'; 5 | 6 | export default { 7 | input: 'tests/src/App.svelte', 8 | output: [ 9 | { file: 'tests/build/app.js', 'format': 'iife', name: 'app' } 10 | ], 11 | plugins: [ 12 | svelte({ 13 | preprocess: autoPreprocess() 14 | }), 15 | typescript(), 16 | resolve() 17 | ] 18 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright MacFJA 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | * permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | * 9 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | * Software. 11 | * 12 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | 18 | import {Writable, writable} from "svelte/store" 19 | 20 | /** 21 | * Undoable store. 22 | * 23 | * A writable store with history. 24 | */ 25 | export interface UndoableStore extends Writable { 26 | /** 27 | * Change the value to the previous saved state 28 | */ 29 | undo(): void, 30 | 31 | /** 32 | * Change the value to the next saved state 33 | */ 34 | redo(): void, 35 | 36 | /** 37 | * Indicate if the store value can be revert to a previous state 38 | */ 39 | canUndo(): boolean, 40 | 41 | /** 42 | * Indicate if the store value can be change to a next state 43 | */ 44 | canRedo(): boolean, 45 | /** 46 | * Revert the value of the store to the oldest state. 47 | * If the parameter is `true`, then the store state history is cleared 48 | * @param {boolean?} clear If `true` the history is cleared 49 | */ 50 | reset(clear?: boolean): void, 51 | length(): number 52 | } 53 | 54 | /** 55 | * Create a store with undo/redo feature 56 | * @param {*} initial The initial value of the store 57 | * @param {number?} capacity The maximum number of entry to remember (any number lower than 2 is considered as infinite size) 58 | * @param {function(*: newValue): boolean} accept The validation function to accept a value to be save in memory.
59 | * Take the new store value as parameter, should return a boolean (`true` to save the value, `false` to dismiss it).
60 | * If ignore, it will accept all value 61 | * @return {UndoableStore<*>} 62 | */ 63 | export function undoable(initial: T, capacity?: number, accept?: (newValue: T) => boolean): UndoableStore { 64 | let states = [initial] 65 | let inAction = true 66 | let index = 0 67 | const store: Writable = writable(initial) 68 | if (typeof accept !== "function") { 69 | accept = () => true 70 | } 71 | 72 | store.subscribe((newValue) => { 73 | if (!inAction && accept(newValue)) { 74 | if (states.length > index + 1) { 75 | states = states.slice(0, index + 1) 76 | } 77 | states.push(newValue) 78 | index++ 79 | 80 | if (capacity > 1 && states.length > capacity) { 81 | states.shift() 82 | index = states.length 83 | } 84 | } 85 | }) 86 | 87 | inAction = false 88 | 89 | /** 90 | * @internal 91 | * Change the value to the previous saved state 92 | */ 93 | const undo = () => { 94 | inAction = true 95 | if (index > 0) { 96 | index-- 97 | } 98 | store.set(states[index]) 99 | inAction = false 100 | } 101 | 102 | /** 103 | * @internal 104 | * Change the value to the next saved state 105 | */ 106 | const redo = () => { 107 | inAction = true 108 | if (index < states.length - 1) { 109 | index++ 110 | } 111 | store.set(states[index]) 112 | inAction = false 113 | } 114 | 115 | /** 116 | * @internal 117 | * Indicate if the store value can be revert to a previous state 118 | * @return {boolean} 119 | */ 120 | const canUndo = () => { 121 | return index > 0 122 | } 123 | 124 | /** 125 | * @internal 126 | * Indicate if the store value can be change to a next state 127 | * @return {boolean} 128 | */ 129 | const canRedo = () => { 130 | return index < states.length - 1 131 | } 132 | 133 | /** 134 | * @internal 135 | * Revert the value of the store to the oldest state. 136 | * If the parameter is `true`, then the store state history is cleared 137 | * @param {boolean?} clear If `true` the history is cleared 138 | */ 139 | const reset = (clear?: boolean) => { 140 | inAction = true 141 | index = 0 142 | store.set(states[index]) 143 | if (clear) { 144 | states = states.slice(0, 1) 145 | } 146 | inAction = false 147 | } 148 | 149 | /** 150 | * @internal 151 | * Get the number of saved states 152 | * @return {number} 153 | */ 154 | const length = () => { 155 | return states.length 156 | } 157 | 158 | return { 159 | subscribe: store.subscribe, 160 | set: store.set, 161 | update: store.update, 162 | undo, 163 | redo, 164 | canUndo, 165 | canRedo, 166 | reset, 167 | length, 168 | } 169 | } 170 | 171 | /** 172 | * Change the value to the previous saved state 173 | * @param {UndoableStore<*>} undoableStore The store to use 174 | */ 175 | export function undo (undoableStore: UndoableStore): void { 176 | undoableStore.undo() 177 | } 178 | 179 | /** 180 | * Change the value to the next saved state 181 | * @param {UndoableStore<*>} undoableStore The store to use 182 | */ 183 | export function redo (undoableStore: UndoableStore): void { 184 | undoableStore.redo() 185 | } 186 | 187 | /** 188 | * Indicate if the store value can be revert to a previous state 189 | * @param {UndoableStore<*>} undoableStore The store to use 190 | * @return {boolean} 191 | */ 192 | export function canUndo (undoableStore: UndoableStore): boolean { 193 | return undoableStore.canUndo() 194 | } 195 | 196 | /** 197 | * Indicate if the store value can be change to a next state 198 | * @param {UndoableStore<*>} undoableStore The store to use 199 | * @return {boolean} 200 | */ 201 | export function canRedo (undoableStore: UndoableStore): boolean { 202 | return undoableStore.canRedo() 203 | } 204 | 205 | /** 206 | * Revert the value of the store to the oldest state. 207 | * If the second parameter is `true`, then the store state history is cleared 208 | * @param {UndoableStore<*>} undoableStore The store to use 209 | * @param {boolean} clear If `true` the history is cleared 210 | */ 211 | export function reset (undoableStore: UndoableStore, clear?: boolean): void { 212 | undoableStore.reset(clear) 213 | } 214 | -------------------------------------------------------------------------------- /tests/e2e.ts: -------------------------------------------------------------------------------- 1 | import { Selector } from "testcafe" 2 | 3 | fixture("Svelte Undoable") 4 | .page("http://localhost:5000") 5 | 6 | const title = Selector('h1'), 7 | input = Selector('#name-input'), 8 | nameUndo = Selector('#undo-btn'), 9 | nameRedo = Selector('#redo-btn'), 10 | nameReset = Selector('#reset-btn'), 11 | counterBtn = Selector('#counter-btn'), 12 | counterUndoBtn = Selector('#counter-undo-btn'), 13 | counterRedoBtn = Selector('#counter-redo-btn') 14 | 15 | test("Initial state", async t => { 16 | await t 17 | .expect(title.innerText).eql("Hello John") 18 | .expect(input.value).eql("John") 19 | .expect(nameUndo.hasAttribute('disabled')).eql(true) 20 | .expect(nameRedo.hasAttribute('disabled')).eql(true) 21 | .expect(nameReset.hasAttribute('disabled')).eql(true) 22 | 23 | .expect(counterBtn.innerText).eql('Clicked 0 times') 24 | .expect(counterUndoBtn.hasAttribute('disabled')).eql(true) 25 | .expect(counterRedoBtn.hasAttribute('disabled')).eql(true) 26 | }) 27 | 28 | test("Standard Undo/Redo", async t => { 29 | await t 30 | .selectText(input) 31 | .typeText(input, 'Paul') 32 | .expect(title.innerText).eql('Hello Paul') 33 | .expect(nameUndo.hasAttribute('disabled')).eql(false) 34 | .expect(nameRedo.hasAttribute('disabled')).eql(true) 35 | .expect(nameReset.hasAttribute('disabled')).eql(false) 36 | 37 | .click(nameUndo) // Pau 38 | .click(nameUndo) // Pa 39 | .click(nameUndo) // P 40 | .click(nameUndo) // John 41 | .expect(title.innerText).eql("Hello John") 42 | .expect(input.value).eql("John") 43 | .expect(nameUndo.hasAttribute('disabled')).eql(true) 44 | .expect(nameRedo.hasAttribute('disabled')).eql(false) 45 | .expect(nameReset.hasAttribute('disabled')).eql(true) 46 | 47 | .click(nameRedo) // P 48 | .click(nameRedo) // Pa 49 | .expect(title.innerText).eql("Hello Pa") 50 | .expect(input.value).eql("Pa") 51 | .expect(nameUndo.hasAttribute('disabled')).eql(false) 52 | .expect(nameRedo.hasAttribute('disabled')).eql(false) 53 | .expect(nameReset.hasAttribute('disabled')).eql(false) 54 | 55 | .click(nameReset) 56 | .expect(title.innerText).eql("Hello John") 57 | .expect(input.value).eql("John") 58 | .expect(nameUndo.hasAttribute('disabled')).eql(true) 59 | .expect(nameRedo.hasAttribute('disabled')).eql(false) 60 | .expect(nameReset.hasAttribute('disabled')).eql(true) 61 | }) 62 | 63 | test('Custom rule', async t => { 64 | await t 65 | .click(counterBtn) 66 | .expect(counterBtn.innerText).eql('Clicked 1 time') 67 | .expect(counterUndoBtn.hasAttribute('disabled')).eql(true) 68 | .expect(counterRedoBtn.hasAttribute('disabled')).eql(true) 69 | 70 | .click(counterBtn) 71 | .expect(counterBtn.innerText).eql('Clicked 2 times') 72 | .expect(counterUndoBtn.hasAttribute('disabled')).eql(false) 73 | .expect(counterRedoBtn.hasAttribute('disabled')).eql(true) 74 | 75 | .click(counterUndoBtn) 76 | .expect(counterBtn.innerText).eql('Clicked 0 times') 77 | .expect(counterUndoBtn.hasAttribute('disabled')).eql(true) 78 | .expect(counterRedoBtn.hasAttribute('disabled')).eql(false) 79 | 80 | .click(counterRedoBtn) 81 | .expect(counterBtn.innerText).eql('Clicked 2 times') 82 | .expect(counterUndoBtn.hasAttribute('disabled')).eql(false) 83 | .expect(counterRedoBtn.hasAttribute('disabled')).eql(true) 84 | 85 | .click(counterBtn) 86 | .expect(counterBtn.innerText).eql('Clicked 3 times') 87 | .expect(counterUndoBtn.hasAttribute('disabled')).eql(false) 88 | .expect(counterRedoBtn.hasAttribute('disabled')).eql(true) 89 | 90 | .click(counterBtn) 91 | .expect(counterBtn.innerText).eql('Clicked 4 times') 92 | .expect(counterUndoBtn.hasAttribute('disabled')).eql(false) 93 | .expect(counterRedoBtn.hasAttribute('disabled')).eql(true) 94 | 95 | .click(counterBtn) 96 | .expect(counterBtn.innerText).eql('Clicked 5 times') 97 | .click(counterBtn) 98 | .expect(counterBtn.innerText).eql('Clicked 6 times') 99 | .click(counterBtn) 100 | .expect(counterBtn.innerText).eql('Clicked 7 times') 101 | .click(counterBtn) 102 | .expect(counterBtn.innerText).eql('Clicked 8 times') 103 | .click(counterBtn) 104 | .expect(counterBtn.innerText).eql('Clicked 9 times') 105 | .click(counterBtn) 106 | .expect(counterBtn.innerText).eql('Clicked 10 times') 107 | .click(counterBtn) 108 | .expect(counterBtn.innerText).eql('Clicked 11 times') 109 | .click(counterUndoBtn) 110 | .expect(counterBtn.innerText).eql('Clicked 8 times') 111 | .click(counterRedoBtn) 112 | .expect(counterBtn.innerText).eql('Clicked 10 times') 113 | .click(counterBtn) 114 | .expect(counterBtn.innerText).eql('Clicked 11 times') 115 | .click(counterBtn) 116 | .expect(counterBtn.innerText).eql('Clicked 12 times') 117 | .click(counterBtn) 118 | .expect(counterBtn.innerText).eql('Clicked 13 times') 119 | .click(counterBtn) 120 | .expect(counterBtn.innerText).eql('Clicked 14 times') 121 | .click(counterBtn) 122 | .expect(counterBtn.innerText).eql('Clicked 15 times') 123 | .click(counterBtn) 124 | .expect(counterBtn.innerText).eql('Clicked 16 times') 125 | .click(counterBtn) 126 | .expect(counterBtn.innerText).eql('Clicked 17 times') 127 | .click(counterBtn) 128 | .expect(counterBtn.innerText).eql('Clicked 18 times') 129 | .click(counterUndoBtn) 130 | .expect(counterBtn.innerText).eql('Clicked 16 times') 131 | .click(counterUndoBtn) 132 | .expect(counterBtn.innerText).eql('Clicked 14 times') 133 | .click(counterUndoBtn) 134 | .expect(counterBtn.innerText).eql('Clicked 12 times') 135 | .click(counterUndoBtn) 136 | .expect(counterBtn.innerText).eql('Clicked 10 times') 137 | .click(counterUndoBtn) 138 | .expect(counterBtn.innerText).eql('Clicked 8 times') 139 | .click(counterUndoBtn) 140 | .expect(counterBtn.innerText).eql('Clicked 6 times') 141 | .click(counterUndoBtn) 142 | .expect(counterBtn.innerText).eql('Clicked 4 times') 143 | .click(counterUndoBtn) 144 | .expect(counterBtn.innerText).eql('Clicked 2 times') 145 | .click(counterUndoBtn) 146 | .expect(counterBtn.innerText).eql('Clicked 0 times') 147 | .expect(counterUndoBtn.hasAttribute('disabled')).eql(true) 148 | .expect(counterRedoBtn.hasAttribute('disabled')).eql(false) 149 | .click(counterRedoBtn) 150 | .click(counterRedoBtn) 151 | .click(counterRedoBtn) 152 | .click(counterRedoBtn) 153 | .click(counterRedoBtn) 154 | .click(counterRedoBtn) 155 | .click(counterRedoBtn) 156 | .click(counterRedoBtn) 157 | .click(counterRedoBtn) 158 | .expect(counterBtn.innerText).eql('Clicked 18 times') 159 | .expect(counterRedoBtn.hasAttribute('disabled')).eql(true) 160 | .click(counterBtn) 161 | .expect(counterBtn.innerText).eql('Clicked 19 times') 162 | .click(counterBtn) 163 | .expect(counterBtn.innerText).eql('Clicked 20 times') 164 | .click(counterUndoBtn) 165 | .click(counterUndoBtn) 166 | .click(counterUndoBtn) 167 | .click(counterUndoBtn) 168 | .click(counterUndoBtn) 169 | .click(counterUndoBtn) 170 | .click(counterUndoBtn) 171 | .click(counterUndoBtn) 172 | .click(counterUndoBtn) 173 | .click(counterUndoBtn) 174 | .expect(counterBtn.innerText).eql('Clicked 2 times') 175 | .expect(counterUndoBtn.hasAttribute('disabled')).eql(true) 176 | }) -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Svelte app 7 | 8 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /tests/src/App.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |

Hello {$name}

15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | Only even number as saved in the store history. The maximum number of remembered value is 10. 23 | (If you go to 20, you can only go back to 2) 24 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | 4 | "include": ["src/*", "src/node_modules"], 5 | "exclude": ["node_modules/*", "docs/*", "dist/*"], 6 | "compilerOptions": { 7 | "emitDeclarationOnly": true, 8 | "declaration": true, 9 | "declarationDir": "types", 10 | "sourceMap": false 11 | }, 12 | "files": ["src/index.ts"] 13 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/"], 3 | "theme": "pages-plugin" 4 | } 5 | --------------------------------------------------------------------------------