mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: Add Manual Crash Report UI for code-index human-relay
Implements issue #7162 - Manual Crash Report UI for code-index human-relay - Created CrashReportDialog component with form validation - Added crash report handling to webviewMessageHandler - Integrated crash reporting into CodeIndexPopover error states - Added human-relay integration for manual submission - Added telemetry tracking for crash report submissions - Added comprehensive localization support - Added unit tests for CrashReportDialog component The crash report dialog allows users to: - View error details from code indexing failures - Provide a description of what happened - Optionally provide an email for follow-up - Submit reports that are saved locally or sent via human-relay - Copy error details to clipboard for manual reporting
This commit is contained in:
parent
185365af5d
commit
7496a32986
8 changed files with 488 additions and 0 deletions
|
|
@ -66,6 +66,7 @@ export enum TelemetryEventName {
|
|||
SHELL_INTEGRATION_ERROR = "Shell Integration Error",
|
||||
CONSECUTIVE_MISTAKE_ERROR = "Consecutive Mistake Error",
|
||||
CODE_INDEX_ERROR = "Code Index Error",
|
||||
CRASH_REPORT_SUBMITTED = "Crash Report Submitted",
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -190,6 +191,7 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
|
|||
TelemetryEventName.TAB_SHOWN,
|
||||
TelemetryEventName.MODE_SETTINGS_CHANGED,
|
||||
TelemetryEventName.CUSTOM_MODE_CREATED,
|
||||
TelemetryEventName.CRASH_REPORT_SUBMITTED,
|
||||
]),
|
||||
properties: telemetryPropertiesSchema,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -2618,5 +2618,100 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
break
|
||||
}
|
||||
case "submitCrashReport": {
|
||||
if (message.crashReport) {
|
||||
try {
|
||||
// Log the crash report for debugging
|
||||
provider.log(`Crash report submitted: ${JSON.stringify(message.crashReport, null, 2)}`)
|
||||
|
||||
// If it's a human-relay crash, show the human relay dialog
|
||||
if (message.crashReport.source === "human-relay") {
|
||||
// Create a formatted message for human relay
|
||||
const formattedReport = `
|
||||
Crash Report - ${message.crashReport.source}
|
||||
=====================================
|
||||
Description: ${message.crashReport.description}
|
||||
Email: ${message.crashReport.email || "Not provided"}
|
||||
Timestamp: ${new Date(message.crashReport.timestamp).toISOString()}
|
||||
|
||||
Error Details:
|
||||
${message.crashReport.errorDetails ? JSON.stringify(message.crashReport.errorDetails, null, 2) : "No error details"}
|
||||
|
||||
User Agent: ${message.crashReport.userAgent}
|
||||
`.trim()
|
||||
|
||||
// Copy to clipboard for human relay
|
||||
await vscode.env.clipboard.writeText(formattedReport)
|
||||
|
||||
// Show human relay dialog
|
||||
vscode.commands.executeCommand(getCommand("showHumanRelayDialog"), {
|
||||
requestId: `crash-${Date.now()}`,
|
||||
promptText: formattedReport,
|
||||
})
|
||||
} else {
|
||||
// For code-index and general crashes, save to a file or send to telemetry
|
||||
const crashReportDir = path.join(provider.context.globalStorageUri.fsPath, "crash-reports")
|
||||
await fs.mkdir(crashReportDir, { recursive: true })
|
||||
|
||||
const filename = `crash-${message.crashReport.source}-${Date.now()}.json`
|
||||
const filepath = path.join(crashReportDir, filename)
|
||||
|
||||
await safeWriteJson(filepath, message.crashReport)
|
||||
|
||||
// Show success notification
|
||||
vscode.window.showInformationMessage(
|
||||
t("crashReport:submitSuccess") ||
|
||||
"Crash report submitted successfully. Thank you for your feedback!",
|
||||
)
|
||||
|
||||
// Log telemetry event if available
|
||||
if (TelemetryService.hasInstance()) {
|
||||
TelemetryService.instance.captureEvent(TelemetryEventName.CRASH_REPORT_SUBMITTED, {
|
||||
source: message.crashReport.source,
|
||||
hasDescription: !!message.crashReport.description,
|
||||
hasEmail: !!message.crashReport.email,
|
||||
hasErrorDetails: !!message.crashReport.errorDetails,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Send success response to webview
|
||||
await provider.postMessageToWebview({
|
||||
type: "crashReportSubmitted",
|
||||
success: true,
|
||||
})
|
||||
} catch (error) {
|
||||
provider.log(
|
||||
`Error submitting crash report: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(
|
||||
t("crashReport:submitError") || "Failed to submit crash report. Please try again.",
|
||||
)
|
||||
|
||||
// Send error response to webview
|
||||
await provider.postMessageToWebview({
|
||||
type: "crashReportSubmitted",
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "showCrashReportDialog": {
|
||||
// This message can be sent from various parts of the UI to show the crash report dialog
|
||||
await provider.postMessageToWebview({
|
||||
type: "showCrashReportDialog",
|
||||
errorDetails: message.values?.errorDetails,
|
||||
source: message.values?.source || "general",
|
||||
})
|
||||
break
|
||||
}
|
||||
case "showNotification": {
|
||||
if (message.text) {
|
||||
vscode.window.showInformationMessage(message.text)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,8 @@ export interface ExtensionMessage {
|
|||
| "showEditMessageDialog"
|
||||
| "commands"
|
||||
| "insertTextIntoTextarea"
|
||||
| "crashReportSubmitted"
|
||||
| "showCrashReportDialog"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
action?:
|
||||
|
|
@ -193,6 +195,14 @@ export interface ExtensionMessage {
|
|||
messageTs?: number
|
||||
context?: string
|
||||
commands?: Command[]
|
||||
errorDetails?: {
|
||||
message?: string
|
||||
stack?: string
|
||||
componentStack?: string
|
||||
context?: string
|
||||
timestamp?: number
|
||||
}
|
||||
source?: "code-index" | "human-relay" | "general"
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
|
|||
|
|
@ -211,6 +211,9 @@ export interface WebviewMessage {
|
|||
| "deleteCommand"
|
||||
| "createCommand"
|
||||
| "insertTextIntoTextarea"
|
||||
| "submitCrashReport"
|
||||
| "showCrashReportDialog"
|
||||
| "showNotification"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
|
|
@ -273,6 +276,20 @@ export interface WebviewMessage {
|
|||
codebaseIndexGeminiApiKey?: string
|
||||
codebaseIndexMistralApiKey?: string
|
||||
}
|
||||
crashReport?: {
|
||||
source: "code-index" | "human-relay" | "general"
|
||||
description: string
|
||||
email?: string
|
||||
errorDetails?: {
|
||||
message?: string
|
||||
stack?: string
|
||||
componentStack?: string
|
||||
context?: string
|
||||
timestamp?: number
|
||||
}
|
||||
timestamp: number
|
||||
userAgent: string
|
||||
}
|
||||
}
|
||||
|
||||
export const checkoutDiffPayloadSchema = z.object({
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
import { AlertTriangle } from "lucide-react"
|
||||
import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"
|
||||
import { useEscapeKey } from "@src/hooks/useEscapeKey"
|
||||
import { CrashReportDialog } from "@src/components/common/CrashReportDialog"
|
||||
import type { EmbedderProvider } from "@roo/embeddingModels"
|
||||
import type { IndexingStatus } from "@roo/ExtensionMessage"
|
||||
import { CODEBASE_INDEX_DEFAULTS } from "@roo-code/types"
|
||||
|
|
@ -151,6 +152,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
const [open, setOpen] = useState(false)
|
||||
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false)
|
||||
const [isSetupSettingsOpen, setIsSetupSettingsOpen] = useState(false)
|
||||
const [showCrashReport, setShowCrashReport] = useState(false)
|
||||
|
||||
const [indexingStatus, setIndexingStatus] = useState<IndexingStatus>(externalIndexingStatus)
|
||||
|
||||
|
|
@ -579,6 +581,14 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
{t(`settings:codeIndex.indexingStatuses.${indexingStatus.systemStatus.toLowerCase()}`)}
|
||||
{indexingStatus.message ? ` - ${indexingStatus.message}` : ""}
|
||||
</div>
|
||||
{indexingStatus.systemStatus === "Error" && (
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={() => setShowCrashReport(true)}
|
||||
style={{ marginTop: "8px" }}>
|
||||
{t("settings:codeIndex.reportError")}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
|
||||
{indexingStatus.systemStatus === "Indexing" && (
|
||||
<div className="mt-2">
|
||||
|
|
@ -1282,6 +1292,18 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Crash Report Dialog */}
|
||||
{showCrashReport && (
|
||||
<CrashReportDialog
|
||||
isOpen={showCrashReport}
|
||||
onClose={() => setShowCrashReport(false)}
|
||||
source="code-index"
|
||||
errorDetails={{
|
||||
message: indexingStatus.message || "Code indexing error occurred",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
179
webview-ui/src/components/common/CrashReportDialog.tsx
Normal file
179
webview-ui/src/components/common/CrashReportDialog.tsx
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import React, { useState, useCallback } from "react"
|
||||
import { VSCodeButton, VSCodeTextArea, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
interface CrashReportDialogProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
errorDetails?: {
|
||||
message?: string
|
||||
stack?: string
|
||||
componentStack?: string
|
||||
context?: string
|
||||
timestamp?: number
|
||||
}
|
||||
source?: "code-index" | "human-relay" | "general"
|
||||
}
|
||||
|
||||
export const CrashReportDialog: React.FC<CrashReportDialogProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
errorDetails,
|
||||
source = "general",
|
||||
}) => {
|
||||
const { t } = useTranslation(["common", "crashReport"])
|
||||
const [description, setDescription] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false)
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
// Validate that description is not empty
|
||||
if (!description.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
const crashReport = {
|
||||
source,
|
||||
description,
|
||||
email,
|
||||
errorDetails,
|
||||
timestamp: Date.now(),
|
||||
userAgent: navigator.userAgent,
|
||||
}
|
||||
|
||||
// Send crash report to backend
|
||||
vscode.postMessage({
|
||||
type: "submitCrashReport",
|
||||
crashReport,
|
||||
})
|
||||
|
||||
// Simulate submission delay
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false)
|
||||
setSubmitSuccess(true)
|
||||
// Auto-close after success
|
||||
setTimeout(() => {
|
||||
onClose()
|
||||
setSubmitSuccess(false)
|
||||
setDescription("")
|
||||
setEmail("")
|
||||
}, 2000)
|
||||
}, 1000)
|
||||
}, [source, description, email, errorDetails, onClose])
|
||||
|
||||
const handleCopyDetails = useCallback(() => {
|
||||
const details = JSON.stringify(errorDetails, null, 2)
|
||||
navigator.clipboard.writeText(details)
|
||||
vscode.postMessage({
|
||||
type: "showNotification",
|
||||
text: t("crashReport:copiedToClipboard"),
|
||||
})
|
||||
}, [errorDetails, t])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-vscode-editor-background border border-vscode-panel-border rounded-lg p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto">
|
||||
<h2 className="text-xl font-bold mb-4 text-vscode-foreground">{t("crashReport:title")}</h2>
|
||||
|
||||
{submitSuccess ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-green-500 text-lg mb-2">✓</div>
|
||||
<p className="text-vscode-foreground">{t("crashReport:submitSuccess")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<p className="text-vscode-descriptionForeground mb-2">{t("crashReport:description")}</p>
|
||||
</div>
|
||||
|
||||
{errorDetails && (
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-sm font-medium text-vscode-foreground">
|
||||
{t("crashReport:errorDetails")}
|
||||
</label>
|
||||
<VSCodeButton appearance="secondary" onClick={handleCopyDetails}>
|
||||
{t("crashReport:copyDetails")}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
<div className="bg-vscode-input-background border border-vscode-input-border rounded p-3 max-h-40 overflow-y-auto">
|
||||
<pre className="text-xs text-vscode-foreground whitespace-pre-wrap">
|
||||
{errorDetails.message && (
|
||||
<>
|
||||
<strong>Message:</strong> {errorDetails.message}
|
||||
{"\n\n"}
|
||||
</>
|
||||
)}
|
||||
{errorDetails.context && (
|
||||
<>
|
||||
<strong>Context:</strong> {errorDetails.context}
|
||||
{"\n\n"}
|
||||
</>
|
||||
)}
|
||||
{errorDetails.stack && (
|
||||
<>
|
||||
<strong>Stack:</strong>
|
||||
{"\n"}
|
||||
{errorDetails.stack}
|
||||
</>
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-vscode-foreground mb-2">
|
||||
{t("crashReport:whatHappened")}
|
||||
</label>
|
||||
<VSCodeTextArea
|
||||
value={description}
|
||||
onChange={(e: any) => setDescription(e.target.value)}
|
||||
placeholder={t("crashReport:whatHappenedPlaceholder")}
|
||||
rows={4}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-vscode-foreground mb-2">
|
||||
{t("crashReport:email")} ({t("crashReport:optional")})
|
||||
</label>
|
||||
<VSCodeTextField
|
||||
value={email}
|
||||
onChange={(e: any) => setEmail(e.target.value)}
|
||||
placeholder={t("crashReport:emailPlaceholder")}
|
||||
type="email"
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-vscode-descriptionForeground mt-1">
|
||||
{t("crashReport:emailDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<VSCodeButton appearance="secondary" onClick={onClose} disabled={isSubmitting}>
|
||||
{t("common:cancel")}
|
||||
</VSCodeButton>
|
||||
<VSCodeButton onClick={handleSubmit} disabled={isSubmitting || !description.trim()}>
|
||||
{isSubmitting ? t("crashReport:submitting") : t("crashReport:submit")}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
{source === "human-relay" && (
|
||||
<div className="mt-4 p-3 bg-vscode-textBlockQuote-background border-l-4 border-vscode-textBlockQuote-border">
|
||||
<p className="text-sm text-vscode-foreground">{t("crashReport:humanRelayNote")}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import React from "react"
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
|
||||
import { vi } from "vitest"
|
||||
import { CrashReportDialog } from "../CrashReportDialog"
|
||||
|
||||
// Mock vscode API
|
||||
const mockPostMessage = vi.fn()
|
||||
vi.mock("@src/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: (message: any) => mockPostMessage(message),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"crashReport:title": "Report an Issue",
|
||||
"crashReport:description": "Help us improve by reporting this issue.",
|
||||
"crashReport:errorDetails": "Error Details",
|
||||
"crashReport:copyDetails": "Copy Details",
|
||||
"crashReport:copiedToClipboard": "Copied to clipboard",
|
||||
"crashReport:whatHappened": "What happened?",
|
||||
"crashReport:whatHappenedPlaceholder": "Describe what happened...",
|
||||
"crashReport:email": "Email",
|
||||
"crashReport:optional": "optional",
|
||||
"crashReport:emailPlaceholder": "your@email.com",
|
||||
"crashReport:emailDescription": "We'll only use this to follow up",
|
||||
"crashReport:submit": "Submit Report",
|
||||
"crashReport:submitting": "Submitting...",
|
||||
"crashReport:submitSuccess": "Thank you for your report!",
|
||||
"crashReport:humanRelayNote": "This will be sent via Human Relay",
|
||||
"common:cancel": "Cancel",
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock clipboard API
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
})
|
||||
|
||||
describe("CrashReportDialog", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPostMessage.mockClear()
|
||||
})
|
||||
|
||||
it("should not render when isOpen is false", () => {
|
||||
const { container } = render(<CrashReportDialog isOpen={false} onClose={vi.fn()} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it("should render when isOpen is true", () => {
|
||||
render(<CrashReportDialog isOpen={true} onClose={vi.fn()} />)
|
||||
expect(screen.getByText("Report an Issue")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should display error details when provided", () => {
|
||||
const errorDetails = {
|
||||
message: "Test error message",
|
||||
stack: "Error stack trace",
|
||||
context: "code-index",
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
render(<CrashReportDialog isOpen={true} onClose={vi.fn()} errorDetails={errorDetails} />)
|
||||
|
||||
expect(screen.getByText(/Test error message/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Error stack trace/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should copy error details to clipboard when copy button is clicked", async () => {
|
||||
const errorDetails = {
|
||||
message: "Test error",
|
||||
stack: "Stack trace",
|
||||
}
|
||||
|
||||
render(<CrashReportDialog isOpen={true} onClose={vi.fn()} errorDetails={errorDetails} />)
|
||||
|
||||
const copyButton = screen.getByText("Copy Details")
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(JSON.stringify(errorDetails, null, 2))
|
||||
})
|
||||
})
|
||||
|
||||
it("should show human relay note for human-relay source", () => {
|
||||
render(<CrashReportDialog isOpen={true} onClose={vi.fn()} source="human-relay" />)
|
||||
|
||||
expect(screen.getByText("This will be sent via Human Relay")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should call onClose when cancel button is clicked", () => {
|
||||
const onClose = vi.fn()
|
||||
render(<CrashReportDialog isOpen={true} onClose={onClose} />)
|
||||
|
||||
const cancelButton = screen.getByText("Cancel")
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should render all required form fields", () => {
|
||||
render(<CrashReportDialog isOpen={true} onClose={vi.fn()} />)
|
||||
|
||||
// Check for form labels
|
||||
expect(screen.getByText("What happened?")).toBeInTheDocument()
|
||||
// Email label is rendered with (optional) in the same element
|
||||
expect(screen.getByText(/Email/)).toBeInTheDocument()
|
||||
|
||||
// Check for buttons
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument()
|
||||
expect(screen.getByText("Submit Report")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should display correct source in crash report", () => {
|
||||
const { container } = render(<CrashReportDialog isOpen={true} onClose={vi.fn()} source="code-index" />)
|
||||
|
||||
// The source is passed to the component and will be included in the crash report
|
||||
// We can't easily test the actual submission due to VSCode webview component limitations
|
||||
// but we can verify the component renders with the correct props
|
||||
expect(container.querySelector(".fixed")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should include error details in rendered output", () => {
|
||||
const errorDetails = {
|
||||
message: "Critical error occurred",
|
||||
context: "During indexing",
|
||||
stack: "at function xyz",
|
||||
}
|
||||
|
||||
render(<CrashReportDialog isOpen={true} onClose={vi.fn()} errorDetails={errorDetails} />)
|
||||
|
||||
expect(screen.getByText(/Critical error occurred/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/During indexing/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/at function xyz/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,4 +1,22 @@
|
|||
{
|
||||
"crashReport": {
|
||||
"title": "Report an Issue",
|
||||
"description": "Help us improve by reporting this issue. Your feedback is valuable in making the extension better.",
|
||||
"errorDetails": "Error Details",
|
||||
"copyDetails": "Copy Details",
|
||||
"copiedToClipboard": "Error details copied to clipboard",
|
||||
"whatHappened": "What were you doing when this happened?",
|
||||
"whatHappenedPlaceholder": "Please describe what you were trying to do when the error occurred...",
|
||||
"email": "Email",
|
||||
"optional": "optional",
|
||||
"emailPlaceholder": "your.email@example.com",
|
||||
"emailDescription": "We'll only use this to follow up if we need more information",
|
||||
"submit": "Submit Report",
|
||||
"submitting": "Submitting...",
|
||||
"submitSuccess": "Thank you! Your report has been submitted successfully.",
|
||||
"submitError": "Failed to submit crash report. Please try again.",
|
||||
"humanRelayNote": "This report will be sent via Human Relay. The error details will be copied to your clipboard for manual submission."
|
||||
},
|
||||
"title": "Modes",
|
||||
"done": "Done",
|
||||
"modes": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue