Skip to content

Commit 9f49687

Browse files
committed
test(extension): isolate reference scenarios and trace native drops
1 parent d387c6f commit 9f49687

2 files changed

Lines changed: 199 additions & 20 deletions

File tree

‎apps/sim/scripts/browser-extension-e2e/chat-file-drop.ts‎

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,102 @@ export async function dropOnChat(page: Page, target: Locator, payload: ChatDropP
2020
],
2121
dragOperationsMask: 1,
2222
}
23-
const session = await page.context().newCDPSession(page)
23+
const observed = await target.evaluateHandle((element) => {
24+
const events: {
25+
event: DragEvent
26+
receipt: {
27+
type: string
28+
trusted: boolean
29+
tag: string | null
30+
intendedTarget: boolean
31+
chatBody: boolean
32+
inert: boolean
33+
files: number
34+
types: string[]
35+
defaultPrevented: boolean
36+
dropEffect: string | null
37+
x: number
38+
y: number
39+
}
40+
}[] = []
41+
const receive = (event: DragEvent) => {
42+
const target = event.target instanceof Element ? event.target : null
43+
const receipt = {
44+
type: event.type,
45+
trusted: event.isTrusted,
46+
tag: target?.tagName ?? null,
47+
intendedTarget: target !== null && element.contains(target),
48+
chatBody: Boolean(target?.closest('main')),
49+
inert: Boolean(target?.closest('[inert]')),
50+
files: event.dataTransfer?.files.length ?? 0,
51+
types: Array.from(event.dataTransfer?.types ?? []),
52+
defaultPrevented: event.defaultPrevented,
53+
dropEffect: event.dataTransfer?.dropEffect ?? null,
54+
x: event.clientX,
55+
y: event.clientY,
56+
}
57+
events.push({ event, receipt })
58+
}
59+
for (const type of ['dragenter', 'dragover', 'drop'] as const)
60+
element.ownerDocument.addEventListener(type, receive, true)
61+
const rect = element.getBoundingClientRect()
62+
const x = rect.x + rect.width / 2
63+
const y = rect.y + rect.height / 2
64+
const hit = element.ownerDocument.elementFromPoint(x, y)
65+
return {
66+
readEvents: () =>
67+
events.map(({ event, receipt }) => ({
68+
...receipt,
69+
defaultPrevented: event.defaultPrevented,
70+
dropEffect: event.dataTransfer?.dropEffect ?? null,
71+
})),
72+
frameTarget: {
73+
x,
74+
y,
75+
width: window.innerWidth,
76+
height: window.innerHeight,
77+
tag: hit?.tagName ?? null,
78+
intendedTarget: hit !== null && element.contains(hit),
79+
inert: Boolean(hit?.closest('[inert]')),
80+
},
81+
connected: () => element.isConnected,
82+
dispose: () => {
83+
for (const type of ['dragenter', 'dragover', 'drop'] as const)
84+
element.ownerDocument.removeEventListener(type, receive, true)
85+
},
86+
}
87+
})
2488
try {
25-
for (const type of ['dragEnter', 'dragOver', 'drop'] as const) {
26-
await session.send('Input.dispatchDragEvent', {
27-
type,
28-
x: box.x + box.width / 2,
29-
y: box.y + box.height / 2,
30-
data,
31-
})
89+
const session = await page.context().newCDPSession(page)
90+
try {
91+
const { windowId, bounds } = await session.send('Browser.getWindowForTarget')
92+
for (const type of ['dragEnter', 'dragOver', 'drop'] as const) {
93+
await session.send('Input.dispatchDragEvent', {
94+
type,
95+
x: box.x + box.width / 2,
96+
y: box.y + box.height / 2,
97+
data,
98+
})
99+
}
100+
return {
101+
box,
102+
nativeWindow: { windowId, bounds },
103+
viewport: page.viewportSize(),
104+
topHitTag: await page.evaluate(
105+
({ x, y }) => document.elementFromPoint(x, y)?.tagName ?? null,
106+
{ x: box.x + box.width / 2, y: box.y + box.height / 2 }
107+
),
108+
...(await observed.evaluate((state) => ({
109+
events: state.readEvents(),
110+
frameTarget: state.frameTarget,
111+
connected: state.connected(),
112+
}))),
113+
}
114+
} finally {
115+
await session.detach()
32116
}
33117
} finally {
34-
await session.detach()
118+
await observed.evaluate((state) => state.dispose()).catch(() => {})
119+
await observed.dispose()
35120
}
36121
}

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

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,26 @@ const chatResponses: {
8585
events: { type: string | null; seq: number | null }[]
8686
error?: string
8787
}[] = []
88+
const uploadRequests: { event: string; stage: string; method: string; status?: number }[] = []
8889
const responseDiagnostics: Promise<void>[] = []
90+
const chatTurns = new Map<string, number>()
91+
const chatAttempts: {
92+
attempt: number
93+
turn: number | null
94+
check: string
95+
scenario: string
96+
phase: string
97+
scope: string
98+
sender: string
99+
}[] = []
100+
const dropReceipts: {
101+
scope: string
102+
phase: string
103+
receipt: Awaited<ReturnType<typeof dropOnChat>>
104+
}[] = []
105+
let currentCheck = ''
106+
let scenario = ''
107+
let phase = ''
89108
let failureState: { path: string; chatId: string | null; text: string }[] = []
90109
let pendingAtFailure: { path: string; method: string; resourceType: string }[] = []
91110
const uploadedPaths = new Set<string>()
@@ -145,6 +164,9 @@ async function check(
145164
run: () => Promise<void>
146165
) {
147166
if (group !== 'all' && group !== owner && !(group === 'native' && owner !== 'access')) return
167+
currentCheck = name
168+
scenario = ''
169+
phase = ''
148170
const started = performance.now()
149171
try {
150172
await run()
@@ -325,8 +347,8 @@ try {
325347
// Only the synthetic page is pregranted; native permission prompts are a separate acceptance check.
326348
manifest.host_permissions.push(`${fixtureOrigin}/*`)
327349
await writeFile(resolve(extensionPath, 'manifest.json'), JSON.stringify(manifest))
328-
const launchBrowserContext = () =>
329-
chromium.launchPersistentContext(resolve(temporary, 'profile'), {
350+
const launchBrowserContext = async () => {
351+
const opened = await chromium.launchPersistentContext(resolve(temporary, 'profile'), {
330352
channel: 'chromium',
331353
headless: process.env.EXTENSION_APP_E2E_HEADED !== '1',
332354
viewport: { width: 390, height: 844 },
@@ -336,6 +358,38 @@ try {
336358
'--use-fake-device-for-media-stream',
337359
],
338360
})
361+
opened.on('request', (request) => {
362+
const url = new URL(request.url())
363+
if (
364+
url.origin !== base.origin ||
365+
url.pathname !== '/api/mothership/chat' ||
366+
request.method() !== 'POST'
367+
)
368+
return
369+
const body = toRecord(request.postDataJSON())
370+
const id = toStringOrNull(body.userMessageId)
371+
if (id && !chatTurns.has(id)) chatTurns.set(id, chatTurns.size + 1)
372+
let sender = 'unknown'
373+
try {
374+
sender = request.frame().page() === panel ? 'current-panel' : 'other-page'
375+
} catch {}
376+
chatAttempts.push({
377+
attempt: chatAttempts.length + 1,
378+
turn: id ? (chatTurns.get(id) ?? null) : null,
379+
check: currentCheck,
380+
scenario,
381+
phase,
382+
scope:
383+
body.organizationId === organizationId
384+
? 'organization'
385+
: body.workspaceId === alphaId
386+
? 'workspace'
387+
: 'other',
388+
sender,
389+
})
390+
})
391+
return opened
392+
}
339393
context = await launchBrowserContext()
340394
let browserContext = context
341395
context.setDefaultTimeout(30_000)
@@ -366,7 +420,16 @@ try {
366420
route.fulfill({ json: { sttAvailable: true } })
367421
)
368422
page.on('pageerror', (error) => browserErrors.push(error.message))
369-
page.on('request', (request) => pendingRequests.add(request))
423+
page.on('request', (request) => {
424+
pendingRequests.add(request)
425+
const url = new URL(request.url())
426+
if (url.origin === base.origin && url.pathname.startsWith('/api/files/uploads'))
427+
uploadRequests.push({
428+
event: 'request',
429+
stage: url.pathname.endsWith('/complete') ? 'complete' : 'session',
430+
method: request.method(),
431+
})
432+
})
370433
page.on('requestfinished', (request) => pendingRequests.delete(request))
371434
page.on('requestfailed', (request) => {
372435
pendingRequests.delete(request)
@@ -377,6 +440,13 @@ try {
377440
})
378441
page.on('response', (response) => {
379442
const url = new URL(response.url())
443+
if (url.origin === base.origin && url.pathname.startsWith('/api/files/uploads'))
444+
uploadRequests.push({
445+
event: 'response',
446+
stage: url.pathname.endsWith('/complete') ? 'complete' : 'session',
447+
method: response.request().method(),
448+
status: response.status(),
449+
})
380450
if (url.origin === base.origin && url.pathname === '/api/mothership/chat') {
381451
const entry: (typeof chatResponses)[number] = {
382452
status: response.status(),
@@ -558,9 +628,12 @@ try {
558628
)
559629
return { name, png }
560630
}
561-
const attachScreenshot = async (target: Locator) => {
631+
const attachScreenshot = async (target: Locator, scope?: string) => {
562632
const { name, png } = await screenshotFile()
563-
return uploadFile(name, () => dropOnChat(shell, target, { files: [png] }))
633+
return uploadFile(name, async () => {
634+
const receipt = await dropOnChat(shell, target, { files: [png] })
635+
if (scope) dropReceipts.push({ scope, phase: 'recovery', receipt })
636+
})
564637
}
565638
const installCaptureGate = () =>
566639
shell.addInitScript((parentOrigin) => {
@@ -689,7 +762,8 @@ try {
689762
const { name, png } = await screenshotFile()
690763
const pageCount = browserContext.pages().length
691764
const previousUrl = app.url()
692-
await dropOnChat(shell, greeting, { files: [png] })
765+
const receipt = await dropOnChat(shell, greeting, { files: [png] })
766+
dropReceipts.push({ scope, phase: 'held-capture', receipt })
693767
await app.evaluate(
694768
() => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
695769
)
@@ -730,7 +804,7 @@ try {
730804
).toHaveCount(0)
731805
assert.equal(uploadAttempts, 0)
732806
await browserContext.unroute(uploadEndpoint, interceptUpload)
733-
await attachScreenshot(input)
807+
await attachScreenshot(input, scope)
734808
await screenshot(`pending-capture-drop-${scope}-recovered`)
735809
await app.getByRole('button', { name: `Remove ${name}`, exact: true }).click()
736810
assert.equal(submitted.length, 1, `${scope}: recovering drops duplicated the turn`)
@@ -1840,13 +1914,26 @@ try {
18401914
if (enabled) await expect(globe).toHaveText('Current page')
18411915
else await expect(globe).not.toHaveText('Current page')
18421916
}
1843-
try {
1917+
const openScenario = async (target: string, name: string) => {
1918+
scenario = name
1919+
phase = 'opening'
1920+
await shell.close()
1921+
panel = await openPanel()
1922+
shell = panel
18441923
await setPanelViewport(320, 844)
1924+
await shell.goto(`chrome-extension://${extensionId}/sidepanel.html`)
1925+
await observeCaptureReceipts()
1926+
await navigate(base.origin + target)
1927+
await expect(
1928+
frame().getByRole('heading', { name: 'How can I help?', exact: true })
1929+
).toBeVisible()
1930+
}
1931+
try {
18451932
for (const org of [false, true]) {
18461933
const target = org ? `/extension/o/${organizationId}` : `/extension/${alphaId}`
18471934
const input = () =>
18481935
frame().getByRole('textbox', { name: org ? 'Ask Sim' : 'Message', exact: true })
1849-
await navigate(base.origin + target)
1936+
await openScenario(target, `${org ? 'organization' : 'workspace'}-reference`)
18501937
await expect(input()).toBeVisible()
18511938
await setReference(true)
18521939
await input().fill('Summarize the current page')
@@ -1858,6 +1945,7 @@ try {
18581945
await screenshot(`reference-${org ? 'organization' : 'workspace'}-enabled-320`)
18591946
await second.bringToFront()
18601947
let count = received.length
1948+
phase = 'send'
18611949
await input().press('Enter')
18621950
await expect.poll(() => received.length).toBe(count + 1)
18631951
const pages = submittedPages(received[count])
@@ -1869,24 +1957,26 @@ try {
18691957
org ? organizationId : alphaId
18701958
)
18711959

1872-
await navigate(base.origin + target)
1960+
await openScenario(target, `${org ? 'organization' : 'workspace'}-manual`)
18731961
await expect(input()).toBeVisible()
18741962
await capture()
18751963
await second.bringToFront()
18761964
await input().fill('Compare the attached snapshot')
18771965
count = received.length
1966+
phase = 'send'
18781967
await input().press('Enter')
18791968
await expect.poll(() => received.length).toBe(count + 1)
18801969
const manual = submittedPages(received[count])
18811970
assert.equal(manual.length, 1)
18821971
assert(manual[0].text.includes('ORCHID-482') && !manual[0].text.includes('JADE-231'))
18831972

1884-
await navigate(base.origin + target)
1973+
await openScenario(target, `${org ? 'organization' : 'workspace'}-denied`)
18851974
await expect(input()).toBeVisible()
18861975
await restricted.bringToFront()
18871976
const draft = 'Keep this draft if access is unavailable'
18881977
await input().fill(draft)
18891978
count = received.length
1979+
phase = 'denied-send'
18901980
await input().press('Enter')
18911981
await expect(
18921982
shell.getByRole('button', { name: 'Request site access', exact: true })
@@ -1905,6 +1995,7 @@ try {
19051995
'Access guidance covered the composer twice'
19061996
)
19071997
await setReference(false)
1998+
phase = 'opt-out-send'
19081999
await input().press('Enter')
19092000
await expect.poll(() => received.length).toBe(count + 1)
19102001
assert.equal(submittedPages(received[count]).length, 0)
@@ -2864,6 +2955,9 @@ try {
28642955
failureState,
28652956
httpErrors,
28662957
chatResponses,
2958+
chatAttempts,
2959+
dropReceipts,
2960+
uploadRequests,
28672961
},
28682962
null,
28692963
2

0 commit comments

Comments
 (0)