Skip to content

Commit c62e455

Browse files
committed
fix(extension): keep workspace selection recoverable
1 parent f963c62 commit c62e455

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

‎apps/sim/app/extension/workspaces.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ import {
99
scrollFadeClass,
1010
useScrollEdges,
1111
} from '@sim/emcn'
12+
import { isApiClientError } from '@/lib/api/client/errors'
1213
import ExtensionLoading from '@/app/extension/loading'
14+
import { ExtensionSignIn } from '@/app/extension/sign-in'
1315
import { useWorkspacesQuery } from '@/hooks/queries/workspace'
1416

1517
export function ExtensionWorkspaces() {
1618
const [scrollElement, setScrollElement] = useState<HTMLElement | null>(null)
1719
const scrollEdges = useScrollEdges(scrollElement)
1820
const { data: workspaces, isPending, error, refetch } = useWorkspacesQuery()
21+
if (isApiClientError(error) && error.status === 401) return <ExtensionSignIn />
1922
if (isPending) return <ExtensionLoading />
23+
const transientError =
24+
!isApiClientError(error) || error.status === 408 || error.status === 429 || error.status >= 500
2025
return (
2126
<main
2227
ref={setScrollElement}
@@ -28,7 +33,7 @@ export function ExtensionWorkspaces() {
2833
>
2934
<div className='flex flex-col gap-4'>
3035
<h1 className='text-lg'>Choose a workspace</h1>
31-
{error ? (
36+
{error && (!workspaces || !transientError) ? (
3237
<Chip onClick={() => void refetch()}>Retry loading workspaces</Chip>
3338
) : workspaces?.length ? (
3439
<nav className='flex flex-col gap-2' aria-label='Workspaces'>

‎apps/sim/scripts/test-browser-extension-e2e.ts‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,92 @@ try {
13281328
window.dispatchEvent(new Event('online'))
13291329
})
13301330
}
1331+
const workspaceListUrl = (url: URL) =>
1332+
url.origin === base.origin && url.pathname === '/api/workspaces'
1333+
for (const [status, name] of [
1334+
[503, 'A transient workspace-list refetch preserves the mounted picker and scroll position'],
1335+
[401, 'An expired workspace-list session replaces cached choices with sign-in recovery'],
1336+
[403, 'A forbidden workspace-list refetch removes cached choices'],
1337+
[404, 'A missing workspace-list refetch removes cached choices'],
1338+
[400, 'An invalid workspace-list request removes cached choices'],
1339+
] as const) {
1340+
await check(name, async () => {
1341+
await navigate(`${base.origin}/extension`)
1342+
const choices = frame().getByRole('navigation', { name: 'Workspaces', exact: true })
1343+
await expect(choices).toBeVisible()
1344+
const original = await choices.elementHandle()
1345+
assert(original)
1346+
const scroller = frame().getByRole('main')
1347+
await scroller.evaluate((element) => {
1348+
element.scrollTop = element.scrollHeight
1349+
})
1350+
const scrollTop = await scroller.evaluate((element) => element.scrollTop)
1351+
assert(scrollTop > 0, 'Workspace picker fixture did not overflow')
1352+
const failures: Request[] = []
1353+
await browserContext.route(workspaceListUrl, async (route) => {
1354+
injectedRequests.add(route.request())
1355+
await route.fulfill({ status, json: { error: 'QA workspace-list failure' } })
1356+
failures.push(route.request())
1357+
})
1358+
try {
1359+
await shell.clock.setFixedTime(Date.now() + 360_000)
1360+
await reconnect()
1361+
await expect.poll(() => failures.length).toBe(2)
1362+
await Promise.all(
1363+
failures.map(async (request) => {
1364+
const response = await request.response()
1365+
assert(response)
1366+
await response.finished()
1367+
})
1368+
)
1369+
await frame().evaluate(
1370+
() =>
1371+
new Promise<void>((resolve) =>
1372+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
1373+
)
1374+
)
1375+
if (status === 503) {
1376+
assert(
1377+
await original.evaluate((element) => element.isConnected),
1378+
'A transient workspace-list refetch unmounted the picker'
1379+
)
1380+
assert.equal(await scroller.evaluate((element) => element.scrollTop), scrollTop)
1381+
await screenshot('workspace-picker-refetch-outage')
1382+
await choices.getByRole('link', { name: 'Extension QA alpha', exact: true }).click()
1383+
await expect(frame().getByRole('textbox', { name: 'Message', exact: true })).toBeVisible()
1384+
} else {
1385+
await expect(choices).toHaveCount(0)
1386+
if (status === 401)
1387+
await expect(frame().getByRole('link', { name: 'Sign in', exact: true })).toBeVisible()
1388+
else
1389+
await expect(
1390+
frame().getByRole('button', { name: 'Retry loading workspaces', exact: true })
1391+
).toBeVisible()
1392+
await screenshot(`workspace-picker-refetch-${status}`)
1393+
}
1394+
} finally {
1395+
await original.dispose()
1396+
await browserContext.unroute(workspaceListUrl)
1397+
await shell.clock.setFixedTime(Date.now())
1398+
}
1399+
})
1400+
}
1401+
await check('An initial workspace-list 401 opens sign-in recovery', async () => {
1402+
await browserContext.route(workspaceListUrl, async (route) => {
1403+
injectedRequests.add(route.request())
1404+
await route.fulfill({ status: 401, json: { error: 'QA workspace-list session expired' } })
1405+
})
1406+
try {
1407+
await navigate(`${base.origin}/extension`)
1408+
await expect(frame().getByRole('link', { name: 'Sign in', exact: true })).toBeVisible()
1409+
await expect(
1410+
frame().getByRole('navigation', { name: 'Workspaces', exact: true })
1411+
).toHaveCount(0)
1412+
await screenshot('workspace-picker-initial-session-expired')
1413+
} finally {
1414+
await browserContext.unroute(workspaceListUrl)
1415+
}
1416+
})
13311417
await check(
13321418
'Transient session and workspace refetch failures preserve the mounted composer and page draft',
13331419
async () => {

0 commit comments

Comments
 (0)