├── .gitignore ├── .npmignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── karma.conf.js ├── lib └── plugin.js ├── package.json ├── src ├── commonjs_bridge.js └── commonjs_bridge.wrapper ├── tasks └── build.js └── test ├── commonjs_bridge.spec.js └── utils.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | client 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | Gruntfile.coffee 3 | karma.conf.js 4 | 5 | src 6 | test 7 | tasks 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - npm install -g grunt-cli 9 | - npm install 10 | 11 | script: 12 | - grunt -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | grunt.initConfig 4 | 5 | build: 6 | client: 'src/**/*.js' 7 | 8 | # JSHint options 9 | # http://www.jshint.com/options/ 10 | jshint: 11 | plugin: 12 | files: 13 | src: 'lib/**/*.js' 14 | options: 15 | # node 16 | node: true, 17 | strict: false 18 | client: 19 | files: 20 | src: 'client/**/*.js' 21 | 22 | options: 23 | quotmark: 'single' 24 | bitwise: true 25 | indent: 2 26 | camelcase: true 27 | strict: true 28 | trailing: true 29 | curly: true 30 | eqeqeq: true 31 | immed: true 32 | latedef: true 33 | newcap: true 34 | noempty: true 35 | unused: true 36 | noarg: true 37 | sub: true 38 | undef: true 39 | maxdepth: 4 40 | maxlen: 100 41 | globals: {} 42 | 43 | karma: 44 | client: 45 | configFile: 'karma.conf.js' 46 | autoWatch: false 47 | singleRun: true 48 | tdd: 49 | configFile: 'karma.conf.js' 50 | autoWatch: true 51 | singleRun: false 52 | 53 | 'npm-contributors': 54 | options: 55 | commitMessage: 'chore: update contributors' 56 | 57 | bump: 58 | options: 59 | commitMessage: 'chore: release v%VERSION%' 60 | pushTo: 'upstream' 61 | 62 | grunt.loadTasks 'tasks' 63 | grunt.loadNpmTasks 'grunt-contrib-jshint' 64 | grunt.loadNpmTasks 'grunt-karma' 65 | grunt.loadNpmTasks 'grunt-bump' 66 | grunt.loadNpmTasks 'grunt-npm' 67 | grunt.loadNpmTasks 'grunt-auto-release' 68 | 69 | grunt.registerTask 'default', ['karma:client', 'build'] 70 | grunt.registerTask 'release', 'Build, bump and publish to NPM.', (type) -> 71 | grunt.task.run [ 72 | 'build' 73 | 'npm-contributors' 74 | "bump:#{type||'patch'}" 75 | 'npm-publish' 76 | ] 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2011-2013 Titanium I.T. LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/karma-runner/karma-commonjs.svg?branch=master)](https://travis-ci.org/karma-runner/karma-commonjs) 2 | 3 | # karma-commonjs 4 | 5 | > A Karma plugin that allows testing [CommonJS] modules in the browser. 6 | 7 | ## Browserify 8 | 9 | If you're using Browserify to compile your projects, you should consider [karma-browserify](https://github.com/Nikku/karma-browserify) which runs Browserify directly. The cost is slightly slower builds (but not too bad, thanks to an incremental loading algorithm) and somewhat messier stack traces. The benefit is support for the full Browserify API and automatic discovery of 'require'd files. 10 | 11 | #### karma-commonjs 12 | 1. Provides a lightweight commonjs wrapper around your code 13 | 2. Supports Node's `require` algorithm 14 | 3. Only reloads files that change 15 | 4. Provides stack traces that point to your original files 16 | 5. Requires you to specify files in the Karma config file 17 | 18 | #### karma-browserify 19 | 1. Creates a temporary bundle using Browserify 20 | 2. Supports the full Browserify API, including transforms, plugins, and shims for Node globals 21 | 3. Uses [watchify](https://github.com/substack/watchify) to perform incremental rebuilds 22 | 4. Can use source maps to provide useful stack traces 23 | 5. Automatically includes required files 24 | 25 | ## Installation 26 | 27 | The easiest way is to keep `karma-commonjs` as a devDependency: 28 | 29 | `npm install karma-commonjs --save-dev` 30 | 31 | which should result in the following entry in your `package.json`: 32 | 33 | ```json 34 | { 35 | "devDependencies": { 36 | "karma": "~0.10", 37 | "karma-commonjs": "~0.2" 38 | } 39 | } 40 | ``` 41 | 42 | ## Configuration 43 | ```js 44 | // karma.conf.js 45 | module.exports = function(config) { 46 | config.set({ 47 | frameworks: ['jasmine', 'commonjs'], 48 | files: [ 49 | // your tests, sources, ... 50 | ], 51 | 52 | preprocessors: { 53 | '**/*.js': ['commonjs'] 54 | } 55 | }); 56 | }; 57 | ``` 58 | Additionally you can specify a root folder (relative to project's directory) which is used to look for required modules: 59 | ``` 60 | commonjsPreprocessor: { 61 | modulesRoot: 'some_folder' 62 | } 63 | ``` 64 | When not specified the root folder defaults to the `karma.basePath/node_modules` configuration option. 65 | 66 | For an example project, check out Karma's [client tests](https://github.com/karma-runner/karma/tree/master/test/client). 67 | 68 | ---- 69 | 70 | For more information on Karma see the [homepage]. 71 | 72 | 73 | [homepage]: http://karma-runner.github.com 74 | [CommonJS]: http://www.commonjs.org/ 75 | [Browserify]: https://github.com/substack/node-browserify 76 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['jasmine'], 4 | files: [ 5 | 'src/*.js', 6 | 'test/*.js' 7 | ], 8 | browsers: process.env.TRAVIS ? ['Firefox'] : ['Chrome'], 9 | reporters: ['dots'] 10 | }); 11 | }; -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var os = require('os'); 3 | 4 | var BRIDGE_FILE_PATH = path.normalize(__dirname + '/../client/commonjs_bridge.js'); 5 | 6 | var initCommonJS = function(/* config.files */ files) { 7 | 8 | // Include the file that resolves all the dependencies on the client. 9 | files.push({ 10 | pattern: BRIDGE_FILE_PATH, 11 | included: true, 12 | served: true, 13 | watched: false 14 | }); 15 | }; 16 | 17 | var createPreprocesor = function(logger, config, basePath) { 18 | var log = logger.create('preprocessor.commonjs'); 19 | var modulesRootPath = path.resolve(config && config.modulesRoot ? config.modulesRoot : path.join(basePath, 'node_modules')); 20 | //normalize root path on Windows 21 | if (process.platform === 'win32') { 22 | modulesRootPath = modulesRootPath.replace(/\\/g, '/'); 23 | } 24 | 25 | log.debug('Configured root path for modules "%s".', modulesRootPath); 26 | 27 | return function(content, file, done) { 28 | 29 | if (file.originalPath === BRIDGE_FILE_PATH) { 30 | return done(content); 31 | } 32 | 33 | log.debug('Processing "%s".', file.originalPath); 34 | 35 | if (path.extname(file.originalPath) === '.json') { 36 | return done('window.__cjs_module__["' + file.path + '"] = ' + content + ';' + os.EOL); 37 | } 38 | 39 | var output = 40 | 'window.__cjs_modules_root__ = "' + modulesRootPath + '";' + 41 | 'window.__cjs_module__ = window.__cjs_module__ || {};' + 42 | 'window.__cjs_module__["' + file.path + '"] = function(require, module, exports, __dirname, __filename) {' + 43 | content + os.EOL + 44 | '}'; 45 | 46 | done(output); 47 | }; 48 | }; 49 | createPreprocesor.$inject = ['logger', 'config.commonjsPreprocessor', 'config.basePath']; 50 | 51 | // PUBLISH DI MODULE 52 | module.exports = { 53 | 'framework:commonjs': ['factory', initCommonJS], 54 | 'preprocessor:commonjs': ['factory', createPreprocesor] 55 | }; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "karma-commonjs", 3 | "version": "1.0.0", 4 | "description": "A Karma plugin. Test CommonJS modules.", 5 | "main": "lib/plugin.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/karma-runner/karma-commonjs.git" 12 | }, 13 | "keywords": [ 14 | "karma-plugin", 15 | "commonjs" 16 | ], 17 | "author": "Vojta Jina ", 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "grunt": "~0.4.1", 21 | "grunt-npm": "~0.0.2", 22 | "grunt-bump": "~0.0.11", 23 | "grunt-karma": "1.x || ~1.0.0", 24 | "grunt-contrib-jshint": "~0.6.0", 25 | "grunt-auto-release": "~0.0.3", 26 | "karma-jasmine": "1.x", 27 | "karma-chrome-launcher": "1.x", 28 | "karma-firefox-launcher": "1.x" 29 | }, 30 | "peerDependencies": { 31 | "karma": ">=0.9" 32 | }, 33 | "license": "MIT", 34 | "contributors": [ 35 | "Vojta Jina ", 36 | "James Shore ", 37 | "Ben Drucker ", 38 | "Mark Ethan Trostler ", 39 | "Marian Zagoruiko " 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/commonjs_bridge.js: -------------------------------------------------------------------------------- 1 | var cachedModules = {}; 2 | 3 | function loadPaths(paths, existingfiles) { 4 | for (var i=0; i 0) { 113 | nextComponent = relativeComponents.shift(); 114 | 115 | if (nextComponent === ".") continue; 116 | else if (nextComponent === "..") baseComponents.pop(); 117 | else baseComponents.push(nextComponent); 118 | } 119 | 120 | return baseComponents.join("/"); 121 | } 122 | 123 | function basename(path) { 124 | return path.substring(path.lastIndexOf('/') + 1); 125 | } 126 | 127 | function dirname(path) { 128 | return path.substring(0, path.lastIndexOf('/')); 129 | } 130 | -------------------------------------------------------------------------------- /src/commonjs_bridge.wrapper: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Titanium I.T. LLC. Licensed under the MIT license. 2 | (function() { 3 | "use strict"; 4 | 5 | if (window.__cjs_module__ === undefined) throw new Error("Could not find any modules. Did you remember to set 'preprocessors' in your Karma config?"); 6 | if (window.__cjs_modules_root__ === undefined) throw new Error("Could not find CommonJS module root path. Please report this issue to the karma-commonjs project."); 7 | 8 | %CONTENT% 9 | 10 | // load all modules, skip package.json 11 | for (var modulePath in window.__cjs_module__) { 12 | if (modulePath.indexOf('package.json', -12) === -1) { 13 | require(modulePath, modulePath); 14 | } 15 | } 16 | 17 | })(window); -------------------------------------------------------------------------------- /tasks/build.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | /** 4 | * Build given file - wrap it with a function call 5 | * TODO(vojta): compile with uglify-js 6 | */ 7 | grunt.registerMultiTask('build', 'Wrap given file into a function call.', function() { 8 | 9 | var src = grunt.file.expand(this.data).pop(); 10 | var dest = src.replace('src/', 'client/'); 11 | var wrapper = src.replace('.js', '.wrapper'); 12 | 13 | grunt.file.copy(wrapper, dest, {process: function(content) { 14 | var wrappers = content.split(/%CONTENT%\r?\n/); 15 | return wrappers[0] + grunt.file.read(src) + wrappers[1]; 16 | }}); 17 | 18 | grunt.log.ok('Created ' + dest); 19 | }); 20 | }; -------------------------------------------------------------------------------- /test/commonjs_bridge.spec.js: -------------------------------------------------------------------------------- 1 | describe('client', function() { 2 | 3 | describe('require high level tests', function () { 4 | 5 | beforeEach(function(){ 6 | window.__cjs_module__ = {}; 7 | window.__cjs_modules_root__ = '/root'; 8 | cachedModules = {}; 9 | }); 10 | 11 | describe('resolve from a file', function () { 12 | 13 | beforeEach(function () { 14 | window.__cjs_module__['/folder/foo.js'] = function(require, module, exports) { 15 | exports.foo = true; 16 | }; 17 | }); 18 | 19 | it('should resolve absolute dependencies', function () { 20 | expect(require('/folder/bar.js', '/folder/foo.js').foo).toBeTruthy(); 21 | expect(require('/folder/bar.js', '/folder/foo').foo).toBeTruthy(); 22 | }); 23 | 24 | it('should resolve absolute dependencies with relative components in the deps path', function () { 25 | expect(require('/folder/bar.js', '/folder/./foo.js').foo).toBeTruthy(); 26 | expect(require('/folder/bar.js', '/folder/../folder/foo').foo).toBeTruthy(); 27 | }); 28 | 29 | it('should resolve relative dependencies', function () { 30 | expect(require('/folder/bar.js', './foo').foo).toBeTruthy(); 31 | expect(require('/folder/bar.js', './foo.js').foo).toBeTruthy(); 32 | expect(require('/folder/bar.js', './../folder/foo.js').foo).toBeTruthy(); 33 | }); 34 | 35 | it('should resolve dependencies to JSON files', function () { 36 | window.__cjs_module__['/folder/baz.json'] = {baz: 'baz'}; 37 | expect(require('/folder/bar.js', './baz.json').baz).toEqual('baz'); 38 | }); 39 | 40 | it('should resolve dependencies to JSON files without extensions', function () { 41 | window.__cjs_module__['/folder/baz.json'] = {baz: 'baz'}; 42 | expect(require('/folder/bar.js', './baz').baz).toEqual('baz'); 43 | }); 44 | 45 | it('should resolve dependencies to JS before resolving JSON', function () { 46 | window.__cjs_module__['/folder/foo.json'] = {foo: false}; 47 | expect(require('/folder/bar.js', './foo').foo).toBeTruthy(); 48 | }); 49 | }); 50 | 51 | describe('resolve from node_modules and configured modules root', function () { 52 | 53 | it('should resolve from the node_modules folder', function () { 54 | window.__cjs_module__['/folder/node_modules/mymodule/foo.js'] = function(require, module, exports) { 55 | exports.foo = true; 56 | }; 57 | expect(require('/folder/bar.js', 'mymodule/foo').foo).toBeTruthy(); 58 | }); 59 | 60 | it('should resolve from the node_modules parent folder', function () { 61 | window.__cjs_module__['/folder/node_modules/mymodule/foo.js'] = function(require, module, exports) { 62 | exports.foo = true; 63 | }; 64 | expect(require('/folder/subfolder/subsubfolder/bar.js', 'mymodule/foo').foo).toBeTruthy(); 65 | }); 66 | 67 | it('should resolve from the configured modules root', function () { 68 | window.__cjs_modules_root__ = '/root/node_modules'; 69 | window.__cjs_module__['/root/node_modules/mymodule/foo.js'] = function(require, module, exports) { 70 | exports.foo = true; 71 | }; 72 | expect(require('/folder/bar.js', 'mymodule/foo').foo).toBeTruthy(); 73 | expect(require('/folder/bar.js', 'mymodule/foo.js').foo).toBeTruthy(); 74 | }); 75 | 76 | }); 77 | 78 | describe('resolve from folders', function () { 79 | 80 | it('should be aware of index.js', function () { 81 | window.__cjs_module__['/folder/foo/index.js'] = function(require, module, exports) { 82 | exports.foo = true; 83 | }; 84 | expect(require('/folder/bar.js', './foo').foo).toBeTruthy(); 85 | }); 86 | 87 | it('should resolve a file before resolving index.js', function () { 88 | window.__cjs_module__['/folder/foo.js'] = function(require, module, exports) { 89 | exports.foo = false; 90 | }; 91 | expect(require('/folder/bar.js', './foo').foo).toBeFalsy(); 92 | }); 93 | 94 | it('should support package.json', function () { 95 | window.__cjs_module__['/somepackage/package.json'] = { 96 | main: 'foo/index.js' 97 | }; 98 | window.__cjs_module__['/somepackage/foo/index.js'] = function(require, module, exports) { 99 | exports.foo = true; 100 | }; 101 | expect(require('/folder/bar.js', '/somepackage').foo).toBeTruthy(); 102 | }); 103 | 104 | it('should support relative paths in package.json', function () { 105 | window.__cjs_module__['/somepackage/package.json'] = { 106 | main: 'foo/./../foo/index.js' 107 | }; 108 | window.__cjs_module__['/somepackage/foo/index.js'] = function(require, module, exports) { 109 | exports.foo = true; 110 | }; 111 | expect(require('/folder/bar.js', '/somepackage').foo).toBeTruthy(); 112 | }); 113 | 114 | it('should support package.json without suffix', function () { 115 | window.__cjs_module__['/somepackage/package.json'] = { 116 | main: 'foo/index' 117 | }; 118 | window.__cjs_module__['/somepackage/foo/index.js'] = function(require, module, exports) { 119 | exports.foo = true; 120 | }; 121 | expect(require('/folder/bar.js', '/somepackage').foo).toBeTruthy(); 122 | }); 123 | 124 | }); 125 | 126 | describe('__dirname and __filename "globals"', function () { 127 | 128 | it('should expose __dirname "global"', function () { 129 | window.__cjs_module__['/folder/file.js'] = function(require, module, exports, __dirname) { 130 | exports.dirname = __dirname; 131 | }; 132 | expect(require('/foo/bar/baz.js', '/folder/file.js').dirname).toEqual('/foo/bar'); 133 | }); 134 | 135 | it('should expose __filename "global"', function () { 136 | window.__cjs_module__['/folder/file.js'] = function(require, module, exports, __dirname, __filename) { 137 | exports.filename = __filename; 138 | }; 139 | expect(require('/foo/bar/baz.js', '/folder/file.js').filename).toEqual('baz.js'); 140 | }); 141 | 142 | }); 143 | 144 | describe('resolve - corner cases', function () { 145 | 146 | it('should throw error when require base path is not absolute', function () { 147 | expect(function () { 148 | require('file.js', './bar') 149 | }).toThrow(new Error("requiringFile path should be full path, but was [file.js]")); 150 | }); 151 | 152 | it('should throw error when a required module does not exist', function () { 153 | expect(function () { 154 | require('/file.js', './bar') 155 | }).toThrow(new Error("Could not find module './bar' from '/file.js'")); 156 | }); 157 | 158 | it('should propery report errors for files requiring from the root path', function () { 159 | expect(function () { 160 | require('/folder/somefile.js', 'bar/baz.js') 161 | }).toThrow(new Error("Could not find module 'bar/baz.js' from '/folder/somefile.js'")); 162 | }); 163 | 164 | it('should correctly resolve modules with circular dependencies issue #6', function(){ 165 | 166 | window.__cjs_module__['/foo.js'] = function(require, module, exports) { 167 | var bar = require('./bar'); 168 | exports.txt = 'foo'; 169 | exports.getText = function() { 170 | return 'foo ' + bar.txt; 171 | }; 172 | }; 173 | 174 | window.__cjs_module__['/bar.js'] = function(require, module, exports) { 175 | var foo = require('./foo'); 176 | exports.txt = 'bar'; 177 | exports.getText = function() { 178 | return 'bar ' + foo.txt; 179 | } 180 | }; 181 | 182 | expect(require('/app.js', './foo').getText()).toEqual('foo bar'); 183 | expect(require('/app.js', './bar').getText()).toEqual('bar foo'); 184 | }); 185 | }); 186 | }); 187 | 188 | }); 189 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | describe('Tests for various utility functions', function () { 2 | 3 | describe('path type detection', function () { 4 | 5 | it('should correctly detect full paths', function () { 6 | 7 | expect(isFullPath('/unix/full/path')).toBeTruthy(); 8 | expect(isFullPath('/unix/full/path/file.js')).toBeTruthy(); 9 | 10 | expect(isFullPath('/c:/windows/full/path')).toBeTruthy(); 11 | expect(isFullPath('/c:/windows/full/path/file.js')).toBeTruthy(); 12 | 13 | expect(isFullPath('relative/path')).toBeFalsy(); 14 | expect(isFullPath('./relative/path')).toBeFalsy(); 15 | expect(isFullPath('../relative/path')).toBeFalsy(); 16 | }); 17 | 18 | it('should correctly detect node_modules paths', function () { 19 | 20 | expect(isNpmModulePath('foo/path/file.js')).toBeTruthy(); 21 | 22 | expect(isNpmModulePath('/foo/path/file.js')).toBeFalsy(); 23 | expect(isNpmModulePath('./foo/path/file.js')).toBeFalsy(); 24 | expect(isNpmModulePath('../foo/path/file.js')).toBeFalsy(); 25 | }); 26 | 27 | }); 28 | 29 | describe('normalize path', function () { 30 | 31 | it('should normalize relative path - happy path scenarios', function () { 32 | expect(normalizePath('/foo/bar/baz/file.js', 'rel')).toEqual('/foo/bar/baz/rel'); 33 | expect(normalizePath('/foo/bar/baz/file.js', './rel')).toEqual('/foo/bar/baz/rel'); 34 | expect(normalizePath('/foo/bar/baz/file.js', '../../rel')).toEqual('/foo/rel'); 35 | expect(normalizePath('/foo/bar/baz/file.js', '../../bar/rel')).toEqual('/foo/bar/rel'); 36 | }); 37 | 38 | //TODO(pk): not sure it is the "right" thing to do, but this test documents how the code works today 39 | it('should handle corner cases without throwing exceptions', function () { 40 | expect(normalizePath('/foo/file.js', '../../../../rel')).toEqual('rel'); 41 | }); 42 | }); 43 | 44 | describe('path utils', function () { 45 | 46 | it('should provide basename-like utility', function () { 47 | expect(basename('/dir/file.extension')).toEqual('file.extension'); 48 | expect(basename('/dir/file')).toEqual('file'); 49 | expect(basename('/dir')).toEqual('dir'); 50 | expect(basename('dir')).toEqual('dir'); 51 | expect(basename('')).toEqual(''); 52 | }); 53 | 54 | it('should provide dirname-like utility', function () { 55 | expect(dirname('/dir/sub/file.extension')).toEqual('/dir/sub'); 56 | expect(dirname('/dir/file.extension')).toEqual('/dir'); 57 | }); 58 | }); 59 | 60 | }); --------------------------------------------------------------------------------