From 437a0ba252b764a25cce93de8463f07024b1590e Mon Sep 17 00:00:00 2001 From: Jevin Date: Thu, 14 May 2026 17:41:51 +0800 Subject: [PATCH 1/2] test: add WXT test engineering --- .gitignore | 3 + README.md | 14 + .../devtools-panel/hooks/useDevToolsPanel.ts | 37 +- entrypoints/devtools-panel/message.ts | 6 +- entrypoints/devtools-panel/utils/cssDiff.ts | 50 ++ .../devtools-panel/utils/formatStyle.ts | 16 +- entrypoints/devtools-panel/utils/index.ts | 1 + package.json | 11 + playwright.config.ts | 33 ++ pnpm-lock.yaml | 508 +++++++++++++++++- tests/e2e/devtools-panel.spec.ts | 111 ++++ tests/setup.ts | 11 + tests/unit/array.test.ts | 12 + tests/unit/cssDiff.test.ts | 45 ++ tests/unit/devtoolsPanel.test.ts | 54 ++ tests/unit/formatStyle.test.ts | 40 ++ vitest.config.ts | 24 + 17 files changed, 932 insertions(+), 44 deletions(-) create mode 100644 entrypoints/devtools-panel/utils/cssDiff.ts create mode 100644 playwright.config.ts create mode 100644 tests/e2e/devtools-panel.spec.ts create mode 100644 tests/setup.ts create mode 100644 tests/unit/array.test.ts create mode 100644 tests/unit/cssDiff.test.ts create mode 100644 tests/unit/devtoolsPanel.test.ts create mode 100644 tests/unit/formatStyle.test.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 00b55f8..dbba7d6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ stats.html stats-*.json .wxt web-ext.config.ts +coverage +playwright-report +test-results # Editor directories and files .vscode/* diff --git a/README.md b/README.md index 7b0da8c..67daab0 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,17 @@ You need to download the zip file from my release and manually drag it into your # Inspiration + https://github.com/kdzwinel/CSS-Diff + +# Testing + +This project uses Vitest for unit/component tests and Playwright for a built-panel smoke test. + +```bash +pnpm test +pnpm test:coverage +pnpm test:e2e +``` + +- `pnpm test` runs fast Vitest tests for pure CSS diff utilities, DOM style formatting, and the Vue DevTools panel shell. +- `pnpm test:coverage` generates local coverage output under `coverage/`. +- `pnpm test:e2e` builds the Chrome extension first, then serves `.output/chrome-mv3` locally and verifies that the built DevTools panel renders. diff --git a/entrypoints/devtools-panel/hooks/useDevToolsPanel.ts b/entrypoints/devtools-panel/hooks/useDevToolsPanel.ts index 2fa26e0..46438a8 100644 --- a/entrypoints/devtools-panel/hooks/useDevToolsPanel.ts +++ b/entrypoints/devtools-panel/hooks/useDevToolsPanel.ts @@ -3,7 +3,7 @@ import { useClipboard } from '@vueuse/core' import { ElMessage } from 'element-plus' import { useI18n } from 'vue-i18n' import SM from '../message' -import { formatStyle, type FormatStyleValue } from '../utils' +import { compareStyles, formatStyle, type FormatStyleValue, getVisibleCssDiffs } from '../utils' export function useDevToolsPanel() { const { t } = useI18n() @@ -84,40 +84,17 @@ export function useDevToolsPanel() { function compareSelectedEl() { const [{ style: styles1 = {} }, { style: styles2 = {} }] = selectedEl - const diffs: Array = [] - - const allProperties = new Set([ - ...Object.keys(styles1), - ...Object.keys(styles2), - ]) - - allProperties.forEach((property) => { - const left = styles1[property] || '未定义' - const right = styles2[property] || '未定义' - - diffs.push({ - property, - left, - right, - isDiff: left !== right, - }) - }) - - cssDiffs.push(...diffs) + cssDiffs.length = 0 + cssDiffs.push(...compareStyles(styles1, styles2)) } const renderCssDiffs = computed(() => { - return inputValueFilter(isAllProperty.value - ? cssDiffs - : cssDiffs.filter(css => css.isDiff), inputValue.value) + return getVisibleCssDiffs(cssDiffs, { + isAllProperty: isAllProperty.value, + inputValue: inputValue.value, + }) }) - function inputValueFilter(cssDiffs: Array, inputValue: string) { - return !inputValue - ? cssDiffs - : cssDiffs.filter(c => c.property.includes(inputValue)) - } - function onTableCellClassName({ columnIndex }: { columnIndex: number }) { return !columnIndex ? 'text-[var(--el-table-text-color)] cursor-auto' : '' } diff --git a/entrypoints/devtools-panel/message.ts b/entrypoints/devtools-panel/message.ts index f4e138c..bab49e9 100644 --- a/entrypoints/devtools-panel/message.ts +++ b/entrypoints/devtools-panel/message.ts @@ -13,7 +13,7 @@ class SendMessage { private async init() { const [currentTab] = await browser.tabs.query({ active: true }) - this.currentTabId = currentTab.id! + this.currentTabId = currentTab?.id } public async send(data: Array) { @@ -21,10 +21,10 @@ class SendMessage { (windows as unknown as Array<_Window>).forEach((window) => { for (const tab of window.tabs) { - if (tab.id !== this.currentTabId) { + if (tab.id != null && tab.id !== this.currentTabId) { // Send selected data to other windows/tabs browser.tabs.sendMessage( - tab.id!, + tab.id, data, ) } diff --git a/entrypoints/devtools-panel/utils/cssDiff.ts b/entrypoints/devtools-panel/utils/cssDiff.ts new file mode 100644 index 0000000..1388d32 --- /dev/null +++ b/entrypoints/devtools-panel/utils/cssDiff.ts @@ -0,0 +1,50 @@ +import type { CssDiffsType } from '../types' + +export type CssStyleRecord = Record + +const MISSING_STYLE_VALUE = '未定义' + +function normalizeStyleValue(value: string | null | undefined) { + return value || MISSING_STYLE_VALUE +} + +export function compareStyles( + leftStyles: CssStyleRecord = {}, + rightStyles: CssStyleRecord = {}, +): Array { + const allProperties = new Set([ + ...Object.keys(leftStyles), + ...Object.keys(rightStyles), + ]) + + return Array.from(allProperties, (property) => { + const left = normalizeStyleValue(leftStyles[property]) + const right = normalizeStyleValue(rightStyles[property]) + + return { + property, + left, + right, + isDiff: left !== right, + } + }) +} + +export function getVisibleCssDiffs( + cssDiffs: Array, + { + isAllProperty, + inputValue, + }: { + isAllProperty: boolean + inputValue: string + }, +): Array { + const source = isAllProperty + ? cssDiffs + : cssDiffs.filter(css => css.isDiff) + + return inputValue + ? source.filter(css => css.property.includes(inputValue)) + : [...source] +} diff --git a/entrypoints/devtools-panel/utils/formatStyle.ts b/entrypoints/devtools-panel/utils/formatStyle.ts index bdb8f57..a6c0a0f 100644 --- a/entrypoints/devtools-panel/utils/formatStyle.ts +++ b/entrypoints/devtools-panel/utils/formatStyle.ts @@ -2,22 +2,24 @@ export interface FormatStyleValue { tag: string id?: string class?: string - style?: Record + style?: Record } -export function formatStyle(element: Element): FormatStyleValue | null { +export function formatStyle(element: Element | null): FormatStyleValue | null { if (!element) { return null } const styles = (element as Node)?.ownerDocument?.defaultView?.getComputedStyle(element) - const outValue: Record = {} + const outValue: Record = {} if (styles) { - for (let i = 0; i <= styles.length; i++) { - const StyleKey = styles[i] - const StyleValue = styles.getPropertyValue(StyleKey) - outValue[StyleKey] = StyleValue + for (let i = 0; i < styles.length; i++) { + const styleKey = styles[i] + + if (styleKey) { + outValue[styleKey] = styles.getPropertyValue(styleKey) + } } } diff --git a/entrypoints/devtools-panel/utils/index.ts b/entrypoints/devtools-panel/utils/index.ts index 0a952bd..f2739a6 100644 --- a/entrypoints/devtools-panel/utils/index.ts +++ b/entrypoints/devtools-panel/utils/index.ts @@ -1,2 +1,3 @@ export * from './array' +export * from './cssDiff' export * from './formatStyle' diff --git a/package.json b/package.json index 9a90329..776489d 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,11 @@ "zip:firefox": "wxt zip -b firefox --mv3", "zip:edge": "wxt zip -b edge", "compile": "vue-tsc --noEmit", + "test": "pnpm test:unit", + "test:unit": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "pnpm build:chrome && playwright test", "postinstall": "wxt prepare", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" }, @@ -29,15 +34,21 @@ }, "devDependencies": { "@antfu/eslint-config": "^3.12.0", + "@playwright/test": "^1.60.0", "@types/chrome": "^0.0.280", + "@vitejs/plugin-vue": "^6.0.6", + "@vitest/coverage-v8": "^4.1.6", + "@vue/test-utils": "^2.4.10", "@vueuse/core": "^12.0.0", "@wxt-dev/module-vue": "^1.0.3", "autoprefixer": "^10.4.20", "conventional-changelog-cli": "^5.0.0", "eslint": "^9.17.0", + "happy-dom": "^20.9.0", "postcss": "^8.4.49", "tailwindcss": "^3.4.17", "typescript": "5.6.3", + "vitest": "^4.1.6", "vue-tsc": "^2.1.10", "wxt": "^0.20.26" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..6c2ec8f --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,33 @@ +import { existsSync } from 'node:fs' +import process from 'node:process' +import { defineConfig } from '@playwright/test' + +const chromiumExecutablePath = [ + process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, + 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', + 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', + 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', + 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', + '/usr/bin/google-chrome', + '/usr/bin/chromium', + '/usr/bin/chromium-browser', +].find((value): value is string => Boolean(value && existsSync(value))) + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 60_000, + expect: { + timeout: 5_000, + }, + fullyParallel: false, + reporter: 'list', + use: { + browserName: 'chromium', + launchOptions: chromiumExecutablePath + ? { executablePath: chromiumExecutablePath } + : undefined, + trace: 'retain-on-failure', + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c016e50..651e06a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,10 +23,22 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^3.12.0 - version: 3.12.0(@typescript-eslint/utils@8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3))(@vue/compiler-sfc@3.5.13)(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3) + version: 3.12.0(@typescript-eslint/utils@8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3))(@vue/compiler-sfc@3.5.13)(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3)(vitest@4.1.6) + '@playwright/test': + specifier: ^1.60.0 + version: 1.60.0 '@types/chrome': specifier: ^0.0.280 version: 0.0.280 + '@vitejs/plugin-vue': + specifier: ^6.0.6 + version: 6.0.6(vite@8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1))(vue@3.5.13(typescript@5.6.3)) + '@vitest/coverage-v8': + specifier: ^4.1.6 + version: 4.1.6(vitest@4.1.6) + '@vue/test-utils': + specifier: ^2.4.10 + version: 2.4.10(@vue/compiler-dom@3.5.13)(@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) '@vueuse/core': specifier: ^12.0.0 version: 12.0.0(typescript@5.6.3) @@ -42,6 +54,9 @@ importers: eslint: specifier: ^9.17.0 version: 9.17.0(jiti@2.7.0) + happy-dom: + specifier: ^20.9.0 + version: 20.9.0 postcss: specifier: ^8.4.49 version: 8.4.49 @@ -51,6 +66,9 @@ importers: typescript: specifier: 5.6.3 version: 5.6.3 + vitest: + specifier: ^4.1.6 + version: 4.1.6(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1)) vue-tsc: specifier: ^2.1.10 version: 2.1.10(typescript@5.6.3) @@ -171,6 +189,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@clack/core@0.3.5': resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} @@ -497,9 +519,15 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -518,6 +546,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@oxc-project/types@0.129.0': resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==} @@ -529,6 +560,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -740,6 +776,9 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@stylistic/eslint-plugin@2.12.1': resolution: {integrity: sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -752,12 +791,18 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/chrome@0.0.280': resolution: {integrity: sha512-AotSmZrL9bcZDDmSI1D9dE7PGbhOur5L0cKxXd7IqbVizQWCY4gcvupPUVsQ4FfDj3V2tt/iOpomT9EY0s+w1g==} '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} @@ -809,6 +854,12 @@ packages: '@types/web-bluetooth@0.0.20': resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -866,6 +917,15 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 + '@vitest/coverage-v8@4.1.6': + resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==} + peerDependencies: + '@vitest/browser': 4.1.6 + vitest: 4.1.6 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/eslint-plugin@1.1.18': resolution: {integrity: sha512-pcnR0hn4KRaWqdEXdZPXLs2wAxzMBD8dKkpVkOuhT+bOOGW1/4BdiUrU3I0LW2loskfVS/XldwcO/lCEoFDyZw==} peerDependencies: @@ -879,6 +939,35 @@ packages: vitest: optional: true + '@vitest/expect@4.1.6': + resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} + + '@vitest/mocker@4.1.6': + resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.6': + resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + + '@vitest/runner@4.1.6': + resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} + + '@vitest/snapshot@4.1.6': + resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} + + '@vitest/spy@4.1.6': + resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + + '@vitest/utils@4.1.6': + resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} + '@volar/language-core@2.4.11': resolution: {integrity: sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==} @@ -931,6 +1020,16 @@ packages: '@vue/shared@3.5.13': resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + '@vue/test-utils@2.4.10': + resolution: {integrity: sha512-SmoZ5EA1kYiAFs9NkYdiFFQF+cSnUwnvlYEbY+DogWQZUiqOm/Y29eSbc5T6yi75SgSF9863SBeXniIEoPajCA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + '@vueuse/core@12.0.0': resolution: {integrity: sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==} @@ -969,6 +1068,10 @@ packages: '@wxt-dev/storage@1.0.1': resolution: {integrity: sha512-05fzQrr4z0WhdJ0rMQGmjCNTWAC7vrCtvlmu6ZWAKbxdc7k0+T9ui5qPIdF+PxcKPRavIY6ebowBy5KV9lRY0w==} + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1043,6 +1146,13 @@ packages: resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} engines: {node: '>=12'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} @@ -1167,6 +1277,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1259,6 +1373,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@2.9.0: resolution: {integrity: sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==} engines: {node: '>= 0.6.x'} @@ -1369,6 +1487,9 @@ packages: engines: {node: '>=18'} hasBin: true + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-js-compat@3.39.0: resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} @@ -1549,6 +1670,11 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + electron-to-chromium@1.5.74: resolution: {integrity: sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==} @@ -1830,6 +1956,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -1925,6 +2055,11 @@ packages: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2032,6 +2167,10 @@ packages: engines: {node: '>=0.4.7'} hasBin: true + happy-dom@20.9.0: + resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} + engines: {node: '>=20.0.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2057,6 +2196,9 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -2253,6 +2395,18 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -2264,6 +2418,18 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2491,9 +2657,16 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.3: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -2747,6 +2920,11 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -2988,6 +3166,16 @@ packages: pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -3254,6 +3442,9 @@ packages: shellwords@0.1.1: resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3321,6 +3512,12 @@ packages: stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stdin-discarder@0.1.0: resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3444,6 +3641,9 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} @@ -3455,6 +3655,10 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -3632,9 +3836,53 @@ packages: yaml: optional: true + vitest@4.1.6: + resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.6 + '@vitest/browser-preview': 4.1.6 + '@vitest/browser-webdriverio': 4.1.6 + '@vitest/coverage-istanbul': 4.1.6 + '@vitest/coverage-v8': 4.1.6 + '@vitest/ui': 4.1.6 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vue-component-type-helpers@3.2.9: + resolution: {integrity: sha512-S3BiWYaLSzHxTpln665ELSrMR9UYmrIDUmhik7nVZxmJjTKL2/a+ew1hvGxksKelivm0ujjWfG1fYOiU/2e8rA==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -3686,6 +3934,10 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} @@ -3701,6 +3953,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@5.0.0: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} @@ -3730,6 +3987,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.3.1: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} @@ -3826,7 +4095,7 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@antfu/eslint-config@3.12.0(@typescript-eslint/utils@8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3))(@vue/compiler-sfc@3.5.13)(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3)': + '@antfu/eslint-config@3.12.0(@typescript-eslint/utils@8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3))(@vue/compiler-sfc@3.5.13)(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3)(vitest@4.1.6)': dependencies: '@antfu/install-pkg': 0.5.0 '@clack/prompts': 0.8.2 @@ -3835,7 +4104,7 @@ snapshots: '@stylistic/eslint-plugin': 2.12.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3) '@typescript-eslint/eslint-plugin': 8.18.1(@typescript-eslint/parser@8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3))(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3) '@typescript-eslint/parser': 8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3) - '@vitest/eslint-plugin': 1.1.18(@typescript-eslint/utils@8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3))(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3) + '@vitest/eslint-plugin': 1.1.18(@typescript-eslint/utils@8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3))(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3)(vitest@4.1.6) eslint: 9.17.0(jiti@2.7.0) eslint-config-flat-gitignore: 0.3.0(eslint@9.17.0(jiti@2.7.0)) eslint-flat-config-utils: 0.4.0 @@ -3913,6 +4182,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} + '@clack/core@0.3.5': dependencies: picocolors: 1.1.1 @@ -4173,11 +4444,18 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -4197,6 +4475,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@one-ini/wasm@0.1.1': {} + '@oxc-project/types@0.129.0': {} '@pkgjs/parseargs@0.11.0': @@ -4204,6 +4484,10 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.60.0': + dependencies: + playwright: 1.60.0 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -4334,6 +4618,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.28.1': optional: true + '@standard-schema/spec@1.1.0': {} + '@stylistic/eslint-plugin@2.12.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3)': dependencies: '@typescript-eslint/utils': 8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3) @@ -4353,6 +4639,11 @@ snapshots: tslib: 2.8.1 optional: true + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/chrome@0.0.280': dependencies: '@types/filesystem': 0.0.36 @@ -4362,6 +4653,8 @@ snapshots: dependencies: '@types/ms': 0.7.34 + '@types/deep-eql@4.0.2': {} + '@types/doctrine@0.0.9': {} '@types/estree@1.0.6': {} @@ -4404,6 +4697,12 @@ snapshots: '@types/web-bluetooth@0.0.20': {} + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.10.2 + '@types/yauzl@2.10.3': dependencies: '@types/node': 22.10.2 @@ -4492,12 +4791,68 @@ snapshots: vite: 8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1) vue: 3.5.13(typescript@5.6.3) - '@vitest/eslint-plugin@1.1.18(@typescript-eslint/utils@8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3))(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3)': + '@vitest/coverage-v8@4.1.6(vitest@4.1.6)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.6 + ast-v8-to-istanbul: 1.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.6(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1)) + + '@vitest/eslint-plugin@1.1.18(@typescript-eslint/utils@8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3))(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3)(vitest@4.1.6)': dependencies: '@typescript-eslint/utils': 8.18.1(eslint@9.17.0(jiti@2.7.0))(typescript@5.6.3) eslint: 9.17.0(jiti@2.7.0) optionalDependencies: typescript: 5.6.3 + vitest: 4.1.6(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1)) + + '@vitest/expect@4.1.6': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.6(vite@8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1))': + dependencies: + '@vitest/spy': 4.1.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1) + + '@vitest/pretty-format@4.1.6': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.6': + dependencies: + '@vitest/utils': 4.1.6 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + '@vitest/utils': 4.1.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.6': {} + + '@vitest/utils@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@volar/language-core@2.4.11': dependencies: @@ -4585,6 +4940,15 @@ snapshots: '@vue/shared@3.5.13': {} + '@vue/test-utils@2.4.10(@vue/compiler-dom@3.5.13)(@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@vue/compiler-dom': 3.5.13 + js-beautify: 1.15.4 + vue: 3.5.13(typescript@5.6.3) + vue-component-type-helpers: 3.2.9 + optionalDependencies: + '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.6.3)) + '@vueuse/core@12.0.0(typescript@5.6.3)': dependencies: '@types/web-bluetooth': 0.0.20 @@ -4649,6 +5013,8 @@ snapshots: async-mutex: 0.5.0 dequal: 2.0.3 + abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -4705,6 +5071,14 @@ snapshots: array-union@3.0.1: {} + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + async-mutex@0.5.0: dependencies: tslib: 2.8.1 @@ -4833,6 +5207,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4931,6 +5307,8 @@ snapshots: colorette@2.0.20: {} + commander@10.0.1: {} + commander@2.9.0: dependencies: graceful-readlink: 1.0.1 @@ -5053,6 +5431,8 @@ snapshots: dependencies: meow: 13.2.0 + convert-source-map@2.0.0: {} + core-js-compat@3.39.0: dependencies: browserslist: 4.24.3 @@ -5204,6 +5584,13 @@ snapshots: eastasianwidth@0.2.0: {} + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.5 + semver: 7.6.3 + electron-to-chromium@1.5.74: {} element-plus@2.9.1(vue@3.5.13(typescript@5.6.3)): @@ -5628,6 +6015,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + expect-type@1.3.0: {} + exsolve@1.0.8: {} extract-zip@2.0.1: @@ -5724,6 +6113,9 @@ snapshots: dependencies: minipass: 3.3.6 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5835,6 +6227,18 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 + happy-dom@20.9.0: + dependencies: + '@types/node': 22.10.2 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + has-flag@4.0.0: {} hasown@2.0.2: @@ -5853,6 +6257,8 @@ snapshots: dependencies: lru-cache: 10.4.3 + html-escaper@2.0.2: {} + html-escaper@3.0.3: {} htmlparser2@10.1.0: @@ -5986,6 +6392,19 @@ snapshots: isobject@3.0.1: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -5996,6 +6415,18 @@ snapshots: jiti@2.7.0: {} + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -6194,12 +6625,20 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.3: dependencies: '@babel/parser': 7.29.3 '@babel/types': 7.29.0 source-map-js: 1.2.1 + make-dir@4.0.0: + dependencies: + semver: 7.6.3 + make-error@1.3.6: {} many-keys-map@2.0.1: {} @@ -6604,6 +7043,10 @@ snapshots: node-releases@2.0.19: {} + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -6867,6 +7310,14 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + playwright-core@1.60.0: {} + + playwright@1.60.0: + dependencies: + playwright-core: 1.60.0 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} postcss-import@15.1.0(postcss@8.4.49): @@ -7176,6 +7627,8 @@ snapshots: shellwords@0.1.1: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -7241,6 +7694,10 @@ snapshots: stable-hash@0.0.4: {} + stackback@0.0.2: {} + + std-env@4.1.0: {} + stdin-discarder@0.1.0: dependencies: bl: 5.1.0 @@ -7388,6 +7845,8 @@ snapshots: through@2.3.8: {} + tinybench@2.9.0: {} + tinyexec@0.3.1: {} tinyexec@1.1.2: {} @@ -7397,6 +7856,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyrainbow@3.1.0: {} + titleize@3.0.0: {} tmp@0.2.5: {} @@ -7559,8 +8020,38 @@ snapshots: jiti: 2.7.0 yaml: 2.6.1 + vitest@4.1.6(@vitest/coverage-v8@4.1.6)(happy-dom@20.9.0)(vite@8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1)): + dependencies: + '@vitest/expect': 4.1.6 + '@vitest/mocker': 4.1.6(vite@8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1)) + '@vitest/pretty-format': 4.1.6 + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.12(esbuild@0.27.7)(jiti@2.7.0)(yaml@2.6.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@vitest/coverage-v8': 4.1.6(vitest@4.1.6) + happy-dom: 20.9.0 + transitivePeerDependencies: + - msw + vscode-uri@3.0.8: {} + vue-component-type-helpers@3.2.9: {} + vue-demi@0.14.10(vue@3.5.13(typescript@5.6.3)): dependencies: vue: 3.5.13(typescript@5.6.3) @@ -7638,6 +8129,8 @@ snapshots: webpack-virtual-modules@0.6.2: {} + whatwg-mimetype@3.0.0: {} + when-exit@2.1.5: {} when@3.7.7: {} @@ -7651,6 +8144,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@5.0.0: dependencies: string-width: 7.2.0 @@ -7681,6 +8179,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.20.1: {} + wsl-utils@0.3.1: dependencies: is-wsl: 3.1.1 diff --git a/tests/e2e/devtools-panel.spec.ts b/tests/e2e/devtools-panel.spec.ts new file mode 100644 index 0000000..4ae7aa7 --- /dev/null +++ b/tests/e2e/devtools-panel.spec.ts @@ -0,0 +1,111 @@ +import type { AddressInfo } from 'node:net' +import { createReadStream, existsSync } from 'node:fs' +import { createServer } from 'node:http' +import path from 'node:path' +import { expect, test } from '@playwright/test' + +const outputDir = path.resolve(process.cwd(), '.output/chrome-mv3') +const panelPath = path.join(outputDir, 'devtools-panel.html') + +async function serveOutputDir() { + const server = createServer((request, response) => { + const url = new URL(request.url ?? '/', 'http://localhost') + const relativePath = url.pathname === '/' + ? 'devtools-panel.html' + : decodeURIComponent(url.pathname.slice(1)) + const filePath = path.resolve(outputDir, relativePath) + + const relativeToOutput = path.relative(outputDir, filePath) + + if (relativeToOutput.startsWith('..') || path.isAbsolute(relativeToOutput) || !existsSync(filePath)) { + response.writeHead(404) + response.end() + return + } + + if (filePath.endsWith('.js')) { + response.setHeader('Content-Type', 'text/javascript') + } + else if (filePath.endsWith('.css')) { + response.setHeader('Content-Type', 'text/css') + } + else if (filePath.endsWith('.html')) { + response.setHeader('Content-Type', 'text/html') + } + + createReadStream(filePath).pipe(response) + }) + + await new Promise((resolve) => { + server.listen(0, '127.0.0.1', resolve) + }) + + const { port } = server.address() as AddressInfo + + return { + url: `http://127.0.0.1:${port}/devtools-panel.html`, + close: () => new Promise((resolve, reject) => { + server.close((error?: Error) => { + if (error) { + reject(error) + } + else { + resolve() + } + }) + }), + } +} + +test('renders the built DevTools panel shell', async ({ page }) => { + test.skip(!existsSync(panelPath), 'Run `pnpm build:chrome` before this E2E test.') + const server = await serveOutputDir() + + try { + await page.addInitScript(() => { + const extensionApi = { + runtime: { + id: 'test-extension', + getURL: (value = '') => value, + onMessage: { + addListener() {}, + }, + sendMessage: async () => undefined, + }, + tabs: { + query: async () => [{ id: 1, active: true }], + sendMessage: async () => undefined, + }, + windows: { + getAll: async () => [], + }, + devtools: { + panels: { + elements: { + onSelectionChanged: { + addListener() {}, + }, + }, + }, + inspectedWindow: { + eval(_expression: string, callback: (result: unknown, isException?: boolean) => void) { + callback(null, false) + }, + }, + }, + } + + ;(window as any).browser = extensionApi + ;(window as any).chrome = extensionApi + }) + + await page.goto(server.url) + + await expect(page.getByRole('heading', { name: 'DOM Diff' })).toBeVisible() + await expect(page.getByText('Select two elements in the Elements tab')).toBeVisible() + await expect(page.getByText('Please select two elements to compare.')).toBeVisible() + } + finally { + await server.close() + } +}) diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..0c2957b --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,11 @@ +import { afterEach, beforeEach, vi } from 'vitest' +import { fakeBrowser } from 'wxt/testing' + +beforeEach(() => { + fakeBrowser.reset() +}) + +afterEach(() => { + vi.restoreAllMocks() + document.body.innerHTML = '' +}) diff --git a/tests/unit/array.test.ts b/tests/unit/array.test.ts new file mode 100644 index 0000000..f459fa8 --- /dev/null +++ b/tests/unit/array.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest' +import { filterJoin } from '../../entrypoints/devtools-panel/utils/array' + +describe('filterJoin', () => { + it('joins only truthy values with the table header separator', () => { + expect(filterJoin('DIV', '', undefined, null, 'primary')).toBe('DIV $$ primary') + }) + + it('returns an empty string when all values are absent', () => { + expect(filterJoin('', undefined, false, null)).toBe('') + }) +}) diff --git a/tests/unit/cssDiff.test.ts b/tests/unit/cssDiff.test.ts new file mode 100644 index 0000000..57ac761 --- /dev/null +++ b/tests/unit/cssDiff.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { compareStyles, getVisibleCssDiffs } from '../../entrypoints/devtools-panel/utils/cssDiff' + +describe('compareStyles', () => { + it('marks equal and different CSS properties across the two selections', () => { + expect(compareStyles( + { color: 'red', display: 'block' }, + { color: 'red', display: 'inline' }, + )).toEqual([ + { property: 'color', left: 'red', right: 'red', isDiff: false }, + { property: 'display', left: 'block', right: 'inline', isDiff: true }, + ]) + }) + + it('keeps properties that only exist on one side and labels the missing side', () => { + expect(compareStyles( + { color: 'red' }, + { margin: '4px' }, + )).toEqual([ + { property: 'color', left: 'red', right: '未定义', isDiff: true }, + { property: 'margin', left: '未定义', right: '4px', isDiff: true }, + ]) + }) +}) + +describe('getVisibleCssDiffs', () => { + const diffs = [ + { property: 'color', left: 'red', right: 'blue', isDiff: true }, + { property: 'display', left: 'block', right: 'block', isDiff: false }, + { property: 'background-color', left: 'red', right: 'green', isDiff: true }, + ] + + it('returns only changed properties by default', () => { + expect(getVisibleCssDiffs(diffs, { isAllProperty: false, inputValue: '' })).toEqual([ + diffs[0], + diffs[2], + ]) + }) + + it('can include unchanged properties and filter by property name', () => { + expect(getVisibleCssDiffs(diffs, { isAllProperty: true, inputValue: 'display' })).toEqual([ + diffs[1], + ]) + }) +}) diff --git a/tests/unit/devtoolsPanel.test.ts b/tests/unit/devtoolsPanel.test.ts new file mode 100644 index 0000000..f2713eb --- /dev/null +++ b/tests/unit/devtoolsPanel.test.ts @@ -0,0 +1,54 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import { createI18n } from 'vue-i18n' +import { fakeBrowser } from 'wxt/testing' +import messages from '../../entrypoints/devtools-panel/lang' + +describe('devtools-panel', () => { + it('renders the initial comparison panel shell', async () => { + vi.resetModules() + await fakeBrowser.tabs.create({ active: true, url: 'https://example.com' }) + + ;(globalThis as any).browser.devtools = { + panels: { + elements: { + onSelectionChanged: { + addListener: vi.fn(), + }, + }, + }, + inspectedWindow: { + eval: vi.fn(), + }, + } + + const { default: DevtoolsPanel } = await import('../../entrypoints/devtools-panel/devtools-panel.vue') + const i18n = createI18n({ + locale: 'en', + messages, + }) + + const wrapper = mount(DevtoolsPanel, { + global: { + plugins: [i18n], + stubs: { + ElBacktop: true, + ElButton: { template: '' }, + ElCheckbox: true, + ElIcon: { template: '' }, + ElInput: true, + ElOption: true, + ElSelect: { template: '' }, + ElTable: { template: '
' }, + ElTableColumn: true, + ElText: { template: '

