diff --git a/packages/ui/client/components/Navigation.vue b/packages/ui/client/components/Navigation.vue index 73353d9a6a13..f611851853dc 100644 --- a/packages/ui/client/components/Navigation.vue +++ b/packages/ui/client/components/Navigation.vue @@ -60,6 +60,7 @@ function expandTests() { v-tooltip.bottom="'Collapse tests'" title="Collapse tests" :disabled="!initialized" + data-testid="collapse-all" icon="i-carbon:collapse-all" @click="collapseTests()" /> @@ -68,6 +69,7 @@ function expandTests() { v-tooltip.bottom="'Expand tests'" :disabled="!initialized" title="Expand tests" + data-testid="expand-all" icon="i-carbon:expand-all" @click="expandTests()" /> diff --git a/packages/ui/client/components/explorer/ExplorerItem.vue b/packages/ui/client/components/explorer/ExplorerItem.vue index b2f008f7b73c..98e968003155 100644 --- a/packages/ui/client/components/explorer/ExplorerItem.vue +++ b/packages/ui/client/components/explorer/ExplorerItem.vue @@ -7,7 +7,7 @@ import { client, isReport, runFiles } from '~/composables/client' import { coverageEnabled } from '~/composables/navigation' import type { TaskTreeNodeType } from '~/composables/explorer/types' import { explorerTree } from '~/composables/explorer' -import { search } from '~/composables/explorer/state' +import { escapeHtml, highlightRegex } from '~/composables/explorer/state' import { showSource } from '~/composables/codemirror' // TODO: better handling of "opened" - it means to forcefully open the tree item and set in TasksList right now @@ -107,16 +107,13 @@ const gridStyles = computed(() => { } ${gridColumns.join(' ')};` }) -const highlightRegex = computed(() => { - const searchString = search.value.toLowerCase() - return searchString.length ? new RegExp(`(${searchString})`, 'gi') : null -}) - +const escapedName = computed(() => escapeHtml(name)) const highlighted = computed(() => { const regex = highlightRegex.value + const useName = escapedName.value return regex - ? name.replace(regex, match => `${match}`) - : name + ? useName.replace(regex, match => `${match}`) + : useName }) 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 c25ec78f2213..99f0bf1e2cb2 100644 --- a/packages/ui/client/composables/explorer/state.ts +++ b/packages/ui/client/composables/explorer/state.ts @@ -22,6 +22,20 @@ export const treeFilter = useLocalStorage( }, ) export const search = ref(treeFilter.value.search) +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 +}) export const isFiltered = computed(() => search.value.trim() !== '') export const filter = reactive({ failed: treeFilter.value.failed, diff --git a/test/ui/fixtures/task-name.test.ts b/test/ui/fixtures/task-name.test.ts new file mode 100644 index 000000000000..a6ec0754b057 --- /dev/null +++ b/test/ui/fixtures/task-name.test.ts @@ -0,0 +1,9 @@ +import { it, expect} from "vitest" + +it('', () => { + expect(true).toBe(true) +}) + +it('<>\'"', () => { + expect(true).toBe(true) +}) diff --git a/test/ui/package.json b/test/ui/package.json index f78c450b4f65..836151a1fbeb 100644 --- a/test/ui/package.json +++ b/test/ui/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "test-e2e": "GITHUB_ACTIONS=false playwright test", + "test-e2e-ui": "GITHUB_ACTIONS=false playwright test --ui", "test-fixtures": "vitest" }, "devDependencies": { diff --git a/test/ui/test/html-report.spec.ts b/test/ui/test/html-report.spec.ts index 4f9e22061d64..b715eac8877a 100644 --- a/test/ui/test/html-report.spec.ts +++ b/test/ui/test/html-report.spec.ts @@ -31,8 +31,8 @@ test.describe('html report', () => { await page.goto(pageUrl) - // dashbaord - await expect(page.locator('[aria-labelledby=tests]')).toContainText('6 Pass 1 Fail 7 Total') + // dashboard + await expect(page.locator('[aria-labelledby=tests]')).toContainText('8 Pass 1 Fail 9 Total') // unhandled errors await expect(page.getByTestId('unhandled-errors')).toContainText( diff --git a/test/ui/test/ui.spec.ts b/test/ui/test/ui.spec.ts index 592caf2f2478..e9585799c393 100644 --- a/test/ui/test/ui.spec.ts +++ b/test/ui/test/ui.spec.ts @@ -36,8 +36,8 @@ test.describe('ui', () => { await page.goto(pageUrl) - // dashbaord - await expect(page.locator('[aria-labelledby=tests]')).toContainText('6 Pass 1 Fail 7 Total') + // dashboard + await expect(page.locator('[aria-labelledby=tests]')).toContainText('8 Pass 1 Fail 9 Total') // unhandled errors await expect(page.getByTestId('unhandled-errors')).toContainText( @@ -96,7 +96,7 @@ test.describe('ui', () => { // match all files when no filter await page.getByPlaceholder('Search...').fill('') - await page.getByText('PASS (3)').click() + await page.getByText('PASS (4)').click() await expect(page.getByTestId('details-panel').getByText('fixtures/sample.test.ts', { exact: true })).toBeVisible() // match nothing @@ -122,5 +122,19 @@ test.describe('ui', () => { await page.getByText('PASS (1)').click() await expect(page.getByTestId('details-panel').getByText('fixtures/console.test.ts', { exact: true })).toBeVisible() await expect(page.getByTestId('details-panel').getByText('fixtures/sample.test.ts', { exact: true })).toBeHidden() + + // html entities in task names are escaped + await page.locator('span').filter({ hasText: /^Pass$/ }).click() + await page.getByPlaceholder('Search...').fill('') + // for some reason, the tree is collapsed by default: we need to click on the nav buttons to expand it + await page.getByTestId('collapse-all').click() + await page.getByTestId('expand-all').click() + await expect(page.getByText('')).toBeVisible() + await expect(page.getByTestId('details-panel').getByText('fixtures/task-name.test.ts', { exact: true })).toBeVisible() + + // html entities in task names are escaped + await page.getByPlaceholder('Search...').fill('<>\'"') + await expect(page.getByText('<>\'"')).toBeVisible() + await expect(page.getByTestId('details-panel').getByText('fixtures/task-name.test.ts', { exact: true })).toBeVisible() }) })