| Tool | Purpose |
|---|---|
mocha + chai |
Unit test runner and assertions |
sinon |
Stubs and mocks |
@vscode/test-electron |
Extension-host tests with real VS Code APIs |
vscode-extension-tester |
NEW: UI automation with Selenium WebDriver |
jsdom |
Webview DOM simulation |
nyc |
Coverage reporting (target: 80% for types.ts, 60% overall) |
test/
├── unit/ # Pure logic, no VS Code host (fast)
├── extension/ # Extension-host tests via @vscode/test-electron
├── ui/ # **NEW**: UI tests with vscode-extension-tester
│ └── suite/ # UI automation tests (InputBox, QuickPick, TreeView)
├── webview/ # DOM tests with jsdom
├── mocks/ # Shared mock factories
└── fixtures/ # Sample workspaces and .weaudit files
NEW: We use vscode-extension-tester (ExTester) for full UI automation. This allows us to:
- Type into InputBox and QuickPick elements programmatically
- Interact with tree views (click, expand, verify content)
- Test the actual user experience, not just the API
- InputBox Interaction: Can type custom titles for findings/notes
- TreeView Testing: Can click items, verify labels, test drag-and-drop
- Command Palette: Can invoke commands via UI
- Full User Experience: Tests what users actually see and interact with
# Run all tests (unit + integration + UI)
npm run test:all
# Run only UI tests
npm run test:ui
# Setup UI test environment (download VS Code + ChromeDriver)
npm run test:ui-setupBoth npm run test:ext and npm run test:ui run VS Code with isolated user data and extensions directories under .test-extensions/ so local/user-installed extensions do not affect test results.
import { InputBox, Workbench } from 'vscode-extension-tester';
it("should create finding with custom title", async () => {
const workbench = new Workbench();
// Execute command
await workbench.executeCommand("weAudit: New Finding from Selection");
// Type into input box (impossible with @vscode/test-electron!)
const input = await InputBox.create();
await input.setText("SQL Injection Vulnerability");
await input.confirm();
// Verify in tree view
// ... assertions
});We use both testing frameworks:
@vscode/test-electron- Fast integration tests for API-level operationsvscode-extension-tester- UI tests for InputBox, QuickPick, TreeView interactions
This gives us the best of both worlds: speed + comprehensive coverage.
UI tests live in test/ui/suite/ and are run via npm run test:ui.
Added UI tests
Implemented in test/ui/suite/commands.ui.test.ts:
- Create finding with a custom title (
weAudit: New Finding from Selection) - Cancel finding creation (Escape in QuickInput)
- Create note with a custom title (
weAudit: New Note from Selection) - Edit finding under cursor (
weAudit: Edit Finding Under Cursor) - Add an additional region via QuickPick (
weAudit: Add Region to a Finding) - Mark region as reviewed and toggle off (
weAudit: Mark Region as Reviewed) - Mark file as reviewed and toggle off (
weAudit: Mark File as Reviewed) - Navigate to next partially audited region (
weAudit: Navigate to Next Partially Audited Region) - Delete finding under cursor — single location (
weAudit: Delete Location Under Cursor) - Delete one location from multi-region finding (
weAudit: Delete Location Under Cursor) - Toggle tree view mode and verify tree items (
weAudit: Toggle View Mode)
Added extension-host tests (non-UI)
Implemented under test/extension/suite/:
- Command registration and activation (
test/extension/suite/activation.test.ts) - Basic command execution and persistence (
test/extension/suite/commands.test.ts)weAudit.addFinding/weAudit.addNote(accepts empty title due to QuickInput limitations)weAudit.showMarkedFilesDayLog
- Tree view integration (
test/extension/suite/treeViews.test.ts)weAudit.toggleTreeViewModeweAudit.findAndLoadConfigurationFiles
- Decoration integration (
test/extension/suite/decorations.test.ts)weAudit.toggleAuditedweAudit.addPartiallyAudited
Not yet added (UI candidates)
From package.json and src/codeMarker.ts, the remaining UI-heavy commands worth adding coverage for:
- QuickPick / QuickInput flows:
weAudit: Edit Repository URL (Client)(weAudit.editClientRemote) – input boxweAudit: Edit Repository URL (Audit)(weAudit.editAuditRemote) – input boxweAudit: Edit Git Commit Hash(weAudit.editGitHash) – input box
- Tree view / command palette UX:
weAudit: Search and Filter Findings(weAudit.showFindingsSearchBar) – triggerslist.findon the tree view
- Context menu & multi-step flows (require tree view interaction, not command palette):
Resolve Finding/Restore Finding(weAudit.resolveFinding,weAudit.restoreFinding)Delete Finding(weAudit.deleteFinding,weAudit.deleteLocation)
- Export findings to markdown (
weAudit: Export Findings as Markdown) - Add a labeled region via QuickPick + InputBox (
weAudit: Add Region to a Finding with Label)
Not yet added (other command candidates)
These are typically better suited to extension-host tests (stubbing VS Code APIs) or webview tests:
- Clipboard/external actions:
weAudit.openGithubIssue,weAudit.openGithubIssueFromDetails(stubvscode.env.openExternal)weAudit.copyEntryPermalink,weAudit.copyEntryPermalinks,weAudit.copySelectedCodePermalink,weAudit.copySelectedCodeClientPermalink(stubvscode.env.clipboard)
- Navigation and toggles:
weAudit.navigateToNextPartiallyAuditedRegion(assert cursor position changes)weAudit.toggleFindingsHighlighting(assert decorations/visibility changes)
- Resolved findings lifecycle:
weAudit.resolveFinding,weAudit.restoreFinding,weAudit.deleteResolvedFinding,weAudit.deleteAllResolvedFinding,weAudit.restoreAllResolvedFindings
- Webview wiring (from
src/codeMarker.tscommand registrations not exposed as palette commands):weAudit.updateCurrentSelectedEntry,weAudit.updateGitConfig,weAudit.showSelectedEntryInFindingDetails
How we use ExTester successfully
Key patterns that made the UI tests stable:
- Prefer
VSBrowser.instance.openResources(...)overvscode.opento open the fixture workspace and files without extra QuickInput interactions. - Avoid polling tree view contents during waits (it opens the weAudit view repeatedly and causes unnecessary UI churn). Instead, assert via the persisted
.vscode/<username>.weauditfile when possible. - QuickPick tip:
InputBox.selectQuickPick(...)often auto-accepts single-select quick picks; callingconfirm()afterwards can throwElementNotInteractableError. - Editor selection tip:
TextEditor.getTextAtLine()selects the whole file internally (it uses a copy-to-clipboard implementation). Avoid it for selection logic. - Cursor positioning tip:
TextEditor.setCursor(line, column)uses 1-based columns (it drives the:Ln,ColUI); column0will time out. - Timeouts: UI tests should use a suite-level Mocha timeout instead of scattered
this.timeout(...)calls. This repo usestest/ui/.mocharc.jsonand passes it via--mocha_configinpackage.json. - Extension isolation: UI tests run with
--disable-extensionsand an isolated--extensions_dir(.test-extensions/extensions) to prevent local VS Code extensions from affecting runs.
vscode-extension-tester has several sharp edges, especially on Linux CI. This section documents issues we hit and the workarounds that work.
VSBrowser.instance.openResources(...) spawns a separate VS Code CLI subprocess (code -r <paths>) via child_process.execSync. This fails silently in several Linux CI scenarios:
- Ubuntu 24.04 / AppArmor (ExTester #2050): AppArmor restricts unprivileged user namespaces, breaking the CLI subprocess. Fix in CI:
- name: Allow unprivileged user namespace (ubuntu) if: runner.os == 'Linux' run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
- Docker / xvfb (ExTester #506): The CLI subprocess launches but can't connect to the running VS Code instance via IPC. No upstream fix exists.
When openResources fails, the workspace may not actually open and files won't appear as editor tabs. Always wrap it in try/catch and provide a fallback.
Always use absolute paths with openResources. Use path.resolve(__dirname, ...) — relative paths resolve from process.cwd() which is unpredictable in test runners.
Our openSampleFile() function uses a layered approach:
- Fast path:
EditorView.openEditor(filename)— succeeds if the tab is already open. - CLI path:
openResources(absolutePath)— works on macOS/Windows, may fail on Linux CI. - Quick Open fallback:
workbench.executeCommand("workbench.action.quickOpen")followed byInputBox.create()/setText(absolutePath)/confirm(). This is the most reliable cross-platform approach documented in ExTester issues. It goes through VS Code's command palette UI, which works everywhere.
The Quick Open approach is preferred over:
Ctrl+P/Cmd+Pkeyboard shortcuts: May not reach VS Code under xvfb/ChromeDriver.Workbench.openCommandPrompt(): Opens the Command Palette with a>prefix. Clearing it viaInputBox.setText()may not work reliably on all platforms becauseclear()on VS Code's custom input widget is inconsistent.- Explorer sidebar navigation: Requires knowing the exact section name (the workspace folder name), which can vary or not exist if the workspace didn't open.
openEditor(title) does not open a file — it finds an existing tab by exact title match and activates it. If the file isn't already open as a tab, it throws EditorTabNotFound. Always pair it with a mechanism that actually opens the file first.
VS Code shows notification toasts (workspace trust, extension recommendations) that can overlay the editor tab bar. Clear them early in the before hook:
await workbench.executeCommand("Notifications: Clear All Notifications");The Continue extension does this as the first thing after opening a workspace.
Since ExTester v4.1.0, security.workspace.trust.enabled is set to false in the test settings. No manual configuration needed unless you specifically want to test the trust dialog.
ViewContent.getSection(title) does a case-sensitive lookup. The Explorer file tree section is named after the workspace folder. If the workspace didn't open (e.g., because openResources failed), the section won't exist. Use getSections() to discover available sections when debugging.
Tree items may not be immediately available after opening a view. Use a wait pattern:
await driver.wait(async () => {
const items = await section.getVisibleItems();
return items.length > 0;
}, 5_000);VS Code's DOM changes dynamically. ExTester page objects can go stale between interactions. The only solution is to re-find elements when this occurs. Wrap fragile interactions in retry loops.
Native dialogs (file open, context menus, title bar menus) are not automatable on macOS. Use VS Code's built-in simple dialogs and the command palette instead.
- ExTester Wiki
- Known Issues
- ExTester #506 — openResources in Docker (Quick Open and TitleBar workarounds)
- ExTester #2050 — AppArmor on Ubuntu 24.04
| Test | Type | Description |
|---|---|---|
validateSerializedData accepts valid payload |
+ | Full payload with all fields |
validateSerializedData accepts minimal payload |
+ | Empty arrays, empty strings |
validateSerializedData accepts legacy data without partiallyAuditedFiles |
+ | Backwards compatibility |
validateSerializedData rejects missing treeEntries |
- | undefined or missing key |
validateSerializedData rejects missing auditedFiles |
- | undefined or missing key |
validateSerializedData rejects missing resolvedEntries |
- | undefined or missing key |
validateSerializedData rejects invalid entryType |
- | Number outside enum range |
validateSerializedData rejects entry missing label |
- | undefined label |
validateSerializedData rejects entry missing author |
- | undefined author |
validateSerializedData rejects entry missing details |
- | undefined details |
validateSerializedData rejects entry missing locations |
- | undefined locations |
validateSerializedData rejects location missing path |
- | undefined path |
validateSerializedData rejects location missing startLine |
- | undefined startLine |
validateSerializedData rejects location missing endLine |
- | undefined endLine |
validateSerializedData rejects location missing label |
- | undefined label |
validateSerializedData rejects auditedFile missing path |
- | undefined path |
validateSerializedData rejects auditedFile missing author |
- | undefined author |
validateSerializedData rejects partiallyAuditedFile missing coordinates |
- | Missing startLine/endLine |
validateSerializedData rejects entryDetails missing severity |
- | undefined severity |
validateSerializedData rejects entryDetails missing difficulty |
- | undefined difficulty |
validateSerializedData rejects entryDetails missing type |
- | undefined type |
| Test | Type | Description |
|---|---|---|
isEntry returns true for FullEntry |
+ | Has entryType field |
isEntry returns false for LocationEntry |
- | Missing entryType |
isEntry returns false for PathOrganizerEntry |
- | Missing entryType |
isLocationEntry returns true for FullLocationEntry |
+ | Has parentEntry field |
isLocationEntry returns false for FullEntry |
- | Missing parentEntry |
isPathOrganizerEntry returns true for path organizer |
+ | Has pathLabel field |
isPathOrganizerEntry returns false for entry |
- | Missing pathLabel |
isOldEntry returns true for entry without rootPath |
+ | Legacy format |
isOldEntry returns false for entry with rootPath |
- | New format |
isConfigurationEntry returns true for config |
+ | Has username field |
isWorkspaceRootEntry returns true for root |
+ | Has label field |
| Test | Type | Description |
|---|---|---|
entryEquals returns true for identical entries |
+ | Same label, type, author, locations |
entryEquals returns false for different labels |
- | Different label |
entryEquals returns false for different authors |
- | Different author |
entryEquals returns false for different types |
- | Finding vs Note |
entryEquals returns false for different location counts |
- | 1 vs 2 locations |
entryEquals returns false for different location paths |
- | Different path |
entryEquals returns false for different line ranges |
- | Different startLine/endLine |
getEntryIndexFromArray finds existing entry |
+ | Returns correct index |
getEntryIndexFromArray returns -1 for missing entry |
- | Entry not in array |
mergeTwoEntryArrays combines unique entries |
+ | No duplicates in result |
mergeTwoEntryArrays removes duplicates |
+ | Duplicate entry appears once |
mergeTwoEntryArrays handles empty arrays |
+ | Empty + non-empty |
mergeTwoAuditedFileArrays removes duplicates |
+ | Same path+author once |
mergeTwoAuditedFileArrays keeps different authors |
+ | Same path, different authors |
mergeTwoPartiallyAuditedFileArrays removes exact duplicates |
+ | Same coordinates |
mergeTwoPartiallyAuditedFileArrays keeps different ranges |
+ | Different line ranges |
configEntryEquals matches identical configs |
+ | Same path, username, root |
configEntryEquals rejects different paths |
- | Different path |
configEntryEquals rejects different usernames |
- | Different username |
configEntryEquals rejects different roots |
- | Different root.label |
| Test | Type | Description |
|---|---|---|
createDefaultSerializedData returns empty arrays |
+ | All arrays empty |
createDefaultSerializedData returns empty strings |
+ | All strings empty |
createDefaultEntryDetails returns undefined enums |
+ | Severity, difficulty, type |
createDefaultEntryDetails returns empty description |
+ | Empty string |
createDefaultEntryDetails returns recommendation template |
+ | Contains "Short term" |
createPathOrganizer creates valid path label |
+ | Returns {pathLabel} |
createLocationEntry ties location to parent |
+ | Returns linked structure |
treeViewModeLabel returns "list" for List mode |
+ | Enum to string |
treeViewModeLabel returns "byFile" for GroupByFile mode |
+ | Enum to string |
| Test | Type | Description |
|---|---|---|
Load .weaudit file parses valid JSON |
+ | Returns SerializedData |
Load .weaudit file handles missing file |
- | Returns default data |
Load .weaudit file handles corrupt JSON |
- | Returns default data, logs warning |
Load .weaudit file handles empty file |
- | Returns default data |
Load .weaudit file rejects invalid schema |
- | Validation fails, returns default |
Save .weaudit file creates .vscode directory |
+ | Directory created if missing |
Save .weaudit file writes valid JSON |
+ | Parseable output |
Save .weaudit file preserves other users' data |
+ | Merge with existing content |
Day log loads from .weauditdaylog |
+ | Returns Map<string, string[]> |
| Day log handles missing file | - | Returns empty map |
| Day log handles corrupt JSON | - | Returns empty map |
| Day log persists changes | + | Written to disk |
| Test | Type | Description |
|---|---|---|
createOrEditEntry creates new finding |
+ | EntryType.Finding, new label |
createOrEditEntry creates new note |
+ | EntryType.Note, new label |
createOrEditEntry edits existing entry label |
+ | Updates label in place |
createOrEditEntry prompts for input |
+ | Shows input box |
createOrEditEntry cancels on empty input |
- | Returns without change |
createOrEditEntry cancels on escape |
- | Returns without change |
deleteFinding removes entry from tree |
+ | Entry no longer in list |
deleteFinding removes all locations |
+ | Multi-location entry fully removed |
deleteFinding handles entry not found |
- | No error, no change |
resolveFinding moves entry to resolved list |
+ | In resolved, not in active |
resolveFinding preserves entry data |
+ | All fields intact |
restoreFinding moves entry back to active |
+ | In active, not in resolved |
restoreFinding handles entry not found |
- | No error, no change |
restoreAllResolvedFindings restores all entries |
+ | Resolved list empty |
restoreAllResolvedFindings filters by author |
+ | Only user's entries restored |
deleteAllResolvedFindings clears resolved list |
+ | List empty |
deleteAllResolvedFindings filters by author |
+ | Only user's entries deleted |
| Test | Type | Description |
|---|---|---|
addRegionToAnEntry adds location to existing entry |
+ | Location count increases |
addRegionToAnEntry uses current selection |
+ | Correct line range |
deleteLocation removes location from entry |
+ | Location count decreases |
deleteLocation deletes entry when last location removed |
+ | Entry removed entirely |
getActiveSelectionLocation handles single line |
+ | startLine == endLine |
getActiveSelectionLocation handles multi-line |
+ | Correct range |
getActiveSelectionLocation handles empty selection |
+ | Zero-length range |
getActiveSelectionLocation handles end-of-file |
+ | No off-by-one error |
getIntersectingTreeEntryIndex finds overlapping entry |
+ | Returns correct index |
getIntersectingTreeEntryIndex returns -1 when none |
- | No overlap |
| Test | Type | Description |
|---|---|---|
toggleAudited marks file as audited |
+ | Added to audited list |
toggleAudited unmarks audited file |
+ | Removed from audited list |
toggleAudited clears partial audits when marking full |
+ | Partial list cleared for file |
toggleAudited updates day log |
+ | Today's date entry updated |
toggleAudited handles sibling folder promotion |
+ | Parent marked → children removed |
addPartiallyAudited adds region |
+ | Region in partial list |
addPartiallyAudited uses current selection |
+ | Correct line range |
addPartiallyAudited skips if file fully audited |
- | No change to partial list |
mergePartialAudits combines overlapping regions |
+ | Adjacent regions merged |
mergePartialAudits combines adjacent regions |
+ | Touching regions merged |
mergePartialAudits leaves disjoint regions separate |
+ | Gap preserved |
mergePartialAudits handles single region |
+ | No change |
mergePartialAudits handles empty list |
+ | No error |
| Test | Type | Description |
|---|---|---|
createUniqueLabels uses basename for single root |
+ | Simple label |
createUniqueLabels disambiguates duplicate basenames |
+ | Adds parent directory |
recurseUniqueLabels handles deeply nested duplicates |
+ | Continues until unique |
getCorrespondingRootAndPath finds correct root |
+ | Returns [WARoot, relativePath] |
getCorrespondingRootAndPath handles nested roots |
+ | Most specific root selected |
getCorrespondingRootAndPath caches results |
+ | Second call uses cache |
getCorrespondingRootAndPath handles file outside all roots |
- | Returns undefined |
getAllCorrespondingRootsAndPaths returns all matching roots |
+ | For nested roots |
isInThisWorkspaceRoot returns true for file in root |
+ | Returns [true, relativePath] |
isInThisWorkspaceRoot returns false for file outside |
- | Returns [false, ""] |
| Test | Type | Description |
|---|---|---|
getRemoteAndPermalink generates GitHub URL |
+ | github.com format |
getRemoteAndPermalink generates GitLab URL |
+ | gitlab.com format |
getRemoteAndPermalink generates Bitbucket URL |
+ | bitbucket.org format |
getRemoteAndPermalink handles single line |
+ | #L10 format |
getRemoteAndPermalink handles line range |
+ | #L10-L20 format |
getRemoteAndPermalink strips .git suffix |
+ | Clean URL |
getRemoteAndPermalink handles SSH remote |
+ | Converts to HTTPS |
getRemoteAndPermalink returns empty for missing remote |
- | Empty string |
getClientPermalink uses client remote |
+ | Different from audit remote |
copyEntryPermalinks uses configured separator |
+ | Newline or custom |
copyEntryPermalinks includes all locations |
+ | Multi-location entry |
| Test | Type | Description |
|---|---|---|
navigateToNextPartiallyAuditedRegion moves to next region |
+ | Opens correct line |
navigateToNextPartiallyAuditedRegion wraps to first region |
+ | After last region |
navigateToNextPartiallyAuditedRegion handles single region |
+ | Stays on same region |
navigateToNextPartiallyAuditedRegion handles no regions |
- | No navigation |
navigateToNextPartiallyAuditedRegion handles multi-file |
+ | Crosses file boundaries |
| Test | Type | Description |
|---|---|---|
getChildrenLinear returns all entries flat |
+ | No nesting |
getChildrenLinear returns empty for no entries |
+ | Empty array |
getChildrenPerFile groups by file path |
+ | PathOrganizer parents |
getChildrenPerFile returns entries under path |
+ | Correct children |
getTreeItem returns bug icon for finding |
+ | ThemeIcon("bug") |
getTreeItem returns bookmark icon for note |
+ | ThemeIcon("bookmark") |
getTreeItem sets correct command |
+ | openFileLines command |
getTreeItem sets tooltip with author |
+ | Author name in tooltip |
| Drag-and-drop reorders entries | + | Order changes |
| Drag-and-drop moves location to different entry | + | Re-parenting works |
| Drag-and-drop rejects invalid drop target | - | No change |
| Test | Type | Description |
|---|---|---|
getChildren returns roots for multi-root |
+ | WorkspaceRootEntry list |
getChildren returns configs for single root |
+ | ConfigurationEntry list |
getChildren returns configs under root |
+ | Filtered by root label |
getTreeItem shows username as label |
+ | Correct label |
getTreeItem shows filename as description |
+ | .weaudit filename |
getTreeItem shows eye icon for active config |
+ | Icon when selected |
findAndLoadConfigurationFiles finds .weaudit files |
+ | All files discovered |
findAndLoadConfigurationFiles handles missing .vscode |
- | No error |
| Test | Type | Description |
|---|---|---|
getChildren returns resolved entries |
+ | All resolved entries |
getChildren returns empty for no resolved |
+ | Empty array |
getTreeItem uses correct icon by type |
+ | Bug vs bookmark |
setResolvedEntries triggers refresh |
+ | Event emitted |
| Test | Type | Description |
|---|---|---|
update-entry updates field value |
+ | Field changed |
update-entry persists when isPersistent true |
+ | Saved to file |
update-entry skips persist when false |
+ | Not saved |
update-repository-config updates all fields |
+ | Client, audit, hash |
choose-workspace-root switches active root |
+ | Root changed |
webview-ready sends initial data |
+ | Entry details sent |
set-workspace-roots populates dropdown |
+ | Root labels sent |
| Invalid message type ignored | - | No error |
| Test | Type | Description |
|---|---|---|
| Constructor loads all decoration types | + | 5 types created |
reloadAllDecorationConfigurations disposes old |
+ | No memory leak |
reloadAllDecorationConfigurations loads new colors |
+ | Config values used |
hoverOnLabel creates correct range |
+ | Range matches input |
labelAfterFirstLineTextDecoration adds after text |
+ | renderOptions set |
| Decoration uses gutter icon | + | gutterIconPath set |
Create mock factories in test/mocks/vscode.ts:
export function createMockWorkspaceFolder(path: string): vscode.WorkspaceFolder;
export function createMockTextEditor(uri: string, selection?: Range): vscode.TextEditor;
export function createMockExtensionContext(): vscode.ExtensionContext;For webview tests, mock acquireVsCodeApi():
globalThis.acquireVsCodeApi = () => ({
postMessage: sinon.stub(),
getState: () => ({}),
setState: sinon.stub(),
});- Stub non-deterministic values:
Date.now(),crypto.randomUUID() - Write temp files to
os.tmpdir()/weaudit-test-*, clean up inafterEach - Use explicit assertions over snapshots; reserve snapshots for markdown export only
- Extension-host tests: use
vscode.commands.executeCommand, not private methods
Extension-host tests require display server on Linux:
- name: Run tests
run: xvfb-run -a npm test
env:
DISPLAY: ':99'Consider VS Code version matrix: stable, insiders.
Extension-host tests run inside a real VS Code instance via @vscode/test-electron. These tests verify that the extension integrates correctly with VS Code APIs.
test/extension/
├── suite/
│ ├── index.ts # Mocha test runner configuration
│ ├── activation.test.ts # Extension activation tests
│ ├── commands.test.ts # Command registration and execution
│ ├── treeViews.test.ts # Tree view integration
│ ├── decorations.test.ts# Editor decorations
│ └── workspace.test.ts # Workspace and file operations
└── fixtures/
└── sample-workspace/ # Test workspace with sample files
| Test | Type | Description |
|---|---|---|
| Extension activates on workspace open | + | onStartupFinished trigger |
| Extension activates on view open | + | onView:weAudit trigger |
| Extension exports public API | + | activate() returns expected API |
| Extension registers all commands | + | All 30+ commands available |
| Extension creates tree views | + | All 5 views registered |
Extension handles missing .vscode folder |
+ | Creates folder on first save |
Extension handles corrupt .weaudit file |
- | Graceful degradation, shows warning |
| Test | Type | Description |
|---|---|---|
weAudit.addFinding creates finding at selection |
+ | Entry added to tree |
weAudit.addFinding prompts for label |
+ | Input box shown |
weAudit.addFinding cancels on escape |
- | No entry created |
weAudit.addNote creates note at selection |
+ | Note entry added |
weAudit.editEntryLabel updates existing label |
+ | Label changed in tree |
weAudit.deleteEntry removes entry |
+ | Entry removed from tree |
weAudit.deleteEntry handles multi-location entry |
+ | All locations removed |
weAudit.toggleAudited marks file as reviewed |
+ | Decoration applied |
weAudit.toggleAudited unmarks reviewed file |
+ | Decoration removed |
weAudit.addPartiallyAudited marks region |
+ | Region decorated |
weAudit.resolveFinding moves to resolved tree |
+ | Entry in resolved view |
weAudit.restoreFinding moves back to active |
+ | Entry in main view |
weAudit.copyPermalink copies to clipboard |
+ | Clipboard contains URL |
weAudit.copyClientPermalink uses client remote |
+ | Different URL than audit |
weAudit.openGitHubIssue opens external URL |
+ | URL contains finding data |
weAudit.exportFindings creates markdown file |
+ | Valid markdown output |
weAudit.showDayLog displays log in output |
+ | Output channel shown |
weAudit.navigateToNextPartiallyAuditedRegion moves cursor |
+ | Selection changes |
weAudit.setTreeViewMode toggles view mode |
+ | Tree refreshes |
| Test | Type | Description |
|---|---|---|
| Findings tree view shows entries | + | Tree populated |
| Findings tree view updates on add | + | New entry appears |
| Findings tree view updates on delete | + | Entry disappears |
| Findings tree view click opens file | + | Editor opens at line |
| Findings tree view context menu appears | + | Menu items available |
| Resolved tree view shows resolved entries | + | Resolved entries visible |
Saved findings tree shows .weaudit files |
+ | Config files listed |
| Saved findings tree toggles visibility | + | Eye icon toggles |
| Tree view drag-and-drop reorders | + | Order persists |
| Tree view refresh command works | + | Tree rebuilds |
| Test | Type | Description |
|---|---|---|
| Finding decorations appear on marked lines | + | Background color visible |
| Note decorations appear on marked lines | + | Different color than findings |
| Audited file decoration covers all lines | + | Whole file decorated |
| Partial audit decoration covers region only | + | Only selected lines |
| Decorations update on file edit | + | Lines shift correctly |
| Decorations clear on entry delete | + | No lingering decoration |
| Gutter icons appear for findings | + | Icon in gutter |
| Hover shows finding label | + | Hover message displayed |
| Decoration colors respect configuration | + | Custom colors applied |
| Theme change updates decorations | + | Colors adapt |
| Test | Type | Description |
|---|---|---|
| Finding details panel opens | + | Webview visible |
| Finding details panel shows entry data | + | Fields populated |
| Finding details panel updates on selection | + | Data changes |
| Finding details panel saves changes | + | Persisted to file |
| Git config panel opens | + | Webview visible |
| Git config panel shows repository URLs | + | Fields populated |
| Git config panel saves changes | + | Config updated |
| Webview survives editor close/reopen | + | State preserved |
| Test | Type | Description |
|---|---|---|
| Single-root workspace loads findings | + | Tree populated |
| Multi-root workspace loads all roots | + | All roots visible |
| Adding workspace folder updates tree | + | New root appears |
| Removing workspace folder updates tree | + | Root removed |
| File rename updates entry locations | + | Paths corrected |
| File delete prompts for entry removal | + | User prompted |
External .weaudit change reloads |
+ | File watcher triggers |
| Configuration change updates behavior | + | Settings respected |
| Test | Type | Description |
|---|---|---|
| Audited files show badge in explorer | + | Badge visible |
| Partially audited files show different badge | + | Distinct indicator |
| Badge updates on audit toggle | + | Real-time update |
| Badge respects file filter | + | Hidden files excluded |
| Test | Type | Description |
|---|---|---|
| Invalid selection shows error message | - | User-friendly error |
| Missing workspace shows warning | - | Prompts to open folder |
| Git remote not configured shows warning | - | Guidance message |
| Concurrent saves handled correctly | + | No data corruption |
| Large file handling doesn't freeze UI | + | Responsive during operation |
| Test | Type | Description |
|---|---|---|
| Extension activates under 500ms | + | Startup time acceptable |
| Tree view renders 100 entries under 100ms | + | No visible lag |
| File decoration applies under 50ms | + | Immediate visual feedback |
Large .weaudit file (1000 entries) loads |
+ | No timeout |
Create test/extension/suite/index.ts:
import * as path from "path";
import Mocha from "mocha";
import { glob } from "glob";
export async function run(): Promise<void> {
const mocha = new Mocha({
ui: "bdd",
color: true,
timeout: 60000,
});
const testsRoot = path.resolve(__dirname, "..");
const files = await glob("**/**.test.js", { cwd: testsRoot });
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
return new Promise((resolve, reject) => {
mocha.run((failures) => {
if (failures > 0) {
reject(new Error(`${failures} tests failed.`));
} else {
resolve();
}
});
});
}Create test/extension/suite/activation.test.ts:
import * as assert from "assert";
import * as vscode from "vscode";
suite("Extension Activation", () => {
test("Extension should be present", () => {
const extension = vscode.extensions.getExtension("trailofbits.weaudit");
assert.ok(extension, "Extension not found");
});
test("Extension should activate", async () => {
const extension = vscode.extensions.getExtension("trailofbits.weaudit");
await extension?.activate();
assert.strictEqual(extension?.isActive, true);
});
test("Commands should be registered", async () => {
const commands = await vscode.commands.getCommands(true);
assert.ok(commands.includes("weAudit.addFinding"));
assert.ok(commands.includes("weAudit.addNote"));
assert.ok(commands.includes("weAudit.toggleAudited"));
});
});Create test/extension/fixtures/sample-workspace/ with:
.vscode/settings.jsonsrc/sample.ts(sample code file).vscode/testuser.weaudit(pre-populated findings)
{
"test": "npm run test:unit && npm run test:ext",
"test:unit": "mocha -r ts-node/register 'test/unit/**/*.test.ts'",
"test:ext": "vscode-test",
"coverage": "nyc npm run test:unit"
}