Skip to content
Open
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
48 changes: 28 additions & 20 deletions apps/desktop/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,23 +178,20 @@ test.describe('desktop shell smoke', () => {
const window = await app.firstWindow()
await window.waitForSelector('#retry', { timeout: 30_000 })
expect(window.url()).toMatch(/^sim-shell:\/\/pages\/offline\.html\?/)
await expect(window.getByRole('img', { name: 'Sim', exact: true })).toBeVisible()
await expect(window.getByRole('img', { name: 'Sim', exact: true })).toHaveAttribute(
'aria-label',
'Sim'
)
const home = window.getByRole('link', { name: 'Home', exact: true })
await expect(home).toBeVisible()
await expect(home).toHaveAttribute('href', 'https://sim.ai')
await expect(window.locator('#title')).toHaveText('Can’t connect to Sim')
// The recovery path for a self-hosted shell pointed at a server it cannot
// reach. Exercised end to end here because it is the only coverage of the
// `server:` local-page IPC gate: the bundled page reads the configuration
// over the real preload bridge, and status.sim.ai is withheld because this
// origin is not one of Sim's own. `toBeHidden` is load-bearing — the page's
// own `button { display: inline-flex }` outranks the UA `[hidden]` rule, so
// the attribute alone does not hide it.
// The real preload bridge must retain server recovery while hiding cloud-only status.
await expect(window.locator('#server')).toBeVisible()
await expect(window.locator('#status')).toBeHidden()
await expect
.poll(() => window.evaluate(() => document.fonts.check('16px "Season Sans"')))
.poll(() =>
window.evaluate(async () => {
const fonts = await document.fonts.load('14px "Sim Auth Geist"')
return fonts.length > 0 && fonts.every((font) => font.status === 'loaded')
})
)
.toBe(true)
await expect(window.locator('#detail')).toHaveAttribute('role', 'status')
})
Expand All @@ -205,18 +202,25 @@ test.describe('desktop shell smoke', () => {
const window = await app.firstWindow()
await expect(window.locator('#server')).toBeVisible()
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'inset')
const titlebar = window.locator('[data-auth-titlebar]')
await expect
.poll(() => titlebar.evaluate((element) => element.getBoundingClientRect().height))
.toBeGreaterThanOrEqual(38)
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].setFullScreen(true)
})
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'fullscreen')
await window.reload()
await expect(window.locator('#server')).toBeVisible()
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'fullscreen')
await expect(window.locator('.desktop-title-bar-page')).toHaveCSS('padding-top', '0px')
await expect(titlebar).toHaveCSS('height', '0px')
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].setFullScreen(false)
})
await expect(window.locator('html')).toHaveAttribute('data-sim-desktop-title-bar', 'inset')
await expect
.poll(() => titlebar.evaluate((element) => element.getBoundingClientRect().height))
.toBeGreaterThanOrEqual(38)
})

