├── mock-local-storage.code-workspace ├── .gitlab └── dependabot.yml ├── .gitignore ├── LICENSE ├── package.json ├── src └── mock-localstorage.js ├── .gitlab-ci.yml ├── README.md └── test └── mock-localstorage.js /mock-local-storage.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /.gitlab/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule.interval: "daily" 6 | assignees: 7 | - KaiSforza 8 | labels: 9 | - dependabot -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Autogenerated 6 | lib/ 7 | # Mocha test results 8 | test-results.xml 9 | coverage/ 10 | .nyc_output/ 11 | # npm pack output 12 | *.tgz 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2021 letsrock-today 4 | Copyright (c) 2021-2022 Kai Giokas 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock-local-storage", 3 | "version": "1.1.24", 4 | "description": "Mock localStorage for headless unit tests", 5 | "main": "lib/mock-localstorage.js", 6 | "module": "src/mock-localstorage.js", 7 | "browser": "lib/mock-localstorage.js", 8 | "scripts": { 9 | "test": "nyc --reporter=text --reporter=cobertura mocha --require @babel/register", 10 | "compile": "babel -d lib/ src/", 11 | "prepare": "npm run compile" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://gitlab.com/kaictl/node/mock-local-storage" 16 | }, 17 | "keywords": [ 18 | "localstorage", 19 | "sessionstorage", 20 | "mock", 21 | "test", 22 | "mocha", 23 | "headless" 24 | ], 25 | "contributors": [ 26 | { 27 | "name": "Nikolay Turpitko" 28 | }, 29 | { 30 | "name": "Kai Giokas", 31 | "email": "kai@kaictl.me", 32 | "url": "https://gitlab.com/kaictl" 33 | } 34 | ], 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://gitlab.com/kaictl/node/mock-local-storage/issues" 38 | }, 39 | "homepage": "https://gitlab.com/kaictl/node/mock-local-storage", 40 | "devDependencies": { 41 | "@babel/cli": "^7.21.5", 42 | "@babel/core": "^7.21.8", 43 | "@babel/eslint-parser": "^7.21.8", 44 | "@babel/preset-env": "^7.21.5", 45 | "@babel/register": "^7.16.0", 46 | "chai": "^4.3.4", 47 | "eslint": "^8.39.0", 48 | "eslint-plugin-security": "^1.4.0", 49 | "mocha": "^10.0.0", 50 | "mocha-junit-reporter": "^2.0.2", 51 | "nyc": "^15.1.0" 52 | }, 53 | "dependencies": { 54 | "core-js": "^3.30.2", 55 | "global": "^4.3.2" 56 | }, 57 | "eslintConfig": { 58 | "parser": "@babel/eslint-parser", 59 | "plugins": [ 60 | "security" 61 | ], 62 | "extends": [ 63 | "plugin:security/recommended" 64 | ] 65 | }, 66 | "babel": { 67 | "presets": [ 68 | "@babel/preset-env" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/mock-localstorage.js: -------------------------------------------------------------------------------- 1 | // Mock localStorage 2 | (function () { 3 | 4 | function createStorage() { 5 | let UNSET = Symbol(); 6 | let s = {}; 7 | let noopCallback = () => {}; 8 | let _itemInsertionCallback = noopCallback; 9 | 10 | Object.defineProperty(s, 'setItem', { 11 | get: () => { 12 | return (k, v = UNSET) => { 13 | if (v === UNSET) { 14 | throw new TypeError(`Failed to execute 'setItem' on 'Storage': 2 arguments required, but only 1 present.`); 15 | } 16 | if (!s.hasOwnProperty(String(k))) { 17 | _itemInsertionCallback(s.length); 18 | } 19 | s[String(k)] = String(v); 20 | }; 21 | } 22 | }); 23 | 24 | Object.defineProperty(s, 'getItem', { 25 | get: () => { 26 | return k => { 27 | if (s.hasOwnProperty(String(k))) { 28 | return s[String(k)]; 29 | } else { 30 | return null; 31 | } 32 | }; 33 | } 34 | }); 35 | 36 | Object.defineProperty(s, 'removeItem', { 37 | get: () => { 38 | return k => { 39 | if (s.hasOwnProperty(String(k))) { 40 | delete s[String(k)]; 41 | } 42 | }; 43 | } 44 | }); 45 | 46 | Object.defineProperty(s, 'clear', { 47 | get: () => { 48 | return () => { 49 | for (let k in s) { 50 | delete s[String(k)]; 51 | } 52 | }; 53 | } 54 | }); 55 | 56 | Object.defineProperty(s, 'length', { 57 | get: () => { 58 | return Object.keys(s).length; 59 | } 60 | }); 61 | 62 | Object.defineProperty(s, "key", { 63 | value: k => { 64 | let key = Object.keys(s)[String(k)]; 65 | return (!key) ? null : key; 66 | }, 67 | }); 68 | 69 | Object.defineProperty(s, 'itemInsertionCallback', { 70 | get: () => { 71 | return _itemInsertionCallback; 72 | }, 73 | set: v => { 74 | if (!v || typeof v != 'function') { 75 | v = noopCallback; 76 | } 77 | _itemInsertionCallback = v; 78 | } 79 | }); 80 | 81 | return s; 82 | } 83 | 84 | const global = require("global") 85 | const window = require("global/window") 86 | 87 | Object.defineProperty(global, 'Storage', { 88 | value: createStorage, 89 | }); 90 | Object.defineProperty(window, 'Storage', { 91 | value: createStorage, 92 | }); 93 | 94 | Object.defineProperty(global, 'localStorage', { 95 | value: createStorage(), 96 | }); 97 | Object.defineProperty(window, 'localStorage', { 98 | value: global.localStorage, 99 | }); 100 | 101 | Object.defineProperty(global, 'sessionStorage', { 102 | value: createStorage(), 103 | }); 104 | Object.defineProperty(window, 'sessionStorage', { 105 | value: global.sessionStorage, 106 | }); 107 | }()); 108 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - template: Jobs/Dependency-Scanning.gitlab-ci.yml 3 | - template: Jobs/SAST.gitlab-ci.yml 4 | - template: Jobs/Secret-Detection.gitlab-ci.yml 5 | # - template: Jobs/License-Scanning.gitlab-ci.yml 6 | - project: 'dependabot-gitlab/dependabot-standalone' 7 | file: '.gitlab-ci.yml' 8 | 9 | variables: 10 | SAST_EXCLUDED_PATHS: 'spec, test, tests, tmp, lib, node_modules' 11 | 12 | .nodestuff: &nodestuff 13 | image: ${CI_DEP_PREFIX}node:lts-alpine 14 | cache: 15 | key: 16 | files: 17 | - package-lock.json 18 | paths: 19 | - node_modules 20 | 21 | install: 22 | stage: build 23 | script: 24 | - npm install 25 | - npm pack 26 | artifacts: 27 | paths: 28 | - ./lib/*.js 29 | - ./*.tgz 30 | <<: *nodestuff 31 | 32 | test: 33 | stage: test 34 | needs: 35 | - install 36 | script: 37 | - npm run test -- --reporter mocha-junit-reporter 38 | coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' 39 | artifacts: 40 | reports: 41 | junit: test-results.xml 42 | coverage_report: 43 | coverage_format: cobertura 44 | path: coverage/cobertura-coverage.xml 45 | <<: *nodestuff 46 | 47 | .pubCheck: &pubCheck 48 | # Compare the versions in git and in node to verify that they are the same 49 | - | 50 | NODE_VERSION=$(node -e "r=require('./package.json');console.log(r.version);") 51 | GIT_VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//') 52 | if [ "$NODE_VERSION" == "$GIT_VERSION" ]; then 53 | echo "Versions match. Continuing." 54 | else 55 | echo "Error, versions do not match." 56 | exit 3 57 | fi 58 | - echo "CI_SHORT_VERSION=$NODE_VERSION" > dotenv 59 | 60 | publish: 61 | <<: *nodestuff 62 | stage: deploy 63 | needs: 64 | - install 65 | - test 66 | rules: 67 | - if: $CI_COMMIT_TAG 68 | before_script: *pubCheck 69 | script: 70 | - echo "//registry.npmjs.org/:_authToken=$VAR_NPM_AUTHTOKEN" > ~/.npmrc 71 | - npm publish || echo "This failed but thats okay." 72 | environment: 73 | name: npmjs 74 | url: https://www.npmjs.com/package/$CI_PROJECT_NAME 75 | artifacts: 76 | reports: 77 | dotenv: './dotenv' 78 | 79 | publish_gitlab: 80 | <<: *nodestuff 81 | stage: deploy 82 | needs: 83 | - install 84 | - test 85 | rules: 86 | - if: $CI_COMMIT_TAG 87 | before_script: *pubCheck 88 | script: 89 | - | 90 | echo "@${CI_PROJECT_ROOT_NAMESPACE}:registry=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/" > ~/.npmrc 91 | echo "${CI_API_V4_URL#https?}/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=\${CI_JOB_TOKEN}" >> ~/.npmrc 92 | - | 93 | sed -i "s/\(^ \"name\": \)\"\($CI_PROJECT_NAME\)\"/\1\"@$CI_PROJECT_ROOT_NAMESPACE\/\2\"/" package.json 94 | - npm publish 95 | environment: 96 | name: gitlab-npm 97 | url: $CI_PROJECT_URL/-/packages?type=npm&orderBy=name&sort=desc&search[]= 98 | 99 | release: 100 | stage: deploy 101 | image: registry.gitlab.com/gitlab-org/release-cli:latest 102 | needs: 103 | - publish 104 | rules: 105 | - if: $CI_COMMIT_TAG 106 | script: 107 | - echo "Creating the release at $CI_COMMIT_TAG" 108 | release: 109 | name: "mock-local-storage $CI_COMMIT_TAG" 110 | tag_name: $CI_COMMIT_TAG 111 | ref: $CI_COMMIT_TAG 112 | description: "Automated release for mock-local-storage at $CI_COMMIT_TAG" 113 | milestones: 114 | - $CI_COMMIT_TAG 115 | assets: 116 | links: 117 | - name: mock-local-storage@$CI_COMMIT_TAG 118 | url: https://www.npmjs.com/package/$CI_PROJECT_NAME/v/$CI_SHORT_VERSION -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://gitlab.com/kaictl/node/mock-local-storage/badges/master/pipeline.svg)](https://gitlab.com/kaictl/node/mock-local-storage/) 2 | [![Code Coverage](https://gitlab.com/kaictl/node/mock-local-storage/badges/master/coverage.svg?job=test)](https://gitlab.com/kaictl/node/mock-local-storage/) 3 | 4 | # mock-local-storage 5 | 6 | Mock `localStorage` for headless unit tests 7 | 8 | Inspired by StackOverflow answers and wrapped into npm package. 9 | 10 | ## Moving Notice 11 | 12 | This package has moved from the [letsrock-today][lrt] repository into 13 | [KaiSforza's GitLab repository.][glab] 14 | 15 | There is a [copy][copy] of the code in Github, as well, but issues and pull 16 | requests will only be watched in Gitlab. 17 | 18 | [lrt]: https://github.com/letsrock-today/mock-local-storage 19 | [glab]: https://gitlab.com/kaictl/node/mock-local-storage 20 | [copy]: https://github.com/KaiSforza/mock-local-storage 21 | 22 | ## Motivation 23 | 24 | Used to mock `localStorage` to run headless tests of cache implementation in terminal (ie. without browser). 25 | 26 | ## Installation 27 | 28 | npm install mock-local-storage --save-dev 29 | 30 | ## Usage 31 | 32 | ### Mocha 33 | 34 | Require in Mocha, which will replace `localStorage` and `sessionStorage` on the `global` and `window` objects: 35 | 36 | mocha --require mock-local-storage 37 | 38 | If you are using `jsdom-global`, make sure it is required before `mock-local-storage`: 39 | 40 | mocha --require jsdom-global --require mock-local-storage 41 | 42 | ### Other testing frameworks 43 | 44 | In a node environment you can mock the `window.localStorage` as follows: 45 | 46 | ```js 47 | global.window = {} 48 | import 'mock-local-storage' 49 | window.localStorage = global.localStorage 50 | ``` 51 | 52 | This is very useful when you want to run headless tests on code meant for the browser that use `localStorage` 53 | 54 | You can even store this in a file that is reused across tests: 55 | 56 | `mock-localstorage.js` 57 | 58 | ```js 59 | global.window = {} 60 | import 'mock-local-storage' 61 | window.localStorage = global.localStorage 62 | ``` 63 | 64 | `using-localstorage.test.js` 65 | 66 | ```js 67 | import './mock-localstorage' 68 | 69 | // unit tests follow here 70 | ``` 71 | 72 | ### Extra 73 | 74 | Besides mocking of conventional `localStorage` interface, this implementation provides 75 | a way for test code to register a callback to be invoked on item insertion. 76 | Mock implementation will invoke it when `localStorage.setItem()` is called 77 | (but not with `localStorage[key]` notation). 78 | 79 | It can be used to emulate allocation errors, like this: 80 | ```js 81 | describe('test with mock localStorage', () => { 82 | afterEach(() => { 83 | localStorage.clear(); 84 | // remove callback 85 | localStorage.itemInsertionCallback = null; 86 | }); 87 | it('emulate quota exceeded error', () => { 88 | localStorage.length.should.equal(0); 89 | // register callback 90 | localStorage.itemInsertionCallback = (len) => { 91 | if (len >= 5) { 92 | let err = new Error('Mock localStorage quota exceeded'); 93 | err.code = 22; 94 | throw err; 95 | } 96 | }; 97 | let handled = false; 98 | try { 99 | for (let i = 0; i < 10; ++i) { 100 | localStorage.setItem(i, i); 101 | } 102 | } catch (e) { 103 | if (e.code == 22) { 104 | // handle quota exceeded error 105 | handled = true; 106 | } 107 | } 108 | handled.should.be.true; 109 | localStorage.length.should.equal(5); 110 | }); 111 | }); 112 | ``` 113 | 114 | ### Caveats 115 | 116 | There are some caveats with using `index` operator. Browser's `localStorage` 117 | works with strings and stringifyes objects stored via `localStorage[key]` notation, 118 | but this implementation does not. 119 | 120 | `localStorage.itemInsertionCallback` won't be invoked with `localStorage[key]` notation. 121 | 122 | ## Tests 123 | 124 | npm install 125 | npm test 126 | 127 | ## Bugs, issues, MRs, participation, contribution 128 | 129 | Please feel free to send us occusionall MRs along with unit tests, 130 | we'll merge them if they successfully build and pass unit tests. 131 | Consider to always provide unit tests, illustrating your problem, along with PR to avoid future regression. 132 | 133 | ## License 134 | 135 | [MIT](https://gitlab.com/kaictl/node/mock-local-storage/-/blob/master/LICENSE) 136 | -------------------------------------------------------------------------------- /test/mock-localstorage.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import storage from '../src/mock-localstorage'; 3 | 4 | describe('# localStorage', () => { 5 | beforeEach(() => { 6 | localStorage.clear(); 7 | }); 8 | afterEach(() => { 9 | localStorage.clear(); 10 | localStorage.itemInsertionCallback = null; 11 | }); 12 | it('len', () => { 13 | expect(localStorage.length).to.equal(0); 14 | localStorage.setItem(42, 42); 15 | expect(localStorage.length).to.equal(1); 16 | for (let i = 0; i < 100; ++i) { 17 | localStorage.setItem(i, i); 18 | } 19 | expect(localStorage.length).to.equal(100); 20 | }); 21 | it('get and set', () => { 22 | expect(localStorage.length).to.equal(0); 23 | expect(localStorage.getItem(42)).to.be.null; 24 | for (let i = 0; i < 100; ++i) { 25 | localStorage.setItem(i, i); 26 | } 27 | expect(localStorage.getItem(42)).to.equal('42'); 28 | expect(function () { 29 | localStorage.setItem('setItem', 42); 30 | }).to.throw(TypeError); 31 | expect(function () { 32 | localStorage.setItem('length', 42); 33 | }).to.throw(TypeError); 34 | }); 35 | it('get and set fail', () => { 36 | expect(function() {localStorage.setItem('setItem');}) 37 | .to.throw(TypeError) 38 | }); 39 | it('[]', () => { 40 | expect(localStorage.length).to.equal(0); 41 | //don't know how to implement: 42 | //should.equal(localStorage['42'], null); 43 | expect(localStorage['42']).to.not.exist; 44 | for (let i = 0; i < 100; ++i) { 45 | localStorage[i + ''] = i + ''; 46 | } 47 | expect(localStorage.length).to.equal(100); 48 | expect(localStorage[42]).to.equal('42'); 49 | expect(function () { 50 | localStorage['setItem'] = '42'; 51 | }).to.throw(TypeError); 52 | expect(function () { 53 | localStorage['length'] = 42; 54 | }).to.throw(TypeError); 55 | expect(function () { 56 | localStorage.setItem = '42'; 57 | }).to.throw(TypeError); 58 | expect(function () { 59 | localStorage.length = 42; 60 | }).to.throw(TypeError); 61 | //don't know how to implement: 62 | //localStorage[17] = 77; 63 | //localStorage[17].should.equal('77'); 64 | localStorage[15] = '55'; 65 | expect(localStorage[15]).to.equal('55'); 66 | expect(localStorage['15']).to.equal('55'); 67 | expect(localStorage.length).to.equal(100); 68 | }); 69 | it('iterate', () => { 70 | expect(localStorage.length).to.equal(0); 71 | for (let i = 0; i < 100; ++i) { 72 | localStorage.setItem(i, i); 73 | } 74 | let i = 0; 75 | for (let k in localStorage) { 76 | expect(localStorage.getItem(localStorage.key(k))).to.equal(i + ''); 77 | ++i; 78 | } 79 | }); 80 | it('remove and clear', () => { 81 | expect(localStorage.length).to.equal(0); 82 | localStorage.removeItem(0); // Test removing nonexistent 83 | expect(localStorage.length).to.equal(0); 84 | for (let i = 0; i < 100; ++i) { 85 | localStorage.setItem(i, i); 86 | } 87 | localStorage.removeItem('a'); // Test removing nonexistent 88 | expect(localStorage.length).to.equal(100); 89 | localStorage.removeItem(42); 90 | expect(localStorage.getItem(42)).to.be.null; 91 | expect(localStorage.length).to.equal(99); 92 | for (let i = 0; i < 10;) { 93 | let k = Math.trunc(Math.random() * 100); 94 | if (localStorage.getItem(k)) { 95 | ++i; 96 | } 97 | localStorage.removeItem(k); 98 | } 99 | expect(localStorage.length).to.equal(89); 100 | localStorage.clear(); 101 | expect(localStorage.length).to.equal(0); 102 | }); 103 | it('insertion callback', () => { 104 | expect(localStorage.length).to.equal(0); 105 | let f = (len) => { 106 | if (len >= 5) { 107 | let err = new Error('Mock localStorage quota exceeded'); 108 | err.code = 22; 109 | throw err; 110 | } 111 | } 112 | localStorage.itemInsertionCallback = f 113 | let handled = false; 114 | try { 115 | for (let i = 0; i < 10; ++i) { 116 | localStorage.setItem(i, i); 117 | } 118 | } catch (e) { 119 | if (e.code == 22) { 120 | // handle quota exceeded error 121 | handled = true; 122 | } 123 | } 124 | expect(handled).to.be.true; 125 | expect(localStorage.length).to.equal(5); 126 | expect(localStorage.itemInsertionCallback).to.equal(f); 127 | }); 128 | it('key', () => { 129 | expect(localStorage.length).to.equal(0); 130 | for (let i = 0; i < 100; ++i) { 131 | localStorage.setItem(i, i); 132 | } 133 | expect(localStorage.key(0)).to.equal('0'); 134 | expect(localStorage.key('a')).to.be.null; 135 | }) 136 | }); 137 | --------------------------------------------------------------------------------