' }, + ElTooltip: { template: '' }, + }, + }, + }) + + expect(wrapper.text()).toContain('DOM Diff') + expect(wrapper.text()).toContain('Select two elements in the Elements tab') + expect(wrapper.text()).toContain('Please select two elements to compare.') + }) +}) diff --git a/tests/unit/formatStyle.test.ts b/tests/unit/formatStyle.test.ts new file mode 100644 index 0000000..298783f --- /dev/null +++ b/tests/unit/formatStyle.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from 'vitest' +import { formatStyle } from '../../entrypoints/devtools-panel/utils/formatStyle' + +describe('formatStyle', () => { + it('returns null for an empty selected element', () => { + expect(formatStyle(null as unknown as Element)).toBeNull() + }) + + it('returns element identity and computed style values', () => { + document.body.innerHTML = '' + const button = document.querySelector('button')! + + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + 0: 'display', + 1: 'color', + length: 2, + getPropertyValue: vi.fn((property: string) => { + if (property === 'display') { + return 'block' + } + + if (property === 'color') { + return 'rgb(255, 0, 0)' + } + + return 'unexpected' + }), + } as unknown as CSSStyleDeclaration) + + expect(formatStyle(button)).toEqual({ + tag: 'BUTTON', + id: 'save', + class: 'primary', + style: { + display: 'block', + color: 'rgb(255, 0, 0)', + }, + }) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..dbe2cc1 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,24 @@ +import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vitest/config' +import { WxtVitest } from 'wxt/testing/vitest-plugin' + +export default defineConfig({ + plugins: [vue(), WxtVitest()], + test: { + environment: 'happy-dom', + globals: true, + setupFiles: ['./tests/setup.ts'], + include: ['tests/unit/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'html'], + include: ['entrypoints/**/*.{ts,vue}'], + exclude: [ + 'entrypoints/**/*.html', + 'entrypoints/**/main.ts', + 'entrypoints/**/lang.ts', + 'entrypoints/**/types.ts', + ], + }, + }, +}) From 9260a84c86c0180cdf889ca2f82628041866b5fc Mon Sep 17 00:00:00 2001 From: Jevin Date: Thu, 14 May 2026 18:03:08 +0800 Subject: [PATCH 2/2] docs: sync Chinese README testing section --- README.zh-CN.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.zh-CN.md b/README.zh-CN.md index d7f7385..e02cc65 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -80,6 +80,20 @@ pnpm build:edge pnpm zip ``` +## 测试 + +本项目使用 Vitest 进行单元/组件测试,并使用 Playwright 对构建后的面板做冒烟测试。 + +```sh +pnpm test +pnpm test:coverage +pnpm test:e2e +``` + +- `pnpm test` 会运行快速 Vitest 测试,覆盖 CSS diff 纯工具、DOM 样式格式化和 Vue DevTools 面板外壳。 +- `pnpm test:coverage` 会在本地 `coverage/` 目录下生成覆盖率报告。 +- `pnpm test:e2e` 会先构建 Chrome 扩展,然后在本地服务 `.output/chrome-mv3` 并验证构建后的 DevTools 面板可以正常渲染。 + ## 技术栈 - [WXT](https://wxt.dev/) 用于浏览器扩展开发