diff --git a/apps/extension/scripts/test-e2e.ts b/apps/extension/scripts/test-e2e.ts index 3d949981a69..1006ab1d6b7 100644 --- a/apps/extension/scripts/test-e2e.ts +++ b/apps/extension/scripts/test-e2e.ts @@ -5,11 +5,14 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' +import { createElement } from 'react' import { type BrowserContext, chromium, expect, type Frame, type Page } from '@playwright/test' import { pageSnapshotSchema } from '@sim/browser-extension-protocol' +import { SecretReveal } from '@sim/emcn' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' +import { renderToStaticMarkup } from 'react-dom/server' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const logger = createLogger('BrowserExtensionE2E') @@ -18,6 +21,9 @@ if (!reportPath) throw new Error('Set EXTENSION_REPORT_PATH to the output JSON r const output = resolve(root, reportPath) const checks: { name: string; status: 'passed' | 'failed'; durationMs: number; error?: string }[] = [] +const secretReveal = renderToStaticMarkup( + createElement(SecretReveal, { value: 'SECRET_REVEAL_SENTINEL' }) +) const fixture = Bun.serve({ hostname: '127.0.0.1', port: 0, @@ -55,6 +61,7 @@ const fixture = Bun.serve({
EDITABLE_SENTINEL
CSS_HIDDEN_SENTINEL
+

Credential instructions

${secretReveal}visible-code-control
`, { headers: { 'content-type': 'text/html' } } @@ -129,12 +136,17 @@ try { await signIn.close() const panel: Page = await context.newPage() // Observe dispatch before the bridge so background metadata queries stay outside each action. - await panel.addInitScript(() => { + const observeDispatch = () => { window.addEventListener('message', () => { document.documentElement.dataset.queryCountAtDispatch = document.documentElement.dataset.browserQueryCount ?? '0' }) - }) + } + const panelBundle = resolve(extensionPath, 'sidepanel.js') + await writeFile( + panelBundle, + `(${observeDispatch.toString()})();\n${await readFile(panelBundle, 'utf8')}` + ) await panel.goto(`chrome-extension://${extensionId}/sidepanel.html`) await panel.getByTitle('Sim chat').waitFor() const frame: Frame = panel.frames()[1] ?? (await panel.waitForEvent('frameattached')) @@ -231,9 +243,11 @@ try { 'A typed sim/ link opens the Sim link resolver on the configured origin', async () => { const tab = await page.context().newPage() - for (const path of ['handbook', 'jira/123?view=board']) { - await tab.goto(`http://sim/${path}`) - assert(tab.url() === `${origin}/sim/${path}`, `sim/${path} reached ${tab.url()}`) + for (const scheme of ['http', 'https']) { + for (const path of ['handbook', 'jira/123?view=board', 'jira/a%2Fb?view=board%20view']) { + await tab.goto(`${scheme}://sim/${path}`) + assert(tab.url() === `${origin}/sim/${path}`, `sim/${path} reached ${tab.url()}`) + } } await tab.close() } @@ -310,6 +324,7 @@ try { await check('Page extraction excludes secrets and strips URL query and fragment', async () => { const [snapshot] = await snapshots() assert(snapshot.text.includes('visible-reference-482'), 'Visible page content missing') + assert(snapshot.text.includes('visible-code-control'), 'Ordinary code was excluded') for (const sentinel of [ 'PASSWORD_SENTINEL', 'TEXTAREA_SENTINEL', @@ -318,6 +333,7 @@ try { 'CSS_HIDDEN_SENTINEL', 'ARIA_SENTINEL', 'SCRIPT_SENTINEL', + 'SECRET_REVEAL_SENTINEL', ]) { assert(!snapshot.text.includes(sentinel), `Captured ${sentinel}`) } @@ -668,6 +684,87 @@ try { await parentCdp.detach() } ) + await check('Ordinary Sim and encoded remote pages attach without sensitive fields', async () => { + for (const url of [ + `${origin}/workspace/example`, + `${origin}/extension-notes`, + `${origin}/workspace/%E2%9C%93`, + `${pageOrigin}/ticket/100%done`, + ]) { + const before = (await snapshots()).length + await page.goto(`${url}?token=URL_SECRET#FRAGMENT_SECRET`) + await page.bringToFront() + await capture() + await expect.poll(async () => (await snapshots()).length).toBe(before + 1) + const snapshot = (await snapshots()).at(-1) + assert(snapshot?.url === url, 'Page URL retained private parameters') + assert(snapshot.text.includes('visible-reference-482'), 'Visible page content is missing') + assert(!snapshot.text.includes('SENTINEL'), 'Sensitive page content was captured') + } + }) + await check('Embedded chat and malformed Sim paths fail closed', async () => { + for (const path of [ + '/extension', + '/extension/workspace', + '/extension/o/organization', + '/%65xtension', + '/%65xtension/workspace', + '/extension%2fworkspace', + '/extension%ZZ/workspace', + ]) { + await page.goto(`${origin}/workspace/example`) + await page.bringToFront() + const previous = (await snapshots()).length + await capture() + await expect.poll(async () => (await snapshots()).length).toBe(previous + 1) + await expect(panel.getByRole('status')).toHaveText('') + const before = (await snapshots()).length + await page.goto(`${origin}${path}`) + await page.bringToFront() + await capture() + await panel.getByRole('status').filter({ hasText: 'This page cannot be attached.' }).waitFor() + assert((await snapshots()).length === before, 'An excluded Sim path was captured') + } + }) + await check( + 'Revealed credentials stay excluded from exact and surrounding selections', + async () => { + await page.goto(`${origin}/workspace/example`) + await page.bringToFront() + const before = (await snapshots()).length + await capture() + await expect.poll(async () => (await snapshots()).length).toBe(before + 1) + await expect(panel.getByRole('status')).toHaveText('') + await page + .locator('#secret-reveal code') + .first() + .evaluate((element) => { + const range = document.createRange() + range.selectNodeContents(element) + window.getSelection()?.removeAllRanges() + window.getSelection()?.addRange(range) + }) + await capture('Selection') + await expect(panel.getByRole('status')).toContainText('could not be read') + assert((await snapshots()).length === before + 1, 'A revealed credential was attached') + await page.locator('#secret-reveal').evaluate((element) => { + const range = document.createRange() + range.selectNodeContents(element) + window.getSelection()?.removeAllRanges() + window.getSelection()?.addRange(range) + }) + await capture('Selection') + await expect.poll(async () => (await snapshots()).length).toBe(before + 2) + const snapshot = (await snapshots()).at(-1) + assert(snapshot, 'Missing surrounding selection snapshot') + assert(snapshot.text.includes('Credential instructions'), 'Public selection text is missing') + assert(snapshot.text.includes('visible-code-control'), 'Ordinary selected code was excluded') + assert( + !snapshot.text.includes('SECRET_REVEAL_SENTINEL'), + 'A surrounding selection leaked the credential' + ) + } + ) await check( 'Site access stays modal without violating the packaged security policy', async () => { diff --git a/apps/extension/src/capture.ts b/apps/extension/src/capture.ts index 9a8ff2b651c..dbd5d253f50 100644 --- a/apps/extension/src/capture.ts +++ b/apps/extension/src/capture.ts @@ -21,14 +21,23 @@ export async function captureActivePage( if (expected && (await digestUrl(tab.url)) !== expected.urlDigest) { throw new Error('The page changed before reading. Attach it again.') } + let pathname = url.pathname + if (url.origin === SIM_ORIGIN) { + try { + pathname = decodeURIComponent(pathname) + } catch { + throw new Error('This page cannot be attached.') + } + } if ( !['http:', 'https:'].includes(url.protocol) || - url.origin === SIM_ORIGIN || + (url.origin === SIM_ORIGIN && + (pathname === '/extension' || pathname.startsWith('/extension/'))) || url.username || url.password || tab.incognito ) { - throw new Error('Choose a regular web page outside Sim to attach.') + throw new Error('This page cannot be attached.') } const results = await chrome.scripting.executeScript({ target: expected diff --git a/apps/sim/app/extension/[workspaceId]/chat.tsx b/apps/sim/app/extension/[workspaceId]/chat.tsx index 8b38a977634..f1f5324a08b 100644 --- a/apps/sim/app/extension/[workspaceId]/chat.tsx +++ b/apps/sim/app/extension/[workspaceId]/chat.tsx @@ -169,12 +169,10 @@ export function ExtensionChat({ workspaceId, workspaceName }: ExtensionChatProps draftScopeKey={`extension:${userId}:${workspaceId}:${selectedChatId ?? 'new'}`} composerAttachments={composer.attachments} composerResourceActions={composer.resourceActions} + composerToolbarActions={composer.toolbarActions} composerModelSelectorOptions={composer.modelSelectorOptions} layout='copilot-view' - className={cn( - 'h-auto min-h-0', - selectedChatId || chat.messages.length > 0 ? 'flex-1' : 'shrink-0' - )} + className={cn('h-auto min-h-0', (selectedChatId || chat.messages.length > 0) && 'flex-1')} /> + connected ? ( + + + requestAction('capture-page')} + aria-label='Attach current page' + /> + + Attach current page + + ) : undefined, + [connected, requestAction] + ) return { attachments, resourceActions, + toolbarActions, modelSelectorOptions: MODEL_SELECTOR_OPTIONS, prepareSubmission, onSubmitted: clearSnapshots, diff --git a/apps/sim/app/o/[organizationId]/components/organization-conversation.tsx b/apps/sim/app/o/[organizationId]/components/organization-conversation.tsx index f9b7faa735c..f99b6155c19 100644 --- a/apps/sim/app/o/[organizationId]/components/organization-conversation.tsx +++ b/apps/sim/app/o/[organizationId]/components/organization-conversation.tsx @@ -35,6 +35,7 @@ import { organizationSearchUrlKeys, } from '@/app/o/[organizationId]/search/search-params' import { ChatResourcePanel } from '@/app/workspace/[workspaceId]/home/components/chat-resource-panel' +import { ComposerScrollArea } from '@/app/workspace/[workspaceId]/home/components/composer-scroll-area' import { useSearchHistoryActions } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-history-context' import { SearchIntegrationConnection } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection' import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components/mothership-chat' @@ -94,6 +95,7 @@ interface OrganizationConversationProps { header: ReactNode attachments?: ReactNode resourceActions?: ReactNode + toolbarActions?: ReactNode modelSelectorOptions?: ModelSelectorOptions onChatIdResolved: (chatId: string) => void onNavigateToChat: (chatId: string) => void @@ -453,6 +455,7 @@ function OrganizationConversationContent({ compact={Boolean(panelOptions)} attachments={panelOptions?.attachments} resourceActions={panelOptions?.resourceActions} + toolbarActions={panelOptions?.toolbarActions} modelSelectorOptions={panelOptions?.modelSelectorOptions} onPlanChange={ !hasChat && destinationMode === 'agent' && planEnabled @@ -535,7 +538,7 @@ function OrganizationConversationContent({

How can I help?

-
{composer}
+ {composer} ) : ( )} {!imagesOnly && contextPicker('skills', Slash, 'Skills')} + {toolbarActions} {onPlanChange && ( +} + +/** Keeps the complete composer reachable when its contents exceed the available chat height. */ +export function ComposerScrollArea({ + children, + className, + onAnimationEnd, +}: ComposerScrollAreaProps) { + const scrollRef = useRef(null) + const edges = useScrollEdges(scrollRef) + + return ( +
+ {children} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index aa5c30fd329..c43c247abfb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -35,6 +35,7 @@ import { MessageActions } from '@/app/workspace/[workspaceId]/components/message import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments' import { ChatPlanNotice } from '@/app/workspace/[workspaceId]/home/components/chat-plan-notice' import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context' +import { ComposerScrollArea } from '@/app/workspace/[workspaceId]/home/components/composer-scroll-area' import { assistantMessageHasRenderableContent, getOrchestratorMessageText, @@ -88,6 +89,7 @@ interface MothershipChatProps { composer?: ReactNode composerAttachments?: ReactNode composerResourceActions?: ReactNode + composerToolbarActions?: ReactNode composerModelSelectorOptions?: ModelSelectorOptions messages: ChatMessage[] isSending: boolean @@ -174,7 +176,7 @@ const LAYOUT_STYLES = { userBubble: 'max-w-[85%] md:max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2', assistantRow: 'group/msg', - footer: 'shrink-0 px-3 pb-[max(12px,env(safe-area-inset-bottom))] md:px-[24px] md:pb-[16px]', + footer: 'px-3 pb-[max(12px,env(safe-area-inset-bottom))] md:px-[24px] md:pb-[16px]', footerInner: 'mx-auto max-w-chat', }, 'copilot-view': { @@ -186,7 +188,7 @@ const LAYOUT_STYLES = { attachmentWidth: 'max-w-[85%]', userBubble: 'max-w-[85%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3 py-2', assistantRow: 'group/msg', - footer: 'shrink-0 px-3 pb-3', + footer: 'px-3 pb-3', footerInner: '', }, } as const @@ -393,6 +395,7 @@ export function MothershipChat({ composer, composerAttachments, composerResourceActions, + composerToolbarActions, composerModelSelectorOptions, messages: messagesProp, isSending, @@ -1028,7 +1031,7 @@ export function MothershipChat({ )} -
@@ -1052,6 +1055,7 @@ export function MothershipChat({ onSubmit={onSubmit} attachments={composerAttachments} resourceActions={composerResourceActions} + toolbarActions={composerToolbarActions} modelSelectorOptions={composerModelSelectorOptions} isSending={isStreamActive} onStopGeneration={onStopGeneration} @@ -1063,7 +1067,7 @@ export function MothershipChat({ /> ))}
- + ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx index e0f6a82e450..aa9a756bb20 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list.tsx @@ -170,7 +170,7 @@ export const AttachedFilesList = React.memo(function AttachedFilesList({
void @@ -98,6 +99,7 @@ const UserInputImpl = forwardRef(function UserI { attachments, resourceActions, + toolbarActions, modelSelectorOptions, requestMode = 'agent', onPlanChange, @@ -679,6 +681,7 @@ const UserInputImpl = forwardRef(function UserI Skills + {toolbarActions} {onPlanChange && ( { + await shell.setViewportSize({ width, height }) + const session = await browserContext.newCDPSession(shell) + try { + const { windowId } = await session.send('Browser.getWindowForTarget') + // OOPIF hit testing uses the native content bounds, including below the browser toolbar. + await session.send('Browser.setContentsSize', { windowId, width, height }) + } finally { + await session.detach() + } + } await panel.goto(`chrome-extension://${extensionId}/sidepanel.html`) const frame = () => { const app = shell.frames().find((candidate) => candidate.url().startsWith(base.origin)) @@ -446,20 +457,24 @@ try { }) }, base.origin) await observeCaptureReceipts() - const capture = async (mode: 'page' | 'selection' = 'page') => { + const capture = async (mode: 'page' | 'selection' | 'shortcut' = 'page') => { await source.bringToFront() const before = Number(await shell.locator('body').getAttribute('data-capture-receipts')) - await frame().getByRole('button', { name: 'Add resources', exact: true }).press('Enter') - await frame() - .getByRole('menuitem', { - name: mode === 'page' ? 'Current page' : 'Selected text', - exact: true, - }) - .press('Enter') + if (mode === 'shortcut') { + await frame().getByRole('button', { name: 'Attach current page', exact: true }).press('Enter') + } else { + await frame().getByRole('button', { name: 'Add resources', exact: true }).press('Enter') + await frame() + .getByRole('menuitem', { + name: mode === 'page' ? 'Current page' : 'Selected text', + exact: true, + }) + .press('Enter') + } await expect(shell.locator('body')).toHaveAttribute('data-capture-receipts', String(before + 1)) } const attachments = () => frame().getByRole('group', { name: 'Page attached to next message' }) - const attachFile = async () => { + const attachFile = async (name = 'extension-notes.txt') => { const completion = shell.waitForResponse( (response) => response.request().method() === 'POST' && @@ -469,7 +484,7 @@ try { await frame() .locator('input[type="file"]') .setInputFiles({ - name: 'extension-notes.txt', + name, mimeType: 'text/plain', buffer: Buffer.from('This synthetic file belongs only to extension QA.'), }) @@ -479,9 +494,7 @@ try { const asset = toRecord(toRecord(toRecord(body).data).result) assert.equal(typeof asset.key, 'string') trackUploadedFile(String(asset.key)) - await expect( - frame().getByRole('button', { name: 'Remove extension-notes.txt', exact: true }) - ).toBeEnabled() + await expect(frame().getByRole('button', { name: `Remove ${name}`, exact: true })).toBeEnabled() } await checkNative( 'Capture, preview, removal, and the three-page cap use the real composer', @@ -528,7 +541,7 @@ try { await navigate(`${base.origin}/extension/${alphaId}`) await expect(frame().getByRole('textbox', { name: 'Message', exact: true })).toBeVisible() for (const width of [320, 390]) { - await shell.setViewportSize({ width, height: 844 }) + await setPanelViewport(width, 844) await expect .poll(() => shell.evaluate( @@ -772,7 +785,51 @@ try { async () => { await capture() await attachFile() + await setPanelViewport(320, 844) + await capture() + await capture() + const geometry = await attachments() + .first() + .evaluate((chip) => { + const strip = chip.parentElement?.parentElement + if (!strip) throw new Error('Missing attachment strip') + return { height: strip.clientHeight, contentHeight: strip.scrollHeight } + }) + assert(geometry.contentHeight > 102, 'The fixture did not exercise multiple attachment rows') + assert( + geometry.contentHeight <= geometry.height + 1, + 'Desktop attachments require nested scrolling' + ) await screenshot('mixed-attachments') + const device = await browserContext.newCDPSession(frame()) + try { + await device.send('Emulation.setTouchEmulationEnabled', { + enabled: true, + maxTouchPoints: 1, + }) + await expect + .poll(() => frame().evaluate(() => matchMedia('(pointer: coarse)').matches)) + .toBe(true) + const touchGeometry = await attachments() + .first() + .evaluate((chip) => { + const strip = chip.parentElement?.parentElement + if (!strip) throw new Error('Missing attachment strip') + return { height: strip.clientHeight, contentHeight: strip.scrollHeight } + }) + assert(touchGeometry.height <= 102, 'Touch attachments lost their height limit') + assert( + touchGeometry.contentHeight > touchGeometry.height, + 'Touch overflow fixture did not wrap' + ) + await screenshot('touch-attachments') + } finally { + await device.send('Emulation.setTouchEmulationEnabled', { enabled: false }) + await device.detach() + } + await attachments().last().getByRole('button', { name: 'Remove page context' }).click() + await attachments().last().getByRole('button', { name: 'Remove page context' }).click() + await setPanelViewport(390, 844) await attachments().first().getByRole('button', { name: 'Remove page context' }).click() await expect( frame().getByRole('button', { name: 'Remove extension-notes.txt', exact: true }) @@ -1223,6 +1280,136 @@ try { await screenshot('organization-first-open-capture') } ) + await checkNative('The current-page shortcut attaches in both shared composers', async () => { + for (const path of [`/extension/${alphaId}`, `/extension/o/${organizationId}`]) { + await navigate(`${base.origin}${path}`) + await expect(attachments()).toHaveCount(0) + await capture('shortcut') + await expect(attachments()).toHaveCount(1) + await attachments() + .first() + .getByRole('button', { name: /^Review page context:/ }) + .click() + const preview = frame().getByRole('dialog', { name: 'Page context', exact: true }) + await expect(preview.locator('pre')).toContainText(captureText) + await preview.getByRole('button', { name: 'Close', exact: true }).click() + await frame().getByRole('button', { name: 'Attach current page', exact: true }).hover() + await expect(frame().getByRole('tooltip', { name: 'Attach current page' })).toBeVisible() + await screenshot(path.includes('/o/') ? 'shortcut-organization-agent' : 'shortcut-workspace') + await attachments().first().getByRole('button', { name: 'Remove page context' }).click() + } + }) + await checkNative( + 'Large attachment stacks keep the composer reachable without nested desktop scrolling', + async () => { + const names = Array.from( + { length: 20 }, + (_, index) => `extension-qa-long-attachment-name-${index + 1}.txt` + ) + const endpoint = `${base.origin}/api/mothership/chat` + for (const [name, path, inputLabel, sendLabel] of [ + ['workspace-new', `/extension/${alphaId}`, 'Message', 'Send message'], + ['workspace-history', `/extension/${alphaId}?chat=${historyId}`, 'Message', 'Send message'], + ['organization-new', `/extension/o/${organizationId}`, 'Ask Sim', 'Send'], + ] as const) { + await navigate(`${base.origin}${path}`) + await setPanelViewport(320, 844) + await capture('shortcut') + for (const filename of names) await attachFile(filename) + const geometry = await attachments() + .first() + .evaluate((chip) => { + const strip = chip.parentElement?.parentElement + if (!strip) throw new Error('Missing attachment strip') + return { height: strip.clientHeight, contentHeight: strip.scrollHeight } + }) + assert(geometry.contentHeight > 844, 'The fixture did not exceed the panel height') + assert( + geometry.contentHeight <= geometry.height + 1, + 'Desktop attachments gained nested scrolling' + ) + await shell.bringToFront() + await shell.mouse.move(160, 760) + await expect + .poll(() => + attachments() + .first() + .evaluate((chip) => chip.getBoundingClientRect().top) + ) + .toBeGreaterThanOrEqual(0) + await screenshot(`large-attachments-${name}-top`) + await shell.mouse.wheel(0, 2000) + const send = frame().getByRole('button', { name: sendLabel, exact: true }) + await expect + .poll( + () => + send.evaluate((button) => { + const rect = button.getBoundingClientRect() + return ( + rect.top >= 0 && + rect.bottom <= innerHeight && + button.contains( + document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2) + ) + ) + }), + { message: `${name}: scrolling must expose Send inside the panel` } + ) + .toBe(true) + const editor = frame().getByRole('textbox', { name: inputLabel, exact: true }) + assert( + await editor.evaluate((element) => { + const rect = element.getBoundingClientRect() + return ( + rect.top >= 0 && + rect.bottom <= innerHeight && + element.contains( + document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2) + ) + ) + }), + `${name}: the editor is clipped` + ) + assert( + await frame() + .getByRole('button', { name: 'New chat', exact: true }) + .evaluate((button) => { + const rect = button.getBoundingClientRect() + return rect.top >= 0 && rect.bottom <= innerHeight + }), + `${name}: scrolling hid the extension header` + ) + await screenshot(`large-attachments-${name}-bottom`) + await browserContext.route(endpoint, (route) => + route.fulfill({ status: 403, json: { error: 'QA attachment capacity admission check' } }) + ) + try { + const request = browserContext.waitForEvent('request', { + predicate: (entry) => entry.url() === endpoint && entry.method() === 'POST', + }) + await editor.fill('Use all attached context.') + await send.click() + const payload = toRecord((await request).postDataJSON()) + assert(Array.isArray(payload.fileAttachments), 'Missing file attachments') + assert.deepEqual( + payload.fileAttachments.map((file) => toRecord(file).filename).sort(), + [...names].sort() + ) + assert( + Array.isArray(payload.contexts) && payload.contexts.length === 1, + 'The attached page was lost' + ) + await expect( + frame().getByText('QA attachment capacity admission check', { exact: false }) + ).toBeVisible() + } finally { + await browserContext.unroute(endpoint) + } + } + await setPanelViewport(390, 844) + await navigate(`${base.origin}/extension/o/${organizationId}`) + } + ) if (liveChat) { await checkNative( 'Organization live admission preserves queued pages, files, and the admitted conversation mode', @@ -1379,6 +1566,9 @@ try { await screenshot('organization-mode-file-recovery') await frame().getByRole('button', { name: 'Remove extension-notes.txt', exact: true }).click() await expect(frame().getByText(/Remove unsupported files/)).not.toBeVisible() + await attachments().first().getByRole('button', { name: 'Remove page context' }).click() + await capture('shortcut') + await expect(attachments()).toHaveCount(1) await screenshot('organization-assistant-page') if (!liveChat) await shell.route('**/api/mothership/chat', (route) => diff --git a/packages/emcn/src/components/secret-reveal/secret-reveal.tsx b/packages/emcn/src/components/secret-reveal/secret-reveal.tsx index 1a87172b74d..3be9a4d6145 100644 --- a/packages/emcn/src/components/secret-reveal/secret-reveal.tsx +++ b/packages/emcn/src/components/secret-reveal/secret-reveal.tsx @@ -44,6 +44,7 @@ export function SecretReveal({ value, className, redacted = false }: SecretRevea return (