From 3df78ba4903dc9beda2aa177fb329c8062d18981 Mon Sep 17 00:00:00 2001 From: max Date: Mon, 7 Sep 2026 17:50:02 +0200 Subject: [PATCH] Filter panel results by project and hide test code Roadmap item 3. One Filters button opens a menu holding both, rather than two more controls in a toolbar that is already crowded in a docked panel: - Hide test code, with the count of test results. - A checkbox per project with its result count, plus All / None. The button shows how many filters are active and the summary line reads "12 of 43 references" while anything is filtered. Projects are persisted as an *exclusion* list, so a project that only appears in a later search shows up instead of being silently hidden. Test hiding seeds from coloredReferences.hideTests on first results, then follows the panel. What counts as test code is coloredReferences.testPattern, a regular expression matched against the workspace-relative path and against the containing project name. Matching on names turned out to need care: the first pattern classified Latest.cs as a test because "Latest" contains "test". The built-in pattern now requires the name to start at a boundary, or an uppercase T for the CamelCase FooTests.cs form, and covers test/tests directories, Tests.cs, foo_test.go, test_foo.py, foo.spec.ts and projects named Something.Tests. An invalid configured regex is reported once and ignored rather than throwing per search. These filters narrow the panel only; the editor view still lists everything. Co-Authored-By: Claude Opus 5 --- README.md | 22 ++++- media/panel.css | 95 +++++++++++++++++++++ media/panel.js | 150 ++++++++++++++++++++++++++++++++- package.json | 10 +++ src/panel.ts | 28 +++++- src/references.ts | 63 ++++++++++++++ src/test/suite/filters.test.ts | 105 +++++++++++++++++++++++ src/test/suite/index.ts | 1 + 8 files changed, 468 insertions(+), 6 deletions(-) create mode 100644 src/test/suite/filters.test.ts diff --git a/README.md b/README.md index 0e4f102..e91efd4 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ highlighted like search matches; **writes are bold** on the theme's stronger wri same colour the editor itself uses for a write. **Panel view** shows the same results as a table with resizable, sortable columns — Code, File, Line, Kind, -Project and Containing member — grouped by file, with a filter box and a reads/writes filter. By default it -docks in the **bottom panel** +Project and Containing member — grouped by file, with a text filter plus filters for reads/writes, project and +test code. By default it docks in the **bottom panel** alongside Terminal and Problems, where a wide, short table reads best; you can drag it to either side bar, or set `coloredReferences.panelLocation` to put it in an editor group instead. @@ -36,6 +36,19 @@ The **All / Reads / Writes** buttons filter by kind, and the summary line counts the text filter, so typing `write` narrows to writes as well. See *Known limitations* for where the kind comes from. +**Filters** opens a menu to hide test code or to include only some projects, each with its result count. The +button shows how many filters are active, and the summary line reads *"12 of 43 references"* while anything is +filtered. Choices persist per panel, and projects are remembered as an *exclusion* list — a project that only +turns up in a later search shows up rather than being silently hidden. + +What counts as test code is a regular expression, `coloredReferences.testPattern`, matched against each +result's workspace-relative path and against its containing project name. The built-in pattern covers +`test`/`tests` directories, `FooTests.cs`, `Tests.cs`, `foo_test.go`, `test_foo.py`, `foo.spec.ts` and projects +named `Something.Tests` — without catching names that merely contain the word, like `Latest.cs` or a `contest` +directory. `coloredReferences.hideTests` sets whether the filter starts on. + +These filters narrow what the panel shows; the editor view always lists every reference. + ## Semantic tokens VS Code colors code twice: a TextMate grammar handles keywords, strings and comments from the text alone, then @@ -63,6 +76,9 @@ grammar-only colouring; results spanning more than 40 files skip it. - `coloredReferences.openBeside` — open results beside the current editor (default `true`) - `coloredReferences.showProject` — show the containing project in file headers (default `true`) - `coloredReferences.reuseTab` — reuse one results tab/panel instead of opening a new one per search (default `true`) +- `coloredReferences.hideTests` — start the panel with test code hidden (default `false`) +- `coloredReferences.testPattern` — regular expression deciding what counts as test code; empty uses the + built-in pattern ## Install @@ -107,6 +123,6 @@ the expected results stay deterministic. 1. ~~Webview panel with resizable, sortable columns~~ — done; missing: virtualized rendering for very large result sets, and remembering column layout per workspace rather than per panel 2. ~~Semantic token overlay~~ — done in both views -3. Filter by project / exclude tests +3. ~~Filter by project / exclude tests~~ — done in the panel 4. ~~Read/write kind~~ — done in both views diff --git a/media/panel.css b/media/panel.css index d284183..c4d6d61 100644 --- a/media/panel.css +++ b/media/panel.css @@ -345,3 +345,98 @@ body.resizing { background: var(--vscode-inputValidation-warningBackground); border-color: var(--vscode-inputValidation-warningBorder, transparent); } + +/* --- filters popover ---------------------------------------------------- */ + +.popover-host { + position: relative; + flex: 0 0 auto; +} + +.popover { + position: absolute; + top: calc(100% + 4px); + right: 0; + z-index: 10; + min-width: 200px; + max-width: 320px; + max-height: 60vh; + overflow-y: auto; + padding: 4px; + color: var(--vscode-menu-foreground, var(--vscode-foreground)); + background: var(--vscode-menu-background, var(--vscode-editorWidget-background)); + border: 1px solid var(--vscode-menu-border, var(--vscode-editorWidget-border)); + border-radius: 4px; + box-shadow: 0 2px 8px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.36)); +} + +.popover[hidden] { + display: none; +} + +.popover-heading { + padding: 6px 6px 2px; + color: var(--vscode-descriptionForeground); + font-size: 0.9em; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.popover-row { + display: flex; + align-items: center; + gap: 6px; + padding: 3px 6px; + border-radius: 2px; + cursor: pointer; + white-space: nowrap; +} + +.popover-row:hover { + background: var(--vscode-list-hoverBackground); +} + +.popover-row.disabled { + opacity: 0.5; +} + +.popover-row input { + flex: 0 0 auto; + margin: 0; + accent-color: var(--vscode-checkbox-background, var(--vscode-button-background)); +} + +.popover-label { + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; +} + +.popover-count { + flex: 0 0 auto; + color: var(--vscode-descriptionForeground); + font-variant-numeric: tabular-nums; +} + +.popover-actions { + display: flex; + gap: 8px; + padding: 4px 6px 2px; + border-top: 1px solid var(--vscode-menu-separatorBackground, var(--vscode-panel-border)); + margin-top: 4px; +} + +.link-button { + padding: 0; + color: var(--vscode-textLink-foreground); + background: none; + border: none; + font-family: inherit; + font-size: inherit; + cursor: pointer; +} + +.link-button:hover { + color: var(--vscode-textLink-activeForeground); + text-decoration: underline; +} diff --git a/media/panel.js b/media/panel.js index 87308b2..29bed43 100644 --- a/media/panel.js +++ b/media/panel.js @@ -28,7 +28,9 @@ { value: 'write', label: 'Writes', title: 'Show only references that write the symbol' }, ]; - /** @type {{symbol: string, languageId: string, rows: any[], fileCount: number}} */ + /** @type {{symbol: string, languageId: string, rows: any[], fileCount: number, + * projects?: {name: string, count: number}[], testCount?: number, + * hideTestsDefault?: boolean}} */ let data = { symbol: '', languageId: '', rows: [], fileCount: 0 }; const restored = vscode.getState() || {}; @@ -38,6 +40,11 @@ group: restored.group !== false, filter: restored.filter || '', kind: KIND_FILTERS.some(k => k.value === restored.kind) ? restored.kind : 'all', + // Projects are stored as an *exclusion* list, so a project that only turns up in + // a later search is visible by default rather than silently filtered out. + hiddenProjects: Array.isArray(restored.hiddenProjects) ? restored.hiddenProjects : [], + // undefined until the first results arrive, then seeded from the setting. + hideTests: typeof restored.hideTests === 'boolean' ? restored.hideTests : undefined, widths: Object.assign({}, restored.widths), collapsed: Object.assign({}, restored.collapsed), selectedId: restored.selectedId, @@ -47,6 +54,8 @@ summary: /** @type {HTMLElement} */ (document.getElementById('summary')), filter: /** @type {HTMLInputElement} */ (document.getElementById('filter')), kindFilter: /** @type {HTMLElement} */ (document.getElementById('kind-filter')), + filters: /** @type {HTMLButtonElement} */ (document.getElementById('filters')), + filtersMenu: /** @type {HTMLElement} */ (document.getElementById('filters-menu')), group: /** @type {HTMLButtonElement} */ (document.getElementById('toggle-group')), refresh: /** @type {HTMLButtonElement} */ (document.getElementById('refresh')), table: /** @type {HTMLElement} */ (document.getElementById('table')), @@ -404,6 +413,12 @@ if (state.kind !== 'all' && row.kind !== state.kind) { return false; } + if (state.hideTests && row.isTest) { + return false; + } + if (state.hiddenProjects.indexOf(row.project) >= 0) { + return false; + } if (!needle) { return true; } @@ -638,6 +653,128 @@ renderRows(); }); + // ----------------------------------------------------------------------- + // Project / test filters + // ----------------------------------------------------------------------- + + function activeFilterCount() { + return state.hiddenProjects.filter( + p => (data.projects || []).some(entry => entry.name === p)).length + + (state.hideTests ? 1 : 0); + } + + function refreshFiltersButton() { + const active = activeFilterCount(); + el.filters.textContent = active > 0 ? `Filters (${active})` : 'Filters'; + el.filters.setAttribute('aria-pressed', String(active > 0)); + } + + function checkboxRow(label, checked, onToggle, count) { + const row = document.createElement('label'); + row.className = 'popover-row'; + + const box = document.createElement('input'); + box.type = 'checkbox'; + box.checked = checked; + box.addEventListener('change', () => onToggle(box.checked)); + row.appendChild(box); + + const text = document.createElement('span'); + text.className = 'popover-label'; + text.textContent = label; + row.appendChild(text); + + if (count !== undefined) { + const badge = document.createElement('span'); + badge.className = 'popover-count'; + badge.textContent = String(count); + row.appendChild(badge); + } + return row; + } + + function buildFiltersMenu() { + const menu = el.filtersMenu; + menu.textContent = ''; + + const tests = data.testCount || 0; + const testRow = checkboxRow('Hide test code', !!state.hideTests, checked => { + state.hideTests = checked; + saveState(); + buildFiltersMenu(); + refreshFiltersButton(); + renderRows(); + }, tests); + if (tests === 0) { + testRow.classList.add('disabled'); + testRow.title = 'No results are in test code'; + } + menu.appendChild(testRow); + + const projects = data.projects || []; + if (projects.length > 1) { + const heading = document.createElement('div'); + heading.className = 'popover-heading'; + heading.textContent = 'Projects'; + menu.appendChild(heading); + + for (const project of projects) { + const shown = state.hiddenProjects.indexOf(project.name) < 0; + menu.appendChild(checkboxRow(project.name || '(no project)', shown, checked => { + state.hiddenProjects = state.hiddenProjects.filter(p => p !== project.name); + if (!checked) { + state.hiddenProjects.push(project.name); + } + saveState(); + buildFiltersMenu(); + refreshFiltersButton(); + renderRows(); + }, project.count)); + } + + const actions = document.createElement('div'); + actions.className = 'popover-actions'; + for (const [label, hidden] of [['All', []], ['None', projects.map(p => p.name)]]) { + const button = document.createElement('button'); + button.className = 'link-button'; + button.textContent = /** @type {string} */ (label); + button.addEventListener('click', () => { + state.hiddenProjects = /** @type {string[]} */ (hidden).slice(); + saveState(); + buildFiltersMenu(); + refreshFiltersButton(); + renderRows(); + }); + actions.appendChild(button); + } + menu.appendChild(actions); + } + } + + function openFilters(open) { + el.filtersMenu.hidden = !open; + el.filters.setAttribute('aria-expanded', String(open)); + if (open) { + buildFiltersMenu(); + const first = el.filtersMenu.querySelector('input'); + if (first) { + /** @type {HTMLElement} */ (first).focus(); + } + } + } + + el.filters.addEventListener('click', event => { + event.stopPropagation(); + openFilters(el.filtersMenu.hidden); + }); + + document.addEventListener('click', event => { + const target = /** @type {HTMLElement} */ (event.target); + if (!el.filtersMenu.hidden && !el.filtersMenu.contains(target) && target !== el.filters) { + openFilters(false); + } + }); + function buildKindFilter() { el.kindFilter.textContent = ''; for (const option of KIND_FILTERS) { @@ -671,6 +808,9 @@ event.preventDefault(); el.filter.focus(); el.filter.select(); + } else if (event.key === 'Escape' && !el.filtersMenu.hidden) { + openFilters(false); + el.filters.focus(); } else if (event.key === 'Escape' && document.activeElement === el.filter) { el.filter.value = ''; state.filter = ''; @@ -692,6 +832,13 @@ if (message.type === 'results') { const newSymbol = message.symbol !== data.symbol; data = message; + if (typeof state.hideTests !== 'boolean') { + state.hideTests = !!data.hideTestsDefault; + } + refreshFiltersButton(); + if (!el.filtersMenu.hidden) { + buildFiltersMenu(); + } // Row ids are positional, so a selection carried over from another symbol // would point at an unrelated reference. if (newSymbol || !data.rows.some(r => r.id === state.selectedId)) { @@ -716,6 +863,7 @@ applyPalette(); el.filter.value = state.filter; buildKindFilter(); + refreshFiltersButton(); el.group.setAttribute('aria-pressed', String(state.group)); el.body.tabIndex = 0; computeAutoWidths(); diff --git a/package.json b/package.json index f82e77d..cb726f0 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,16 @@ "type": "boolean", "default": true, "description": "Use semantic tokens from the language server so identifiers are coloured by what they are. The editor view gets the theme-exact colouring; the panel gets the classification with an approximated palette. Turn off for grammar-only colouring." + }, + "coloredReferences.hideTests": { + "type": "boolean", + "default": false, + "description": "Hide references in test code by default. The panel's Filters button toggles it per search." + }, + "coloredReferences.testPattern": { + "type": "string", + "default": "", + "markdownDescription": "Regular expression deciding what counts as test code, matched against each result's workspace-relative path and against its containing project name. Leave empty for the built-in pattern, which covers `test`/`tests` directories, `*Tests.cs`, `*_test.go`, `*.spec.ts` and projects named `*.Tests`. Set it to a pattern that matches nothing (for example `$^`) to treat everything as production code." } } }, diff --git a/src/panel.ts b/src/panel.ts index 87d93b9..09b489d 100644 --- a/src/panel.ts +++ b/src/panel.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { - RefKind, ReferenceResults, applyKinds, containingMembers, displayLine, referenceKinds, relativePath, + RefKind, ReferenceResults, applyKinds, containingMembers, displayLine, isTest, referenceKinds, + relativePath, testPattern, } from './references'; import { SEMANTIC_FILE_LIMIT, sourceLegend, sourceTokens } from './semantic'; @@ -80,6 +81,8 @@ export interface PanelRow { kind: RefKind; /** Semantic token spans within `code`, empty when the server serves none. */ spans: CodeSpan[]; + /** Whether this reference lives in test code. */ + isTest: boolean; } /** @@ -97,9 +100,12 @@ export function buildRows( const rows: PanelRow[] = []; const locations = new Map(); + const pattern = testPattern(); + let id = 0; for (const file of results.files) { const relative = relativePath(file.uri); + const test = isTest(file.uri, file.project, pattern); const dir = path.dirname(relative); for (const source of file.lines) { const { text, symbolRanges } = displayLine(source); @@ -119,6 +125,7 @@ export function buildRows( member, kind: symbolRanges[index].kind, spans: spans.get(`${file.uri.toString()}|${source.line}`) ?? [], + isTest: test, }); locations.set(id, new vscode.Location(file.uri, range.start)); id++; @@ -198,12 +205,23 @@ export class ResultsView { const { rows, locations } = buildRows(results, members, kinds, spans); this.locations = locations; + // Projects in result order, with counts, so the filter list needs no work + // in the webview. + const projects = new Map(); + for (const row of rows) { + projects.set(row.project, (projects.get(row.project) ?? 0) + 1); + } + this.post({ type: 'results', symbol: results.symbol, languageId: results.languageId, fileCount: results.files.length, writeCount: rows.filter(r => r.kind === 'write').length, + testCount: rows.filter(r => r.isTest).length, + projects: [...projects].map(([name, count]) => ({ name, count })), + hideTestsDefault: vscode.workspace.getConfiguration('coloredReferences') + .get('hideTests', false), rows, }); } @@ -293,8 +311,14 @@ export class ResultsView {
+
+ + +
+ title="Group results by file">Group
diff --git a/src/references.ts b/src/references.ts index 592ddae..4add2dc 100644 --- a/src/references.ts +++ b/src/references.ts @@ -402,6 +402,69 @@ export function invalidateSymbols(uri: vscode.Uri): void { symbolCache.delete(uri.toString()); } +// --------------------------------------------------------------------------- +// Test code +// --------------------------------------------------------------------------- + +/** + * Matches test projects and test files by the usual conventions: a `test`/`tests` + * directory, a project or file named `…Test`/`…Tests`, Go's `_test.go`, and + * `.spec.` / `.test.` files. + */ +const TEST_EXTENSIONS = '(?:cs|fs|vb|ts|tsx|js|jsx|py|go|rs|java|kt)'; + +export const DEFAULT_TEST_PATTERN = [ + // a test / tests directory anywhere in the path + '(?:^|[\\\\/])[Tt]ests?[\\\\/]', + // Tests.cs, foo_test.go, foo.test.ts — the name must *start* at a boundary, so + // "Latest.cs" and "Attestation.cs" are not caught by the "test" inside them + `(?:^|[\\\\/_.\\-])[Tt]ests?\\.${TEST_EXTENSIONS}$`, + // FooTests.cs — CamelCase, so only an uppercase T counts here + `[A-Za-z0-9]Tests?\\.${TEST_EXTENSIONS}$`, + // test_foo.py — pytest's default prefix convention + '(?:^|[\\\\/])[Tt]est_', + // foo.spec.ts + '\\.spec\\.(?:ts|tsx|js|jsx)$', + // a project named Tests or Something.Tests + '(?:^|\\.)[Tt]ests?$', +].join('|'); + +let cachedPattern: { source: string; regex: RegExp | undefined } | undefined; + +/** The configured test pattern, compiled once. Invalid regexes are reported and ignored. */ +export function testPattern(): RegExp | undefined { + const configured = vscode.workspace.getConfiguration('coloredReferences') + .get('testPattern', '') ?? ''; + // An unset setting means "use the built-in pattern"; to treat everything as + // production code, configure a pattern that cannot match. + const source = configured.trim().length > 0 ? configured : DEFAULT_TEST_PATTERN; + if (cachedPattern?.source === source) { + return cachedPattern.regex; + } + let regex: RegExp | undefined; + try { + regex = new RegExp(source); + } catch (error) { + void vscode.window.showWarningMessage( + `Colored References: coloredReferences.testPattern is not a valid regular expression (${error}).`); + } + cachedPattern = { source, regex }; + return regex; +} + +/** Whether a reference lives in test code, by path or by containing project name. */ +export function isTest( + uri: vscode.Uri, project: string | undefined, pattern = testPattern(), +): boolean { + if (!pattern) { + return false; + } + // Test both separators so one pattern works on either platform. + const relative = relativePath(uri); + return pattern.test(relative) || pattern.test(relative.replace(/\//g, '\\')) || + (project !== undefined && pattern.test(project)); +} + // --------------------------------------------------------------------------- // Read / write kind // --------------------------------------------------------------------------- diff --git a/src/test/suite/filters.test.ts b/src/test/suite/filters.test.ts new file mode 100644 index 0000000..8f1ad65 --- /dev/null +++ b/src/test/suite/filters.test.ts @@ -0,0 +1,105 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +import { buildRows } from '../../panel'; +import { DEFAULT_TEST_PATTERN, gather, isTest, testPattern } from '../../references'; + +const TARGET_FILE = path.join('Nerfed.Runtime', 'Profiler.cs'); +const TARGET_SYMBOL = 'Profiler'; +const TARGET_DECL = 'public static class Profiler'; + +const builtIn = new RegExp(DEFAULT_TEST_PATTERN); + +/** isTest against the built-in pattern, without touching configuration. */ +function classify(relativePath: string, project?: string): boolean { + // isTest() resolves the path through asRelativePath, so feed it an absolute path + // inside the workspace and let it come back out relative. + const folder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(folder, 'no workspace folder'); + const uri = vscode.Uri.joinPath(folder.uri, ...relativePath.split('/')); + return isTest(uri, project, builtIn); +} + +suite('Test-code detection', () => { + test('the built-in pattern matches the usual conventions', () => { + assert.ok(classify('tests/Foo.cs'), 'tests/ directory'); + assert.ok(classify('Nerfed.Runtime/Tests/Foo.cs'), 'nested Tests/ directory'); + assert.ok(classify('src/FooTests.cs'), 'FooTests.cs'); + assert.ok(classify('src/FooTest.cs'), 'FooTest.cs'); + assert.ok(classify('pkg/thing_test.go'), 'Go test file'); + assert.ok(classify('src/thing.spec.ts'), 'spec.ts'); + assert.ok(classify('src/thing.test.ts'), 'test.ts'); + assert.ok(classify('src/test_thing.py'), 'pytest prefix'); + }); + + test('a project named after tests counts even when the path does not', () => { + assert.ok(classify('src/Widget.cs', 'Nerfed.Tests'), 'project Nerfed.Tests'); + assert.ok(classify('src/Widget.cs', 'Tests'), 'project Tests'); + assert.ok(!classify('src/Widget.cs', 'Nerfed.Runtime'), 'production project'); + assert.ok(!classify('src/Widget.cs', 'Latest'), 'project ending in "test"'); + assert.ok(!classify('src/Widget.cs', 'Contest'), 'project containing "test"'); + }); + + test('production code is not mistaken for test code', () => { + assert.ok(!classify('Nerfed.Runtime/Profiler.cs')); + assert.ok(!classify('Nerfed.Runtime/Systems/LocalToWorldSystem.cs')); + // These all contain "test" without being test code — the boundary rules matter. + assert.ok(!classify('src/Latest.cs'), 'Latest.cs'); + assert.ok(!classify('src/Protest.cs'), 'Protest.cs'); + assert.ok(!classify('src/greatest.ts'), 'greatest.ts'); + assert.ok(!classify('src/contest/Entry.cs'), 'contest/ directory'); + assert.ok(!classify('src/Attestation.cs'), 'Attestation.cs'); + }); + + test('a configured pattern that cannot match disables detection', () => { + assert.ok(!isTest(vscode.Uri.file('/repo/tests/Foo.cs'), 'Tests', new RegExp('$^'))); + }); + + test('the default configuration resolves to the built-in pattern', () => { + const pattern = testPattern(); + assert.ok(pattern, 'no pattern compiled'); + assert.strictEqual(pattern.source, builtIn.source); + }); +}); + +suite('Project and test filtering data', () => { + test('rows expose the project and a test flag the panel can filter on', async () => { + const ext = vscode.extensions.getExtension('local.colored-references'); + assert.ok(ext); + await ext.activate(); + + const folder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(folder); + const uri = vscode.Uri.joinPath(folder.uri, ...TARGET_FILE.split(path.sep)); + const doc = await vscode.workspace.openTextDocument(uri); + + let position = new vscode.Position(0, 0); + for (let i = 0; i < doc.lineCount; i++) { + const at = doc.lineAt(i).text.indexOf(TARGET_DECL); + if (at >= 0) { + position = new vscode.Position(i, doc.lineAt(i).text.indexOf(TARGET_SYMBOL, at) + 1); + break; + } + } + const locations = await vscode.commands.executeCommand( + 'vscode.executeReferenceProvider', uri, position); + assert.ok(locations && locations.length > 5); + + const results = await gather( + TARGET_SYMBOL, 'csharp', { uri, position }, locations, true); + const { rows } = buildRows(results, new Map()); + assert.ok(rows.length > 0); + + // The Nerfed solution has no test projects, so nothing should be flagged — + // this is the false-positive check against real paths. + const flagged = rows.filter(r => r.isTest).map(r => r.relPath); + assert.deepStrictEqual([...new Set(flagged)], [], + `flagged as test code: ${[...new Set(flagged)].join(', ')}`); + + const projects = [...new Set(rows.map(r => r.project))].sort(); + console.log(`[test] projects in results: ${projects.join(', ')}`); + assert.ok(projects.length > 1, 'expected references in more than one project'); + assert.ok(projects.every(p => p.length > 0), 'every row should name a project'); + }); +}); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 0033e16..6ec99d9 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -14,6 +14,7 @@ export function run(): Promise { mocha.addFile(path.resolve(__dirname, 'panel.test.js')); mocha.addFile(path.resolve(__dirname, 'kinds.test.js')); mocha.addFile(path.resolve(__dirname, 'semantic.test.js')); + mocha.addFile(path.resolve(__dirname, 'filters.test.js')); return new Promise((resolve, reject) => { try {