name: code-review 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.
Code Review
Review the PR diff against the standards in this repository. Read AGENTS.md for full repo context.
Before reviewing anything, run:
gh pr diff --name-only # build the exact list of changed files — only read these
gh pr checks # surface any failing CI checks before reading code
Only read files that appear in the diff output.
What to check on every PR
PR metadata
- Title: JIRA format
[XX-000]: descriptionor conventional commits (feat:,fix:, etc.) - Template adherence: lint/test run locally, new tests added, related PRs linked
- Multi-package PRs: validate each changed package separately
CI checks
Review the gh pr checks output before reading any files:
- Lint failing → Medium finding — paste the relevant error lines in the finding
- Unit tests failing → Medium finding — note the package and failing test file
- E2E tests failing → Medium finding — note the test name and failure reason
- All checks passing → add "All CI checks passed ✅" to the summary comment
Changelog (per changed package)
Version bumps happen in a separate dedicated PR — do not require or flag missing semver bumps.
If runtime code, public API, XML schema, or behavior changed:
- Require
CHANGELOG.mdentry (Keep a Changelog format) - Suggest:
pnpm -w changelog
If refactor/docs/tests-only: changelog entry not required — confirm with author.
Mendix-specific
AGENTS.md covers the core rules (canExecute, loading states, lowerCamelCase XML keys). Flag these additional review issues:
- XML changed but TS props not updated, or widget ID is not unique
EditableValueread without checking.status— can render stale/undefined dataActionValue.execute()called without checking.canExecutefirst
React
Flag these patterns — general React conventions are assumed known:
- Missing or wrong
useEffect/useMemo/useCallbackdeps; stale closures - Async effect sets state without a cleanup guard:
useEffect(() => { let active = true; (async () => { const data = await fetchData(); if (active) setState(data); })(); return () => { active = false; }; }, [fetchData]); - Array index used as list
key— requires a stable unique key - Props spread onto DOM nodes (
<div {...props}>) — strips unknown HTML attributes
MobX
makeAutoObservableormakeObservablemissing from store constructor- State mutation outside
action— suggestrunInActionoractionwrapper computedwith side effects — must be pure- Missing
observerHOC oruseSubscribe()from@mendix/widget-plugin-mobx-kiton React components that read MobX state
Styling
Conventions are in docs/requirements/frontend-guidelines.md. Flag only deviations:
- Inline styles used for static design
- Core Atlas UI classes overridden
!importantused- CSS class not prefixed with widget name
Unit tests
Files live in src/**/__tests__/*.spec.ts(x) and run with Jest + RTL (enzyme-free).
Structure
- Use
describe/itblocks; group related cases under a nesteddescribe - Define a
defaultPropsconstant and a factory render helper to avoid repetition:const defaultProps: MyWidgetProps = { ... }; const renderWidget = (props = defaultProps) => render(<MyWidget {...props} />);
Mendix data mocking — always use builders, never manual objects
import { EditableValueBuilder, ListValueBuilder, actionValue, obj } from "@mendix/widget-plugin-test-utils";
const value = new EditableValueBuilder<string>().withValue("hello").build();
const readOnly = new EditableValueBuilder<string>().withValue("x").isReadOnly().build();
const loading = new EditableValueBuilder<string>().isLoading().build();
const list = new ListValueBuilder().withItems([obj("A"), obj("B")]).build();
const action = actionValue(); // jest.fn() — assert with .execute toHaveBeenCalled()
What to cover
- All Mendix data states:
Available,Loading,Unavailable,ReadOnly - All prop/behavior branches (null checks, conditional renders, edge cases)
- User interactions via
fireEventoruserEvent - Verify
setValue/executecalls:expect(action.execute).toHaveBeenCalled() - Accessibility assertions:
getByRole,getByLabelText, ARIA attributes
What to flag
- Manual mock objects instead of builders — brittle and miss status edge cases
- Enzyme patterns (
shallow,mount,instance()) — must use RTL - Snapshot tests on dynamic content (dates, IDs, async state) — use specific assertions instead
- Missing
afterEachmock cleanup — causes test pollution - Missing
ResizeObserver/window.mxglobal setup when widget requires it (add tojest.setup.ts) - Jest config not extending
@mendix/pluggable-widgets-tools/test-config/jest.config
E2E tests
Full rules, waiting strategies, locator patterns, and spec template are in docs/requirements/e2e-test-guidelines.md. Flag only deviations:
What to flag
- Using raw
import { test } from "@playwright/test"— must use@mendix/run-e2e/fixturesto get worker-scoped sessions and auto-logout - Manual
afterEachsession logout viawindow.mx.session.logout()— no longer needed; the fixture handles it page.waitForTimeout()/ hardcodedsleep— replace with a web-first locator assertionpage.waitForLoadState("networkidle")— replace withwaitForMendixApp(page)from helpers, or prefer web-first assertions. Correct pattern:test.beforeEach(async ({ page }) => { await waitForMendixApp(page); // or: await expect(page.locator(".mx-name-...")).toBeVisible(); });- Selectors that don't use
.mx-name-*when a Mendix widget name is available - Screenshot baselines not committed —
toHaveScreenshotrequires a baseline PNG in the repo - E2E file not following
WidgetName.spec.jsnaming convention
Security
- No
dangerouslySetInnerHTMLunless input is sanitized with a trusted library (e.g. DOMPurify); flag any unsanitized usage as high severity - No secrets or tokens hardcoded in source — API keys, client IDs, URLs with credentials
- Safe external data handling: validate and sanitize data from Mendix props before rendering or passing to DOM; guard against prototype pollution when merging objects
- No
eval()ornew Function()with dynamic input - Third-party scripts: dynamic
<script>injection must be reviewed for XSS risk - URL handling: verify
href/srcvalues derived from user input are validated (guard againstjavascript:anddata:URIs) - Event handlers: avoid attaching global
window/documentlisteners without cleanup — can leak references and be exploited
Accessibility
Full requirements are in docs/requirements/frontend-guidelines.md. Flag only deviations:
- Interactive element is a
<div>or<span>withonClick— replace with<button>or<a> - Icon-only button missing
aria-label <img>missingaltattribute (usealt=""for decorative)- Dialog/modal does not trap focus or restore it on close — use
FloatingFocusManager - Colour used as the sole differentiator — pair with text or icon
Heuristics
| Situation | Severity | Comment |
|---|---|---|
| Code/XML changed, no CHANGELOG entry | Medium | pnpm -w changelog — version bumps are a separate PR |
| CI check failing (lint / unit / E2E) | Medium | Paste the error; do not approve until green or explained |
| Feature/fix without tests | Medium | Add unit tests; consider E2E for interactive behaviour |
dangerouslySetInnerHTML without sanitization |
High | Require DOMPurify or equivalent before merge |
| Hardcoded secret, token, or credential | Critical | Must be removed and rotated immediately |
javascript: or data: URI from user input |
High | Validate before use — XSS risk |
Scope
What to review
| Path pattern | What to check |
|---|---|
packages/pluggableWidgets/*/src/**/*.{ts,tsx} |
Widget logic, React hooks, MobX, Mendix data API usage |
packages/pluggableWidgets/*/*.xml |
Widget manifest: property keys (lowerCamelCase), unique ID, XML ↔ TS alignment |
packages/pluggableWidgets/*/**/*.scss |
Styling: BEM naming, Atlas UI classes, no !important, no inline styles |
packages/pluggableWidgets/*/src/**/__tests__/*.spec.{ts,tsx} |
Unit test coverage, builder usage, RTL patterns |
packages/pluggableWidgets/*/e2e/*.spec.js |
E2E structure, @mendix/run-e2e/fixtures import, selectors, no hardcoded waits |
packages/pluggableWidgets/*/package.json |
Version bumps happen in a separate PR — do not flag missing bumps |
packages/pluggableWidgets/*/CHANGELOG.md |
Keep a Changelog entry present when runtime/XML/behavior changed |
packages/pluggableWidgets/*/*.editorConfig.ts |
Studio Pro design-time config aligns with XML properties |
packages/pluggableWidgets/*/*.editorPreview.tsx |
Preview component renders without crashing; no production-only imports |
packages/shared/*/src/**/*.{ts,tsx} |
Shared utility changes — check for breaking API changes affecting widget consumers |
packages/modules/*/src/**/*.{ts,tsx} |
Module-level logic and Mendix integration patterns |
automation/**/*.{ts,mjs,cjs} |
Build/test automation scripts — no destructive ops, no hardcoded paths |
.github/workflows/*.yml |
See workflow rules below |
.agents/skills/** |
See skill rules below |
What to ignore
dist/**— build output, never reviewpnpm-lock.yaml— lockfile-only changes need no review unless paired withpackage.jsonchanges**/.turbo/**,**/node_modules/**— generated/cached artifacts**/test-results/**,**/results/**— test output directoriesdist/tmp/**— intermediate build artifacts**/__snapshots__/**— auto-generated snapshots (flag only if snapshot was deleted without test change)*.mpk— compiled Mendix packages
Multi-package PRs
When a PR touches multiple packages, validate each changed package separately:
- CHANGELOG entry per package (version bumps are out of scope)
- XML ↔ TS alignment per widget
- Test coverage per changed component
GitHub Actions workflows (.github/workflows/**)
- All action references must be SHA-pinned with a version comment:
uses: actions/foo@<sha> # vX.Y - Secrets must be read from
${{ secrets.* }}— never hardcoded values - Permissions must be minimal: only declare what the job actually needs
- Jobs that assume AWS/cloud roles must have
id-token: writeand use OIDC (aws-actions/configure-aws-credentials) - Jobs running on
pull_requestevents should guard against fork PRs: checkhead.repo.full_nameorgithub.repository - Jobs triggerable by comments (
issue_comment) must restrict byauthor_associationto prevent external contributors from triggering privileged workflows - Every long-running job should have
timeout-minutesset - Shared
concurrencygroups withcancel-in-progress: truewill cancel in-flight jobs — use per-job concurrency for interactive workflows
Claude Code skills (.agents/skills/**)
- Frontmatter (
name,description) must be present and accurate - Instructions should be actionable and specific — vague guidance leads to inconsistent reviews
- Code examples must match the patterns actually used in this repo (check against
src/**if unsure) - New sections should include a "What to flag" list of concrete anti-patterns
Output format
Determine the execution context first by checking whether the CI environment variable is set:
- CI environment (
CI=true): post the review as a PR comment usinggh, following the template below. - Local environment (
CIunset or empty): print the review directly to the terminal in the same format. Do NOT post anyghcomment.
Use inline comments for issues that reference a specific line; use the summary comment/output for file-level or cross-cutting issues.
Summary comment template
Verdict line — always the first line, one of:
✅ Approved — no issues found⚠️ Approved with suggestions — low-severity items only, safe to merge🔶 Changes requested — one or more medium-severity items must be addressed🚨 Blocked — high-severity issue (security, data loss, broken API) must be fixed
Post the comment using exactly this structure (GitHub Markdown — render as-is, do not wrap in a code fence):
The comment must start with:
## AI Code Review
Then a blockquote verdict on the next line:
> ✅ Approved — no issues found
(replace with the appropriate verdict emoji and text)
Then a horizontal rule ---, then a ### What was reviewed section with a Markdown table:
### What was reviewed
| File | Change |
| --- | --- |
| `path/to/file.ts` | Brief description of what changed |
| `path/to/other.yml` | Brief description |
Skipped (out of scope): `dist/`, `pnpm-lock.yaml`
Then ---, then a ### Findings section (omit entirely on a clean PR) where each finding is a level-4 heading:
### Findings
#### 🚨 High — <short title>
**File:** `path/to/file.ts` line 42
**Problem:** What is wrong and why it matters.
**Fix:**
\`\`\`ts
// suggested fix snippet
\`\`\`
---
#### 🔶 Medium — <short title>
**File:** `path/to/file.ts` line 10
**Problem:** What is wrong and why it matters.
**Fix:** What to do (prose or snippet).
---
#### ⚠️ Low — <short title>
**File:** `path/to/file.ts` line 5
**Note:** What to consider, not blocking.
Then ---, then a ### Positives section as a bullet list:
### Positives
- Specific thing done well (not generic praise)
- Another concrete positive
Rules
- The summary comment is mandatory — even on a clean PR. It proves the review ran.
- Omit the Findings section entirely on a clean PR; do not write "no findings".
- Each finding must include: severity emoji, a short title, the file + line, the problem, and a concrete fix.
- Keep Positives specific — name what was done right (e.g. "SHAs are pinned with version comments"), not generic ("good job").
- One finding per
####block — do not bundle multiple issues. - Be specific and actionable; avoid noise.