Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,9 @@ stats.html
stats-*.json
.wxt
web-ext.config.ts
coverage
playwright-report
test-results

# Editor directories and files
.vscode/*
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,6 +80,20 @@ Package extensions:
pnpm zip
```

## Testing

This project uses Vitest for unit/component tests and Playwright for a built-panel smoke test.
Comment on lines +83 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep localized READMEs in sync

The new Testing section is added only to README.md, while README.zh-CN.md still jumps from build/zip instructions directly to the tech stack. This violates the repository instruction in AGENTS.md that generated README changes must be present and synchronized across all language variants, so Chinese readers will not see the newly documented test commands or E2E behavior. I checked the only other README with find . -maxdepth 3 -iname '*readme*' -print.

Useful? React with 👍 / 👎.


```sh
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.

## Tech Stack

- [WXT](https://wxt.dev/) for browser extension development
Expand Down
14 changes: 14 additions & 0 deletions README.zh-CN.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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/) 用于浏览器扩展开发
Expand Down
37 changes: 7 additions & 30 deletions entrypoints/devtools-panel/hooks/useDevToolsPanel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ import { useClipboard } from '@vueuse/core'
import { ElMessage } from 'element-plus'
import { t } from '../lang'
import SM from '../message'
import { formatStyle, type FormatStyleValue } from '../utils'
import { compareStyles, formatStyle, type FormatStyleValue, getVisibleCssDiffs } from '../utils'

export function useDevToolsPanel() {
const inputValue = ref('')
Expand DownExpand Up@@ -83,40 +83,17 @@ export function useDevToolsPanel() {
function compareSelectedEl() {
const [{ style: styles1 = {} }, { style: styles2 = {} }] = selectedEl

const diffs: Array<CssDiffsType> = []

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<CssDiffsType>, 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' : ''
}
Expand Down
6 changes: 3 additions & 3 deletions entrypoints/devtools-panel/message.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,18 +13,18 @@ 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<SelectedElType>) {
const windows = await browser.windows.getAll({ populate: true });

(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,
)
}
Expand Down
50 changes: 50 additions & 0 deletions entrypoints/devtools-panel/utils/cssDiff.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
import type { CssDiffsType } from '../types'

export type CssStyleRecord = Record<string, string | null | undefined>

const MISSING_STYLE_VALUE = '未定义'

function normalizeStyleValue(value: string | null | undefined) {
return value || MISSING_STYLE_VALUE
}

export function compareStyles(
leftStyles: CssStyleRecord = {},
rightStyles: CssStyleRecord = {},
): Array<CssDiffsType> {
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<CssDiffsType>,
{
isAllProperty,
inputValue,
}: {
isAllProperty: boolean
inputValue: string
},
): Array<CssDiffsType> {
const source = isAllProperty
? cssDiffs
: cssDiffs.filter(css => css.isDiff)

return inputValue
? source.filter(css => css.property.includes(inputValue))
: [...source]
}
16 changes: 9 additions & 7 deletions entrypoints/devtools-panel/utils/formatStyle.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,24 @@ export interface FormatStyleValue {
tag: string
id?: string
class?: string
style?: Record<string, any>
style?: Record<string, string>
}

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<string, any> = {}
const outValue: Record<string, string> = {}

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

Expand Down
1 change: 1 addition & 0 deletions entrypoints/devtools-panel/utils/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
export * from './array'
export * from './cssDiff'
export * from './formatStyle'
11 changes: 11 additions & 0 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"
},
Expand All@@ -28,14 +33,20 @@
},
"devDependencies": {
"@antfu/eslint-config": "^3.12.0",
"@playwright/test": "^1.60.0",
"@tailwindcss/vite": "^4.3.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",
"conventional-changelog-cli": "^5.0.0",
"eslint": "^9.17.0",
"happy-dom": "^20.9.0",
"tailwindcss": "^4.3.0",
"typescript": "5.6.3",
"vitest": "^4.1.6",
"vue-tsc": "^2.1.10",
"wxt": "^0.20.26"
}
Expand Down
33 changes: 33 additions & 0 deletions playwright.config.ts
Original file line numberDiff line numberDiff line change
@@ -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',
},
})
Loading