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()
})
})