| name | trace-error-messages |
| description | Use this skill when writing tests that assert on error messages, or when an error assertion is failing and the source of the message is unclear. Invoke automatically when: a Cypress assertion on error text is failing, you are writing a new test for an error state, the user asks "why is this error message showing?", or you need to mock an API error in a test. Also invoke when adding error state coverage to a form or page.
|
| argument-hint | [component-or-test-file] |
| disable-model-invocation | false |
| user-invocable | true |
| allowed-tools | Bash, Read, Grep, Glob |
Trace Error Messages
Systematic approach to locating error message sources before writing test assertions.
Rule: Never assume an error message. Trace it to its source first.
When to invoke
Invoke this skill when:
- A Cypress assertion on error text is failing
- You are writing a new test for an error state
- The user asks "why is this error message showing?"
- You need to mock an API error in a test
- You are adding error state coverage to a form or page
Stack layers
Errors flow through these layers ā identify which layer owns the message before writing assertions:
API Definition (OpenAPI)
ā
Generated Service (src/api/__generated__/services/)
ā
React Query Hook (src/api/hooks/)
ā
MSW Mock Handler (src/api/hooks/[hook]/__handlers__/)
ā
React Component (error display: Toast / Alert / inline)
ā
RJSF Form (client-side validation only)
ā
Page Object (cypress/pages/)
ā
Cypress Test (cypress/e2e/)
Decision tree ā start here
Is this a form validation error (before any API call)?
āā YES ā Step 5 (RJSF). Check JSON Schema required fields.
ā Assert on the RJSF error summary panel.
ā
āā NO ā Is this an API error?
āā YES ā Steps 1ā4. Trace: OpenAPI ā hook ā MSW handler ā component.
ā
āā NO ā Network/timeout error ā Step 4. React Query + generic error UI.
7-step tracing guide
Step 1 ā OpenAPI service definition
Where: src/api/__generated__/services/[ServiceName].ts
Find the errors object ā these are the API contract messages:
errors: {
400: `Bridge is invalid`,
401: `The requested credentials could not be authenticated.`,
}
The errors field values propagate as ApiError.message. They are not what the UI displays ā components usually show a different i18n key or ApiError.body.title.
Step 2 ā React Query hook
Where: src/api/hooks/[hookName]/[hookName].ts
Verify the error type: useMutation<Success, ApiError, Input>.
ApiError contains:
message ā status text from the OpenAPI errors object
body.title ā human-readable message from the response body
status ā HTTP status code
The component typically displays err.body.title, not err.message.
Step 3 ā MSW mock handler
Where: src/api/hooks/[hookName]/__handlers__/index.ts
This controls what error the test actually sees. If an error handler does not exist yet, add one:
http.post('*/endpoint', () => {
return HttpResponse.json({ title: 'Your error text here' }, { status: 400 })
})
The title in the response body is what ApiError.body.title contains ā and what the component displays.
Step 4 ā React component
Where: src/modules/[Module]/components/[Component].tsx
Find where the hook's error is consumed:
.catch((err: ApiError) => ...) on mutations
error property from useQuery
Identify what the component actually renders:
err.body.title ā the server-provided message (from the MSW handler)
t('some.key') ā a generic i18n message
- Both mixed (generic title + specific detail from
err.body.title)
Step 5 ā RJSF client-side validation
Where: JSON Schema for the form (in src/api/schemas/ or inline in the component)
RJSF generates error messages automatically from the schema:
- Required field:
"[fieldName] is a required property"
- Format error:
"[fieldName] must match format 'email'"
These appear before any API call. Assert on the RJSF error summary panel:
cy.get('[role="alert"] ul li').eq(0).should('contain.text', 'id is a required property')
Step 6 ā Page Object getter
Where: cypress/pages/[Module]/[ModulePage].ts
Check whether an error getter already exists:
grep -r "error\|alert\|toast" cypress/pages/[Module]/ --include="*.ts"
If not, add one. Never use raw selectors in test bodies.
Step 7 ā Cypress test
Write assertions only after completing steps 1ā6. Use the exact message from whichever layer owns it:
bridgePage.config.errorSummaryItem(0).should('contain.text', 'id is a required property')
loginPage.errorAlertMessage.should('have.text', 'Invalid username and/or password')
Use contain.text for partial matches when message format may evolve. Use have.text for exact matches on critical UX strings.
Error Message Source Matrix
| Error type | Source | Example | When it appears |
|---|
| Client-side validation | RJSF / JSON Schema | "id is a required property" | Before API call |
| API contract message | OpenAPI errors object | "Bridge is invalid" | ApiError.message |
| API response body | MSW handler body.title | "Invalid username and/or password" | ApiError.body.title ā what the UI shows |
| Generic user-facing | i18n translation key | "Unable to create bridge" | Generic label wrapping the detail |
| Network error | Axios / Fetch | "Network Error" | Connection failure |
Do and don't
Do:
- Trace the full stack before writing any assertion
- Use partial matches (
contain.text) when message format may change
- Create page object getters for error containers before writing assertions
- Comment the error source in the test:
// MSW handler: body.title
- Test RJSF validation and API errors as separate test cases
Don't:
- Assume the error text ā always verify in source
- Hard-code DOM selectors in test bodies (use page objects)
- Mix up client validation errors and API errors in the same assertion
- Test toast errors with Percy ā timing makes snapshots unreliable