From 717cbcd698c745ad6cc7308b617be23c384216d9 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Thu, 13 Aug 2026 11:56:22 +0300 Subject: [PATCH 01/12] feat: create skill for cypress tests --- skills/cypress/SKILL.md | 653 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 653 insertions(+) create mode 100644 skills/cypress/SKILL.md diff --git a/skills/cypress/SKILL.md b/skills/cypress/SKILL.md new file mode 100644 index 0000000000000..ccaa4a2866550 --- /dev/null +++ b/skills/cypress/SKILL.md @@ -0,0 +1,653 @@ +--- +name: cypress-test-writer +description: | + Write, review, and improve Cypress component tests for UI5 web components following the project's testing conventions. Use this skill whenever the user asks to: + - Write or add Cypress tests for a UI5 component ("write cypress tests for X", "add cypress spec for X", "create tests for X") + - Review existing cypress test files for quality or correctness + - Add custom Cypress commands for a component ("add cypress commands for X") + - Extract repetitive interaction patterns into reusable commands + - Check whether tests follow UI5 testing conventions (real events, attribute selectors, async API patterns) + Trigger even when the user says things like "test this component", "make sure X works", or "help me write a spec" without saying "Cypress" explicitly — if the context is a UI5 web component in this repo, use this skill. +--- + +# Cypress Test Writer for UI5 Web Components + +## Overview + +This skill helps write and review Cypress component tests for UI5 web components. It encodes the project's testing conventions so every test file and command is consistent with the existing codebase. + +The primary reference for testing patterns is `docs/07-development/10-testing.md`. Read it when in doubt about a pattern. + +--- + +## Quick Rules + +### Always use real events +| Instead of | Use | +|---|---| +| `cy.click()` | `cy.realClick()` | +| `cy.type('a')` | `cy.realPress('a')` | +| `cy.type('text')` | `cy.realType('text')` | + +### Always use attribute selectors +```typescript +// Wrong +cy.get("ui5-button") +// Right +cy.get("[ui5-button]") +``` + +### Calling async base API methods +The UI5 base API (`setLanguage`, `setTheme`, etc.) returns Promises. Never call them bare — Cypress won't await them. Always use `cy.wrap({ fn }).then(async ({ fn }) => { await fn(args); })`: + +```typescript +// Wrong — promise not awaited +setLanguage("bg"); + +// Wrong — .then(api => ...) does not await the promise +cy.wrap({ setLanguage }).then(api => api.setLanguage("bg")); + +// Correct — async/await inside .then() ensures the promise is resolved before Cypress continues +cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("bg"); + }); + +// Reading the result after an async call +cy.wrap({ getLanguage }) + .then(({ getLanguage }) => getLanguage()) + .should("equal", "bg"); +``` + +### DOM traversal +```typescript +// Shadow DOM +cy.get("[ui5-button]").shadow().find("button") + +// Slots / light DOM children +cy.get("[ui5-button]").find("[ui5-icon]") +``` + +--- + +## TypeScript Generics + +### Type `cy.get()` at every element boundary + +Pass the concrete component type as a generic to `cy.get()` so TypeScript can verify that the commands chained on the result are valid for that element type. Do this for every `cy.get` that selects a UI5 component — both selector strings and aliases. + +```typescript +// Wrong — TypeScript cannot verify commands are valid for this element +cy.get("[ui5-date-picker]").ui5DatePickerGetCalendar(); +cy.get("@datePicker").ui5DatePickerGetCalendar(); + +// Right — TypeScript knows the subject is a DatePicker +cy.get("[ui5-date-picker]").ui5DatePickerGetCalendar(); +cy.get("@datePicker").ui5DatePickerGetCalendar(); + +// When a command returns a typed element, the next get must match +cy.get("@datePicker").ui5DatePickerGetCalendar().as("calendar"); +cy.get("@calendar").ui5CalendarGetDayPicker().should("be.visible"); +``` + +### `import type` vs plain import + +Use `import type` when a name is only used in type positions. Use a plain import when the name is also used as a value (JSX, static property access, `instanceof`, etc.). + +```typescript +// In a spec file: +import DatePicker from "../../src/DatePicker.js"; // plain — used in JSX and DatePicker.i18nBundle +import type Calendar from "../../src/Calendar.js"; // type-only — only used in cy.get() + +// In a commands file: +import type DatePicker from "../../../src/DatePicker.js"; // type-only — only used as JQuery +import type Calendar from "../../../src/Calendar.js"; // type-only — only used as JQuery +import type ResponsivePopover from "../../../src/ResponsivePopover.js"; // type-only — only used as .find() +``` + +--- + +## Writing a Test File + +### Location +``` +packages/{package}/cypress/specs/{ComponentName}.cy.tsx +``` + +### Minimal structure +```typescript +import ComponentName from "../../src/ComponentName.js"; + +describe("{ComponentName}", () => { + it("renders and shows expected default state", () => { + cy.mount(); + cy.get("[ui5-component-name]").should("exist"); + // Add at least one meaningful assertion beyond "exist" + cy.get("[ui5-component-name]").shadow().find(".ui5-component-root").should("be.visible"); + }); +}); +``` + +### What makes a test meaningful + +A test only asserting `"exist"` is not meaningful. A meaningful test asserts: +- The **rendered state** reflects the props (e.g. `design="Negative"` adds the right CSS class) +- **Events** fire when expected (use `cy.stub` or `cy.spy`) +- **Accessibility** attributes are correct (`aria-label`, `role`, `aria-disabled`) +- **Behavior** after interaction (open/close, value change, focus movement) + +**Weak (avoid):** +```typescript +cy.get("[ui5-button]").should("exist"); +``` + +**Strong (prefer):** +```typescript +cy.get("[ui5-button]").should("have.attr", "disabled"); +cy.get("[ui5-button]").shadow().find("button").should("have.attr", "disabled"); +``` + +### Testing events +```typescript +cy.mount(); + +cy.get("[ui5-button]").then($button => { + cy.stub($button[0], "dispatchEvent").as("dispatchEvent"); +}); + +// Or use addEventListener with a stub +cy.get("[ui5-button]").then($el => { + $el[0].addEventListener("click", cy.stub().as("clicked")); +}); + +cy.get("[ui5-button]").realClick(); +cy.get("@clicked").should("have.been.called"); +``` + +### Configuration (theme, language) + +```typescript +import { setTheme, getTheme } from "@ui5/webcomponents-base/dist/config/Theme.js"; + +cy.wrap({ setTheme }) + .then(async ({ setTheme }) => { + await setTheme("sap_horizon_hcb"); + }); + +cy.wrap({ getTheme }) + .then(({ getTheme }) => getTheme()) + .should("equal", "sap_horizon_hcb"); +``` + +For language tests, always import Assets.js: +```typescript +import "../../src/Assets.js"; // required for extra languages + +cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("bg"); + }); +``` + +### Mobile / device simulation +```typescript +cy.mount(); +cy.ui5SimulateDevice("phone"); +cy.get("[ui5-my-component]").should("have.class", "ui5-my-component-mobile"); +``` + +--- + +## Identifying Repetitive Patterns → Custom Commands + +When writing or reviewing tests, look for: +- The same sequence of `cy.get` + `cy.invoke("attr", ...)` + assertion appearing in 2+ tests +- Open/close sequences for overlay components (dialogs, popovers, menus, pickers) +- "Wait for component to be ready" sequences (shadow DOM existence + popover open + size > 0) +- Multi-step form interactions (type, blur, assert validation) + +When you find such a pattern, extract it into a custom command. + +--- + +## POM Coverage: Every Internal Sub-Element Needs a Command + +When creating or reviewing a commands file for a component, **every internal sub-element the tests interact with must have a dedicated getter command**. Do not leave raw `.shadow().find("ui5-...")` chains in the spec file. + +### What to cover + +For a picker-type component (DatePicker, DateTimePicker, etc.): +- The input element (`ui5DatePickerGetDateTimeInput`) +- The native input inside the input (`ui5DatePickerGetInnerInput`) +- The icon (`ui5DatePickerGetIcon`) +- The popover/responsive-popover (`ui5DatePickerGetPopover`) +- The calendar (`ui5DatePickerGetCalendar`) — chains off datePicker +- Navigation buttons (`ui5DatePickerGetNextButton`, `ui5DatePickerGetPreviousButton`) +- Header buttons (`ui5DatePickerGetMonthButton`, `ui5DatePickerGetYearButton`) + +Sub-elements of `ui5-calendar` belong in `Calendar.commands.ts`, not in the picker's commands file (see "Commands belong to the subject's component" below): +- The day picker (`ui5CalendarGetDayPicker`) +- The month picker (`ui5CalendarGetMonthPicker`) +- The year picker (`ui5CalendarGetYearPicker`) + +For a menu/navigation component: the trigger, the list items, the sub-menus. +For a table: rows, cells, column headers, toolbar. + +### Commands belong to the subject's component type + +A command lives in the commands file that matches **the type of its `prevSubject`**, not the top-level component being tested. If a command's subject is a `Calendar` element, it belongs in `Calendar.commands.ts` regardless of which test file uses it. + +```typescript +// Wrong — DayPicker is a sub-element of Calendar, not DatePicker +// DatePicker.commands.ts: +Cypress.Commands.add("ui5DatePickerGetDayPicker", { prevSubject: true }, (subject: JQuery) => { ... }); + +// Right — the subject is Calendar, so it lives in Calendar.commands.ts +// Calendar.commands.ts: +Cypress.Commands.add("ui5CalendarGetDayPicker", { prevSubject: true }, (subject: JQuery) => { ... }); +``` + +Usage in a spec chains the two commands naturally: +```typescript +cy.get("@datePicker") + .ui5DatePickerGetCalendar() // returns Chainable> + .ui5CalendarGetDayPicker() // subject is Calendar — correct + .should("be.visible"); +``` + +### Chaining pattern for nested sub-elements + +```typescript +// Chain directly when result is used once +cy.get("@datePicker") + .ui5DatePickerGetCalendar() + .ui5CalendarGetDayPicker() + .shadow() + .find(".ui5-dp-content"); + +// Alias the intermediate element for repeated access in one test +cy.get("@datePicker") + .ui5DatePickerGetCalendar() + .as("calendar"); + +cy.get("@calendar").ui5CalendarGetMonthPicker().should("be.visible"); +cy.get("@calendar").ui5CalendarGetYearPicker().should("be.visible"); +``` + +### Rule: no bare tag selectors in specs + +In specs, **never** use bare tag names in `find()`: +```typescript +// Wrong — bare tag selector, bypasses POM +cy.get("@datePicker").shadow().find("ui5-calendar") +cy.get("@calendar").shadow().find("ui5-daypicker") + +// Right — use POM commands +cy.get("@datePicker").ui5DatePickerGetCalendar() +cy.get("@calendar").ui5CalendarGetDayPicker() +``` + +The only exception: `find()` inside a getter command's own implementation — the command itself must reference the tag name to locate the element. + +--- + +## Mount Helper Functions + +When a spec needs the same component configuration in many `it()` blocks, extract it into a named helper function at the top of the file rather than repeating the JSX inline. This keeps each `it()` block focused on the assertion, not the setup. + +```typescript +// Define helpers at the top of the spec file, before describe() +const getDefaultCalendar = (date: Date) => { + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + + return ( + + + + ); +}; + +const getCalendarWithDisabledDates = (id: string, formatPattern: string, ranges: DateRange[]) => ( + + {ranges.map((range, idx) => ( + + ))} + +); + +// Use in tests +describe("Calendar", () => { + it("navigates to the current day", () => { + cy.mount(getDefaultCalendar(new Date(Date.UTC(2000, 10, 22)))); + // ... + }); +}); +``` + +Use fragment wrappers (`<>...`) when a helper needs to render multiple sibling components: + +```typescript +const getCalendarsWithWeekNumbers = () => (<> + + + + + + +); +``` + +--- + +## `beforeEach` and `afterEach` + +Use `beforeEach` and `afterEach` at the `describe` level to share setup and teardown across every test in that block. Do not use them for things that only one test needs — keep those inline. + +### `beforeEach` — shared mount or shared state + +**Shared component mount:** When every test in a `describe` block uses the same component tree, mount it in `beforeEach` rather than repeating `cy.mount()` in every `it()`. + +```typescript +describe("ComboBox - keyboard navigation", () => { + beforeEach(() => { + cy.mount(<> + + + + + + ); + }); + + it("moves focus to the first link in the value state message", () => { + cy.get("[ui5-combobox]").realClick(); + // ... + }); + + it("moves focus back on Escape", () => { + cy.get("[ui5-combobox]").realClick(); + cy.realPress("Escape"); + // ... + }); +}); +``` + +**Shared device/environment setup:** When all tests in a block require the same device simulation or global config state, set it in `beforeEach`: + +```typescript +describe("ComboBox - mobile", () => { + beforeEach(() => { + cy.ui5SimulateDevice("phone"); + }); + + it("renders the mobile picker", () => { + cy.mount(); + // ... + }); +}); +``` + +**Shared language baseline:** When a `describe` block depends on a specific language being set, ensure it in `beforeEach` so tests don't rely on whatever the previous test left: + +```typescript +describe("Calendar accessibility", () => { + beforeEach(() => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("en"); + }); + }); + // ... +}); +``` + +### `afterEach` — mandatory cleanup for global state + +Any test that changes global configuration (language, theme) **must reset it in `afterEach`**. Without cleanup, a failing test corrupts state for every test that follows. + +**Language reset:** +```typescript +import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js"; +import "../../src/Assets.js"; // required for non-English languages + +describe("DatePicker - language", () => { + afterEach(() => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("en"); + }); + }); + + it("displays Bulgarian month names", () => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("bg"); + }); + // ... + }); +}); +``` + +**Theme reset** — same pattern, reset to `"sap_horizon"`: +```typescript +afterEach(() => { + cy.wrap({ setTheme }) + .then(async ({ setTheme }) => { + await setTheme("sap_horizon"); + }); +}); +``` + +### When NOT to use beforeEach/afterEach + +- Do not mount in `beforeEach` when tests need different component configurations — use mount helper functions instead (see above). +- Do not use `afterEach` to reset state that the next `cy.mount()` will implicitly reset anyway (e.g. component-local state). +- Do not use `beforeEach` for setup that only one or two tests need — keep it inline. + +--- + +## Popup Open/Closed Utilities + +Do not repeat the popover state assertion inline in every command. Import `isPopupOpen` and `isPopupClosed` from the shared utils file: + +``` +packages/main/cypress/support/commands/utils/popup-open.ts +``` + +These helpers verify the full set of conditions that mean a popup is truly open or closed: the `open` attribute, the `:popover-open` CSS pseudo-class, and non-zero dimensions. + +**Using them in a commands file:** + +```typescript +import { isPopupOpen, isPopupClosed } from "./utils/popup-open.js"; + +Cypress.Commands.add("ui5DialogOpened", { prevSubject: true }, (subject: JQuery) => { + isPopupOpen(() => cy.wrap(subject)); +}); + +Cypress.Commands.add("ui5DialogClosed", { prevSubject: true }, (subject: JQuery) => { + isPopupClosed(() => cy.wrap(subject)); +}); +``` + +When the popup is a shadow-DOM child (e.g. `ResponsivePopover` renders a `ui5-dialog` on phone), pass a getter that navigates to the correct element: + +```typescript +Cypress.Commands.add("ui5ResponsivePopoverOpened", { prevSubject: true }, (subject: JQuery) => { + if (isPhone()) { + isPopupOpen(() => + cy.wrap(subject).shadow().find("[ui5-dialog]") + ); + } else { + isPopupOpen(() => cy.wrap(subject)); + } +}); +``` + +**Never** copy the assertion block manually into a new command — always import from utils. + +--- + +## Shared Types in `commands/common/types.ts` + +The file `packages/main/cypress/support/commands/common/types.ts` exports shared types used across multiple command files. Before defining a local type in a commands file, check whether it already exists here. + +Currently exported: + +```typescript +export type ModifierKey = "shiftKey" | "ctrlKey" | "altKey" | "metaKey"; +``` + +Import it when writing commands that accept modifier keys: + +```typescript +import type { ModifierKey } from "../common/types.js"; + +Cypress.Commands.add("ui5InputType", { prevSubject: true }, (subject: JQuery, text: string, modifier?: ModifierKey) => { + // ... +}); +``` + +Add new shared types here (rather than duplicating them) when the same type would appear in two or more commands files. + +--- + +## Creating Custom Commands + +### File location +``` +packages/{package}/cypress/support/commands/{ComponentName}.commands.ts +``` + +### File structure + +Every commands file must: +1. Import component types as `import type` at the top +2. Implement `Cypress.Commands.add` for each command, with the subject typed as `JQuery` +3. Include `declare global { namespace Cypress { interface Chainable { ... } } }` **in the same file**, after the implementations, using `this: Chainable>` constraints and typed return values +4. Use `{ prevSubject: true }` for commands that chain off a previous subject (most UI5 commands do) + +**Template:** +```typescript +import type MyComponent from "../../../src/MyComponent.js"; +import type InnerElement from "../../../src/InnerElement.js"; + +Cypress.Commands.add("ui5MyComponentGetInnerElement", { prevSubject: true }, (subject: JQuery) => { + return cy.wrap(subject) + .shadow() + .find("[ui5-inner-element]"); +}); + +Cypress.Commands.add("ui5MyComponentOpen", { prevSubject: true }, (subject: JQuery) => { + cy.wrap(subject) + .as("component") + .invoke("attr", "open", true); + + cy.get("@component").ui5MyComponentOpened(); +}); + +Cypress.Commands.add("ui5MyComponentOpened", { prevSubject: true }, (subject: JQuery) => { + cy.wrap(subject).as("component"); + cy.get("@component").should("have.attr", "open"); + cy.get("@component") + .shadow() + .find("[ui5-responsive-popover]") + .should($rp => { + expect($rp.is(":popover-open")).to.be.true; + expect($rp.width()).to.not.equal(0); + expect($rp.height()).to.not.equal(0); + }) + .and("have.attr", "open"); +}); + +Cypress.Commands.add("ui5MyComponentClosed", { prevSubject: true }, (subject: JQuery) => { + cy.wrap(subject).as("component"); + cy.get("@component").should("not.have.attr", "open"); + cy.get("@component") + .shadow() + .find("[ui5-responsive-popover]") + .should($rp => { + expect($rp.is(":popover-open")).to.be.false; + }) + .and("not.have.attr", "open"); +}); + +declare global { + namespace Cypress { + interface Chainable { + ui5MyComponentGetInnerElement( + this: Chainable> + ): Chainable> + ui5MyComponentOpen( + this: Chainable>, + options?: { opener?: string } + ): Chainable + ui5MyComponentOpened( + this: Chainable> + ): Chainable + ui5MyComponentClosed( + this: Chainable> + ): Chainable + } + } +} +``` + +Key points about the `declare global` block: +- Use `this: Chainable>` to constrain which subject type the command accepts — TypeScript will error if you chain it off the wrong element type +- Return type should be the specific element type when known (`Chainable>`, `Chainable>`) rather than `Chainable>` +- Use `Chainable` for commands that assert or interact but don't return a new subject + +### Command naming conventions +All UI5 custom commands are prefixed `ui5` followed by the component name in PascalCase, then the action. The component name in the prefix matches the **subject type**, not the test file: +- `ui5MenuOpen` / `ui5MenuOpened` / `ui5MenuClosed` +- `ui5DatePickerGetInnerInput` — subject is DatePicker +- `ui5CalendarGetDayPicker` — subject is Calendar (not `ui5DatePickerGetDayPicker`) +- `ui5SegmentedButtonItemToggleSelect` + +--- + +## Registering Commands in commands.ts + +After creating `{ComponentName}.commands.ts`, add an import to the package's `cypress/support/commands.ts`: + +```typescript +// Keep imports in alphabetical order +import "./commands/{ComponentName}.commands.js"; +``` + +**Note:** The `declare global` block belongs in the individual `{ComponentName}.commands.ts` file, not in `commands.ts`. Do not add type declarations to `commands.ts` — they belong in the component's own commands file. + +--- + +## Reviewing Existing Tests + +When reviewing a test file, check: + +1. **Meaningful assertions** — is every `it()` block asserting something beyond `"exist"`? +2. **Real events** — is `cy.realClick` / `cy.realPress` / `cy.realType` used instead of simulated events? +3. **Attribute selectors** — are components selected with `[ui5-button]` not `ui5-button`? +4. **Async safety** — are base API calls using `async .then()` with explicit `await`, not bare calls or `.then(api => api.method())`? +5. **Repetition** — are the same 3+ line interaction sequences duplicated across `it()` blocks? +6. **Shadow DOM** — when asserting on internal structure, is `.shadow().find(...)` used, or better, a POM command? +7. **Event testing** — when testing that events fire, is `cy.stub` / `cy.spy` used rather than relying on side effects? +8. **POM usage** — are raw `.shadow().find("ui5-...")` chains in the spec replaced by POM commands? +9. **TypeScript generics** — does every `cy.get()` that selects a UI5 component use `cy.get()`? +10. **Unique test names** — does every `it()` block have a unique, descriptive name within its `describe` block? +11. **Alias consistency** — is the element aliased before use, and is that alias used consistently rather than re-selecting the same element? + +For each issue found, either fix it directly or explain what to extract and where to put it. + +--- + +## Workflow + +1. **Read the component source** (`packages/{package}/src/{ComponentName}.ts`) to understand props, events, and shadow DOM structure +2. **Check for existing commands** in `packages/{package}/cypress/support/commands/` — don't duplicate +3. **Write the spec file** using the patterns above, with `cy.get()` generics throughout +4. **Extract commands** for any interaction sequence used more than once +5. **Create the commands file** with typed subjects, typed return values, `this:` constraints in `declare global`, and `import type` for all component imports +6. **Update commands.ts** with the import +7. Tell the user what was created and where From 67c8d4b0b2dee3e82d7689947b8ef8ac302d2bb1 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Fri, 14 Aug 2026 09:33:34 +0300 Subject: [PATCH 02/12] refactor: update structure --- .claude/skills/cypress | 1 + .../cypress-tests.instructions.md | 22 ++ docs/06-Skills.md | 1 + skills/cypress/SKILL.md | 308 +----------------- skills/cypress/references/COMMANDS.md | 266 +++++++++++++++ skills/cypress/references/REVIEWING.md | 17 + 6 files changed, 324 insertions(+), 291 deletions(-) create mode 120000 .claude/skills/cypress create mode 100644 .github/instructions/cypress-tests.instructions.md create mode 100644 skills/cypress/references/COMMANDS.md create mode 100644 skills/cypress/references/REVIEWING.md diff --git a/.claude/skills/cypress b/.claude/skills/cypress new file mode 120000 index 0000000000000..61a509e43cdaf --- /dev/null +++ b/.claude/skills/cypress @@ -0,0 +1 @@ +../../skills/cypress \ No newline at end of file diff --git a/.github/instructions/cypress-tests.instructions.md b/.github/instructions/cypress-tests.instructions.md new file mode 100644 index 0000000000000..0577e662a9c51 --- /dev/null +++ b/.github/instructions/cypress-tests.instructions.md @@ -0,0 +1,22 @@ +--- +applyTo: "packages/*/cypress/specs/**/*.cy.tsx,packages/*/cypress/support/commands/**/*.ts" +--- + +# Cypress Testing Instructions for UI5 Web Components + +When writing, modifying, or reviewing Cypress component tests (`*.cy.tsx`) or +custom Cypress commands in this repository, follow the project's Cypress testing +skill: + +- **Primary guidance:** [`skills/cypress/SKILL.md`](../../skills/cypress/SKILL.md) +- **Custom commands:** [`skills/cypress/references/COMMANDS.md`](../../skills/cypress/references/COMMANDS.md) +- **Reviewing existing specs:** [`skills/cypress/references/REVIEWING.md`](../../skills/cypress/references/REVIEWING.md) + +## Key rules (see the skill for full details) + +- Use **real events** (`cy.realClick()`, `cy.realPress()`, `cy.realType()`) instead of synthetic `.click()` / `.type()`. +- Select components by **attribute notation** — `cy.get("[ui5-button]")`, never the tag name. +- Type element boundaries with generics — `cy.get); -cy.get("[ui5-button]").then($button => { - cy.stub($button[0], "dispatchEvent").as("dispatchEvent"); -}); +cy.get("[ui5-button]") + .then($button => { + cy.stub($button[0], "dispatchEvent").as("dispatchEvent"); + }); // Or use addEventListener with a stub -cy.get("[ui5-button]").then($el => { - $el[0].addEventListener("click", cy.stub().as("clicked")); -}); +cy.get("[ui5-button]") + .then($el => { + $el[0].addEventListener("click", cy.stub().as("clicked")); + }); -cy.get("[ui5-button]").realClick(); -cy.get("@clicked").should("have.been.called"); +cy.get("[ui5-button]") + .realClick(); +cy.get("@clicked") + .should("have.been.called"); ``` ### Configuration (theme, language) @@ -212,13 +330,13 @@ cy.get("@clicked").should("have.been.called"); import { setTheme, getTheme } from "@ui5/webcomponents-base/dist/config/Theme.js"; cy.wrap({ setTheme }) - .then(async ({ setTheme }) => { - await setTheme("sap_horizon_hcb"); - }); + .then(async ({ setTheme }) => { + await setTheme("sap_horizon_hcb"); + }); cy.wrap({ getTheme }) - .then(({ getTheme }) => getTheme()) - .should("equal", "sap_horizon_hcb"); + .then(({ getTheme }) => getTheme()) + .should("equal", "sap_horizon_hcb"); ``` For language tests, always import Assets.js: @@ -226,9 +344,9 @@ For language tests, always import Assets.js: import "../../src/Assets.js"; // required for extra languages cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("bg"); - }); + .then(async ({ setLanguage }) => { + await setLanguage("bg"); + }); ``` ### Mobile / device simulation @@ -260,31 +378,31 @@ When a spec needs the same component configuration in many `it()` blocks, extrac ```typescript // Define helpers at the top of the spec file, before describe() const getDefaultCalendar = (date: Date) => { - const day = String(date.getDate()).padStart(2, "0"); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const year = date.getFullYear(); - - return ( - - - - ); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + + return ( + + + + ); }; const getCalendarWithDisabledDates = (id: string, formatPattern: string, ranges: DateRange[]) => ( - - {ranges.map((range, idx) => ( - - ))} - + + {ranges.map((range, idx) => ( + + ))} + ); // Use in tests describe("Calendar", () => { - it("navigates to the current day", () => { - cy.mount(getDefaultCalendar(new Date(Date.UTC(2000, 10, 22)))); - // ... - }); + it("navigates to the current day", () => { + cy.mount(getDefaultCalendar(new Date(Date.UTC(2000, 10, 22)))); + // ... + }); }); ``` @@ -292,12 +410,12 @@ Use fragment wrappers (`<>...`) when a helper needs to render multiple siblin ```typescript const getCalendarsWithWeekNumbers = () => (<> - - - - - - + + + + + + ); ``` @@ -313,26 +431,28 @@ Use `beforeEach` and `afterEach` at the `describe` level to share setup and tear ```typescript describe("ComboBox - keyboard navigation", () => { - beforeEach(() => { - cy.mount(<> - - - - - - ); - }); - - it("moves focus to the first link in the value state message", () => { - cy.get("[ui5-combobox]").realClick(); - // ... - }); - - it("moves focus back on Escape", () => { - cy.get("[ui5-combobox]").realClick(); - cy.realPress("Escape"); - // ... - }); + beforeEach(() => { + cy.mount(<> + + + + + + ); + }); + + it("moves focus to the first link in the value state message", () => { + cy.get("[ui5-combobox]") + .realClick(); + // ... + }); + + it("moves focus back on Escape", () => { + cy.get("[ui5-combobox]") + .realClick(); + cy.realPress("Escape"); + // ... + }); }); ``` @@ -340,14 +460,14 @@ describe("ComboBox - keyboard navigation", () => { ```typescript describe("ComboBox - mobile", () => { - beforeEach(() => { - cy.ui5SimulateDevice("phone"); - }); - - it("renders the mobile picker", () => { - cy.mount(); - // ... - }); + beforeEach(() => { + cy.ui5SimulateDevice("phone"); + }); + + it("renders the mobile picker", () => { + cy.mount(); + // ... + }); }); ``` @@ -355,13 +475,13 @@ describe("ComboBox - mobile", () => { ```typescript describe("Calendar accessibility", () => { - beforeEach(() => { - cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("en"); - }); - }); - // ... + beforeEach(() => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("en"); + }); + }); + // ... }); ``` @@ -375,30 +495,30 @@ import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js"; import "../../src/Assets.js"; // required for non-English languages describe("DatePicker - language", () => { - afterEach(() => { - cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("en"); - }); - }); - - it("displays Bulgarian month names", () => { - cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("bg"); - }); - // ... - }); + afterEach(() => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("en"); + }); + }); + + it("displays Bulgarian month names", () => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("bg"); + }); + // ... + }); }); ``` **Theme reset** — same pattern, reset to `"sap_horizon"`: ```typescript afterEach(() => { - cy.wrap({ setTheme }) - .then(async ({ setTheme }) => { - await setTheme("sap_horizon"); - }); + cy.wrap({ setTheme }) + .then(async ({ setTheme }) => { + await setTheme("sap_horizon"); + }); }); ``` @@ -416,27 +536,35 @@ Use `have.attr` for reflected properties and ARIA attributes; use `have.prop` fo ```typescript // Reflected to DOM attribute — use have.attr -cy.get("[ui5-button]").should("have.attr", "title", "my tooltip"); -cy.get("[ui5-input]").should("have.attr", "aria-label", "Search"); +cy.get("[ui5-button]") + .should("have.attr", "title", "my tooltip"); +cy.get("[ui5-input]") + .should("have.attr", "aria-label", "Search"); // Non-reflected JS property — use have.prop -cy.get("#myInput").should("have.prop", "focused", true); +cy.get("#myInput") + .should("have.prop", "focused", true); ``` + ### Asserting on events No global event helper — attach a stub: ```typescript -cy.get("[ui5-tag]").then($tag => { - $tag[0].addEventListener("click", cy.stub().as("clicked")); -}); +cy.get("[ui5-tag]") + .then($tag => { + $tag[0].addEventListener("click", cy.stub().as("clicked")); + }); -cy.get("[ui5-tag]").realClick(); -cy.get("@clicked").should("have.been.calledOnce"); +cy.get("[ui5-tag]") + .realClick(); +cy.get("@clicked") + .should("have.been.calledOnce"); // Assert event payload -cy.get("@clickHandler").should("be.calledWithMatch", { detail: { ctrlKey: true } }); +cy.get("@clickHandler") + .should("be.calledWithMatch", { detail: { ctrlKey: true } }); ``` ### Asserting on focus @@ -445,10 +573,13 @@ cy.get("@clickHandler").should("be.calledWithMatch", { detail: { ctrlKey: true } ```typescript // Wrong — races in CI -cy.get("@defaultColorButton").should("have.focus"); +cy.get("@defaultColorButton") + .should("have.focus"); // Right — cy.focused() returns the live inner shadow focus ref -cy.focused().should("have.attr", "aria-label").and("include", "cyan"); +cy.focused() + .should("have.attr", "aria-label") + .and("include", "cyan"); ``` `cy.focused()` returns the inner shadow focus ref, not the host element — assert `aria-label` or other attributes present on that ref, not host-level properties. @@ -461,14 +592,16 @@ Never compare against English string literals. Compare against the i18n bundle s ```typescript // Wrong — breaks if the bundle text ever changes -cy.get("[ui5-form-group]").should("have.attr", "aria-label", "Group 1"); +cy.get("[ui5-form-group]") + .should("have.attr", "aria-label", "Group 1"); // Right — compare against the bundle -cy.get("[ui5-form-group]").should( - "have.attr", - "aria-label", - Form.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, "1") -); +cy.get("[ui5-form-group]") + .should( + "have.attr", + "aria-label", + Form.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, "1") + ); ``` For non-default locales, always import `Assets.js` (see "Configuration" above). diff --git a/skills/cypress/references/COMMANDS.md b/skills/cypress/references/COMMANDS.md index 99ccd077f3851..540d3368e6eed 100644 --- a/skills/cypress/references/COMMANDS.md +++ b/skills/cypress/references/COMMANDS.md @@ -75,9 +75,9 @@ Cypress.Commands.add("ui5CalendarGetDayPicker", { prevSubject: true }, (subject: Usage in a spec chains the two commands naturally: ```typescript cy.get("@datePicker") - .ui5DatePickerGetCalendar() // returns Chainable> - .ui5CalendarGetDayPicker() // subject is Calendar — correct - .should("be.visible"); + .ui5DatePickerGetCalendar() // returns Chainable> + .ui5CalendarGetDayPicker() // subject is Calendar — correct + .should("be.visible"); ``` ### Chaining pattern for nested sub-elements @@ -85,18 +85,22 @@ cy.get("@datePicker") ```typescript // Chain directly when result is used once cy.get("@datePicker") - .ui5DatePickerGetCalendar() - .ui5CalendarGetDayPicker() - .shadow() - .find(".ui5-dp-content"); + .ui5DatePickerGetCalendar() + .ui5CalendarGetDayPicker() + .shadow() + .find(".ui5-dp-content"); // Alias the intermediate element for repeated access in one test cy.get("@datePicker") - .ui5DatePickerGetCalendar() - .as("calendar"); - -cy.get("@calendar").ui5CalendarGetMonthPicker().should("be.visible"); -cy.get("@calendar").ui5CalendarGetYearPicker().should("be.visible"); + .ui5DatePickerGetCalendar() + .as("calendar"); + +cy.get("@calendar") + .ui5CalendarGetMonthPicker() + .should("be.visible"); +cy.get("@calendar") + .ui5CalendarGetYearPicker() + .should("be.visible"); ``` ### Rule: no bare tag selectors in specs @@ -104,12 +108,18 @@ cy.get("@calendar").ui5CalendarGetYearPicker().should("be.visible"); In specs, **never** use bare tag names in `find()`: ```typescript // Wrong — bare tag selector, bypasses POM -cy.get("@datePicker").shadow().find("ui5-calendar") -cy.get("@calendar").shadow().find("ui5-daypicker") +cy.get("@datePicker") + .shadow() + .find("ui5-calendar") +cy.get("@calendar") + .shadow() + .find("ui5-daypicker") // Right — use POM commands -cy.get("@datePicker").ui5DatePickerGetCalendar() -cy.get("@calendar").ui5CalendarGetDayPicker() +cy.get("@datePicker") + .ui5DatePickerGetCalendar() +cy.get("@calendar") + .ui5CalendarGetDayPicker() ``` The only exception: `find()` inside a getter command's own implementation — the command itself must reference the tag name to locate the element. @@ -137,63 +147,68 @@ import type MyComponent from "../../../src/MyComponent.js"; import type InnerElement from "../../../src/InnerElement.js"; Cypress.Commands.add("ui5MyComponentGetInnerElement", { prevSubject: true }, (subject: JQuery) => { - return cy.wrap(subject) - .shadow() - .find("[ui5-inner-element]"); + return cy.wrap(subject) + .shadow() + .find("[ui5-inner-element]"); }); Cypress.Commands.add("ui5MyComponentOpen", { prevSubject: true }, (subject: JQuery) => { - cy.wrap(subject) - .as("component") - .invoke("attr", "open", true); + cy.wrap(subject) + .as("component") + .invoke("attr", "open", true); - cy.get("@component").ui5MyComponentOpened(); + cy.get("@component") + .ui5MyComponentOpened(); }); Cypress.Commands.add("ui5MyComponentOpened", { prevSubject: true }, (subject: JQuery) => { - cy.wrap(subject).as("component"); - cy.get("@component").should("have.attr", "open"); - cy.get("@component") - .shadow() - .find("[ui5-responsive-popover]") - .should($rp => { - expect($rp.is(":popover-open")).to.be.true; - expect($rp.width()).to.not.equal(0); - expect($rp.height()).to.not.equal(0); - }) - .and("have.attr", "open"); + cy.wrap(subject) + .as("component"); + cy.get("@component") + .should("have.attr", "open"); + cy.get("@component") + .shadow() + .find("[ui5-responsive-popover]") + .should($rp => { + expect($rp.is(":popover-open")).to.be.true; + expect($rp.width()).to.not.equal(0); + expect($rp.height()).to.not.equal(0); + }) + .and("have.attr", "open"); }); Cypress.Commands.add("ui5MyComponentClosed", { prevSubject: true }, (subject: JQuery) => { - cy.wrap(subject).as("component"); - cy.get("@component").should("not.have.attr", "open"); - cy.get("@component") - .shadow() - .find("[ui5-responsive-popover]") - .should($rp => { - expect($rp.is(":popover-open")).to.be.false; - }) - .and("not.have.attr", "open"); + cy.wrap(subject) + .as("component"); + cy.get("@component") + .should("not.have.attr", "open"); + cy.get("@component") + .shadow() + .find("[ui5-responsive-popover]") + .should($rp => { + expect($rp.is(":popover-open")).to.be.false; + }) + .and("not.have.attr", "open"); }); declare global { - namespace Cypress { - interface Chainable { - ui5MyComponentGetInnerElement( - this: Chainable> - ): Chainable> - ui5MyComponentOpen( - this: Chainable>, - options?: { opener?: string } - ): Chainable - ui5MyComponentOpened( - this: Chainable> - ): Chainable - ui5MyComponentClosed( - this: Chainable> - ): Chainable - } - } + namespace Cypress { + interface Chainable { + ui5MyComponentGetInnerElement( + this: Chainable> + ): Chainable> + ui5MyComponentOpen( + this: Chainable>, + options?: { opener?: string } + ): Chainable + ui5MyComponentOpened( + this: Chainable> + ): Chainable + ui5MyComponentClosed( + this: Chainable> + ): Chainable + } + } } ``` @@ -227,11 +242,11 @@ These helpers verify the full set of conditions that mean a popup is truly open import { isPopupOpen, isPopupClosed } from "./utils/popup-open.js"; Cypress.Commands.add("ui5DialogOpened", { prevSubject: true }, (subject: JQuery) => { - isPopupOpen(() => cy.wrap(subject)); + isPopupOpen(() => cy.wrap(subject)); }); Cypress.Commands.add("ui5DialogClosed", { prevSubject: true }, (subject: JQuery) => { - isPopupClosed(() => cy.wrap(subject)); + isPopupClosed(() => cy.wrap(subject)); }); ``` @@ -239,13 +254,15 @@ When the popup is a shadow-DOM child (e.g. `ResponsivePopover` renders a `ui5-di ```typescript Cypress.Commands.add("ui5ResponsivePopoverOpened", { prevSubject: true }, (subject: JQuery) => { - if (isPhone()) { - isPopupOpen(() => - cy.wrap(subject).shadow().find("[ui5-dialog]") - ); - } else { - isPopupOpen(() => cy.wrap(subject)); - } + if (isPhone()) { + isPopupOpen(() => + cy.wrap(subject) + .shadow() + .find("[ui5-dialog]") + ); + } else { + isPopupOpen(() => cy.wrap(subject)); + } }); ``` From 88e5a9840e3cb713d325e70ace9b9fd0080ecfbf Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 19 Aug 2026 12:26:24 +0300 Subject: [PATCH 06/12] refactor: enhance skill --- skills/cypress/SKILL.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/skills/cypress/SKILL.md b/skills/cypress/SKILL.md index 8d29b87771050..00c05a8749ce6 100644 --- a/skills/cypress/SKILL.md +++ b/skills/cypress/SKILL.md @@ -256,9 +256,13 @@ import type ResponsivePopover from "../../../src/ResponsivePopover.js"; // type- ## Writing a Test File ### Location -``` -packages/{package}/cypress/specs/{ComponentName}.cy.tsx -``` + +| File | Purpose | +|------|---------| +| `packages/{package}/cypress/specs/{ComponentName}.cy.tsx` | Main spec | +| `packages/{package}/cypress/specs/{ComponentName}.mobile.cy.tsx` | Phone-only tests — use when tests require `cy.ui5SimulateDevice("phone")` for the entire file | + +Use a separate `.mobile.cy.tsx` file when all tests in it need phone simulation. Do not add `cy.ui5SimulateDevice("phone")` to a `beforeEach` in the main spec just to group mobile tests — put them in a dedicated mobile file instead. ### Minimal structure ```typescript @@ -283,9 +287,11 @@ describe("{ComponentName}", () => { A test only asserting `"exist"` is not meaningful. A meaningful test asserts: - The **rendered state** reflects the props (e.g. `design="Negative"` adds the right CSS class) -- **Events** fire when expected (use `cy.stub` or `cy.spy`) +- **Events** fire when expected (use `cy.stub` or `cy.spy`), including the correct payload and call count - **Accessibility** attributes are correct (`aria-label`, `role`, `aria-disabled`) - **Behavior** after interaction (open/close, value change, focus movement) +- The **keyboard path**, not just the click path — test `realPress` navigation as well as `realClick` +- **Disabled and read-only states do not react** — assert that interactions produce no change **Weak (avoid):** ```typescript @@ -369,6 +375,8 @@ cy.get("[ui5-my-component]").should("have.class", "ui5-my-component-mobile"); **`cy.ui5DOMRef()` is declared in `support/commands.ts` but never implemented — it will fail at runtime. Do not call it.** +**Import every icon you use.** The test bundle contains all icons, so a missing import passes locally and breaks in a real application. + --- ## Mount Helper Functions From baf17d34c73f11c291b2498c8660fef0294cc7d6 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 19 Aug 2026 12:28:53 +0300 Subject: [PATCH 07/12] refactor: enhance reviews section --- skills/cypress/references/REVIEWING.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/skills/cypress/references/REVIEWING.md b/skills/cypress/references/REVIEWING.md index 03ae5bbbb624e..35f8d6dc5763d 100644 --- a/skills/cypress/references/REVIEWING.md +++ b/skills/cypress/references/REVIEWING.md @@ -4,14 +4,19 @@ When reviewing a test file, check: 1. **Meaningful assertions** — is every `it()` block asserting something beyond `"exist"`? 2. **Real events** — is `cy.realClick` / `cy.realPress` / `cy.realType` used instead of simulated events? -3. **Attribute selectors** — are components selected with `[ui5-button]` not `ui5-button`? +3. **Attribute selectors** — are components selected with `[ui5-button]` not `ui5-button`? Does the rule hold inside `.shadow().find(...)` too? 4. **Async safety** — are base API calls using `async .then()` with explicit `await`, not bare calls or `.then(api => api.method())`? 5. **Repetition** — are the same 3+ line interaction sequences duplicated across `it()` blocks? 6. **Shadow DOM** — when asserting on internal structure, is `.shadow().find(...)` used, or better, a POM command? -7. **Event testing** — when testing that events fire, is `cy.stub` / `cy.spy` used rather than relying on side effects? +7. **Event testing** — when testing that events fire, is `cy.stub` / `cy.spy` used rather than relying on side effects? Is the payload and call count asserted? 8. **POM usage** — are raw `.shadow().find("ui5-...")` chains in the spec replaced by POM commands? 9. **TypeScript generics** — does every `cy.get()` that selects a UI5 component use `cy.get()`? 10. **Unique test names** — does every `it()` block have a unique, descriptive name within its `describe` block? 11. **Alias consistency** — is the element aliased before use, and is that alias used consistently rather than re-selecting the same element? +12. **Formatting** — is every chained method on its own line with a leading tab? Are distinct steps separated by blank lines? +13. **Coverage** — are the keyboard path, disabled/read-only states, and ARIA attributes tested, not just the click path? +14. **File placement** — if all tests in the file require phone simulation, should this be a `.mobile.cy.tsx` file instead? +15. **Icon imports** — is every icon used in `cy.mount()` explicitly imported? +16. **Global state cleanup** — does every test that changes language or theme reset it in `afterEach`? For each issue found, either fix it directly or explain what to extract and where to put it. From a91165915361a8a27312553fea973e3c99cc8144 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 19 Aug 2026 12:42:11 +0300 Subject: [PATCH 08/12] refactor: enhance skill --- skills/cypress/SKILL.md | 141 +++++++++++++++++++++++-- skills/cypress/references/REVIEWING.md | 1 + 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/skills/cypress/SKILL.md b/skills/cypress/SKILL.md index 00c05a8749ce6..0e361d842bced 100644 --- a/skills/cypress/SKILL.md +++ b/skills/cypress/SKILL.md @@ -310,15 +310,24 @@ cy.get("[ui5-button]") ``` ### Testing events + +Two patterns exist — use JSX props when the event is exposed as a prop, `addEventListener` otherwise: + ```typescript -cy.mount(); +// When the event is exposed as a JSX prop — pass the stub directly +const onNavigate = cy.stub().as("navigate"); +cy.mount( + + + +); -cy.get("[ui5-button]") - .then($button => { - cy.stub($button[0], "dispatchEvent").as("dispatchEvent"); - }); +cy.get("@navigate") + .should("have.been.calledOnce"); + +// When the event is not exposed as a JSX prop — attach via addEventListener +cy.mount(); -// Or use addEventListener with a stub cy.get("[ui5-button]") .then($el => { $el[0].addEventListener("click", cy.stub().as("clicked")); @@ -359,7 +368,105 @@ cy.wrap({ setLanguage }) ```typescript cy.mount(); cy.ui5SimulateDevice("phone"); -cy.get("[ui5-my-component]").should("have.class", "ui5-my-component-mobile"); +cy.get("[ui5-my-component]") + .should("have.class", "ui5-my-component-mobile"); +``` + +### Freezing time with `cy.clock` + +Components that depend on the current date/time (`Calendar`, `DatePicker`, `DateTimePicker`, `TimePicker`, `DateRangePicker`, `DynamicDateRange`) render differently every day. A test that mounts them without pinning the clock is non-deterministic — it passes today and fails on another date. Freeze the clock in `beforeEach` **before** `cy.mount()`, and only stub the `Date` object: + +```typescript +describe("DatePicker", () => { + beforeEach(() => { + cy.clock(new Date("Jan 15, 2024").getTime(), ["Date"]); + }); + + it("renders the fixed value", () => { + cy.mount(); + // today's date now resolves to Jan 15, 2024 everywhere in the component + }); +}); +``` + +Rules: +- Pass `["Date"]` as the second argument so only `Date` is faked — faking `setTimeout`/`setInterval` (the default) can freeze the component's own async rendering and hang the test. +- Set the clock **before** `cy.mount()` so the component reads the frozen time during its first render. +- Reuse a single `FIXED_VALUE` constant for the value and the clock date so they never drift apart. +- Never assert against "today" computed at runtime — assert against the frozen date literal. + +### Viewport sizing for responsive tests + +`cy.ui5SimulateDevice("phone")` only flips the `isPhone` flag — it does **not** resize the window. To test overflow, breakpoints, or layout that reacts to the actual window size (e.g. `Toolbar`, `Carousel`, `Dialog`, `Tokenizer`, `Popover`), set the real viewport with `cy.viewport(width, height)`: + +```typescript +it("overflows items into the menu below 400px", () => { + cy.viewport(300, 600); + cy.mount( + + + + + + ); + + cy.get("[ui5-toolbar]") + .shadow() + .find(".ui5-tb-overflow-btn") + .should("be.visible"); +}); +``` + +Rules: +- Call `cy.viewport()` **before** `cy.mount()` when the first render must already reflect the size. +- To restore the configured default within a test, use `cy.viewport(Cypress.config("viewportWidth"), Cypress.config("viewportHeight"))` rather than a hard-coded size. +- Use `cy.viewport()` for pixel-size / overflow behavior; use `cy.ui5SimulateDevice("phone")` for phone-specific rendering paths. They are independent — combine them when a test needs both. + +### Disabling animations + +Use `setAnimationMode("none")` in a `before()` hook when testing components that have animations, to prevent timing-dependent failures: + +```typescript +import { setAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js"; + +before(() => { + cy.wrap({ setAnimationMode }) + .then(async ({ setAnimationMode }) => { + await setAnimationMode("none"); + }); +}); +``` + +### Wrapper elements for layout testing + +When testing responsive or layout-dependent behavior, wrap the component in a `div` with inline styles: + +```typescript +cy.mount( +
+ + Link 1 + Link 2 + +
+); +``` + +### Form validity testing + +For form components, test `validity`, `formValidity`, `checkValidity()`, `reportValidity()`, and the `:invalid` CSS pseudo-class: + +```typescript +cy.get("#cb") + .then($el => { + const checkbox = $el[0] as CheckBox; + expect(checkbox.validity.valueMissing).to.be.true; + expect(checkbox.validity.valid).to.be.false; + expect(checkbox.checkValidity()).to.be.false; + }); + +cy.get("#cb:invalid") + .should("exist"); ``` ### Available framework commands @@ -554,6 +661,26 @@ cy.get("#myInput") .should("have.prop", "focused", true); ``` +### Asserting on computed styles and CSS custom properties + +When a test needs to verify applied styling — a CSS class is not enough, or the component publishes a CSS custom property (`--_ui5_...`) on its host — read the computed style inside a `.then()` callback with `getComputedStyle(...).getPropertyValue(...)`. Trim the result, since custom-property values are returned with leading whitespace: + +```typescript +cy.get("[ui5-input]") + .shadow() + .find("[ui5-icon]") + .then($icon => { + const padding = getComputedStyle($icon[0]) + .getPropertyValue("--_ui5_input_icon_state_padding") + .trim(); + expect(padding).to.not.equal(""); + }); +``` + +Rules: +- Assert the specific declared value where one exists (`.to.equal("none")`), not just `.to.not.equal("")`. +- Only reach for computed styles when a class assertion cannot express the check — prefer `should("have.class", ...)` when a class reflects the state. +- Private custom properties (`--_ui5_*`) are internal contracts; when asserting on them, add a short comment explaining which selector publishes the value. ### Asserting on events diff --git a/skills/cypress/references/REVIEWING.md b/skills/cypress/references/REVIEWING.md index 35f8d6dc5763d..0cf60309c9daa 100644 --- a/skills/cypress/references/REVIEWING.md +++ b/skills/cypress/references/REVIEWING.md @@ -18,5 +18,6 @@ When reviewing a test file, check: 14. **File placement** — if all tests in the file require phone simulation, should this be a `.mobile.cy.tsx` file instead? 15. **Icon imports** — is every icon used in `cy.mount()` explicitly imported? 16. **Global state cleanup** — does every test that changes language or theme reset it in `afterEach`? +17. **No stray `.only` / `.skip`** — is the file free of `it.only`, `describe.only`, `it.skip` left over from local debugging? They must be removed before committing. For each issue found, either fix it directly or explain what to extract and where to put it. From 68d07eefaf45152f5c9f89752aafbde37a44fac5 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 19 Aug 2026 12:51:57 +0300 Subject: [PATCH 09/12] refactor: extract section --- skills/cypress/SKILL.md | 394 +------------------- skills/cypress/references/WRITING-SPECS.md | 396 +++++++++++++++++++++ 2 files changed, 400 insertions(+), 390 deletions(-) create mode 100644 skills/cypress/references/WRITING-SPECS.md diff --git a/skills/cypress/SKILL.md b/skills/cypress/SKILL.md index 0e361d842bced..723a6b5ac891d 100644 --- a/skills/cypress/SKILL.md +++ b/skills/cypress/SKILL.md @@ -21,10 +21,10 @@ This skill helps write and review Cypress component tests for UI5 web components | Task | Files to load | |---|---| -| Writing a new spec file | This file only | +| Writing a new spec file | This file + [`WRITING-SPECS.md`](./references/WRITING-SPECS.md) | | Adding or modifying custom commands | This file + [`COMMANDS.md`](./references/COMMANDS.md) | | Reviewing an existing spec | This file + [`REVIEWING.md`](./references/REVIEWING.md) | -| Both writing a spec and adding commands | This file + [`COMMANDS.md`](./references/COMMANDS.md) | +| Both writing a spec and adding commands | This file + [`WRITING-SPECS.md`](./references/WRITING-SPECS.md) + [`COMMANDS.md`](./references/COMMANDS.md) | | Debugging a flaky or intermittent test | This file + [`FLAKY-TESTS.md`](./references/FLAKY-TESTS.md) | --- @@ -255,393 +255,7 @@ import type ResponsivePopover from "../../../src/ResponsivePopover.js"; // type- ## Writing a Test File -### Location - -| File | Purpose | -|------|---------| -| `packages/{package}/cypress/specs/{ComponentName}.cy.tsx` | Main spec | -| `packages/{package}/cypress/specs/{ComponentName}.mobile.cy.tsx` | Phone-only tests — use when tests require `cy.ui5SimulateDevice("phone")` for the entire file | - -Use a separate `.mobile.cy.tsx` file when all tests in it need phone simulation. Do not add `cy.ui5SimulateDevice("phone")` to a `beforeEach` in the main spec just to group mobile tests — put them in a dedicated mobile file instead. - -### Minimal structure -```typescript -import ComponentName from "../../src/ComponentName.js"; - -describe("{ComponentName}", () => { - it("renders and shows expected default state", () => { - cy.mount(); - - cy.get("[ui5-component-name]") - .should("exist"); - // Add at least one meaningful assertion beyond "exist" - cy.get("[ui5-component-name]") - .shadow() - .find(".ui5-component-root") - .should("be.visible"); - }); -}); -``` - -### What makes a test meaningful - -A test only asserting `"exist"` is not meaningful. A meaningful test asserts: -- The **rendered state** reflects the props (e.g. `design="Negative"` adds the right CSS class) -- **Events** fire when expected (use `cy.stub` or `cy.spy`), including the correct payload and call count -- **Accessibility** attributes are correct (`aria-label`, `role`, `aria-disabled`) -- **Behavior** after interaction (open/close, value change, focus movement) -- The **keyboard path**, not just the click path — test `realPress` navigation as well as `realClick` -- **Disabled and read-only states do not react** — assert that interactions produce no change - -**Weak (avoid):** -```typescript -cy.get("[ui5-button]") - .should("exist"); -``` - -**Strong (prefer):** -```typescript -cy.get("[ui5-button]") - .should("have.attr", "disabled"); -cy.get("[ui5-button]") - .shadow() - .find("button") - .should("have.attr", "disabled"); -``` - -### Testing events - -Two patterns exist — use JSX props when the event is exposed as a prop, `addEventListener` otherwise: - -```typescript -// When the event is exposed as a JSX prop — pass the stub directly -const onNavigate = cy.stub().as("navigate"); -cy.mount( - - - -); - -cy.get("@navigate") - .should("have.been.calledOnce"); - -// When the event is not exposed as a JSX prop — attach via addEventListener -cy.mount(); - -cy.get("[ui5-button]") - .then($el => { - $el[0].addEventListener("click", cy.stub().as("clicked")); - }); - -cy.get("[ui5-button]") - .realClick(); -cy.get("@clicked") - .should("have.been.called"); -``` - -### Configuration (theme, language) - -```typescript -import { setTheme, getTheme } from "@ui5/webcomponents-base/dist/config/Theme.js"; - -cy.wrap({ setTheme }) - .then(async ({ setTheme }) => { - await setTheme("sap_horizon_hcb"); - }); - -cy.wrap({ getTheme }) - .then(({ getTheme }) => getTheme()) - .should("equal", "sap_horizon_hcb"); -``` - -For language tests, always import Assets.js: -```typescript -import "../../src/Assets.js"; // required for extra languages - -cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("bg"); - }); -``` - -### Mobile / device simulation -```typescript -cy.mount(); -cy.ui5SimulateDevice("phone"); -cy.get("[ui5-my-component]") - .should("have.class", "ui5-my-component-mobile"); -``` - -### Freezing time with `cy.clock` - -Components that depend on the current date/time (`Calendar`, `DatePicker`, `DateTimePicker`, `TimePicker`, `DateRangePicker`, `DynamicDateRange`) render differently every day. A test that mounts them without pinning the clock is non-deterministic — it passes today and fails on another date. Freeze the clock in `beforeEach` **before** `cy.mount()`, and only stub the `Date` object: - -```typescript -describe("DatePicker", () => { - beforeEach(() => { - cy.clock(new Date("Jan 15, 2024").getTime(), ["Date"]); - }); - - it("renders the fixed value", () => { - cy.mount(); - // today's date now resolves to Jan 15, 2024 everywhere in the component - }); -}); -``` - -Rules: -- Pass `["Date"]` as the second argument so only `Date` is faked — faking `setTimeout`/`setInterval` (the default) can freeze the component's own async rendering and hang the test. -- Set the clock **before** `cy.mount()` so the component reads the frozen time during its first render. -- Reuse a single `FIXED_VALUE` constant for the value and the clock date so they never drift apart. -- Never assert against "today" computed at runtime — assert against the frozen date literal. - -### Viewport sizing for responsive tests - -`cy.ui5SimulateDevice("phone")` only flips the `isPhone` flag — it does **not** resize the window. To test overflow, breakpoints, or layout that reacts to the actual window size (e.g. `Toolbar`, `Carousel`, `Dialog`, `Tokenizer`, `Popover`), set the real viewport with `cy.viewport(width, height)`: - -```typescript -it("overflows items into the menu below 400px", () => { - cy.viewport(300, 600); - cy.mount( - - - - - - ); - - cy.get("[ui5-toolbar]") - .shadow() - .find(".ui5-tb-overflow-btn") - .should("be.visible"); -}); -``` - -Rules: -- Call `cy.viewport()` **before** `cy.mount()` when the first render must already reflect the size. -- To restore the configured default within a test, use `cy.viewport(Cypress.config("viewportWidth"), Cypress.config("viewportHeight"))` rather than a hard-coded size. -- Use `cy.viewport()` for pixel-size / overflow behavior; use `cy.ui5SimulateDevice("phone")` for phone-specific rendering paths. They are independent — combine them when a test needs both. - -### Disabling animations - -Use `setAnimationMode("none")` in a `before()` hook when testing components that have animations, to prevent timing-dependent failures: - -```typescript -import { setAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js"; - -before(() => { - cy.wrap({ setAnimationMode }) - .then(async ({ setAnimationMode }) => { - await setAnimationMode("none"); - }); -}); -``` - -### Wrapper elements for layout testing - -When testing responsive or layout-dependent behavior, wrap the component in a `div` with inline styles: - -```typescript -cy.mount( -
- - Link 1 - Link 2 - -
-); -``` - -### Form validity testing - -For form components, test `validity`, `formValidity`, `checkValidity()`, `reportValidity()`, and the `:invalid` CSS pseudo-class: - -```typescript -cy.get("#cb") - .then($el => { - const checkbox = $el[0] as CheckBox; - expect(checkbox.validity.valueMissing).to.be.true; - expect(checkbox.validity.valid).to.be.false; - expect(checkbox.checkValidity()).to.be.false; - }); - -cy.get("#cb:invalid") - .should("exist"); -``` - -### Available framework commands - -| Command | Behaviour | -|---------|-----------| -| `cy.mount(jsx)` | Mount, wait for render, wait for `document.fonts.ready` | -| `cy.waitRenderFinished()` | Drain the render queue — use instead of `cy.wait()` | -| `cy.ui5SimulateDevice("phone")` | Force phone behaviour; `"phone"` is the only valid device | -| `cy.ui5AssertValidityState(partial)` | Assert any subset of form validity state | -| `realClick`, `realHover`, `realPress`, `realType` | Wait for render before dispatching real events | -| `cy.screenshot` | Honoured with `SCREENSHOT_DELAY` env var | - -**`cy.ui5DOMRef()` is declared in `support/commands.ts` but never implemented — it will fail at runtime. Do not call it.** - -**Import every icon you use.** The test bundle contains all icons, so a missing import passes locally and breaks in a real application. - ---- - -## Mount Helper Functions - -When a spec needs the same component configuration in many `it()` blocks, extract it into a named helper function at the top of the file rather than repeating the JSX inline. This keeps each `it()` block focused on the assertion, not the setup. - -```typescript -// Define helpers at the top of the spec file, before describe() -const getDefaultCalendar = (date: Date) => { - const day = String(date.getDate()).padStart(2, "0"); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const year = date.getFullYear(); - - return ( - - - - ); -}; - -const getCalendarWithDisabledDates = (id: string, formatPattern: string, ranges: DateRange[]) => ( - - {ranges.map((range, idx) => ( - - ))} - -); - -// Use in tests -describe("Calendar", () => { - it("navigates to the current day", () => { - cy.mount(getDefaultCalendar(new Date(Date.UTC(2000, 10, 22)))); - // ... - }); -}); -``` - -Use fragment wrappers (`<>...`) when a helper needs to render multiple sibling components: - -```typescript -const getCalendarsWithWeekNumbers = () => (<> - - - - - - -); -``` - ---- - -## `beforeEach` and `afterEach` - -Use `beforeEach` and `afterEach` at the `describe` level to share setup and teardown across every test in that block. Do not use them for things that only one test needs — keep those inline. - -### `beforeEach` — shared mount or shared state - -**Shared component mount:** When every test in a `describe` block uses the same component tree, mount it in `beforeEach` rather than repeating `cy.mount()` in every `it()`. - -```typescript -describe("ComboBox - keyboard navigation", () => { - beforeEach(() => { - cy.mount(<> - - - - - - ); - }); - - it("moves focus to the first link in the value state message", () => { - cy.get("[ui5-combobox]") - .realClick(); - // ... - }); - - it("moves focus back on Escape", () => { - cy.get("[ui5-combobox]") - .realClick(); - cy.realPress("Escape"); - // ... - }); -}); -``` - -**Shared device/environment setup:** When all tests in a block require the same device simulation or global config state, set it in `beforeEach`: - -```typescript -describe("ComboBox - mobile", () => { - beforeEach(() => { - cy.ui5SimulateDevice("phone"); - }); - - it("renders the mobile picker", () => { - cy.mount(); - // ... - }); -}); -``` - -**Shared language baseline:** When a `describe` block depends on a specific language being set, ensure it in `beforeEach` so tests don't rely on whatever the previous test left: - -```typescript -describe("Calendar accessibility", () => { - beforeEach(() => { - cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("en"); - }); - }); - // ... -}); -``` - -### `afterEach` — mandatory cleanup for global state - -Any test that changes global configuration (language, theme) **must reset it in `afterEach`**. Without cleanup, a failing test corrupts state for every test that follows. - -**Language reset:** -```typescript -import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js"; -import "../../src/Assets.js"; // required for non-English languages - -describe("DatePicker - language", () => { - afterEach(() => { - cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("en"); - }); - }); - - it("displays Bulgarian month names", () => { - cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("bg"); - }); - // ... - }); -}); -``` - -**Theme reset** — same pattern, reset to `"sap_horizon"`: -```typescript -afterEach(() => { - cy.wrap({ setTheme }) - .then(async ({ setTheme }) => { - await setTheme("sap_horizon"); - }); -}); -``` - -### When NOT to use beforeEach/afterEach - -- Do not mount in `beforeEach` when tests need different component configurations — use mount helper functions instead (see above). -- Do not use `afterEach` to reset state that the next `cy.mount()` will implicitly reset anyway (e.g. component-local state). -- Do not use `beforeEach` for setup that only one or two tests need — keep it inline. +When writing or restructuring a spec file, load [`WRITING-SPECS.md`](./references/WRITING-SPECS.md). It covers file location, minimal structure, what makes a test meaningful, event testing, configuration (theme/language), device simulation, `cy.clock`, viewport sizing, disabling animations, form validity, the framework command reference, mount helper functions, and shared `beforeEach`/`afterEach` setup. --- @@ -753,7 +367,7 @@ See [`FLAKY-TESTS.md`](./references/FLAKY-TESTS.md) for a full list of causes an 1. **Read the component source** (`packages/{package}/src/{ComponentName}.ts`) to understand props, events, and shadow DOM structure 2. **Check for existing commands** in `packages/{package}/cypress/support/commands/` — don't duplicate -3. **Write the spec file** using the patterns above, with `cy.get()` generics throughout +3. **Write the spec file** following [`WRITING-SPECS.md`](./references/WRITING-SPECS.md), with `cy.get()` generics throughout 4. **Extract commands** for any interaction sequence used more than once — see [`COMMANDS.md`](./references/COMMANDS.md) 5. **Create the commands file** following the template and conventions in [`COMMANDS.md`](./references/COMMANDS.md) 6. **Update commands.ts** with the import (alphabetical order) diff --git a/skills/cypress/references/WRITING-SPECS.md b/skills/cypress/references/WRITING-SPECS.md new file mode 100644 index 0000000000000..dca23995bddcd --- /dev/null +++ b/skills/cypress/references/WRITING-SPECS.md @@ -0,0 +1,396 @@ +# Writing Spec Files + +Detailed conventions for authoring a Cypress spec: file layout, meaningful assertions, event testing, configuration, device/viewport/time control, mount helpers, and shared `beforeEach`/`afterEach` setup. Load this alongside `SKILL.md` whenever writing or restructuring a spec file. + +--- + +## Writing a Test File + +### Location + +| File | Purpose | +|------|---------| +| `packages/{package}/cypress/specs/{ComponentName}.cy.tsx` | Main spec | +| `packages/{package}/cypress/specs/{ComponentName}.mobile.cy.tsx` | Phone-only tests — use when tests require `cy.ui5SimulateDevice("phone")` for the entire file | + +Use a separate `.mobile.cy.tsx` file when all tests in it need phone simulation. Do not add `cy.ui5SimulateDevice("phone")` to a `beforeEach` in the main spec just to group mobile tests — put them in a dedicated mobile file instead. + +### Minimal structure +```typescript +import ComponentName from "../../src/ComponentName.js"; + +describe("{ComponentName}", () => { + it("renders and shows expected default state", () => { + cy.mount(); + + cy.get("[ui5-component-name]") + .should("exist"); + // Add at least one meaningful assertion beyond "exist" + cy.get("[ui5-component-name]") + .shadow() + .find(".ui5-component-root") + .should("be.visible"); + }); +}); +``` + +### What makes a test meaningful + +A test only asserting `"exist"` is not meaningful. A meaningful test asserts: +- The **rendered state** reflects the props (e.g. `design="Negative"` adds the right CSS class) +- **Events** fire when expected (use `cy.stub` or `cy.spy`), including the correct payload and call count +- **Accessibility** attributes are correct (`aria-label`, `role`, `aria-disabled`) +- **Behavior** after interaction (open/close, value change, focus movement) +- The **keyboard path**, not just the click path — test `realPress` navigation as well as `realClick` +- **Disabled and read-only states do not react** — assert that interactions produce no change + +**Weak (avoid):** +```typescript +cy.get("[ui5-button]") + .should("exist"); +``` + +**Strong (prefer):** +```typescript +cy.get("[ui5-button]") + .should("have.attr", "disabled"); +cy.get("[ui5-button]") + .shadow() + .find("button") + .should("have.attr", "disabled"); +``` + +### Testing events + +Two patterns exist — use JSX props when the event is exposed as a prop, `addEventListener` otherwise: + +```typescript +// When the event is exposed as a JSX prop — pass the stub directly +const onNavigate = cy.stub().as("navigate"); +cy.mount( + + + +); + +cy.get("@navigate") + .should("have.been.calledOnce"); + +// When the event is not exposed as a JSX prop — attach via addEventListener +cy.mount(); + +cy.get("[ui5-button]") + .then($el => { + $el[0].addEventListener("click", cy.stub().as("clicked")); + }); + +cy.get("[ui5-button]") + .realClick(); +cy.get("@clicked") + .should("have.been.called"); +``` + +### Configuration (theme, language) + +```typescript +import { setTheme, getTheme } from "@ui5/webcomponents-base/dist/config/Theme.js"; + +cy.wrap({ setTheme }) + .then(async ({ setTheme }) => { + await setTheme("sap_horizon_hcb"); + }); + +cy.wrap({ getTheme }) + .then(({ getTheme }) => getTheme()) + .should("equal", "sap_horizon_hcb"); +``` + +For language tests, always import Assets.js: +```typescript +import "../../src/Assets.js"; // required for extra languages + +cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("bg"); + }); +``` + +### Mobile / device simulation +```typescript +cy.mount(); +cy.ui5SimulateDevice("phone"); +cy.get("[ui5-my-component]") + .should("have.class", "ui5-my-component-mobile"); +``` + +### Freezing time with `cy.clock` + +Components that depend on the current date/time (`Calendar`, `DatePicker`, `DateTimePicker`, `TimePicker`, `DateRangePicker`, `DynamicDateRange`) render differently every day. A test that mounts them without pinning the clock is non-deterministic — it passes today and fails on another date. Freeze the clock in `beforeEach` **before** `cy.mount()`, and only stub the `Date` object: + +```typescript +describe("DatePicker", () => { + beforeEach(() => { + cy.clock(new Date("Jan 15, 2024").getTime(), ["Date"]); + }); + + it("renders the fixed value", () => { + cy.mount(); + // today's date now resolves to Jan 15, 2024 everywhere in the component + }); +}); +``` + +Rules: +- Pass `["Date"]` as the second argument so only `Date` is faked — faking `setTimeout`/`setInterval` (the default) can freeze the component's own async rendering and hang the test. +- Set the clock **before** `cy.mount()` so the component reads the frozen time during its first render. +- Reuse a single `FIXED_VALUE` constant for the value and the clock date so they never drift apart. +- Never assert against "today" computed at runtime — assert against the frozen date literal. + +### Viewport sizing for responsive tests + +`cy.ui5SimulateDevice("phone")` only flips the `isPhone` flag — it does **not** resize the window. To test overflow, breakpoints, or layout that reacts to the actual window size (e.g. `Toolbar`, `Carousel`, `Dialog`, `Tokenizer`, `Popover`), set the real viewport with `cy.viewport(width, height)`: + +```typescript +it("overflows items into the menu below 400px", () => { + cy.viewport(300, 600); + cy.mount( + + + + + + ); + + cy.get("[ui5-toolbar]") + .shadow() + .find(".ui5-tb-overflow-btn") + .should("be.visible"); +}); +``` + +Rules: +- Call `cy.viewport()` **before** `cy.mount()` when the first render must already reflect the size. +- To restore the configured default within a test, use `cy.viewport(Cypress.config("viewportWidth"), Cypress.config("viewportHeight"))` rather than a hard-coded size. +- Use `cy.viewport()` for pixel-size / overflow behavior; use `cy.ui5SimulateDevice("phone")` for phone-specific rendering paths. They are independent — combine them when a test needs both. + +### Disabling animations + +Use `setAnimationMode("none")` in a `before()` hook when testing components that have animations, to prevent timing-dependent failures: + +```typescript +import { setAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js"; + +before(() => { + cy.wrap({ setAnimationMode }) + .then(async ({ setAnimationMode }) => { + await setAnimationMode("none"); + }); +}); +``` + +### Wrapper elements for layout testing + +When testing responsive or layout-dependent behavior, wrap the component in a `div` with inline styles: + +```typescript +cy.mount( +
+ + Link 1 + Link 2 + +
+); +``` + +### Form validity testing + +For form components, test `validity`, `formValidity`, `checkValidity()`, `reportValidity()`, and the `:invalid` CSS pseudo-class: + +```typescript +cy.get("#cb") + .then($el => { + const checkbox = $el[0] as CheckBox; + expect(checkbox.validity.valueMissing).to.be.true; + expect(checkbox.validity.valid).to.be.false; + expect(checkbox.checkValidity()).to.be.false; + }); + +cy.get("#cb:invalid") + .should("exist"); +``` + +### Available framework commands + +| Command | Behaviour | +|---------|-----------| +| `cy.mount(jsx)` | Mount, wait for render, wait for `document.fonts.ready` | +| `cy.waitRenderFinished()` | Drain the render queue — use instead of `cy.wait()` | +| `cy.ui5SimulateDevice("phone")` | Force phone behaviour; `"phone"` is the only valid device | +| `cy.ui5AssertValidityState(partial)` | Assert any subset of form validity state | +| `realClick`, `realHover`, `realPress`, `realType` | Wait for render before dispatching real events | +| `cy.screenshot` | Honoured with `SCREENSHOT_DELAY` env var | + +**`cy.ui5DOMRef()` is declared in `support/commands.ts` but never implemented — it will fail at runtime. Do not call it.** + +**Import every icon you use.** The test bundle contains all icons, so a missing import passes locally and breaks in a real application. + +--- + +## Mount Helper Functions + +When a spec needs the same component configuration in many `it()` blocks, extract it into a named helper function at the top of the file rather than repeating the JSX inline. This keeps each `it()` block focused on the assertion, not the setup. + +```typescript +// Define helpers at the top of the spec file, before describe() +const getDefaultCalendar = (date: Date) => { + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + + return ( + + + + ); +}; + +const getCalendarWithDisabledDates = (id: string, formatPattern: string, ranges: DateRange[]) => ( + + {ranges.map((range, idx) => ( + + ))} + +); + +// Use in tests +describe("Calendar", () => { + it("navigates to the current day", () => { + cy.mount(getDefaultCalendar(new Date(Date.UTC(2000, 10, 22)))); + // ... + }); +}); +``` + +Use fragment wrappers (`<>...`) when a helper needs to render multiple sibling components: + +```typescript +const getCalendarsWithWeekNumbers = () => (<> + + + + + + +); +``` + +--- + +## `beforeEach` and `afterEach` + +Use `beforeEach` and `afterEach` at the `describe` level to share setup and teardown across every test in that block. Do not use them for things that only one test needs — keep those inline. + +### `beforeEach` — shared mount or shared state + +**Shared component mount:** When every test in a `describe` block uses the same component tree, mount it in `beforeEach` rather than repeating `cy.mount()` in every `it()`. + +```typescript +describe("ComboBox - keyboard navigation", () => { + beforeEach(() => { + cy.mount(<> + + + + + + ); + }); + + it("moves focus to the first link in the value state message", () => { + cy.get("[ui5-combobox]") + .realClick(); + // ... + }); + + it("moves focus back on Escape", () => { + cy.get("[ui5-combobox]") + .realClick(); + cy.realPress("Escape"); + // ... + }); +}); +``` + +**Shared device/environment setup:** When all tests in a block require the same device simulation or global config state, set it in `beforeEach`: + +```typescript +describe("ComboBox - mobile", () => { + beforeEach(() => { + cy.ui5SimulateDevice("phone"); + }); + + it("renders the mobile picker", () => { + cy.mount(); + // ... + }); +}); +``` + +**Shared language baseline:** When a `describe` block depends on a specific language being set, ensure it in `beforeEach` so tests don't rely on whatever the previous test left: + +```typescript +describe("Calendar accessibility", () => { + beforeEach(() => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("en"); + }); + }); + // ... +}); +``` + +### `afterEach` — mandatory cleanup for global state + +Any test that changes global configuration (language, theme) **must reset it in `afterEach`**. Without cleanup, a failing test corrupts state for every test that follows. + +**Language reset:** +```typescript +import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js"; +import "../../src/Assets.js"; // required for non-English languages + +describe("DatePicker - language", () => { + afterEach(() => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("en"); + }); + }); + + it("displays Bulgarian month names", () => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("bg"); + }); + // ... + }); +}); +``` + +**Theme reset** — same pattern, reset to `"sap_horizon"`: +```typescript +afterEach(() => { + cy.wrap({ setTheme }) + .then(async ({ setTheme }) => { + await setTheme("sap_horizon"); + }); +}); +``` + +### When NOT to use beforeEach/afterEach + +- Do not mount in `beforeEach` when tests need different component configurations — use mount helper functions instead (see above). +- Do not use `afterEach` to reset state that the next `cy.mount()` will implicitly reset anyway (e.g. component-local state). +- Do not use `beforeEach` for setup that only one or two tests need — keep it inline. + From e2f3cf4b51989a92ab04d26c6f9d677bd0dfd77f Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 19 Aug 2026 13:00:51 +0300 Subject: [PATCH 10/12] refactor: enhance routing --- skills/cypress/SKILL.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/skills/cypress/SKILL.md b/skills/cypress/SKILL.md index 723a6b5ac891d..41e96ac993c25 100644 --- a/skills/cypress/SKILL.md +++ b/skills/cypress/SKILL.md @@ -19,13 +19,14 @@ This skill helps write and review Cypress component tests for UI5 web components ### Which files to load -| Task | Files to load | +`SKILL.md` (this file) is always loaded. Load the additional references below that match your task — combine them when a task spans several. + +| When you are… | Also load | |---|---| -| Writing a new spec file | This file + [`WRITING-SPECS.md`](./references/WRITING-SPECS.md) | -| Adding or modifying custom commands | This file + [`COMMANDS.md`](./references/COMMANDS.md) | -| Reviewing an existing spec | This file + [`REVIEWING.md`](./references/REVIEWING.md) | -| Both writing a spec and adding commands | This file + [`WRITING-SPECS.md`](./references/WRITING-SPECS.md) + [`COMMANDS.md`](./references/COMMANDS.md) | -| Debugging a flaky or intermittent test | This file + [`FLAKY-TESTS.md`](./references/FLAKY-TESTS.md) | +| Writing or restructuring a spec file | [`WRITING-SPECS.md`](./references/WRITING-SPECS.md) | +| Adding or modifying custom commands | [`COMMANDS.md`](./references/COMMANDS.md) | +| Reviewing an existing spec | [`REVIEWING.md`](./references/REVIEWING.md) (checklist) + [`WRITING-SPECS.md`](./references/WRITING-SPECS.md) (the standard) | +| Debugging a flaky or intermittent test | [`FLAKY-TESTS.md`](./references/FLAKY-TESTS.md) | --- From 0222ea298a9bfdded12deebf1992efb34a418719 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 19 Aug 2026 13:05:43 +0300 Subject: [PATCH 11/12] refactor: remove sections --- skills/cypress/SKILL.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/skills/cypress/SKILL.md b/skills/cypress/SKILL.md index 41e96ac993c25..5c6b2896ae5a6 100644 --- a/skills/cypress/SKILL.md +++ b/skills/cypress/SKILL.md @@ -254,12 +254,6 @@ import type ResponsivePopover from "../../../src/ResponsivePopover.js"; // type- --- -## Writing a Test File - -When writing or restructuring a spec file, load [`WRITING-SPECS.md`](./references/WRITING-SPECS.md). It covers file location, minimal structure, what makes a test meaningful, event testing, configuration (theme/language), device simulation, `cy.clock`, viewport sizing, disabling animations, form validity, the framework command reference, mount helper functions, and shared `beforeEach`/`afterEach` setup. - ---- - ## Asserting Use `have.attr` for reflected properties and ARIA attributes; use `have.prop` for state that is not reflected to an attribute: @@ -358,12 +352,6 @@ For non-default locales, always import `Assets.js` (see "Configuration" above). --- -## Flaky Tests - -See [`FLAKY-TESTS.md`](./references/FLAKY-TESTS.md) for a full list of causes and recipes. Load it only when debugging an intermittent failure. - ---- - ## Workflow 1. **Read the component source** (`packages/{package}/src/{ComponentName}.ts`) to understand props, events, and shadow DOM structure From f12f5d03b63b6785dd2d34c64b8503e66ea61d53 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 19 Aug 2026 13:08:51 +0300 Subject: [PATCH 12/12] refactor: update section name --- skills/cypress/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/cypress/SKILL.md b/skills/cypress/SKILL.md index 5c6b2896ae5a6..ab604df031b72 100644 --- a/skills/cypress/SKILL.md +++ b/skills/cypress/SKILL.md @@ -30,7 +30,7 @@ This skill helps write and review Cypress component tests for UI5 web components --- -## Quick Rules +## Core Rules ### Interacting — always use real events