Skip to content

Commit d2db2dd

Browse files
committed
chore(release): include Chat extension review fixes
2 parents fcebbb2 + 5e57170 commit d2db2dd

11 files changed

Lines changed: 143 additions & 59 deletions

File tree

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -532,14 +532,28 @@ try {
532532
}
533533
assert(snapshot.url === `${pageOrigin}/ticket`, 'Source URL contains query or fragment')
534534
})
535-
await check('Selection attaches only the selected excerpt', async () => {
535+
await check('A focused read-only custom element attaches only the selected excerpt', async () => {
536536
await page.evaluate(() => {
537537
const selected = document.getElementById('selected')
538538
if (!selected) throw new Error('Missing selection fixture')
539+
customElements.define('readable-article', class extends HTMLElement {})
540+
const host = document.createElement('readable-article')
541+
host.tabIndex = 0
542+
selected.replaceWith(host)
543+
host.append(selected)
544+
host.focus()
539545
const range = document.createRange()
540546
range.selectNodeContents(selected)
541-
window.getSelection()?.removeAllRanges()
542-
window.getSelection()?.addRange(range)
547+
const selection = window.getSelection()
548+
selection?.removeAllRanges()
549+
selection?.addRange(range)
550+
if (
551+
document.activeElement !== host ||
552+
host.isContentEditable ||
553+
host.shadowRoot ||
554+
selection?.toString() !== selected.textContent
555+
)
556+
throw new Error('Fixture did not focus and select ordinary custom-element text')
543557
})
544558
await capture('Selection')
545559
await app.waitForFunction(
@@ -551,6 +565,7 @@ try {
551565
snapshot.text === 'The customer needs a replacement by Friday.',
552566
'Selection widened beyond the user excerpt'
553567
)
568+
await page.locator('readable-article').evaluate((host) => host.replaceWith(...host.childNodes))
554569
})
555570
await check('A focused child frame cannot substitute an old parent-page selection', async () => {
556571
await page.evaluate(() => {

‎apps/extension/src/background.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ function open(tab: chrome.tabs.Tab, trigger: 'page' | 'selection' | 'keyboard')
5555
active instanceof HTMLElement &&
5656
(active.isContentEditable ||
5757
active.matches('iframe,frame,input,textarea,select') ||
58-
active.localName.includes('-') ||
5958
Boolean(chrome.dom.openOrClosedShadowRoot(active)?.activeElement))
6059
return {
6160
href: location.href,

‎apps/extension/src/extract.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export function extractPageText(
2121
active instanceof HTMLElement &&
2222
(active.isContentEditable ||
2323
active.matches('input,textarea,select') ||
24-
active.localName.includes('-') ||
2524
chrome.dom.openOrClosedShadowRoot(active)?.activeElement)
2625
)
2726
return { error: 'Select text outside an editable or embedded field.' }

‎apps/sim/app/o/[organizationId]/home/components/composer/composer.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,8 @@ export function Composer({
203203
history.reset()
204204
editor.clear()
205205
},
206-
() => editorRef.current.getValue() === raw && attachedFilesRef.current === filesAtSend
206+
() => editorRef.current.getValue() === raw && attachedFilesRef.current === filesAtSend,
207+
textareaRef.current
207208
)
208209
}
209210

‎apps/sim/app/sim-links/[organizationId]/[name]/page.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ interface OrganizationLinkPageProps {
1313

1414
/** Bookmarks stay in their exporting organization even after the browser's active team changes. */
1515
export default async function OrganizationLinkPage({ params }: OrganizationLinkPageProps) {
16-
const { organizationId, name } = await params
16+
const { organizationId, name: encodedName } = await params
17+
const name = decodeURIComponent(encodedName)
1718
if (!simLinkNameSchema.safeParse(name).success) notFound()
1819
const routes = organizationRoutes(organizationId)
1920
const session = await getSession()

‎apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,8 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
574574
clearComposer,
575575
() =>
576576
editorRef.current.getValue() === raw &&
577-
filesRef.current.attachedFiles === currentFiles.attachedFiles
577+
filesRef.current.attachedFiles === currentFiles.attachedFiles,
578+
currentEditor.textareaRef.current
578579
)
579580
}, [chatPlanRequired, workspaceId, onSubmit, clearComposer, acceptSubmission])
580581

‎apps/sim/hooks/use-composer-submission.ts‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,37 +26,48 @@ interface UseComposerSubmissionProps {
2626

2727
export function useComposerSubmission({ scope, mode }: UseComposerSubmissionProps) {
2828
const active = useRef<PendingComposerSubmission | null>(null)
29-
const focusBeforeSend = useRef<HTMLElement | null>(null)
29+
const focusAfterSend = useRef<{
30+
input: HTMLTextAreaElement
31+
initiator: Element | null
32+
} | null>(null)
3033
const [pending, setPending] = useState(false)
3134

3235
useLayoutEffect(() => {
3336
setPending(false)
3437
return () => {
3538
active.current = null
36-
focusBeforeSend.current = null
39+
focusAfterSend.current = null
3740
}
3841
}, [scope, mode])
3942

4043
useLayoutEffect(() => {
4144
if (pending) return
42-
const previous = focusBeforeSend.current
43-
focusBeforeSend.current = null
44-
if (previous?.isConnected && document.hasFocus() && document.activeElement === document.body) {
45-
previous.focus({ preventScroll: true })
45+
const focus = focusAfterSend.current
46+
focusAfterSend.current = null
47+
if (
48+
focus?.input.isConnected &&
49+
document.hasFocus() &&
50+
(document.activeElement === document.body || document.activeElement === focus.initiator)
51+
) {
52+
focus.input.focus({ preventScroll: true })
4653
}
4754
}, [pending])
4855

4956
const accept = useCallback(
50-
(submit: () => unknown, clear: () => void, unchanged: () => boolean) => {
57+
(
58+
submit: () => unknown,
59+
clear: () => void,
60+
unchanged: () => boolean,
61+
input: HTMLTextAreaElement | null
62+
) => {
5163
if (active.current) return
5264
const result = submit()
5365
if (!(result instanceof PendingComposerSubmission)) {
5466
if (result !== false) clear()
5567
return
5668
}
5769
active.current = result
58-
focusBeforeSend.current =
59-
document.activeElement instanceof HTMLElement ? document.activeElement : null
70+
focusAfterSend.current = input ? { input, initiator: document.activeElement } : null
6071
setPending(true)
6172
void result.ready
6273
.then((commit) => {

‎apps/sim/proxy.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,14 +233,20 @@ const SUSPICIOUS_UA_PATTERNS = [
233233
] as const
234234

235235
/**
236-
* Handles authentication-based redirects for root paths
236+
* Resolves legacy homepage aliases and authentication-based root redirects.
237237
*/
238238
function handleRootPathRedirects(
239239
request: NextRequest,
240240
hasActiveSession: boolean
241241
): NextResponse | null {
242242
const url = request.nextUrl
243243

244+
if (url.pathname !== '/sim' && url.pathname.toLowerCase() === '/sim') {
245+
const destination = url.clone()
246+
destination.pathname = '/'
247+
return NextResponse.redirect(destination, 308)
248+
}
249+
244250
if (url.pathname !== '/' || url.searchParams.has('home')) {
245251
return null
246252
}

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

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2023,35 +2023,52 @@ try {
20232023
assert.equal(manual.length, 1)
20242024
assert(manual[0].text.includes('ORCHID-482') && !manual[0].text.includes('JADE-231'))
20252025

2026-
await openScenario(target, `${org ? 'organization' : 'workspace'}-denied`)
2027-
await expect(input()).toBeVisible()
2028-
await restricted.bringToFront()
2029-
const draft = 'Keep this draft if access is unavailable'
2030-
await input().fill(draft)
2031-
count = received.length
2032-
phase = 'denied-send'
2033-
await input().press('Enter')
2034-
await expect(
2035-
shell.getByRole('button', { name: 'Request site access', exact: true })
2036-
).toBeEnabled()
2037-
await expect(input()).toHaveValue(draft)
2038-
assert.equal(received.length, count)
2039-
await screenshot(`reference-${org ? 'organization' : 'workspace'}-denied`)
2040-
const accessNotice = await shell
2041-
.getByRole('status')
2042-
.first()
2043-
.evaluate((status) => status.firstChild?.textContent?.trim())
2044-
assert(accessNotice)
2045-
assert.equal(
2046-
await frame().getByText(accessNotice, { exact: true }).count(),
2047-
0,
2048-
'Access guidance covered the composer twice'
2049-
)
2050-
await setReference(false)
2051-
phase = 'opt-out-send'
2052-
await input().press('Enter')
2053-
await expect.poll(() => received.length).toBe(count + 1)
2054-
assert.equal(submittedPages(received[count]).length, 0)
2026+
for (const gesture of ['keyboard', 'mouse'] as const) {
2027+
await openScenario(target, `${org ? 'organization' : 'workspace'}-denied-${gesture}`)
2028+
await expect(input()).toBeVisible()
2029+
await restricted.bringToFront()
2030+
const draft = 'Keep this draft if access is unavailable'
2031+
await input().fill(draft)
2032+
count = received.length
2033+
phase = `denied-${gesture}-send`
2034+
if (gesture === 'keyboard') await input().press('Enter')
2035+
else
2036+
await frame()
2037+
.getByRole('button', { name: org ? 'Send' : 'Send message', exact: true })
2038+
.click()
2039+
await expect(
2040+
shell.getByRole('button', { name: 'Request site access', exact: true })
2041+
).toBeEnabled()
2042+
await expect(input()).toHaveValue(draft)
2043+
await expect
2044+
.poll(() => input().evaluate((element) => Boolean(element.closest('[inert]'))))
2045+
.toBe(false)
2046+
assert.equal(received.length, count)
2047+
assert(
2048+
await frame().evaluate(() => document.hasFocus()),
2049+
`The denied ${gesture} send must keep the chat document focused`
2050+
)
2051+
await expect(input()).toBeFocused()
2052+
await shell.keyboard.type(' Continue typing after the denied capture.')
2053+
await expect(input()).toHaveValue(`${draft} Continue typing after the denied capture.`)
2054+
assert.equal(received.length, count)
2055+
await screenshot(`reference-${org ? 'organization' : 'workspace'}-denied-${gesture}`)
2056+
const accessNotice = await shell
2057+
.getByRole('status')
2058+
.first()
2059+
.evaluate((status) => status.firstChild?.textContent?.trim())
2060+
assert(accessNotice)
2061+
assert.equal(
2062+
await frame().getByText(accessNotice, { exact: true }).count(),
2063+
0,
2064+
'Access guidance covered the composer twice'
2065+
)
2066+
await setReference(false)
2067+
phase = 'opt-out-send'
2068+
await input().press('Enter')
2069+
await expect.poll(() => received.length).toBe(count + 1)
2070+
assert.equal(submittedPages(received[count]).length, 0)
2071+
}
20552072
}
20562073
} finally {
20572074
await browserContext.unroute(endpoint)

‎apps/sim/scripts/test-landing-seo-e2e.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,34 @@ async function run() {
327327
assert.deepEqual(failures, [])
328328
})
329329

330+
await check(
331+
'legacy Sim aliases preserve queries without redirecting the Sim Links entry',
332+
async () => {
333+
const entry = await get('/sim')
334+
assert.equal(entry.status, 307)
335+
assert(entry.location, 'The Sim Links entry must offer sign-in')
336+
const login = new URL(entry.location, baseUrl)
337+
assert.equal(login.pathname, '/login')
338+
assert.equal(login.searchParams.get('callbackUrl'), '/sim')
339+
for (const path of ['/Sim', '/SIM', '/sIm']) {
340+
const { status, location } = await get(`${path}?home&utm_source=legacy`)
341+
assert.equal(status, 308, path)
342+
assert(location, `${path}: expected the legacy homepage redirect`)
343+
const target = new URL(location, baseUrl)
344+
assert.equal(target.origin, baseUrl.origin, path)
345+
assert.equal(target.pathname, '/', path)
346+
assert.deepEqual(
347+
[...target.searchParams],
348+
[
349+
['home', ''],
350+
['utm_source', 'legacy'],
351+
],
352+
path
353+
)
354+
}
355+
}
356+
)
357+
330358
await check('integration sitemap dates are per integration, not one catalog date', async () => {
331359
const mismatches: string[] = []
332360
for (const integration of integrations) {

0 commit comments

Comments
 (0)