docs/testing/e2e-selector-strategy.md
// ❌ 问题 1:依赖文本
page.getByText(/Original Prompt|原始提示词|原始提示/i)
// ❌ 问题 2:可能匹配多个按钮
page.getByRole('button', { name: /^(分析|Analyze)$/i })
// ❌ 问题 3:XPath 脆弱
title.locator('xpath=ancestor::*[contains(@class,"n-card")][1]')
data-testid 属性为关键 UI 元素添加 data-testid 属性,提供稳定、语言无关的定位标识。
data-testid命名规范:
data-testid="{模式}-{功能}-{元素类型}"
示例:
basic-system-input-panel - Basic System 模式的输入面板basic-system-analyze-button - Basic System 模式的分析按钮basic-user-analyze-button - Basic User 模式的分析按钮pro-multi-message-list - Pro Multi 模式的消息列表evaluation-score-badge - 评估分数徽章(通用)在 InputPanel.vue 的关键按钮上添加 data-testid:
<template>
<NSpace vertical :size="16" :data-testid="testIdPrefix + '-input-panel'">
<!-- 标题区域 -->
<NFlex justify="space-between" align="center" :wrap="false">
<NText :data-testid="testIdPrefix + '-input-label'">{{ label }}</NText>
<!-- AI提取变量按钮 -->
<NButton
v-if="enableVariableExtraction && showExtractButton"
:data-testid="testIdPrefix + '-extract-variables-button'"
@click="$emit('extract-variables')"
>
...
</NButton>
<!-- 预览按钮 -->
<NButton
v-if="showPreview"
:data-testid="testIdPrefix + '-preview-button'"
@click="$emit('open-preview')"
>
...
</NButton>
</NFlex>
<!-- 输入框 -->
<VariableAwareInput
v-if="enableVariableExtraction"
:data-testid="testIdPrefix + '-input'"
...
/>
<NInput
v-else
:data-testid="testIdPrefix + '-input'"
...
/>
<!-- 操作按钮区域 -->
<NSpace>
<!-- 分析按钮 -->
<NButton
v-if="showAnalyzeButton"
:data-testid="testIdPrefix + '-analyze-button'"
@click="$emit('analyze')"
:loading="analyzeLoading"
>
{{ $t('promptOptimizer.analyze') }}
</NButton>
<!-- 优化按钮 -->
<NButton
:data-testid="testIdPrefix + '-optimize-button'"
@click="$emit('optimize')"
:loading="optimizeLoading"
>
{{ $t('promptOptimizer.optimize') }}
</NButton>
</NSpace>
</NSpace>
</template>
<script setup lang="ts">
interface Props {
// ... 现有 props
/** 🆕 测试 ID 前缀(用于区分不同模式) */
testIdPrefix?: string
}
const props = withDefaults(defineProps<Props>(), {
// ... 现有默认值
testIdPrefix: 'input-panel'
})
</script>
在工作区中传递 testIdPrefix:
<template>
<div data-testid="basic-system-workspace">
<InputPanelUI
v-model="promptModel"
test-id-prefix="basic-system"
:show-analyze-button="true"
@analyze="handleAnalyze"
/>
<!-- 评估分数徽章 -->
<EvaluationScoreBadge
data-testid="basic-system-score-badge"
:score="evaluationScore"
/>
</div>
</template>
<template>
<div data-testid="basic-user-workspace">
<InputPanelUI
v-model="promptModel"
test-id-prefix="basic-user"
:show-analyze-button="true"
@analyze="handleAnalyze"
/>
<EvaluationScoreBadge
data-testid="basic-user-score-badge"
:score="evaluationScore"
/>
</div>
</template>
<template>
<div
class="evaluation-score-badge"
:class="[sizeClass, levelClass, { clickable: !loading, loading }]"
data-testid="evaluation-score-badge"
>
<template v-if="loading">
<NSpin :size="spinSize" data-testid="score-loading" />
</template>
<template v-else-if="score !== null && score !== undefined">
<span
class="score-value"
data-testid="score-value"
>{{ score }}</span>
</template>
</div>
</template>
import { expect, type Page } from '@playwright/test'
/**
* 填写原始提示词(使用 data-testid)
* @param page Playwright Page 对象
* @param mode 模式前缀(如 'basic-system', 'basic-user')
* @param value 提示词内容
*/
export async function fillOriginalPrompt(
page: Page,
mode: string,
value: string
): Promise<void> {
const input = page.locator(`[data-testid="${mode}-input"]`)
await expect(input).toBeVisible({ timeout: 15000 })
// 检查是否是 CodeMirror
const cmContent = input.locator('.cm-content')
if ((await cmContent.count()) > 0) {
await cmContent.click()
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A')
await page.keyboard.type(value)
} else {
await input.fill(value)
}
// 等待按钮可用
const analyzeButton = page.locator(`[data-testid="${mode}-analyze-button"]`)
await expect(analyzeButton).toBeEnabled({ timeout: 15000 })
}
/**
* 点击分析按钮(使用 data-testid)
* @param page Playwright Page 对象
* @param mode 模式前缀
*/
export async function clickAnalyzeButton(page: Page, mode: string): Promise<void> {
const analyzeButton = page.locator(`[data-testid="${mode}-analyze-button"]`)
await expect(analyzeButton).toBeVisible({ timeout: 15000 })
await expect(analyzeButton).toBeEnabled({ timeout: 15000 })
await analyzeButton.click()
}
/**
* 获取评估分数(使用 data-testid)
* @param page Playwright Page 对象
* @param mode 模式前缀(可选,用于更精确定位)
* @returns 分数(0-100)
*/
export async function getEvaluationScore(
page: Page,
mode?: string
): Promise<number> {
const badgeSelector = mode
? `[data-testid="${mode}-score-badge"]`
: '[data-testid="evaluation-score-badge"]'
const scoreBadge = page.locator(badgeSelector)
await expect(scoreBadge).toBeVisible({ timeout: 90000 })
await expect(scoreBadge).not.toHaveClass(/loading/, { timeout: 60000 })
const scoreValue = scoreBadge.locator('[data-testid="score-value"]')
await expect(scoreValue).toBeVisible({ timeout: 10000 })
const scoreText = await scoreValue.textContent()
const score = parseInt(scoreText?.trim() || '0')
expect(score).toBeGreaterThan(0)
expect(score).toBeLessThanOrEqual(100)
return score
}
/**
* 验证分析按钮在输入为空时禁用
* @param page Playwright Page 对象
* @param mode 模式前缀
*/
export async function verifyAnalyzeButtonDisabledWhenEmpty(
page: Page,
mode: string
): Promise<void> {
const analyzeButton = page.locator(`[data-testid="${mode}-analyze-button"]`)
await expect(analyzeButton).toBeVisible({ timeout: 15000 })
await expect(analyzeButton).toBeDisabled()
}
import { test, expect } from '../fixtures'
import { navigateToMode } from '../helpers/common'
import {
fillOriginalPrompt,
clickAnalyzeButton,
getEvaluationScore,
verifyAnalyzeButtonDisabledWhenEmpty
} from '../helpers/analysis'
const MODE = 'basic-system'
test.describe('Basic System - 提示词分析', () => {
test('分析提示词并显示评估结果', async ({ page }) => {
test.setTimeout(180000)
// 1. 导航到 basic-system 工作区
await navigateToMode(page, 'basic', 'system')
await page.waitForTimeout(3000)
// 2. 填写提示词(使用 data-testid)
const testPrompt = '写一个排序算法'
await fillOriginalPrompt(page, MODE, testPrompt)
// 3. 点击分析按钮(使用 data-testid)
await clickAnalyzeButton(page, MODE)
await page.waitForTimeout(500)
// 4. 验证评估分数(使用 data-testid)
const score = await getEvaluationScore(page, MODE)
console.log(`✓ ${MODE} 评估分数: ${score}/100`)
})
test('验证分析按钮在没有提示词时禁用', async ({ page }) => {
await navigateToMode(page, 'basic', 'system')
await page.waitForTimeout(1000)
// 使用 data-testid 验证按钮状态
await verifyAnalyzeButtonDisabledWhenEmpty(page, MODE)
})
})
// 依赖文本,易受国际化影响
const card = page.getByText(/Original Prompt|原始提示词/i)
// 可能匹配多个按钮
const button = card.getByRole('button', { name: /分析|Analyze/i })
// XPath 脆弱
const ancestor = card.locator('xpath=ancestor::*[contains(@class,"n-card")]')
// 稳定、语言无关
await fillOriginalPrompt(page, 'basic-system', '测试内容')
await clickAnalyzeButton(page, 'basic-system')
const score = await getEvaluationScore(page, 'basic-system')
InputPanel.vue - 添加 testIdPrefix prop 和 data-testidBasicSystemWorkspace.vue - 传递 testIdPrefix="basic-system"BasicUserWorkspace.vue - 传递 testIdPrefix="basic-user"EvaluationScoreBadge.vue - 添加 data-testid="evaluation-score-badge"helpers/analysis.ts 使用新选择器analysis/basic-system.spec.ts 和 basic-user.spec.tsContextSystemWorkspace.vue - 添加 data-testidContextUserWorkspace.vue - 添加 data-testidImageText2ImageWorkspace.vue - 添加 data-testidImageImage2ImageWorkspace.vue - 添加 data-testiddata-testid 不影响现有功能data-testid 在生产环境保留(文件大小增加可忽略)