var fs = require('fs-extra'), path = require('path'), expect = require('chai').expect, glob = require('glob'), launcher = require('./helpers/launcher.js'), SessionStub = require('./helpers/SessionStub.js'), ScriptFileStorage = require('../lib/ScriptFileStorage.js').ScriptFileStorage, ScriptManager = require('../lib/ScriptManager.js').ScriptManager; var TEMP_FILE = path.join(__dirname, 'fixtures', 'temp.js'); var TEMP_DIR = path.join(__dirname, 'work'); // directory that does not look like a part of a node.js application var NON_APP_DIR = path.join(__dirname, '..', 'front-end', 'cm'); beforeEach(deleteTemps); describe('ScriptFileStorage', function() { var storage; beforeEach(function() { storage = createScriptFileStorage(); }); it('saves new content without node.js module wrapper', function(done) { runLiveEdit(function(debuggerClient, originalScript, runtimeScript) { var storage = createScriptFileStorage(); storage.save(TEMP_FILE, edited(runtimeScript), function(err) { if (err) throw err; var newScript = fs.readFileSync(TEMP_FILE, { encoding: 'utf-8' }); expect(newScript).to.equal(edited(originalScript)); done(); }); }); }); it('preserves shebang when saving new content', function(done) { runLiveEdit( function(content) { return '#!/usr/bin/node\n' + content; }, function(debuggerClient, originalScript, runtimeScript) { var storage = createScriptFileStorage(); storage.save(TEMP_FILE, edited(runtimeScript), function(err) { if (err) throw err; var newScript = fs.readFileSync(TEMP_FILE, { encoding: 'utf-8' }); expect(newScript).to.equal(edited(originalScript)); done(); }); } ); }); it('loads content with node.js module wrapper', function(done) { fs.writeFileSync(TEMP_FILE, '/* content */'); storage.load(TEMP_FILE, function(err, content) { if (err) throw err; expect(content).to.match( /^\(function \(exports, require,.*\) \{ \/\* content \*\/\n\}\);$/); done(); }); }); it('loads content without shebang', function(done) { fs.writeFileSync(TEMP_FILE, '#!/usr/bin/env/node\n/* content */'); storage.load(TEMP_FILE, function(err, content) { if (err) throw err; expect(content).to.not.contain('#!'); expect(content).to.contain('/* content */'); done(); }); }); it('finds application root for subdir/app.js by checking package.json file in parent', function(done) { givenTempFiles('subdir/', 'subdir/app.js', 'package.json'); storage.findApplicationRoot( path.join(TEMP_DIR, 'subdir', 'app.js'), expectRootToEqual.bind(this, done, TEMP_DIR) ); } ); it('finds application root for root/app.js with no package.json files around', function(done) { // If the parent directory of app.js does not contain package.json, // it should not be considered as an application root. givenTempFiles('root/', 'root/app.js'); storage.findApplicationRoot( path.join(TEMP_DIR, 'root', 'app.js'), expectRootToEqual.bind(this, done, path.join(TEMP_DIR, 'root')) ); } ); it('finds application root for root/app.js by checking package.json file in root/', function(done) { givenTempFiles('root/', 'root/app.js', 'root/package.json', 'package.json'); storage.findApplicationRoot( path.join(TEMP_DIR, 'root', 'app.js'), expectRootToEqual.bind(this, done, path.join(TEMP_DIR, 'root')) ); } ); it('finds also files in start directory', function(done) { var expectedFiles = givenTempFiles( // Globally installed module, e.g. mocha 'global/', 'global/runner.js', 'global/lib/', 'global/lib/module.js', // Local application we are developing 'local/', 'local/app.js', 'local/test/', 'local/test/app.js', // Other files in a place close to globally installed modules 'unrelated/', 'unrelated/file.js' ); givenTempFiles('global/package.json', 'local/package.json'); // remove unrelated/file.js expect(expectedFiles.pop()).to.match(/unrelated[\/\\]file.js$/); storage.findAllApplicationScripts( path.join(TEMP_DIR, 'local'), path.join(TEMP_DIR, 'global', 'runner.js'), function(err, files) { if (err) throw err; expect(files.map(relativeToTemp)) .to.have.members(expectedFiles.map(relativeToTemp)); done(); } ); }); it('lists only well-known subdirectories when package.json is missing', function(done) { var expectedFiles = givenTempFiles( 'app.js', 'root.js', 'lib/helper.js', 'test/unit.js', 'node_modules/module/index.js' ); givenTempFiles( 'extra/file.js' ); storage.findAllApplicationScripts( path.join(TEMP_DIR), path.join(TEMP_DIR, 'app.js'), function(err, files) { if (err) throw err; expect(files.map(relativeToTemp)) .to.have.members(expectedFiles.map(relativeToTemp)); done(); } ); }); it('lists all subdirectories when package.json is present', function(done) { var expectedFiles = givenTempFiles( 'app.js', 'root.js', 'lib/helper.js', 'test/unit.js', 'node_modules/module/index.js', 'extra/file.js' ); givenTempFiles('package.json'); storage.findAllApplicationScripts( path.join(TEMP_DIR), path.join(TEMP_DIR, 'app.js'), function(err, files) { if (err) throw err; expect(files.map(relativeToTemp)) .to.have.members(expectedFiles.map(relativeToTemp)); done(); } ); }); it('removes duplicate entries from files found', function(done) { var expectedFiles = givenTempFiles('app.js'); storage.findAllApplicationScripts( TEMP_DIR, path.join(TEMP_DIR, 'app.js'), function(err, files) { if (err) throw err; expect(files.map(relativeToTemp)) .to.have.members(expectedFiles.map(relativeToTemp)); expect(files).to.have.length(expectedFiles.length); done(); } ); }); it('excludes files to hide', function(done) { var expectedFiles = givenTempFiles('app.js', 'mod.js').slice(0, 1); storage = createScriptFileStorage({hidden: [/mod.js/i]}); storage.findAllApplicationScripts( TEMP_DIR, path.join(TEMP_DIR, 'app.js'), function(err, files) { if (err) throw err; expect(files.map(relativeToTemp)) .to.have.members(expectedFiles.map(relativeToTemp)); expect(files).to.have.length(expectedFiles.length); done(); } ); }); it('disables preloading files', function(done) { givenTempFiles('app.js', 'mod.js'); storage = createScriptFileStorage({preload: false}); storage.findAllApplicationScripts( TEMP_DIR, path.join(TEMP_DIR, 'app.js'), function(err, files) { if (err) throw err; expect(files).to.have.length(0); done(); } ); }); function relativeToTemp(p) { return path.relative(TEMP_DIR, p); } function expectRootToEqual(done, expected, err, root) { if (err) throw err; expect(root).to.equal(expected); done(); } function edited(source) { return source.replace(';', '; /* edited */'); } function runLiveEdit(transformSource, callback) { if (!callback) { callback = transformSource; transformSource = null; } var originalScript = createTempFileAsCopyOf('LiveEdit.js', transformSource); launcher.startDebugger(TEMP_FILE, function(childProcess, session) { getScriptSourceByName(session.debuggerClient, TEMP_FILE, function(source) { callback(session.debuggerClient, originalScript, source); }); }); } function createTempFileAsCopyOf(fixture, transform) { var sourcePath = path.join(__dirname, 'fixtures', fixture); var content = fs.readFileSync(sourcePath, { encoding: 'utf-8' }); if (transform) content = transform(content); fs.writeFileSync(TEMP_FILE, content); return content; } function givenTempFiles() { var files = []; if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR); Array.prototype.forEach.call(arguments, function(f) { f = path.join(TEMP_DIR, globPathToNative(f)); if (isDir(f)) { fs.mkdirSync(f); } else { fs.mkdirpSync(path.dirname(f)); fs.writeFileSync(f, ''); files.push(f); } }); return files; } }); function createScriptFileStorage(config) { var session = new SessionStub(); session.scriptManager = new ScriptManager(config, session); var storage = new ScriptFileStorage(config, session); return storage; } function globPathToNative(p) { return p.split('/').join(path.sep); } function getScriptSourceByName(debuggerClient, scriptName, callback) { debuggerClient.request( 'scripts', { includeSource: true, types: 4, filter: scriptName }, function(err, result) { if (err) throw err; callback(result[0].source); } ); } function isDir(path) { return path.match(/[\/\\]$/); } function deleteTemps() { if (fs.existsSync(TEMP_FILE)) { fs.unlinkSync(TEMP_FILE); } if (fs.existsSync(TEMP_DIR)) { var entries = glob.sync( '**', { cwd: TEMP_DIR, dot: true, mark: true } ); entries = entries .map(function(f) { return path.join(TEMP_DIR, globPathToNative(f)); }) .sort() .reverse(); entries.forEach(function(f) { if (isDir(f)) { fs.rmdirSync(f); } else { fs.unlinkSync(f); } }); } }