Automattic / Automattic/wordpress-skill-experiments
Proposal: Complete set of scenarios to eval the iAPI reference
- Dominant language
- JavaScript
- Stars
- 1
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
# Proposal: Interactivity API eval scenario set + folder organization
We want the skillsmith eval suite to drive how we evolve the **Interactivity API reference** in the `wordpress-development` skill. The current handful of scenarios under `eval/scenarios/` was an initial set for exercising the workflow, not real coverage.
This issue proposes:
1. A curated set of **Interactivity-API scenarios** aiming to cover the full API surface with no overlap, and
2. A **folder organization** for them.
Nothing here is built yet — it's a proposal to discuss the scenarios and the organization before we implement them.
## Principles behind the set
- **Interactivity-API-specific only** — every scenario exercises iAPI: `data-wp-*` directives, the `@wordpress/interactivity` store, the `wp_interactivity_*` server helpers, or the client-side router.
- **Real, outcome-focused prompts** — each scenario is phrased as a feature request a user would actually make, describing the behavior they want, not the directives to use.
- **One concept per scenario, no overlap** — the set is heavily de-duplicated.
- **Full coverage** — every concept in the API (directives, store/client API, server-side rendering, client navigation, TypeScript typing, and the common real-world UX patterns) maps to at least one scenario.
Each scenario below lists the user-facing behavior and, in *Tests:*, the iAPI concept it exists to exercise.
**`data-wp-on-async` is intentionally excluded** — it's deprecated (`data-wp-on` is async by default now). We should drop it from the best-practices rubric so an `on-async` answer isn't graded as correct.
## Proposed scenarios & organization
Organize by **iAPI capability / primary concept** (not by UX pattern or difficulty). I guess the list will change/evolve once we start working on the scenarios, but I wanted to start from a solid foundation that we agree on.
| Folder | Count |
|---|---:|
| `foundations` | 3 |
| `reactive-bindings` | 5 |
| `state-and-context` | 9 |
| `derived-state` | 5 |
| `server-rendering` | 3 |
| `events` | 6 |
| `async-actions` | 5 |
| `lifecycle` | 5 |
| `lists` | 4 |
| `client-navigation` | 12 |
| `typescript` | 4 |
| `ux-patterns` | 6 |
### `foundations` — getting iAPI wired up
- **Minimal scaffold** — the smallest correctly-wired interactive block that proves the runtime is alive. *Tests:* the minimal wiring — `supports.interactivity` + `viewScriptModule`, `data-wp-interactive`, and an init callback that runs once per instance.
- **Classic theme dismissible banner** — a dismissible announcement bar rendered from a classic (non-block) theme template. *Tests:* `wp_interactivity_process_directives()` and running iAPI on server HTML outside the block system.
- **Multiple directives on one element** — a block that runs two independent setup steps when it first appears, each wired as its own callback on the same element. *Tests:* the unique-ID directive suffix (`data-wp-init---`, triple-hyphen) for attaching the same directive more than once to one element — the mechanism generalizes to `data-wp-watch` / `data-wp-run`.
### `reactive-bindings` — binding reactive values to the DOM (text / class / style / attributes)
- **Greeting rotator** — a button cycles a displayed greeting through several strings. *Tests:* `data-wp-text` reactive text content driven from state (no manual `textContent` writes).
- **Selectable tile class toggle** — a card whose selected state toggles a highlight, per instance. *Tests:* `data-wp-class--` toggling a CSS class from a boolean.
- **CSS variable progress meter** — a bar whose fill grows/shrinks via buttons. *Tests:* `data-wp-style--` binding an inline CSS custom property (value from a derived getter).
- **On sale link attribute removal** — a badge whose link/tooltip are present when on sale and fully absent otherwise. *Tests:* `data-wp-bind--` where a null/false getter removes the attribute entirely (not an empty value).
- **Accessible disclosure toggle** — a show/hide button whose announced state and label stay in sync. *Tests:* boolean-attribute binding (`hidden` / `aria-expanded`) plus a derived label from a single boolean.
### `state-and-context` — where data lives and who can touch it
- **Independent counters** — each instance keeps its own count. *Tests:* per-instance local context (`data-wp-context` + `getContext()`).
- **Nested theme card** — an inner panel inherits some parent values and overrides others. *Tests:* nested context inheritance and selective override.
- **PHP seeded context list** — the initial list is built server-side per instance. *Tests:* server-seeded local context via `wp_interactivity_data_wp_context()`.
- **Product card quantity stepper** — each card tracks its own quantity. *Tests:* choosing local context over global state (the two-instances-share-state footgun); a global-state answer fails.
- **Shared state tally** — every instance shows the same page-wide count. *Tests:* global state shared across instances of one block.
- **Cart count across two blocks** — an "Add to cart" button updates a separate header badge. *Tests:* cross-block communication through one shared global-state namespace.
- **Locked private store** — a widget other plugins can't tamper with. *Tests:* the store `lock` option preventing re-opening/overriding the namespace.
- **Pageview analytics bridge** — a UI-less block forwarding a shared value's changes to an analytics stub. *Tests:* the programmatic `watch()` function subscribing at module load (with unsubscribe).
- **Now playing cross namespace** — an indicator that lights up from another plugin's state. *Tests:* declarative cross-namespace reads (`namespace::state.x`) in a directive, no JS import.
### `derived-state` — computed values from a single source of truth
- **Derived double** — shows a counter and its doubled value. *Tests:* a derived getter computed from one source of truth (global).
- **Price with VAT per instance** — each card shows its price with tax, computed once. *Tests:* a derived getter reading `getContext()` so one getter yields per-instance results (server-seeded via a closure).
- **Tax calculator mixed scope** — one global tax rate applied to each card's own price. *Tests:* a derived getter combining global state with per-instance context.
- **Results panel no flash** — the "No results" message never flashes when there are results. *Tests:* server-computed derived state so the initial HTML is correct before hydration.
- **Shopping list server derived icon** — in-cart items show an icon, correct on first paint. *Tests:* per-row server-computed derived state via a `wp_interactivity_state` closure using `wp_interactivity_get_context()`.
### `server-rendering` — server-side seeding & PHP helpers
- **Config REST fetch with nonce** — a button loads data over REST. *Tests:* `wp_interactivity_config()` / `getConfig()` for immutable server→client values (REST URL + nonce), kept out of state.
- **Welcome banner i18n state** — a localized greeting that's correct on load. *Tests:* translating seeded state on the server (`__()` inside `wp_interactivity_state`), with no client-side translation.
- **Stable tab IDs across navigation** — ARIA-linked IDs that don't change between renders. *Tests:* `wp_unique_id_from_values()` producing deterministic IDs stable across renders and soft navigations.
### `events` — the `data-wp-on` family
- **Like button** — a heart button that toggles liked/unliked and updates a like count when clicked. *Tests:* the basic `data-wp-on--click` element handler — wiring a click to a store action (the simplest event).
- **Live character counter** — a message field showing a live "x / 280" character count that flags going over the limit as the user types. *Tests:* `data-wp-on--input` reacting to each keystroke (basic input-event handling).
- **Viewport width display** — shows the live window width. *Tests:* `data-wp-on-window--resize` (window-level listener lifecycle).
- **Command palette document keydown** — a "/" shortcut opens an overlay from anywhere. *Tests:* `data-wp-on-document--keydown` (page-wide key handling) + `withSyncEvent` `preventDefault`.
- **Menu touch vs hover** — submenu opens on hover with a mouse, on tap with touch. *Tests:* branching on `event.pointerType` inside pointer-event handlers.
- **Newsletter submit guard** — submit shows an inline confirmation without reloading. *Tests:* `withSyncEvent()` to call `event.preventDefault()` synchronously; judgment that the handler must be sync, not async.
### `async-actions` — user-triggered work that takes time
- **Joke of the day fetch** — a button fetches and displays a joke. *Tests:* a generator action (`function*` + `yield`) for async fetch, mutating state after the yield.
- **Load more posts** — a button appends the next batch of posts. *Tests:* fetching and appending results into a growing list (append, not replace).
- **Validated async form** — inline validation + async submit with status states. *Tests:* form handling — validation getters + generator POST + `disabled`/loading/success/error states.
- **Batch processor progress** — processes a large list while staying responsive. *Tests:* `splitTask()` yielding to the main thread inside a long loop.
- **Live scoreboard polling** — a value that refreshes from the server every few seconds. *Tests:* an interval fetch loop (`setInterval` + generator `fetch`) with cleanup on teardown.
### `lifecycle` — behavior tied to an element being on the page
- **Autofocus revealed form** — a revealed form focuses its first field. *Tests:* `data-wp-init` running once on mount with `getElement().ref` focus.
- **Instrumented mount (multi-init)** — two independent setup steps on first render: an analytics "viewed" ping and a separate debug init marker. *Tests:* multiple `data-wp-init---` callbacks on one element (the triple-hyphen form).
- **Counter change watch** — a readout reacts to the count changing, including on mount. *Tests:* `data-wp-watch` re-running on dependency change (and on mount).
- **Interval with cleanup toggle** — an on/off switch starts/stops a ticking timer with no leaks. *Tests:* a `data-wp-watch` cleanup function (teardown on re-run / unmount).
- **Countdown to event** — a live countdown that ends with a message. *Tests:* `withScope()` wrapping a `setInterval` callback so store helpers resolve, with cleanup.
- **In view reveal** — a banner animates in the first time it scrolls into view. *Tests:* `data-wp-run` with `useState`/`useEffect` hooks (+ IntersectionObserver, effect cleanup).
### `lists` — iteration
- **Fruit list with add** — a seeded list with an Add button. *Tests:* `data-wp-each` over a seeded array, `data-wp-each-child` SSR hydration, and in-place `.push()` mutation.
- **Reorderable keyed todo list** — todos that reorder without rebuilding rows. *Tests:* `data-wp-each--` + `data-wp-each-key` (keyed iteration of objects; DOM reuse on reorder).
- **Image carousel with arrow keys** — a gallery navigated with Left/Right keys. *Tests:* arrow-key navigation over an indexed collection with wrap-around (derived current / has-next).
- **Live search filter** — a list that narrows as you type. *Tests:* a derived filtered list rendered via `data-wp-each` (client-side reactive filtering).
### `client-navigation` — the router
- **Footer client navigation opt in** — a static footer that survives soft transitions. *Tests:* the `supports.interactivity.clientNavigation` opt-in for a no-JS block.
- **Paginated posts router region** — Next/Prev swap only the list, no reload. *Tests:* `data-wp-router-region` + `actions.navigate()` with `data-wp-key` and a no-JS anchor fallback.
- **Prefetch on hover pagination** — hovering a page link preloads it. *Tests:* `actions.prefetch()` on hover + `actions.navigate()` on click.
- **Section jump scroll to top** — soft navigation scrolls back to the top. *Tests:* a post-navigation side effect (`window.scrollTo` after `yield actions.navigate`).
- **Filter bar replace history** — filtering updates the URL without cluttering history. *Tests:* `actions.navigate(url, { replace: true })` (replaceState semantics).
- **Comment form force refresh** — after posting, the list refreshes without a stale cache. *Tests:* `actions.navigate(href, { force: true })` to bypass the cache after a mutation.
- **Resilient link HTML fallback** — a link that shows a fallback panel if the fetch fails. *Tests:* `actions.navigate(url, { html })` fallback after a manual fetch try/catch.
- **External link respects browser** — modifier-clicks and cross-origin links use the browser. *Tests:* guarding on modifier keys / same-origin before `preventDefault` so the router is skipped.
- **Pageview tracker on router state** — fires a pageview on load and on every soft navigation. *Tests:* reacting to the router's `state.url` via `data-wp-watch`.
- **Global toast attached to body** — a toast region present on every page. *Tests:* the `data-wp-router-region` `attachTo` option (dynamic region injection on navigation).
- **Page header server state sync** — a site-wide header shows a server-provided section label that refreshes on each soft navigation, while a "compact view" toggle the reader set persists. *Tests:* `getServerState()` selectively syncing global state across navigations — refresh the server field, leave the client-only field untouched.
- **Recipe card server context sync** — the title updates per page while a client adjustment persists. *Tests:* `getServerContext()` re-syncing local context across navigations.
### `typescript` — typing constraints
- **TypeScript counter inference** — a counter built in TS with no explicit types. *Tests:* typed store via inference (no annotations, generics, or casts).
- **TypeScript server state merge** — a TS counter whose count comes from PHP. *Tests:* typing server-seeded state via `ServerState & typeof storeDef`.
- **TypeScript async and derived types** — a TS counter with an async action and a derived value. *Tests:* `AsyncAction` for generators + explicit derived-getter return types (breaking TS reference cycles).
- **TypeScript typed context import** — a second plugin adds to an existing plugin's store. *Tests:* importing another module's typed `{ state, actions }` (+ typed `getContext()`) with a script-module dependency.
### `ux-patterns` — composed accessible widgets
- **Accessible tabs** — a tab strip with one visible panel. *Tests:* single-active-selection tabs with roving arrow-key navigation and correct ARIA.
- **Accessible accordion** — an FAQ where multiple sections can be open. *Tests:* an independent multi-toggle disclosure group with ARIA, correct before JS runs.
- **Accessible modal dialog** — a modal with focus trap and Escape. *Tests:* dialog semantics present only while open (null-removal), focus trap, Escape, and focus restoration.
- **Image lightbox** — click to zoom an image full-screen. *Tests:* an overlay driven by CSS custom properties from `getBoundingClientRect` + dialog semantics and focus restoration.
- **Side drawer with focus trap** — a hamburger drawer of nav links. *Tests:* off-canvas nav with Tab focus trap, Escape, and focus return to the trigger.
- **Expandable search** — a magnifier button that expands a search field. *Tests:* focus management + outside-click / `focusout` close (`ref.contains(relatedTarget)`).
## Tooling / migration notes
- **Nested subfolders** (`eval/scenarios///`) is the recommendation. Playwright already discovers specs recursively (`testMatch: **/e2e.spec.mjs`), and the plugin scaffolding is unaffected.
- It needs one small change to `eval/utils/verify-e2e.ts` (so a nested path is used for the spec lookup while failure attribution stays keyed on the scenario dir), and the skillsmith scenario discovery must look one level deeper (`eval/scenarios/*/*/scenario.yaml`).
## Open questions
- Anything you're missing? Any iAPI concept not covered?
- Anything you'd remove?
- Does the organization make sense?
## Next steps
- Settle the above here.
- Implement the agreed scenarios in the skillsmith format (one `scenario.yaml` + `e2e.spec.mjs` each).
- Improve the reference until all scenarios pass.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reviewing the existing scenarios under eval/scenarios/ and the proposed capability folders and scenario list in this issue. Compare the coverage against the Interactivity API concepts and the best-practices rubric, including the removal of data-wp-on-async. Done means the scenario set and folder organization have been agreed before implementation begins.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, php, typescript
- Domain
- documentation, frontend, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100