mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
feat(browser): guide Google sign-in after cookie imports (STA-3811) (#13666)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { Info } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'
|
||||
|
||||
export function BrowserCookieImportDisclosure(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="flex max-w-64 items-start gap-2 whitespace-normal py-2 font-normal">
|
||||
<Info aria-hidden className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span className="min-w-0">
|
||||
<span className="block font-medium leading-4 text-foreground">
|
||||
{translate(
|
||||
'auto.components.BrowserCookieImportDisclosure.title',
|
||||
"Google logins aren't imported"
|
||||
)}
|
||||
</span>
|
||||
<span className="block leading-4 text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.BrowserCookieImportDisclosure.description',
|
||||
'Sign in to Google directly in Orca.'
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuLabel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*
|
||||
* STA-3811: imports never touch the Google cookie family, so both import menus must say so at
|
||||
* the moment of decision. Covers the browser toolbar menu and the Settings profile row.
|
||||
* STA-3811: imports never touch the Google cookie family, so every import menu must disclose it
|
||||
* at the moment of decision.
|
||||
*/
|
||||
import { act, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import en from '@/i18n/locales/en.json'
|
||||
|
||||
const DISCLOSURE = 'Google requires signing in directly - imports skip it.'
|
||||
const DISCLOSURE_TITLE = "Google logins aren't imported"
|
||||
const DISCLOSURE_DESCRIPTION = 'Sign in to Google directly in Orca.'
|
||||
|
||||
vi.mock('@/components/ui/dropdown-menu', () => dropdownMenuStubs())
|
||||
vi.mock('../ui/dropdown-menu', () => dropdownMenuStubs())
|
||||
vi.mock('@/components/ui/popover', () => popoverStubs())
|
||||
vi.mock('@/store', () => ({ useAppStore: appStoreStub() }))
|
||||
vi.mock('../../store', () => ({ useAppStore: appStoreStub() }))
|
||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
|
||||
|
||||
import { BrowserCookieImportDisclosure } from './BrowserCookieImportDisclosure'
|
||||
import { BrowserImportHintButton } from './browser-pane/BrowserImportHintButton'
|
||||
import { BrowserToolbarMenuDropdown } from './browser-pane/browser-toolbar-menu-dropdown'
|
||||
import { BrowserProfileRow } from './settings/BrowserProfileRow'
|
||||
import { BrowserUseCookieImportStep } from './settings/BrowserUseCookieImportStep'
|
||||
|
||||
const DETECTED_BROWSERS = [
|
||||
{
|
||||
@@ -29,7 +34,7 @@ const DETECTED_BROWSERS = [
|
||||
}
|
||||
]
|
||||
|
||||
describe('cookie-import Google disclosure caption', () => {
|
||||
describe('cookie-import Google disclosure footer', () => {
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
@@ -44,9 +49,10 @@ describe('cookie-import Google disclosure caption', () => {
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('is shown in the browser toolbar import menu', () => {
|
||||
act(() => {
|
||||
root.render(
|
||||
it.each([
|
||||
[
|
||||
'browser toolbar overflow',
|
||||
() => (
|
||||
<BrowserToolbarMenuDropdown
|
||||
menuOpen
|
||||
onMenuOpenChange={vi.fn()}
|
||||
@@ -63,14 +69,22 @@ describe('cookie-import Google disclosure caption', () => {
|
||||
onApplyViewportPreset={vi.fn()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
expect(container.textContent).toContain(DISCLOSURE)
|
||||
})
|
||||
|
||||
it('is shown in the Settings browser-profile import menu', () => {
|
||||
act(() => {
|
||||
root.render(
|
||||
],
|
||||
['browser toolbar hint', () => <BrowserImportHintButton profileId="default" />],
|
||||
[
|
||||
'Settings browser-use setup',
|
||||
() => (
|
||||
<BrowserUseCookieImportStep
|
||||
cookiesImported={false}
|
||||
isImportingDefault={false}
|
||||
step3Blocked={false}
|
||||
sourceLabel={null}
|
||||
/>
|
||||
)
|
||||
],
|
||||
[
|
||||
'Settings browser-profile row',
|
||||
() => (
|
||||
<BrowserProfileRow
|
||||
profile={{ id: 'default', name: 'Default', partition: 'persist:default' } as never}
|
||||
detectedBrowsers={DETECTED_BROWSERS}
|
||||
@@ -79,18 +93,29 @@ describe('cookie-import Google disclosure caption', () => {
|
||||
onSelect={vi.fn()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
]
|
||||
] satisfies [string, () => ReactNode][])('is shown in the %s menu', (_name, renderSurface) => {
|
||||
act(() => root.render(renderSurface()))
|
||||
|
||||
expect(container.textContent).toContain(DISCLOSURE)
|
||||
expect(container.textContent).toContain(DISCLOSURE_TITLE)
|
||||
expect(container.textContent).toContain(DISCLOSURE_DESCRIPTION)
|
||||
})
|
||||
|
||||
// Why: the rendered text comes from the catalog, not the translate() fallback, so a copy
|
||||
// drift in en.json alone would otherwise slip through both render assertions.
|
||||
it('reads the same copy from the catalog on both surfaces', () => {
|
||||
expect(catalogEntry('auto.components.browser.pane.BrowserToolbarMenu.c186b4d890')).toBe(
|
||||
DISCLOSURE
|
||||
it('renders the icon and separator as non-interactive footer chrome', () => {
|
||||
act(() => root.render(<BrowserCookieImportDisclosure />))
|
||||
|
||||
const label = container.querySelector('[data-testid="dropdown-menu-label"]')
|
||||
expect(label?.querySelector('svg')).not.toBeNull()
|
||||
expect(label?.previousElementSibling?.tagName).toBe('HR')
|
||||
})
|
||||
|
||||
it('reads the footer copy from the catalog', () => {
|
||||
expect(catalogEntry('auto.components.BrowserCookieImportDisclosure.title')).toBe(
|
||||
DISCLOSURE_TITLE
|
||||
)
|
||||
expect(catalogEntry('auto.components.BrowserCookieImportDisclosure.description')).toBe(
|
||||
DISCLOSURE_DESCRIPTION
|
||||
)
|
||||
expect(catalogEntry('auto.components.settings.BrowserProfileRow.654a0c2073')).toBe(DISCLOSURE)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -113,11 +138,13 @@ function dropdownMenuStubs(): Record<string, unknown> {
|
||||
DropdownMenu: passthrough,
|
||||
DropdownMenuContent: block,
|
||||
DropdownMenuItem: block,
|
||||
DropdownMenuLabel: block,
|
||||
DropdownMenuLabel: ({ children }: { children?: ReactNode }): ReactNode => (
|
||||
<div data-testid="dropdown-menu-label">{children}</div>
|
||||
),
|
||||
DropdownMenuPortal: passthrough,
|
||||
DropdownMenuRadioGroup: passthrough,
|
||||
DropdownMenuRadioItem: block,
|
||||
DropdownMenuSeparator: () => null,
|
||||
DropdownMenuSeparator: () => <hr />,
|
||||
DropdownMenuSub: passthrough,
|
||||
DropdownMenuSubContent: block,
|
||||
DropdownMenuSubTrigger: block,
|
||||
@@ -125,11 +152,33 @@ function dropdownMenuStubs(): Record<string, unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
function popoverStubs(): Record<string, unknown> {
|
||||
const passthrough = ({ children }: { children?: ReactNode }): ReactNode => children
|
||||
const block = ({ children }: { children?: ReactNode }): ReactNode => <div>{children}</div>
|
||||
return { Popover: passthrough, PopoverContent: block, PopoverTrigger: passthrough }
|
||||
}
|
||||
|
||||
function appStoreStub(): unknown {
|
||||
const state = {
|
||||
browserImportHintHidden: false,
|
||||
browserSessionImportState: null,
|
||||
detectedBrowsers: [
|
||||
{
|
||||
family: 'chrome',
|
||||
label: 'Google Chrome',
|
||||
profiles: [{ name: 'Default', directory: 'Default' }],
|
||||
selectedProfile: 'Default'
|
||||
}
|
||||
],
|
||||
detectedBrowsersLoaded: true,
|
||||
fetchDetectedBrowsers: vi.fn(),
|
||||
importCookiesFromBrowser: vi.fn(),
|
||||
importCookiesToProfile: vi.fn(),
|
||||
openSettingsTarget: vi.fn(),
|
||||
openSettingsPage: vi.fn()
|
||||
openSettingsPage: vi.fn(),
|
||||
persistedUIReady: true,
|
||||
setBrowserImportHintHidden: vi.fn(),
|
||||
settingsSearchQuery: ''
|
||||
}
|
||||
const useAppStore = (selector?: (s: typeof state) => unknown): unknown =>
|
||||
selector ? selector(state) : state
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Import } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { emitBrowserCookieImportToast } from '@/lib/browser-cookie-import-toast'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { BrowserCookieImportDisclosure } from '@/components/BrowserCookieImportDisclosure'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -100,7 +101,8 @@ export function BrowserImportHintButton({
|
||||
value1: browser?.label ?? browserFamily,
|
||||
value2: browserProfile ? ` (${browserProfile})` : ''
|
||||
}
|
||||
)
|
||||
),
|
||||
result.profileId
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -120,7 +122,8 @@ export function BrowserImportHintButton({
|
||||
'auto.components.browser.pane.BrowserImportHintButton.d40d584769',
|
||||
'Imported {{value0}} cookies from file.',
|
||||
{ value0: result.summary.importedCookies }
|
||||
)
|
||||
),
|
||||
result.profileId
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -257,6 +260,7 @@ export function BrowserImportHintButton({
|
||||
'From File…'
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<BrowserCookieImportDisclosure />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
|
||||
@@ -205,7 +205,8 @@ export function BrowserToolbarMenu({
|
||||
value0: result.summary.importedCookies,
|
||||
value1: browser?.label ?? browserFamily
|
||||
}
|
||||
)
|
||||
),
|
||||
result.profileId
|
||||
)
|
||||
} else {
|
||||
toast.error(result.reason)
|
||||
@@ -221,7 +222,8 @@ export function BrowserToolbarMenu({
|
||||
'auto.components.browser.pane.BrowserToolbarMenu.53bbe3dab4',
|
||||
'Imported {{value0}} cookies from file.',
|
||||
{ value0: result.summary.importedCookies }
|
||||
)
|
||||
),
|
||||
result.profileId
|
||||
)
|
||||
} else if (result.reason !== 'canceled') {
|
||||
toast.error(result.reason)
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { BrowserCookieImportDisclosure } from '@/components/BrowserCookieImportDisclosure'
|
||||
import { useAppStore } from '@/store'
|
||||
import { BROWSER_FAMILY_LABELS } from '../../../../shared/constants'
|
||||
import type { BrowserSessionProfile, BrowserViewportPresetId } from '../../../../shared/types'
|
||||
@@ -128,12 +128,6 @@ export function BrowserToolbarMenuDropdown({
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
{translate(
|
||||
'auto.components.browser.pane.BrowserToolbarMenu.c186b4d890',
|
||||
'Google requires signing in directly - imports skip it.'
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
{detectedBrowsers.map((browser) =>
|
||||
browser.profiles.length > 1 ? (
|
||||
<DropdownMenuSub key={browser.family}>
|
||||
@@ -177,6 +171,7 @@ export function BrowserToolbarMenuDropdown({
|
||||
'From File…'
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<BrowserCookieImportDisclosure />
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger
|
||||
} from '../ui/dropdown-menu'
|
||||
import { BrowserCookieImportDisclosure } from '../BrowserCookieImportDisclosure'
|
||||
import { useAppStore } from '../../store'
|
||||
import { BROWSER_FAMILY_LABELS } from '../../../../shared/constants'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
@@ -81,7 +81,8 @@ export function BrowserProfileRow({
|
||||
value1: browser?.label ?? browserFamily,
|
||||
value2: profile.label
|
||||
}
|
||||
)
|
||||
),
|
||||
result.profileId
|
||||
)
|
||||
} else {
|
||||
toast.error(result.reason)
|
||||
@@ -97,7 +98,8 @@ export function BrowserProfileRow({
|
||||
'auto.components.settings.BrowserProfileRow.b4c167764d',
|
||||
'Imported {{value0}} cookies from file into {{value1}}.',
|
||||
{ value0: result.summary.importedCookies, value1: profile.label }
|
||||
)
|
||||
),
|
||||
result.profileId
|
||||
)
|
||||
} else if (result.reason !== 'canceled') {
|
||||
toast.error(result.reason)
|
||||
@@ -172,12 +174,6 @@ export function BrowserProfileRow({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
{translate(
|
||||
'auto.components.settings.BrowserProfileRow.654a0c2073',
|
||||
'Google requires signing in directly - imports skip it.'
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
{detectedBrowsers.map((browser) =>
|
||||
browser.profiles.length > 1 ? (
|
||||
<DropdownMenuSub key={browser.family}>
|
||||
@@ -220,6 +216,7 @@ export function BrowserProfileRow({
|
||||
<DropdownMenuItem onSelect={() => void handleImportFromFile()}>
|
||||
{translate('auto.components.settings.BrowserProfileRow.ebb78dfd6f', 'From File…')}
|
||||
</DropdownMenuItem>
|
||||
<BrowserCookieImportDisclosure />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{isDefault ? (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { toast } from 'sonner'
|
||||
import { emitBrowserCookieImportToast } from '@/lib/browser-cookie-import-toast'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '../ui/button'
|
||||
import { BrowserCookieImportDisclosure } from '../BrowserCookieImportDisclosure'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -58,7 +59,8 @@ export function BrowserUseCookieImportStep({
|
||||
value1: browser?.label ?? browserFamily,
|
||||
value2: browserProfile ? ` (${browserProfile})` : ''
|
||||
}
|
||||
)
|
||||
),
|
||||
result.profileId
|
||||
)
|
||||
} else {
|
||||
toast.error(result.reason)
|
||||
@@ -74,7 +76,8 @@ export function BrowserUseCookieImportStep({
|
||||
'auto.components.settings.BrowserUsePane.8f2675c2f3',
|
||||
'Imported {{value0}} cookies from file.',
|
||||
{ value0: result.summary.importedCookies }
|
||||
)
|
||||
),
|
||||
result.profileId
|
||||
)
|
||||
} else if (result.reason !== 'canceled') {
|
||||
toast.error(result.reason)
|
||||
@@ -204,6 +207,7 @@ export function BrowserUseCookieImportStep({
|
||||
<DropdownMenuItem onSelect={() => void handleImportFromFile()}>
|
||||
{translate('auto.components.settings.BrowserUsePane.be6df68384', 'From File…')}
|
||||
</DropdownMenuItem>
|
||||
<BrowserCookieImportDisclosure />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -757,7 +757,9 @@
|
||||
"toast": {
|
||||
"restartFallbackUnavailableNone": "None of the {{value0}} cookies could be loaded, and the restart fallback was unavailable. The previous cookies for this profile were replaced. Try the import again.",
|
||||
"restartFallbackUnavailablePartial": "Imported {{value0}} of {{value1}} cookies. The rest could not be loaded, and the restart fallback was unavailable. Try the import again.",
|
||||
"googleCookiesSkipped": "Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google."
|
||||
"googleCookiesSkipped": "Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.",
|
||||
"googleDirectSignInAction": "Sign in to Google",
|
||||
"googleDirectSignInUnavailable": "Could not open the browser profile. Open it and sign in at accounts.google.com."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -926,6 +928,10 @@
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"BrowserCookieImportDisclosure": {
|
||||
"title": "Google logins aren't imported",
|
||||
"description": "Sign in to Google directly in Orca."
|
||||
},
|
||||
"CodexRestartChip": {
|
||||
"a4c8e1b2f7": "Codex is still signed in as {{value0}}",
|
||||
"c72a5fb234": "Restart",
|
||||
@@ -5858,8 +5864,7 @@
|
||||
"a3f8c2d1e0b4": "Imported {{value0}} cookies from {{value1}} ({{value2}}) into {{value3}}.",
|
||||
"b4e9d3f2a1c5": "Imported {{value0}} cookies from {{value1}} into {{value2}}.",
|
||||
"c5a273a809": "From {{value0}}",
|
||||
"b5c0479e21": "Unmodified user agent",
|
||||
"654a0c2073": "Google requires signing in directly - imports skip it."
|
||||
"b5c0479e21": "Unmodified user agent"
|
||||
},
|
||||
"BrowserUseComputerUseNotice": {
|
||||
"15b5e680ba": "Open Computer Use",
|
||||
@@ -14214,8 +14219,7 @@
|
||||
"bf648471c5": "Creating…",
|
||||
"53bbe3dab4": "Imported {{value0}} cookies from file.",
|
||||
"c5f0e4d3b2a1": "Imported {{value0}} cookies from {{value1}} ({{value2}}).",
|
||||
"d6a1f5e4c3b2": "Imported {{value0}} cookies from {{value1}}.",
|
||||
"c186b4d890": "Google requires signing in directly - imports skip it."
|
||||
"d6a1f5e4c3b2": "Imported {{value0}} cookies from {{value1}}."
|
||||
},
|
||||
"GrabConfirmationSheet": {
|
||||
"314a0aaa5b": "Attach to AI",
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { successToastMock, warningToastMock } = vi.hoisted(() => ({
|
||||
const { successToastMock, warningToastMock, errorToastMock, getStateMock } = vi.hoisted(() => ({
|
||||
successToastMock: vi.fn(),
|
||||
warningToastMock: vi.fn()
|
||||
warningToastMock: vi.fn(),
|
||||
errorToastMock: vi.fn(),
|
||||
getStateMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { success: successToastMock, warning: warningToastMock }
|
||||
toast: { success: successToastMock, warning: warningToastMock, error: errorToastMock }
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: { getState: getStateMock }
|
||||
}))
|
||||
|
||||
import type { BrowserCookieImportSummary } from '../../../shared/types'
|
||||
@@ -23,6 +29,13 @@ describe('emitBrowserCookieImportToast', () => {
|
||||
beforeEach(() => {
|
||||
successToastMock.mockReset()
|
||||
warningToastMock.mockReset()
|
||||
errorToastMock.mockReset()
|
||||
getStateMock.mockReset()
|
||||
getStateMock.mockReturnValue({
|
||||
activeWorktreeId: 'worktree-1',
|
||||
closeSettingsPage: vi.fn(),
|
||||
openBrowserProfileTabInActiveWorkspace: vi.fn().mockResolvedValue(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the localized total-failure warning', () => {
|
||||
@@ -35,7 +48,8 @@ describe('emitBrowserCookieImportToast', () => {
|
||||
failedCookies: 3
|
||||
}
|
||||
},
|
||||
'Imported 3 cookies.'
|
||||
'Imported 3 cookies.',
|
||||
'profile-1'
|
||||
)
|
||||
|
||||
expect(warningToastMock).toHaveBeenCalledWith(
|
||||
@@ -54,7 +68,8 @@ describe('emitBrowserCookieImportToast', () => {
|
||||
failedCookies: 1
|
||||
}
|
||||
},
|
||||
'Imported 3 cookies.'
|
||||
'Imported 3 cookies.',
|
||||
'profile-1'
|
||||
)
|
||||
|
||||
expect(warningToastMock).toHaveBeenCalledWith(
|
||||
@@ -64,7 +79,7 @@ describe('emitBrowserCookieImportToast', () => {
|
||||
})
|
||||
|
||||
it('shows success when the import has no warning', () => {
|
||||
emitBrowserCookieImportToast(summary, 'Imported 3 cookies.')
|
||||
emitBrowserCookieImportToast(summary, 'Imported 3 cookies.', 'profile-1')
|
||||
|
||||
expect(successToastMock).toHaveBeenCalledWith('Imported 3 cookies.')
|
||||
expect(warningToastMock).not.toHaveBeenCalled()
|
||||
@@ -73,13 +88,15 @@ describe('emitBrowserCookieImportToast', () => {
|
||||
it('shows a separate Google sign-in warning after the concise success toast', () => {
|
||||
emitBrowserCookieImportToast(
|
||||
{ ...summary, importedCookies: 2, skippedCookies: 1, googleCookiesSkipped: 1 },
|
||||
'Imported 2 cookies.'
|
||||
'Imported 2 cookies.',
|
||||
'profile-1'
|
||||
)
|
||||
|
||||
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies.')
|
||||
expect(warningToastMock).toHaveBeenCalledWith(
|
||||
expect(warningToastMock.mock.calls[0][0]).toBe(
|
||||
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
|
||||
)
|
||||
expect(warningToastMock.mock.calls[0][1].action.label).toBe('Sign in to Google')
|
||||
expect(successToastMock.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
warningToastMock.mock.invocationCallOrder[0]
|
||||
)
|
||||
@@ -88,7 +105,8 @@ describe('emitBrowserCookieImportToast', () => {
|
||||
it('does not infer a Google warning from generic skipped cookies', () => {
|
||||
emitBrowserCookieImportToast(
|
||||
{ ...summary, importedCookies: 2, skippedCookies: 1 },
|
||||
'Imported 2 cookies.'
|
||||
'Imported 2 cookies.',
|
||||
'profile-1'
|
||||
)
|
||||
|
||||
expect(successToastMock).toHaveBeenCalledWith('Imported 2 cookies.')
|
||||
@@ -108,7 +126,8 @@ describe('emitBrowserCookieImportToast', () => {
|
||||
failedCookies: 1
|
||||
}
|
||||
},
|
||||
'Imported 1 cookie.'
|
||||
'Imported 1 cookie.',
|
||||
'profile-1'
|
||||
)
|
||||
|
||||
expect(successToastMock).not.toHaveBeenCalled()
|
||||
@@ -117,4 +136,81 @@ describe('emitBrowserCookieImportToast', () => {
|
||||
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
|
||||
])
|
||||
})
|
||||
|
||||
it('opens Google with the imported profile from the warning action', async () => {
|
||||
const closeSettingsPage = vi.fn()
|
||||
const openBrowserProfileTabInActiveWorkspace = vi.fn().mockResolvedValue(true)
|
||||
getStateMock.mockReturnValue({
|
||||
activeWorktreeId: 'worktree-1',
|
||||
closeSettingsPage,
|
||||
openBrowserProfileTabInActiveWorkspace
|
||||
})
|
||||
|
||||
emitBrowserCookieImportToast(
|
||||
{ ...summary, googleCookiesSkipped: 1 },
|
||||
'Imported 2 cookies.',
|
||||
'profile-1'
|
||||
)
|
||||
warningToastMock.mock.calls[0][1].action.onClick()
|
||||
|
||||
await vi.waitFor(() => expect(closeSettingsPage).toHaveBeenCalledTimes(1))
|
||||
expect(openBrowserProfileTabInActiveWorkspace).toHaveBeenCalledWith(
|
||||
'https://accounts.google.com/',
|
||||
'profile-1'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps guidance but omits the action without an active worktree', () => {
|
||||
getStateMock.mockReturnValue({ activeWorktreeId: null })
|
||||
|
||||
emitBrowserCookieImportToast(
|
||||
{ ...summary, googleCookiesSkipped: 1 },
|
||||
'Imported 2 cookies.',
|
||||
'profile-1'
|
||||
)
|
||||
|
||||
expect(warningToastMock).toHaveBeenLastCalledWith(
|
||||
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
|
||||
)
|
||||
})
|
||||
|
||||
it('reports when the profile tab cannot be opened', async () => {
|
||||
const openBrowserProfileTabInActiveWorkspace = vi.fn().mockResolvedValue(false)
|
||||
getStateMock.mockReturnValue({
|
||||
activeWorktreeId: 'worktree-1',
|
||||
closeSettingsPage: vi.fn(),
|
||||
openBrowserProfileTabInActiveWorkspace
|
||||
})
|
||||
|
||||
emitBrowserCookieImportToast(
|
||||
{ ...summary, googleCookiesSkipped: 1 },
|
||||
'Imported 2 cookies.',
|
||||
'profile-1'
|
||||
)
|
||||
warningToastMock.mock.calls[0][1].action.onClick()
|
||||
|
||||
await vi.waitFor(() => expect(errorToastMock).toHaveBeenCalledTimes(1))
|
||||
expect(errorToastMock).toHaveBeenCalledWith(
|
||||
'Could not open the browser profile. Open it and sign in at accounts.google.com.'
|
||||
)
|
||||
})
|
||||
|
||||
it('reports when opening the profile tab rejects', async () => {
|
||||
getStateMock.mockReturnValue({
|
||||
activeWorktreeId: 'worktree-1',
|
||||
closeSettingsPage: vi.fn(),
|
||||
openBrowserProfileTabInActiveWorkspace: vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('runtime unavailable'))
|
||||
})
|
||||
|
||||
emitBrowserCookieImportToast(
|
||||
{ ...summary, googleCookiesSkipped: 1 },
|
||||
'Imported 2 cookies.',
|
||||
'profile-1'
|
||||
)
|
||||
warningToastMock.mock.calls[0][1].action.onClick()
|
||||
|
||||
await vi.waitFor(() => expect(errorToastMock).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { toast } from 'sonner'
|
||||
import type { BrowserCookieImportSummary } from '../../../shared/types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
type CookieImportWarning = NonNullable<BrowserCookieImportSummary['warning']>
|
||||
|
||||
@@ -24,23 +25,63 @@ function formatCookieImportWarning(warning: CookieImportWarning): string {
|
||||
}
|
||||
}
|
||||
|
||||
function emitGoogleCookieImportWarning(summary: BrowserCookieImportSummary): void {
|
||||
const GOOGLE_SIGN_IN_URL = 'https://accounts.google.com/'
|
||||
|
||||
function googleSignInErrorMessage(): string {
|
||||
return translate(
|
||||
'auto.lib.browser.cookie.import.toast.googleDirectSignInUnavailable',
|
||||
'Could not open the browser profile. Open it and sign in at accounts.google.com.'
|
||||
)
|
||||
}
|
||||
|
||||
function emitGoogleCookieImportWarning(
|
||||
summary: BrowserCookieImportSummary,
|
||||
profileId: string
|
||||
): void {
|
||||
if (!summary.googleCookiesSkipped) {
|
||||
return
|
||||
}
|
||||
toast.warning(
|
||||
translate(
|
||||
'auto.lib.browser.cookie.import.toast.googleCookiesSkipped',
|
||||
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
|
||||
)
|
||||
const message = translate(
|
||||
'auto.lib.browser.cookie.import.toast.googleCookiesSkipped',
|
||||
'Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.'
|
||||
)
|
||||
if (!useAppStore.getState().activeWorktreeId) {
|
||||
toast.warning(message)
|
||||
return
|
||||
}
|
||||
toast.warning(message, {
|
||||
duration: 12000,
|
||||
action: {
|
||||
label: translate(
|
||||
'auto.lib.browser.cookie.import.toast.googleDirectSignInAction',
|
||||
'Sign in to Google'
|
||||
),
|
||||
onClick: () => {
|
||||
void useAppStore
|
||||
.getState()
|
||||
.openBrowserProfileTabInActiveWorkspace(GOOGLE_SIGN_IN_URL, profileId)
|
||||
.then((opened) => {
|
||||
if (!opened) {
|
||||
toast.error(googleSignInErrorMessage())
|
||||
return
|
||||
}
|
||||
// Why: Settings would cover the newly opened browser tab.
|
||||
useAppStore.getState().closeSettingsPage()
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(googleSignInErrorMessage())
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Why: a degraded import returns ok:true with a warning, so every call site must route it to a
|
||||
// warning toast instead of reporting an unqualified success (#9355).
|
||||
export function emitBrowserCookieImportToast(
|
||||
summary: BrowserCookieImportSummary,
|
||||
successMessage: string
|
||||
successMessage: string,
|
||||
profileId: string
|
||||
): void {
|
||||
const warning = summary.warning
|
||||
if (warning) {
|
||||
@@ -48,5 +89,5 @@ export function emitBrowserCookieImportToast(
|
||||
} else {
|
||||
toast.success(successMessage)
|
||||
}
|
||||
emitGoogleCookieImportWarning(summary)
|
||||
emitGoogleCookieImportWarning(summary, profileId)
|
||||
}
|
||||
|
||||
@@ -164,6 +164,21 @@ describe('createBrowserSlice annotations', () => {
|
||||
expect(store.getState().recordFeatureInteraction).toHaveBeenCalledWith('browser-tab-created')
|
||||
})
|
||||
|
||||
it('opens a local sign-in tab with the imported browser profile', async () => {
|
||||
const store = createTestStore()
|
||||
|
||||
await expect(
|
||||
store
|
||||
.getState()
|
||||
.openBrowserProfileTabInActiveWorkspace('https://accounts.google.com/', 'profile-1')
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(store.getState().browserTabsByWorktree['wt-1']?.[0]).toMatchObject({
|
||||
url: 'https://accounts.google.com/',
|
||||
sessionProfileId: 'profile-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('clears page annotations when the browser page URL changes', () => {
|
||||
const store = createTestStore()
|
||||
const tab = store.getState().createBrowserTab('wt-1', 'https://example.com')
|
||||
@@ -1028,6 +1043,38 @@ describe('createBrowserSlice runtime guard', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('opens a remote sign-in tab with the imported browser profile', async () => {
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
activeWorktreeId: 'wt-remote',
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'],
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
{
|
||||
id: 'wt-remote',
|
||||
repoId: 'repo-1',
|
||||
hostId: 'local',
|
||||
runtimeOwnerEnvironmentId: 'env-1'
|
||||
} as never
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
await expect(
|
||||
store
|
||||
.getState()
|
||||
.openBrowserProfileTabInActiveWorkspace('https://accounts.google.com/', 'profile-1')
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(createWebRuntimeSessionBrowserTabMock).toHaveBeenCalledWith({
|
||||
worktreeId: 'wt-remote',
|
||||
environmentId: 'env-1',
|
||||
url: 'https://accounts.google.com/',
|
||||
profileId: 'profile-1'
|
||||
})
|
||||
expect(store.getState().browserTabsByWorktree['wt-remote']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not create a local fallback tab when remote browser creation throws', async () => {
|
||||
const store = createTestStore()
|
||||
createWebRuntimeSessionBrowserTabMock.mockRejectedValueOnce(new Error('remote down'))
|
||||
|
||||
@@ -135,6 +135,7 @@ export type BrowserSlice = {
|
||||
options?: CreateBrowserTabOptions
|
||||
) => BrowserWorkspace
|
||||
openNewBrowserTabInActiveWorkspace: (groupId: string) => Promise<void>
|
||||
openBrowserProfileTabInActiveWorkspace: (url: string, profileId: string) => Promise<boolean>
|
||||
closeBrowserTab: (tabId: string) => void
|
||||
shutdownWorktreeBrowsers: (worktreeId: string) => Promise<void>
|
||||
reopenClosedBrowserTab: (worktreeId: string) => BrowserWorkspace | null
|
||||
@@ -706,6 +707,34 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
|
||||
})
|
||||
get().recordFeatureInteraction('browser-tab-created')
|
||||
},
|
||||
|
||||
openBrowserProfileTabInActiveWorkspace: async (url, profileId) => {
|
||||
const state = get()
|
||||
const worktreeId = state.activeWorktreeId
|
||||
if (!worktreeId) {
|
||||
return false
|
||||
}
|
||||
const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(state, worktreeId)
|
||||
if (runtimeEnvironmentId) {
|
||||
const { createWebRuntimeSessionBrowserTab } = await import('@/runtime/web-runtime-session')
|
||||
try {
|
||||
return await createWebRuntimeSessionBrowserTab({
|
||||
worktreeId,
|
||||
environmentId: runtimeEnvironmentId,
|
||||
url,
|
||||
profileId
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[browser] remote profile tab creation failed:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
get().createBrowserTab(worktreeId, url, { activate: true, sessionProfileId: profileId })
|
||||
return true
|
||||
},
|
||||
closeBrowserTab: (tabId) => {
|
||||
let remotePagesToClose: { worktreeId: string; handle: RemoteBrowserPageHandle }[] = []
|
||||
set((s) => {
|
||||
|
||||
Reference in New Issue
Block a user