├── .gitignore ├── icon.png ├── test ├── .eslintrc.json ├── suite │ ├── extension.test.js │ └── index.js └── runTest.js ├── .vscodeignore ├── .vscode ├── extensions.json └── launch.json ├── jsconfig.json ├── src ├── commands │ ├── runTestSuite.js │ ├── runFailedTests.js │ ├── runStaleTests.js │ ├── watchTestSuite.js │ ├── runTestCoverage.js │ ├── runTestFile.js │ ├── watchTestFile.js │ ├── runTestFolder.js │ ├── runTestAtCursor.js │ ├── watchTestAtCursor.js │ ├── watchTestFolder.js │ ├── lastTestCommand.js │ └── jumpToTest.js ├── helpers │ ├── storage.js │ ├── term.js │ ├── validations.js │ ├── mix.js │ └── test.js └── extension.js ├── .eslintrc.json ├── .travis.yml ├── LICENSE ├── CHANGELOG.md ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zog/vscode-elixir-test/master/icon.png -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-console": "off" 7 | } 8 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | vsc-extension-quickstart.md 6 | **/jsconfig.json 7 | **/*.map 8 | **/.eslintrc.json 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "checkJs": false, /* Typecheck .js files. */ 6 | "lib": [ 7 | "es6" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/runTestSuite.js: -------------------------------------------------------------------------------- 1 | const mix = require('../helpers/mix'); 2 | const test = require('../helpers/test'); 3 | 4 | function handler(context) { 5 | test.onRootFolder(context, () => mix.test()); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirRunTestSuite', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "extends": ["airbnb-base"], 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "import/no-unresolved": [2, { "ignore": ["vscode"]}] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/runFailedTests.js: -------------------------------------------------------------------------------- 1 | const mix = require('../helpers/mix'); 2 | const test = require('../helpers/test'); 3 | 4 | function handler(context) { 5 | test.onRootFolder(context, () => mix.testFailed()); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirRunFailedTests', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/runStaleTests.js: -------------------------------------------------------------------------------- 1 | const mix = require('../helpers/mix'); 2 | const test = require('../helpers/test'); 3 | 4 | function handler(context) { 5 | test.onRootFolder(context, () => mix.testStale()); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirRunStaleTests', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/watchTestSuite.js: -------------------------------------------------------------------------------- 1 | const mix = require('../helpers/mix'); 2 | const test = require('../helpers/test'); 3 | 4 | function handler(context) { 5 | test.onRootFolder(context, () => mix.testWatch()); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirWatchTestSuite', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/runTestCoverage.js: -------------------------------------------------------------------------------- 1 | const mix = require('../helpers/mix'); 2 | const test = require('../helpers/test'); 3 | 4 | function handler(context) { 5 | test.onRootFolder(context, () => mix.testCoverage()); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirRunTestCoverage', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/runTestFile.js: -------------------------------------------------------------------------------- 1 | const test = require('../helpers/test'); 2 | const mix = require('../helpers/mix'); 3 | 4 | function handler(context) { 5 | test.onTestFile(context, (fileName) => mix.testPath(fileName)); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirRunTestFile', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/watchTestFile.js: -------------------------------------------------------------------------------- 1 | const test = require('../helpers/test'); 2 | const mix = require('../helpers/mix'); 3 | 4 | function handler(context) { 5 | test.onTestFile(context, (fileName) => mix.testWatchPath(fileName)); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirWatchTestFile', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/runTestFolder.js: -------------------------------------------------------------------------------- 1 | const test = require('../helpers/test'); 2 | const mix = require('../helpers/mix'); 3 | 4 | function handler(context, folderUri) { 5 | test.onTestFolder(context, folderUri, (folder) => mix.testPath(folder)); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirRunTestFolder', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/runTestAtCursor.js: -------------------------------------------------------------------------------- 1 | const test = require('../helpers/test'); 2 | const mix = require('../helpers/mix'); 3 | 4 | function handler(context) { 5 | test.onTestFile(context, (fileName, cursorLine) => mix.testFileAt(fileName, cursorLine)); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirRunTestAtCursor', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/watchTestAtCursor.js: -------------------------------------------------------------------------------- 1 | const test = require('../helpers/test'); 2 | const mix = require('../helpers/mix'); 3 | 4 | function handler(context) { 5 | test.onTestFile(context, (fileName, cursorLine) => mix.testWatchAt(fileName, cursorLine)); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirWatchTestAtCursor', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/watchTestFolder.js: -------------------------------------------------------------------------------- 1 | const test = require('../helpers/test'); 2 | const mix = require('../helpers/mix'); 3 | 4 | function handler(context, folderUri) { 5 | test.onTestFolder(context, folderUri, (folderName) => mix.testWatchPath(folderName)); 6 | } 7 | 8 | module.exports = { 9 | name: 'extension.elixirWatchTestFolder', 10 | handler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/lastTestCommand.js: -------------------------------------------------------------------------------- 1 | const storage = require('../helpers/storage'); 2 | const term = require('../helpers/term'); 3 | 4 | function handler(context) { 5 | const lastCmd = storage.get(context, 'lastCmd'); 6 | 7 | if (lastCmd != null) { 8 | term.run(context, lastCmd, false); 9 | } 10 | } 11 | 12 | module.exports = { 13 | name: 'extension.elixirRunLastTestCommand', 14 | handler, 15 | }; 16 | -------------------------------------------------------------------------------- /src/helpers/storage.js: -------------------------------------------------------------------------------- 1 | function get(context, key) { 2 | return context.workspaceState.get(`vscode-elixir-test.${key}`); 3 | } 4 | 5 | function update(context, key, value) { 6 | return context.workspaceState.update(`vscode-elixir-test.${key}`, value); 7 | } 8 | 9 | function clear(context) { 10 | return context.workspaceState.update('vscode-elixir-test', undefined); 11 | } 12 | 13 | module.exports = { 14 | get, 15 | update, 16 | clear, 17 | }; 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | node_js: 5 | - "node" 6 | 7 | before_install: 8 | - if [ $TRAVIS_OS_NAME == "linux" ]; then 9 | export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; 10 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 11 | fi 12 | 13 | install: 14 | - npm install 15 | 16 | script: 17 | - npm run lint 18 | - npm test 19 | 20 | after_success: 21 | - bash <(curl -s https://codecov.io/bash) 22 | -------------------------------------------------------------------------------- /test/suite/extension.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | const vscode = require('vscode'); 6 | // const myExtension = require('../extension'); 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.equal(-1, [1, 2, 3].indexOf(5)); 13 | assert.equal(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/extension.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | function activate(context) { 6 | const commandsDir = path.join(__dirname, '/commands'); 7 | const commands = fs.readdirSync(commandsDir).map((file) => `./commands/${file}`); 8 | 9 | commands.forEach((commandEntry) => { 10 | const command = require(commandEntry); 11 | const disposable = vscode.commands.registerCommand( 12 | command.name, 13 | (args) => command.handler(context, args), 14 | ); 15 | context.subscriptions.push(disposable); 16 | }); 17 | } 18 | 19 | function deactivate() {} 20 | 21 | module.exports = { 22 | activate, 23 | deactivate, 24 | }; 25 | -------------------------------------------------------------------------------- /src/helpers/term.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const storage = require('./storage'); 3 | 4 | function terminal() { 5 | return vscode.window.activeTerminal || vscode.window.createTerminal(); 6 | } 7 | 8 | function run(context, cmd, store = true) { 9 | const config = vscode.workspace.getConfiguration('vscode-elixir-test'); 10 | const term = terminal(); 11 | 12 | if (config.beforeTest !== null) { 13 | term.sendText(config.beforeTest); 14 | } 15 | 16 | term.sendText(cmd); 17 | 18 | if (config.focusOnTerminalAfterTest) { 19 | term.show(); 20 | } 21 | 22 | if (store) { 23 | storage.update(context, 'lastCmd', cmd); 24 | } 25 | } 26 | 27 | module.exports = { 28 | run, 29 | }; 30 | -------------------------------------------------------------------------------- /test/runTest.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { runTests } = require('vscode-test'); 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../'); 10 | 11 | // The path to the extension test script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ] 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/test/suite/index" 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/suite/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Mocha = require('mocha'); 3 | const glob = require('glob'); 4 | 5 | function run() { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | }); 10 | // Use any mocha API 11 | mocha.useColors(true); 12 | 13 | const testsRoot = path.resolve(__dirname, '..'); 14 | 15 | return new Promise((c, e) => { 16 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 17 | if (err) { 18 | return e(err); 19 | } 20 | 21 | // Add files to the test suite 22 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 23 | 24 | try { 25 | // Run the mocha test 26 | mocha.run((failures) => { 27 | if (failures > 0) { 28 | e(new Error(`${failures} tests failed.`)); 29 | } else { 30 | c(); 31 | } 32 | }); 33 | } catch (error) { 34 | return e(error); 35 | } 36 | return 0; 37 | }); 38 | }); 39 | } 40 | 41 | module.exports = { 42 | run, 43 | }; 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Samuel Pordeus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/helpers/validations.js: -------------------------------------------------------------------------------- 1 | function isWindows(openedFilename) { 2 | return openedFilename.includes('\\'); 3 | } 4 | 5 | const isFolder = (openedFilename, folderName) => { 6 | if (!isWindows(openedFilename)) { 7 | return openedFilename.includes(`/${folderName}/`); 8 | } 9 | return openedFilename.includes(`\\${folderName}\\`); 10 | }; 11 | 12 | function isTestFile(openedFilename) { 13 | return openedFilename.includes('_test.exs'); 14 | } 15 | 16 | function isUmbrella(openedFilename) { 17 | return isFolder(openedFilename, 'apps'); 18 | } 19 | 20 | function isTestFolder(openedFilename) { 21 | return isFolder(openedFilename, 'test'); 22 | } 23 | 24 | function isCodeFile(openedFilename) { 25 | if (!isWindows(openedFilename)) { 26 | return openedFilename.match(/(.*\/)(test|lib)(\/)(.*)(\.\w+)$/); 27 | } 28 | return openedFilename.match(/(.*\\)(test|lib)(\\)(.*)(\.\w+)$/); 29 | } 30 | 31 | function getTestPathFilter(checkIsUmbrella, checkIsWindows) { 32 | if (!checkIsWindows) { 33 | return checkIsUmbrella ? /.*\/(apps\/.*)$/ : /.*\/(test\/.*)$/; 34 | } 35 | return checkIsUmbrella ? /.*\\(apps\\.*)$/ : /.*\\(test\\.*)$/; 36 | } 37 | 38 | module.exports = { 39 | isWindows, 40 | isTestFile, 41 | isUmbrella, 42 | isTestFolder, 43 | isCodeFile, 44 | getTestPathFilter, 45 | }; 46 | -------------------------------------------------------------------------------- /src/helpers/mix.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | 3 | function mix({ prefix = null, command, args = null, flags = null }) { 4 | const config = vscode.workspace.getConfiguration('vscode-elixir-test'); 5 | let mixEnv = ''; 6 | 7 | if (config.mixEnv) { 8 | mixEnv = `MIX_ENV=${config.mixEnv}`; 9 | } 10 | 11 | const p = prefix ? `${prefix} ` : mixEnv; 12 | const c = command ? `${command} ` : ''; 13 | const a = args ? `${args} ` : ''; 14 | const f = flags ? `${flags} ` : ''; 15 | 16 | return `${p} mix ${c}${a}${f}`.trim(); 17 | } 18 | 19 | function test() { 20 | return mix({ command: 'test' }); 21 | } 22 | 23 | function testCoverage() { 24 | return mix({ command: 'test', flags: '--cover' }); 25 | } 26 | 27 | function testFailed() { 28 | return mix({ command: 'test', flags: '--failed' }); 29 | } 30 | 31 | function testStale() { 32 | return mix({ command: 'test', flags: '--stale' }); 33 | } 34 | 35 | function testPath(fileOrDirectory) { 36 | return mix({ command: 'test', args: fileOrDirectory }); 37 | } 38 | 39 | function testFileAt(fileName, cursorPosition) { 40 | return mix({ command: 'test', args: `${fileName}:${cursorPosition}` }); 41 | } 42 | 43 | function testWatch() { 44 | return mix({ command: 'test.watch' }); 45 | } 46 | 47 | function testWatchPath(fileOrDirectory) { 48 | return mix({ command: 'test.watch', args: fileOrDirectory }); 49 | } 50 | 51 | function testWatchAt(fileName, cursorPosition) { 52 | return mix({ command: "test.watch", args: `${fileName}:${cursorPosition}` }); 53 | } 54 | 55 | module.exports = { 56 | test, 57 | testCoverage, 58 | testFailed, 59 | testStale, 60 | testPath, 61 | testFileAt, 62 | testWatch, 63 | testWatchPath, 64 | testWatchAt 65 | }; 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.8.1] = 2023-11-21 4 | 5 | - Fix a bug that would prevent users from using the `mixEnv` option. 6 | 7 | ## [1.8.0] = 2023-11-20 8 | 9 | - New: Support for custom command before running tests. 10 | - New: Workspace support: the extension will `cd` into the projects dir before running commands. 11 | - New: Added support to run test coverage 12 | - New: Added suppor to run the last test command 13 | - New: Custom `MIX_ENV` via config file 14 | - Improved: When jumping from a test file, the extension will allow to create the module file if it doesn't exist. 15 | - Fix: `cd` the root folder to run `mix test`, `mix test.watch`... 16 | - Fix: Apply linter suggestions to the project 17 | - Fix: Big refactoring remove duplicated code and prepare to start adding tests 18 | - Internal: Dynamic command loading 19 | - Internal: Bump all dependencies to the latest versions 20 | 21 | ## [1.7.1] = 2021-05-31 22 | 23 | - Fix jump to tests for multi-workspace projects 24 | 25 | ## [1.7.0] = 2020-11-07 26 | 27 | - Change shortcut for running tests to CMD+SHIFT+I to avoid conflict with default keybinding 28 | 29 | ## [1.6.0] = 2020-10-30 30 | 31 | - Fix command bugs for Windows users (path validation with `\\` instead of `/`) 32 | - Add all test commands with mix_test_watch library 33 | - Add validation helper to make it simple to maintain 34 | - Update README with Watch tests section 35 | 36 | ## [1.5.0] - 2020-10-16 37 | 38 | - Improve template on new test file 39 | - Improve test jump structure 40 | 41 | ## [1.4.0] - 2020-03-10 42 | 43 | - Add configuration to focus on editor after running tests 44 | 45 | ## [1.3.0] - 2019-11-12 46 | 47 | - Add Keybindings to run test commands 48 | - Add command to run all tests in folder 49 | - Add `when` on keybindings 50 | 51 | ## [1.2.0] - 2019-10-03 52 | 53 | - Run test suite 54 | - Add command to run test at cursor 55 | - Add module definition to generated test files 56 | 57 | ## [1.1.1] - 2019-10-03 58 | 59 | - Update repository name to vscode-test-elixir 60 | 61 | ## [1.1.0] - 2019-10-03 62 | 63 | - Run all tests in current file 64 | 65 | ## [1.0.0] - 2019-10-03 66 | 67 | - Update extension into `elixir-test` 68 | - Create test file when it does not exist 69 | 70 | ## [0.2.0] - 2019-10-03 71 | 72 | - Add cmd + shift + j as keybinding 73 | - Add warning when user tries to jump from a file without a matching test 74 | - Fix the issue with files on the `lib` folder and umbrella apps 75 | 76 | ## [0.1.0] - 2019-10-02 77 | 78 | - Initial release 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir Test 2 | [![Build Status](https://travis-ci.org/samuelpordeus/vscode-elixir-test.svg?branch=master)](https://travis-ci.org/samuelpordeus/vscode-elixir-test) 3 | 4 | A Visual Studio Code extension that helps you with your tests in Elixir! 5 | 6 | ## Installation 7 | 8 | You can install it through the [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=samuel-pordeus.elixir-test). 9 | 10 | ## Options 11 | 12 | There are a few options that you can set for this extension under the `vscode-elixir-test` object: 13 | 14 | - `mixEnv`: when set, injects the `MIX_ENV` env var before mix commands. Default: `null`. 15 | - `focusOnTerminalAfterTest`: focus the terminal after the test is executed. Default `true`. 16 | - `beforeTest`: a custom command to run on the terminal before running the test command. Default `null`. 17 | 18 | Example: 19 | ```json 20 | { 21 | "vscode-elixir-test": { 22 | "mixEnv": "test", 23 | "focusOnTerminalAfterTest": true, 24 | "beforeTest": "clear", 25 | } 26 | } 27 | ``` 28 | 29 | ## Features 30 | 31 | ### Elixir Test: Jump 32 | 33 | This enables you to navigate back and forth between your elixir file and its test. 34 | 35 | The default keybinding is `Cmd + Shift + J` (macOS) and `Ctrl + Shift + J` (Linux/Windows) 36 | 37 | ![Jump](https://media.giphy.com/media/JOFKl3KctzbrXYReLj/giphy.gif) 38 | 39 | ### Elixir Test: Run all tests on file 40 | 41 | The default keybinding is `Cmd + Shift + I, F` (macOS) and `Ctrl + Shift + 8, F` (Linux/Windows) 42 | 43 | ![Run all tests on file](https://media.giphy.com/media/lr81HlKkF60BoMnlvU/giphy.gif) 44 | 45 | ### Elixir Test: Run test at cursor 46 | 47 | This one does the same as above, but for a single test. 48 | 49 | The default keybinding is `Cmd + Shift + I, C` (macOS) and `Ctrl + Shift + 8, C` (Linux/Windows) 50 | 51 | ### Elixir Test: Run all tests in a folder 52 | 53 | This one does the same as above, but for all tests within a folder. 54 | 55 | The default keybinding is `Cmd + Shift + I, D` (macOS) and `Ctrl + Shift + 8, D` (Linux/Windows) 56 | 57 | Alternatively, right-click the folder in the Explorer and select `Elixir Test: Run all tests in a folder`. 58 | 59 | ### Elixir Test: Run test suite 60 | 61 | This one does the same as above, but for the entire test suite. 62 | 63 | The default keybinding is `Cmd + Shift + I, S` (macOS) and `Ctrl + Shift + 8, S` (Linux/Windows) 64 | 65 | ### Elixir Test: Run last command 66 | 67 | This one will run the last test command you ran from this extension. 68 | 69 | The default keybinding is `Cmd + Shift + I, L` (macOS) and `Ctrl + Shift + 8, L` (Linux/Windows) 70 | 71 | ### Watch tests 72 | 73 | To watch tests, you need to install [mix_test_watch](https://hex.pm/packages/mix_test_watch) dependency. 74 | 75 | ## Contributing 76 | 77 | Feel free to suggest some new features or report bugs [creating a new issue](https://github.com/samuelpordeus/vscode-elixir-test/issues/new). 78 | Or even better, you can open a pull request! :smile: 79 | -------------------------------------------------------------------------------- /src/helpers/test.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const path = require('path'); 3 | const term = require('./term'); 4 | const validations = require('./validations'); 5 | 6 | function maybeEnterProjectRoot(context, openedFilename) { 7 | const multiRoot = vscode.workspace.workspaceFolders.length > 1; 8 | 9 | if (multiRoot) { 10 | const project = vscode.workspace.workspaceFolders.find((entry) => openedFilename.includes(entry.uri.path)); 11 | const projectRoot = project.uri.path; 12 | term.run(context, `cd ${projectRoot}`, false); 13 | } 14 | 15 | return multiRoot; 16 | } 17 | 18 | function onTestFile(context, callback) { 19 | const activeFile = vscode.window.activeTextEditor; 20 | 21 | if (!activeFile) { 22 | return; 23 | } 24 | 25 | /* 26 | We do a +1 here because the `line` returned is zero based. 27 | Ref: https://code.visualstudio.com/api/references/vscode-api#Position 28 | */ 29 | const cursorLine = activeFile.selection.active.line + 1; 30 | 31 | const openedFilename = activeFile.document.fileName; 32 | const isTestFile = validations.isTestFile(openedFilename); 33 | const isWindows = validations.isWindows(openedFilename); 34 | const isUmbrella = validations.isUmbrella(openedFilename); 35 | 36 | if (isTestFile === true) { 37 | const testPathFilter = validations.getTestPathFilter(isUmbrella, isWindows); 38 | const fileName = openedFilename.match(testPathFilter)[1]; 39 | const cmd = callback(fileName, cursorLine); 40 | 41 | maybeEnterProjectRoot(context, openedFilename); 42 | 43 | term.run(context, cmd); 44 | } else { 45 | vscode.window.showInformationMessage('The current file is not a test file.'); 46 | } 47 | } 48 | 49 | function onTestFolder(context, folderUri, callback) { 50 | let selectedFolder; 51 | 52 | if (folderUri) { 53 | selectedFolder = folderUri.fsPath; 54 | } else { 55 | const activeFile = vscode.window.activeTextEditor; 56 | if (!activeFile) { 57 | return; 58 | } 59 | 60 | selectedFolder = path.dirname(activeFile.document.fileName); 61 | } 62 | 63 | selectedFolder += selectedFolder.endsWith('/') ? '' : '/'; 64 | 65 | const isWindows = validations.isWindows(selectedFolder); 66 | const isTestFolder = validations.isTestFolder(selectedFolder); 67 | const isUmbrella = validations.isUmbrella(selectedFolder); 68 | 69 | if (isTestFolder === true) { 70 | const testPathFilter = validations.getTestPathFilter(isUmbrella, isWindows); 71 | const folderName = selectedFolder.match(testPathFilter)[1]; 72 | const cmd = callback(folderName); 73 | 74 | maybeEnterProjectRoot(context, selectedFolder); 75 | 76 | term.run(context, cmd); 77 | } else { 78 | vscode.window.showInformationMessage('This folder is not a test folder.'); 79 | } 80 | } 81 | 82 | function onRootFolder(context, callback) { 83 | const activeFile = vscode.window.activeTextEditor; 84 | const openedFilename = activeFile.document.fileName; 85 | 86 | if (maybeEnterProjectRoot(context, openedFilename) === false) { 87 | const root = vscode.workspace.workspaceFolders[0].uri.path; 88 | term.run(context, `cd ${root}`, false); 89 | } 90 | 91 | const cmd = callback(); 92 | term.run(context, cmd); 93 | } 94 | 95 | module.exports = { 96 | onTestFile, 97 | onTestFolder, 98 | onRootFolder, 99 | }; 100 | -------------------------------------------------------------------------------- /src/commands/jumpToTest.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const validations = require('../helpers/validations'); 3 | 4 | function openFile(file) { 5 | return vscode.workspace 6 | .openTextDocument(vscode.Uri.file(file)) 7 | .then(vscode.window.showTextDocument); 8 | } 9 | 10 | function showConfirmationDialog(text, button) { 11 | return vscode.window.showWarningMessage(text, { modal: true }, button); 12 | } 13 | 14 | async function getModuleName(uriFile) { 15 | const content = (await vscode.workspace.fs.readFile(uriFile)).toString(); 16 | const moduleDefinition = content.match(/defmodule (.*) do/); 17 | 18 | return moduleDefinition[1]; 19 | } 20 | 21 | async function createNewFile(uriFile, template) { 22 | const ws = new vscode.WorkspaceEdit(); 23 | ws.createFile(uriFile); 24 | ws.insert(uriFile, new vscode.Position(0, 0), template); 25 | await vscode.workspace.applyEdit(ws); 26 | } 27 | 28 | async function createNewTestFile(dir, file) { 29 | const uriDir = vscode.Uri.file(dir); 30 | const uriFile = vscode.Uri.file(`${dir}${file}`); 31 | 32 | const originalFile = vscode.window.activeTextEditor.document.fileName; 33 | const originalFileUri = vscode.Uri.file(originalFile); 34 | const moduleName = await getModuleName(originalFileUri); 35 | 36 | await vscode.workspace.fs.createDirectory(uriDir); 37 | 38 | const template = `defmodule ${moduleName}Test do\n\tuse ExUnit.Case\n\tdoctest ${moduleName}\n\talias ${moduleName}\n\nend\n`; 39 | await createNewFile(uriFile, template); 40 | } 41 | 42 | async function createNewModuleFile(dir, file) { 43 | const uriDir = vscode.Uri.file(dir); 44 | const uriFile = vscode.Uri.file(`${dir}${file}`); 45 | 46 | const originalFile = vscode.window.activeTextEditor.document.fileName; 47 | const originalFileUri = vscode.Uri.file(originalFile); 48 | const moduleName = (await getModuleName(originalFileUri)).slice(0, -4); 49 | 50 | await vscode.workspace.fs.createDirectory(uriDir); 51 | 52 | const template = `defmodule ${moduleName} do\n @moduledoc """\n Documentation for ${moduleName}.\n """\n \nend\n`; 53 | 54 | await createNewFile(uriFile, template); 55 | } 56 | 57 | function askToCreateANewFile(dir, file, isTestFile) { 58 | let msg = `Create the test file at ${dir}?`; 59 | let callback = createNewTestFile; 60 | 61 | if (isTestFile) { 62 | msg = `Create the module file at ${dir}?`; 63 | callback = createNewModuleFile; 64 | } 65 | 66 | return showConfirmationDialog( 67 | msg, 68 | 'Create', 69 | ).then((answer) => { 70 | if (answer === 'Create') { 71 | callback(dir, file).then(() => { 72 | openFile(`${dir}${file}`); 73 | }); 74 | } 75 | }); 76 | } 77 | 78 | function handler() { 79 | const activeFile = vscode.window.activeTextEditor; 80 | 81 | if (!activeFile) { 82 | return; 83 | } 84 | 85 | const openedFilename = activeFile.document.fileName; 86 | const openedFile = validations.isCodeFile(openedFilename); 87 | 88 | if (!openedFile) { 89 | return; 90 | } 91 | 92 | const startDir = openedFile[1]; 93 | const testOrLib = openedFile[2]; 94 | const postDir = openedFile[3]; 95 | const fileName = openedFile[4]; 96 | const replacedLibOrTest = testOrLib === 'lib' ? 'test' : 'lib'; 97 | let newFilename = ''; 98 | 99 | if (fileName.includes('_test')) { 100 | const strippedFileName = fileName.replace('_test', ''); 101 | newFilename = `${strippedFileName}.ex`; 102 | } else { 103 | newFilename = `${fileName}_test.exs`; 104 | } 105 | 106 | const fileToOpen = vscode.workspace.asRelativePath( 107 | startDir + replacedLibOrTest + postDir + newFilename, 108 | false, 109 | ); 110 | 111 | vscode.workspace.findFiles(fileToOpen, '**/.elixir_ls/**').then((files) => { 112 | if (!files.length) { 113 | const isTestFile = validations.isTestFile(openedFilename); 114 | askToCreateANewFile(startDir + replacedLibOrTest + postDir, newFilename, isTestFile); 115 | } else { 116 | const file = files[0].fsPath; 117 | openFile(file); 118 | } 119 | }); 120 | } 121 | 122 | module.exports = { 123 | name: 'extension.elixirJumpToTest', 124 | handler, 125 | }; 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elixir-test", 3 | "publisher": "samuel-pordeus", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/samuelpordeus/vscode-elixir-test" 7 | }, 8 | "displayName": "Elixir Test", 9 | "description": "An extension with a few commands that helps you with your Elixir tests", 10 | "icon": "icon.png", 11 | "version": "1.8.1", 12 | "engines": { 13 | "vscode": "^1.84.0" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "main": "./src/extension.js", 19 | "contributes": { 20 | "commands": [ 21 | { 22 | "command": "extension.elixirJumpToTest", 23 | "title": "Elixir Test: Jump" 24 | }, 25 | { 26 | "command": "extension.elixirRunTestFile", 27 | "title": "Elixir Test: Run all tests on file" 28 | }, 29 | { 30 | "command": "extension.elixirRunTestCoverage", 31 | "title": "Elixir Test: Run test coverage on the project" 32 | }, 33 | { 34 | "command": "extension.elixirRunTestFolder", 35 | "title": "Elixir Test: Run all tests in a folder" 36 | }, 37 | { 38 | "command": "extension.elixirRunTestAtCursor", 39 | "title": "Elixir Test: Run test at cursor" 40 | }, 41 | { 42 | "command": "extension.elixirRunTestSuite", 43 | "title": "Elixir Test: Run test suite" 44 | }, 45 | { 46 | "command": "extension.elixirRunFailedTests", 47 | "title": "Elixir Test: Run failed tests" 48 | }, 49 | { 50 | "command": "extension.elixirRunLastTestCommand", 51 | "title": "Elixir Test: Run last test command" 52 | }, 53 | { 54 | "command": "extension.elixirRunStaleTests", 55 | "title": "Elixir Test: Run stale tests" 56 | }, 57 | { 58 | "command": "extension.elixirWatchTestFile", 59 | "title": "Elixir Test: Watch all tests on file" 60 | }, 61 | { 62 | "command": "extension.elixirWatchTestFolder", 63 | "title": "Elixir Test: Watch all tests in a folder" 64 | }, 65 | { 66 | "command": "extension.elixirWatchTestAtCursor", 67 | "title": "Elixir Test: Watch test at cursor" 68 | }, 69 | { 70 | "command": "extension.elixirWatchTestSuite", 71 | "title": "Elixir Test: Watch test suite" 72 | } 73 | ], 74 | "menus": { 75 | "explorer/context": [ 76 | { 77 | "when": "explorerResourceIsFolder", 78 | "command": "extension.elixirRunTestFolder", 79 | "group": "elixirTest" 80 | } 81 | ] 82 | }, 83 | "keybindings": [ 84 | { 85 | "command": "extension.elixirJumpToTest", 86 | "key": "ctrl+shift+j", 87 | "mac": "cmd+shift+j", 88 | "when": "editorTextFocus && editorLangId == 'elixir'" 89 | }, 90 | { 91 | "command": "extension.elixirRunTestAtCursor", 92 | "key": "ctrl+shift+8 c", 93 | "mac": "cmd+shift+i c", 94 | "when": "editorTextFocus && editorLangId == 'elixir'" 95 | }, 96 | { 97 | "command": "extension.elixirRunTestFile", 98 | "key": "ctrl+shift+8 f", 99 | "mac": "cmd+shift+i f", 100 | "when": "editorTextFocus && editorLangId == 'elixir'" 101 | }, 102 | { 103 | "command": "extension.elixirRunTestCoverage", 104 | "key": "ctrl+shift+8 k", 105 | "mac": "cmd+shift+i k", 106 | "when": "editorTextFocus && editorLangId == 'elixir'" 107 | }, 108 | { 109 | "command": "extension.elixirRunTestFolder", 110 | "key": "ctrl+shift+8 d", 111 | "mac": "cmd+shift+i d" 112 | }, 113 | { 114 | "command": "extension.elixirRunTestSuite", 115 | "key": "ctrl+shift+8 s", 116 | "mac": "cmd+shift+i s", 117 | "when": "editorTextFocus && editorLangId == 'elixir'" 118 | }, 119 | { 120 | "command": "extension.elixirRunLastTestCommand", 121 | "key": "ctrl+shift+8 l", 122 | "mac": "cmd+shift+i l", 123 | "when": "editorTextFocus && editorLangId == 'elixir'" 124 | } 125 | ], 126 | "configuration": { 127 | "title": "Elixir Test Configuration", 128 | "type": "object", 129 | "properties": { 130 | "vscode-elixir-test": { 131 | "type": "object", 132 | "properties": { 133 | "focusOnTerminalAfterTest": { 134 | "type": "boolean", 135 | "default": true, 136 | "description": "Get focus on terminal panel after test" 137 | }, 138 | "mixEnv": { 139 | "type": "string", 140 | "default": null, 141 | "description": "Set the value for the MIX_ENV env var. If null, the MIX_ENV env var will not be set" 142 | }, 143 | "beforeTest": { 144 | "type": "string", 145 | "default": null, 146 | "description": "Command to run on terminal before running tests" 147 | } 148 | } 149 | } 150 | } 151 | } 152 | }, 153 | "scripts": { 154 | "lint": "npx eslint \"./src/**/*.js\"", 155 | "test": "node ./test/runTest.js" 156 | }, 157 | "devDependencies": { 158 | "@types/glob": "^8.1.0", 159 | "@types/mocha": "^10.0.4", 160 | "@types/node": "^20.9.0", 161 | "@types/vscode": "^1.38.0", 162 | "eslint": "^8.53.0", 163 | "eslint-config-airbnb-base": "^15.0.0", 164 | "eslint-plugin-import": "^2.18.2", 165 | "glob": "^10.3.10", 166 | "mocha": "^10.2.0", 167 | "typescript": "^5.2.2", 168 | "@vscode/vsce": "^2.15.0", 169 | "@vscode/test-electron": "^2.3.6" 170 | } 171 | } --------------------------------------------------------------------------------