├── .github ├── FUNDING.yaml └── workflows │ ├── build.yaml │ └── release.yaml ├── assets ├── icon.png └── icon.svg ├── tests ├── examples │ ├── basic.yaml │ ├── more.yaml │ └── extreme.yaml └── validate.test.ts ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── src ├── state │ └── state.ts ├── utils │ ├── env_config.ts │ ├── rating.ts │ ├── utils.ts │ └── device.ts ├── extension.ts ├── testExplorer │ └── testExplorer.ts └── provider │ └── treeView.ts ├── tsconfig.json ├── eslint.config.mjs ├── LICENSE ├── esbuild.js ├── .gitignore ├── vsc-extension-quickstart.md ├── snippets └── snippets.v0.json ├── CHANGELOG.md ├── package.json ├── CONTRIBUTING.md └── README.md /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: Mastersam07 2 | custom: ['https://flutterwave.com/pay/codefarmer'] -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nexlabstudio/maestro-workbench/HEAD/assets/icon.png -------------------------------------------------------------------------------- /tests/examples/basic.yaml: -------------------------------------------------------------------------------- 1 | appId: com.example 2 | --- 3 | - launchApp 4 | - stopApp 5 | - launchApp: 6 | appId: com.example.example -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | esbuild.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/eslint.config.mjs 12 | **/*.map 13 | **/*.ts 14 | **/.vscode-test.* 15 | hidden/ 16 | .github/ 17 | COMMAND.md 18 | *.vsix -------------------------------------------------------------------------------- /src/state/state.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { MaestroWorkBenchTreeViewProvider } from '../provider/treeView'; 3 | 4 | class GlobalState { 5 | maestroTerminal: vscode.Terminal | undefined; 6 | treeDataProvider: MaestroWorkBenchTreeViewProvider | undefined; 7 | fileWatcher: vscode.FileSystemWatcher | undefined; 8 | } 9 | 10 | export const globalState = new GlobalState(); -------------------------------------------------------------------------------- /tests/examples/more.yaml: -------------------------------------------------------------------------------- 1 | appId: com.example.example 2 | name: 'A more complex test' 3 | --- 4 | - launchApp: 5 | arguments: 6 | "think": "different" 7 | - waitForAnimationToEnd 8 | - takeScreenshot: 9 | path: cropped 10 | label: potato 11 | # cropOn: 12 | # text: "Swipe Test" 13 | - assertTrue: 14 | condition: "textExists" 15 | label: Check if text exists 16 | - addMedia: 17 | files: 18 | - one.png 19 | label: Potato 20 | - assertNotVisible: 21 | label: This 22 | - runFlow: 23 | file: ./subflows/subflow.yaml -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it 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 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | on: pull_request 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-node@v4 10 | with: 11 | node-version: 20 12 | - run: npm ci 13 | - run: npm run test:unit # Can't run the UI tests without a UI 14 | - run: npm install -g @vscode/vsce 15 | - run: vsce package 16 | - name: Publish VS Code extension artifact 17 | uses: actions/upload-artifact@v4 18 | with: 19 | name: maestro-workbench-extension 20 | path: maestro-workbench-*.vsix 21 | compression-level: 0 22 | retention-days: 14 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2022", 5 | "lib": [ 6 | "ES2022" 7 | ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true, /* enable all strict type-checking options */ 11 | /* Additional Checks */ 12 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 13 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 14 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | "resolveJsonModule": true, 16 | }, 17 | "include": [ 18 | "./schema/schema.v0.json", 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "tests/" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | 4 | export default [{ 5 | files: ["**/*.ts"], 6 | }, { 7 | plugins: { 8 | "@typescript-eslint": typescriptEslint, 9 | }, 10 | 11 | languageOptions: { 12 | parser: tsParser, 13 | ecmaVersion: 2022, 14 | sourceType: "module", 15 | }, 16 | 17 | rules: { 18 | "@typescript-eslint/naming-convention": ["warn", { 19 | selector: "import", 20 | format: ["camelCase", "PascalCase"], 21 | }], 22 | 23 | curly: "warn", 24 | eqeqeq: "warn", 25 | "no-throw-literal": "warn", 26 | semi: "warn", 27 | }, 28 | }]; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off", 13 | "yaml.schemas": { 14 | "./schema/schema.v0.json": ["tests/examples/*.yaml"] 15 | }, 16 | "maestroWorkbench.filePatterns": ["tests/examples/*.yaml"], 17 | "cSpell.words": ["airplane"] 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Samuel Abada 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/utils/env_config.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export async function getEnvironmentVariables(testItem?: vscode.TestItem): Promise<{ [key: string]: string }> { 4 | const config = vscode.workspace.getConfiguration('maestroWorkbench'); 5 | let envVariables = config.get<{ [key: string]: string }>('envVariables', {}); 6 | 7 | const resolvedEnv: { [key: string]: string } = {}; 8 | for (const [key, value] of Object.entries(envVariables)) { 9 | const envMatch = value.match(/^\{ENV:([^:]+)(?::(.+))?\}$/); 10 | if (envMatch) { 11 | const envVarName = envMatch[1]; 12 | const defaultValue = envMatch[2] || ''; 13 | resolvedEnv[key] = process.env[envVarName] || defaultValue; 14 | } else { 15 | resolvedEnv[key] = value; 16 | } 17 | } 18 | 19 | return resolvedEnv; 20 | } 21 | 22 | export async function constructTestCommand(testItem: vscode.TestItem): Promise { 23 | const envVariables = await getEnvironmentVariables(testItem); 24 | const envArgs = Object.entries(envVariables).map(([key, value]) => `-e ${key}="${value}"`).join(' '); 25 | return `maestro test ${envArgs} ${testItem.uri?.fsPath}`; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/rating.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function promptForRating(context: vscode.ExtensionContext) { 4 | const RATE_PROMPT_KEY = 'maestro-workbench.ratePrompt'; 5 | const didPrompt = context.globalState.get(RATE_PROMPT_KEY); 6 | 7 | if (!didPrompt) { 8 | vscode.window.showInformationMessage( 9 | 'Enjoying Maestro Workbench? Please consider giving us a rating on the VS Code Marketplace or GitHub!', 10 | 'Rate on Marketplace', 11 | 'Star on GitHub', 12 | 'Remind Me Later', 13 | 'No, Thanks' 14 | ).then(selection => { 15 | if (selection === 'Rate on Marketplace') { 16 | vscode.env.openExternal(vscode.Uri.parse('https://marketplace.visualstudio.com/items?itemName=Mastersam.maestro-workbench')); 17 | context.globalState.update(RATE_PROMPT_KEY, true); 18 | } else if (selection === 'Star on GitHub') { 19 | vscode.env.openExternal(vscode.Uri.parse('https://github.com/Mastersam07/maestro-workbench')); 20 | context.globalState.update(RATE_PROMPT_KEY, true); 21 | } else if (selection === 'No, Thanks' || selection === undefined) { 22 | context.globalState.update(RATE_PROMPT_KEY, true); 23 | } 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | 3 | const production = process.argv.includes('--production'); 4 | const watch = process.argv.includes('--watch'); 5 | 6 | /** 7 | * @type {import('esbuild').Plugin} 8 | */ 9 | const esbuildProblemMatcherPlugin = { 10 | name: 'esbuild-problem-matcher', 11 | 12 | setup(build) { 13 | build.onStart(() => { 14 | console.log('[watch] build started'); 15 | }); 16 | build.onEnd((result) => { 17 | result.errors.forEach(({ text, location }) => { 18 | console.error(`✘ [ERROR] ${text}`); 19 | console.error(` ${location.file}:${location.line}:${location.column}:`); 20 | }); 21 | console.log('[watch] build finished'); 22 | }); 23 | }, 24 | }; 25 | 26 | async function main() { 27 | const ctx = await esbuild.context({ 28 | entryPoints: [ 29 | 'src/extension.ts' 30 | ], 31 | bundle: true, 32 | format: 'cjs', 33 | minify: production, 34 | sourcemap: !production, 35 | sourcesContent: false, 36 | platform: 'node', 37 | outfile: 'dist/extension.js', 38 | external: ['vscode'], 39 | logLevel: 'silent', 40 | plugins: [ 41 | /* add to the end of plugins array */ 42 | esbuildProblemMatcherPlugin, 43 | ], 44 | }); 45 | if (watch) { 46 | await ctx.watch(); 47 | } else { 48 | await ctx.rebuild(); 49 | await ctx.dispose(); 50 | } 51 | } 52 | 53 | main().catch(e => { 54 | console.error(e); 55 | process.exit(1); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/validate.test.ts: -------------------------------------------------------------------------------- 1 | import * as YAML from 'yaml'; 2 | import * as fs from 'fs'; 3 | import Ajv from "ajv" 4 | import * as schema from '../schema/schema.v0.json' 5 | import { assert } from 'chai'; 6 | 7 | const ajv = new Ajv({ 8 | allowUnionTypes: true, 9 | strict: false, 10 | }) 11 | 12 | const validate = ajv.compile(schema) 13 | 14 | const testFiles = [ 15 | './tests/examples/basic.yaml', 16 | './tests/examples/more.yaml', 17 | './tests/examples/extreme.yaml', 18 | ] 19 | 20 | const parseFile = (file: string) => { 21 | const fileContent = fs.readFileSync(file, 'utf-8'); 22 | const parsed = YAML.parseAllDocuments(fileContent) as any[]; 23 | return { 24 | config: parsed[0].toJSON(), 25 | content: parsed[1].toJSON() 26 | } 27 | } 28 | 29 | 30 | for (const file of testFiles) { 31 | it(`should validate the config of ${file}`, () => { 32 | const config = parseFile(file).config 33 | 34 | const valid = validate(config) 35 | try { 36 | assert.isTrue(valid) 37 | } catch (e) { 38 | // For more useful output 39 | throw new Error(JSON.stringify(validate.errors, null, 2)) 40 | } 41 | }) 42 | it(`should validate the content of ${file}`, () => { 43 | const content = parseFile(file).content 44 | 45 | const valid = validate(content) 46 | try { 47 | assert.isTrue(valid) 48 | } catch (e) { 49 | // For more useful output 50 | throw new Error(JSON.stringify(validate.errors, null, 2)) 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "dependsOn": [ 9 | "npm: watch:tsc", 10 | "npm: watch:esbuild" 11 | ], 12 | "presentation": { 13 | "reveal": "never" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch:esbuild", 23 | "group": "build", 24 | "problemMatcher": "$esbuild-watch", 25 | "isBackground": true, 26 | "label": "npm: watch:esbuild", 27 | "presentation": { 28 | "group": "watch", 29 | "reveal": "never" 30 | } 31 | }, 32 | { 33 | "type": "npm", 34 | "script": "watch:tsc", 35 | "group": "build", 36 | "problemMatcher": "$tsc-watch", 37 | "isBackground": true, 38 | "label": "npm: watch:tsc", 39 | "presentation": { 40 | "group": "watch", 41 | "reveal": "never" 42 | } 43 | }, 44 | { 45 | "type": "npm", 46 | "script": "watch-tests", 47 | "problemMatcher": "$tsc-watch", 48 | "isBackground": true, 49 | "presentation": { 50 | "reveal": "never", 51 | "group": "watchers" 52 | }, 53 | "group": "build" 54 | }, 55 | { 56 | "label": "tasks: watch-tests", 57 | "dependsOn": [ 58 | "npm: watch", 59 | "npm: watch-tests" 60 | ], 61 | "problemMatcher": [] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: 🚀 Maestro Workbench Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Release Version (e.g., v1.0.0)" 8 | required: true 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | release: 15 | name: Create GitHub Release & Publish to VS Code Marketplace 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | 29 | - name: Install vsce (VS Code Extension CLI) 30 | run: npm install -g @vscode/vsce 31 | 32 | - name: Install dependencies 33 | run: npm install 34 | 35 | - name: Package VS Code Extension 36 | run: vsce package 37 | 38 | - name: Create Git Tag (If Not Exists) 39 | run: | 40 | if git rev-parse ${{ inputs.version }} >/dev/null 2>&1; then 41 | echo "Tag ${{ inputs.version }} already exists. Skipping..." 42 | else 43 | git config user.name "github-actions[bot]" 44 | git config user.email "github-actions[bot]@users.noreply.github.com" 45 | git tag ${{ inputs.version }} HEAD -m "Release ${{ inputs.version }}" 46 | git push origin ${{ inputs.version }} 47 | fi 48 | 49 | - name: Create GitHub Release 50 | uses: softprops/action-gh-release@v2 51 | with: 52 | tag_name: ${{ inputs.version }} 53 | name: Release ${{ inputs.version }} 54 | draft: false 55 | prerelease: false 56 | generate_release_notes: true 57 | files: "*.vsix" 58 | 59 | - name: Publish to VS Code Marketplace 60 | run: vsce publish --packagePath *.vsix 61 | env: 62 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | hidden/ 133 | *.vsix -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | * install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint) 15 | 16 | 17 | ## Get up and running straight away 18 | 19 | * Press `F5` to open a new window with your extension loaded. 20 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 21 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 22 | * Find output from your extension in the debug console. 23 | 24 | ## Make changes 25 | 26 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 27 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 28 | 29 | 30 | ## Explore the API 31 | 32 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 33 | 34 | ## Run tests 35 | 36 | * Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) 37 | * Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. 38 | * Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` 39 | * See the output of the test result in the Test Results view. 40 | * Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. 41 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 42 | * You can create folders inside the `test` folder to structure your tests any way you want. 43 | 44 | ## Go further 45 | 46 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 47 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 48 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 49 | -------------------------------------------------------------------------------- /snippets/snippets.v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "Launch App": { 3 | "prefix": "launchApp", 4 | "body": [ 5 | "- launchApp:", 6 | " appId: \"${1:com.example.app}\"", 7 | " clearState: ${2:true}", 8 | " stopApp: ${3:true}", 9 | " permissions:", 10 | " ${4:notifications}: ${5:allow}", 11 | " ${6:android.permission.ACCESS_FINE_LOCATION}: ${7:deny}" 12 | ], 13 | "description": "Template for launching an app with optional permissions." 14 | }, 15 | "Assert Visible": { 16 | "prefix": "assertVisible", 17 | "body": [ 18 | "- assertVisible:", 19 | " text: \"${1:Sample Text}\"", 20 | " id: \"${2:sample-id}\"", 21 | " enabled: ${3:true}" 22 | ], 23 | "description": "Template for asserting visibility of an element." 24 | }, 25 | "Assert Not Visible": { 26 | "prefix": "assertNotVisible", 27 | "body": [ 28 | "- assertNotVisible:", 29 | " text: \"${1:Sample Text}\"", 30 | " id: \"${2:sample-id}\"", 31 | " enabled: ${3:false}" 32 | ], 33 | "description": "Template for asserting an element is not visible." 34 | }, 35 | "Input Text": { 36 | "prefix": "inputText", 37 | "body": [ 38 | "- inputText: \"${1:sample@example.com}\"" 39 | ], 40 | "description": "Template for inputting text into a field." 41 | }, 42 | "Repeat": { 43 | "prefix": "repeat", 44 | "body": [ 45 | "- repeat:", 46 | " times: ${1:3}", 47 | " commands:", 48 | " - ${2:command}" 49 | ], 50 | "description": "Template for repeating a set of commands." 51 | }, 52 | "Run Flow": { 53 | "prefix": "runFlow", 54 | "body": [ 55 | "- runFlow:", 56 | " file: \"${1:anotherFlow.yaml}\"", 57 | " env:", 58 | " ${2:ENV_VAR}: ${3:value}" 59 | ], 60 | "description": "Template for running a flow from a file." 61 | }, 62 | "Scroll Until Visible": { 63 | "prefix": "scrollUntilVisible", 64 | "body": [ 65 | "- scrollUntilVisible:", 66 | " element:", 67 | " text: \"${1:Sample Text}\"", 68 | " direction: \"${2:DOWN}\"", 69 | " timeout: ${3:20000}" 70 | ], 71 | "description": "Template for scrolling until an element is visible." 72 | }, 73 | "Tap On": { 74 | "prefix": "tapOn", 75 | "body": [ 76 | "- tapOn:", 77 | " text: \"${1:Sample Text}\"", 78 | " id: \"${2:sample-id}\"", 79 | " below:", 80 | " text: \"${3:Another Element}\"" 81 | ], 82 | "description": "Template for tapping on an element." 83 | }, 84 | "Long Press On": { 85 | "prefix": "longPressOn", 86 | "body": [ 87 | "- longPressOn:", 88 | " text: \"${1:Sample Text}\"", 89 | " id: \"${2:sample-id}\"", 90 | " above:", 91 | " text: \"${3:Another Element}\"" 92 | ], 93 | "description": "Template for long-pressing an element." 94 | }, 95 | "Swipe": { 96 | "prefix": "swipe", 97 | "body": [ 98 | "- swipe:", 99 | " start: \"${1:50%,50%}\"", 100 | " end: \"${2:10%,10%}\"", 101 | " duration: ${3:500}" 102 | ], 103 | "description": "Template for performing a swipe gesture." 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { globalState } from '../state/state'; 3 | 4 | export function getFilePatterns(): string[] { 5 | const config = vscode.workspace.getConfiguration('maestroWorkbench'); 6 | return config.get('filePatterns', [ 7 | "maestro/**/*.yaml", 8 | "maestro/**/*.yml", 9 | "**/.maestro/**/*.yaml", 10 | "**/.maestro/**/*.yml" 11 | ]); 12 | } 13 | 14 | export function getOrCreateTerminal(): vscode.Terminal { 15 | if (!globalState.maestroTerminal) { 16 | globalState.maestroTerminal = vscode.window.createTerminal({ 17 | name: "Maestro Test", 18 | }); 19 | 20 | globalState.maestroTerminal.show(); 21 | vscode.window.onDidCloseTerminal((closedTerminal) => { 22 | if (closedTerminal.name === "Maestro Test") { 23 | globalState.maestroTerminal = undefined; 24 | } 25 | }); 26 | } 27 | return globalState.maestroTerminal; 28 | } 29 | 30 | /** 31 | * Updates the YAML schema associations in VS Code settings to ensure only our schema is used 32 | * for Maestro YAML files. This prevents conflicts with other YAML schemas. 33 | * Note: This function only manages the main extension schema patterns and does not affect 34 | * test-specific schema configurations. 35 | */ 36 | export async function updateYamlSchemaAssociations(schemaPath: string = './schema/schema.v0.json') { 37 | const yamlConfig = vscode.workspace.getConfiguration('yaml'); 38 | const currentSchemas = yamlConfig.get<{ [key: string]: string[] }>('schemas') || {}; 39 | const filePatterns = getFilePatterns(); 40 | 41 | // Create updated schemas by removing ALL entries that point to our schema file 42 | const updatedSchemas = Object.fromEntries( 43 | Object.entries(currentSchemas).filter(([schema]) => { 44 | // Remove any schema entry that points to our schema file, regardless of the path 45 | return !schema.endsWith('schema.v0.json'); 46 | }) 47 | ); 48 | 49 | // First, remove any schema registrations from global settings 50 | const globalSchemas = Object.fromEntries( 51 | Object.entries(currentSchemas).filter(([schema]) => 52 | !schema.endsWith('schema.v0.json') 53 | ) 54 | ); 55 | await yamlConfig.update('schemas', globalSchemas, vscode.ConfigurationTarget.Global); 56 | 57 | // Then update workspace settings with our current configuration 58 | updatedSchemas[schemaPath] = filePatterns; 59 | await yamlConfig.update('schemas', updatedSchemas, vscode.ConfigurationTarget.Workspace); 60 | } 61 | 62 | export function checkYamlExtension() { 63 | const yamlExtension = vscode.extensions.getExtension('redhat.vscode-yaml'); 64 | 65 | if (!yamlExtension) { 66 | vscode.window 67 | .showWarningMessage('The YAML extension is not installed. Some features of Maestro Workbench may not work correctly. Would you like to install it?', 'Install', 'Cancel') 68 | .then(selection => { 69 | if (selection === 'Install') { 70 | vscode.commands.executeCommand('workbench.extensions.search', 'redhat.vscode-yaml'); 71 | } 72 | }); 73 | } else if (!yamlExtension.isActive) { 74 | Promise.resolve(yamlExtension.activate()).then(() => { 75 | vscode.window.showInformationMessage('YAML extension activated successfully for Maestro Workbench.'); 76 | }).catch(() => { 77 | vscode.window.showErrorMessage('Failed to activate the YAML extension. Some features may not work correctly.'); 78 | }); 79 | } 80 | } -------------------------------------------------------------------------------- /src/utils/device.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { exec } from 'child_process'; 3 | import { promisify } from 'util'; 4 | 5 | const execAsync = promisify(exec); 6 | 7 | export interface Device { 8 | id: string; 9 | name: string; 10 | type: string; 11 | } 12 | 13 | export async function getAvailableDevices(): Promise { 14 | const devices: Device[] = []; 15 | 16 | try { 17 | const { stdout: adbOutput } = await execAsync('adb devices'); 18 | const androidDevices = parseAdbDevicesOutput(adbOutput); 19 | devices.push(...androidDevices); 20 | } catch (error) { 21 | console.error('Error getting Android devices:', error); 22 | } 23 | 24 | try { 25 | const { stdout: xcrunOutput } = await execAsync('xcrun xctrace list devices'); 26 | const iosDevices = parseXcrunDevicesOutput(xcrunOutput); 27 | devices.push(...iosDevices); 28 | } catch (error) { 29 | console.error('Error getting iOS devices:', error); 30 | } 31 | 32 | return devices; 33 | } 34 | 35 | export async function getDefaultDevice(): Promise { 36 | const config = vscode.workspace.getConfiguration('maestroWorkbench'); 37 | return config.get('defaultDevice'); 38 | } 39 | 40 | function parseAdbDevicesOutput(output: string): Device[] { 41 | const lines = output.split('\n'); 42 | const devices: Device[] = []; 43 | 44 | for (let i = 1; i < lines.length; i++) { 45 | const line = lines[i].trim(); 46 | if (line && !line.includes('---')) { 47 | const [id, status] = line.split(/\s+/); 48 | if (id && status === 'device') { 49 | devices.push({ 50 | id, 51 | name: id, 52 | type: 'Android Device' 53 | }); 54 | } 55 | } 56 | } 57 | 58 | return devices; 59 | } 60 | 61 | function parseXcrunDevicesOutput(output: string): Device[] { 62 | const lines = output.split('\n'); 63 | const devices: Device[] = []; 64 | let currentSection = ''; 65 | 66 | for (const line of lines) { 67 | const trimmedLine = line.trim(); 68 | 69 | if (!trimmedLine || trimmedLine.startsWith('==')) { 70 | if (trimmedLine.startsWith('== Devices ==')) { 71 | currentSection = 'device'; 72 | } else if (trimmedLine.startsWith('== Devices Offline ==')) { 73 | currentSection = 'offline'; 74 | } else if (trimmedLine.startsWith('== Simulators ==')) { 75 | currentSection = 'simulator'; 76 | } 77 | continue; 78 | } 79 | 80 | const match = trimmedLine.match(/^(.+?)(?:\s+\(([^)]+)\))?\s+\(([A-F0-9-]+)\)$/); 81 | if (match) { 82 | const [, name, version, id] = match; 83 | devices.push({ 84 | id, 85 | name: version ? `${name} (${version})` : name, 86 | type: currentSection === 'simulator' ? 'iOS Simulator' : 87 | currentSection === 'offline' ? 'iOS Device (Offline)' : 'iOS Device' 88 | }); 89 | } 90 | } 91 | 92 | return devices; 93 | } 94 | 95 | export async function selectDevice(): Promise { 96 | const devices = await getAvailableDevices(); 97 | if (devices.length === 0) { 98 | vscode.window.showErrorMessage('No devices found. Please connect a device and try again.'); 99 | return undefined; 100 | } 101 | 102 | const items = devices.map(device => ({ 103 | label: `${device.name} (${device.id})`, 104 | description: device.type, 105 | deviceId: device.id 106 | })); 107 | 108 | const selected = await vscode.window.showQuickPick(items, { 109 | placeHolder: 'Select a device to run tests on' 110 | }); 111 | 112 | return selected?.deviceId; 113 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from "path"; 3 | import { MaestroWorkBenchTreeViewProvider } from './provider/treeView'; 4 | import { promptForRating } from './utils/rating'; 5 | import { globalState } from './state/state'; 6 | import { checkYamlExtension, getFilePatterns, getOrCreateTerminal, updateYamlSchemaAssociations } from './utils/utils'; 7 | import { watchTestFiles, discoverTests, registerTestProfiles } from './testExplorer/testExplorer'; 8 | import { getAvailableDevices } from './utils/device'; 9 | 10 | let deviceOutputChannel: vscode.OutputChannel; 11 | 12 | function updateFileWatcherAndTreeView(controller: vscode.TestController, context: vscode.ExtensionContext) { 13 | const filePatterns = getFilePatterns(); 14 | 15 | globalState.treeDataProvider = new MaestroWorkBenchTreeViewProvider(filePatterns); 16 | vscode.window.registerTreeDataProvider('maestroBenchTreeView', globalState.treeDataProvider); 17 | 18 | const watcher = watchTestFiles(controller); 19 | 20 | context.subscriptions.push(globalState.fileWatcher ?? watcher); 21 | } 22 | 23 | export function activate(context: vscode.ExtensionContext) { 24 | checkYamlExtension(); 25 | 26 | const schemaPath = vscode.Uri.file(path.join(context.extensionPath, "schema", "schema.v0.json")).toString(); 27 | 28 | const controller = vscode.tests.createTestController( 29 | 'maestroWorkbenchTestProvider', 30 | 'Maestro Tests' 31 | ); 32 | 33 | context.subscriptions.push(controller); 34 | 35 | discoverTests(controller); 36 | registerTestProfiles(controller); 37 | promptForRating(context); 38 | updateFileWatcherAndTreeView(controller, context); 39 | 40 | vscode.workspace.onDidChangeConfiguration((e) => { 41 | if (e.affectsConfiguration('maestroWorkbench.filePatterns')) { 42 | vscode.window.showInformationMessage('Maestro File patterns updated. Refreshing workbench...'); 43 | updateFileWatcherAndTreeView(controller, context); 44 | updateYamlSchemaAssociations(schemaPath); 45 | } 46 | }); 47 | 48 | vscode.workspace.onDidChangeConfiguration((e) => { 49 | if (e.affectsConfiguration('maestroWorkbench.envVariables')) { 50 | vscode.window.showInformationMessage('Maestro test variables updated.'); 51 | } 52 | }); 53 | 54 | context.subscriptions.push( 55 | vscode.commands.registerCommand('maestroWorkbench.refreshTree', () => { 56 | if (globalState.treeDataProvider) { globalState.treeDataProvider.refresh(); } 57 | }), 58 | ); 59 | 60 | context.subscriptions.push( 61 | vscode.commands.registerCommand('maestroWorkbench.openMaestroStudio', () => { 62 | const terminal = getOrCreateTerminal(); 63 | terminal.sendText("maestro studio"); 64 | }) 65 | ); 66 | 67 | let listDevicesCommand = vscode.commands.registerCommand('maestroWorkbench.listDevices', async () => { 68 | if (!deviceOutputChannel) { 69 | deviceOutputChannel = vscode.window.createOutputChannel('Maestro Devices'); 70 | } 71 | 72 | deviceOutputChannel.clear(); 73 | deviceOutputChannel.show(true); 74 | deviceOutputChannel.appendLine('Fetching available devices...'); 75 | 76 | await vscode.window.withProgress({ 77 | location: vscode.ProgressLocation.Notification, 78 | title: "Fetching Maestro Devices", 79 | cancellable: false 80 | }, async (progress) => { 81 | progress.report({ message: "Scanning for devices..." }); 82 | const devices = await getAvailableDevices(); 83 | 84 | if (devices.length === 0) { 85 | deviceOutputChannel.appendLine('No devices found. Please connect a device and try again.'); 86 | return; 87 | } 88 | 89 | const groupedDevices = devices.reduce((acc, device) => { 90 | if (!acc[device.type]) { 91 | acc[device.type] = []; 92 | } 93 | acc[device.type].push(device); 94 | return acc; 95 | }, {} as Record); 96 | 97 | Object.entries(groupedDevices).forEach(([type, deviceList]) => { 98 | deviceOutputChannel.appendLine(`\n${type}:`); 99 | deviceList.forEach(device => { 100 | deviceOutputChannel.appendLine(` • ${device.name} (${device.id})`); 101 | }); 102 | }); 103 | }); 104 | }); 105 | 106 | context.subscriptions.push(listDevicesCommand); 107 | } 108 | 109 | export function deactivate() { 110 | globalState.fileWatcher?.dispose(); 111 | globalState.maestroTerminal?.dispose(); 112 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.9.4] - 2024-04-02 6 | 7 | ### Added 8 | 9 | - Collapsible tree view in test explorer ([#70](https://github.com/Mastersam07/maestro-workbench/issues/70)) 10 | 11 | ### Fixed 12 | 13 | - Fixed buggy treeview ([#65](https://github.com/Mastersam07/maestro-workbench/issues/65)) 14 | - Fixed VSCode Config being created / amended in unrelated projects ([#73](https://github.com/Mastersam07/maestro-workbench/issues/73)) 15 | 16 | ## [0.9.3] - 2024-03-21 17 | 18 | ### Added 19 | 20 | - Support for test variables 21 | - Support for selecting and setting test device 22 | 23 | ### Fixed 24 | 25 | - Fixed schema validation issues by properly managing YAML schema associations 26 | - Removed duplicate schema entries from global settings 27 | - Ensured schema configurations are properly scoped to workspace settings 28 | - Fixed issue where test schema configurations were being added to user settings 29 | 30 | ## [0.9.2] - 2025-02-19 31 | 32 | ### Fixed 33 | 34 | - Schema: Support for all element selectors in AssertVisible / AssertNotVisible 35 | 36 | ## [0.9.1] - 2025-02-18 37 | 38 | ### Fixed 39 | 40 | - Schema: Support for missing element selector options 41 | - Schema: Support for providing element selector index by JS expression 42 | - Schema: Fix required properties for extendedWaitUntil 43 | 44 | ## [0.9.0] - 2025-01-21 45 | 46 | ### Added 47 | 48 | - Support for all maestro commands 49 | - Improved intellisense 50 | 51 | ## [0.4.3] - 2025-01-09 52 | 53 | ### Fixed 54 | 55 | - Corrected addMedia command 56 | - [addMedia command only supports the array form](https://github.com/Mastersam07/maestro-workbench/pull/20) 57 | 58 | ## [0.4.2] - 2025-01-09 59 | 60 | ### Fixed 61 | 62 | - Set current working directory on running test process 63 | - [Unable to run tests that take screenshots](https://github.com/Mastersam07/maestro-workbench/issues/17) 64 | 65 | ## [0.4.1] - 2025-01-06 66 | 67 | ### Fixed 68 | 69 | - Resolved an issue where multiple schema associations from different extension versions were not properly updated, ensuring only the latest schema is associated with Maestro YAML files. 70 | 71 | 72 | ## [0.4.0] - 2025-01-02 73 | 74 | ### Fixed 75 | 76 | - Failed initialization on certan ends 77 | 78 | ### Added 79 | 80 | - Enforce `redhat.vscode-yaml` as dependency of maestro workbench 81 | 82 | ## [0.3.2] - 2025-01-02 83 | 84 | ### Fixed 85 | 86 | - Allow dynamic timeout values for the following commands: 87 | - scrollUntilVisible 88 | - waitForAnimationToEnd. 89 | - extendedWaitUntil. 90 | 91 | ## [0.3.1] - 2025-01-02 92 | 93 | ### Added 94 | 95 | - Flow configurations: 96 | - Added support for env. 97 | - Added support for onFlowStart. 98 | - Added support for onFlowComplete. 99 | 100 | ## [0.3.0] - 2024-12-30 101 | 102 | ### Added 103 | 104 | - Workbench: 105 | - View flow dependencies (e.g., runFlow references, runScript references). 106 | 107 | ### Fixed 108 | 109 | - Update test explorer on adding and removing test flow 110 | - Update workbench with on adding and removing test flow 111 | 112 | ## [0.2.2] - 2024-12-30 113 | 114 | ### Changed 115 | 116 | - Apply schema configuration for maestro file patterns only 117 | 118 | ## [0.2.1] - 2024-12-30 119 | 120 | ### Fixed 121 | 122 | update global schema configuration for maestro file patterns only 123 | 124 | - Resolved issue with jsonSchema 125 | 126 | ## [0.2.0] - 2024-12-29 127 | 128 | ### Added 129 | 130 | - Test explorer: 131 | - View all tests in test explorer. 132 | - Run tests in test explorer. 133 | - Stop running/queued up tests. 134 | - View test logs. 135 | 136 | ## [0.1.0] - 2024-12-28 137 | 138 | ### Added 139 | 140 | - Initial release of Maestro Workbench with core features: 141 | - YAML Schema Validation and IntelliSense. 142 | - Test Execution via CLI integration. 143 | - File Management with a dedicated tree view. 144 | - Documentation Access with links to official resources. 145 | 146 | ## [Unreleased] 147 | 148 | ### Added 149 | 150 | - **YAML Schema Validation and IntelliSense** 151 | - Auto-complete for Maestro commands (`tapOn`, `assertVisible`, `runFlow`, etc.). 152 | - Validation for command parameters (e.g., `visible`, `id`, `text`, etc.). 153 | - Inline documentation and examples for commands via tooltips. 154 | - Insert templates/snippets for common Maestro commands. 155 | 156 | - **Test Execution** 157 | - Run Maestro flows directly from VS Code via CLI integration. 158 | - Display basic test results in a terminal output. 159 | 160 | - **File Management** 161 | - Dedicated tree view for managing Maestro YAML files. 162 | - Highlight dependencies between flows (e.g., `runFlow` referencing another file). 163 | 164 | - **Documentation Access** 165 | - Integrated links to Maestro's official documentation. 166 | - Quick-start guide for new users. 167 | 168 | ### Changed 169 | 170 | - Improved performance of the tree view for large projects. 171 | - Enhanced error messages for YAML validation to provide more context. 172 | 173 | ### Fixed 174 | 175 | - Resolved issue where IntelliSense suggestions were not appearing for certain Maestro commands. 176 | - Fixed bug causing the extension to crash when opening non-YAML files. 177 | 178 | [0.1.0]: https://github.com/Mastersam07/maestro-workbench/releases/tag/v0.1.0 179 | -------------------------------------------------------------------------------- /src/testExplorer/testExplorer.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { exec, ChildProcess } from 'child_process'; 3 | import * as path from 'path'; 4 | 5 | import { getFilePatterns } from '../utils/utils'; 6 | import { globalState } from '../state/state'; 7 | import { constructTestCommand } from '../utils/env_config'; 8 | import { selectDevice, getDefaultDevice } from '../utils/device'; 9 | 10 | export function registerTestProfiles(controller: vscode.TestController) { 11 | controller.createRunProfile( 12 | 'Run Tests', 13 | vscode.TestRunProfileKind.Run, 14 | (request, token) => runHandler(controller, request, token), 15 | true 16 | ); 17 | } 18 | 19 | export async function discoverTests(controller: vscode.TestController) { 20 | const filePatterns = getFilePatterns(); 21 | const includePattern = `{${filePatterns.join(',')}}`; 22 | 23 | try { 24 | const files = await vscode.workspace.findFiles(includePattern); 25 | const testItems = new Map(); 26 | 27 | controller.items.forEach(item => controller.items.delete(item.id)); 28 | 29 | files.forEach(file => { 30 | const relativePath = vscode.workspace.asRelativePath(file); 31 | const parts = relativePath.split('/'); 32 | 33 | let currentPath = ''; 34 | let parentItem: vscode.TestItem | undefined; 35 | 36 | parts.forEach((part, index) => { 37 | currentPath = currentPath ? path.join(currentPath, part) : part; 38 | const isFile = index === parts.length - 1; 39 | 40 | let item = testItems.get(currentPath); 41 | if (!item) { 42 | item = controller.createTestItem(currentPath, part, file); 43 | item.canResolveChildren = !isFile; 44 | 45 | if (parentItem) { 46 | parentItem.children.add(item); 47 | } else { 48 | controller.items.add(item); 49 | } 50 | 51 | testItems.set(currentPath, item); 52 | } 53 | 54 | parentItem = item; 55 | }); 56 | }); 57 | } catch (error) { 58 | console.error('Error discovering test files:', error); 59 | } 60 | } 61 | 62 | async function runHandler( 63 | controller: vscode.TestController, 64 | request: vscode.TestRunRequest, 65 | token: vscode.CancellationToken 66 | ) { 67 | const run = controller.createTestRun(request); 68 | 69 | const queue: vscode.TestItem[] = request.include ? [...request.include] : []; 70 | if (!request.include) { 71 | controller.items.forEach((testItem) => { 72 | queue.push(testItem); 73 | }); 74 | } 75 | 76 | token.onCancellationRequested(() => { 77 | queue.forEach(test => run.skipped(test)); 78 | run.end(); 79 | }); 80 | 81 | let deviceId = await getDefaultDevice(); 82 | if (!deviceId) { 83 | deviceId = await selectDevice(); 84 | if (!deviceId) { 85 | queue.forEach(test => run.skipped(test)); 86 | run.end(); 87 | return; 88 | } 89 | } 90 | 91 | while (queue.length > 0) { 92 | const test = queue.pop()!; 93 | if (request.exclude?.includes(test)) { 94 | continue; 95 | } 96 | 97 | run.started(test); 98 | 99 | try { 100 | await executeTestWithEnv(test, token, deviceId); 101 | run.passed(test); 102 | } catch (error) { 103 | const message = error instanceof Error ? error.message : 'An unknown error occurred.'; 104 | run.failed(test, new vscode.TestMessage(message)); 105 | } 106 | } 107 | 108 | run.end(); 109 | } 110 | 111 | async function executeTestWithEnv( 112 | test: vscode.TestItem, 113 | token: vscode.CancellationToken, 114 | deviceId: string 115 | ): Promise { 116 | return new Promise(async (resolve, reject) => { 117 | const { uri } = test; 118 | if (!uri) { 119 | return reject(new Error('Test item URI is undefined.')); 120 | } 121 | 122 | const { fsPath } = uri; 123 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri)?.uri.fsPath; 124 | 125 | if (!workspaceFolder) { 126 | return reject(new Error('Workspace folder is undefined.')); 127 | } 128 | 129 | try { 130 | const baseCommand = await constructTestCommand(test); 131 | const command = `maestro --device ${deviceId} ${baseCommand.replace('maestro test', 'test')}`; 132 | 133 | const process = exec(command, { cwd: workspaceFolder }, (error, stdout, stderr) => { 134 | if (token.isCancellationRequested) { 135 | return reject(new Error('Test execution cancelled.')); 136 | } 137 | if (error) { 138 | const errorMessage = stdout.trim() === '' ? stderr : stdout; 139 | return reject(new Error(errorMessage)); 140 | } 141 | resolve(process); 142 | }); 143 | 144 | token.onCancellationRequested(() => { 145 | process.kill(); 146 | reject(new Error('Test execution cancelled.')); 147 | }); 148 | } catch (error) { 149 | reject(error); 150 | } 151 | }); 152 | } 153 | 154 | export function watchTestFiles(controller: vscode.TestController) { 155 | if (globalState.fileWatcher) { 156 | globalState.fileWatcher?.dispose(); 157 | } 158 | globalState.fileWatcher = vscode.workspace.createFileSystemWatcher("**/*.{yaml,yml}"); 159 | 160 | let debounceTimeout: NodeJS.Timeout | undefined; 161 | 162 | const debouncedRefresh = () => { 163 | if (debounceTimeout) { 164 | clearTimeout(debounceTimeout); 165 | } 166 | debounceTimeout = setTimeout(() => { 167 | refreshTestExplorer(controller); 168 | }, 300); 169 | }; 170 | 171 | globalState.fileWatcher.onDidCreate(uri => { 172 | debouncedRefresh(); 173 | }); 174 | 175 | globalState.fileWatcher.onDidChange(uri => { 176 | debouncedRefresh(); 177 | }); 178 | 179 | globalState.fileWatcher.onDidDelete(uri => { 180 | debouncedRefresh(); 181 | }); 182 | 183 | return globalState.fileWatcher; 184 | } 185 | 186 | export async function refreshTestExplorer(controller: vscode.TestController) { 187 | await discoverTests(controller); 188 | globalState.treeDataProvider?.refresh(); 189 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maestro-workbench", 3 | "displayName": "Maestro Workbench", 4 | "description": "A VS Code extension for Maestro YAML validation, IntelliSense, and testing.", 5 | "publisher": "Mastersam", 6 | "license": "MIT", 7 | "version": "0.9.4", 8 | "icon": "assets/icon.png", 9 | "author": { 10 | "email": "abadasamuelosp@gmail.com", 11 | "name": "Samuel Abada", 12 | "url": "https://github.com/mastersam07" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/Mastersam07/maestro-workbench" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/Mastersam07/maestro-workbench/issues", 20 | "email": "abadasamuelosp@gmail.com" 21 | }, 22 | "engines": { 23 | "vscode": "^1.61.0" 24 | }, 25 | "extensionDependencies": [ 26 | "redhat.vscode-yaml" 27 | ], 28 | "categories": [ 29 | "Other", 30 | "Snippets", 31 | "Extension Packs", 32 | "Debuggers", 33 | "Linters", 34 | "Testing" 35 | ], 36 | "keywords": [ 37 | "maestro", 38 | "tests", 39 | "ui testing", 40 | "ui test", 41 | "maestro studio", 42 | "yaml" 43 | ], 44 | "homepage": "https://github.com/Mastersam07/maestro-workbench", 45 | "activationEvents": [ 46 | "onStartupFinished" 47 | ], 48 | "main": "./dist/extension.js", 49 | "contributes": { 50 | "languages": [ 51 | { 52 | "id": "yaml", 53 | "extensions": [ 54 | ".yaml", 55 | ".yml" 56 | ], 57 | "aliases": [ 58 | "YAML", 59 | "yaml" 60 | ], 61 | "configuration": "./language-configuration.json" 62 | } 63 | ], 64 | "viewsContainers": { 65 | "activitybar": [ 66 | { 67 | "id": "maestroWorkbench", 68 | "title": "Maestro Workbench", 69 | "icon": "assets/icon.svg" 70 | } 71 | ] 72 | }, 73 | "views": { 74 | "maestroWorkbench": [ 75 | { 76 | "id": "maestroBenchTreeView", 77 | "name": "Maestro Workbench", 78 | "icon": "assets/icon.svg" 79 | } 80 | ] 81 | }, 82 | "commands": [ 83 | { 84 | "command": "maestroWorkbench.openMaestroStudio", 85 | "title": "Open Maestro Studio", 86 | "icon": "$(debug-console)" 87 | }, 88 | { 89 | "command": "maestroWorkbench.refreshTree", 90 | "title": "Refresh Maestro Tree", 91 | "icon": "$(refresh)" 92 | }, 93 | { 94 | "command": "maestroWorkbench.runTest", 95 | "title": "Run Test for Flow", 96 | "icon": "$(debug-start)" 97 | }, 98 | { 99 | "command": "maestroWorkbench.runFolderTests", 100 | "title": "Run All Flows in Folder", 101 | "icon": "$(debug-start)" 102 | }, 103 | { 104 | "command": "maestroWorkbench.listDevices", 105 | "title": "List Available Devices", 106 | "icon": "$(device-desktop)" 107 | } 108 | ], 109 | "menus": { 110 | "view/title": [ 111 | { 112 | "command": "maestroWorkbench.openMaestroStudio", 113 | "when": "view == maestroBenchTreeView", 114 | "group": "navigation" 115 | }, 116 | { 117 | "command": "maestroWorkbench.refreshTree", 118 | "when": "view == maestroBenchTreeView", 119 | "group": "navigation" 120 | }, 121 | { 122 | "command": "maestroWorkbench.listDevices", 123 | "when": "view == maestroBenchTreeView", 124 | "group": "navigation" 125 | } 126 | ] 127 | }, 128 | "snippets": [ 129 | { 130 | "language": "yaml", 131 | "path": "./snippets/snippets.v0.json" 132 | } 133 | ], 134 | "configuration": { 135 | "type": "object", 136 | "title": "Maestro Workbench Settings", 137 | "properties": { 138 | "maestroWorkbench.filePatterns": { 139 | "type": "array", 140 | "items": { 141 | "type": "string" 142 | }, 143 | "default": [ 144 | "maestro/**/*.yaml", 145 | "maestro/**/*.yml", 146 | "**/.maestro/**/*.yaml", 147 | "**/.maestro/**/*.yml" 148 | ], 149 | "description": "Glob patterns to locate Maestro files. You can customize these to include or exclude specific directories." 150 | }, 151 | "maestroWorkbench.defaultDevice": { 152 | "type": "string", 153 | "default": "", 154 | "description": "Default device to run tests on. Leave empty to use the first available device." 155 | } 156 | } 157 | } 158 | }, 159 | "scripts": { 160 | "vscode:prepublish": "npm run package", 161 | "compile": "npm run check-types && npm run lint && node esbuild.js", 162 | "watch": "npm-run-all -p watch:*", 163 | "watch:esbuild": "node esbuild.js --watch", 164 | "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", 165 | "package": "npm run check-types && npm run lint && node esbuild.js --production", 166 | "compile-tests": "tsc -p . --outDir out", 167 | "watch-tests": "tsc -p . -w --outDir out", 168 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 169 | "check-types": "tsc --noEmit", 170 | "lint": "eslint src", 171 | "test:unit": "mocha --import=tsx tests/**/*.test.ts --exit", 172 | "test:integration": "vscode-test", 173 | "test": "npm run test:unit && npm run test:integration" 174 | }, 175 | "devDependencies": { 176 | "@types/chai": "^5.0.1", 177 | "@types/mocha": "^10.0.10", 178 | "@types/node": "20.x", 179 | "@types/vscode": "^1.61.0", 180 | "@typescript-eslint/eslint-plugin": "^8.17.0", 181 | "@typescript-eslint/parser": "^8.17.0", 182 | "@vscode/test-cli": "^0.0.10", 183 | "@vscode/test-electron": "^2.4.1", 184 | "ajv": "^8.17.1", 185 | "chai": "^5.1.2", 186 | "esbuild": "^0.25.1", 187 | "eslint": "^9.16.0", 188 | "mocha": "^11.0.1", 189 | "npm-run-all": "^4.1.5", 190 | "tsx": "^4.19.2", 191 | "typescript": "^5.7.2" 192 | }, 193 | "dependencies": { 194 | "minimatch": "^10.0.1", 195 | "yaml": "^2.7.0" 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Maestro Workbench 2 | 3 | Thank you for considering contributing to Maestro Workbench! We welcome contributions that enhance the functionality and usability of this extension. This guide will help you get started with adding new commands or extending existing ones. 4 | 5 | ## Table of Contents 6 | 7 | - [Getting Started](#getting-started) 8 | - [Adding Support for a Maestro Command](#adding-support-for-a-maestro-command) 9 | - [Adding a New Command](#adding-a-new-command) 10 | - [Extending an Existing Command](#extending-an-existing-command) 11 | - [Testing Your Changes](#testing-your-changes) 12 | - [Submitting Your Contribution](#submitting-your-contribution) 13 | - [Resources](#resources) 14 | 15 | ## Getting Started 16 | 17 | 1. **Fork the Repository**: Begin by forking the [Maestro Workbench repository](https://github.com/Mastersam07/maestro-workbench) to your GitHub account. 18 | 19 | 2. **Clone the Repository**: Clone your forked repository to your local machine: 20 | 21 | ```bash 22 | git clone https://github.com/your-username/maestro-workbench.git 23 | ``` 24 | 3. **Install Dependencies**: Navigate to the project directory and install the necessary dependencies: 25 | 26 | ```bash 27 | cd maestro-workbench 28 | npm install 29 | ``` 30 | 4. **Open in VS Code**: Open the project in Visual Studio Code: 31 | 32 | ```bash 33 | code . 34 | ``` 35 | 36 | 37 | ## Adding Support for a Maestro Command 38 | 39 | > To contribute a new command or add missing properties to an existing command in the Maestro Workbench extension, you'll need to update the JSON schema that defines the structure and validation rules for Maestro YAML files 40 | 41 | 1. **Locate the JSON Schema File:**: Locate the json schema at ./schema/schema.v0.json 42 | 43 | 2. **Understand the Schema Structure**: Each command is represented as a property within the `properties` section of `items` in the schema. 44 | 45 | 3. **Add a New Command**: 46 | - To introduce a new command, add a new property to the `properties` section 47 | - Define the command's structure, including its `type`, `required fields`, and any additional validation rules. 48 | 49 | ```json 50 | { 51 | "properties": { 52 | "newCommand": { 53 | "type": "object", 54 | "properties": { 55 | "parameter1": { 56 | "type": "string", 57 | "description": "Description of parameter1" 58 | }, 59 | "parameter2": { 60 | "type": "integer", 61 | "description": "Description of parameter2" 62 | } 63 | }, 64 | "required": ["parameter1"], 65 | "additionalProperties": false, 66 | "description": "Description of the new command" 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | 4. **Modify an Existing Command**: 73 | - To add missing properties to an existing command, locate the command within the `properties` section. 74 | - Add the new properties under the `properties` subsection of the command, specifying their types and descriptions. 75 | 76 | ```json 77 | { 78 | "properties": { 79 | "existingCommand": { 80 | "type": "object", 81 | "properties": { 82 | "existingParameter": { 83 | "type": "string", 84 | "description": "Description of existingParameter" 85 | }, 86 | "newParameter": { 87 | "type": "boolean", 88 | "description": "Description of newParameter" 89 | } 90 | }, 91 | "required": ["existingParameter"], 92 | "additionalProperties": false, 93 | "description": "Description of the existing command with newParameter added" 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | 5. **Validate the Schema**: 100 | - After making changes, ensure the JSON schema is valid. 101 | - Use tools like [JSON Schema Validator](https://www.jsonschemavalidator.net/) to check for errors. 102 | 103 | 6. **Test the Changes**: 104 | - Implement the updated schema in the extension.. 105 | - Create sample Maestro YAML files that utilize the new or updated commands to verify that IntelliSense, validation, and other features work as expected. 106 | 107 | 108 | ## Adding a New Command To Maestro Workbench 109 | 110 | > To add a new command to Maestro Workbench: 111 | 112 | 1. **Define the Command in package.json**: Locate the contributes.commands section in package.json and add your new command: 113 | 114 | ```bash 115 | { 116 | "contributes": { 117 | "commands": [ 118 | { 119 | "command": "maestroWorkbench.newCommand", 120 | "title": "Maestro: New Command", 121 | "category": "Maestro Workbench" 122 | } 123 | ] 124 | } 125 | } 126 | ``` 127 | 128 | This definition makes your command available in the Command Palette under the "Maestro Workbench" category. 129 | 130 | 2. **Implement the Command in `extension.ts`**: Open `src/extension.ts` and register your command within the `activate` function: 131 | 132 | ```ts 133 | import * as vscode from 'vscode'; 134 | 135 | export function activate(context: vscode.ExtensionContext) { 136 | // Other activation code... 137 | 138 | const newCommand = vscode.commands.registerCommand('maestroWorkbench.newCommand', () => { 139 | // Command implementation logic 140 | vscode.window.showInformationMessage('New Command Executed!'); 141 | }); 142 | 143 | context.subscriptions.push(newCommand); 144 | } 145 | ``` 146 | 147 | This code registers and implements the functionality of your new command. 148 | 149 | 150 | ## Extending an Existing Command 151 | 152 | > To add properties or modify the behavior of an existing command: 153 | 154 | 1. **Locate the Command Registration**: Find where the command is registered in extension.ts or related files. 155 | 156 | 2. **Modify the Command Implementation**: Update the command's logic as needed. For example, to add parameters: 157 | 158 | ```ts 159 | const existingCommand = vscode.commands.registerCommand('maestroWorkbench.existingCommand', (args) => { 160 | // Updated command logic utilizing args 161 | vscode.window.showInformationMessage(`Command executed with args: ${args}`); 162 | }); 163 | ``` 164 | 165 | Ensure that any new parameters or properties are appropriately handled within the command's implementation. 166 | 167 | 3. **Update package.json if Necessary**: If you've changed how the command appears or is invoked, reflect these changes in the contributes.commands section of package.json. 168 | 169 | 170 | ## Testing Your Changes 171 | 172 | > To add properties or modify the behavior of an existing command: 173 | 174 | 1. **Run the Extension**: Press `F5` in VS Code to open a new window with your extension loaded. 175 | 176 | 2. **Execute the Command**: Open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS) and run your new or updated command to verify its functionality. 177 | 178 | 3. **Debugging: Use VS Code's debugging tools to set breakpoints and inspect variables as needed. 179 | 180 | 181 | ## Submitting Your Contribution 182 | 183 | 1. **Commit Your Changes**: Ensure your changes are well-documented and commit them with a descriptive message: 184 | 185 | ```bash 186 | git add . 187 | git commit -m "Add new command: Maestro: New Command" 188 | ``` 189 | 190 | 2. **Push to Your Fork**: Push your changes to your forked repository: 191 | 192 | ```bash 193 | git push origin your-branch-name 194 | ``` 195 | 196 | 3. **Create a Pull Request**: Navigate to the original repository and submit a pull request detailing your changes and their purpose. 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Maestro logo 4 | 5 | Maestro Workbench 6 |

