| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- ---
- description: 为 Label Studio 编写和更新 Cypress 集成测试
- globs:
- alwaysApply: false
- ---
- ## Label Studio 的 Cypress 测试生成规则
- ### 项目结构和组织
- **测试文件结构:**
- - 测试应放置在 `web/libs/editor/tests/integration/e2e/` 中,采用语义化文件夹组织
- - 遵循现有文件夹结构:`core/`、`image_segmentation/`、`control_tags/`、`audio/`、`video/`、`timeseries/`、`relations/`、`outliner/`、`bulk_mode/`、`config/`、`drafts/`、`linking_modes/`、`ner/`、`sync/`、`view_all/`
- - 测试文件应以 `.cy.ts` 扩展名结尾
- - 测试数据应放置在 `web/libs/editor/tests/integration/data/` 中,遵循相同的文件夹结构
- **文件命名约定:**
- - 使用反映被测试功能的描述性名称
- - 使用 kebab-case 命名文件(例如 `audio-regions.cy.ts`、`image-segmentation.cy.ts`)
- - 将相关测试分组到逻辑文件夹中
- ### 导入标准
- **必需的导入:**
- 始终从集中的辅助库导入辅助函数:
- ```typescript
- import { LabelStudio, ImageView, Sidebar, Labels, Hotkeys } from "@humansignal/frontend-test/helpers/LSF";
- ```
- **测试数据导入:**
- 使用相对路径从数据文件夹导入测试数据:
- ```typescript
- import { configName, dataName, resultName } from "../../data/folder_name/file_name";
- ```
- **可用的辅助函数:**
- - `LabelStudio` - 核心初始化和控制
- - `ImageView` - 图像交互和绘制
- - `VideoView` - 视频播放和交互
- - `AudioView` - 音频播放和区域
- - `Sidebar` - 大纲视图和区域管理
- - `Labels` - 标签选择和管理
- - `Hotkeys` - 跨平台键盘快捷键(Mac/PC 兼容性)
- - `Taxonomy`、`Choices`、`DateTime`、`Number`、`Rating`、`Textarea` - 控制标签辅助函数
- - `Relations` - 关系管理
- - `ToolBar` - 工具栏交互
- - `Modals` - 模态对话框处理
- - `Tooltip` - 工具提示验证
- ### 测试结构标准
- **基本测试结构:**
- ```typescript
- describe("功能名称 - 特定区域", () => {
- it("应该执行特定操作", () => {
- // 测试实现
- });
- });
- ```
- **嵌套 Describes:**
- 使用嵌套的 describe 块进行逻辑分组:
- ```typescript
- describe("图像分割", () => {
- describe("矩形工具", () => {
- it("应该绘制矩形", () => {
- // 测试实现
- });
- });
- });
- ```
- ### LabelStudio 初始化模式
- **简单初始化:**
- ```typescript
- LabelStudio.init({
- config: configString,
- task: {
- id: 1,
- annotations: [{ id: 1001, result: [] }],
- predictions: [],
- data: { image: "url" },
- },
- });
- ```
- **流式 API 初始化(推荐):**
- ```typescript
- LabelStudio.params()
- .config(configString)
- .data(dataObject)
- .withResult(expectedResult)
- .init();
- ```
- **带附加参数:**
- ```typescript
- LabelStudio.params()
- .config(config)
- .data(data)
- .withResult([])
- .withInterface("panel")
- .withEventListener("eventName", handlerFunction)
- .withParam("customParam", value)
- .init();
- ```
- ### 必需的测试准备步骤
- **始终包含:**
- 1. LabelStudio 初始化
- 2. 等待对象就绪:`LabelStudio.waitForObjectsReady();`
- 3. (可选,通常 waitForObjectsReady 就足够了)等待媒体加载(用于图像/视频/音频):`ImageView.waitForImage();`
- 4. 初始状态验证:`Sidebar.hasNoRegions();`
- 5. (可选,如果可能)操作后的某些状态验证,例如:`Sidebar.hasRegions(count);`
- ### 交互模式
- **图像交互:**
- ```typescript
- // 等待图像加载
- ImageView.waitForImage();
- // 选择工具
- ImageView.selectRectangleToolByButton();
- ImageView.selectPolygonToolByButton();
- // 绘制操作
- ImageView.drawRect(x, y, width, height);
- ImageView.drawRectRelative(0.1, 0.1, 0.4, 0.8); // 推荐
- // 点击交互
- ImageView.clickAt(x, y);
- ImageView.clickAtRelative(0.5, 0.5); // 推荐
- // 截图比较
- ImageView.capture("screenshot_name");
- ImageView.canvasShouldChange("screenshot_name", threshold);
- ```
- **标签管理:**
- ```typescript
- // 绘制前选择标签
- Labels.select("标签名称");
- // 验证标签选择
- Labels.isSelected("标签名称");
- ```
- **侧边栏操作:**
- ```typescript
- // 区域验证
- Sidebar.hasRegions(count);
- Sidebar.hasNoRegions();
- Sidebar.hasSelectedRegions(count);
- // 区域操作
- Sidebar.toggleRegionVisibility(index);
- Sidebar.toggleRegionSelection(index);
- ```
- ### 断言模式
- **标准 Cypress 断言:**
- ```typescript
- cy.get(selector).should("be.visible");
- cy.get(selector).should("have.text", "期望文本");
- cy.get(selector).should("have.class", "class-name");
- ```
- **自定义辅助断言:**
- ```typescript
- Sidebar.hasRegions(expectedCount);
- Sidebar.hasSelectedRegions(expectedCount);
- ImageView.canvasShouldChange("screenshot", threshold);
- ```
- **访问 Window 对象:**
- ```typescript
- cy.window().then((win) => {
- expect(win.Htx.annotationStore.selected.names.get("image")).to.exist;
- });
- ```
- ### 测试数据结构
- **配置格式:**
- ```typescript
- export const configName = `
- <View>
- <Image name="img" value="$image"/>
- <RectangleLabels name="tag" toName="img">
- <Label value="Planet"/>
- <Label value="Moonwalker" background="blue"/>
- </RectangleLabels>
- </View>
- `;
- ```
- **数据格式:**
- ```typescript
- export const dataName = {
- image: "https://htx-pub.s3.us-east-1.amazonaws.com/examples/images/example.jpg",
- text: "用于处理的示例文本",
- };
- ```
- **结果格式:**
- ```typescript
- export const resultName = [
- {
- id: "unique_id",
- type: "rectanglelabels",
- value: {
- x: 10.5,
- y: 15.2,
- width: 25.8,
- height: 30.1,
- rectanglelabels: ["Planet"]
- },
- origin: "manual",
- to_name: "img",
- from_name: "tag",
- }
- ];
- ```
- ### 功能标志管理
- **设置功能标志:**
- ```typescript
- // 导航前
- LabelStudio.setFeatureFlagsOnPageLoad({
- featureName: true,
- });
- // 导航后(通常不需要)
- LabelStudio.setFeatureFlags({
- featureName: true,
- });
- // 验证
- LabelStudio.featureFlag("featureName").should("be.true");
- ```
- ### 错误处理和重试策略
- **对于不稳定的测试:**
- ```typescript
- const suiteConfig = {
- retries: {
- runMode: 3,
- openMode: 0,
- },
- };
- describe("测试套件名称", suiteConfig, () => {
- // 这里是测试
- });
- ```
- ### 日志和调试
- **始终包含描述性日志:**
- ```typescript
- cy.log("使用图像分割配置初始化 LSF");
- cy.log("在相对位置绘制矩形");
- cy.log("验证区域已创建");
- ```
- ### 性能考虑
- **使用相对坐标:**
- - 优先使用 `*Relative()` 方法而非绝对坐标
- - 使用 `ImageView.drawRectRelative()` 而非 `ImageView.drawRect()`
- **高效等待:**
- - 使用 `LabelStudio.waitForObjectsReady()` 进行初始化检查
- - 使用 UI 状态检查确保操作已完成且 UI 已准备好进行下一个操作
- - **避免使用 `cy.wait(milliseconds)` - 改用基于事件或基于状态的等待**
- **等待最佳实践:**
- ```typescript
- // ❌ 错误 - 任意时间等待不可靠且缓慢
- cy.wait(500); // 不知道操作是否真正完成
- cy.wait(300); // 在慢速机器上可能太短,在快速机器上太长
- // ✅ 正确 - 等待特定的 UI 状态变化
- Sidebar.hasRegions(1); // 等待区域出现
- Labels.isSelected("标签名称"); // 等待标签选择
- cy.get(".loading-spinner").should("not.exist"); // 等待加载完成
- // ✅ 正确 - 等待元素状态变化
- cy.get("[data-testid='submit-button']").should("be.enabled");
- cy.get(".htx-timeseries-channel svg").should("be.visible");
- cy.get(".region").should("have.class", "selected");
- // ✅ 正确 - 等待网络请求(必要时)
- cy.intercept("POST", "/api/annotations").as("saveAnnotation");
- cy.get("[data-testid='save-button']").click();
- cy.wait("@saveAnnotation"); // 等待特定的网络调用
- // ✅ 正确 - 等待动画完成
- cy.get(".modal").should("have.class", "fade-in-complete");
- cy.get(".tooltip").should("be.visible").and("not.have.class", "animating");
- ```
- **何时基于时间的等待可能可以接受:**
- - 非常短的等待(50ms 或更少)用于 UI 防抖
- - 单个动画帧等待(16-32ms)用于快速重渲染
- - 当没有完成事件可用时等待 CSS 动画
- - 解决已知的浏览器时序问题(记录为临时修复)
- **使用常量而非魔法数字:**
- ```typescript
- // 导入时序常量
- import { SINGLE_FRAME_TIMEOUT, TWO_FRAMES_TIMEOUT } from "../utils/constants";
- // ✅ 正确 - 使用常量表示帧时序
- cy.wait(SINGLE_FRAME_TIMEOUT); // 等待单个动画帧(60fps)
- cy.wait(TWO_FRAMES_TIMEOUT); // 等待 1-2 个动画帧用于快速但更复杂的重渲染
- // ❌ 错误 - 魔法数字不清楚
- cy.wait(16); // 16 是什么?为什么是 16?
- cy.wait(32); // 32 是什么?为什么是 32?
- // ✅ 正确 - 使用意图明确的常量
- ImageView.clickAt(100, 100);
- cy.wait(SINGLE_FRAME_TIMEOUT); // 允许画布完成重绘
- // 对于其他时序需求可接受,需要文档说明
- cy.wait(50); // 允许防抖输入稳定
- cy.wait(100); // TODO: 替换为适当的动画完成检查
- ```
- **截图比较:**
- ```typescript
- // 操作前捕获
- ImageView.capture("before_action");
- // 执行操作
- Labels.select("Label");
- ImageView.drawRectRelative(0.2, 0.2, 0.6, 0.6);
- // 验证视觉变化
- ImageView.canvasShouldChange("before_action", 0.1);
- ```
- - 仅在必要时使用,因为它们可能会减慢测试速度
- ### 常见测试模式
- **基本绘制测试:**
- ```typescript
- it("应该绘制矩形区域", () => {
- LabelStudio.params()
- .config(imageConfig)
- .data(imageData)
- .withResult([])
- .init();
- LabelStudio.waitForObjectsReady();
- ImageView.waitForImage();
- Sidebar.hasNoRegions();
- Labels.select("标签名称");
- ImageView.drawRectRelative(0.1, 0.1, 0.4, 0.8);
- Sidebar.hasRegions(1);
- });
- ```
- **控制标签交互测试:**
- ```typescript
- it("应该选择分类选项", () => {
- LabelStudio.params()
- .config(taxonomyConfig)
- .data(simpleData)
- .withResult([])
- .init();
- Taxonomy.open();
- Taxonomy.findItem("选项 1").click();
- Taxonomy.hasSelected("选项 1");
- });
- ```
- **状态验证测试:**
- ```typescript
- it("应该在交互后保持状态", () => {
- LabelStudio.params()
- .config(config)
- .data(data)
- .withResult(existingResult)
- .init();
- LabelStudio.waitForObjectsReady();
- Sidebar.hasRegions(1);
- // 执行操作
- cy.contains("button", "更新").click();
- // 验证状态
- LabelStudio.serialize().then((results) => {
- expect(results).to.have.length(1);
- expect(results[0].value).to.deep.equal(expectedValue);
- });
- });
- ```
- ### 无障碍性和用户体验
- **包含适当的 ARIA 标签:**
- ```typescript
- cy.get('[aria-label="rectangle-tool"]').click();
- ```
- **测试键盘交互:**
- ```typescript
- // 始终优先使用 Hotkeys 辅助函数以实现跨平台兼容性
- Hotkeys.undo(); // 自动处理 Ctrl+Z/Cmd+Z
- Hotkeys.redo(); // 自动处理 Ctrl+Shift+Z/Cmd+Shift+Z
- Hotkeys.deleteRegion(); // Backspace
- Hotkeys.deleteAllRegions(); // Ctrl+Backspace/Cmd+Backspace
- Hotkeys.unselectAllRegions(); // Escape
- // 其他可用的特定辅助函数
- ImageView.zoomIn(); // 放大
- Labels.selectWithHotkey("1"); // 通过快捷键选择标签
- // 直接 Cypress 命令(在大多数情况下避免 - 仅作为最后手段使用)
- cy.get("body").type("{esc}"); // ❌ 优先使用 Hotkeys.unselectAllRegions()
- cy.get("body").type("{ctrl}{+}"); // ❌ 不跨平台,优先使用辅助函数
- cy.get("body").type("{cmd}{+}"); // ❌ 平台特定,优先使用辅助函数
- ```
- **重要:** 在大多数情况下,你应该**避免**直接使用 Cypress 键盘命令(`cy.get("body").type()`)。改用辅助函数,因为:
- - `Hotkeys` 辅助函数自动处理 Mac(Cmd)与 PC(Ctrl)的差异
- - 辅助函数提供更好的抽象,更易于维护
- - 辅助函数不太容易出现跨平台问题
- - 仅在没有辅助函数存在且需要非常特定的键盘交互时使用直接命令
- ### 创建新辅助函数
- **辅助函数架构模式:**
- Label Studio 使用两种主要模式创建辅助函数:
- 1. **静态对象模式**(用于单例组件):
- ```typescript
- export const ComponentName = {
- get root() {
- return cy.get(".component-selector");
- },
-
- get subElement() {
- return this.root.find(".sub-element");
- },
-
- performAction() {
- cy.log("在 ComponentName 上执行操作");
- this.root.click();
- },
-
- assertState(expectedValue: string) {
- this.subElement.should("contain.text", expectedValue);
- }
- };
- ```
- 2. **基于类的模式**(用于可参数化的组件):
- ```typescript
- class ComponentHelper {
- private get _baseRootSelector() {
- return ".component-base";
- }
-
- private _rootSelector: string;
-
- constructor(rootSelector: string) {
- this._rootSelector = rootSelector.replace(/^\&/, this._baseRootSelector);
- }
-
- get root() {
- return cy.get(this._rootSelector);
- }
-
- performAction() {
- cy.log(`在 ${this._rootSelector} 上执行操作`);
- this.root.click();
- }
- }
- // 导出单例和工厂
- const ComponentName = new ComponentHelper("&:eq(0)");
- const useComponentName = (rootSelector: string) => {
- return new ComponentHelper(rootSelector);
- };
- export { ComponentName, useComponentName };
- ```
- **辅助函数创建规则:**
- 1. **文件放置:**
- - 将 UI 辅助函数放在 `web/libs/frontend-test/src/helpers/LSF/`
- - 将工具函数放在 `web/libs/frontend-test/src/helpers/utils/`
- - 文件名使用 PascalCase(例如 `MyComponent.ts`)
- 2. **工具 vs 辅助函数决策:**
- - **提取到 `utils/`** 如果函数:
- - 不使用 Cypress 命令(`cy.*`)
- - 不特定于任何 UI 组件
- - 执行通用计算、解析或数据操作
- - 可以独立进行单元测试
- - **保留在辅助类中** 如果函数:
- - 使用 Cypress 命令进行 UI 交互
- - 特定于 UI 组件
- - 管理元素状态或用户交互
- ```typescript
- // ✅ 正确 - 提取到 utils/SVGTransformUtils.ts
- export class SVGTransformUtils {
- static parseTransformString(transformStr: string): DOMMatrix {
- // 纯工具 - 无 Cypress,无 UI 依赖
- const matrix = new DOMMatrix();
- // ... 实现
- return matrix;
- }
- }
- // ❌ 错误 - 不要将工具放在 UI 辅助函数中
- class TimeSeriesHelper {
- // 这应该在工具中
- private parseTransformString(transformStr: string): DOMMatrix { ... }
- }
- ```
- 3. **命名约定:**
- - 使用与 UI 组件匹配的描述性名称
- - 可参数化的辅助函数前缀使用 `use`(例如 `useChoices`)
- - 使用一致的方法命名:
- - `get propertyName()` 用于元素获取器
- - `performAction()` 用于操作
- - `assertState()` 用于断言
- - `hasState()` 用于布尔检查
- 4. **辅助函数内容指南:**
- - **应该包含**:可复用的 UI 交互、元素获取器、简单状态验证
- - **不应该包含**:复杂的多步骤场景、完整的测试工作流、一次性业务逻辑
- 5. **必需元素:**
- - 始终包含 `get root()` 作为主元素获取器
- - 使用语义化子元素获取器(例如 `get submitButton()`)
- - 使用 `cy.log()` 包含描述性日志
- 6. **选择器最佳实践:**
- - 尽可能使用 ARIA 属性而非其他方法
- 7. **方法类型:**
- - **获取器**:返回 Cypress 元素以供进一步链接
- - **操作**:执行用户交互(点击、输入等)
- - **断言**:使用 `.should()` 验证预期状态
- - **简单工具**:特定 UI 操作的辅助方法(不是通用工具)
- **完整辅助函数示例:**
- ```typescript
- import TriggerOptions = Cypress.TriggerOptions;
- class NewComponentHelper {
- private get _baseRootSelector() {
- return ".lsf-new-component";
- }
-
- private _rootSelector: string;
-
- constructor(rootSelector: string) {
- this._rootSelector = rootSelector.replace(/^\&/, this._baseRootSelector);
- }
-
- get root() {
- return cy.get(this._rootSelector);
- }
-
- get input() {
- return this.root.find('input[type="text"]');
- }
-
- get submitButton() {
- return this.root.find('[aria-label="submit"]');
- }
-
- get items() {
- return this.root.find('.item');
- }
-
- fillInput(text: string) {
- cy.log(`填充输入:${text}`);
- this.input.clear().type(text);
- }
-
- selectItem(index: number) {
- cy.log(`选择索引为 ${index} 的项目`);
- this.items.eq(index).click();
- }
-
- findItem(text: string) {
- return this.items.contains(text);
- }
-
- submit() {
- cy.log("提交表单");
- this.submitButton.click();
- }
-
- hasItem(text: string) {
- this.findItem(text).should("be.visible");
- }
-
- hasNoItems() {
- this.items.should("not.exist");
- }
-
- hasSelectedItem(text: string) {
- this.findItem(text).should("have.class", "selected");
- }
- }
- // 导出模式
- const NewComponent = new NewComponentHelper("&:eq(0)");
- const useNewComponent = (rootSelector: string) => {
- return new NewComponentHelper(rootSelector);
- };
- export { NewComponent, useNewComponent };
- ```
- **将辅助函数添加到索引:**
- 创建辅助函数后,将其添加到索引文件:
- ```typescript
- // 在 web/libs/frontend-test/src/helpers/LSF/index.ts 中
- export { NewComponent, useNewComponent } from "./NewComponent";
- ```
- ### 最佳实践总结
- 1. **始终使用语义化文件夹组织**
- 2. **从集中位置导入辅助函数**
- 3. **使用流式 API 进行 LabelStudio 初始化**
- 4. **包含适当的等待机制**
- 5. **使用相对坐标进行响应式测试**
- 6. **添加描述性日志**
- 7. **单独构建测试数据**
- 8. **包含正面和负面测试用例**
- 9. **在可用时使用辅助方法而非原始 Cypress 命令**
- 10. **避免直接使用 Cypress 键盘命令 - 使用 `Hotkeys` 辅助函数实现跨平台兼容性**
- 11. **遵循现有命名约定**
- 12. **为常见 UI 模式创建可复用的辅助函数**
- 13. **对可参数化的组件使用基于类的模式**
- 14. **在辅助函数中包含全面的错误处理**
- 15. **使用 JSDoc 注释记录复杂的辅助方法**
- 16. **在生产测试中使用辅助函数之前先测试它们**
- 17. **当工具函数不使用 Cypress 时,将其提取到单独的 utils 文件**
- 18. **将复杂的测试场景保留在测试文件中,而不是辅助函数中 - 辅助函数应该简单且可复用**
- 19. **避免使用 cy.wait(time) - 使用基于事件或基于状态的等待以获得更可靠的测试**
|