|
| 1 | +--- |
| 2 | +name: code-review |
| 3 | +description: Review a pull request in the mendix/web-widgets monorepo. Checks Mendix widget conventions, React/MobX patterns, versioning, test coverage, Atlas UI styling, security, and accessibility. |
| 4 | +--- |
| 5 | + |
| 6 | +# Code Review |
| 7 | + |
| 8 | +Review the PR diff against the standards in this repository. Read `AGENTS.md` for full repo context. |
| 9 | + |
| 10 | +## What to check on every PR |
| 11 | + |
| 12 | +### PR metadata |
| 13 | + |
| 14 | +- **Title**: JIRA format `[XX-000]: description` or conventional commits (`feat:`, `fix:`, etc.) |
| 15 | +- **Template adherence**: lint/test run locally, new tests added, related PRs linked |
| 16 | +- **Multi-package PRs**: validate each changed package separately |
| 17 | + |
| 18 | +### Changelog (per changed package) |
| 19 | + |
| 20 | +Version bumps happen in a separate dedicated PR — do not require or flag missing semver bumps. |
| 21 | + |
| 22 | +If runtime code, public API, XML schema, or behavior changed: |
| 23 | + |
| 24 | +- Require `CHANGELOG.md` entry (Keep a Changelog format) |
| 25 | +- Suggest: `pnpm -w changelog` |
| 26 | + |
| 27 | +If refactor/docs/tests-only: changelog entry not required — confirm with author. |
| 28 | + |
| 29 | +### Mendix-specific |
| 30 | + |
| 31 | +`AGENTS.md` covers the core rules (canExecute, loading states, lowerCamelCase XML keys). Flag these additional review issues: |
| 32 | + |
| 33 | +- XML changed but TS props not updated, or widget ID is not unique |
| 34 | +- `EditableValue` read without checking `.status` — can render stale/undefined data |
| 35 | +- `ActionValue.execute()` called without checking `.canExecute` first |
| 36 | + |
| 37 | +### React |
| 38 | + |
| 39 | +Flag these patterns — general React conventions are assumed known: |
| 40 | + |
| 41 | +- Missing or wrong `useEffect`/`useMemo`/`useCallback` deps; stale closures |
| 42 | +- Async effect sets state without a cleanup guard: |
| 43 | + ```ts |
| 44 | + useEffect(() => { |
| 45 | + let active = true; |
| 46 | + fetchData().then(data => { |
| 47 | + if (active) setState(data); |
| 48 | + }); |
| 49 | + return () => { |
| 50 | + active = false; |
| 51 | + }; |
| 52 | + }, [fetchData]); |
| 53 | + ``` |
| 54 | +- Array index used as list `key` — requires a stable unique key |
| 55 | +- Props spread onto DOM nodes (`<div {...props}>`) — strips unknown HTML attributes |
| 56 | + |
| 57 | +### MobX |
| 58 | + |
| 59 | +- `makeAutoObservable` or `makeObservable` missing from store constructor |
| 60 | +- State mutation outside `action` — suggest `runInAction` or `action` wrapper |
| 61 | +- `computed` with side effects — must be pure |
| 62 | +- Missing `observer` HOC or `useSubscribe()` from `@mendix/widget-plugin-mobx-kit` on React components that read MobX state |
| 63 | + |
| 64 | +### Styling |
| 65 | + |
| 66 | +Conventions are in `docs/requirements/frontend-guidelines.md`. Flag only deviations: |
| 67 | + |
| 68 | +- Inline styles used for static design |
| 69 | +- Core Atlas UI classes overridden |
| 70 | +- `!important` used |
| 71 | +- CSS class not prefixed with widget name |
| 72 | + |
| 73 | +### Unit tests |
| 74 | + |
| 75 | +Files live in `src/**/__tests__/*.spec.ts(x)` and run with Jest + RTL (enzyme-free). |
| 76 | +
|
| 77 | +**Structure** |
| 78 | +
|
| 79 | +- Use `describe`/`it` blocks; group related cases under a nested `describe` |
| 80 | +- Define a `defaultProps` constant and a factory render helper to avoid repetition: |
| 81 | + ```ts |
| 82 | + const defaultProps: MyWidgetProps = { ... }; |
| 83 | + const renderWidget = (props = defaultProps) => render(<MyWidget {...props} />); |
| 84 | + ``` |
| 85 | +
|
| 86 | +**Mendix data mocking — always use builders, never manual objects** |
| 87 | +
|
| 88 | +```ts |
| 89 | +import { EditableValueBuilder, ListValueBuilder, actionValue, obj } from "@mendix/widget-plugin-test-utils"; |
| 90 | +
|
| 91 | +const value = new EditableValueBuilder<string>().withValue("hello").build(); |
| 92 | +const readOnly = new EditableValueBuilder<string>().withValue("x").isReadOnly().build(); |
| 93 | +const loading = new EditableValueBuilder<string>().isLoading().build(); |
| 94 | +const list = new ListValueBuilder().withItems([obj("A"), obj("B")]).build(); |
| 95 | +const action = actionValue(); // jest.fn() — assert with .execute toHaveBeenCalled() |
| 96 | +``` |
| 97 | +
|
| 98 | +**What to cover** |
| 99 | +
|
| 100 | +- All Mendix data states: `Available`, `Loading`, `Unavailable`, `ReadOnly` |
| 101 | +- All prop/behavior branches (null checks, conditional renders, edge cases) |
| 102 | +- User interactions via `fireEvent` or `userEvent` |
| 103 | +- Verify `setValue` / `execute` calls: `expect(action.execute).toHaveBeenCalled()` |
| 104 | +- Accessibility assertions: `getByRole`, `getByLabelText`, ARIA attributes |
| 105 | +
|
| 106 | +**What to flag** |
| 107 | +
|
| 108 | +- Manual mock objects instead of builders — brittle and miss status edge cases |
| 109 | +- Enzyme patterns (`shallow`, `mount`, `instance()`) — must use RTL |
| 110 | +- Snapshot tests on dynamic content (dates, IDs, async state) — use specific assertions instead |
| 111 | +- Missing `afterEach` mock cleanup — causes test pollution |
| 112 | +- Missing `ResizeObserver` / `window.mx` global setup when widget requires it (add to `jest.setup.ts`) |
| 113 | +- Jest config not extending `@mendix/pluggable-widgets-tools/test-config/jest.config` |
| 114 | +
|
| 115 | +### E2E tests |
| 116 | +
|
| 117 | +Files live in `e2e/*.spec.js` and run with Playwright (Chromium). Config inherits from `@mendix/run-e2e/playwright.config.cjs`. |
| 118 | +
|
| 119 | +**Mandatory structure — every file must have this** |
| 120 | +
|
| 121 | +```js |
| 122 | +import { test, expect } from "@playwright/test"; |
| 123 | +
|
| 124 | +test.afterEach("Cleanup session", async ({ page }) => { |
| 125 | + await page.evaluate(() => window.mx.session.logout()); |
| 126 | +}); |
| 127 | +
|
| 128 | +test.describe("WidgetName", () => { |
| 129 | + test.beforeEach(async ({ page }) => { |
| 130 | + await page.goto("/"); |
| 131 | + await page.waitForLoadState("networkidle"); |
| 132 | + }); |
| 133 | +}); |
| 134 | +``` |
| 135 | +
|
| 136 | +**Selectors — in order of preference** |
| 137 | +
|
| 138 | +1. `.mx-name-*` — Mendix widget names (most stable): `page.locator(".mx-name-myWidget")` |
| 139 | +2. ARIA roles: `page.getByRole("button", { name: "Save" })` |
| 140 | +3. Widget CSS classes: `page.locator(".widget-badge-button-text")` |
| 141 | +4. Avoid brittle selectors: nth-child chains, deeply nested CSS, text-only locators |
| 142 | +
|
| 143 | +**Assertions** |
| 144 | +
|
| 145 | +```js |
| 146 | +await expect(page.locator(".mx-name-myWidget")).toBeVisible(); |
| 147 | +await expect(page.locator(".badge")).toContainText("New"); |
| 148 | +await expect(page.locator(".mx-name-myWidget")).toHaveScreenshot("myWidget-default.png"); |
| 149 | +``` |
| 150 | +
|
| 151 | +**Accessibility scanning (use for new interactive widgets)** |
| 152 | +
|
| 153 | +```js |
| 154 | +import AxeBuilder from "@axe-core/playwright"; |
| 155 | +const results = await new AxeBuilder({ page }).analyze(); |
| 156 | +expect(results.violations).toEqual([]); |
| 157 | +``` |
| 158 | +
|
| 159 | +**What to flag** |
| 160 | +
|
| 161 | +- Missing `afterEach` session logout — will exceed Mendix's 5-session license limit in CI |
| 162 | +- `page.waitForTimeout()` / hardcoded `sleep` — replace with `waitForLoadState` or a Playwright locator assertion |
| 163 | +- Selectors that don't use `.mx-name-*` when a Mendix widget name is available |
| 164 | +- Screenshot baselines not committed — `toHaveScreenshot` requires a baseline PNG in the repo |
| 165 | +- Missing `page.waitForLoadState("networkidle")` in `beforeEach` — causes flaky tests |
| 166 | +- E2E file not following `WidgetName.spec.js` naming convention |
| 167 | +
|
| 168 | +### Security |
| 169 | +
|
| 170 | +- **No `dangerouslySetInnerHTML`** unless input is sanitized with a trusted library (e.g. DOMPurify); flag any unsanitized usage as high severity |
| 171 | +- **No secrets or tokens** hardcoded in source — API keys, client IDs, URLs with credentials |
| 172 | +- **Safe external data handling**: validate and sanitize data from Mendix props before rendering or passing to DOM; guard against prototype pollution when merging objects |
| 173 | +- **No `eval()` or `new Function()`** with dynamic input |
| 174 | +- **Third-party scripts**: dynamic `<script>` injection must be reviewed for XSS risk |
| 175 | +- **URL handling**: verify `href`/`src` values derived from user input are validated (guard against `javascript:` and `data:` URIs) |
| 176 | +- **Event handlers**: avoid attaching global `window`/`document` listeners without cleanup — can leak references and be exploited |
| 177 | +
|
| 178 | +### Accessibility |
| 179 | +
|
| 180 | +Full requirements are in `docs/requirements/frontend-guidelines.md`. Flag only deviations: |
| 181 | +
|
| 182 | +- Interactive element is a `<div>` or `<span>` with `onClick` — replace with `<button>` or `<a>` |
| 183 | +- Icon-only button missing `aria-label` |
| 184 | +- `<img>` missing `alt` attribute (use `alt=""` for decorative) |
| 185 | +- Dialog/modal does not trap focus or restore it on close — use `FloatingFocusManager` |
| 186 | +- Colour used as the sole differentiator — pair with text or icon |
| 187 | +
|
| 188 | +## Heuristics |
| 189 | +
|
| 190 | +| Situation | Severity | Comment | |
| 191 | +| ---------------------------------------------- | -------- | ------------------------------------------------------ | |
| 192 | +| Code/XML changed, no CHANGELOG entry | Medium | `pnpm -w changelog` — version bumps are a separate PR | |
| 193 | +| Feature/fix without tests | Medium | Add unit tests; consider E2E for interactive behaviour | |
| 194 | +| `dangerouslySetInnerHTML` without sanitization | High | Require DOMPurify or equivalent before merge | |
| 195 | +| Hardcoded secret, token, or credential | Critical | Must be removed and rotated immediately | |
| 196 | +| `javascript:` or `data:` URI from user input | High | Validate before use — XSS risk | |
| 197 | +
|
| 198 | +## Scope |
| 199 | +
|
| 200 | +### What to review |
| 201 | +
|
| 202 | +| Path pattern | What to check | |
| 203 | +| -------------------------------------------------------------- | ---------------------------------------------------------------------------------- | |
| 204 | +| `packages/pluggableWidgets/*/src/**/*.{ts,tsx}` | Widget logic, React hooks, MobX, Mendix data API usage | |
| 205 | +| `packages/pluggableWidgets/*/*.xml` | Widget manifest: property keys (lowerCamelCase), unique ID, XML ↔ TS alignment | |
| 206 | +| `packages/pluggableWidgets/*/**/*.scss` | Styling: BEM naming, Atlas UI classes, no `!important`, no inline styles | |
| 207 | +| `packages/pluggableWidgets/*/src/**/__tests__/*.spec.{ts,tsx}` | Unit test coverage, builder usage, RTL patterns | |
| 208 | +| `packages/pluggableWidgets/*/e2e/*.spec.js` | E2E structure, selectors, afterEach logout, no hardcoded waits | |
| 209 | +| `packages/pluggableWidgets/*/package.json` | Version bumps happen in a separate PR — do not flag missing bumps | |
| 210 | +| `packages/pluggableWidgets/*/CHANGELOG.md` | Keep a Changelog entry present when runtime/XML/behavior changed | |
| 211 | +| `packages/pluggableWidgets/*/*.editorConfig.ts` | Studio Pro design-time config aligns with XML properties | |
| 212 | +| `packages/pluggableWidgets/*/*.editorPreview.tsx` | Preview component renders without crashing; no production-only imports | |
| 213 | +| `packages/shared/*/src/**/*.{ts,tsx}` | Shared utility changes — check for breaking API changes affecting widget consumers | |
| 214 | +| `packages/modules/*/src/**/*.{ts,tsx}` | Module-level logic and Mendix integration patterns | |
| 215 | +| `automation/**/*.{ts,mjs,cjs}` | Build/test automation scripts — no destructive ops, no hardcoded paths | |
| 216 | +| `.github/workflows/*.yml` | See workflow rules below | |
| 217 | +| `.claude/skills/**` | See skill rules below | |
| 218 | +
|
| 219 | +### What to ignore |
| 220 | +
|
| 221 | +- `dist/**` — build output, never review |
| 222 | +- `pnpm-lock.yaml` — lockfile-only changes need no review unless paired with `package.json` changes |
| 223 | +- `**/.turbo/**`, `**/node_modules/**` — generated/cached artifacts |
| 224 | +- `**/test-results/**`, `**/results/**` — test output directories |
| 225 | +- `dist/tmp/**` — intermediate build artifacts |
| 226 | +- `**/__snapshots__/**` — auto-generated snapshots (flag only if snapshot was deleted without test change) |
| 227 | +- `*.mpk` — compiled Mendix packages |
| 228 | +
|
| 229 | +### Multi-package PRs |
| 230 | +
|
| 231 | +When a PR touches multiple packages, validate each changed package separately: |
| 232 | +
|
| 233 | +- CHANGELOG entry per package (version bumps are out of scope) |
| 234 | +- XML ↔ TS alignment per widget |
| 235 | +- Test coverage per changed component |
| 236 | +
|
| 237 | +### GitHub Actions workflows (`.github/workflows/**`) |
| 238 | +
|
| 239 | +- All action references must be SHA-pinned with a version comment: `uses: actions/foo@<sha> # vX.Y` |
| 240 | +- Secrets must be read from `${{ secrets.* }}` — never hardcoded values |
| 241 | +- Permissions must be minimal: only declare what the job actually needs |
| 242 | +- Jobs that assume AWS/cloud roles must have `id-token: write` and use OIDC (`aws-actions/configure-aws-credentials`) |
| 243 | +- Jobs running on `pull_request` events should guard against fork PRs: check `head.repo.full_name` or `github.repository` |
| 244 | +- Jobs triggerable by comments (`issue_comment`) must restrict by `author_association` to prevent external contributors from triggering privileged workflows |
| 245 | +- Every long-running job should have `timeout-minutes` set |
| 246 | +- Shared `concurrency` groups with `cancel-in-progress: true` will cancel in-flight jobs — use per-job concurrency for interactive workflows |
| 247 | +
|
| 248 | +### Claude Code skills (`.claude/skills/**`) |
| 249 | +
|
| 250 | +- Frontmatter (`name`, `description`) must be present and accurate |
| 251 | +- Instructions should be actionable and specific — vague guidance leads to inconsistent reviews |
| 252 | +- Code examples must match the patterns actually used in this repo (check against `src/**` if unsure) |
| 253 | +- New sections should include a "What to flag" list of concrete anti-patterns |
| 254 | +
|
| 255 | +## Output format |
| 256 | +
|
| 257 | +**Determine the execution context first** by checking whether the `CI` environment variable is set (`echo $CI`): |
| 258 | +
|
| 259 | +- **CI environment** (`CI=true`): post the review as a PR comment using `gh`, following the template below. |
| 260 | +- **Local environment** (`CI` unset or empty): print the review directly to the terminal in the same format. Do NOT post any `gh` comment. |
| 261 | +
|
| 262 | +Use inline comments for issues that reference a specific line; use the summary comment/output for file-level or cross-cutting issues. |
| 263 | +
|
| 264 | +### Summary comment template |
| 265 | +
|
| 266 | +**Verdict line** — always the first line, one of: |
| 267 | +
|
| 268 | +- `✅ Approved — no issues found` |
| 269 | +- `⚠️ Approved with suggestions — low-severity items only, safe to merge` |
| 270 | +- `🔶 Changes requested — one or more medium-severity items must be addressed` |
| 271 | +- `🚨 Blocked — high-severity issue (security, data loss, broken API) must be fixed` |
| 272 | +
|
| 273 | +**Post the comment using exactly this structure** (GitHub Markdown — render as-is, do not wrap in a code fence): |
| 274 | +
|
| 275 | +The comment must start with: |
| 276 | +
|
| 277 | +``` |
| 278 | +## AI Code Review |
| 279 | +``` |
| 280 | +
|
| 281 | +Then a blockquote verdict on the next line: |
| 282 | +
|
| 283 | +``` |
| 284 | +> ✅ Approved — no issues found |
| 285 | +``` |
| 286 | +
|
| 287 | +(replace with the appropriate verdict emoji and text) |
| 288 | +
|
| 289 | +Then a horizontal rule `---`, then a `### What was reviewed` section with a Markdown table: |
| 290 | +
|
| 291 | +``` |
| 292 | +### What was reviewed |
| 293 | +
|
| 294 | +| File | Change | |
| 295 | +| --- | --- | |
| 296 | +| `path/to/file.ts` | Brief description of what changed | |
| 297 | +| `path/to/other.yml` | Brief description | |
| 298 | +
|
| 299 | +Skipped (out of scope): `dist/`, `pnpm-lock.yaml` |
| 300 | +``` |
| 301 | +
|
| 302 | +Then `---`, then a `### Findings` section (omit entirely on a clean PR) where each finding is a level-4 heading: |
| 303 | +
|
| 304 | +``` |
| 305 | +### Findings |
| 306 | +
|
| 307 | +#### 🚨 High — <short title> |
| 308 | +
|
| 309 | +**File:** `path/to/file.ts` line 42 |
| 310 | +**Problem:** What is wrong and why it matters. |
| 311 | +**Fix:** |
| 312 | +\`\`\`ts |
| 313 | +// suggested fix snippet |
| 314 | +\`\`\` |
| 315 | +
|
| 316 | +--- |
| 317 | +
|
| 318 | +#### 🔶 Medium — <short title> |
| 319 | +
|
| 320 | +**File:** `path/to/file.ts` line 10 |
| 321 | +**Problem:** What is wrong and why it matters. |
| 322 | +**Fix:** What to do (prose or snippet). |
| 323 | +
|
| 324 | +--- |
| 325 | +
|
| 326 | +#### ⚠️ Low — <short title> |
| 327 | +
|
| 328 | +**File:** `path/to/file.ts` line 5 |
| 329 | +**Note:** What to consider, not blocking. |
| 330 | +``` |
| 331 | +
|
| 332 | +Then `---`, then a `### Positives` section as a bullet list: |
| 333 | +
|
| 334 | +``` |
| 335 | +### Positives |
| 336 | +
|
| 337 | +- Specific thing done well (not generic praise) |
| 338 | +- Another concrete positive |
| 339 | +``` |
| 340 | +
|
| 341 | +### Rules |
| 342 | +
|
| 343 | +- **The summary comment is mandatory** — even on a clean PR. It proves the review ran. |
| 344 | +- Omit the **Findings** section entirely on a clean PR; do not write "no findings". |
| 345 | +- Each finding must include: severity emoji, a short title, the file + line, the problem, and a concrete fix. |
| 346 | +- Keep **Positives** specific — name what was done right (e.g. "SHAs are pinned with version comments"), not generic ("good job"). |
| 347 | +- One finding per `####` block — do not bundle multiple issues. |
| 348 | +- Be specific and actionable; avoid noise. |
0 commit comments