Skip to content

Commit b2fbbfe

Browse files
feat(evo-testing): create skill for test creation (#635)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a3670e4 commit b2fbbfe

1 file changed

Lines changed: 270 additions & 0 deletions

File tree

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
---
2+
name: evo-testing
3+
description: Write, extend, or review tests for ebayui-core (Marko) and ebayui-core-react in the evo-web monorepo using Testing Library and Storybook interactions, aligned with WCAG 2.2 A/AA. Use this skill whenever the user asks for component tests, accessibility tests, Storybook play functions, keyboard or focus coverage, ARIA assertions, a test plan for an ebay-* component, or help avoiding duplicate or undocumented a11y tests. Also use for flaky test diagnosis, given/when/then structure, or splitting simple vs complex interaction coverage.
4+
---
5+
6+
# Evo-Web component testing (Marko + React)
7+
8+
You are helping write thorough unit and end-to-end (Storybook interaction) tests for **@ebay/ebayui-core** and **@ebay/ebayui-core-react**. Other packages (@evo-web/react, @evo-web/marko) may mirror these patterns where present; default to the paths below for the two main libraries.
9+
10+
**Standard:** WCAG 2.2 levels A and AA. Prefer what the component’s accessibility doc promises over inventing new ARIA contracts.
11+
12+
**Naming:** Components use the `ebay-` prefix on disk. Documentation URLs omit it (e.g. `ebay-button``button`).
13+
14+
## Documentation (read before testing)
15+
16+
For component `{component}` (URL segment without `ebay-`):
17+
18+
- Overview: `https://opensource.ebay.com/evo-web/components/{component}`
19+
- Accessibility: `https://opensource.ebay.com/evo-web/components/{component}/accessibility`
20+
- CSS: `https://opensource.ebay.com/evo-web/components/{component}/css`
21+
22+
## Where tests live
23+
24+
**Marko (ebayui-core)**
25+
26+
- Browser / interaction: `packages/ebayui-core/src/components/ebay-{component}/test/test.browser.js`
27+
- SSR: `.../test/test.server.js`
28+
- Stories (for `play` interaction tests): `.../{component}.stories.ts`
29+
30+
**React (ebayui-core-react)**
31+
32+
- Unit / integration: `packages/ebayui-core-react/src/ebay-{component}/__tests__/*.spec.tsx`
33+
- Stories (for `play` interaction tests): `.../__tests__/index.stories.tsx` (and other `*.stories.tsx` in that folder)
34+
35+
**Commands:** See the `evo-commands` skill for `npx vitest run ...` and workspace-scoped npm scripts.
36+
37+
## Test categories (group and label tests this way)
38+
39+
1. **Click interactions**
40+
2. **Keyboard interactions**
41+
3. **Focus management**
42+
4. **ARIA attributes** (only what docs / implemented behavior specify)
43+
44+
Include **disabled** (or non-interactive) states when the component supports them.
45+
46+
## Simple vs complex interactions
47+
48+
- **Unit / simple:** One action → one outcome (e.g. key press → state or event). Keep in `test.browser.js` or `*.spec.tsx`.
49+
- **E2E / complex:** Multiple steps (e.g. open dialog → assert focus target). Prefer Storybook **`play`** functions: [Storybook interaction testing](https://storybook.js.org/docs/writing-tests/interaction-testing).
50+
51+
### Marko simple example (Vitest browser + `@marko/testing-library`)
52+
53+
```javascript
54+
describe("when Space key is pressed", () => {
55+
beforeEach(async () => {
56+
const checkbox = component.getByRole("checkbox");
57+
checkbox.focus();
58+
await userEvent.keyboard(" ");
59+
});
60+
61+
it("then it toggles to checked state", () => {
62+
expect(component.getByRole("checkbox")).toBeChecked();
63+
});
64+
65+
it("then it emits change event", () => {
66+
const changeEvents = component.emitted("change");
67+
expect(changeEvents).has.length(1);
68+
69+
const [[changeEvent]] = changeEvents;
70+
expect(changeEvent).has.property("checked", true);
71+
});
72+
});
73+
```
74+
75+
### Marko complex example (nested given/when/then)
76+
77+
```javascript
78+
describe("given disabled checkbox is initially checked", () => {
79+
beforeEach(async () => {
80+
component = await render(Disabled, {
81+
checked: true,
82+
});
83+
});
84+
85+
it("then it renders in checked state", () => {
86+
expect(component.getByRole("checkbox")).has.property("checked", true);
87+
});
88+
89+
describe("when checkbox is clicked", () => {
90+
beforeEach(async () => {
91+
await fireEvent.click(component.getByRole("checkbox"));
92+
});
93+
94+
it("then it remains checked", () => {
95+
expect(component.getByRole("checkbox")).has.property("checked", true);
96+
});
97+
98+
it("then it does not emit change event", () => {
99+
expect(component.emitted("change")).has.length(0);
100+
});
101+
});
102+
});
103+
```
104+
105+
## Workflow
106+
107+
### Analysis
108+
109+
1. Read overview, accessibility, and CSS docs for the component.
110+
2. Infer **Marko vs React** from path (`packages/ebayui-core/...` vs `packages/ebayui-core-react/...`).
111+
3. Read existing tests in that component’s `test/` or `__tests__/` to **avoid duplication**.
112+
4. Produce a short **test plan** split into unit vs Storybook interaction tests. If repeated scenarios apply across many variants, plan a **shared helper** (colocated module or existing `common/test-utils` patterns)—do not assume a `storybook-tests/` root unless you confirm it exists in the repo.
113+
114+
#### Test Plan Format
115+
116+
Structure the **test plan** as follows:
117+
118+
```markdown
119+
# Test Plan for {component}
120+
121+
## Existing Tests
122+
123+
Overview of the existing tests and their purpose.
124+
125+
## Marko Browser Tests
126+
127+
List the test cases to be created, use given/when/then language, group by test focus.
128+
129+
## React Browser Tests
130+
131+
List the test cases to be created, use given/when/then language, group by test focus.
132+
133+
## Interaction Tests
134+
135+
List the test cases to be created, use given/when/then language, group by test focus and steps.
136+
137+
## Clarifying Questions
138+
139+
Ask questions about any tests or requirements that there is low confidence about.
140+
```
141+
142+
### Generation
143+
144+
1. **Marko a11y-focused browser tests:** add accessibility tests to `test.browser.js` and group according to test focus.
145+
2. **React a11y-focused tests:** add accessibility tests to `*.spec.tsx` and group according to test focus.
146+
3. **Storybook plays:** Marko → `src/components/ebay-{component}/{component}.stories.ts` (for example, `src/components/ebay-button/button.stories.ts`). React → `__tests__/index.stories.tsx` (or colocated stories).
147+
4. Organize by Click / Keyboard / Focus / ARIA.
148+
5. Use **`beforeEach`** for nested setup (given/when/then).
149+
6. Prefer **`userEvent`** from `@testing-library/user-event` (or `vitest/browser` where the existing file already does) over ad-hoc `pressKey` helpers.
150+
151+
## Constraints
152+
153+
- Do not duplicate tests already covered in the same framework for that component.
154+
- Do not assert ARIA or keyboard behavior that is **not** documented or clearly implemented—tie tests to the accessibility page and real markup.
155+
156+
## Examples by type
157+
158+
### Keyboard — React unit
159+
160+
```javascript
161+
describe("when Enter key is pressed", () => {
162+
beforeEach(async () => {
163+
const button = screen.getByRole("button");
164+
button.focus();
165+
await user.keyboard("{Enter}");
166+
});
167+
168+
it("then it emits click event", () => {
169+
expect(clickHandler).toHaveBeenCalledTimes(1);
170+
});
171+
});
172+
```
173+
174+
### Keyboard — Storybook `play`
175+
176+
```typescript
177+
Default.play = async ({ canvasElement, step }) => {
178+
const canvas = within(canvasElement);
179+
const button = canvas.getByRole("button");
180+
181+
await step("Test keyboard interaction - Enter key", async () => {
182+
button.focus();
183+
await userEvent.keyboard("{Enter}");
184+
await expect(button).toHaveFocus();
185+
});
186+
};
187+
```
188+
189+
### Focus — Marko unit
190+
191+
```javascript
192+
describe("when button receives focus", () => {
193+
beforeEach(async () => {
194+
const button = component.getByRole("button");
195+
button.focus();
196+
});
197+
198+
it("then button has focus", () => {
199+
const button = component.getByRole("button");
200+
expect(document.activeElement).toBe(button);
201+
});
202+
203+
it("then it emits focus event", () => {
204+
const focusEvents = component.emitted("focus");
205+
expect(focusEvents).has.length(1);
206+
207+
const [[focusEvent]] = focusEvents;
208+
expect(focusEvent).has.property("originalEvent").is.an.instanceOf(Event);
209+
});
210+
});
211+
```
212+
213+
### Focus — Storybook `play`
214+
215+
```typescript
216+
Default.play = async ({ canvasElement, step }) => {
217+
const canvas = within(canvasElement);
218+
const button = canvas.getByRole("button");
219+
220+
await step("Test focus management", async () => {
221+
await userEvent.click(button);
222+
await expect(button).toHaveFocus();
223+
224+
await userEvent.keyboard("{Tab}");
225+
await expect(button).not.toHaveFocus();
226+
});
227+
};
228+
```
229+
230+
### ARIA — Marko unit
231+
232+
```javascript
233+
describe("given a standard button", () => {
234+
beforeEach(async () => {
235+
component = await render(template, {
236+
renderBody: "Standard button",
237+
});
238+
});
239+
240+
it("then it has correct role", () => {
241+
const button = component.getByRole("button");
242+
expect(button).toBeTruthy();
243+
});
244+
});
245+
```
246+
247+
### ARIA — Storybook `play`
248+
249+
```typescript
250+
LoadingState.play = async ({ canvasElement, step }) => {
251+
const canvas = within(canvasElement);
252+
const button = canvas.getByRole("button");
253+
254+
await step("Verify loading state and aria-label", async () => {
255+
await expect(button).toHaveAttribute("aria-label", "Loading, please wait");
256+
257+
const spinner = button.querySelector(".progress-spinner");
258+
await expect(spinner).toBeInTheDocument();
259+
});
260+
261+
await step("Test button is still interactive in loading state", async () => {
262+
button.focus();
263+
await expect(button).toHaveFocus();
264+
});
265+
};
266+
```
267+
268+
## Story / locator naming
269+
270+
Story exports may not be `Default`. Open the component’s `*.stories.ts` and use the **actual story names** when attaching `play` or selecting canvases.

0 commit comments

Comments
 (0)