关于 umbraco-e2e-testing
使用 Playwright 和 Umbraco acceptance test helpers 对 Umbraco 后台扩展进行端到端测试的技能。它区分核心 Umbraco 操作所用的辅助工具与自定义扩展界面所用的原始 Playwright 操作。
主要功能
- 01针对运行中的 Umbraco 实例测试
- 02使用辅助工具操作核心界面和 API
- 03用原始 Playwright 测试自定义扩展元素
- 04配置身份验证和可复用测试装置
- 05重置持久化数据以保证测试起点一致
适用场景
- 01验证完整的后台用户工作流
- 02测试数据持久化和 API 响应
- 03检查身份验证与授权
- 04在发布前运行验收测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ umbraco/ umbraco-cms-backoffice-skills --skill umbraco-e2e-testing安装后在 Claude Code 中输入/umbraco-e2e-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
examples/document-type-crud/.gitignorepackage-lock.jsonpackage.jsonplaywright.config.tsREADME.mdtsconfig.jsonumbraco.config.tsexamples/document-type-crud/tests/auth.setup.tsdocument-type.spec.ts详细介绍
Umbraco 端到端测试
使用 Playwright 和 @umbraco-cms/acceptance-test-helpers 对 Umbraco 后台扩展进行端到端测试。这种方式会针对实际运行的 Umbraco 实例进行测试,验证完整的用户工作流。
重要:使用 Testhelpers 操作 Umbraco 核心功能
对Umbraco 核心操作使用 @umbraco-cms/acceptance-test-helpers:
| 包 | 用途 | 必需原因 |
|---|---|---|
@umbraco-cms/acceptance-test-helpers | UI 和 API 辅助工具 | 处理身份验证、导航和核心实体的增删改查 |
@umbraco/json-models-builders | 测试数据构建器 | 创建结构正确的有效 Umbraco 实体 |
为什么要对 Umbraco 核心功能使用 testhelpers?
- Umbraco 使用
data-mark而非data-testid,testhelpers 会处理这一差异 - 身份验证令牌管理较为复杂,testhelpers 会管理
STORAGE_STAGE_PATH - API 设置和清理需要特定的载荷格式,构建器可确保格式正确
- 选择器会随版本变化,testhelpers 会屏蔽这些差异
// WRONG - Raw Playwright for core Umbraco (brittle)
await page.goto('/umbraco');
await page.fill('[name="email"]', '[email protected]');
// CORRECT - Testhelpers for core Umbraco
import { test } from '@umbraco-cms/acceptance-test-helpers';
test('my test', async ({ umbracoApi, umbracoUi }) => {
await umbracoUi.goToBackOffice();
await umbracoUi.login.enterEmail('[email protected]');
});
何时使用原生 Playwright
对于自定义扩展,请使用 umbracoUi.page(原生 Playwright),因为 testhelpers 不认识你的自定义元素:
test('my custom extension', async ({ umbracoUi }) => {
// Testhelpers for core navigation
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.settings);
// Raw Playwright for YOUR custom elements
await umbracoUi.page.getByRole('link', { name: 'My Custom Item' }).click();
await expect(umbracoUi.page.locator('my-custom-workspace')).toBeVisible();
});
| 使用 Testhelpers 的场景 | 使用 umbracoUi.page 的场景 |
|---|---|
| 登录/注销 | 自定义树项 |
| 导航到任意分区(包括自定义分区) | 自定义工作区元素 |
| 通过 API 创建/编辑文档 | 自定义实体操作 |
| 内置 UI 交互 | 自定义 UI 组件 |
使用场景
- 测试完整的用户工作流
- 测试数据持久性
- 测试身份验证/授权
- 发布前进行验收测试
- 使用真实 API 响应进行集成测试
相关技能
- umbraco-testing - 测试概览的主技能
- umbraco-playwright-testhelpers - testhelpers 包的完整参考
- umbraco-test-builders - 用于测试数据的 JsonModels.Builders
- umbraco-mocked-backoffice - 无需真实后端即可测试(速度更快)
文档
- Playwright:https://playwright.dev/docs/intro
- 参考测试:
Umbraco-CMS/tests/Umbraco.Tests.AcceptanceTest
设置
依赖项
添加到 package.json:
{
"devDependencies": {
"@playwright/test": "^1.56",
"@umbraco-cms/acceptance-test-helpers": "^18.0.0",
"@umbraco/json-models-builders": "^18.0.0",
"dotenv": "^16.3.1"
},
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
}
}
然后运行:
npm install
npx playwright install chromium
版本兼容性:testhelpers 版本应与 Umbraco 版本匹配:
| Umbraco | Testhelpers |
|---|---|
| 18.x | ^18.0.0 |
| 17.x | ^17.0.15 |
| 14.x | ^14.x |
配置
创建 playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export const STORAGE_STATE = join(__dirname, 'tests/e2e/.auth/user.json');
// CRITICAL: Testhelpers read auth tokens from this file
process.env.STORAGE_STAGE_PATH = STORAGE_STATE;
export default defineConfig({
testDir: './tests/e2e',
timeout: 30 * 1000,
expect: { timeout: 5000 },
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: process.env.CI ? 'line' : 'html',
use: {
baseURL: process.env.UMBRACO_URL || 'https://localhost:44325',
trace: 'retain-on-failure',
ignoreHTTPSErrors: true,
// CRITICAL: Umbraco uses 'data-mark' not 'data-testid'
testIdAttribute: 'data-mark',
},
projects: [
{
name: 'setup',
testMatch: '**/*.setup.ts',
},
{
name: 'e2e',
testMatch: '**/*.spec.ts',
dependencies: ['setup'],
use: {
...devices['Desktop Chrome'],
ignoreHTTPSErrors: true,
storageState: STORAGE_STATE,
},
},
],
});
重要设置
| 设置 | 值 | 必需原因 |
|---|---|---|
testIdAttribute | 'data-mark' | Umbraco 使用 data-mark,而非 data-testid |
STORAGE_STAGE_PATH | 指向 user.json 的路径 | Testhelpers 会从该文件读取身份验证令牌 |
ignoreHTTPSErrors | true | 用于使用自签名证书的本地开发环境 |
如果不设置 testIdAttribute: 'data-mark',所有 getByTestId() 调用都会失败。
身份验证设置
创建 tests/e2e/auth.setup.ts:
import { test as setup } from '@playwright/test';
import { STORAGE_STATE } from '../../playwright.config';
import { ConstantHelper, UiHelpers } from '@umbraco-cms/acceptance-test-helpers';
setup('authenticate', async ({ page }) => {
const umbracoUi = new UiHelpers(page);
await umbracoUi.goToBackOffice();
await umbracoUi.login.enterEmail(process.env.UMBRACO_USER_LOGIN!);
await umbracoUi.login.enterPassword(process.env.UMBRACO_USER_PASSWORD!);
await umbracoUi.login.clickLoginButton();
await umbracoUi.login.goToSection(ConstantHelper.sections.settings);
await page.context().storageState({ path: STORAGE_STATE });
});
环境变量
创建 .env(并将其添加到 .gitignore):
UMBRACO_URL=https://localhost:44325
[email protected]
UMBRACO_USER_PASSWORD=yourpassword
UMBRACO_DATA_PATH=/path/to/Umbraco.Web.UI/App_Data # Optional: for data reset
| 变量 | 必需 | 用途 |
|---|---|---|
UMBRACO_URL | 是 | 后台 URL |
UMBRACO_USER_LOGIN | 是 | 管理员邮箱 |
UMBRACO_USER_PASSWORD | 是 | 管理员密码 |
UMBRACO_DATA_PATH | 否 | 用于重置测试数据的 App_Data 路径(参见“使用持久化数据进行测试”) |
目录结构
my-extension/
├── src/
│ └── ...
├── tests/
│ └── e2e/
│ ├── .auth/
│ │ └── user.json # Auth state (gitignored)
│ ├── auth.setup.ts # Authentication
│ └── my-extension.spec.ts
├── playwright.config.ts
├── .env # Gitignored
├── .env.example
└── package.json
模式
测试夹具
import { test } from '@umbraco-cms/acceptance-test-helpers';
test('my test', async ({ umbracoApi, umbracoUi }) => {
// umbracoApi - API helpers for setup/teardown
// umbracoUi - UI helpers for backoffice interaction
});
AAA 模式(准备-执行-断言)
test('can create content', async ({ umbracoApi, umbracoUi }) => {
// Arrange - Setup via API
await umbracoApi.documentType.createDefaultDocumentType('TestDocType');
// Act - Perform user actions via UI
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.clickActionsMenuAtRoot();
// Assert - Verify results
expect(await umbracoApi.document.doesNameExist('TestContent')).toBeTruthy();
});
幂等清理
test.afterEach(async ({ umbracoApi }) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
API 辅助工具(umbracoApi)
文档类型:
await umbracoApi.documentType.createDefaultDocumentType('TestDocType');
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(
'TestDocType', 'Textstring', dataTypeData.id
);
await umbracoApi.documentType.ensureNameNotExists('TestDocType');
文档:
await umbracoApi.document.createDefaultDocument('TestContent', docTypeId);
await umbracoApi.document.createDocumentWithTextContent(
'TestContent', docTypeId, 'value', 'Textstring'
);
await umbracoApi.document.publish(contentId);
await umbracoApi.document.ensureNameNotExists('TestContent');
数据类型:
const dataType = await umbracoApi.dataType.getByName('Textstring');
await umbracoApi.dataType.create('MyType', 'Umbraco.TextBox', 'Umb.PropertyEditorUi.TextBox', []);
使用构建器处理复杂数据
对于复杂的测试数据,请使用 @umbraco/json-models-builders:
import { DocumentTypeBuilder, DocumentBuilder } from '@umbraco/json-models-builders';
test('create complex document type', async ({ umbracoApi }) => {
// Build a document type with multiple properties
const docType = new DocumentTypeBuilder()
.withName('Article')
.withAlias('article')
.addGroup()
.withName('Content')
.addTextBoxProperty()
.withAlias('title')
.withLabel('Title')
.done()
.addRichTextProperty()
.withAlias('body')
.withLabel('Body')
.done()
.done()
.build();
await umbracoApi.documentType.create(docType);
});
为什么要使用构建器?
- 流畅的 API 让复杂结构一目了然
- 确保载荷结构符合 Umbraco API 的要求
- 处理必填字段和默认值
- 在 TypeScript 中提供类型安全
UI 辅助工具(umbracoUi)
导航:
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName('My Page');
在侧边栏中测试自定义树
测试自定义树扩展(例如设置中的扩展)时,请使用此模式处理异步加载和滚动:
test('should click custom tree item', async ({ umbracoUi }) => {
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.settings);
// 1. Wait for your tree heading (custom trees often at bottom of sidebar)
await umbracoUi.page.getByRole('heading', { name: 'My Tree' }).waitFor({ timeout: 15000 });
// 2. Scroll into view (important - sidebar may be long)
await umbracoUi.page.getByRole('heading', { name: 'My Tree' }).scrollIntoViewIfNeeded();
// 3. Wait for tree items to load (async from API)
const item1Link = umbracoUi.page.getByRole('link', { name: 'Item 1' });
await item1Link.waitFor({ timeout: 15000 });
// 4. Click the item
await item1Link.click();
// Assert workspace loads
await expect(umbracoUi.page.locator('my-tree-workspace-editor')).toBeVisible({ timeout: 15000 });
});
为什么使用此模式?
- 自定义树通常位于设置侧边栏底部
- 树项会从 API 异步加载
- 使用
getByRole('link', { name: '...' })比通用的umb-tree-item选择器更可靠 - 内置树(文档类型等)也使用
umb-tree-item,会导致选择器冲突
内容操作:
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType('TestDocType');
await umbracoUi.content.enterContentName('My Page');
await umbracoUi.content.enterTextstring('My text value');
await umbracoUi.content.clickSaveButton();
常量:
import { ConstantHelper } from '@umbraco-cms/acceptance-test-helpers';
ConstantHelper.sections.content
ConstantHelper.sections.settings
ConstantHelper.buttons.save
ConstantHelper.buttons.saveAndPublish
示例
完整测试
import { expect } from '@playwright/test';
import { ConstantHelper, NotificationConstantHelper, test } from '@umbraco-cms/acceptance-test-helpers';
const contentName = 'TestContent';
const documentTypeName = 'TestDocType';
const dataTypeName = 'Textstring';
const contentText = 'Test content text';
test.afterEach(async ({ umbracoApi }) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can create content', { tag: '@smoke' }, async ({ umbracoApi, umbracoUi }) => {
// Arrange
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(
documentTypeName, dataTypeName, dataTypeData.id
);
// Act
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.enterTextstring(contentText);
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.waitForContentToBeCreated();
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value).toBe(contentText);
});
test('can publish content', async ({ umbracoApi, umbracoUi }) => {
// Arrange
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const docTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(
documentTypeName, dataTypeName, dataTypeData.id
);
await umbracoApi.document.createDocumentWithTextContent(
contentName, docTypeId, contentText, dataTypeName
);
// Act
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.clickActionsMenuForContent(contentName);
await umbracoUi.content.clickPublishActionMenuOption();
await umbracoUi.content.clickConfirmToPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(
NotificationConstantHelper.success.published
);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe('Published');
});
可运行示例:tree-example
tree-example 演示了如何对自定义树扩展进行端到端测试:
位置:umbraco-backoffice/examples/tree-example/Client/
# Run E2E tests (requires running Umbraco)
URL=https://localhost:44325 \
[email protected] \
UMBRACO_USER_PASSWORD=yourpassword \
npm run test:e2e # 7 tests
主要文件:
tests/playwright.e2e.config.ts- 包含身份验证设置的端到端配置tests/auth.setup.ts- 使用 testhelpers 进行身份验证tests/tree-e2e.spec.ts- 对设置侧边栏中的自定义树进行测试
可运行示例:notes-wiki(带数据重置的全栈示例)
notes-wiki 演示了如何对持久化数据和 CRUD 操作进行端到端测试:
位置:umbraco-backoffice/examples/notes-wiki/Client/
# Run E2E tests (with data reset)
URL=https://localhost:44325 \
[email protected] \
UMBRACO_USER_PASSWORD=yourpassword \
UMBRACO_DATA_PATH=/path/to/App_Data \
npm run test:e2e # 16 tests
主要文件:
tests/playwright.e2e.config.ts- 配置中包含用于重置数据的globalSetuptests/global-setup.ts- 在测试前将数据重置为初始状态tests/test-seed-data.json- 已知的测试数据(笔记、文件夹)tests/notes-wiki-e2e.spec.ts- CRUD 和导航测试
演示内容:
- 使用
goToSection('notes')测试自定义分区 - 在每次测试运行前重置基于文件的数据
- 测试树导航、文件夹和工作区
- 通过“查看操作”按钮执行实体操作(比右键单击更可靠)
- 测试仪表板和工作区视图
测试具有持久化数据的扩展
如果扩展会持久化数据(JSON 文件、数据库等),测试需要可预测的初始状态。
全局设置模式
添加 globalSetup,以便在测试前重置数据:
playwright.e2e.config.ts:
export default defineConfig({
// ... other config
globalSetup: './global-setup.ts',
});
global-setup.ts:
import { FullConfig } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
async function globalSetup(config: FullConfig) {
const dataPath = process.env.UMBRACO_DATA_PATH;
if (!dataPath) {
console.warn('⚠️ UMBRACO_DATA_PATH not set. Skipping data reset.');
return;
}
const targetFile = path.join(dataPath, 'MyExtension/data.json');
const seedFile = path.join(__dirname, 'test-seed-data.json');
// Ensure directory exists
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
// Copy seed data to target
fs.copyFileSync(seedFile, targetFile);
console.log('🌱 Reset data to seed state');
}
export default globalSetup;
test-seed-data.json:
{
"items": [
{ "id": "test-1", "name": "Test Item 1" },
{ "id": "test-2", "name": "Test Item 2" }
]
}
环境变量
添加 UMBRACO_DATA_PATH,以定位 Umbraco 的 App_Data 文件夹:
UMBRACO_DATA_PATH=/path/to/Umbraco.Web.UI/App_Data npm run test:e2e
测试自定义分区
自定义分区可使用 testhelpers 的 goToSection() 方法,只需传入分区路径名:
// Section pathname - matches what you defined in section/constants.ts
const MY_SECTION = 'my-section';
// Helper to navigate to custom section using testhelpers
async function goToMySection(umbracoUi: any) {
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(MY_SECTION);
await umbracoUi.page.waitForTimeout(500);
}
test('should navigate to custom section', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
// Assert - your dashboard or tree should be visible
await expect(umbracoUi.page.getByText('Welcome')).toBeVisible({ timeout: 15000 });
});
// To verify the section exists in the section bar:
test('should display my section', async ({ umbracoUi }) => {
await umbracoUi.goToBackOffice();
await expect(umbracoUi.page.getByRole('tab', { name: 'My Section' })).toBeVisible({ timeout: 15000 });
});
测试上下文菜单(实体操作)
测试树项上的实体操作。由于 testhelpers 不涵盖自定义实体操作,因此使用 umbracoUi.page。
重要:在 Umbraco 中,实体操作会作为按钮呈现在下拉菜单中,而不是直接使用 menuitem 角色。最可靠的做法是使用“查看操作”按钮,而不是右键单击:
test('should show delete action via actions button', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
// Wait for tree item
const itemLink = umbracoUi.page.getByRole('link', { name: 'My Item' });
await itemLink.waitFor({ timeout: 15000 });
// Hover to reveal action buttons
await itemLink.hover();
// Click the "View actions" button to open dropdown
const actionsButton = umbracoUi.page.getByRole('button', { name: "View actions for 'My Item'" });
await actionsButton.click();
// Wait for dropdown and check for actions (actions are BUTTONS, not menuitems!)
await umbracoUi.page.waitForTimeout(500);
const deleteButton = umbracoUi.page.getByRole('button', { name: 'Delete' });
const renameButton = umbracoUi.page.getByRole('button', { name: 'Rename' });
// Assert - at least one action should be visible
await expect(deleteButton.or(renameButton)).toBeVisible({ timeout: 5000 });
});
test('should delete item via actions menu', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
const itemLink = umbracoUi.page.getByRole('link', { name: 'Item to Delete' });
await itemLink.waitFor({ timeout: 15000 });
// Hover and open actions menu
await itemLink.hover();
await umbracoUi.page.getByRole('button', { name: "View actions for 'Item to Delete'" }).click();
// Click delete button
await umbracoUi.page.getByRole('button', { name: 'Delete' }).click();
// Confirm deletion (if modal appears)
const confirmButton = umbracoUi.page.getByRole('button', { name: /Confirm|Delete/i });
if (await confirmButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await confirmButton.click();
}
// Assert - item should be gone
await expect(itemLink).not.toBeVisible({ timeout: 15000 });
});
替代方案:右键上下文菜单
右键单击也可行,但使用操作按钮的方法更可靠:
// Right-click approach (less reliable than actions button)
await itemLink.click({ button: 'right' });
await umbracoUi.page.waitForTimeout(500);
await umbracoUi.page.getByRole('button', { name: 'Delete' }).click();
CRUD 测试模式
对于自定义扩展,请使用 umbracoUi.page 进行 UI 交互。对于 Umbraco 核心内容,建议使用 umbracoApi 助手进行设置和清理。
通过操作菜单创建(自定义扩展)
test('should create new item', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
// Hover over parent folder and use Create button
const folderLink = umbracoUi.page.getByRole('link', { name: 'Parent Folder' });
await folderLink.hover();
// Scope to specific menu to avoid ambiguity with multiple items
const folderMenu = umbracoUi.page.getByRole('menu').filter({ hasText: 'Parent Folder' });
const createButton = folderMenu.getByRole('button', { name: 'Create Note' });
await createButton.click();
// Assert - workspace should open with "New" indicator
await expect(umbracoUi.page.locator('my-workspace-editor')).toBeVisible({ timeout: 15000 });
});
限定到特定树项
如果存在多个包含相似元素的树项,请将选择器限定到特定范围,以避免产生歧义:
// WRONG - ambiguous when multiple folders have "Create" buttons
const createButton = page.getByRole('button', { name: 'Create' });
// CORRECT - scoped to specific folder's menu
const folderMenu = page.getByRole('menu').filter({ hasText: 'My Folder' });
const createButton = folderMenu.getByRole('button', { name: 'Create' });
更新并保存
test('should update item', async ({ umbracoUi }) => {
await goToMySection(umbracoUi);
// Navigate to item
await umbracoUi.page.getByRole('link', { name: 'Test Item' }).click();
await umbracoUi.page.locator('my-workspace-editor').waitFor({ timeout: 15000 });
// Update field
const titleInput = umbracoUi.page.locator('uui-input#title');
await titleInput.clear();
await titleInput.fill('Updated Title');
// Save
await umbracoUi.page.getByRole('button', { name: /Save/i }).click();
// Wait for save to complete
await umbracoUi.page.waitForTimeout(2000);
// Assert - header should reflect change
await expect(umbracoUi.page.getByText('Updated Title')).toBeVisible();
});
运行测试
# Run all E2E tests
npm run test:e2e
# Run with UI mode (visual debugging)
npm run test:e2e:ui
# Run specific test file
npx playwright test tests/e2e/my-extension.spec.ts
# Run with specific tag
npx playwright test --grep "@smoke"
# Run in debug mode
npx playwright test --debug
故障排除
getByTestId() 找不到元素
确保在 playwright.config.ts 中设置了 testIdAttribute: 'data-mark'。
身份验证失败
- 检查
.env凭据是否正确 - 确保 Umbraco 实例正在运行
- 验证是否已设置
STORAGE_STAGE_PATH
测试超时
- 在配置中增加超时时间
- 确保 Umbraco 响应正常
- 检查浏览器控制台中的 JS 错误
测试在 CI 中失败
- 确保可以访问 Umbraco 实例
- 在 CI 中设置环境变量
- 使用
npx playwright install chromium
替代方案:MSW 模式(无需后端)
如需在没有真实 Umbraco 后端的情况下更快地进行测试,请使用模拟的 backoffice方法。
调用:skill: umbraco-mocked-backoffice
| 方面 | 真实后端(此技能) | MSW 模式 |
|---|---|---|
| 设置 | 运行中的 Umbraco 实例 | 克隆 Umbraco-CMS,npm install |
| 身份验证 | 必需 | 不需要 |
| 速度 | 较慢 | 较快 |
| 使用场景 | 集成/验收 | UI/组件测试 |