diff --git a/apps/desktop/e2e/smoke.spec.ts b/apps/desktop/e2e/smoke.spec.ts index 7867c67d108..3d2f6e5bdb7 100644 --- a/apps/desktop/e2e/smoke.spec.ts +++ b/apps/desktop/e2e/smoke.spec.ts @@ -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') }) @@ -205,6 +202,10 @@ 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) }) @@ -212,11 +213,14 @@ test.describe('desktop shell smoke', () => { 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 () => { @@ -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() @@ -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' }) @@ -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', diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 303f47a78f4..92826c3aec4 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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", diff --git a/apps/desktop/scripts/build.ts b/apps/desktop/scripts/build.ts index c87f64c0e00..73dff48279f 100644 --- a/apps/desktop/scripts/build.ts +++ b/apps/desktop/scripts/build.ts @@ -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] : [] @@ -130,6 +133,10 @@ const renderer: BuildOptions = { async function run(): Promise { 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) diff --git a/apps/desktop/src/main/local-pages.ts b/apps/desktop/src/main/local-pages.ts index 42e80f6b2ad..1d871eacef6 100644 --- a/apps/desktop/src/main/local-pages.ts +++ b/apps/desktop/src/main/local-pages.ts @@ -37,6 +37,8 @@ const LOCAL_PAGES: ReadonlySet = new Set(['offline.html', 'se const SERVABLE_FILES: ReadonlySet = new Set([ ...LOCAL_PAGES, 'SeasonSansUprightsVF.woff2', + 'geist-variable.woff2', + 'geist-mono-variable.woff2', 'server.js', 'server.css', 'offline.js', diff --git a/apps/desktop/src/renderer/auth.css b/apps/desktop/src/renderer/auth.css new file mode 100644 index 00000000000..6b42c572ca9 --- /dev/null +++ b/apps/desktop/src/renderer/auth.css @@ -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); +} diff --git a/apps/desktop/src/renderer/offline/index.tsx b/apps/desktop/src/renderer/offline/index.tsx index 2797d496129..45141ccba49 100644 --- a/apps/desktop/src/renderer/offline/index.tsx +++ b/apps/desktop/src/renderer/offline/index.tsx @@ -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: { @@ -57,51 +63,55 @@ function OfflinePage({ isSimCloud }: OfflinePageProps) { } return ( - - } - logo={ - - - - } - > - + } + titlebar={