|
| 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