Skip to content

Commit bb6d3e6

Browse files
committed
fix: enhance file removal functionality and add coverage tests
1 parent 5a95d80 commit bb6d3e6

18 files changed

Lines changed: 603 additions & 57 deletions

.github/workflows/validate.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ jobs:
5555
- name: Type check
5656
run: tsc -p ./
5757

58-
- name: Run Unit and Property tests
59-
run: npm test
58+
- name: Run Unit and Property tests with coverage
59+
run: npm run test:coverage
6060

6161
- name: Package extension
6262
run: npx @vscode/vsce package --out virtual-tabs.vsix

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ test-resources/
3636
# Testing
3737
/coverage
3838
test-ui-output.txt
39+
docs/assets/fix_rounded_corners.py

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ All notable changes to the "VirtualTabs" extension will be documented in this fi
1111
### Fixed
1212

1313
- Directory drag-and-drop now skips hidden directories whose names start with `.`, while still including dotfiles such as `.gitignore` and `.editorconfig`.
14+
- Removing selected files from a group now works when the group stores workspace-relative file paths after config reload.
15+
16+
### Tests and CI
17+
18+
- Added focused unit coverage for file-entry matching across URI, absolute path, and workspace-relative path storage forms.
19+
- Added `npm run test:coverage` and updated PR validation to run Jest coverage for the file-entry matching helper before packaging.
1420

1521
## [0.5.5] - 2026-05-13
1622

DEVELOPMENT.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,23 @@ VirtualTabs uses three automated test layers:
137137
| Layer | Command | Purpose |
138138
| :--- | :--- | :--- |
139139
| TypeScript + Jest unit tests | `npm run test` | Compiles the extension and runs Jest unit tests. |
140+
| Jest coverage gate | `npm run test:coverage` | Runs Jest with coverage enabled for focused unit-tested core helpers and enforces configured thresholds. |
140141
| Property tests | `npm run test:properties` | Exercises config scope discovery, path routing, and tree aggregation invariants with generated inputs. |
141142
| VS Code UI/E2E tests | `npm run test:ui` | Launches a real VS Code instance with `vscode-extension-tester` and verifies Activity Bar, sidebar, and multi-root scope behavior. |
142143

143144
### Run the Full Local Test Set
144145

145146
```bash
146147
npm run test
148+
npm run test:coverage
147149
npm run test:properties
148150
npm run test:ui
149151
```
150152

153+
### Coverage Gate
154+
155+
`npm run test:coverage` uses Jest coverage and is part of the PR validation workflow. Coverage is intentionally scoped in `jest.config.js`; when adding files to `collectCoverageFrom`, add focused unit tests in the same PR so the gate remains meaningful instead of reflecting unrelated legacy or UI-heavy code.
156+
151157
### UI/E2E Test Setup
152158

153159
The UI test script compiles the UI test files and then runs them through `extest`:

jest.config.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,21 @@ module.exports = {
77
modulePathIgnorePatterns: ['<rootDir>/.vscode-test/'],
88
transform: {
99
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.test.json' }]
10+
},
11+
moduleNameMapper: {
12+
'^(\\.{1,2}/.*)\\.js$': '$1'
13+
},
14+
collectCoverageFrom: [
15+
'src/core/FileEntryMatcher.ts',
16+
'src/core/GroupFileRemoval.ts',
17+
'src/core/GroupFileTargets.ts'
18+
],
19+
coverageThreshold: {
20+
global: {
21+
statements: 90,
22+
branches: 80,
23+
functions: 90,
24+
lines: 90
25+
}
1026
}
1127
};

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "virtual-tabs",
33
"displayName": "VirtualTabs - Virtual File Directories & AI context management",
44
"description": "%extension.description%",
5-
"version": "0.5.7",
5+
"version": "0.5.8",
66
"publisher": "winterdrive",
77
"icon": "docs/assets/virtualtabs_icon_128.png",
88
"categories": [
@@ -683,6 +683,7 @@
683683
},
684684
"scripts": {
685685
"test": "tsc -p ./ && jest --runInBand",
686+
"test:coverage": "jest --runInBand --coverage",
686687
"test:properties": "jest --runInBand src/test/properties",
687688
"test:ui:setup": "extest setup-tests",
688689
"test:ui": "tsc -p tsconfig.test.ui.json && extest setup-and-run \"out/test/ui/**/*.test.js\" -e \".vscode-test/extensions\" -r \"test-resources/multi-root/virtual-tabs.code-workspace\" -c 1.96.0",

src/commands.ts

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { executeWithConfirmation } from './util';
99
import { SkillGenerator } from './mcp/SkillGenerator';
1010
import { McpConfigPanel } from './mcp/McpConfigPanel';
1111
import { SendToManager } from './sendTo';
12+
import { groupItemsByGroupIdx } from './core/GroupFileTargets';
1213

1314
// Global clipboard for VirtualTabs items
1415
let globalClipboardItems: (TempFileItem | TempFolderItem)[] = [];
@@ -633,15 +634,7 @@ export function registerCommands(
633634
context.subscriptions.push(vscode.commands.registerCommand('virtualTabs.removeSelectedFilesFromGroup', (item?: TempFileItem) => {
634635
const filesToRemove = resolveTargetItems(item, provider);
635636
if (filesToRemove.length === 0) return;
636-
const filesByGroup = new Map<number, TempFileItem[]>();
637-
for (const fileItem of filesToRemove) {
638-
const current = filesByGroup.get(fileItem.groupIdx);
639-
if (current) {
640-
current.push(fileItem);
641-
} else {
642-
filesByGroup.set(fileItem.groupIdx, [fileItem]);
643-
}
644-
}
637+
const filesByGroup = groupItemsByGroupIdx(filesToRemove);
645638

646639
for (const [groupIdx, groupFiles] of filesByGroup) {
647640
provider.removeFilesFromGroup(groupIdx, groupFiles);
@@ -855,16 +848,9 @@ export function registerCommands(
855848

856849

857850
const executeRemove = () => {
858-
const filesByGroup = new Map<number, TempFileItem[]>();
859-
for (const fileItem of filesToRemove) {
860-
if (!(fileItem instanceof TempFileItem)) continue;
861-
const current = filesByGroup.get(fileItem.groupIdx);
862-
if (current) {
863-
current.push(fileItem);
864-
} else {
865-
filesByGroup.set(fileItem.groupIdx, [fileItem]);
866-
}
867-
}
851+
const filesByGroup = groupItemsByGroupIdx(
852+
filesToRemove.filter((fileItem): fileItem is TempFileItem => fileItem instanceof TempFileItem)
853+
);
868854

869855
for (const [groupIdx, groupFiles] of filesByGroup) {
870856
provider.removeFilesFromGroup(groupIdx, groupFiles);

src/core/GroupFileRemoval.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { TempGroup } from '../types';
2+
import { matchesStoredFileEntry } from './FileEntryMatcher';
3+
4+
export interface FileRemovalTarget {
5+
uri: string;
6+
fsPath: string;
7+
}
8+
9+
export function removeStoredFileEntriesFromGroup(
10+
group: Pick<TempGroup, 'files' | 'bookmarks'>,
11+
targets: FileRemovalTarget[],
12+
scopeRoot?: string
13+
): boolean {
14+
if (!group.files || targets.length === 0) {
15+
return false;
16+
}
17+
18+
const originalLength = group.files.length;
19+
group.files = group.files.filter(storedEntry =>
20+
!targets.some(target => matchesStoredFileEntry(storedEntry, target.uri, target.fsPath, scopeRoot))
21+
);
22+
23+
if (group.bookmarks) {
24+
for (const bookmarkKey of Object.keys(group.bookmarks)) {
25+
const shouldDelete = targets.some(target =>
26+
matchesStoredFileEntry(bookmarkKey, target.uri, target.fsPath, scopeRoot)
27+
);
28+
if (shouldDelete) {
29+
delete group.bookmarks[bookmarkKey];
30+
}
31+
}
32+
if (Object.keys(group.bookmarks).length === 0) {
33+
delete group.bookmarks;
34+
}
35+
}
36+
37+
return group.files.length !== originalLength;
38+
}

src/core/GroupFileTargets.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface GroupIndexedItem {
2+
groupIdx: number;
3+
}
4+
5+
export function groupItemsByGroupIdx<T extends GroupIndexedItem>(items: T[]): Map<number, T[]> {
6+
const itemsByGroup = new Map<number, T[]>();
7+
8+
for (const item of items) {
9+
const current = itemsByGroup.get(item.groupIdx);
10+
if (current) {
11+
current.push(item);
12+
} else {
13+
itemsByGroup.set(item.groupIdx, [item]);
14+
}
15+
}
16+
17+
return itemsByGroup;
18+
}

0 commit comments

Comments
 (0)