Skip to content
Merged
2 changes: 1 addition & 1 deletion docs/docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ In addition to testing for static things like text and attributes, you can also

You can read more about these in below documentations:

- [React Testing Library User Events](https://testing-library.com/docs/ecosystem-user-event)
- [React Testing Library User Events](https://testing-library.com/docs/user-event/intro/)
- [React Testing Library Jest DOM](https://testing-library.com/docs/ecosystem-jest-dom)
- [Official Testing Library](https://testing-library.com/docs/).

Expand Down
4 changes: 2 additions & 2 deletions docs/versioned_docs/version-2.4/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ In addition to testing for static things like text and attributes, you can also

You can read more about these in below documentations:

- [React Testing Library User Events](https://testing-library.com/docs/ecosystem-user-event)
- [React Testing Library User Events](https://testing-library.com/docs/user-event/intro/)
- [React Testing Library Jest DOM](https://testing-library.com/docs/ecosystem-jest-dom)
- [Official Testing Library](https://testing-library.com/docs/).

Expand Down Expand Up @@ -1118,7 +1118,7 @@ export const standard = (variables) => {

> An alternative explanation, written in TypeScript and featuring a Storybook example, [can be found on the RedwoodJS forum](https://community.redwoodjs.com/t/testing-forms-using-testing-library-user-event/2058).

To test our forms, we can make use of of the [`@testing-library/user-event`](https://testing-library.com/docs/ecosystem-user-event/) library which helps us approximate the the events that would actually happen in the browser if a real user were interacting with our forms. For example, calling `userEvent.click(checkbox)` toggles a checkbox as if a user had clicked it.
To test our forms, we can make use of of the [`@testing-library/user-event`](https://testing-library.com/docs/user-event/intro/) library which helps us approximate the the events that would actually happen in the browser if a real user were interacting with our forms. For example, calling `userEvent.click(checkbox)` toggles a checkbox as if a user had clicked it.

### Installing `@testing-library/user-event`

Expand Down
198 changes: 177 additions & 21 deletions packages/web/src/components/DevFatalErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// This file is a hard fork of panic-overlay for RedwoodJS. The original code
// This file is a hard fork of panic-overlay for CedarJS. The original code
// is licensed under The Unlicense - https://github.com/xpl/panic-overlay/blob/master/LICENSE
// making it fine for embedding inside this project.

// Stacktracey requires buffer, which Vite does not polyfill by default
import React, { useState } from 'react'
import React, { useState, useEffect, useRef } from 'react'

import type { GraphQLError } from 'graphql'
import StackTracey from 'stacktracey'
Expand Down Expand Up @@ -42,7 +42,113 @@ type ErrorWithRequestMeta = Error & {
mostRecentResponse?: any
}

function formatErrorForClipboard(
err: Error,
stack: StackTracey,
typeName: string,
msg: string,
errorWithMeta: ErrorWithRequestMeta,
): string {
const lines: string[] = []

lines.push('='.repeat(80))
lines.push('FATAL ERROR REPORT')
lines.push('='.repeat(80))
lines.push('')

// Error summary
lines.push(`ERROR TYPE: ${typeName}`)
lines.push(`ERROR MESSAGE: ${msg}`)
lines.push('')

// Request details if available
const mostRecentRequest =
errorWithMeta.mostRecentRequest ||
errorWithMeta.graphQLErrors?.find((gqlErr) => gqlErr.__RedwoodEnhancedError)
?.__RedwoodEnhancedError

if (mostRecentRequest) {
lines.push('-'.repeat(80))
lines.push('REQUEST CONTEXT')
lines.push('-'.repeat(80))
lines.push(`Operation: ${mostRecentRequest.operationName}`)
lines.push(`Kind: ${mostRecentRequest.operationKind}`)
lines.push('')
lines.push('Variables:')
try {
lines.push(JSON.stringify(mostRecentRequest.variables, null, 2))
} catch {
lines.push('Unable to stringify variables')
}
lines.push('')
lines.push('Query:')
lines.push(mostRecentRequest.query)
lines.push('')
}

// Response details if available
if (errorWithMeta.mostRecentResponse) {
lines.push('-'.repeat(80))
lines.push('RESPONSE CONTEXT')
lines.push('-'.repeat(80))
try {
lines.push(JSON.stringify(errorWithMeta.mostRecentResponse, null, 2))
} catch {
lines.push('Unable to stringify response')
}
lines.push('')
}

// Stack trace
lines.push('-'.repeat(80))
lines.push('STACK TRACE')
lines.push('-'.repeat(80))

stack.items.forEach((entry, i) => {
const fileShort = entry.fileShort || '[Unknown]'
const callee = entry.callee || '[Anonymous]'
const line = entry.line || '?'
const column = entry.column || '?'

lines.push(`[${i}] ${fileShort}:${line}:${column} in ${callee}`)

// Include source code context if available
const sourceFile = entry.sourceFile as any
if (sourceFile?.lines && sourceFile.lines.length > 0) {
const lineIndex = (entry.line || 1) - 1
const window = 2
const start = Math.max(0, lineIndex - window)
const end = Math.min(sourceFile.lines.length, lineIndex + window + 1)

lines.push(' Source context:')
for (let idx = start; idx < end; idx++) {
const marker = idx === lineIndex ? '> ' : ' '
lines.push(
` ${marker}${(idx + 1).toString().padStart(4)} | ${sourceFile.lines[idx]}`,
)
}
lines.push('')
}
})

lines.push('')
lines.push('='.repeat(80))

return lines.join('\n')
}

export const DevFatalErrorPage = (props: { error?: ErrorWithRequestMeta }) => {
const [copyFeedback, setCopyFeedback] = useState('')
const timeoutRef = useRef<NodeJS.Timeout | null>(null)

useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])

// Safety fallback
if (!props.error) {
return (
Expand All @@ -65,6 +171,20 @@ export const DevFatalErrorPage = (props: { error?: ErrorWithRequestMeta }) => {
<a href={toVSCodeURL(stack.items[0])}>{stack.items[0].fileName}</a>
) : null

const handleCopyAll = async () => {
const errorText = formatErrorForClipboard(err, stack, typeName, msg, err)

try {
await navigator.clipboard.writeText(errorText)
setCopyFeedback('✓ Copied to clipboard')
timeoutRef.current = setTimeout(() => setCopyFeedback(''), 3000)
} catch (clipboardError) {
setCopyFeedback('Failed to copy')
timeoutRef.current = setTimeout(() => setCopyFeedback(''), 3000)
console.error('Failed to copy error to clipboard:', clipboardError)
}
}

return (
<main className="error-page">
<style
Expand All @@ -76,7 +196,17 @@ export const DevFatalErrorPage = (props: { error?: ErrorWithRequestMeta }) => {
<nav>
<h1>A fatal runtime error occurred when rendering {FileRef}</h1>
<div>
Get help via <Discord /> or <Discourse />
<button
onClick={handleCopyAll}
className="copy-button"
title="Copy all error context to clipboard"
>
📋 Copy All
</button>
{copyFeedback && (
<span className="copy-feedback">{copyFeedback}</span>
)}
Get help via <Discord />
</div>
</nav>

Expand All @@ -86,13 +216,16 @@ export const DevFatalErrorPage = (props: { error?: ErrorWithRequestMeta }) => {
<span className="error-type">{typeName}</span>
<span className="error-message">{prettyMessage(msg)}</span>
</h3>
</div>
<ResponseRequest error={props.error} />
<div className="error">
<h3 className="section-title">Stack Trace</h3>
<div className="error-stack">
{stack.items.map((entry, i) => (
<StackEntry key={i} entry={entry} i={i} message={msg} />
))}
</div>
</div>
<ResponseRequest error={props.error} />
</section>
</main>
)
Expand Down Expand Up @@ -212,7 +345,10 @@ function StackEntry({
function toVSCodeURL(entry: StackTracey.Entry) {
// To account for folks using vscode-insiders etc
// This is defined by vite from .env
const scheme = RWJS_DEBUG_ENV.REDWOOD_ENV_EDITOR || 'vscode'
const scheme =
(typeof RWJS_DEBUG_ENV !== 'undefined' &&
RWJS_DEBUG_ENV.REDWOOD_ENV_EDITOR) ||
'vscode'
return `${scheme}://file/${entry.fileShort}:${entry.line}:${entry.column}`
}

Expand Down Expand Up @@ -333,6 +469,29 @@ main.error-page nav div a {
margin: 0 0.3em;
}

main.error-page nav .copy-button {
background-color: rgb(191, 71, 34);
color: white;
border: none;
padding: 0.5em 1em;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
margin-right: 1em;
transition: background-color 0.2s;
font-family: inherit;
}

main.error-page nav .copy-button:hover {
background-color: rgb(200, 32, 32);
}

main.error-page nav .copy-feedback {
color: rgb(191, 71, 34);
font-weight: bold;
margin-right: 1em;
}

main.error-page nav svg {
width: 24px;
height: 24px;
Expand Down Expand Up @@ -377,6 +536,18 @@ main.error-page nav svg:hover {
white-space: nowrap;
text-align: center;
}

.panic-overlay .section-title {
display: flex;
align-items: center;
padding: 1em 1em;
background: rgb(195, 74, 37);
color: white;
font-size: 1.1em;
font-weight: 600;
margin: 2em 0 1em 0;
}

.panic-overlay .error-counter {
color: white;
opacity: 0.3;
Expand Down Expand Up @@ -550,23 +721,8 @@ main.error-page nav svg:hover {
}
`

const Discourse = () => (
<a
href="https://community.redwoodjs.com"
title="Go to Redwood's Discourse server"
>
<svg
className="discourse"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
>
<path d="M16.1357143,0 C7.37857143,0 0,7.03571429 0,15.7214286 C0,16 0.00714285714,32 0.00714285714,32 L16.1357143,31.9857143 C24.9,31.9857143 32,24.6785714 32,15.9928571 C32,7.30714286 24.9,0 16.1357143,0 Z M16,25.1428571 C14.6142857,25.1428571 13.2928571,24.8357143 12.1142857,24.2785714 L6.32142857,25.7142857 L7.95714286,20.3571429 C7.25714286,19.0642857 6.85714286,17.5785714 6.85714286,16 C6.85714286,10.95 10.95,6.85714286 16,6.85714286 C21.05,6.85714286 25.1428571,10.95 25.1428571,16 C25.1428571,21.05 21.05,25.1428571 16,25.1428571 Z"></path>
</svg>
</a>
)

const Discord = () => (
<a href="https://discord.gg/redwoodjs" title="Go to Redwood's Discord server">
<a href="https://cedarjs.com/discord" title="Go to CedarJS's Discord server">
<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
<path d="M29.9699 7.7544C27.1043 5.44752 22.5705 5.05656 22.3761 5.04288C22.2284 5.03072 22.0806 5.0648 21.9531 5.1404C21.8257 5.216 21.7249 5.32937 21.6647 5.4648C21.5783 5.65936 21.5049 5.85949 21.4451 6.06384C23.3409 6.38424 25.6694 7.02864 27.7761 8.33616C27.8565 8.38604 27.9262 8.45126 27.9814 8.52809C28.0366 8.60493 28.0761 8.69187 28.0976 8.78397C28.1192 8.87607 28.1224 8.97151 28.1071 9.06485C28.0917 9.15819 28.0582 9.24759 28.0083 9.32796C27.9584 9.40833 27.8932 9.47809 27.8164 9.53325C27.7395 9.58842 27.6526 9.62791 27.5605 9.64947C27.4684 9.67103 27.373 9.67424 27.2796 9.65892C27.1863 9.6436 27.0969 9.61004 27.0165 9.56016C23.3949 7.3116 18.8719 7.2 17.9999 7.2C17.1287 7.2 12.6028 7.31232 8.98338 9.55944C8.90301 9.60932 8.81361 9.64288 8.72027 9.6582C8.62693 9.67352 8.53149 9.67031 8.43939 9.64875C8.25339 9.6052 8.09231 9.48955 7.99158 9.32724C7.89085 9.16493 7.85873 8.96925 7.90227 8.78325C7.94582 8.59725 8.06147 8.43617 8.22378 8.33544C10.3305 7.03152 12.659 6.38424 14.5547 6.06672C14.4453 5.7096 14.3459 5.48424 14.3387 5.4648C14.2788 5.32841 14.1776 5.2143 14.0493 5.13859C13.921 5.06288 13.7721 5.0294 13.6238 5.04288C13.4294 5.05728 8.89554 5.44752 5.99034 7.78536C4.47474 9.18792 1.43994 17.3894 1.43994 24.48C1.43994 24.6067 1.47378 24.7277 1.5357 24.8371C3.62802 28.5163 9.3405 29.4775 10.6423 29.52H10.6646C10.7782 29.5203 10.8903 29.4937 10.9916 29.4424C11.093 29.3911 11.1808 29.3165 11.2478 29.2248L12.5632 27.4133C9.01146 26.4967 7.19706 24.9386 7.09338 24.8458C6.95017 24.7194 6.86303 24.5412 6.85115 24.3506C6.83927 24.1599 6.90361 23.9723 7.03002 23.8291C7.15643 23.6859 7.33456 23.5988 7.52522 23.5869C7.71588 23.575 7.90345 23.6394 8.04666 23.7658C8.08842 23.8054 11.4299 26.64 17.9999 26.64C24.5807 26.64 27.9223 23.7938 27.9561 23.7658C28.0998 23.6403 28.2874 23.5769 28.4777 23.5896C28.668 23.6023 28.8456 23.69 28.9713 23.8334C29.0335 23.9042 29.0812 23.9864 29.1117 24.0756C29.1421 24.1647 29.1546 24.259 29.1486 24.353C29.1426 24.447 29.1181 24.5389 29.0766 24.6235C29.035 24.708 28.9772 24.7836 28.9065 24.8458C28.8028 24.9386 26.9884 26.4967 23.4367 27.4133L24.7528 29.2248C24.8198 29.3164 24.9074 29.3909 25.0087 29.4422C25.1099 29.4935 25.2218 29.5202 25.3353 29.52H25.3569C26.6601 29.4775 32.3719 28.5156 34.4649 24.8371C34.5261 24.7277 34.5599 24.6067 34.5599 24.48C34.5599 17.3894 31.5251 9.18864 29.9699 7.7544V7.7544ZM13.3199 21.6C11.9275 21.6 10.7999 20.3112 10.7999 18.72C10.7999 17.1288 11.9275 15.84 13.3199 15.84C14.7124 15.84 15.8399 17.1288 15.8399 18.72C15.8399 20.3112 14.7124 21.6 13.3199 21.6ZM22.6799 21.6C21.2875 21.6 20.1599 20.3112 20.1599 18.72C20.1599 17.1288 21.2875 15.84 22.6799 15.84C24.0724 15.84 25.1999 17.1288 25.1999 18.72C25.1999 20.3112 24.0724 21.6 22.6799 21.6Z"></path>
</svg>
Expand Down
Loading
Loading