[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(ui): escape html for console log view (#4724)

authored by

Hiroshi Ogawa and committed by
GitHub
(Dec 18, 2023, 2:21 PM +0100) e0dde6ab 26a9fb05

+74 -87
+16 -45
pnpm-lock.yaml
··· 1909 1909 '@playwright/test': 1910 1910 specifier: ^1.39.0 1911 1911 version: 1.39.0 1912 - execa: 1913 - specifier: ^6.1.0 1914 - version: 6.1.0 1912 + '@testing-library/dom': 1913 + specifier: ^9.3.3 1914 + version: 9.3.3 1915 + happy-dom: 1916 + specifier: latest 1917 + version: 12.10.3 1915 1918 vitest: 1916 1919 specifier: workspace:* 1917 1920 version: link:../../packages/vitest ··· 8386 8389 dependencies: 8387 8390 '@storybook/client-logger': 6.5.10 8388 8391 '@storybook/instrumenter': 6.5.10(react-dom@17.0.2)(react@17.0.2) 8389 - '@testing-library/dom': 8.17.1 8390 - '@testing-library/user-event': 13.5.0(@testing-library/dom@8.17.1) 8392 + '@testing-library/dom': 8.19.0 8393 + '@testing-library/user-event': 13.5.0(@testing-library/dom@8.19.0) 8391 8394 ts-dedent: 2.2.0 8392 8395 transitivePeerDependencies: 8393 8396 - react ··· 8640 8643 cypress: 13.6.0 8641 8644 dev: true 8642 8645 8643 - /@testing-library/dom@8.17.1: 8644 - resolution: {integrity: sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==} 8645 - engines: {node: '>=12'} 8646 - dependencies: 8647 - '@babel/code-frame': 7.22.13 8648 - '@babel/runtime': 7.23.2 8649 - '@types/aria-query': 4.2.2 8650 - aria-query: 5.3.0 8651 - chalk: 4.1.2 8652 - dom-accessibility-api: 0.5.16 8653 - lz-string: 1.4.4 8654 - pretty-format: 27.5.1 8655 - dev: true 8656 - 8657 8646 /@testing-library/dom@8.19.0: 8658 8647 resolution: {integrity: sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==} 8659 8648 engines: {node: '>=12'} ··· 8664 8653 aria-query: 5.3.0 8665 8654 chalk: 4.1.2 8666 8655 dom-accessibility-api: 0.5.16 8667 - lz-string: 1.5.0 8668 - pretty-format: 27.5.1 8669 - dev: true 8670 - 8671 - /@testing-library/dom@9.3.1: 8672 - resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} 8673 - engines: {node: '>=14'} 8674 - dependencies: 8675 - '@babel/code-frame': 7.22.13 8676 - '@babel/runtime': 7.23.2 8677 - '@types/aria-query': 5.0.1 8678 - aria-query: 5.1.3 8679 - chalk: 4.1.2 8680 - dom-accessibility-api: 0.5.16 8681 - lz-string: 1.5.0 8656 + lz-string: 1.4.4 8682 8657 pretty-format: 27.5.1 8683 8658 dev: true 8684 8659 ··· 8758 8733 react-dom: <18.0.0 8759 8734 dependencies: 8760 8735 '@babel/runtime': 7.18.9 8761 - '@testing-library/dom': 8.17.1 8736 + '@testing-library/dom': 8.19.0 8762 8737 '@types/react-dom': 17.0.17 8763 8738 react: 17.0.2 8764 8739 react-dom: 17.0.2(react@17.0.2) ··· 8772 8747 react-dom: ^18.0.0 8773 8748 dependencies: 8774 8749 '@babel/runtime': 7.18.9 8775 - '@testing-library/dom': 8.17.1 8750 + '@testing-library/dom': 8.19.0 8776 8751 '@types/react-dom': 18.0.6 8777 8752 react: 18.0.0 8778 8753 react-dom: 18.0.0(react@18.0.0) ··· 8786 8761 react-dom: ^18.0.0 8787 8762 dependencies: 8788 8763 '@babel/runtime': 7.18.9 8789 - '@testing-library/dom': 8.17.1 8764 + '@testing-library/dom': 8.19.0 8790 8765 '@types/react-dom': 18.0.6 8791 8766 react: 18.2.0 8792 8767 react-dom: 18.2.0(react@18.2.0) ··· 8800 8775 react-dom: ^18.0.0 8801 8776 dependencies: 8802 8777 '@babel/runtime': 7.18.9 8803 - '@testing-library/dom': 8.17.1 8778 + '@testing-library/dom': 8.19.0 8804 8779 '@types/react-dom': 18.0.8 8805 8780 react: 18.2.0 8806 8781 react-dom: 18.2.0(react@18.2.0) ··· 8826 8801 peerDependencies: 8827 8802 svelte: ^3 || ^4 8828 8803 dependencies: 8829 - '@testing-library/dom': 9.3.1 8804 + '@testing-library/dom': 9.3.3 8830 8805 svelte: 4.1.1 8831 8806 dev: true 8832 8807 8833 - /@testing-library/user-event@13.5.0(@testing-library/dom@8.17.1): 8808 + /@testing-library/user-event@13.5.0(@testing-library/dom@8.19.0): 8834 8809 resolution: {integrity: sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==} 8835 8810 engines: {node: '>=10', npm: '>=6'} 8836 8811 peerDependencies: 8837 8812 '@testing-library/dom': '>=7.21.4' 8838 8813 dependencies: 8839 8814 '@babel/runtime': 7.23.2 8840 - '@testing-library/dom': 8.17.1 8815 + '@testing-library/dom': 8.19.0 8841 8816 dev: true 8842 8817 8843 8818 /@testing-library/user-event@14.4.3(@testing-library/dom@9.3.3): ··· 8874 8849 8875 8850 /@types/aria-query@4.2.2: 8876 8851 resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} 8877 - dev: true 8878 - 8879 - /@types/aria-query@5.0.1: 8880 - resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} 8881 8852 dev: true 8882 8853 8883 8854 /@types/aria-query@5.0.3:
+2 -1
test/ui/package.json
··· 8 8 }, 9 9 "devDependencies": { 10 10 "@playwright/test": "^1.39.0", 11 - "execa": "^6.1.0", 11 + "@testing-library/dom": "^9.3.3", 12 + "happy-dom": "latest", 12 13 "vitest": "workspace:*" 13 14 } 14 15 }
+1
test/ui/vitest.config.ts
··· 3 3 export default defineConfig({ 4 4 test: { 5 5 dir: './fixtures', 6 + environment: 'happy-dom', 6 7 }, 7 8 })
+33
test/ui/fixtures/console.test.ts
··· 1 + /* eslint-disable no-console */ 2 + 3 + import { it } from "vitest"; 4 + import { prettyDOM } from "@testing-library/dom" 5 + 6 + // https://github.com/vitest-dev/vitest/issues/2765 7 + it('regexp', () => { 8 + console.log(/(?<char>\w)/) 9 + }) 10 + 11 + // https://github.com/vitest-dev/vitest/issues/3934 12 + it('html-raw', async () => { 13 + console.log(` 14 + <form> 15 + <label for="email">Email Address</label> 16 + <input name="email" /> 17 + <button>Submit</button> 18 + </form> 19 + `); 20 + }) 21 + 22 + // https://github.com/vitest-dev/vitest/issues/1279 23 + it('html-pretty', () => { 24 + const div = document.createElement("div"); 25 + div.innerHTML = ` 26 + <form> 27 + <label for="email">Email Address</label> 28 + <input name="email" /> 29 + <button>Submit</button> 30 + </form> 31 + `.replaceAll(/\n */gm, ""); // strip new liens 32 + console.log(prettyDOM(div)) 33 + })
+1 -1
test/ui/test/html-report.spec.ts
··· 32 32 await page.goto(pageUrl) 33 33 34 34 // dashbaord 35 - await expect(page.locator('[aria-labelledby=tests]')).toContainText('1 Pass 0 Fail 1 Total') 35 + await expect(page.locator('[aria-labelledby=tests]')).toContainText('4 Pass 0 Fail 4 Total') 36 36 37 37 // report 38 38 await page.getByText('sample.test.ts').click()
+8 -1
test/ui/test/ui.spec.ts
··· 23 23 await page.goto(pageUrl) 24 24 25 25 // dashbaord 26 - await expect(page.locator('[aria-labelledby=tests]')).toContainText('1 Pass 0 Fail 1 Total') 26 + await expect(page.locator('[aria-labelledby=tests]')).toContainText('4 Pass 0 Fail 4 Total') 27 27 28 28 // report 29 29 await page.getByText('sample.test.ts').click() ··· 39 39 await expect(page.getByTestId('console')).toContainText('log test') 40 40 41 41 expect(pageErrors).toEqual([]) 42 + }) 43 + 44 + test('console', async ({ page }) => { 45 + await page.goto(pageUrl) 46 + await page.getByText('fixtures/console.test.ts').click() 47 + await page.getByTestId('btn-console').click() 48 + await page.getByText('/(?<char>\\w)/').click() 42 49 }) 43 50 })
+8
packages/ui/client/utils/escape.ts
··· 1 + export function escapeHtml(unsafe: string) { 2 + return unsafe 3 + .replace(/&/g, '&amp;') 4 + .replace(/</g, '&lt;') 5 + .replace(/>/g, '&gt;') 6 + .replace(/"/g, '&quot;') 7 + .replace(/'/g, '&#39;') 8 + }
+3 -9
packages/ui/client/components/views/ViewConsoleOutput.vue
··· 3 3 import { client, currentLogs as logs } from '~/composables/client' 4 4 import { isDark } from '~/composables/dark' 5 5 import { createAnsiToHtmlFilter } from '~/composables/error' 6 + import { escapeHtml } from "~/utils/escape" 6 7 7 8 const formattedLogs = computed(() => { 8 9 const data = logs.value 9 10 if (data) { 10 11 const filter = createAnsiToHtmlFilter(isDark.value) 11 - return data.map(({ taskId, type, time, content }) => { 12 - const trimmed = content.trim() 13 - const value = filter.toHtml(trimmed) 14 - return value !== trimmed 15 - ? { taskId, type, time, html: true, content: value } 16 - : { taskId, type, time, html: false, content } 17 - }) 12 + return data.map(({ taskId, type, time, content }) => ({ taskId, type, time, content: filter.toHtml(escapeHtml(content)) })) 18 13 } 19 14 }) 20 15 ··· 26 21 27 22 <template> 28 23 <div v-if="formattedLogs?.length" h-full class="scrolls" flex flex-col data-testid="logs"> 29 - <div v-for="{ taskId, type, time, html, content } of formattedLogs" :key="taskId" font-mono> 24 + <div v-for="{ taskId, type, time, content } of formattedLogs" :key="taskId" font-mono> 30 25 <ViewConsoleOutputEntry 31 26 :task-name="getTaskName(taskId)" 32 27 :type="type" 33 28 :time="time" 34 29 :content="content" 35 - :html="html" 36 30 /> 37 31 </div> 38 32 </div>
-18
packages/ui/client/components/views/ViewConsoleOutputEntry.cy.tsx
··· 2 2 import ViewConsoleOutputEntry from './ViewConsoleOutputEntry.vue' 3 3 4 4 const htmlSelector = '[data-type=html]' 5 - const textSelector = '[data-type=text]' 6 5 7 6 describe('ViewConsoleOutputEntry', () => { 8 - it('test plain entry', () => { 9 - const content = new Date().toISOString() 10 - const container = cy.mount( 11 - <ViewConsoleOutputEntry 12 - task-name="test/text" 13 - type="stderr" 14 - time={Date.now()} 15 - html={false} 16 - content={content} 17 - />, 18 - ).get(textSelector) 19 - container.should('exist') 20 - container.invoke('text').then((t) => { 21 - expect(t, 'the message has the correct message').equals(content) 22 - }) 23 - }) 24 7 it('test html entry', () => { 25 8 const now = new Date().toISOString() 26 9 const content = new Filter().toHtml(`\x1B[33m${now}\x1B[0m`) ··· 29 12 task-name="test/html" 30 13 type="stderr" 31 14 time={Date.now()} 32 - html={true} 33 15 content={content} 34 16 />, 35 17 ).get(htmlSelector)
+1 -3
packages/ui/client/components/views/ViewConsoleOutputEntry.vue
··· 6 6 type: UserConsoleLog['type'] 7 7 time: UserConsoleLog['time'] 8 8 content: UserConsoleLog['content'] 9 - html: boolean 10 9 }>() 11 10 12 11 function formatTime(t: number) { ··· 22 21 > 23 22 {{ formatTime(time) }} | {{ taskName }} | {{ type }} 24 23 </div> 25 - <pre v-if="html" data-type="html" v-html="content" /> 26 - <pre v-else data-type="text" v-text="content" /> 24 + <pre data-type="html" v-html="content" /> 27 25 </div> 28 26 </template>
+1 -9
packages/ui/client/components/views/ViewReport.vue
··· 5 5 import { isDark } from '~/composables/dark' 6 6 import { createAnsiToHtmlFilter } from '~/composables/error' 7 7 import { config } from '~/composables/client' 8 + import { escapeHtml } from '~/utils/escape' 8 9 9 10 const props = defineProps<{ 10 11 file?: File ··· 22 23 return [{ ...task, level }] 23 24 else 24 25 return [{ ...task, level }, ...task.tasks.flatMap(t => collectFailed(t, level + 1))] 25 - } 26 - 27 - function escapeHtml(unsafe: string) { 28 - return unsafe 29 - .replace(/&/g, '&amp;') 30 - .replace(/</g, '&lt;') 31 - .replace(/>/g, '&gt;') 32 - .replace(/"/g, '&quot;') 33 - .replace(/'/g, '&#039;') 34 26 } 35 27 36 28 function createHtmlError(filter: Convert, error: ErrorWithDiff) {