mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
perf(renderer): keep modal blur with independent source regions (#33799)
* perf(renderer): drop full-window overlay blurs and the idle usage-chip pulse A full-window backdrop-filter is re-blurred on every frame while anything beneath it animates, which held ~370 MB of extra GPU render-pass textures and swapchain buffers while a dialog, sheet, palette or onboarding was open. The 2px scrim blur is barely visible under the black scrim, so keep the dim only (5 points darker to keep the background as recessive) and drop the panel blur that sits under a 96%-opaque background. The status-bar usage chip also pulsed forever before its first fetch (unfocused windows never poll); show it static until a fetch is actually running. * perf(renderer): preserve modal blur with independent source regions * fix(renderer): preserve modal blur recovery and Space coverage * fix(ci): remove stale context-menu import after main merge --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { ConfirmationDialogProvider } from './components/confirmation-dialog'
|
||||
@@ -108,7 +109,10 @@ function App(): React.JSX.Element {
|
||||
<BrowserWebAuthnAccountDialog />
|
||||
</LinkRoutingPreferenceDialogProvider>
|
||||
</ConfirmationDialogProvider>
|
||||
<Toaster closeButton toastOptions={{ className: 'font-sans text-sm' }} />
|
||||
{createPortal(
|
||||
<Toaster closeButton toastOptions={{ className: 'font-sans text-sm' }} />,
|
||||
document.body
|
||||
)}
|
||||
<PlainSshTerminalPanels />
|
||||
</TooltipProvider>
|
||||
<SkillFreshnessNudge />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OnboardingLanguageLoading } from '../components/onboarding/OnboardingLa
|
||||
import { useLanguageReadiness } from '../i18n/language-readiness'
|
||||
import { NotificationCardStack } from '../components/NotificationCardStack'
|
||||
import { Suspense } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { lazyWithRetry as lazy } from '@/lib/lazy-with-retry'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { RecoverableRenderErrorBoundary } from '../components/error-boundaries/RecoverableRenderErrorBoundary'
|
||||
@@ -347,28 +348,32 @@ export function AppRootSurfaces(props: {
|
||||
>
|
||||
<CrashReportDialog />
|
||||
</RecoverableRenderErrorBoundary>
|
||||
{onboardingGate.onboarding && onboardingGate.shouldRender ? (
|
||||
<Suspense fallback={null}>
|
||||
<RecoverableRenderErrorBoundary
|
||||
boundaryId="modal.onboarding"
|
||||
surface="modal"
|
||||
title={translate('auto.App.f02d37278a', 'Onboarding hit an error.')}
|
||||
description={translate(
|
||||
'auto.App.221a95ba38',
|
||||
'Retry onboarding or close it and continue in the app.'
|
||||
)}
|
||||
>
|
||||
{languageReady ? (
|
||||
<OnboardingFlow
|
||||
onboarding={onboardingGate.onboarding}
|
||||
onOnboardingChange={onboardingGate.setOnboarding}
|
||||
/>
|
||||
) : (
|
||||
<OnboardingLanguageLoading />
|
||||
)}
|
||||
</RecoverableRenderErrorBoundary>
|
||||
</Suspense>
|
||||
) : null}
|
||||
{onboardingGate.onboarding && onboardingGate.shouldRender
|
||||
? createPortal(
|
||||
<Suspense fallback={null}>
|
||||
<RecoverableRenderErrorBoundary
|
||||
boundaryId="modal.onboarding"
|
||||
surface="modal"
|
||||
className="fixed inset-0 z-40 bg-background"
|
||||
title={translate('auto.App.f02d37278a', 'Onboarding hit an error.')}
|
||||
description={translate(
|
||||
'auto.App.221a95ba38',
|
||||
'Retry onboarding or close it and continue in the app.'
|
||||
)}
|
||||
>
|
||||
{languageReady ? (
|
||||
<OnboardingFlow
|
||||
onboarding={onboardingGate.onboarding}
|
||||
onOnboardingChange={onboardingGate.setOnboarding}
|
||||
/>
|
||||
) : (
|
||||
<OnboardingLanguageLoading />
|
||||
)}
|
||||
</RecoverableRenderErrorBoundary>
|
||||
</Suspense>,
|
||||
document.body
|
||||
)
|
||||
: null}
|
||||
{shouldMountDictationController ? (
|
||||
<Suspense fallback={null}>
|
||||
<OverlayBoundary boundaryId="overlay.dictation" resetKey={activeView}>
|
||||
|
||||
@@ -865,6 +865,23 @@ body:has(> [data-slot='dialog-overlay'], > [data-slot='sheet-overlay'])
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Blur source regions independently so status-bar animation cannot re-filter the workspace. */
|
||||
body:has(> [data-slot='dialog-overlay'], > [data-slot='sheet-overlay'], > [data-onboarding-overlay])
|
||||
.app-layout
|
||||
> :not(.window-controls) {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
body
|
||||
> :is(
|
||||
[data-slot='dialog-content'],
|
||||
[data-slot='command-dialog-content'],
|
||||
[data-slot='sheet-content'],
|
||||
[data-onboarding-overlay]
|
||||
):has(~ [data-slot='dialog-overlay'], ~ [data-slot='sheet-overlay']) {
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
/* Why: native windows have a stable content box and need no wake-time viewport reflow. */
|
||||
html.native-shell,
|
||||
html.native-shell body,
|
||||
|
||||
@@ -30,7 +30,6 @@ import { isEditableTarget } from '@/lib/editable-target'
|
||||
import { findEditablePrimarySelectionPasteTarget } from '@/lib/primary-selection-paste'
|
||||
import { NativeChatCopyOrcaSessionIdMenuItem } from './NativeChatCopyOrcaSessionIdMenuItem'
|
||||
import { NativeChatSelectionQuote } from './NativeChatSelectionQuote'
|
||||
import { StructuredChatPaneMenuItems } from './StructuredChatPaneMenuItems'
|
||||
import { NativeChatPaneMenuItems } from './NativeChatPaneMenuItems'
|
||||
import { NativeChatStructuredPaneMenuItems } from './NativeChatStructuredPaneMenuItems'
|
||||
import type { NativeChatComposerHandle } from './native-chat-composer-types'
|
||||
|
||||
@@ -197,7 +197,7 @@ export default function OnboardingFlow({
|
||||
return (
|
||||
<TooltipProvider delayDuration={0} skipDelayDuration={0}>
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center overflow-hidden bg-black/50 p-4 text-foreground backdrop-blur-[2px]"
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center overflow-hidden bg-black/50 p-4 text-foreground"
|
||||
data-onboarding-overlay
|
||||
onPointerDown={(event) => {
|
||||
if (!shouldRequestOnboardingSkipConfirmation(event)) {
|
||||
|
||||
@@ -263,20 +263,12 @@ export function ProviderSegment({
|
||||
: []
|
||||
)
|
||||
|
||||
if (!p || p.status === 'idle') {
|
||||
// Why static: unfocused windows never poll, so a pending chip's pulse would redraw the compositor indefinitely.
|
||||
if (!p || p.status === 'idle' || (p.status === 'fetching' && !tightest)) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-muted-foreground">
|
||||
<ProviderIcon provider={provider} />
|
||||
<span className="animate-pulse">···</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (p.status === 'fetching' && !tightest) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-muted-foreground">
|
||||
<ProviderIcon provider={provider} />
|
||||
<span className="animate-pulse">···</span>
|
||||
<span>···</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as DialogPrimitive from 'radix-ui/dialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { handleImeOverlayEscape } from '@/lib/ime-overlay-escape'
|
||||
import { useImeTextFieldProps } from '@/lib/ime-text-field'
|
||||
import { DialogOverlay } from '@/components/ui/dialog'
|
||||
|
||||
const commandVariants = cva('flex h-full w-full flex-col overflow-hidden rounded-md', {
|
||||
variants: {
|
||||
@@ -49,6 +50,7 @@ function CommandDialog({
|
||||
onCloseAutoFocus,
|
||||
contentClassName,
|
||||
overlayClassName,
|
||||
surface = 'default',
|
||||
commandProps,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root> & {
|
||||
@@ -59,6 +61,8 @@ function CommandDialog({
|
||||
onCloseAutoFocus?: (e: Event) => void
|
||||
contentClassName?: string
|
||||
overlayClassName?: string
|
||||
/** `palette` is the larger full-search surface (worktree jump palette). */
|
||||
surface?: 'default' | 'palette'
|
||||
commandProps?: React.ComponentProps<typeof CommandPrimitive>
|
||||
}) {
|
||||
const { className: commandClassName, ...commandRootProps } = commandProps ?? {}
|
||||
@@ -66,17 +70,14 @@ function CommandDialog({
|
||||
return (
|
||||
<DialogPrimitive.Root {...props}>
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
// Match DialogOverlay’s scrim/blur so it remains visible over the dark canvas.
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/55 backdrop-blur-[2px] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||
overlayClassName
|
||||
)}
|
||||
/>
|
||||
<DialogOverlay className={overlayClassName} />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="command-dialog-content"
|
||||
// Match DialogContent’s surface/border/shadow recipe so the palette remains distinct in dark mode.
|
||||
className={cn(
|
||||
'fixed top-[20%] left-[50%] z-50 w-[660px] max-w-[90vw] translate-x-[-50%] rounded-lg border border-black/14 bg-background/96 text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-2xl outline-none dark:border-white/14 dark:bg-[rgba(23,23,23,0.96)] dark:shadow-[0_24px_72px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.06)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
'fixed top-[20%] left-[50%] z-50 w-[660px] max-w-[90vw] translate-x-[-50%] rounded-lg border border-black/14 bg-background/96 text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.08)] outline-none dark:border-white/14 dark:bg-[rgba(23,23,23,0.96)] dark:shadow-[0_24px_72px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.06)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
surface === 'palette' &&
|
||||
'rounded-xl border-border/70 shadow-[0_26px_84px_rgba(0,0,0,0.32)]',
|
||||
contentClassName
|
||||
)}
|
||||
onEscapeKeyDown={(event) => handleImeOverlayEscape(event)}
|
||||
|
||||
@@ -35,9 +35,8 @@ function DialogOverlay({
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
// Deepen and blur the scrim so it remains visible over the near-black dark canvas.
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/55 backdrop-blur-[2px] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||
'fixed inset-0 z-50 bg-black/55 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -115,7 +114,7 @@ function DialogContent({
|
||||
data-size={size}
|
||||
// Match dropdown surface/border/shadow tokens for dark-mode contrast; minmax(0,1fr) prevents long tokens from widening the grid and pushing footers outside.
|
||||
className={cn(
|
||||
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] grid-cols-[minmax(0,1fr)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-black/14 bg-background/96 p-6 text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-2xl duration-200 outline-none dark:border-white/14 dark:bg-[rgba(23,23,23,0.96)] dark:shadow-[0_24px_72px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.06)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg data-[size=compact]:sm:max-w-sm',
|
||||
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] grid-cols-[minmax(0,1fr)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-black/14 bg-background/96 p-6 text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.08)] duration-200 outline-none dark:border-white/14 dark:bg-[rgba(23,23,23,0.96)] dark:shadow-[0_24px_72px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.06)] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg data-[size=compact]:sm:max-w-sm',
|
||||
dialogContentSurfaceVariants({ surface }),
|
||||
scrollable && 'max-h-[calc(100vh-3rem)] overflow-y-auto scrollbar-sleek',
|
||||
className
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineFixture } from '../../component-fixtures/component-fixture'
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './sheet'
|
||||
|
||||
export const modal = defineFixture({
|
||||
width: 1100,
|
||||
render: () => (
|
||||
<>
|
||||
<div className="app-layout bg-background p-6 text-foreground">
|
||||
<p>Workspace content stays live behind the sheet.</p>
|
||||
</div>
|
||||
<Sheet defaultOpen>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet preview</SheetTitle>
|
||||
<SheetDescription>
|
||||
The sheet text stays sharp above the blurred workspace.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -29,9 +29,8 @@ function SheetOverlay({
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
// Match DialogOverlay’s scrim/blur so it remains visible over the dark canvas.
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/55 backdrop-blur-[2px] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||
'fixed inset-0 z-50 bg-black/55 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
// Disable native dragging: the titlebar hit-test ignores z-index and would drag the window instead of activating sheet controls.
|
||||
@@ -43,7 +42,7 @@ function SheetOverlay({
|
||||
|
||||
const sheetContentVariants = cva(
|
||||
// Match dropdown/dialog surface/border/shadow tokens for a visible edge in either theme.
|
||||
'fixed z-50 flex flex-col gap-0 bg-background/96 text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-2xl outline-none transition ease-in-out dark:bg-[rgba(23,23,23,0.96)] dark:shadow-[0_24px_72px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.06)] data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:animate-in data-[state=open]:duration-300',
|
||||
'fixed z-50 flex flex-col gap-0 bg-background/96 text-foreground shadow-[0_20px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.08)] outline-none transition ease-in-out dark:bg-[rgba(23,23,23,0.96)] dark:shadow-[0_24px_72px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.06)] data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:animate-in data-[state=open]:duration-300',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
|
||||
@@ -40,8 +40,8 @@ export function WorktreeJumpPaletteSurface({
|
||||
'auto.components.WorktreeJumpPalette.2770f02910',
|
||||
'Search chats, terminals, workspaces, tasks, settings, and actions'
|
||||
)}
|
||||
overlayClassName="bg-black/55 backdrop-blur-[2px]"
|
||||
contentClassName="top-[min(10%,4rem)] w-[900px] max-w-[96vw] max-h-[min(90vh,calc(100vh-1.5rem))] overflow-hidden rounded-xl border border-border/70 bg-background/96 shadow-[0_26px_84px_rgba(0,0,0,0.32)] backdrop-blur-xl"
|
||||
surface="palette"
|
||||
contentClassName="top-[min(10%,4rem)] w-[900px] max-w-[96vw] max-h-[min(90vh,calc(100vh-1.5rem))] overflow-hidden"
|
||||
commandProps={{
|
||||
loop: true,
|
||||
value: controller.commandSelectedItemId,
|
||||
|
||||
@@ -79,7 +79,9 @@ function SpaceWindowRoot(): React.JSX.Element {
|
||||
>
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<ConfirmationDialogProvider>
|
||||
<WorkspaceSpacePage />
|
||||
<div className="app-layout">
|
||||
<WorkspaceSpacePage />
|
||||
</div>
|
||||
{activeModal === 'delete-worktree' ? <DeleteWorktreeDialog /> : null}
|
||||
{activeModal === 'preserved-branch-review' ? <PreservedBranchBatchReviewModal /> : null}
|
||||
</ConfirmationDialogProvider>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
import { openSidebarProjectDialog } from './helpers/sidebar-project-dialog'
|
||||
|
||||
test.use({ seedTestRepo: false, orcaAppExtraEnv: { ORCA_BACKGROUND_LAUNCH: '1' } })
|
||||
|
||||
for (const theme of ['dark', 'light'] as const) {
|
||||
test(`modal surfaces blur the live scene and stay sharp (${theme})`, async ({
|
||||
electronApp,
|
||||
orcaPage
|
||||
}, testInfo) => {
|
||||
const windows = await electronApp.evaluate(({ BrowserWindow }) =>
|
||||
BrowserWindow.getAllWindows().map((window) => ({
|
||||
visible: window.isVisible(),
|
||||
focused: window.isFocused()
|
||||
}))
|
||||
)
|
||||
expect(windows.every((window) => !window.visible && !window.focused)).toBe(true)
|
||||
await orcaPage.evaluate(async (theme) => {
|
||||
await window.__store?.getState().updateSettings({ theme, uiLanguage: 'en' })
|
||||
}, theme)
|
||||
|
||||
const scene = orcaPage.locator('.app-layout > :first-child')
|
||||
await expect(scene).toHaveCSS('filter', 'none')
|
||||
await openSidebarProjectDialog(orcaPage)
|
||||
await expect(scene).toHaveCSS('filter', 'blur(2px)')
|
||||
await expect(orcaPage.locator('.app-layout')).toHaveCSS('filter', 'none')
|
||||
const dialog = orcaPage.getByRole('dialog', { name: /Add a project/i })
|
||||
await expect(dialog).toHaveCSS('filter', 'none')
|
||||
await expect(dialog).toHaveCSS('backdrop-filter', 'none')
|
||||
await expect(orcaPage.locator('[data-slot="dialog-overlay"]')).toHaveCSS(
|
||||
'backdrop-filter',
|
||||
'none'
|
||||
)
|
||||
await orcaPage.screenshot({ path: testInfo.outputPath(`dialog-blur-${theme}.png`) })
|
||||
await orcaPage.keyboard.press('Escape')
|
||||
await expect(scene).toHaveCSS('filter', 'none')
|
||||
|
||||
await orcaPage.getByRole('button', { name: 'Search worktrees and browser tabs' }).click()
|
||||
const palette = orcaPage.locator('[data-slot="command-dialog-content"]')
|
||||
await expect(palette).toBeVisible()
|
||||
await expect(scene).toHaveCSS('filter', 'blur(2px)')
|
||||
await expect(palette).toHaveCSS('filter', 'none')
|
||||
await expect(palette).toHaveCSS('backdrop-filter', 'none')
|
||||
await orcaPage.keyboard.press('Escape')
|
||||
await expect(scene).toHaveCSS('filter', 'none')
|
||||
})
|
||||
}
|
||||
|
||||
test('Space window dialogs blur their scene and release it on close', async ({
|
||||
electronApp,
|
||||
orcaPage
|
||||
}, testInfo) => {
|
||||
const spaceWindow = electronApp.waitForEvent('window')
|
||||
await orcaPage.evaluate(async () => {
|
||||
await window.api.workspaceSpace.openWindow?.()
|
||||
})
|
||||
const spacePage = await spaceWindow
|
||||
await spacePage.waitForFunction(() => Boolean(window.__store))
|
||||
const scene = spacePage.locator('.app-layout > :first-child')
|
||||
await expect(scene).toHaveCSS('filter', 'none')
|
||||
await spacePage.evaluate(() => {
|
||||
window.__store?.getState().openModal('preserved-branch-review', {
|
||||
branches: [{ worktreeId: 'blur-preview', branchName: 'preview' }]
|
||||
})
|
||||
})
|
||||
const dialog = spacePage.getByRole('dialog', { name: 'Review kept branches' })
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(scene).toHaveCSS('filter', 'blur(2px)')
|
||||
await expect(dialog).toHaveCSS('filter', 'none')
|
||||
await expect(dialog).toHaveCSS('backdrop-filter', 'none')
|
||||
const windows = await electronApp.evaluate(({ BrowserWindow }) =>
|
||||
BrowserWindow.getAllWindows().map((window) => ({
|
||||
visible: window.isVisible(),
|
||||
focused: window.isFocused()
|
||||
}))
|
||||
)
|
||||
expect(windows.every((window) => !window.visible && !window.focused)).toBe(true)
|
||||
await spacePage.screenshot({ path: testInfo.outputPath('space-dialog-blur.png') })
|
||||
await spacePage.keyboard.press('Escape')
|
||||
await expect(scene).toHaveCSS('filter', 'none')
|
||||
})
|
||||
|
||||
test.describe('onboarding scene blur', () => {
|
||||
test.use({ dismissOnboarding: false })
|
||||
|
||||
test('onboarding stays sharp until a nested confirmation opens', async ({ orcaPage }) => {
|
||||
const scene = orcaPage.locator('.app-layout > :first-child')
|
||||
const onboarding = orcaPage.locator('[data-onboarding-overlay]')
|
||||
await expect(onboarding).toBeVisible()
|
||||
await expect(scene).toHaveCSS('filter', 'blur(2px)')
|
||||
await expect(onboarding).toHaveCSS('filter', 'none')
|
||||
await expect(onboarding).toHaveCSS('backdrop-filter', 'none')
|
||||
await onboarding.getByRole('button').first().focus()
|
||||
await orcaPage.keyboard.press('Escape')
|
||||
const confirmation = orcaPage.getByRole('dialog', { name: /Skip onboarding\?/i })
|
||||
await expect(confirmation).toBeVisible()
|
||||
await expect(confirmation).toHaveCSS('filter', 'none')
|
||||
await expect(onboarding).toHaveCSS('filter', 'blur(2px)')
|
||||
await confirmation.getByRole('button', { name: /No, keep going/i }).click()
|
||||
await expect(onboarding).toHaveCSS('filter', 'none')
|
||||
await expect(scene).toHaveCSS('filter', 'blur(2px)')
|
||||
})
|
||||
|
||||
test('onboarding render recovery stays within the viewport', async ({ orcaPage }, testInfo) => {
|
||||
await expect(orcaPage.locator('[data-onboarding-overlay]')).toBeVisible()
|
||||
await orcaPage.evaluate(async () => {
|
||||
const onboarding = await window.api.onboarding.get()
|
||||
Object.defineProperty(onboarding, 'lastCompletedStep', {
|
||||
get() {
|
||||
throw new Error('Injected onboarding render failure')
|
||||
}
|
||||
})
|
||||
window.dispatchEvent(new CustomEvent('orca:onboarding-reopened', { detail: onboarding }))
|
||||
})
|
||||
|
||||
const crashReport = orcaPage.getByRole('dialog').filter({
|
||||
has: orcaPage.getByRole('heading', { name: 'Orca hit a recoverable UI error' })
|
||||
})
|
||||
await crashReport.getByRole('button', { name: 'Close', exact: true }).click()
|
||||
const recovery = orcaPage.getByRole('alert').filter({ hasText: 'Onboarding hit an error.' })
|
||||
const retry = recovery.getByRole('button', { name: 'Retry', exact: true })
|
||||
await expect(retry).toBeInViewport()
|
||||
await orcaPage.screenshot({ path: testInfo.outputPath('onboarding-render-recovery.png') })
|
||||
|
||||
await orcaPage.evaluate(async () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('orca:onboarding-reopened', { detail: await window.api.onboarding.get() })
|
||||
)
|
||||
})
|
||||
await retry.click()
|
||||
await expect(recovery).toHaveCount(0)
|
||||
await expect(orcaPage.locator('[data-onboarding-overlay]')).toBeVisible()
|
||||
await expect(orcaPage.locator('.app-layout > :first-child')).toHaveCSS('filter', 'blur(2px)')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user