diff --git a/packages/ui/client/components/explorer/ExplorerItem.vue b/packages/ui/client/components/explorer/ExplorerItem.vue index a9cf324be..44f797e43 100644 --- a/packages/ui/client/components/explorer/ExplorerItem.vue +++ b/packages/ui/client/components/explorer/ExplorerItem.vue @@ -7,8 +7,9 @@ import { client, config, isReport, runFiles, runTask } from '~/composables/clien import { showTaskSource } from '~/composables/codemirror' import { explorerTree } from '~/composables/explorer' import { hasFailedSnapshot } from '~/composables/explorer/collector' -import { escapeHtml, highlightRegex } from '~/composables/explorer/state' +import { highlightRegex } from '~/composables/explorer/state' import { coverageEnabled, disableCoverage } from '~/composables/navigation' +import { escapeHtml } from '~/utils/escape' import { getProjectBadgeStyle } from '~/utils/task' import IconAction from '../IconAction.vue' import IconButton from '../IconButton.vue' @@ -124,13 +125,18 @@ const runButtonTitle = computed(() => { : 'Run current test' }) -const escapedName = computed(() => escapeHtml(name)) const highlighted = computed(() => { const regex = highlightRegex.value - const useName = escapedName.value - return regex - ? useName.replace(regex, (match) => `${match}`) - : useName + if (!regex) { + return escapeHtml(name) + } + // the capture group puts every match at an odd index + return name + .split(regex) + .map((part, i) => + i % 2 ? `${escapeHtml(part)}` : escapeHtml(part), + ) + .join('') }) const disableShowDetails = computed(() => type !== 'file' && disableTaskLocation) diff --git a/packages/ui/client/composables/explorer/state.ts b/packages/ui/client/composables/explorer/state.ts index b24cd73c9..e13948fac 100644 --- a/packages/ui/client/composables/explorer/state.ts +++ b/packages/ui/client/composables/explorer/state.ts @@ -13,6 +13,7 @@ import { availableProjects } from '~/composables/client' import { caseInsensitiveMatch } from '~/utils/task' // importing from the source because we need to bundle it import { createTagsFilter } from '../../../../vitest/src/runtime/runner/utils/tags' +import { escapeRegExp } from '../../../../vitest/src/utils/base' import { config } from '../client' import { explorerTree } from './index' @@ -83,19 +84,8 @@ function createSafeFilter(query: string) { return { matcher: () => false, error: error.message } } } -const htmlEntities: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', -} -export function escapeHtml(str: string) { - return str.replace(/[&<>"']/g, (m) => htmlEntities[m]) -} export const highlightRegex = computed(() => { - const searchString = search.value.toLowerCase() - return searchString.length ? new RegExp(`(${escapeHtml(searchString)})`, 'gi') : null + return search.value ? new RegExp(`(${escapeRegExp(search.value)})`, 'gi') : null }) export const isFiltered = computed(() => search.value.trim() !== '') export const filter = reactive({ diff --git a/test/ui/test/ui.spec.ts b/test/ui/test/ui.spec.ts index feffc0917..51cf006dd 100644 --- a/test/ui/test/ui.spec.ts +++ b/test/ui/test/ui.spec.ts @@ -773,6 +773,12 @@ async function testFilter(page: Page, options: { mode: 'ui' | 'static' }) { await page.getByPlaceholder('Search...').fill('char () - Square root of nine (9)') const testItem = getExplorerItem(page, 'char () - Square root of nine (9)') await expect(testItem).toBeVisible() + await expect(testItem.locator('.highlight')).toHaveText('char () - Square root of nine (9)') + + // a partial search with an unclosed parenthesis + await page.getByPlaceholder('Search...').fill('nine (9') + await expect(testItem).toBeVisible() + await expect(testItem.locator('.highlight')).toHaveText('nine (9') if (options.mode === 'ui') { await testItem.hover() await testItem.getByLabel('Run current test').click()