Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions apps/extension/scripts/test-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,14 +532,28 @@ try {
}
assert(snapshot.url === `${pageOrigin}/ticket`, 'Source URL contains query or fragment')
})
await check('Selection attaches only the selected excerpt', async () => {
await check('A focused read-only custom element attaches only the selected excerpt', async () => {
await page.evaluate(() => {
const selected = document.getElementById('selected')
if (!selected) throw new Error('Missing selection fixture')
customElements.define('readable-article', class extends HTMLElement {})
const host = document.createElement('readable-article')
host.tabIndex = 0
selected.replaceWith(host)
host.append(selected)
host.focus()
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
const range = document.createRange()
range.selectNodeContents(selected)
window.getSelection()?.removeAllRanges()
window.getSelection()?.addRange(range)
const selection = window.getSelection()
selection?.removeAllRanges()
selection?.addRange(range)
if (
document.activeElement !== host ||
host.isContentEditable ||
host.shadowRoot ||
selection?.toString() !== selected.textContent
)
throw new Error('Fixture did not focus and select ordinary custom-element text')
})
await capture('Selection')
await app.waitForFunction(
Expand All @@ -551,6 +565,7 @@ try {
snapshot.text === 'The customer needs a replacement by Friday.',
'Selection widened beyond the user excerpt'
)
await page.locator('readable-article').evaluate((host) => host.replaceWith(...host.childNodes))
})
await check('A focused child frame cannot substitute an old parent-page selection', async () => {
await page.evaluate(() => {
Expand Down
1 change: 0 additions & 1 deletion apps/extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ function open(tab: chrome.tabs.Tab, trigger: 'page' | 'selection' | 'keyboard')
active instanceof HTMLElement &&
(active.isContentEditable ||
active.matches('iframe,frame,input,textarea,select') ||
active.localName.includes('-') ||
Boolean(chrome.dom.openOrClosedShadowRoot(active)?.activeElement))
return {
href: location.href,
Expand Down
1 change: 0 additions & 1 deletion apps/extension/src/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export function extractPageText(
active instanceof HTMLElement &&
(active.isContentEditable ||
active.matches('input,textarea,select') ||
active.localName.includes('-') ||
chrome.dom.openOrClosedShadowRoot(active)?.activeElement)
)
return { error: 'Select text outside an editable or embedded field.' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ export function Composer({
history.reset()
editor.clear()
},
() => editorRef.current.getValue() === raw && attachedFilesRef.current === filesAtSend
() => editorRef.current.getValue() === raw && attachedFilesRef.current === filesAtSend,
textareaRef.current
)
}

Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/sim-links/[organizationId]/[name]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ interface OrganizationLinkPageProps {

/** Bookmarks stay in their exporting organization even after the browser's active team changes. */
export default async function OrganizationLinkPage({ params }: OrganizationLinkPageProps) {
const { organizationId, name } = await params
const { organizationId, name: encodedName } = await params
const name = decodeURIComponent(encodedName)
if (!simLinkNameSchema.safeParse(name).success) notFound()
const routes = organizationRoutes(organizationId)
const session = await getSession()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,8 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
clearComposer,
() =>
editorRef.current.getValue() === raw &&
filesRef.current.attachedFiles === currentFiles.attachedFiles
filesRef.current.attachedFiles === currentFiles.attachedFiles,
currentEditor.textareaRef.current
)
}, [chatPlanRequired, workspaceId, onSubmit, clearComposer, acceptSubmission])

Expand Down
29 changes: 20 additions & 9 deletions apps/sim/hooks/use-composer-submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,48 @@ interface UseComposerSubmissionProps {

export function useComposerSubmission({ scope, mode }: UseComposerSubmissionProps) {
const active = useRef<PendingComposerSubmission | null>(null)
const focusBeforeSend = useRef<HTMLElement | null>(null)
const focusAfterSend = useRef<{
input: HTMLTextAreaElement
initiator: Element | null
} | null>(null)
const [pending, setPending] = useState(false)

useLayoutEffect(() => {
setPending(false)
return () => {
active.current = null
focusBeforeSend.current = null
focusAfterSend.current = null
}
}, [scope, mode])

useLayoutEffect(() => {
if (pending) return
const previous = focusBeforeSend.current
focusBeforeSend.current = null
if (previous?.isConnected && document.hasFocus() && document.activeElement === document.body) {
previous.focus({ preventScroll: true })
const focus = focusAfterSend.current
focusAfterSend.current = null
if (
focus?.input.isConnected &&
document.hasFocus() &&
(document.activeElement === document.body || document.activeElement === focus.initiator)
) {
focus.input.focus({ preventScroll: true })
}
}, [pending])

const accept = useCallback(
(submit: () => unknown, clear: () => void, unchanged: () => boolean) => {
(
submit: () => unknown,
clear: () => void,
unchanged: () => boolean,
input: HTMLTextAreaElement | null
) => {
if (active.current) return
const result = submit()
if (!(result instanceof PendingComposerSubmission)) {
if (result !== false) clear()
return
}
active.current = result
focusBeforeSend.current =
document.activeElement instanceof HTMLElement ? document.activeElement : null
focusAfterSend.current = input ? { input, initiator: document.activeElement } : null
setPending(true)
void result.ready
.then((commit) => {
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,20 @@ const SUSPICIOUS_UA_PATTERNS = [
] as const

/**
* Handles authentication-based redirects for root paths
* Resolves legacy homepage aliases and authentication-based root redirects.
*/
function handleRootPathRedirects(
request: NextRequest,
hasActiveSession: boolean
): NextResponse | null {
const url = request.nextUrl

if (url.pathname !== '/sim' && url.pathname.toLowerCase() === '/sim') {
const destination = url.clone()
destination.pathname = '/'
return NextResponse.redirect(destination, 308)
}

if (url.pathname !== '/' || url.searchParams.has('home')) {
return null
}
Expand Down
75 changes: 46 additions & 29 deletions apps/sim/scripts/test-browser-extension-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2023,35 +2023,52 @@ try {
assert.equal(manual.length, 1)
assert(manual[0].text.includes('ORCHID-482') && !manual[0].text.includes('JADE-231'))

await openScenario(target, `${org ? 'organization' : 'workspace'}-denied`)
await expect(input()).toBeVisible()
await restricted.bringToFront()
const draft = 'Keep this draft if access is unavailable'
await input().fill(draft)
count = received.length
phase = 'denied-send'
await input().press('Enter')
await expect(
shell.getByRole('button', { name: 'Request site access', exact: true })
).toBeEnabled()
await expect(input()).toHaveValue(draft)
assert.equal(received.length, count)
await screenshot(`reference-${org ? 'organization' : 'workspace'}-denied`)
const accessNotice = await shell
.getByRole('status')
.first()
.evaluate((status) => status.firstChild?.textContent?.trim())
assert(accessNotice)
assert.equal(
await frame().getByText(accessNotice, { exact: true }).count(),
0,
'Access guidance covered the composer twice'
)
await setReference(false)
phase = 'opt-out-send'
await input().press('Enter')
await expect.poll(() => received.length).toBe(count + 1)
assert.equal(submittedPages(received[count]).length, 0)
for (const gesture of ['keyboard', 'mouse'] as const) {
await openScenario(target, `${org ? 'organization' : 'workspace'}-denied-${gesture}`)
await expect(input()).toBeVisible()
await restricted.bringToFront()
const draft = 'Keep this draft if access is unavailable'
await input().fill(draft)
count = received.length
phase = `denied-${gesture}-send`
if (gesture === 'keyboard') await input().press('Enter')
else
await frame()
.getByRole('button', { name: org ? 'Send' : 'Send message', exact: true })
.click()
await expect(
shell.getByRole('button', { name: 'Request site access', exact: true })
).toBeEnabled()
await expect(input()).toHaveValue(draft)
await expect
.poll(() => input().evaluate((element) => Boolean(element.closest('[inert]'))))
.toBe(false)
assert.equal(received.length, count)
assert(
await frame().evaluate(() => document.hasFocus()),
`The denied ${gesture} send must keep the chat document focused`
)
await expect(input()).toBeFocused()
await shell.keyboard.type(' Continue typing after the denied capture.')
await expect(input()).toHaveValue(`${draft} Continue typing after the denied capture.`)
assert.equal(received.length, count)
await screenshot(`reference-${org ? 'organization' : 'workspace'}-denied-${gesture}`)
const accessNotice = await shell
.getByRole('status')
.first()
.evaluate((status) => status.firstChild?.textContent?.trim())
assert(accessNotice)
assert.equal(
await frame().getByText(accessNotice, { exact: true }).count(),
0,
'Access guidance covered the composer twice'
)
await setReference(false)
phase = 'opt-out-send'
await input().press('Enter')
await expect.poll(() => received.length).toBe(count + 1)
assert.equal(submittedPages(received[count]).length, 0)
}
}
} finally {
await browserContext.unroute(endpoint)
Expand Down
28 changes: 28 additions & 0 deletions apps/sim/scripts/test-landing-seo-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,34 @@ async function run() {
assert.deepEqual(failures, [])
})

await check(
'legacy Sim aliases preserve queries without redirecting the Sim Links entry',
async () => {
const entry = await get('/sim')
assert.equal(entry.status, 307)
assert(entry.location, 'The Sim Links entry must offer sign-in')
const login = new URL(entry.location, baseUrl)
assert.equal(login.pathname, '/login')
assert.equal(login.searchParams.get('callbackUrl'), '/sim')
for (const path of ['/Sim', '/SIM', '/sIm']) {
const { status, location } = await get(`${path}?home&utm_source=legacy`)
assert.equal(status, 308, path)
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
assert(location, `${path}: expected the legacy homepage redirect`)
const target = new URL(location, baseUrl)
assert.equal(target.origin, baseUrl.origin, path)
assert.equal(target.pathname, '/', path)
assert.deepEqual(
[...target.searchParams],
[
['home', ''],
['utm_source', 'legacy'],
],
path
)
}
}
)

await check('integration sitemap dates are per integration, not one catalog date', async () => {
const mismatches: string[] = []
for (const integration of integrations) {
Expand Down
30 changes: 18 additions & 12 deletions apps/sim/scripts/test-sim-links-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,24 +403,30 @@ async function run() {
await api({ ...outsider, orgId }, '', { expected: 404 })
})

await check(
'a team bookmark keeps its organization across sign-in and a same-name link elsewhere',
async () => {
const name = 'managed-help'
const path = `/sim-links/${encodeURIComponent(orgId)}/${name}`
const destination = 'https://team.example.com/help'
await check('team bookmarks preserve encoded names and organization across sign-in', async () => {
const destination = 'https://team.example.com/help'
for (const name of ['managed-help', 'managed:help@team&docs']) {
const path = `/sim-links/${encodeURIComponent(orgId)}/${encodeURIComponent(name)}`
await create(owner, { name, url: destination })
await create(outsider, { name, url: 'https://another-team.example.com/help' })
const signedOut = await send(path, null)
assert.equal(signedOut.status, 307, `${name}: expected sign-in redirect`)
const login = new URL(signedOut.headers.get('location') ?? '/', baseUrl)
assert.equal(login.pathname, '/login')
assert.equal(login.searchParams.get('callbackUrl'), path)
assert.equal((await send(path, member)).headers.get('location'), destination)
assert.equal(login.pathname, '/login', name)
assert.equal(login.searchParams.get('callbackUrl'), path, name)
const opened = await send(path, member)
assert.equal(opened.status, 307, `${name}: expected destination redirect`)
assert.equal(opened.headers.get('location'), destination, name)
const denied = await send(path, outsider)
assert.equal(denied.status, 404)
assert(!denied.headers.get('location'))
assert.equal(denied.status, 404, name)
assert(!denied.headers.get('location'), name)
}
)
for (const encodedName of ['%256Danaged-help', 'managed%2Fhelp']) {
const denied = await send(`/sim-links/${encodeURIComponent(orgId)}/${encodedName}`, member)
assert.equal(denied.status, 404, encodedName)
assert(!denied.headers.get('location'), encodedName)
}
})

await check(
'bookmark export requires admin access and drops stale or foreign selections',
Expand Down
Loading