Skip to content

Commit 32e8532

Browse files
committed
feat(mcp-server): add web component testing docs + isAbstract flag
1 parent 7d91c9a commit 32e8532

6 files changed

Lines changed: 224 additions & 0 deletions

File tree

docs/knowledge-base/Testing.mdx

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { Footer, TableOfContent } from '@sb/components';
2+
import { Meta } from '@storybook/addon-docs/blocks';
3+
4+
<Meta title="Testing" />
5+
6+
# Testing
7+
8+
<TableOfContent />
9+
10+
This guide covers the particularities of testing UI5 Web Components for React — the things that
11+
behave differently from plain React components because these are custom elements with Shadow DOM.
12+
It is **not tied to a specific test runner**: the advice applies to any tool that drives a
13+
**real browser** (for example Cypress, Playwright, or WebdriverIO).
14+
15+
> **Use a real browser, not JSDOM.**
16+
> UI5 Web Components are custom elements that rely on the real DOM — Shadow DOM, custom-element
17+
> upgrades, slots, and CSS. Virtual-DOM environments such as JSDOM (the default for Jest and
18+
> Vitest) do not implement these, so components will not render or behave correctly. Choose a
19+
> runner that executes in an actual browser.
20+
21+
## Component test setup
22+
23+
When you mount a component in isolation (component testing), you have to provide the same runtime
24+
context the real app would. Two things are required:
25+
26+
1. **Wrap the mounted component in `ThemeProvider`, as the outermost element.** Components read
27+
theming and configuration from it, so it must sit above whatever you render.
28+
29+
```tsx
30+
mount(
31+
<ThemeProvider>
32+
<YourComponentUnderTest />
33+
</ThemeProvider>,
34+
);
35+
```
36+
37+
Set this up once in your runner's mount wrapper (e.g. a custom Cypress `mount` command, or
38+
Playwright CT's `beforeMount` hook) so every test gets it automatically.
39+
40+
2. **Import the assets** once in your test setup if the test depends on translations (i18n),
41+
theming, or locale-specific (CLDR) formatting — otherwise components render with English text
42+
only and theme switching won't work:
43+
44+
```tsx
45+
import '@ui5/webcomponents-react/dist/Assets.js';
46+
```
47+
48+
> This setup applies to **component testing**. In **end-to-end** tests you drive the real
49+
> application, which already renders `ThemeProvider` and imports assets — so you don't add either
50+
> in the test itself.
51+
52+
## Selecting elements
53+
54+
Select the host element by its **attribute selector** — the tag name written as an attribute in
55+
square brackets, e.g. `[ui5-button]` for `<ui5-button>` — rather than by the tag name itself.
56+
This keeps selectors working when custom-element [scoping](https://ui5.github.io/webcomponents/docs/advanced/scoping/)
57+
adds a suffix to the tag name (e.g. `<ui5-button-a1b2c3>`).
58+
59+
```js
60+
// ✅ attribute selector — robust against scoping
61+
get('[ui5-button]')
62+
63+
// ❌ tag-name selector — breaks when scoping is enabled
64+
get('ui5-button')
65+
```
66+
67+
Component internals live in the Shadow DOM. Real-browser runners can pierce it, so if a test
68+
genuinely needs an internal element you can query and assert on it — for example Cypress with
69+
`includeShadowDom: true`, or Playwright, whose locators pierce shadow roots by default. Prefer
70+
asserting on the component's public surface (props, attributes, emitted events) where you can;
71+
reach into the shadow tree only when there's no equivalent public signal, and expect such
72+
assertions to be more brittle since internal structure can change between versions.
73+
74+
To identify which item fired a collection event (e.g. from `e.detail`), see
75+
[IDs via dataset](https://ui5.github.io/webcomponents-react/?path=/docs/knowledge-base-ids-via-dataset--docs).
76+
77+
## Web components are asynchronous
78+
79+
This is the pitfall behind most flaky UI5 tests. A UI5 Web Component is not fully usable the
80+
moment it appears in the DOM — it becomes ready in **two asynchronous stages**:
81+
82+
1. **Custom element definition** — the browser has to register (define + upgrade) the custom
83+
element before the host is anything more than an inert placeholder. This happens
84+
asynchronously, especially when component modules are loaded on demand.
85+
2. **Internal rendering** — even after definition, the component renders its own Shadow DOM (and
86+
sometimes nested sub-components) asynchronously. So the shadow parts, inner elements, and
87+
final layout appear a tick _after_ the host exists.
88+
89+
A test that queries an internal node, reads a computed style, or clicks a shadow element **too
90+
early** will intermittently fail — the target may not exist or may not be positioned yet.
91+
92+
**Wait by asserting on the element, not with a fixed delay.** The reliable, framework-agnostic
93+
approach is to let the runner's retrying assertions wait for the actual state you care about —
94+
they keep re-checking until the async definition and rendering have completed:
95+
96+
- Wait for the host to exist / be visible via its attribute selector (e.g. `[ui5-button]`).
97+
- For interactions with internals, assert on the specific target (a shadow part, an option, a
98+
menu item) — prefer a visibility assertion over a mere existence check, since a node can be
99+
present in the DOM before it is actually shown (see overlays below).
100+
- For overlays, wait on the state that indicates readiness (e.g. a popover's `open` attribute —
101+
see the "Popovers, menus, and dialogs" section below).
102+
103+
Avoid fixed timeouts (`wait(500)`) to "let the component settle" — they are both flaky and slow.
104+
Gate on a retrying assertion of the element or its state instead.
105+
106+
## Abstract components render into their parent
107+
108+
Some components are **abstract** (marked `@abstract`). Unlike a normal component, an abstract
109+
component does **not** render its own top-level shadow root — it renders into the DOM of its
110+
**parent**. Typical examples are slotted item components such as `SuggestionItem`, `Tab`,
111+
`ToolbarButton`, `WizardStep`, and `SideNavigationItem`.
112+
113+
Abstract components are marked as such in their API documentation.
114+
115+
This matters for testing because the host placeholder element you write in JSX is not the node
116+
that actually renders or carries the interaction handlers. To reach the real node, use the
117+
component's DOM utility methods:
118+
119+
- **`getDomRef()`** — returns the DOM element that represents the component. For an abstract
120+
component this is the **parent-owned** node that renders it, not a placeholder.
121+
- **`getFocusDomRef()`** — returns the element marked `data-sap-focus-ref` (the node that
122+
receives focus by default).
123+
124+
```js
125+
// For an abstract component, reach the rendered node via its instance handle,
126+
// by evaluating in the browser: element.getDomRef()
127+
rendered = get('[ui5-tab]').evaluate((el) => el.getDomRef())
128+
```
129+
130+
For non-abstract components this is rarely necessary — the host element renders its own shadow
131+
DOM, and interacting with the host works directly (see the next section).
132+
133+
## Popovers, menus, and dialogs: wait until open
134+
135+
Overlay content — popovers, menus, dialogs, dropdowns — is only **visible and interactable while
136+
it is open**, and when open it is displayed in a top-layer/overlay container rather than inline in
137+
the page flow.
138+
139+
Note that "in the DOM" and "interactable" are not the same thing here. Slotted content (for
140+
example a `Menu`'s `MenuItem`s) is authored by you as light-DOM children, so unless you render it
141+
conditionally it is **always** in the DOM — even while the overlay is closed. Querying for its
142+
existence therefore succeeds regardless of open state, yet the content is not visible or clickable
143+
until the overlay opens.
144+
145+
Two consequences for tests:
146+
147+
1. **Wait for the open state (or visibility), not just existence, before interacting.** Asserting
148+
the overlay's `open` attribute — or that the target is _visible_ — waits for the state that
149+
actually makes it interactable, whereas an existence check can pass while the item is still
150+
hidden.
151+
152+
```js
153+
// open the menu, then wait for it to be open before interacting
154+
click(opener)
155+
expect('[ui5-menu]').toHaveAttribute('open')
156+
click('[ui5-menu-item][text="New File"]')
157+
```
158+
159+
2. **Prefer a real, coordinate-based click over a synthetic one.** A real click (what
160+
browser-driving runners perform) hit-tests through the Shadow DOM and lands on the node that
161+
actually carries the handler. A **synthetic** `element.click()` dispatched on the host element
162+
fires on an ancestor of the handler node and may never reach it — so it can silently do
163+
nothing. Let your runner perform its normal click on the element rather than calling
164+
`element.click()` yourself in page-evaluated code.
165+
166+
Because overlays open and animate asynchronously, always gate interactions on the open state (as
167+
above) rather than on fixed delays.
168+
169+
<Footer />

packages/mcp-server/scripts/utils/cem.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ export function loadCemData(monorepoRoot: string): {
9999
* - cssParts on component root
100100
*/
101101
export function enrichWithCem(apiData: ComponentApiData, cemDecl: CemDeclaration): void {
102+
if (cemDecl._ui5abstract) {
103+
apiData.isAbstract = true;
104+
}
105+
102106
if (cemDecl.events) {
103107
for (const event of cemDecl.events) {
104108
if (event._ui5privacy && event._ui5privacy !== 'public') continue;

packages/mcp-server/src/tools/get_component_api/get_component_api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ EXAMPLE INPUT: { "componentName": "Dialog" }
112112
name: string, // Use as: componentSelector::part(name) { ... }
113113
description: string
114114
}>,
115+
isAbstract?: boolean, // true = abstract component: renders into its PARENT's DOM, not its own shadow root. For testing, target the node from getDomRef() (parent-owned), not the host placeholder. See the "Testing" knowledge-base section (get_documentation).
115116
subTypeDocs?: string // Markdown docs for complex prop types (e.g. column definition properties)
116117
docUrl?: string // Upstream docs link for complex behavioral concepts
117118
}
@@ -137,6 +138,12 @@ EXAMPLE INPUT: { "componentName": "Dialog" }
137138
.array(z.object({ name: z.string(), description: z.string() }))
138139
.optional()
139140
.describe('CSS ::part() selectors for shadow DOM styling'),
141+
isAbstract: z
142+
.boolean()
143+
.optional()
144+
.describe(
145+
'True for abstract components (marked @abstract): they render into a parent’s DOM, not their own shadow root. When interacting in a real browser (tests), target the node returned by getDomRef() rather than the host placeholder. See the "Testing" knowledge-base section.',
146+
),
140147
subTypeDocs: z
141148
.string()
142149
.optional()

packages/mcp-server/src/tools/get_documentation/documentation_sections.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,42 @@
203203
],
204204
"localPath": "docs--knowledge-base--Styling.mdx"
205205
},
206+
{
207+
"name": "Testing",
208+
"description": "Particularities of testing UI5 Web Components for React in a real browser (Cypress, Playwright, etc. — not JSDOM): setup, selectors, async readiness, abstract components, and overlays.",
209+
"link": "https://ui5.github.io/webcomponents-react/v2/?path=/docs/knowledge-base-testing--docs",
210+
"sourcePath": "docs/knowledge-base/Testing.mdx",
211+
"content": [
212+
"Real browser required (not JSDOM/virtual DOM)",
213+
"Component-test setup: wrap in ThemeProvider (outermost), import Assets for i18n/theming",
214+
"Setup applies to component testing, not E2E (app already provides it)",
215+
"Attribute selectors [ui5-*] over tag names (scoping-safe)",
216+
"Shadow DOM is pierceable (Cypress includeShadowDom, Playwright by default); prefer public surface",
217+
"Web components are async: custom element definition + internal rendering",
218+
"Wait via retrying assertions on the element/state, not fixed delays",
219+
"Abstract components render into their parent (isAbstract flag)",
220+
"getDomRef() / getFocusDomRef() for reaching rendered nodes",
221+
"Popovers/menus/dialogs: wait for open/visible state (slotted items may exist in DOM while hidden)",
222+
"Real coordinate-based click vs synthetic element.click()"
223+
],
224+
"tags": [
225+
"testing",
226+
"cypress",
227+
"playwright",
228+
"e2e",
229+
"component-testing",
230+
"themeprovider",
231+
"assets",
232+
"i18n",
233+
"async",
234+
"flaky-tests",
235+
"shadow-dom",
236+
"abstract-components",
237+
"getdomref",
238+
"popovers"
239+
],
240+
"localPath": "docs--knowledge-base--Testing.mdx"
241+
},
206242
{
207243
"name": "Accessibility",
208244
"description": "How to make UI5 Web Components applications accessible. Covers accessibility APIs (accessibleName, accessibleNameRef, accessibleDescription, accessibleRole, accessibilityAttributes), label-input relationships, invisible messaging, keyboard handling, and high contrast themes.",

packages/mcp-server/src/types/cem.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export interface CemDeclaration {
4141
members?: unknown[];
4242
slots?: unknown[];
4343
superclass?: { name: string; package: string; module: string } | null;
44+
/** True when the component is marked `@abstract` (renders into a parent's DOM, not its own shadow root). */
45+
_ui5abstract?: boolean;
4446
}
4547

4648
export interface CemModule {

packages/mcp-server/src/types/component-api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export interface ComponentApiData {
5757
props: Record<string, CleanedProp>;
5858
methods: MethodInfo[];
5959
cssParts?: CssPart[];
60+
/**
61+
* True when the component is abstract (marked `@abstract`): it renders into a parent's DOM
62+
* rather than its own shadow root. Present (and `true`) only for abstract components.
63+
* See the "Testing" knowledge-base section for how this affects interacting with it.
64+
*/
65+
isAbstract?: boolean;
6066
/** Additional documentation for complex prop types (e.g. column definition shape). */
6167
subTypeDocs?: string;
6268
/** Link to upstream UI5 Web Components documentation for complex behavioral concepts. */

0 commit comments

Comments
 (0)