├── .nvmrc ├── index.js ├── .babelrc ├── .gitignore ├── .npmignore ├── demo ├── mock-reject.js ├── mock-resolve.js ├── mock-async-resolve.js ├── readdir.js ├── events.js ├── memfs.js ├── promise.js ├── props.js ├── async.js └── basic.js ├── .travis.yml ├── renovate.json ├── gulpfile.js ├── package.json ├── src ├── index.js └── index.test.js ├── README.md └── lib ├── index.js └── index.test.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.17.0 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('lib/index'); 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "es2016", "es2017", "stage-0", "flow"], 3 | "comments": false 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | /.idea 4 | .nyc_output 5 | coverage 6 | package-lock.json 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | /typings 3 | example.js 4 | tsd.json 5 | simple.js 6 | node_modules 7 | /test 8 | .idea 9 | .idea/ 10 | .nyc_output 11 | coverage 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /demo/mock-reject.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {spy} from '../src'; 3 | 4 | 5 | const sfs = spy(fs, function(action) { 6 | action.reject(Error('Ups')); 7 | }); 8 | 9 | sfs.readdirSync('/'); 10 | -------------------------------------------------------------------------------- /demo/mock-resolve.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {spy} from '../src'; 3 | 4 | 5 | const sfs = spy(fs, function(action) { 6 | action.resolve(['lol']); 7 | }); 8 | 9 | console.log(sfs.readdirSync('/')); 10 | -------------------------------------------------------------------------------- /demo/mock-async-resolve.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {spy} from '../src'; 3 | 4 | 5 | const sfs = spy(fs, function(action) { 6 | action.resolve(['lol-async']); 7 | }); 8 | 9 | console.log(sfs.readdir('/', () => {})); 10 | -------------------------------------------------------------------------------- /demo/readdir.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {spy} from "../src/index"; 3 | 4 | 5 | const sfs = spy(fs, async function(action) { 6 | console.log(await action); // prints directory files... 7 | }); 8 | 9 | sfs.readdir(__dirname, () => {}); 10 | -------------------------------------------------------------------------------- /demo/events.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {spy} from "../src/index"; 3 | 4 | 5 | const sfs = spy(fs); 6 | 7 | sfs.on('readdirSync', async function(action) { 8 | console.log(action.args, await action); 9 | }); 10 | 11 | sfs.readdirSync('/'); 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | os: 3 | - linux 4 | language: node_js 5 | node_js: 6 | - '8' 7 | - '7' 8 | - '6' 9 | - '5' 10 | matrix: 11 | allow_failures: [] 12 | fast_finish: true 13 | cache: 14 | yarn: true 15 | directories: 16 | - "node_modules" 17 | -------------------------------------------------------------------------------- /demo/memfs.js: -------------------------------------------------------------------------------- 1 | import {fs} from 'memfs'; 2 | import {spy} from "../src/index"; 3 | 4 | 5 | const sfs = spy(fs, async function(action) { 6 | console.log(await action); 7 | }); 8 | 9 | sfs.writeFile('/foo', 'bar', () => {}); 10 | sfs.readFile('/foo', 'utf8', () => {}); 11 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "pinVersions": false, 7 | "major": { 8 | "automerge": false 9 | }, 10 | "devDependencies": { 11 | "automerge": true, 12 | "pinVersions": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demo/promise.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {spy} from "../src/index"; 3 | 4 | 5 | const sfs = spy(fs, action => { 6 | action.catch(err => { 7 | console.log(err.message); // ENOENT: no such file or directory, access '/foo' 8 | }) 9 | }); 10 | 11 | try { 12 | sfs.accessSync('/foo'); 13 | } catch (err) {} 14 | -------------------------------------------------------------------------------- /demo/props.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {spy} from "../src/index"; 3 | 4 | 5 | const sfs = spy(fs, action => { 6 | console.log(action.method); // readdir, readdirSync 7 | console.log(action.isAsync); // true, false 8 | console.log(action.args); // [ '/' ], [ '/' ] 9 | }); 10 | 11 | sfs.readdir('/', () => {}); 12 | sfs.readdirSync('/'); 13 | -------------------------------------------------------------------------------- /demo/async.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {spy} from "../src/index"; 3 | 4 | 5 | const sfs = spy(fs, async function({pause, unpause, exec, resolve}) { 6 | pause(); 7 | try { 8 | await exec(); 9 | } catch(err) { 10 | resolve([['lol']]); 11 | } 12 | unpause(); 13 | }); 14 | 15 | sfs.readdir('/', (err, res) => { 16 | console.log(err, res); 17 | }); 18 | -------------------------------------------------------------------------------- /demo/basic.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {spy} from '../src'; 3 | 4 | 5 | const sfs = spy(fs); 6 | sfs.subscribe(async function(action) { 7 | const result = await action; 8 | console.log(result); 9 | }); 10 | 11 | sfs.on('readFile', async function(action) { 12 | const result = await action; 13 | console.log(result); 14 | }); 15 | 16 | sfs.readFile('./index.js', 'utf8', () => {}); 17 | 18 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var ts = require('gulp-typescript'); 3 | 4 | 5 | gulp.task('build-ts', function () { 6 | return gulp.src('src/**/*.ts') 7 | .pipe(ts({ 8 | "target": "es5", 9 | "module": "commonjs", 10 | "removeComments": false, 11 | "noImplicitAny": false, 12 | "sourceMap": false, 13 | })) 14 | .pipe(gulp.dest('lib')); 15 | }); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spyfs", 3 | "version": "1.0.2", 4 | "description": "Filesystem spy", 5 | "main": "lib/index.js", 6 | "keywords": [ 7 | "fs", 8 | "spy", 9 | "filesystem", 10 | "fs.js", 11 | "file", 12 | "file system", 13 | "test", 14 | "testing", 15 | "mock" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/streamich/spyfs.git" 20 | }, 21 | "dependencies": { 22 | "fs-monkey": "0.4.0" 23 | }, 24 | "devDependencies": { 25 | "mocha": "11.7.2", 26 | "chai": "6.0.1", 27 | "typescript": "5.9.2", 28 | "ts-node": "10.9.2", 29 | "babel-cli": "6.26.0", 30 | "babel-polyfill": "6.26.0", 31 | "babel-preset-stage-0": "6.24.1", 32 | "babel-preset-es2015": "6.24.1", 33 | "babel-preset-es2016": "6.24.1", 34 | "babel-preset-es2017": "6.24.1", 35 | "babel-preset-flow": "6.23.0", 36 | "gulp": "5.0.1", 37 | "gulp-typescript": "5.0.1", 38 | "source-map-support": "0.5.21", 39 | "nyc": "17.1.0", 40 | "watch": "1.0.2", 41 | "memfs": "4.42.0" 42 | }, 43 | "nyc": { 44 | "per-file": true, 45 | "include": [ 46 | "src/**/*.js" 47 | ], 48 | "exclude": [ 49 | "src/**/*.test.js" 50 | ], 51 | "extension": [ 52 | ".js" 53 | ], 54 | "require": [ 55 | "ts-node/register" 56 | ], 57 | "reporter": [ 58 | "text", 59 | "json", 60 | "lcov", 61 | "text-summary" 62 | ], 63 | "sourceMap": true, 64 | "instrument": true, 65 | "cache": true 66 | }, 67 | "scripts": { 68 | "build": "npm run build-ts && npm run build-js", 69 | "build-ts": "gulp build-ts", 70 | "build-js": "babel src --out-dir lib", 71 | "test": "npm run test-basic-js", 72 | "test-basic-js": "mocha --compilers js:babel-core/register --require babel-polyfill src/**/*.test.js", 73 | "test-watch-js": "mocha --compilers js:babel-core/register --require babel-polyfill src/**/*.test.js --watch", 74 | "test-coverage-ts": "nyc --per-file mocha --compilers js:babel-core/register --require babel-polyfill --require source-map-support/register --full-trace --bail src/**/*.test.js", 75 | "watch": "watch 'npm run build' ./src" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {fsSyncMethods, fsAsyncMethods} from 'fs-monkey/lib/util/lists'; 2 | import {EventEmitter} from 'events'; 3 | 4 | 5 | interface ActionBase extends Promise { 6 | method: string, 7 | isAsync: boolean, 8 | args: any[], 9 | } 10 | 11 | interface ActionSync extends ActionBase { 12 | result: ActionSync; 13 | resolve: (result: any) => void; 14 | reject: (error: Error) => void; 15 | exec: (...args) => any; 16 | } 17 | 18 | interface ActionAsync extends ActionBase { 19 | result: ActionAsync; 20 | resolve: (results: any[]) => void; 21 | reject: (error: Error) => void; 22 | exec: () => Promise; 23 | pause: () => void; 24 | unpause: () => void; 25 | proceed: () => void; 26 | } 27 | 28 | type TListener = (action: ActionAsync | ActionAsync) => void; 29 | 30 | 31 | const noop = () => {}; 32 | 33 | 34 | function createAction(method, isAsync, args, callback): ActionBase { 35 | const promise = new Promise(callback); 36 | promise.method = method; 37 | promise.isAsync = isAsync; 38 | promise.args = args; 39 | return promise; 40 | } 41 | 42 | 43 | export class Spy extends EventEmitter { 44 | 45 | constructor(fs, listener: ?TListener) { 46 | super(); 47 | 48 | for(let method of fsSyncMethods) { 49 | const func = fs[method]; 50 | if(typeof func !== 'function') continue; 51 | this[method] = this._createSyncMethod(fs, method, func); 52 | } 53 | 54 | for(let method of fsAsyncMethods) { 55 | const func = fs[method]; 56 | if(typeof func !== 'function') continue; 57 | 58 | // Special case, `exists` is not supported. 59 | if(method === 'exists') { 60 | this[method] = fs[method].bind(fs); 61 | continue; 62 | } 63 | 64 | this[method] = this._createAsyncMethod(fs, method, func); 65 | } 66 | 67 | if(listener) this.subscribe(listener); 68 | } 69 | 70 | _createSyncMethod(fs, method, func) { 71 | return (...args) => { 72 | let result, error; 73 | 74 | function exec() { 75 | try { 76 | result = func.apply(fs, args); 77 | error = undefined; 78 | } catch (reason) { 79 | result = undefined; 80 | error = reason; 81 | } 82 | } 83 | 84 | function returnOrThrow() { 85 | if(typeof result !== 'undefined') { 86 | return result; 87 | } else { 88 | throw error; 89 | } 90 | } 91 | 92 | const action = createAction(method, false, args, (resolve, reject) => { 93 | process.nextTick(() => { 94 | if(typeof result !== 'undefined') resolve(result); 95 | else reject(error); 96 | }); 97 | }); 98 | 99 | action.result = action; 100 | 101 | action.resolve = value => { 102 | result = value; 103 | error = undefined; 104 | }; 105 | 106 | action.reject = reason => { 107 | result = undefined; 108 | error = reason; 109 | }; 110 | 111 | action.exec = () => { 112 | exec(); 113 | return returnOrThrow(); 114 | }; 115 | 116 | // To disable Node's: 117 | // DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections 118 | // that are not handled will terminate the Node.js process with a non-zero exit code. 119 | action.catch(noop); 120 | 121 | this.emit(action); 122 | 123 | if(typeof result !== 'undefined') { 124 | return result; 125 | } else if(typeof error !== 'undefined') { 126 | throw error; 127 | } else { 128 | exec(); 129 | return returnOrThrow(); 130 | } 131 | }; 132 | } 133 | 134 | /** 135 | * Ways async methods can be executed: 136 | * 137 | * 1. User does not intervene, call is simply executed 138 | * 2. User immediately (same event loop cycle) `.reject`s or `.resolve`s the action. 139 | * 3. Use rpauses the action and then: 140 | * 3.1 Unpauses, or 141 | * 3.2 Rejects or resolves 142 | * 143 | * User should be able to pause, reject, and resolve only once. 144 | * 145 | * @param fs 146 | * @param method 147 | * @param func 148 | * @returns {function(...[*]=)} 149 | * @private 150 | */ 151 | _createAsyncMethod(fs, method, func) { 152 | return (...args) => { 153 | const callback = args[args.length - 1]; 154 | if(typeof callback !== 'function') return func.apply(fs, args); 155 | 156 | let paused = false, proceeding = false, finished = false; 157 | 158 | 159 | // The actual resolve and reject methods from the action Promise. 160 | let _resolve, _reject; 161 | 162 | function resolve(value) { 163 | if(!finished) { 164 | finished = true; 165 | value = value instanceof Array ? value : [value]; 166 | _resolve(value); 167 | if(value instanceof Array) callback(null, ...value); 168 | else callback(null, value); 169 | } 170 | } 171 | 172 | function reject(reason) { 173 | if(!finished) { 174 | finished = true; 175 | _reject(reason); 176 | callback(reason); 177 | } 178 | } 179 | 180 | 181 | // Cache for `exec` 182 | let _exec; 183 | 184 | // Executes the real filesystem call. 185 | function exec() { 186 | if(_exec) return _exec; 187 | 188 | _exec = new Promise((resolve, reject) => { 189 | args[args.length - 1] = (reason, ...results) => { 190 | if(reason) reject(reason); 191 | else resolve(results); 192 | }; 193 | func.apply(fs, args); 194 | }); 195 | 196 | // To disable Node's: 197 | // DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections 198 | // that are not handled will terminate the Node.js process with a non-zero exit code. 199 | _exec.catch(noop); 200 | 201 | return _exec; 202 | } 203 | 204 | 205 | // Proceed with executing the real fs call. 206 | function proceed() { 207 | proceeding = true; 208 | exec().then(result => resolve(result), err => reject(err)); 209 | } 210 | 211 | 212 | const action = createAction(method, true, args.slice(0, args.length - 1), (resolve, reject) => { 213 | _resolve = resolve; 214 | _reject = reject; 215 | 216 | process.nextTick(() => { 217 | this.emit(action); 218 | setImmediate(() => { 219 | if(!paused && !proceeding) proceed(); 220 | }); 221 | }); 222 | }); 223 | 224 | action.result = action; 225 | action.exec = exec; 226 | action.resolve = resolve; 227 | action.reject = reject; 228 | 229 | action.pause = (cb) => { 230 | if(proceeding) 231 | throw Error('Cannot pause anymore, already executing the real filesystem call.'); 232 | if(paused) 233 | throw Error('Already paused once.'); 234 | paused = true; 235 | if(cb) cb(proceed); 236 | }; 237 | action.unpause = proceed; 238 | action.proceed = proceed; 239 | 240 | // To disable Node's: 241 | // DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections 242 | // that are not handled will terminate the Node.js process with a non-zero exit code. 243 | action.catch(noop); 244 | }; 245 | } 246 | 247 | emit(action) { 248 | super.emit('action', action); 249 | super.emit(action.method, action); 250 | } 251 | 252 | subscribe(listener: (action: ActionSync | ActionAsync) => void) { 253 | this.addListener('action', listener); 254 | } 255 | 256 | unsubscribe(listener: ?(action: ActionSync | ActionAsync) => void) { 257 | this.removeListener('action', listener); 258 | } 259 | 260 | on(event, listener) { 261 | this.addListener(event, listener); 262 | } 263 | 264 | off(event, listener) { 265 | this.removeListener(event, listener); 266 | } 267 | } 268 | 269 | 270 | export function spy(fs, listener) { 271 | const sfs = new Spy(fs); 272 | if(typeof listener === 'function') sfs.subscribe(listener); 273 | return sfs; 274 | } 275 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spyfs [![npm-img]][npm-url] 2 | 3 | Spy on filesystem calls. Create file system mocks. Use for testing. 4 | 5 | Install: 6 | 7 | npm install --save spyfs 8 | 9 | Create a new file system that spies: 10 | 11 | ```js 12 | import * as fs from 'fs'; 13 | import {spy} from 'spyfs'; 14 | 15 | const sfs = spy(fs); 16 | ``` 17 | 18 | Now you can use `sfs` for all your filesystem operations. 19 | 20 | ```js 21 | const data = sfs.readFileSync('./package.json').toString(); 22 | ``` 23 | 24 | Subscribe to all actions happening on that filesystem: 25 | 26 | ```js 27 | sfs.subscribe(action => { 28 | // ... 29 | }); 30 | ``` 31 | 32 | Every time somebody uses `sfs`, the subscription callback will be called. 33 | You will receive a single argument: an `action` which is a `Promise` object 34 | containing all the information about the performed filesystem operation and its result. 35 | 36 | You can also subscribe by providing a listener at creation: 37 | 38 | ```js 39 | const sfs = spy(fs, action => { 40 | // ... 41 | }); 42 | ``` 43 | 44 | 45 | ### Want to spy on real filesystem? 46 | 47 | Overwrite the real `fs` module using [`fs-monkey`][fs-monkey] to spy on all filesystem 48 | calls: 49 | 50 | ```js 51 | import {patchFs} from 'fs-monkey'; 52 | 53 | patchFs(sfs); 54 | ``` 55 | 56 | 57 | ### Use `async/await` 58 | 59 | `spyfs` returns *actions* which are instances of the `Promise` constructor, 60 | so you can use *asynchronous* functions for convenience: 61 | 62 | ```js 63 | const sfs = spy(fs, async function(action) { 64 | console.log(await action); // prints directory files... 65 | }); 66 | 67 | sfs.readdir('/', () => {}); 68 | ``` 69 | 70 | 71 | ### Use with [`memfs`][memfs] 72 | 73 | You can use `spyfs` with any *fs-like* object, including [`memfs`][memfs]: 74 | 75 | ```js 76 | import {fs} from 'memfs'; 77 | import {spy} from 'spyfs'; 78 | 79 | const sfs = spy(fs, async function(action) { 80 | console.log(await action); // bar 81 | }); 82 | 83 | sfs.writeFile('/foo', 'bar', () => {}); 84 | sfs.readFile('/foo', 'utf8', () => {}); 85 | ``` 86 | 87 | 88 | ### Action properties 89 | 90 | `spyfs` actions have extra properties that tell you more about the action 91 | being executed: 92 | 93 | - `action.method` *(string)* - name of filesystem method called. 94 | - `action.isAsync` *(boolean)* - whether the filesystem method called was asynchronous. 95 | - `action.args` *(Array)* - list of arguments with which the method was called (sans callback). 96 | 97 | ```js 98 | const sfs = spy(fs, action => { 99 | console.log(action.method); // readdir, readdirSync 100 | console.log(action.isAsync); // true, false 101 | console.log(action.args); // [ '/' ], [ '/' ] 102 | }); 103 | 104 | sfs.readdir('/', () => {}); 105 | sfs.readdirSync('/'); 106 | ``` 107 | 108 | 109 | ### Subscribe to events 110 | 111 | The returned filesystem object is also an event emitter, you can subscribe 112 | to specific filesystem actions using the `.on()` method, in that case you 113 | will receive only actions for that method: 114 | 115 | ```js 116 | sfs.on('readdirSync', async function(action) { 117 | console.log(action.args, await action); 118 | }); 119 | 120 | sfs.readdirSync('/'); 121 | ``` 122 | 123 | Listening for `action` event is equivalent to subscribing using `.subscribe()`. 124 | 125 | ```js 126 | sfs.on('action', listener); 127 | sfs.subscribe(listener); 128 | ``` 129 | 130 | 131 | ## Mock responses 132 | 133 | You can overwrite what is returned by the filesystem call at runtime or even 134 | throw your custom errors, this way you can mock any filesystem call: 135 | 136 | For example, prohibit `readFileSync` for `/usr/foo/.bashrc` file: 137 | 138 | ```js 139 | sfs.on('readFileSync', ({args, reject}) => { 140 | if(args[0] === '/usr/foo/.bashrc') 141 | reject(new Error("Cant't touch this!")); 142 | }); 143 | ``` 144 | 145 | 146 | ### Sync mocking 147 | 148 | #### `action.resolve(result)` 149 | 150 | Returns to the user `result` as successfully executed action, below 151 | all operations `readFileSync` will return `'123'`: 152 | 153 | ```js 154 | sfs.on('readFileSync', action => { 155 | action.resolve('123'); 156 | }); 157 | ``` 158 | 159 | #### `action.reject(error)` 160 | 161 | Throws `error`: 162 | 163 | ```js 164 | sfs.on('statSync', action => { 165 | action.reject(new Error('This filesystem does not support stat')); 166 | }); 167 | ``` 168 | 169 | #### `action.exec()` 170 | 171 | Executes an action user was intended to perform and returns back result 172 | only to you. This method can throw. 173 | 174 | ```js 175 | sfs.on('readFileSync', action => { 176 | const result = action.exec(); 177 | if(result.length > 100) action.reject(new Error('File too long')); 178 | }); 179 | ``` 180 | 181 | #### `action.result` 182 | 183 | `result` is a reference to the `action` for your convenience: 184 | 185 | ### Async mocking 186 | 187 | Just like sync mocking actions support `resolve`, `reject` and `exec` methods, 188 | but, in addition, async mocking also has `pause` and `unpause` methods. 189 | 190 | #### `action.resolve(results)` 191 | 192 | Successfully executes user's filesystem call. `results` is an array, because 193 | some Node's async filesystem calls return more than one result. 194 | 195 | ```js 196 | sfs.on('readFile', ({resolve}) => { 197 | resolve(['123']); 198 | }); 199 | ``` 200 | 201 | #### `action.reject(error)` 202 | 203 | Fails filesystem call and returns your specified error. 204 | 205 | ```js 206 | sfs.on('readFile', ({reject}) => { 207 | reject(new Error('You cannot touch this file!')); 208 | }); 209 | ``` 210 | 211 | #### `action.pause()` 212 | 213 | Pauses the async filesystem call until you un-pause it. 214 | 215 | ```js 216 | sfs.on('readFile', ({pause}) => { 217 | pause(); 218 | // The readFile operation will never end, 219 | // if you don't unpause it. 220 | }); 221 | ``` 222 | 223 | Pausing is useful if you want to perform some other async IO before yielding 224 | back to the original filesystem operation. 225 | 226 | #### `action.unpause()` 227 | 228 | Un-pauses previously pauses filesystem operation: 229 | 230 | ```js 231 | sfs.on('readFile', ({pause, unpause}) => { 232 | // This effectively does nothing: 233 | pause(); 234 | unpause(); 235 | }); 236 | ``` 237 | 238 | #### `action.exec()` 239 | 240 | Executes user's intended filesystem call and returns the result only to you. 241 | Unlike the sync version `exec`, async `exec` returns a promise. 242 | 243 | You should use it together with `pause()` and `unpause()`. 244 | 245 | ```js 246 | sfs.on('readFile', ({exec, pause, unpause, reject}) => { 247 | pause(); 248 | exec().then(result => { 249 | if(result.length < 100) { 250 | reject(new Error('File too small')); 251 | } else { 252 | unpause(); 253 | } 254 | }); 255 | }); 256 | ``` 257 | 258 | Use `async/await` with `exec()`: 259 | 260 | ```js 261 | sfs.on('readFile', async function({exec, pause, unpause, reject}) { 262 | pause(); 263 | let result = await exec(); 264 | if(result.length < 100) { 265 | reject(new Error('File too small')); 266 | } else { 267 | unpause(); 268 | } 269 | }); 270 | ``` 271 | 272 | #### `action.result` 273 | 274 | `result` is a reference to the `action` for your convenience: 275 | 276 | 277 | ## `Spy` constructor 278 | 279 | Create spying filesystems manually: 280 | 281 | ```js 282 | import {Spy} from 'spyfs'; 283 | ``` 284 | 285 | #### `new Spy(fs[, listener])` 286 | 287 | `fs` is the file system to spy on. Note that `Spy` will not overwrite or 288 | in any way modify your original filesystem, but rather it will create a 289 | new object for you. 290 | 291 | `listener` is an optional callback that will be set using the `.subscribe()` 292 | method, see below. 293 | 294 | #### `sfs.subscribe(listener)` 295 | 296 | Subscribe to all filesystem actions: 297 | 298 | ```js 299 | const sfs = new Spy(fs); 300 | sfs.subscribe(action => { 301 | 302 | }); 303 | ``` 304 | 305 | It is equivalent to calling `sfs.on('action', listener)`. 306 | 307 | #### `sfs.unsubscribe(listener)` 308 | 309 | Unsubscribes your listener. It is equivalent to calling `sfs.off('action', listener)`. 310 | 311 | #### `sfs.on(method, listener)` 312 | 313 | Subscribes to a specific filesystem call. 314 | 315 | ```js 316 | sfs.on('readFile', listener); 317 | ``` 318 | 319 | #### `sfs.off(method, listener)` 320 | 321 | Unsubscribes from a specific filesystem call. 322 | 323 | 324 | 325 | [npm-url]: https://www.npmjs.com/package/spyfs 326 | [npm-img]: https://img.shields.io/npm/v/spyfs.svg 327 | [memfs]: https://github.com/streamich/memfs 328 | [unionfs]: https://github.com/streamich/unionfs 329 | [linkfs]: https://github.com/streamich/linkfs 330 | [spyfs]: https://github.com/streamich/spyfs 331 | [fs-monkey]: https://github.com/streamich/fs-monkey 332 | 333 | 334 | 335 | 336 | 337 | # License 338 | 339 | This is free and unencumbered software released into the public domain. 340 | 341 | Anyone is free to copy, modify, publish, use, compile, sell, or 342 | distribute this software, either in source code form or as a compiled 343 | binary, for any purpose, commercial or non-commercial, and by any 344 | means. 345 | 346 | In jurisdictions that recognize copyright laws, the author or authors 347 | of this software dedicate any and all copyright interest in the 348 | software to the public domain. We make this dedication for the benefit 349 | of the public at large and to the detriment of our heirs and 350 | successors. We intend this dedication to be an overt act of 351 | relinquishment in perpetuity of all present and future rights to this 352 | software under copyright law. 353 | 354 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 355 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 356 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 357 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 358 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 359 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 360 | OTHER DEALINGS IN THE SOFTWARE. 361 | 362 | For more information, please refer to 363 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {Volume} from 'memfs'; 3 | import {Spy} from './index'; 4 | 5 | 6 | function create(listener) { 7 | const vol = Volume.fromJSON({ 8 | '/foo': 'bar', 9 | }); 10 | const sfs = new Spy(vol, listener); 11 | return [sfs, vol]; 12 | } 13 | 14 | 15 | describe('SpyFS', () => { 16 | it('Loads without crashing', () => { 17 | create(); 18 | }); 19 | it('Creates new fs with, does not overwrite the old one', () => { 20 | const vol = new Volume; 21 | const readFile = vol.readFile; 22 | const sfs = new Spy(vol); 23 | expect(typeof sfs.readFile).to.equal('function'); 24 | expect(readFile === vol.readFile).to.equal(true); 25 | }); 26 | describe('Sync methods', () => { 27 | it('Returns action promises', () => { 28 | const [sfs, vol] = create(action => { 29 | expect(action).to.be.an.instanceof(Promise); 30 | }); 31 | sfs.readFileSync('/foo', 'utf8'); 32 | }); 33 | it('`result` is just a circular reference to `action`', () => { 34 | const [sfs, vol] = create(action => { 35 | const {result} = action; 36 | expect(result).to.equal(action); 37 | }); 38 | sfs.readFileSync('/foo', 'utf8'); 39 | }); 40 | it('Action promise has expected API', () => { 41 | const [sfs, vol] = create(action => { 42 | const {method, isAsync, args, result, resolve, reject, exec} = action; 43 | 44 | expect(typeof method).to.equal('string'); 45 | expect(typeof isAsync).to.equal('boolean'); 46 | expect(args instanceof Array).to.equal(true); 47 | expect(result instanceof Promise).to.equal(true); 48 | expect(result).to.equal(action); 49 | expect(typeof resolve).to.equal('function'); 50 | expect(typeof reject).to.equal('function'); 51 | expect(typeof exec).to.equal('function'); 52 | }); 53 | sfs.readFileSync('/foo', 'utf8'); 54 | }); 55 | it('Returns the correct method name', () => { 56 | const [sfs, vol] = create(({method}) => { 57 | expect(method).to.equal('readFileSync'); 58 | }); 59 | sfs.readFileSync('/foo', 'utf8'); 60 | }); 61 | it('Returns the correct synchronicity type', () => { 62 | const [sfs, vol] = create(({isAsync}) => { 63 | expect(isAsync).to.equal(false); 64 | }); 65 | sfs.readFileSync('/foo', 'utf8'); 66 | }); 67 | it('Returns the correct arguments', () => { 68 | const [sfs, vol] = create(({args}) => { 69 | expect(args instanceof Array).to.equal(true); 70 | expect(args.length).to.equal(2); 71 | expect(args).to.eql(['/foo', 'utf8']); 72 | }); 73 | sfs.readFileSync('/foo', 'utf8'); 74 | }); 75 | it('Without mocking, returns the original fs result', () => { 76 | const [sfs, vol] = create(); 77 | const res = sfs.readFileSync('/foo', 'utf8'); 78 | expect(res).to.equal('bar'); 79 | }); 80 | it('Mocking result works', () => { 81 | const [sfs, vol] = create(({resolve}) => { 82 | resolve('lol'); 83 | }); 84 | const res = sfs.readFileSync('/foo', 'utf8'); 85 | expect(res).to.equal('lol'); 86 | }); 87 | it('Mocking error works', () => { 88 | const [sfs, vol] = create(action => { 89 | const {reject} = action; 90 | reject(Error('1234')); 91 | }); 92 | try { 93 | sfs.readFileSync('/foo', 'utf8'); 94 | throw Error('This should not throw'); 95 | } catch (err) { 96 | expect(err.message).to.equal('1234'); 97 | } 98 | }); 99 | it('`exec` executes the real filesystem call', () => { 100 | const [sfs, vol] = create(({exec}) => { 101 | expect(exec()).to.equal('bar'); 102 | }); 103 | sfs.readFileSync('/foo', 'utf8'); 104 | }); 105 | }); 106 | describe('Async methods', () => { 107 | it('Returns action promises', done => { 108 | const [sfs, vol] = create(action => { 109 | expect(action).to.be.an.instanceof(Promise); 110 | done(); 111 | }); 112 | sfs.readFile('/foo', 'utf8', () => {}); 113 | }); 114 | it('`result` is just a circular reference to `action`', done => { 115 | const [sfs, vol] = create(action => { 116 | const {result} = action; 117 | expect(result).to.equal(action); 118 | done(); 119 | }); 120 | sfs.readFile('/foo', 'utf8', () => {}); 121 | }); 122 | it('Action promise has expected API', done => { 123 | const [sfs, vol] = create(action => { 124 | const {method, isAsync, args, result, resolve, reject, exec, pause, unpause, proceed} = action; 125 | 126 | expect(typeof method).to.equal('string'); 127 | expect(typeof isAsync).to.equal('boolean'); 128 | expect(args instanceof Array).to.equal(true); 129 | expect(result instanceof Promise).to.equal(true); 130 | expect(result).to.equal(action); 131 | expect(typeof resolve).to.equal('function'); 132 | expect(typeof reject).to.equal('function'); 133 | expect(typeof exec).to.equal('function'); 134 | expect(exec()).to.be.an.instanceof(Promise); 135 | expect(typeof pause).to.equal('function'); 136 | expect(typeof unpause).to.equal('function'); 137 | expect(typeof proceed).to.equal('function'); 138 | 139 | done(); 140 | }); 141 | sfs.readFile('/foo', 'utf8', () => {}); 142 | }); 143 | it('Returns the correct method name', done => { 144 | const [sfs, vol] = create(({method}) => { 145 | expect(method).to.equal('readFile'); 146 | done(); 147 | }); 148 | sfs.readFile('/foo', 'utf8', () => {}); 149 | }); 150 | it('Returns the correct synchronicity type', done => { 151 | const [sfs, vol] = create(({isAsync}) => { 152 | expect(isAsync).to.equal(true); 153 | done(); 154 | }); 155 | sfs.readFile('/foo', 'utf8', () => {}); 156 | }); 157 | it('Returns the correct arguments', done => { 158 | const [sfs, vol] = create(({args}) => { 159 | expect(args instanceof Array).to.equal(true); 160 | expect(args.length).to.equal(2); 161 | expect(args).to.eql(['/foo', 'utf8']); 162 | done(); 163 | }); 164 | sfs.readFile('/foo', 'utf8', () => {}); 165 | }); 166 | it('Without mocking, returns the original fs result', done => { 167 | const [sfs, vol] = create(); 168 | sfs.readFile('/foo', 'utf8', (err, res) => { 169 | expect(res).to.equal('bar'); 170 | done(); 171 | }); 172 | }); 173 | it('Mocking result works', done => { 174 | const [sfs, vol] = create(({resolve}) => { 175 | resolve('lala'); 176 | }); 177 | sfs.readFile('/foo', 'utf8', (err, res) => { 178 | expect(res).to.equal('lala'); 179 | done(); 180 | }); 181 | }); 182 | it('Mocking error works', done => { 183 | const [sfs, vol] = create(({reject}) => { 184 | reject(Error('1234')); 185 | }); 186 | sfs.readFile('/foo', 'utf8', (err, res) => { 187 | expect(err).to.be.an.instanceof(Error); 188 | expect(err.message).to.equal('1234'); 189 | done(); 190 | }); 191 | }); 192 | it('`exec` executes the real filesystem call', done => { 193 | const [sfs, vol] = create(({exec}) => { 194 | exec().then(res => { 195 | expect(res[0]).to.equal('bar'); 196 | done(); 197 | }); 198 | }); 199 | sfs.readFile('/foo', 'utf8', () => {}); 200 | }); 201 | it('await `exec` executes the real filesystem call', done => { 202 | const [sfs, vol] = create(async function({exec}) { 203 | expect(await exec()).to.eql(['bar']); 204 | done(); 205 | }); 206 | sfs.readFile('/foo', 'utf8', () => {}); 207 | }); 208 | it('`pause` pauses the execution of the async action', done => { 209 | let executed = false; 210 | const [sfs, vol] = create(async function({pause}) { 211 | pause(); 212 | setTimeout(() => { 213 | expect(executed).to.equal(false); 214 | done(); 215 | }, 1); 216 | }); 217 | sfs.readFile('/foo', 'utf8', () => { 218 | executed = true; 219 | }); 220 | }); 221 | it('`unpause` un-pauses the execution of the async action', done => { 222 | let executed = false; 223 | const [sfs, vol] = create(async function({pause, unpause}) { 224 | pause(); 225 | unpause(); 226 | setTimeout(() => { 227 | expect(executed).to.equal(true); 228 | done(); 229 | }, 1); 230 | }); 231 | sfs.readFile('/foo', 'utf8', () => { 232 | executed = true; 233 | }); 234 | }); 235 | it('`resolve` after `pause` resolves the action', done => { 236 | const [sfs, vol] = create(async function({pause, resolve}) { 237 | pause(); 238 | resolve(['lol']); 239 | }); 240 | sfs.readFile('/foo', 'utf8', (err, res) => { 241 | expect(res).to.equal('lol'); 242 | done(); 243 | }); 244 | }); 245 | it('`reject` after `pause` rejects the action', done => { 246 | const [sfs, vol] = create(async function({pause, reject}) { 247 | pause(); 248 | reject(Error('1234')); 249 | }); 250 | sfs.readFile('/foo', 'utf8', (err, res) => { 251 | expect(err.message).to.equal('1234'); 252 | done(); 253 | }); 254 | }); 255 | it('Action resolves with the first provided `resolve` value', done => { 256 | const [sfs, vol] = create(async function({resolve}) { 257 | resolve(['1']); 258 | resolve(['2']); 259 | resolve(['3']); 260 | }); 261 | sfs.readFile('/foo', 'utf8', (err, res) => { 262 | expect(res).to.equal('1'); 263 | done(); 264 | }); 265 | }); 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Spy = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; 11 | 12 | exports.spy = spy; 13 | 14 | var _lists = require('fs-monkey/lib/util/lists'); 15 | 16 | var _events = require('events'); 17 | 18 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 19 | 20 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 21 | 22 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 23 | 24 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 25 | 26 | var noop = function noop() {}; 27 | 28 | function createAction(method, isAsync, args, callback) { 29 | var promise = new Promise(callback); 30 | promise.method = method; 31 | promise.isAsync = isAsync; 32 | promise.args = args; 33 | return promise; 34 | } 35 | 36 | var Spy = exports.Spy = function (_EventEmitter) { 37 | _inherits(Spy, _EventEmitter); 38 | 39 | function Spy(fs, listener) { 40 | _classCallCheck(this, Spy); 41 | 42 | var _this = _possibleConstructorReturn(this, (Spy.__proto__ || Object.getPrototypeOf(Spy)).call(this)); 43 | 44 | var _iteratorNormalCompletion = true; 45 | var _didIteratorError = false; 46 | var _iteratorError = undefined; 47 | 48 | try { 49 | 50 | for (var _iterator = _lists.fsSyncMethods[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 51 | var _method = _step.value; 52 | 53 | var func = fs[_method]; 54 | if (typeof func !== 'function') continue; 55 | _this[_method] = _this._createSyncMethod(fs, _method, func); 56 | } 57 | } catch (err) { 58 | _didIteratorError = true; 59 | _iteratorError = err; 60 | } finally { 61 | try { 62 | if (!_iteratorNormalCompletion && _iterator.return) { 63 | _iterator.return(); 64 | } 65 | } finally { 66 | if (_didIteratorError) { 67 | throw _iteratorError; 68 | } 69 | } 70 | } 71 | 72 | var _iteratorNormalCompletion2 = true; 73 | var _didIteratorError2 = false; 74 | var _iteratorError2 = undefined; 75 | 76 | try { 77 | for (var _iterator2 = _lists.fsAsyncMethods[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 78 | var _method2 = _step2.value; 79 | 80 | var _func = fs[_method2]; 81 | if (typeof _func !== 'function') continue; 82 | 83 | if (_method2 === 'exists') { 84 | _this[_method2] = fs[_method2].bind(fs); 85 | continue; 86 | } 87 | 88 | _this[_method2] = _this._createAsyncMethod(fs, _method2, _func); 89 | } 90 | } catch (err) { 91 | _didIteratorError2 = true; 92 | _iteratorError2 = err; 93 | } finally { 94 | try { 95 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 96 | _iterator2.return(); 97 | } 98 | } finally { 99 | if (_didIteratorError2) { 100 | throw _iteratorError2; 101 | } 102 | } 103 | } 104 | 105 | if (listener) _this.subscribe(listener); 106 | return _this; 107 | } 108 | 109 | _createClass(Spy, [{ 110 | key: '_createSyncMethod', 111 | value: function _createSyncMethod(fs, method, func) { 112 | var _this2 = this; 113 | 114 | return function () { 115 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 116 | args[_key] = arguments[_key]; 117 | } 118 | 119 | var result = void 0, 120 | error = void 0; 121 | 122 | function exec() { 123 | try { 124 | result = func.apply(fs, args); 125 | error = undefined; 126 | } catch (reason) { 127 | result = undefined; 128 | error = reason; 129 | } 130 | } 131 | 132 | function returnOrThrow() { 133 | if (typeof result !== 'undefined') { 134 | return result; 135 | } else { 136 | throw error; 137 | } 138 | } 139 | 140 | var action = createAction(method, false, args, function (resolve, reject) { 141 | process.nextTick(function () { 142 | if (typeof result !== 'undefined') resolve(result);else reject(error); 143 | }); 144 | }); 145 | 146 | action.result = action; 147 | 148 | action.resolve = function (value) { 149 | result = value; 150 | error = undefined; 151 | }; 152 | 153 | action.reject = function (reason) { 154 | result = undefined; 155 | error = reason; 156 | }; 157 | 158 | action.exec = function () { 159 | exec(); 160 | return returnOrThrow(); 161 | }; 162 | 163 | action.catch(noop); 164 | 165 | _this2.emit(action); 166 | 167 | if (typeof result !== 'undefined') { 168 | return result; 169 | } else if (typeof error !== 'undefined') { 170 | throw error; 171 | } else { 172 | exec(); 173 | return returnOrThrow(); 174 | } 175 | }; 176 | } 177 | }, { 178 | key: '_createAsyncMethod', 179 | value: function _createAsyncMethod(fs, method, func) { 180 | var _this3 = this; 181 | 182 | return function () { 183 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 184 | args[_key2] = arguments[_key2]; 185 | } 186 | 187 | var callback = args[args.length - 1]; 188 | if (typeof callback !== 'function') return func.apply(fs, args); 189 | 190 | var paused = false, 191 | proceeding = false, 192 | finished = false; 193 | 194 | var _resolve = void 0, 195 | _reject = void 0; 196 | 197 | function resolve(value) { 198 | if (!finished) { 199 | finished = true; 200 | value = value instanceof Array ? value : [value]; 201 | _resolve(value); 202 | if (value instanceof Array) callback.apply(undefined, [null].concat(_toConsumableArray(value)));else callback(null, value); 203 | } 204 | } 205 | 206 | function reject(reason) { 207 | if (!finished) { 208 | finished = true; 209 | _reject(reason); 210 | callback(reason); 211 | } 212 | } 213 | 214 | var _exec = void 0; 215 | 216 | function exec() { 217 | if (_exec) return _exec; 218 | 219 | _exec = new Promise(function (resolve, reject) { 220 | args[args.length - 1] = function (reason) { 221 | for (var _len3 = arguments.length, results = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { 222 | results[_key3 - 1] = arguments[_key3]; 223 | } 224 | 225 | if (reason) reject(reason);else resolve(results); 226 | }; 227 | func.apply(fs, args); 228 | }); 229 | 230 | _exec.catch(noop); 231 | 232 | return _exec; 233 | } 234 | 235 | function proceed() { 236 | proceeding = true; 237 | exec().then(function (result) { 238 | return resolve(result); 239 | }, function (err) { 240 | return reject(err); 241 | }); 242 | } 243 | 244 | var action = createAction(method, true, args.slice(0, args.length - 1), function (resolve, reject) { 245 | _resolve = resolve; 246 | _reject = reject; 247 | 248 | process.nextTick(function () { 249 | _this3.emit(action); 250 | setImmediate(function () { 251 | if (!paused && !proceeding) proceed(); 252 | }); 253 | }); 254 | }); 255 | 256 | action.result = action; 257 | action.exec = exec; 258 | action.resolve = resolve; 259 | action.reject = reject; 260 | 261 | action.pause = function (cb) { 262 | if (proceeding) throw Error('Cannot pause anymore, already executing the real filesystem call.'); 263 | if (paused) throw Error('Already paused once.'); 264 | paused = true; 265 | if (cb) cb(proceed); 266 | }; 267 | action.unpause = proceed; 268 | action.proceed = proceed; 269 | 270 | action.catch(noop); 271 | }; 272 | } 273 | }, { 274 | key: 'emit', 275 | value: function emit(action) { 276 | _get(Spy.prototype.__proto__ || Object.getPrototypeOf(Spy.prototype), 'emit', this).call(this, 'action', action); 277 | _get(Spy.prototype.__proto__ || Object.getPrototypeOf(Spy.prototype), 'emit', this).call(this, action.method, action); 278 | } 279 | }, { 280 | key: 'subscribe', 281 | value: function subscribe(listener) { 282 | this.addListener('action', listener); 283 | } 284 | }, { 285 | key: 'unsubscribe', 286 | value: function unsubscribe(listener) { 287 | this.removeListener('action', listener); 288 | } 289 | }, { 290 | key: 'on', 291 | value: function on(event, listener) { 292 | this.addListener(event, listener); 293 | } 294 | }, { 295 | key: 'off', 296 | value: function off(event, listener) { 297 | this.removeListener(event, listener); 298 | } 299 | }]); 300 | 301 | return Spy; 302 | }(_events.EventEmitter); 303 | 304 | function spy(fs, listener) { 305 | var sfs = new Spy(fs); 306 | if (typeof listener === 'function') sfs.subscribe(listener); 307 | return sfs; 308 | } -------------------------------------------------------------------------------- /lib/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 4 | 5 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 6 | 7 | var _chai = require('chai'); 8 | 9 | var _memfs = require('memfs'); 10 | 11 | var _index = require('./index'); 12 | 13 | function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } 14 | 15 | function create(listener) { 16 | var vol = _memfs.Volume.fromJSON({ 17 | '/foo': 'bar' 18 | }); 19 | var sfs = new _index.Spy(vol, listener); 20 | return [sfs, vol]; 21 | } 22 | 23 | describe('SpyFS', function () { 24 | it('Loads without crashing', function () { 25 | create(); 26 | }); 27 | it('Creates new fs with, does not overwrite the old one', function () { 28 | var vol = new _memfs.Volume(); 29 | var readFile = vol.readFile; 30 | var sfs = new _index.Spy(vol); 31 | (0, _chai.expect)(_typeof(sfs.readFile)).to.equal('function'); 32 | (0, _chai.expect)(readFile === vol.readFile).to.equal(true); 33 | }); 34 | describe('Sync methods', function () { 35 | it('Returns action promises', function () { 36 | var _create = create(function (action) { 37 | (0, _chai.expect)(action).to.be.an.instanceof(Promise); 38 | }), 39 | _create2 = _slicedToArray(_create, 2), 40 | sfs = _create2[0], 41 | vol = _create2[1]; 42 | 43 | sfs.readFileSync('/foo', 'utf8'); 44 | }); 45 | it('`result` is just a circular reference to `action`', function () { 46 | var _create3 = create(function (action) { 47 | var result = action.result; 48 | 49 | (0, _chai.expect)(result).to.equal(action); 50 | }), 51 | _create4 = _slicedToArray(_create3, 2), 52 | sfs = _create4[0], 53 | vol = _create4[1]; 54 | 55 | sfs.readFileSync('/foo', 'utf8'); 56 | }); 57 | it('Action promise has expected API', function () { 58 | var _create5 = create(function (action) { 59 | var method = action.method, 60 | isAsync = action.isAsync, 61 | args = action.args, 62 | result = action.result, 63 | resolve = action.resolve, 64 | reject = action.reject, 65 | exec = action.exec; 66 | 67 | 68 | (0, _chai.expect)(typeof method === 'undefined' ? 'undefined' : _typeof(method)).to.equal('string'); 69 | (0, _chai.expect)(typeof isAsync === 'undefined' ? 'undefined' : _typeof(isAsync)).to.equal('boolean'); 70 | (0, _chai.expect)(args instanceof Array).to.equal(true); 71 | (0, _chai.expect)(result instanceof Promise).to.equal(true); 72 | (0, _chai.expect)(result).to.equal(action); 73 | (0, _chai.expect)(typeof resolve === 'undefined' ? 'undefined' : _typeof(resolve)).to.equal('function'); 74 | (0, _chai.expect)(typeof reject === 'undefined' ? 'undefined' : _typeof(reject)).to.equal('function'); 75 | (0, _chai.expect)(typeof exec === 'undefined' ? 'undefined' : _typeof(exec)).to.equal('function'); 76 | }), 77 | _create6 = _slicedToArray(_create5, 2), 78 | sfs = _create6[0], 79 | vol = _create6[1]; 80 | 81 | sfs.readFileSync('/foo', 'utf8'); 82 | }); 83 | it('Returns the correct method name', function () { 84 | var _create7 = create(function (_ref) { 85 | var method = _ref.method; 86 | 87 | (0, _chai.expect)(method).to.equal('readFileSync'); 88 | }), 89 | _create8 = _slicedToArray(_create7, 2), 90 | sfs = _create8[0], 91 | vol = _create8[1]; 92 | 93 | sfs.readFileSync('/foo', 'utf8'); 94 | }); 95 | it('Returns the correct synchronicity type', function () { 96 | var _create9 = create(function (_ref2) { 97 | var isAsync = _ref2.isAsync; 98 | 99 | (0, _chai.expect)(isAsync).to.equal(false); 100 | }), 101 | _create10 = _slicedToArray(_create9, 2), 102 | sfs = _create10[0], 103 | vol = _create10[1]; 104 | 105 | sfs.readFileSync('/foo', 'utf8'); 106 | }); 107 | it('Returns the correct arguments', function () { 108 | var _create11 = create(function (_ref3) { 109 | var args = _ref3.args; 110 | 111 | (0, _chai.expect)(args instanceof Array).to.equal(true); 112 | (0, _chai.expect)(args.length).to.equal(2); 113 | (0, _chai.expect)(args).to.eql(['/foo', 'utf8']); 114 | }), 115 | _create12 = _slicedToArray(_create11, 2), 116 | sfs = _create12[0], 117 | vol = _create12[1]; 118 | 119 | sfs.readFileSync('/foo', 'utf8'); 120 | }); 121 | it('Without mocking, returns the original fs result', function () { 122 | var _create13 = create(), 123 | _create14 = _slicedToArray(_create13, 2), 124 | sfs = _create14[0], 125 | vol = _create14[1]; 126 | 127 | var res = sfs.readFileSync('/foo', 'utf8'); 128 | (0, _chai.expect)(res).to.equal('bar'); 129 | }); 130 | it('Mocking result works', function () { 131 | var _create15 = create(function (_ref4) { 132 | var resolve = _ref4.resolve; 133 | 134 | resolve('lol'); 135 | }), 136 | _create16 = _slicedToArray(_create15, 2), 137 | sfs = _create16[0], 138 | vol = _create16[1]; 139 | 140 | var res = sfs.readFileSync('/foo', 'utf8'); 141 | (0, _chai.expect)(res).to.equal('lol'); 142 | }); 143 | it('Mocking error works', function () { 144 | var _create17 = create(function (action) { 145 | var reject = action.reject; 146 | 147 | reject(Error('1234')); 148 | }), 149 | _create18 = _slicedToArray(_create17, 2), 150 | sfs = _create18[0], 151 | vol = _create18[1]; 152 | 153 | try { 154 | sfs.readFileSync('/foo', 'utf8'); 155 | throw Error('This should not throw'); 156 | } catch (err) { 157 | (0, _chai.expect)(err.message).to.equal('1234'); 158 | } 159 | }); 160 | it('`exec` executes the real filesystem call', function () { 161 | var _create19 = create(function (_ref5) { 162 | var exec = _ref5.exec; 163 | 164 | (0, _chai.expect)(exec()).to.equal('bar'); 165 | }), 166 | _create20 = _slicedToArray(_create19, 2), 167 | sfs = _create20[0], 168 | vol = _create20[1]; 169 | 170 | sfs.readFileSync('/foo', 'utf8'); 171 | }); 172 | }); 173 | describe('Async methods', function () { 174 | it('Returns action promises', function (done) { 175 | var _create21 = create(function (action) { 176 | (0, _chai.expect)(action).to.be.an.instanceof(Promise); 177 | done(); 178 | }), 179 | _create22 = _slicedToArray(_create21, 2), 180 | sfs = _create22[0], 181 | vol = _create22[1]; 182 | 183 | sfs.readFile('/foo', 'utf8', function () {}); 184 | }); 185 | it('`result` is just a circular reference to `action`', function (done) { 186 | var _create23 = create(function (action) { 187 | var result = action.result; 188 | 189 | (0, _chai.expect)(result).to.equal(action); 190 | done(); 191 | }), 192 | _create24 = _slicedToArray(_create23, 2), 193 | sfs = _create24[0], 194 | vol = _create24[1]; 195 | 196 | sfs.readFile('/foo', 'utf8', function () {}); 197 | }); 198 | it('Action promise has expected API', function (done) { 199 | var _create25 = create(function (action) { 200 | var method = action.method, 201 | isAsync = action.isAsync, 202 | args = action.args, 203 | result = action.result, 204 | resolve = action.resolve, 205 | reject = action.reject, 206 | exec = action.exec, 207 | pause = action.pause, 208 | unpause = action.unpause, 209 | proceed = action.proceed; 210 | 211 | 212 | (0, _chai.expect)(typeof method === 'undefined' ? 'undefined' : _typeof(method)).to.equal('string'); 213 | (0, _chai.expect)(typeof isAsync === 'undefined' ? 'undefined' : _typeof(isAsync)).to.equal('boolean'); 214 | (0, _chai.expect)(args instanceof Array).to.equal(true); 215 | (0, _chai.expect)(result instanceof Promise).to.equal(true); 216 | (0, _chai.expect)(result).to.equal(action); 217 | (0, _chai.expect)(typeof resolve === 'undefined' ? 'undefined' : _typeof(resolve)).to.equal('function'); 218 | (0, _chai.expect)(typeof reject === 'undefined' ? 'undefined' : _typeof(reject)).to.equal('function'); 219 | (0, _chai.expect)(typeof exec === 'undefined' ? 'undefined' : _typeof(exec)).to.equal('function'); 220 | (0, _chai.expect)(exec()).to.be.an.instanceof(Promise); 221 | (0, _chai.expect)(typeof pause === 'undefined' ? 'undefined' : _typeof(pause)).to.equal('function'); 222 | (0, _chai.expect)(typeof unpause === 'undefined' ? 'undefined' : _typeof(unpause)).to.equal('function'); 223 | (0, _chai.expect)(typeof proceed === 'undefined' ? 'undefined' : _typeof(proceed)).to.equal('function'); 224 | 225 | done(); 226 | }), 227 | _create26 = _slicedToArray(_create25, 2), 228 | sfs = _create26[0], 229 | vol = _create26[1]; 230 | 231 | sfs.readFile('/foo', 'utf8', function () {}); 232 | }); 233 | it('Returns the correct method name', function (done) { 234 | var _create27 = create(function (_ref6) { 235 | var method = _ref6.method; 236 | 237 | (0, _chai.expect)(method).to.equal('readFile'); 238 | done(); 239 | }), 240 | _create28 = _slicedToArray(_create27, 2), 241 | sfs = _create28[0], 242 | vol = _create28[1]; 243 | 244 | sfs.readFile('/foo', 'utf8', function () {}); 245 | }); 246 | it('Returns the correct synchronicity type', function (done) { 247 | var _create29 = create(function (_ref7) { 248 | var isAsync = _ref7.isAsync; 249 | 250 | (0, _chai.expect)(isAsync).to.equal(true); 251 | done(); 252 | }), 253 | _create30 = _slicedToArray(_create29, 2), 254 | sfs = _create30[0], 255 | vol = _create30[1]; 256 | 257 | sfs.readFile('/foo', 'utf8', function () {}); 258 | }); 259 | it('Returns the correct arguments', function (done) { 260 | var _create31 = create(function (_ref8) { 261 | var args = _ref8.args; 262 | 263 | (0, _chai.expect)(args instanceof Array).to.equal(true); 264 | (0, _chai.expect)(args.length).to.equal(2); 265 | (0, _chai.expect)(args).to.eql(['/foo', 'utf8']); 266 | done(); 267 | }), 268 | _create32 = _slicedToArray(_create31, 2), 269 | sfs = _create32[0], 270 | vol = _create32[1]; 271 | 272 | sfs.readFile('/foo', 'utf8', function () {}); 273 | }); 274 | it('Without mocking, returns the original fs result', function (done) { 275 | var _create33 = create(), 276 | _create34 = _slicedToArray(_create33, 2), 277 | sfs = _create34[0], 278 | vol = _create34[1]; 279 | 280 | sfs.readFile('/foo', 'utf8', function (err, res) { 281 | (0, _chai.expect)(res).to.equal('bar'); 282 | done(); 283 | }); 284 | }); 285 | it('Mocking result works', function (done) { 286 | var _create35 = create(function (_ref9) { 287 | var resolve = _ref9.resolve; 288 | 289 | resolve('lala'); 290 | }), 291 | _create36 = _slicedToArray(_create35, 2), 292 | sfs = _create36[0], 293 | vol = _create36[1]; 294 | 295 | sfs.readFile('/foo', 'utf8', function (err, res) { 296 | (0, _chai.expect)(res).to.equal('lala'); 297 | done(); 298 | }); 299 | }); 300 | it('Mocking error works', function (done) { 301 | var _create37 = create(function (_ref10) { 302 | var reject = _ref10.reject; 303 | 304 | reject(Error('1234')); 305 | }), 306 | _create38 = _slicedToArray(_create37, 2), 307 | sfs = _create38[0], 308 | vol = _create38[1]; 309 | 310 | sfs.readFile('/foo', 'utf8', function (err, res) { 311 | (0, _chai.expect)(err).to.be.an.instanceof(Error); 312 | (0, _chai.expect)(err.message).to.equal('1234'); 313 | done(); 314 | }); 315 | }); 316 | it('`exec` executes the real filesystem call', function (done) { 317 | var _create39 = create(function (_ref11) { 318 | var exec = _ref11.exec; 319 | 320 | exec().then(function (res) { 321 | (0, _chai.expect)(res[0]).to.equal('bar'); 322 | done(); 323 | }); 324 | }), 325 | _create40 = _slicedToArray(_create39, 2), 326 | sfs = _create40[0], 327 | vol = _create40[1]; 328 | 329 | sfs.readFile('/foo', 'utf8', function () {}); 330 | }); 331 | it('await `exec` executes the real filesystem call', function (done) { 332 | var _create41 = create(function () { 333 | var _ref12 = _asyncToGenerator(regeneratorRuntime.mark(function _callee(_ref13) { 334 | var exec = _ref13.exec; 335 | return regeneratorRuntime.wrap(function _callee$(_context) { 336 | while (1) { 337 | switch (_context.prev = _context.next) { 338 | case 0: 339 | _context.t0 = _chai.expect; 340 | _context.next = 3; 341 | return exec(); 342 | 343 | case 3: 344 | _context.t1 = _context.sent; 345 | _context.t2 = ['bar']; 346 | (0, _context.t0)(_context.t1).to.eql(_context.t2); 347 | 348 | done(); 349 | 350 | case 7: 351 | case 'end': 352 | return _context.stop(); 353 | } 354 | } 355 | }, _callee, this); 356 | })); 357 | 358 | return function (_x) { 359 | return _ref12.apply(this, arguments); 360 | }; 361 | }()), 362 | _create42 = _slicedToArray(_create41, 2), 363 | sfs = _create42[0], 364 | vol = _create42[1]; 365 | 366 | sfs.readFile('/foo', 'utf8', function () {}); 367 | }); 368 | it('`pause` pauses the execution of the async action', function (done) { 369 | var executed = false; 370 | 371 | var _create43 = create(function () { 372 | var _ref14 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2(_ref15) { 373 | var pause = _ref15.pause; 374 | return regeneratorRuntime.wrap(function _callee2$(_context2) { 375 | while (1) { 376 | switch (_context2.prev = _context2.next) { 377 | case 0: 378 | pause(); 379 | setTimeout(function () { 380 | (0, _chai.expect)(executed).to.equal(false); 381 | done(); 382 | }, 1); 383 | 384 | case 2: 385 | case 'end': 386 | return _context2.stop(); 387 | } 388 | } 389 | }, _callee2, this); 390 | })); 391 | 392 | return function (_x2) { 393 | return _ref14.apply(this, arguments); 394 | }; 395 | }()), 396 | _create44 = _slicedToArray(_create43, 2), 397 | sfs = _create44[0], 398 | vol = _create44[1]; 399 | 400 | sfs.readFile('/foo', 'utf8', function () { 401 | executed = true; 402 | }); 403 | }); 404 | it('`unpause` un-pauses the execution of the async action', function (done) { 405 | var executed = false; 406 | 407 | var _create45 = create(function () { 408 | var _ref16 = _asyncToGenerator(regeneratorRuntime.mark(function _callee3(_ref17) { 409 | var pause = _ref17.pause, 410 | unpause = _ref17.unpause; 411 | return regeneratorRuntime.wrap(function _callee3$(_context3) { 412 | while (1) { 413 | switch (_context3.prev = _context3.next) { 414 | case 0: 415 | pause(); 416 | unpause(); 417 | setTimeout(function () { 418 | (0, _chai.expect)(executed).to.equal(true); 419 | done(); 420 | }, 1); 421 | 422 | case 3: 423 | case 'end': 424 | return _context3.stop(); 425 | } 426 | } 427 | }, _callee3, this); 428 | })); 429 | 430 | return function (_x3) { 431 | return _ref16.apply(this, arguments); 432 | }; 433 | }()), 434 | _create46 = _slicedToArray(_create45, 2), 435 | sfs = _create46[0], 436 | vol = _create46[1]; 437 | 438 | sfs.readFile('/foo', 'utf8', function () { 439 | executed = true; 440 | }); 441 | }); 442 | it('`resolve` after `pause` resolves the action', function (done) { 443 | var _create47 = create(function () { 444 | var _ref18 = _asyncToGenerator(regeneratorRuntime.mark(function _callee4(_ref19) { 445 | var pause = _ref19.pause, 446 | resolve = _ref19.resolve; 447 | return regeneratorRuntime.wrap(function _callee4$(_context4) { 448 | while (1) { 449 | switch (_context4.prev = _context4.next) { 450 | case 0: 451 | pause(); 452 | resolve(['lol']); 453 | 454 | case 2: 455 | case 'end': 456 | return _context4.stop(); 457 | } 458 | } 459 | }, _callee4, this); 460 | })); 461 | 462 | return function (_x4) { 463 | return _ref18.apply(this, arguments); 464 | }; 465 | }()), 466 | _create48 = _slicedToArray(_create47, 2), 467 | sfs = _create48[0], 468 | vol = _create48[1]; 469 | 470 | sfs.readFile('/foo', 'utf8', function (err, res) { 471 | (0, _chai.expect)(res).to.equal('lol'); 472 | done(); 473 | }); 474 | }); 475 | it('`reject` after `pause` rejects the action', function (done) { 476 | var _create49 = create(function () { 477 | var _ref20 = _asyncToGenerator(regeneratorRuntime.mark(function _callee5(_ref21) { 478 | var pause = _ref21.pause, 479 | reject = _ref21.reject; 480 | return regeneratorRuntime.wrap(function _callee5$(_context5) { 481 | while (1) { 482 | switch (_context5.prev = _context5.next) { 483 | case 0: 484 | pause(); 485 | reject(Error('1234')); 486 | 487 | case 2: 488 | case 'end': 489 | return _context5.stop(); 490 | } 491 | } 492 | }, _callee5, this); 493 | })); 494 | 495 | return function (_x5) { 496 | return _ref20.apply(this, arguments); 497 | }; 498 | }()), 499 | _create50 = _slicedToArray(_create49, 2), 500 | sfs = _create50[0], 501 | vol = _create50[1]; 502 | 503 | sfs.readFile('/foo', 'utf8', function (err, res) { 504 | (0, _chai.expect)(err.message).to.equal('1234'); 505 | done(); 506 | }); 507 | }); 508 | it('Action resolves with the first provided `resolve` value', function (done) { 509 | var _create51 = create(function () { 510 | var _ref22 = _asyncToGenerator(regeneratorRuntime.mark(function _callee6(_ref23) { 511 | var resolve = _ref23.resolve; 512 | return regeneratorRuntime.wrap(function _callee6$(_context6) { 513 | while (1) { 514 | switch (_context6.prev = _context6.next) { 515 | case 0: 516 | resolve(['1']); 517 | resolve(['2']); 518 | resolve(['3']); 519 | 520 | case 3: 521 | case 'end': 522 | return _context6.stop(); 523 | } 524 | } 525 | }, _callee6, this); 526 | })); 527 | 528 | return function (_x6) { 529 | return _ref22.apply(this, arguments); 530 | }; 531 | }()), 532 | _create52 = _slicedToArray(_create51, 2), 533 | sfs = _create52[0], 534 | vol = _create52[1]; 535 | 536 | sfs.readFile('/foo', 'utf8', function (err, res) { 537 | (0, _chai.expect)(res).to.equal('1'); 538 | done(); 539 | }); 540 | }); 541 | }); 542 | }); --------------------------------------------------------------------------------