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
34 changes: 22 additions & 12 deletions wework/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, test, vi } from 'vitest'
import App from './App'

vi.mock('@/features/auth/AuthProvider', () => ({
AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))

vi.mock('@/features/auth/useAuth', () => ({
useAuth: () => ({
user: null,
isLoading: false,
adminPasswordSetupRequired: false,
adminUsername: 'admin',
login: vi.fn(),
logout: vi.fn(),
refresh: vi.fn(),
loginWithOidcToken: vi.fn(),
setupAdminPassword: vi.fn(),
}),
}))

describe('App auth routing', () => {
beforeEach(() => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ required: false }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
)
)
afterEach(() => {
cleanup()
})

test('renders login page on /login', async () => {
Expand Down
5 changes: 5 additions & 0 deletions wework/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
import { AppshotBridge } from '@/features/appshots/AppshotBridge'
import { SystemDragPanel } from '@/features/system-drag/SystemDragPanel'
import { SystemDragBridge } from '@/features/system-drag/SystemDragBridge'
import { installMacOSInputArrowKeyGuard } from '@/lib/macosInputArrowKeyGuard'

const WORKBENCH_STARTUP_REVEAL_TIMEOUT_MS = 6000

Expand Down Expand Up @@ -377,6 +378,10 @@ function AppShell() {
}
}, [isTauri])

useEffect(() => {
return installMacOSInputArrowKeyGuard()
}, [])

useEffect(() => {
if (
path === '/login' ||
Expand Down
2 changes: 1 addition & 1 deletion wework/src/components/chat/ChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@ describe('ChatInput', () => {
'data-enter-animation',
'main'
)
expect(selectorButton).toHaveStyle({ width: '240px' })
expect(selectorButton).toHaveStyle({ width: 'var(--model-selector-width, auto)' })
expect(screen.queryByTestId('model-selector-tooltip')).not.toBeInTheDocument()
expect(screen.getByTestId('model-selector-menu').parentElement).toHaveClass(
'fixed',
Expand Down
2 changes: 2 additions & 0 deletions wework/src/components/chat/composer/ComposerToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface ComposerToolbarProps {
}

const COMPACT_TOOLBAR_WIDTH = 475
const NARROW_MODEL_SELECTOR_MAX_WIDTH = 160

export function ComposerToolbar({
canSend,
Expand Down Expand Up @@ -131,6 +132,7 @@ export function ComposerToolbar({
onSelectModelOption={onSelectModelOption}
onBlockedModelSelect={onBlockedModelSelect}
buttonClassName="opacity-90 hover:opacity-100"
maxClosedWidth={compact ? NARROW_MODEL_SELECTOR_MAX_WIDTH : undefined}
/>
) : (
<div className="h-11 w-32 shrink-0" data-testid="model-selector-loading" />
Expand Down
308 changes: 308 additions & 0 deletions wework/src/components/chat/composer/ModelSelector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import type { UnifiedModel } from '@/types/api'

const isMobileMock = vi.hoisted(() => vi.fn())
vi.mock('@/hooks/useIsMobile', () => ({
useIsMobile: isMobileMock,
}))

const configuredKeybindingMock = vi.hoisted(() => vi.fn())
vi.mock('@/hooks/useConfiguredKeybinding', () => ({
useConfiguredKeybinding: configuredKeybindingMock,
}))

const modelExecutionMock = vi.hoisted(() => ({
getModelExecutionOverride: vi.fn(),
}))
vi.mock('@/features/cloud-connection/modelExecution', () => modelExecutionMock)

vi.mock('@/hooks/useTranslation', () => ({
useTranslation: () => ({
t: (key: string, fallback?: string) => fallback ?? key,
}),
}))

import { ModelSelector } from './ModelSelector'

const SHELL_LEFT = 800
const WINDOW_INNER_WIDTH = 1200
const WINDOW_INNER_HEIGHT = 900

function createShellElement(options?: { hidden?: boolean }): HTMLDivElement {
const shell = document.createElement('div')
shell.id = 'right-workspace-panel-shell'
shell.setAttribute('data-testid', 'right-workspace-panel-shell')
if (options?.hidden) {
shell.setAttribute('aria-hidden', 'true')
} else {
shell.setAttribute('aria-hidden', 'false')
}
shell.getBoundingClientRect = () =>
({
top: 0,
left: options?.hidden ? WINDOW_INNER_WIDTH : SHELL_LEFT,
right: WINDOW_INNER_WIDTH,
bottom: WINDOW_INNER_HEIGHT,
width: options?.hidden ? 0 : WINDOW_INNER_WIDTH - SHELL_LEFT,
height: WINDOW_INNER_HEIGHT,
x: options?.hidden ? WINDOW_INNER_WIDTH : SHELL_LEFT,
y: 0,
toJSON: () => {},
}) as DOMRect
document.body.appendChild(shell)
return shell
}

function setButtonRect(button: HTMLElement, right = 850) {
const left = right - 240
button.getBoundingClientRect = () =>
({
top: 500,
left,
right,
bottom: 540,
width: 240,
height: 40,
x: left,
y: 500,
toJSON: () => {},
}) as DOMRect
}

const SAMPLE_MODEL: UnifiedModel = {
name: 'gpt-5.5',
modelId: 'gpt-5.5',
displayName: 'GPT 5.5',
type: 'runtime',
provider: 'local',
config: {
weworkModelKind: 'codex-official',
ui: { family: 'codex-official', controls: ['speed'] },
},
}

describe('ModelSelector desktop layout', () => {
const originalInnerWidth = window.innerWidth
const originalInnerHeight = window.innerHeight

beforeEach(() => {
isMobileMock.mockReturnValue(false)
configuredKeybindingMock.mockReturnValue(null)
modelExecutionMock.getModelExecutionOverride.mockReturnValue(undefined)
Object.defineProperty(window, 'innerWidth', {
configurable: true,
value: WINDOW_INNER_WIDTH,
})
Object.defineProperty(window, 'innerHeight', {
configurable: true,
value: WINDOW_INNER_HEIGHT,
})
})

afterEach(() => {
cleanup()
document.querySelectorAll('#right-workspace-panel-shell').forEach(shell => shell.remove())
vi.restoreAllMocks()
Object.defineProperty(window, 'innerWidth', {
configurable: true,
value: originalInnerWidth,
})
Object.defineProperty(window, 'innerHeight', {
configurable: true,
value: originalInnerHeight,
})
})

test('positions the menu to the left of the right workspace panel shell', async () => {
createShellElement()

render(
<ModelSelector
models={[SAMPLE_MODEL]}
selectedModel={SAMPLE_MODEL}
selectedModelOptions={{}}
disabled={false}
onSelectModel={vi.fn()}
onSelectModelOption={vi.fn()}
/>
)

const button = screen.getByTestId('model-selector-button')
setButtonRect(button)
fireEvent.click(button)

const menu = await waitFor(() => screen.getByTestId('model-selector-menu'))
const wrapper = menu.parentElement
expect(wrapper).not.toBeNull()
const left = parseInt(wrapper!.style.left, 10)
expect(left).toBeLessThanOrEqual(SHELL_LEFT - 16)
})

test('caps the trigger width when maxClosedWidth is provided', () => {
createShellElement({ hidden: true })

const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect
HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
if (this.getAttribute('data-testid') === 'model-selector-button') {
return {
top: 500,
left: 700,
right: 940,
bottom: 540,
width: 240,
height: 40,
x: 700,
y: 500,
toJSON: () => {},
} as DOMRect
}
if (this.getAttribute('aria-hidden') === 'true' && this.tagName === 'SPAN') {
return {
top: 0,
left: 0,
right: 300,
bottom: 0,
width: 300,
height: 0,
x: 0,
y: 0,
toJSON: () => {},
} as DOMRect
}
return originalGetBoundingClientRect.call(this)
}

try {
render(
<ModelSelector
models={[SAMPLE_MODEL]}
selectedModel={SAMPLE_MODEL}
selectedModelOptions={{}}
disabled={false}
onSelectModel={vi.fn()}
onSelectModelOption={vi.fn()}
maxClosedWidth={160}
/>
)

const button = screen.getByTestId('model-selector-button')
expect(button.style.getPropertyValue('--model-selector-width')).toBe('160px')
expect(button.style.width).toBe('var(--model-selector-width, auto)')
} finally {
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect
}
})

test('keeps the trigger capped when opened with maxClosedWidth', async () => {
createShellElement({ hidden: true })

const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect
HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
if (this.getAttribute('data-testid') === 'model-selector-button') {
return {
top: 500,
left: 700,
right: 940,
bottom: 540,
width: 240,
height: 40,
x: 700,
y: 500,
toJSON: () => {},
} as DOMRect
}
if (this.getAttribute('aria-hidden') === 'true' && this.tagName === 'SPAN') {
return {
top: 0,
left: 0,
right: 300,
bottom: 0,
width: 300,
height: 0,
x: 0,
y: 0,
toJSON: () => {},
} as DOMRect
}
return originalGetBoundingClientRect.call(this)
}

try {
render(
<ModelSelector
models={[SAMPLE_MODEL]}
selectedModel={SAMPLE_MODEL}
selectedModelOptions={{}}
disabled={false}
onSelectModel={vi.fn()}
onSelectModelOption={vi.fn()}
maxClosedWidth={160}
/>
)

const button = screen.getByTestId('model-selector-button')
fireEvent.click(button)
await waitFor(() => screen.getByTestId('model-selector-menu'))

expect(button.style.getPropertyValue('--model-selector-width')).toBe('160px')
expect(button.style.width).toBe('var(--model-selector-width, auto)')
} finally {
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect
}
})

test('uses the default closed width cap when maxClosedWidth is omitted', () => {
createShellElement({ hidden: true })

const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect
HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
if (this.getAttribute('data-testid') === 'model-selector-button') {
return {
top: 500,
left: 700,
right: 940,
bottom: 540,
width: 240,
height: 40,
x: 700,
y: 500,
toJSON: () => {},
} as DOMRect
}
if (this.getAttribute('aria-hidden') === 'true' && this.tagName === 'SPAN') {
return {
top: 0,
left: 0,
right: 300,
bottom: 0,
width: 300,
height: 0,
x: 0,
y: 0,
toJSON: () => {},
} as DOMRect
}
return originalGetBoundingClientRect.call(this)
}

try {
render(
<ModelSelector
models={[SAMPLE_MODEL]}
selectedModel={SAMPLE_MODEL}
selectedModelOptions={{}}
disabled={false}
onSelectModel={vi.fn()}
onSelectModelOption={vi.fn()}
/>
)

const button = screen.getByTestId('model-selector-button')
expect(button.style.getPropertyValue('--model-selector-width')).toBe('208px')
expect(button.style.width).toBe('var(--model-selector-width, auto)')
} finally {
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect
}
})
})
Loading
Loading