7 | 8 | [![Version](https://img.shields.io/visual-studio-marketplace/v/Mastersam.maestro-workbench?style=for-the-badge&colorA=252525&colorB=0079CC)](https://marketplace.visualstudio.com/items?itemName=Mastersam.maestro-workbench) 9 | [![Downloads](https://img.shields.io/visual-studio-marketplace/d/Mastersam.maestro-workbench?style=for-the-badge&colorA=252525&colorB=0079CC)](https://marketplace.visualstudio.com/items?itemName=Mastersam.maestro-workbench) 10 | 11 |
12 | 13 | --- 14 | [![🚀 Maestro Workbench Release](https://github.com/Mastersam07/maestro-workbench/actions/workflows/release.yaml/badge.svg?branch=dev)](https://github.com/Mastersam07/maestro-workbench/actions/workflows/release.yaml) 15 | 16 | 17 | Maestro Workbench is a Visual Studio Code extension designed to enhance the development and testing of Maestro YAML files. It offers features such as IntelliSense, syntax highlighting, formatting, test execution, and output visualization to streamline your workflow. 18 | 19 | ## Features 20 | 21 | - **IntelliSense and Syntax Highlighting**: Provides code completions and highlights syntax for Maestro YAML files, reducing errors and improving readability. 22 | 23 | - **Schema Validation**: Ensure the correctness of your Maestro YAML files with integrated schema validation, highlighting errors and enforcing best practices. 24 | 25 | https://github.com/user-attachments/assets/35543dba-094e-43eb-bba4-443ba16205ad 26 | 27 | - **Test Execution with Feedback**: Run your Maestro tests directly from the test explorer, receive real-time feedback on their status—running, passed, or failed and view outputs. 28 | 29 | https://github.com/user-attachments/assets/27043757-1dd6-4227-a206-7e961fa8a3e7 30 | 31 | https://github.com/user-attachments/assets/0bd7246e-3e42-41af-83af-d14211f56803 32 | 33 | - **Snippets**: Utilize predefined code snippets to quickly scaffold Maestro commands and flows, enhancing productivity. 34 | 35 | https://github.com/user-attachments/assets/bacb850d-e74a-4008-9fed-30434d4254dd 36 | 37 | - **Customizable File Patterns**: Configure the extension to detect Maestro YAML files based on your project's structure by setting custom file patterns. 38 | 39 | https://github.com/user-attachments/assets/4c72c7e0-2eeb-4ab3-9bf7-8fec572b2b6f 40 | 41 | - **Environment Variable Support**: Define environment variables within your workspace settings and reference system environment variables using `{ENV:VAR_NAME}` syntax. Support for default values is also available, e.g., `{ENV:API_KEY:default123}`. 42 | 43 | - **Integrated Tree View**: Visualize and manage your Maestro test files within a dedicated tree view, providing quick access and organization as well as viewing flow dependencies. 44 | 45 | https://github.com/user-attachments/assets/b2b8d083-b1fa-4ae3-a5c3-e9a2f7f388d4 46 | 47 | - **Maestro Studio Integration**: Launch Maestro Studio directly from the extension. 48 | 49 | - **Test Explorer Integration** 50 | - View all Maestro test files in the test explorer 51 | - Run individual tests or all tests at once 52 | - Automatic device selection for test execution 53 | - Support for both Android and iOS devices 54 | - Environment variable management for tests 55 | 56 | - **Device Management** 57 | - List available Android and iOS devices 58 | - Select device for test execution 59 | - Set default device in settings 60 | - Support for physical devices, simulators, and offline devices 61 | 62 | - **File Management** 63 | - Tree view for Maestro test files 64 | - Automatic test discovery 65 | - File watching for changes 66 | - YAML schema validation 67 | 68 | ## Configuration 69 | 70 | ### Device Settings 71 | 72 | You can configure the default device for test execution in VS Code settings: 73 | 74 | ```json 75 | { 76 | "maestroWorkbench.defaultDevice": "device-id" 77 | } 78 | ``` 79 | 80 | Leave this empty to be prompted for device selection each time you run tests. 81 | 82 | ### Environment Variables 83 | 84 | Configure environment variables for your tests: 85 | 86 | ```json 87 | { 88 | "maestroWorkbench.envVariables": { 89 | "API_URL": "https://example.com", 90 | "TOKEN": "{ENV:TOKEN}", 91 | "AUTH_TOKEN": "{ENV:API_KEY:default_token}" 92 | } 93 | } 94 | ``` 95 | 96 | Environment variables can be: 97 | - Direct values: `"API_URL": "https://example.com"` 98 | - System environment variables: `"TOKEN": "{ENV:TOKEN}"` 99 | - System environment variables with defaults: `"AUTH_TOKEN": "{ENV:API_KEY:default_token}"` 100 | 101 | ## Usage 102 | 103 | ### Running Tests 104 | 105 | 1. Open the Test Explorer view in VS Code 106 | 2. Select one or more test files to run 107 | 3. Click the "Run Tests" button or use the test explorer's run command 108 | 4. If no default device is set, select a device from the list 109 | 5. Tests will run on the selected device 110 | 111 | ### Managing Devices 112 | 113 | 1. Use the "List Available Devices" command to view all connected devices 114 | 2. Devices are grouped by type (Android Device, iOS Device, iOS Simulator) 115 | 3. Each device entry shows: 116 | - Device name 117 | - Device ID 118 | - Device type and status 119 | 120 | ### Setting Default Device 121 | 122 | 1. Open VS Code settings 123 | 2. Search for "maestroWorkbench.defaultDevice" 124 | 3. Enter the device ID of your preferred device 125 | 4. The device will be used automatically when running tests 126 | 127 | ## Commands 128 | 129 | - `maestroWorkbench.listDevices`: List all available devices 130 | - `maestroWorkbench.runTest`: Run a specific test file 131 | - `maestroWorkbench.runFolderTests`: Run all tests in a folder 132 | - `maestroWorkbench.refreshTree`: Refresh the test explorer 133 | - `maestroWorkbench.openMaestroStudio`: Open Maestro Studio 134 | 135 | ## Requirements 136 | 137 | Ensure that you have Maestro installed on your system to utilize the testing features of this extension. You can download and install Maestro from the [official repository](https://maestro.mobile.dev/getting-started/installing-maestro). 138 | 139 | ## Extension Settings 140 | 141 | Maestro Workbench allows customization of file patterns to detect Maestro YAML files in your workspace. You can configure this in your workspace or user settings. 142 | 143 | **Default File Patterns:** 144 | 145 | ```json 146 | "maestroWorkbench.filePatterns": [ 147 | "maestro/**/*.yaml", 148 | "maestro/**/*.yml", 149 | "**/.maestro/**/*.yaml", 150 | "**/.maestro/**/*.yml" 151 | ] 152 | ``` 153 | 154 | To modify these patterns, navigate to your VS Code settings and update the maestroWorkbench.filePatterns configuration. 155 | 156 | * `maestroWorkbench.filePatterns`: Glob patterns to locate Maestro files 157 | * `maestroWorkbench.defaultDevice`: Default device to run tests on 158 | * `maestroWorkbench.envVariables`: Environment variables for test execution 159 | 160 | ## Known Issues 161 | 162 | - Device selection is required for each test run if no default device is set 163 | - Some iOS simulator features may require Xcode to be running 164 | 165 | ## Contributing 166 | 167 | Contributions are welcome! Please fork the repository and submit pull requests for any enhancements or bug fixes. For detailed guidelines, refer to our [CONTRIBUTING.md](https://github.com/Mastersam07/maestro-workbench/blob/dev/CONTRIBUTING.md). 168 | 169 | ## Star Our Repository 170 | 171 | If you find [Maestro Workbench](https://github.com/Mastersam07/maestro-workbench) useful, please consider starring our repository on GitHub! Your support helps us continue to improve the extension. 172 | 173 | [![GitHub stars](https://img.shields.io/github/stars/Mastersam07/maestro-workbench?style=social)](https://github.com/Mastersam07/maestro-workbench/stargazers) 174 | 175 | ## License 176 | 177 | This extension is licensed under the [MIT License](https://github.com/Mastersam07/maestro-workbench/blob/dev/LICENSE). 178 | 179 | --- 180 | 181 | For detailed documentation and contribution guidelines, please visit the [GitHub repository](https://github.com/Mastersam07/maestro-workbench). 182 | -------------------------------------------------------------------------------- /tests/examples/extreme.yaml: -------------------------------------------------------------------------------- 1 | appId: com.example 2 | name: My Test 3 | tags: 4 | - tag-build 5 | - pull-request 6 | env: 7 | USERNAME: user@example.com 8 | PASSWORD: 123 9 | jsEngine: graaljs 10 | onFlowStart: 11 | - runFlow: setup.yaml 12 | - runScript: setup.js 13 | onFlowComplete: 14 | - runFlow: teardown.yaml 15 | - runScript: teardown.js 16 | --- 17 | - action: 'back' 18 | - action: 'hideKeyboard' 19 | - action: 'scroll' 20 | - action: 'clearKeychain' 21 | - action: 'pasteText' 22 | 23 | - addMedia: 24 | - "./assets/foo.png" 25 | - "./assets/foo.mp4" 26 | - addMedia: 27 | files: 28 | - "./assets/foo.png" 29 | - "./assets/foo.mp4" 30 | optional: true 31 | 32 | - assertNoDefectsWithAI 33 | - assertNoDefectsWithAI: 34 | label: 'Check for defects with AI' 35 | optional: false 36 | 37 | - assertNotVisible: 'kwyjibo' 38 | - assertNotVisible: 39 | text: 'kwyjibo' 40 | - assertNotVisible: 41 | text: 'Form Test' 42 | enabled: false 43 | below: 'Some Text' 44 | above: 45 | text: 'Other Text' 46 | 47 | - assertTrue: ${"test" == "test"} 48 | - assertTrue: 49 | condition: ${12 < 20} 50 | label: 'Check if 12 is less than 20' 51 | 52 | - assertVisible: 'Form Test' 53 | - assertVisible: 54 | text: 'Form Test' 55 | - assertVisible: 56 | id: 'fabAddIcon' 57 | - assertVisible: 'Login' 58 | - assertVisible: 59 | text: 'Form Test' 60 | below: 'Some Text' 61 | 62 | - assertWithAI: 'Login and password text fields are visible.' 63 | - assertWithAI: 64 | assertion: 'Login and password text fields are visible.' 65 | label: 'Check for login fields with AI' 66 | optional: false 67 | 68 | - copyTextFrom: '.*Total.*' 69 | - copyTextFrom: 70 | text: '\d+' 71 | 72 | - back 73 | - back: 74 | label: 'Go back' 75 | 76 | - clearKeychain 77 | - clearKeychain: 78 | optional: false 79 | 80 | - clearState 81 | - clearState: com.example.someapp 82 | - clearState: 83 | appId: com.example.someapp 84 | label: 'Wipe the data' 85 | optional: true 86 | 87 | - copyTextFrom: '.*Total.*' 88 | - copyTextFrom: 89 | text: '\d+' 90 | label: 'Copy the total' 91 | optional: false 92 | 93 | - eraseText 94 | - eraseText: 3 95 | - eraseText: 96 | charactersToErase: 10 97 | label: 'Delete ten chars' 98 | 99 | - evalScript: ${output.test = 'foo'} 100 | - evalScript: 101 | script: ${output.test = 'foo'} 102 | label: 'Evalscript test' 103 | 104 | - extendedWaitUntil: 105 | timeout: 10000 106 | visible: 107 | text: 'Swipe Test' 108 | 109 | - extendedWaitUntil: 110 | timeout: 100 111 | notVisible: 112 | text: 'Non Existent Text' 113 | 114 | - extractTextWithAI: 'CAPTCHA Value' 115 | - extractTextWithAI: 116 | query: 'CAPTCHA Value' 117 | outputVariable: 'theCaptchaValue' 118 | label: 'Extract the CAPTCHA value' 119 | optional: false 120 | 121 | - hideKeyboard 122 | - hideKeyboard: 123 | label: 'Hide the keyboard' 124 | optional: false 125 | 126 | - inputRandomEmail 127 | - inputRandomEmail: 128 | label: 'Enter an email address' 129 | optional: true 130 | - inputRandomNumber 131 | - inputRandomNumber: 132 | length: 4 133 | label: 'Enter a number' 134 | optional: false 135 | - inputRandomPersonName 136 | - inputRandomPersonName: 137 | label: 'Enter a name' 138 | optional: false 139 | - inputRandomText 140 | - inputRandomText: 141 | length: 4 142 | label: 'Enter some text' 143 | optional: false 144 | 145 | - inputText: 'foo' 146 | - inputText: ${MYVAR} 147 | - inputText: 148 | text: 'user123' 149 | label: 'Enter username' 150 | 151 | - killApp 152 | - killApp: com.example 153 | - killApp: 154 | appId: com.example 155 | label: 'Kill the Example app' 156 | optional: false 157 | 158 | - launchApp: com.example 159 | 160 | - launchApp: 161 | appId: com.example.example 162 | clearState: true 163 | clearKeychain: true 164 | stopApp: false 165 | 166 | - launchApp: 167 | label: 'Launch the app with custom permissions' 168 | permissions: 169 | all: deny 170 | camera: allow 171 | location: allow 172 | 173 | - launchApp 174 | 175 | - openLink: https://example.com 176 | - openLink: 'example://login' 177 | # https://github.com/Mastersam07/maestro-workbench/issues/37 178 | #- openLink: 179 | # link: 'example://login' 180 | - openLink: 181 | link: https://example.com 182 | autoVerify: true 183 | label: 'Open link in app' 184 | - openLink: 185 | link: https://example.com 186 | browser: true 187 | label: 'Open link in browser' 188 | optional: false 189 | 190 | - pasteText 191 | - pasteText: 192 | label: 'Paste the text' 193 | optional: true 194 | 195 | - pressKey: 'HOME' 196 | - pressKey: 'BACK' 197 | - pressKey: 'Home' 198 | - pressKey: 'Back' 199 | - pressKey: 'home' 200 | - pressKey: 'back' 201 | - pressKey: 202 | key: 'home' 203 | label: 'Press the home key' 204 | optional: false 205 | 206 | - repeat: 207 | times: 3 208 | commands: 209 | - tapOn: 210 | id: 'fabAddIcon' 211 | 212 | - repeat: 213 | while: 214 | true: ${output.counter < 3} 215 | commands: 216 | - tapOn: 217 | id: 'fabAddIcon' 218 | 219 | - repeat: 220 | while: 221 | visible: 222 | text: 'Some Text' 223 | containsChild: 224 | id: 'someChild' 225 | containsDescendants: 226 | - id: 'someDescendant1' 227 | - text: 'someDescendant2' 228 | below: 229 | id: 'someDescendant1' 230 | - id: 'someDescendant3' 231 | childOf: 232 | id: 'someParent' 233 | commands: 234 | - tapOn: 235 | id: 'fabAddIcon' 236 | 237 | - retry: 238 | maxRetries: 3 239 | commands: 240 | - tapOn: 241 | id: 'fabAddIcon' 242 | retryTapIfNoChange: false 243 | - waitForAnimationToEnd 244 | - retry: 245 | commands: 246 | - tapOn: 'My Button' 247 | - retry: 248 | maxRetries: 0 249 | file: runOnce.yaml 250 | - retry: 251 | file: folder/some-flow.yaml 252 | - retry: 253 | maxRetries: 3 254 | file: folder/some-flow.yaml 255 | env: 256 | MYVAR: "foo" 257 | label: 'Run the flow. Retry on failure' 258 | optional: false 259 | 260 | - runFlow: setup.yaml 261 | - runFlow: 262 | commands: 263 | - assertTrue: ${output.test == 'bar'} 264 | - tapOn: 265 | id: 'fabAddIcon' 266 | - runFlow: 267 | env: 268 | THIS_THING: "six" 269 | commands: 270 | - assertTrue: ${THIS_THING == "six"} 271 | - runFlow: 272 | when: 273 | visible: 'Some Text' 274 | file: folder/some-flow.yaml 275 | - runFlow: 276 | when: 277 | visible: 'Some Text' 278 | notVisible: 279 | id: 'fabAddIcon' 280 | platform: iOS 281 | true: ${output.test == 'bar'} 282 | file: folder/some-flow.yaml 283 | - runFlow: 284 | when: 285 | platform: iOS 286 | commands: 287 | - tapOn: 'Thing1' 288 | - runFlow: 289 | label: 'Nested flow' 290 | commands: 291 | - tapOn: 'Thing2' 292 | 293 | - runScript: setup.js 294 | - runScript: 295 | env: 296 | THIS_THING: "six" 297 | file: runScript.js 298 | - runScript: 299 | when: 300 | visible: 'Some Text' 301 | notVisible: 302 | id: 'fabAddIcon' 303 | platform: iOS 304 | true: ${output.test == 'bar'} 305 | file: myScript.js 306 | 307 | - scroll 308 | - scroll: 309 | label: 'Scroll down' 310 | optional: false 311 | 312 | - scrollUntilVisible: 313 | element: 314 | id: "viewId" 315 | direction: DOWN 316 | timeout: 50000 317 | speed: 40 318 | visibilityPercentage: 100 319 | centerElement: false 320 | 321 | - setAirplaneMode: enabled 322 | - setAirplaneMode: disabled 323 | - setAirplaneMode: 324 | value: 'enabled' 325 | label: 'Enable airplane mode' 326 | optional: false 327 | 328 | - toggleAirplaneMode 329 | - toggleAirplaneMode: 330 | label: 'Toggle airplane mode' 331 | optional: false 332 | 333 | - setLocation: 334 | latitude: 52.3599976 335 | longitude: 4.8830301 336 | - setLocation: 337 | latitude: 52.3599976 338 | longitude: 4.8830301 339 | label: 'Set the location' 340 | optional: false 341 | 342 | - startRecording: recording 343 | - startRecording: 344 | path: recording 345 | label: 'Record test evidence' 346 | 347 | - stopApp 348 | - stopApp: com.android.chrome 349 | - stopApp: 350 | appId: com.android.chrome 351 | label: 'Stop Chrome' 352 | optional: true 353 | 354 | - stopRecording 355 | - stopRecording: 356 | label: 'Stop recording' 357 | optional: false 358 | 359 | - swipe: 360 | start: 90%, 50% 361 | end: 10%, 50% 362 | - swipe: 363 | start: 100, 200 364 | end: 300, 400 365 | - swipe: 366 | direction: LEFT 367 | label: 'Swipe left' 368 | - swipe: 369 | from: 370 | id: "feeditem_identifier" 371 | index: 0 372 | direction: UP 373 | - swipe: 374 | direction: LEFT 375 | duration: 2000 376 | 377 | - takeScreenshot: MainScreen 378 | - takeScreenshot: 379 | path: LoginScreen 380 | label: 'Take a screenshot of the login screen' 381 | optional: false 382 | 383 | - tapOn: "My text" 384 | - tapOn: .*Some Regex.* 385 | - tapOn: 386 | id: "viewId" 387 | - tapOn: 388 | text: "Button" 389 | repeat: 3 390 | delay: 500 391 | - tapOn: 392 | id: "someId" 393 | retryTapIfNoChange: false 394 | - tapOn: 395 | text: "Button" 396 | waitToSettleTimeoutMs: 500 397 | - tapOn: 398 | point: 50%,50% 399 | - tapOn: 400 | text: "A text with a hyperlink" 401 | point: "90%,50%" 402 | - tapOn: 403 | id: "someId" 404 | repeat: 3 405 | delay: 100 406 | traits: 'square' 407 | waitUntilVisible: true 408 | retryTapIfNoChange: false 409 | waitToSettleTimeoutMs: 500 410 | label: 'Tap a square thing thrice when it appears' 411 | - tapOn: 412 | text: 'Add to Basket' 413 | index: ${output.index} 414 | 415 | - longPressOn: Text 416 | - longPressOn: 417 | id: view_id 418 | - longPressOn: 419 | point: 50%,50% 420 | - longPressOn: 421 | text: "A text with a hyperlink" 422 | point: "90%,50%" 423 | 424 | - doubleTapOn: "Button" 425 | - doubleTapOn: 426 | id: "someId" 427 | delay: 100 428 | 429 | - travel: 430 | points: 431 | - 0.0,0.0 432 | - 0.1,0.0 433 | - 0.1,0.1 434 | - 0.0,0.1 435 | speed: 7900 436 | label: 'Fake some movement' 437 | optional: false 438 | 439 | - waitForAnimationToEnd 440 | - waitForAnimationToEnd: 441 | timeout: 10000 442 | label: 'Wait for the animation to end' 443 | optional: false 444 | -------------------------------------------------------------------------------- /src/provider/treeView.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import * as vscode from 'vscode'; 4 | import YAML from 'yaml'; 5 | import { minimatch } from 'minimatch'; 6 | 7 | export class MaestroWorkBenchTreeViewProvider implements vscode.TreeDataProvider { 8 | private filePatterns: string[]; 9 | private dependencyMap: Map = new Map(); 10 | 11 | private _onDidChangeTreeData: vscode.EventEmitter = 12 | new vscode.EventEmitter(); 13 | readonly onDidChangeTreeData: vscode.Event = 14 | this._onDidChangeTreeData.event; 15 | 16 | constructor(filePatterns: string[]) { 17 | this.filePatterns = filePatterns; 18 | } 19 | 20 | 21 | getTreeItem(element: FileItem): vscode.TreeItem { 22 | return element; 23 | } 24 | 25 | async getChildren(element?: FileItem): Promise { 26 | if (!vscode.workspace.workspaceFolders) { 27 | vscode.window.showErrorMessage('No workspace folder open.'); 28 | return []; 29 | } 30 | 31 | const workspaceFolder = vscode.workspace.workspaceFolders[0].uri.fsPath; 32 | 33 | if (element) { 34 | if (element.contextValue === FileType.File) { 35 | const dependencies = this.dependencyMap.get(element.resourceUri.fsPath) || []; 36 | const uniqueDependencies = new Set(dependencies); 37 | return Array.from(uniqueDependencies).map((dep) => { 38 | const isMissing = !fs.existsSync(dep); 39 | return new FileItem( 40 | path.basename(dep), 41 | vscode.TreeItemCollapsibleState.None, 42 | vscode.Uri.file(dep), 43 | FileType.Dependency, 44 | isMissing ? 'Missing dependency' : 'Dependency', 45 | isMissing ? 'error' : undefined 46 | ); 47 | }); 48 | } 49 | 50 | return this.getFilesInFolder(element.resourceUri.fsPath); 51 | } else { 52 | const allFiles = await this.findFiles(); 53 | this.analyzeDependencies(allFiles); 54 | const treeItems = this.createTreeItemsFromPaths(allFiles, workspaceFolder); 55 | 56 | treeItems.forEach(item => { 57 | if (item.contextValue === FileType.File) { 58 | const dependencies = this.dependencyMap.get(item.resourceUri.fsPath) || []; 59 | item.collapsibleState = 60 | dependencies.length > 0 61 | ? vscode.TreeItemCollapsibleState.Collapsed 62 | : vscode.TreeItemCollapsibleState.None; 63 | } 64 | }); 65 | 66 | return treeItems; 67 | } 68 | } 69 | 70 | public refresh(): void { 71 | this._onDidChangeTreeData.fire(); 72 | } 73 | 74 | private async findFiles(): Promise { 75 | const patterns = `{${this.filePatterns.join(',')}}`; 76 | const files = await vscode.workspace.findFiles(patterns); 77 | return files.map((file) => file.fsPath); 78 | } 79 | 80 | private async getFilesInFolder(folderPath: string): Promise { 81 | const entries = await fs.promises.readdir(folderPath, { withFileTypes: true }); 82 | 83 | const fileItems = entries.map((entry) => { 84 | const fullPath = path.join(folderPath, entry.name); 85 | 86 | const isFolder = entry.isDirectory(); 87 | return new FileItem( 88 | entry.name, 89 | isFolder ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None, 90 | vscode.Uri.file(fullPath), 91 | isFolder ? FileType.Folder : FileType.File 92 | ); 93 | }); 94 | 95 | const filteredItems = fileItems.filter(item => { 96 | if (item.contextValue === FileType.Folder) { 97 | return true; 98 | } 99 | 100 | const workspaceRoot = vscode.workspace.workspaceFolders![0].uri.fsPath; 101 | const relativePath = path.relative(workspaceRoot, item.resourceUri.fsPath); 102 | 103 | return this.shouldIncludeFile(relativePath); 104 | }); 105 | 106 | const nonEmptyFolders = filteredItems 107 | .filter(item => item.contextValue === FileType.Folder) 108 | .filter(folder => { 109 | const folderPath = folder.resourceUri.fsPath; 110 | return filteredItems.some(item => { 111 | if (item.contextValue === FileType.File) { 112 | return item.resourceUri.fsPath.startsWith(folderPath + path.sep); 113 | } 114 | return false; 115 | }); 116 | }); 117 | 118 | const finalItems = [ 119 | ...filteredItems.filter(item => item.contextValue !== FileType.Folder), 120 | ...nonEmptyFolders 121 | ]; 122 | 123 | const filePaths = finalItems 124 | .filter((item) => item.contextValue === FileType.File) 125 | .map((item) => item.resourceUri.fsPath); 126 | 127 | this.analyzeDependencies(filePaths); 128 | 129 | finalItems.forEach((item) => { 130 | if (item.contextValue === FileType.File) { 131 | const dependencies = this.dependencyMap.get(item.resourceUri.fsPath) || []; 132 | item.collapsibleState = 133 | dependencies.length > 0 134 | ? vscode.TreeItemCollapsibleState.Collapsed 135 | : vscode.TreeItemCollapsibleState.None; 136 | } 137 | }); 138 | 139 | return finalItems; 140 | } 141 | 142 | private shouldIncludeFile(relativePath: string): boolean { 143 | const normalizedPath = relativePath.replace(/\\/g, '/'); 144 | return this.filePatterns.some(pattern => 145 | minimatch(normalizedPath, pattern, { dot: true }) 146 | ); 147 | } 148 | 149 | private createTreeItemsFromPaths(filePaths: string[], rootPath: string): FileItem[] { 150 | const tree: { [key: string]: any } = {}; 151 | 152 | filePaths.forEach((filePath) => { 153 | const relativePath = path.relative(rootPath, filePath); 154 | const parts = relativePath.split(path.sep); 155 | 156 | let currentLevel = tree; 157 | parts.forEach((part, index) => { 158 | if (!currentLevel[part]) { 159 | currentLevel[part] = index === parts.length - 1 ? filePath : {}; 160 | } 161 | currentLevel = currentLevel[part]; 162 | }); 163 | }); 164 | 165 | return this.buildTreeItems(tree, rootPath); 166 | } 167 | 168 | private buildTreeItems(tree: any, parentPath: string): FileItem[] { 169 | return Object.entries(tree).map(([key, value]) => { 170 | const fullPath = path.join(parentPath, key); 171 | const isFolder = typeof value === 'object'; 172 | 173 | return new FileItem( 174 | key, 175 | isFolder ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None, 176 | vscode.Uri.file(fullPath), 177 | isFolder ? FileType.Folder : FileType.File 178 | ); 179 | }); 180 | } 181 | 182 | private analyzeDependencies(filePaths: string[]): void { 183 | this.dependencyMap.clear(); 184 | 185 | filePaths.forEach((filePath) => { 186 | try { 187 | const fileContent = fs.readFileSync(filePath, 'utf-8'); 188 | const parsedDocuments = YAML.parseAllDocuments(fileContent) as any[]; 189 | 190 | const dependencies = new Set(); 191 | 192 | parsedDocuments.forEach((doc) => { 193 | const parsed = doc.toJS(); 194 | const flows = Array.isArray(parsed) ? parsed : [parsed]; 195 | 196 | flows.forEach((flow) => { 197 | if (!flow) return; 198 | 199 | if (flow.runFlow && flow.runFlow.file) { 200 | const dependencyPath = path.resolve(path.dirname(filePath), flow.runFlow.file); 201 | dependencies.add(dependencyPath); 202 | } 203 | 204 | if (flow.runScript && flow.runScript.file) { 205 | const dependencyPath = path.resolve(path.dirname(filePath), flow.runScript.file); 206 | dependencies.add(dependencyPath); 207 | } 208 | 209 | if (flow.addMedia) { 210 | if (Array.isArray(flow.addMedia)) { 211 | flow.addMedia.forEach((mediaFile: string) => { 212 | const dependencyPath = path.resolve(path.dirname(filePath), mediaFile); 213 | dependencies.add(dependencyPath); 214 | }); 215 | } else if (flow.addMedia.files && Array.isArray(flow.addMedia.files)) { 216 | flow.addMedia.files.forEach((mediaFile: string) => { 217 | const dependencyPath = path.resolve(path.dirname(filePath), mediaFile); 218 | dependencies.add(dependencyPath); 219 | }); 220 | } 221 | } 222 | }); 223 | }); 224 | 225 | this.dependencyMap.set(filePath, Array.from(dependencies)); 226 | } catch (error) { 227 | console.error(`Failed to parse YAML file ${filePath}:`, error); 228 | } 229 | }); 230 | } 231 | } 232 | 233 | enum FileType { 234 | File = 'file', 235 | Folder = 'folder', 236 | Dependency = 'dependency', 237 | } 238 | 239 | class FileItem extends vscode.TreeItem { 240 | 241 | constructor( 242 | public readonly label: string, 243 | public collapsibleState: vscode.TreeItemCollapsibleState, 244 | public readonly resourceUri: vscode.Uri, 245 | public readonly contextValue: FileType, 246 | public readonly tooltip?: string, 247 | private readonly icon?: string 248 | ) { 249 | super(label, collapsibleState); 250 | this.resourceUri = resourceUri; 251 | this.contextValue = contextValue; 252 | 253 | this.setIcon(); 254 | 255 | if (contextValue === FileType.File || contextValue === FileType.Dependency) { 256 | this.command = { 257 | title: 'Open File', 258 | command: 'vscode.open', 259 | arguments: [resourceUri], 260 | }; 261 | } 262 | } 263 | 264 | private setIcon() { 265 | if (this.contextValue === FileType.Dependency) { 266 | if (this.icon === 'error') { 267 | this.iconPath = new vscode.ThemeIcon('error', new vscode.ThemeColor('testing.iconErrored')); 268 | } else if (this.icon === 'link') { 269 | this.iconPath = new vscode.ThemeIcon('link', new vscode.ThemeColor('testing.iconPassed')); 270 | } 271 | } else if (this.contextValue === FileType.File) { 272 | this.iconPath = vscode.ThemeIcon.File; 273 | } else if (this.contextValue === FileType.Folder) { 274 | this.iconPath = vscode.ThemeIcon.Folder; 275 | } 276 | } 277 | } -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | --------------------------------------------------------------------------------