test('bundled dialogs follow the app theme independently of the system theme', async () => {
Expand Down Expand Up @@ -310,7 +314,7 @@ test.describe('desktop shell smoke', () => {
// Its page, the pre-filled value (which crosses the local-page IPC gate) and
// Escape are asserted together because the packaged build once opened it as
// a blank sheet with no way out.
test('the offline server picker renders EMCN controls and handles validation and dismissal', async () => {
test('the offline server picker handles validation and dismissal', async () => {
const testInfo = test.info()
app = await launchApp('http://127.0.0.1:1')
const window = await app.firstWindow()
Expand All @@ -333,17 +337,21 @@ test.describe('desktop shell smoke', () => {
.toBe(true)
await picker.getByLabel('Server URL').fill('http://example.com')
await picker.getByLabel('Server URL').press('Enter')
await expect(picker.getByRole('alert')).toBeVisible()
await expect(picker.locator('#server-origin-errors')).toBeVisible()
await expect(picker.getByLabel('Server URL')).toHaveAccessibleDescription(
/Server URL must use HTTPS/
)
await expect(picker.getByLabel('Server URL')).toHaveAttribute('aria-invalid', 'true')
await picker.getByLabel('Server URL').fill('http://127.0.0.1:1')
await expect(picker.getByRole('alert')).toHaveCount(0)
await expect(picker.locator('#server-origin-errors')).toHaveCount(0)
await expect(picker.getByLabel('Server URL')).not.toHaveAttribute('aria-invalid', 'true')
await picker.getByRole('button', { name: 'Connect', exact: true }).click()
await expect(picker.getByRole('status')).toHaveText('Already connected to this server.')
await expect
.poll(() =>
picker
.locator('[data-chip-modal-body]')
.evaluate((element) => element.scrollHeight <= element.clientHeight)
.getByRole('dialog')
.evaluate((element) => element.scrollHeight <= globalThis.innerHeight)
)
.toBe(true)
await picker.emulateMedia({ colorScheme: 'light' })
Expand All @@ -352,7 +360,7 @@ test.describe('desktop shell smoke', () => {
animations: 'disabled',
})
await picker.emulateMedia({ colorScheme: 'dark' })
await expect(picker.locator('html')).toHaveClass('dark')
await expect(picker.locator('[data-auth-root]')).toHaveCSS('color-scheme', 'light')
await picker.screenshot({
path: testInfo.outputPath('server-modal-dark.png'),
animations: 'disabled',
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"electron-updater": "6.8.9",
"micromatch": "4.0.8",
"safe-regex2": "5.1.2",
"pdf-lib": "1.17.1"
"pdf-lib": "1.17.1",
"@sim/auth-ui": "workspace:*"
},
"devDependencies": {
"@electron/fuses": "1.8.0",
Expand Down
13 changes: 10 additions & 3 deletions apps/desktop/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,17 @@ const renderer: BuildOptions = {
{
name: 'desktop-tailwind',
setup(builder) {
builder.onLoad({ filter: /shell\.css$/ }, async ({ path }) => {
builder.onLoad({ filter: /(?:shell|utilities|styles\.module)\.css$/ }, async ({ path }) => {
const config = await loadPostcssConfig({}, resolve('../sim'))
const result = await postcss(config.plugins).process(readFileSync(path, 'utf8'), {
from: path,
})
return {
contents: result.css,
loader: 'css',
contents: result.css.replace(
/url\(["']?\.\/assets\/(geist[^)"']+)["']?\)/g,
'url(./$1)'
),
loader: path.endsWith('.module.css') ? 'local-css' : 'css',
resolveDir: dirname(path),
watchFiles: result.messages.flatMap((message) =>
message.type === 'dependency' ? [message.file as string] : []
Expand All @@ -130,6 +133,10 @@ const renderer: BuildOptions = {
async function run(): Promise<void> {
execFileSync(process.execPath, ['run', 'scripts/build-native.ts'], { stdio: 'inherit' })
compileComputerUse()
mkdirSync('dist/renderer', { recursive: true })
for (const font of ['geist-variable.woff2', 'geist-mono-variable.woff2']) {
cpSync(resolve('../../packages/auth-ui/src/assets', font), join('dist/renderer', font))
}
if (watch) {
const { context } = await import('esbuild')
const rendererCtx = await context(renderer)
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/local-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const LOCAL_PAGES: ReadonlySet<string> = new Set<LocalPage>(['offline.html', 'se
const SERVABLE_FILES: ReadonlySet<string> = new Set([
...LOCAL_PAGES,
'SeasonSansUprightsVF.woff2',
'geist-variable.woff2',
'geist-mono-variable.woff2',
'server.js',
'server.css',
'offline.js',
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src/renderer/auth.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
html,
body {
margin: 0;
padding: 0;
background: white;
color-scheme: light;
}

html {
--desktop-title-bar-height: 0px;
}

html[data-sim-desktop-title-bar="inset"] {
--desktop-title-bar-height: max(env(titlebar-area-height, 38px), 38px);
}
90 changes: 50 additions & 40 deletions apps/desktop/src/renderer/offline/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { useState } from 'react'
import {
AuthRoot,
AuthShellView,
AuthTextLink,
AuthWordmark,
AuthButton as Chip,
AuthStatusContent as StatusPageContent,
} from '@sim/auth-ui'
import { observeDesktopTitleBar, type SimDesktopApi } from '@sim/desktop-bridge'
import { Chip, LogoPage, SimWordmark, StatusPageContent } from '@sim/emcn'
import { createRoot } from 'react-dom/client'
import { initializeShellPage } from '@/renderer/shell'
import '@/renderer/shell.css'
import '@/renderer/auth.css'

const ERROR_COPY = {
offline: {
Expand Down Expand Up @@ -57,51 +63,55 @@ function OfflinePage({ isSimCloud }: OfflinePageProps) {
}

return (
<LogoPage
center
className='desktop-title-bar-page'
titleBar={
<div aria-hidden className='desktop-login-window-drag-region desktop-window-drag-region' />
}
logo={
<span role='img' aria-label='Sim' className='flex h-[30px] items-center'>
<SimWordmark />
</span>
}
>
<StatusPageContent
titleId='title'
title={copy.title}
description={copy.message}
detail={
<p
id='detail'
role='status'
className='max-w-full break-words text-[var(--text-muted)] text-caption'
<AuthRoot>
<AuthShellView
homeHref='https://sim.ai'
logo={<AuthWordmark />}
titlebar={<div data-auth-titlebar='' aria-hidden='true' />}
footer={
<AuthTextLink
onClick={() => {
void bridge?.openExternal('mailto:support@sim.ai')
}}
>
{actionError || detail}
</p>
Contact support
</AuthTextLink>
}
>
<Chip id='retry' variant='primary' onClick={() => bridge?.offlineRetry()}>
Retry
</Chip>
{isSimCloud ? (
<Chip id='status' onClick={checkStatus}>
Check status
<StatusPageContent
titleId='title'
title={copy.title}
description={copy.message}
detail={
<p
id='detail'
role='status'
className='au:max-w-full au:break-words au:text-[var(--auth-secondary)] au:text-caption'
>
{actionError || detail}
</p>
}
>
<Chip fullWidth id='retry' variant='primary' onClick={() => bridge?.offlineRetry()}>
Retry
</Chip>
) : null}
<Chip id='server' onClick={() => bridge?.server?.open()}>
Change server
</Chip>
</StatusPageContent>
</LogoPage>
{isSimCloud ? (
<Chip fullWidth id='status' onClick={checkStatus}>
Check status
</Chip>
) : null}
<Chip fullWidth variant='ghost' id='server' onClick={() => bridge?.server?.open()}>
Change server
</Chip>
</StatusPageContent>
</AuthShellView>
</AuthRoot>
)
}

const container = document.getElementById('root')
if (!container) throw new Error('Offline page root is missing')
void initializeShellPage().then(() => {
{
observeDesktopTitleBar(document.documentElement, navigator.userAgent, bridge)
const root = createRoot(container)
root.render(<OfflinePage isSimCloud={false} />)
Expand All @@ -111,4 +121,4 @@ void initializeShellPage().then(() => {
root.render(<OfflinePage isSimCloud={isSimCloud} />)
})
.catch(() => {})
})
}
8 changes: 5 additions & 3 deletions apps/desktop/src/renderer/server/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createRoot } from 'react-dom/client'
import { ServerModal } from '@/renderer/server/server-modal'
import { initializeShellPage, shellWindow } from '@/renderer/shell'
import '@/renderer/shell.css'
import type { ShellWindowApi } from '@/shared/shell'

const shellWindow = (window as Window & { simShell?: ShellWindowApi }).simShell
import '@/renderer/auth.css'

const container = document.getElementById('root')
if (!container || !shellWindow) throw new Error('Server modal host is unavailable')
Expand All @@ -17,4 +19,4 @@ async function renderServerModal() {
}
}

void initializeShellPage().then(renderServerModal)
void renderServerModal()
Loading
Loading