mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix: contain unbroken dialog failure output (#13531)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { cleanup, render, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { CrashReportRecord } from '../../../../shared/crash-reporting'
|
||||
import { CrashReportDialogSurface } from './CrashReportDialogSurface'
|
||||
|
||||
const viewer = vi.fn(async () => null)
|
||||
|
||||
vi.mock('./use-crash-report-copy', () => ({
|
||||
useCrashReportCopy: () => vi.fn(async () => {})
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/dialog', () => ({
|
||||
Dialog: ({ open, children }: { open: boolean; children?: ReactNode }) =>
|
||||
open ? <div>{children}</div> : null,
|
||||
DialogContent: ({ className, children }: { className?: string; children?: ReactNode }) => (
|
||||
<div role="dialog" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogDescription: ({ children }: { children?: ReactNode }) => <p>{children}</p>,
|
||||
DialogFooter: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
|
||||
DialogHeader: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
|
||||
DialogTitle: ({ children }: { children?: ReactNode }) => <h2>{children}</h2>
|
||||
}))
|
||||
|
||||
function crashReport(error: string): CrashReportRecord {
|
||||
return {
|
||||
id: 'crash-1',
|
||||
createdAt: '2026-08-10T00:00:00.000Z',
|
||||
status: 'pending',
|
||||
source: 'renderer',
|
||||
processType: 'renderer',
|
||||
reason: 'crashed',
|
||||
exitCode: 5,
|
||||
appVersion: '1.0.0',
|
||||
platform: 'darwin',
|
||||
osRelease: 'test',
|
||||
arch: 'arm64',
|
||||
electronVersion: '41',
|
||||
chromeVersion: '141',
|
||||
details: { error }
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
viewer.mockClear()
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
value: { gh: { viewer } }
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => cleanup())
|
||||
|
||||
describe('CrashReportDialogSurface overflow containment', () => {
|
||||
it('keeps unbroken diagnostic output inside the dialog grid', async () => {
|
||||
const unbrokenError = 'A'.repeat(1000)
|
||||
const { container } = render(
|
||||
<CrashReportDialogSurface
|
||||
open
|
||||
report={crashReport(unbrokenError)}
|
||||
loading={false}
|
||||
onOpenChange={() => {}}
|
||||
onReportChange={() => {}}
|
||||
/>
|
||||
)
|
||||
await waitFor(() => expect(viewer).toHaveBeenCalledOnce())
|
||||
|
||||
const dialog = container.querySelector('[role="dialog"]')
|
||||
const output = dialog?.querySelector('pre')
|
||||
expect(output?.textContent).toContain(unbrokenError)
|
||||
expect(output?.className).toContain('[overflow-wrap:anywhere]')
|
||||
expect(output?.className).not.toContain('break-words')
|
||||
|
||||
const gridChild = Array.from(dialog?.children ?? []).find((child) =>
|
||||
child.contains(output ?? null)
|
||||
)
|
||||
expect(gridChild?.className).toContain('min-w-0')
|
||||
expect(output?.parentElement?.className).toContain('min-w-0')
|
||||
})
|
||||
})
|
||||
@@ -241,7 +241,7 @@ export function CrashReportDialogSurface({
|
||||
<DialogDescription className="text-xs">{getDialogDescription(report)}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="min-w-0 space-y-3">
|
||||
{report ? (
|
||||
<>
|
||||
<div className="rounded-md border border-border/70 bg-muted/30 p-3 text-xs">
|
||||
@@ -252,14 +252,14 @@ export function CrashReportDialogSurface({
|
||||
{report.appVersion}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<div className="text-[11px] font-medium text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.crash.report.CrashReportDialog.6d3ebe216a',
|
||||
'Diagnostic text'
|
||||
)}
|
||||
</div>
|
||||
<pre className="max-h-44 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-muted/20 p-3 font-mono text-[11px] leading-5 text-muted-foreground scrollbar-sleek">
|
||||
<pre className="max-h-44 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md border border-border bg-muted/20 p-3 font-mono text-[11px] leading-5 text-muted-foreground scrollbar-sleek">
|
||||
{diagnosticText}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -73,8 +73,10 @@ vi.mock('@/components/ui/collapsible', () => ({
|
||||
CollapsibleTrigger: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
|
||||
// Tagged so tests can prove WHERE content sits: this mock renders it whether
|
||||
// or not the disclosure is open, so presence in the DOM alone proves nothing.
|
||||
CollapsibleContent: ({ children }: { children?: ReactNode }) => (
|
||||
<div data-collapsible-content>{children}</div>
|
||||
CollapsibleContent: ({ className, children }: { className?: string; children?: ReactNode }) => (
|
||||
<div className={className} data-collapsible-content>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
|
||||
@@ -386,6 +388,34 @@ describe('SkillFreshnessUpdateDialog', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('contains unbroken failure output inside the dialog grid', async () => {
|
||||
const unbrokenOutput = 'A'.repeat(1000)
|
||||
const unbrokenMessage = 'B'.repeat(1000)
|
||||
await renderDialog()
|
||||
await openViaRequest()
|
||||
await emitRun({
|
||||
state: 'error',
|
||||
names: ['orca-cli'],
|
||||
failedNames: ['orca-cli'],
|
||||
finishedAt: 3,
|
||||
output: unbrokenOutput,
|
||||
message: unbrokenMessage
|
||||
})
|
||||
|
||||
const output = container?.querySelector('pre')
|
||||
expect(output?.textContent).toContain(unbrokenOutput)
|
||||
expect(output?.className).toContain('[overflow-wrap:anywhere]')
|
||||
expect(output?.className).not.toContain('break-words')
|
||||
expect(output?.parentElement?.className).toContain('min-w-0')
|
||||
expect(output?.closest('[data-collapsible-open]')?.className).toContain('min-w-0')
|
||||
|
||||
const message = Array.from(container?.querySelectorAll('p') ?? []).find(
|
||||
(candidate) => candidate.textContent === unbrokenMessage
|
||||
)
|
||||
expect(message?.className).toContain('[overflow-wrap:anywhere]')
|
||||
expect(message?.parentElement?.className).toContain('min-w-0')
|
||||
})
|
||||
|
||||
it('shows the up-to-date state once every installation is current', async () => {
|
||||
mocks.inventory = {
|
||||
schemaVersion: 1,
|
||||
@@ -673,6 +703,21 @@ describe('SkillFreshnessUpdateDialog', () => {
|
||||
expect(container?.querySelector('[data-skill-row="orca-cli"]')).toBeNull()
|
||||
expect(findButton('Update 1 skill')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('wraps an unbroken scan error inside the dialog grid', async () => {
|
||||
const unbrokenError = 'C'.repeat(1000)
|
||||
mocks.inventory = null
|
||||
mocks.error = unbrokenError
|
||||
await renderDialog()
|
||||
await openViaRequest()
|
||||
|
||||
const error = Array.from(container?.querySelectorAll('p') ?? []).find(
|
||||
(candidate) => candidate.textContent === unbrokenError
|
||||
)
|
||||
expect(error?.className).toContain('min-w-0')
|
||||
expect(error?.className).toContain('[overflow-wrap:anywhere]')
|
||||
})
|
||||
|
||||
it('shows incomplete plugin coverage without presenting a fabricated skill copy', async () => {
|
||||
mocks.inventory = {
|
||||
schemaVersion: 1,
|
||||
|
||||
@@ -41,7 +41,7 @@ function RunLog({ output }: { output: string }): React.JSX.Element | null {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Collapsible>
|
||||
<Collapsible className="min-w-0">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -53,9 +53,9 @@ function RunLog({ output }: { output: string }): React.JSX.Element | null {
|
||||
{translate('auto.components.skills.SkillFreshnessUpdateDialog.showLog', 'Show log')}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-1">
|
||||
<CollapsibleContent className="mt-1 min-w-0">
|
||||
{/* Displayed verbatim, never parsed — `skills update` has no --json. */}
|
||||
<pre className="scrollbar-sleek max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-muted px-3 py-2.5 font-mono text-[11px] leading-relaxed text-muted-foreground">
|
||||
<pre className="scrollbar-sleek max-h-40 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md border border-border bg-muted px-3 py-2.5 font-mono text-[11px] leading-relaxed text-muted-foreground">
|
||||
{output.trim()}
|
||||
</pre>
|
||||
</CollapsibleContent>
|
||||
@@ -278,7 +278,7 @@ export function SkillFreshnessUpdateDialog(): React.JSX.Element {
|
||||
</DialogHeader>
|
||||
|
||||
{state.error && !isRunning && !showResult ? (
|
||||
<p className="text-xs text-destructive">{state.error}</p>
|
||||
<p className="min-w-0 [overflow-wrap:anywhere] text-xs text-destructive">{state.error}</p>
|
||||
) : (
|
||||
headline
|
||||
)}
|
||||
@@ -323,14 +323,14 @@ export function SkillFreshnessUpdateDialog(): React.JSX.Element {
|
||||
) : null}
|
||||
|
||||
{run.state === 'error' ? (
|
||||
<div className="space-y-2.5 rounded-md border border-destructive/35 bg-destructive/10 p-3">
|
||||
<div className="min-w-0 space-y-2.5 rounded-md border border-destructive/35 bg-destructive/10 p-3">
|
||||
<p className="text-[13px] font-medium text-foreground">
|
||||
{translate(
|
||||
'auto.components.skills.SkillFreshnessUpdateDialog.errorTitle',
|
||||
"The update didn't finish"
|
||||
)}
|
||||
</p>
|
||||
<p className="break-words font-mono text-[11px] leading-relaxed text-muted-foreground">
|
||||
<p className="[overflow-wrap:anywhere] font-mono text-[11px] leading-relaxed text-muted-foreground">
|
||||
{run.message}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
|
||||
Reference in New Issue
Block a user