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
6 changes: 5 additions & 1 deletion app/components/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ export default function Terminal({ ws }: TerminalProps) {

return (
<>
<div className="h-full w-[calc(100%-3rem)] text-mono-code" ref={terminalRef} />
<div
role="application"
className="h-full w-[calc(100%-3rem)] text-mono-code"
ref={terminalRef}
/>
<div className="absolute right-0 top-0 space-y-2 text-default">
<ScrollButton onClick={() => term?.scrollToTop()} aria-label="Scroll to top">
<DirectionUpIcon aria-hidden />
Expand Down
30 changes: 28 additions & 2 deletions app/msw-mock-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,17 @@ const randomStatus = () => {

const sleep = async (ms: number) => new Promise((res) => setTimeout(res, ms))

async function streamString(socket: WebSocket, s: string, delayMs = 50) {
for (const c of s) {
socket.send(c)
await sleep(delayMs)
}
}

export async function startMockAPI() {
// dynamic imports to make extremely sure none of this code ends up in the prod bundle
const { handlers } = await import('../mock-api/msw/handlers')
const { http, HttpResponse } = await import('msw')
const { http, HttpResponse, ws } = await import('msw')
const { setupWorker } = await import('msw/browser')

// defined in here because it depends on the dynamic import
Expand All @@ -77,8 +84,27 @@ export async function startMockAPI() {
// don't return anything means fall through to the real handlers
})

// serial console
const secure = window.location.protocol === 'https:'
const protocol = secure ? 'wss' : 'ws'
const serialConsole = `${protocol}://${window.location.host}/v1/instances/:instance/serial-console/stream`

// https://mswjs.io/docs/api/setup-worker/start#options
await setupWorker(interceptAll, ...handlers).start({
await setupWorker(
interceptAll,
...handlers,

ws.link(serialConsole).addEventListener('connection', async ({ client }) => {
client.addEventListener('message', (event) => {
// Mirror client messages back (lets you type in the terminal). If it's
// an enter key, send a newline.
// eslint-disable-next-line @typescript-eslint/no-base-to-string
client.send(event.data.toString() === '13' ? '\r\n' : event.data)
})
await sleep(1000) // make sure everything is ready first (especially a problem in CI)
await streamString(client.socket, 'Wake up Neo...')
})
).start({
quiet: true, // don't log successfully handled requests
// custom handler only to make logging less noisy. unhandled requests still
// pass through to the server
Expand Down
28 changes: 24 additions & 4 deletions test/e2e/instance-serial.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
*
* Copyright Oxide Computer Company
*/
import { clickRowAction, expect, test } from './utils'
import { expect, test, type Page } from '@playwright/test'

import { clickRowAction } from './utils'

test('serial console can connect while starting', async ({ page }) => {
// create an instance
Expand All @@ -29,11 +31,10 @@ test('serial console can connect while starting', async ({ page }) => {
await expect(page.getByText('The instance is starting')).toBeVisible()
await expect(page.getByText('The instance is')).toBeHidden()

// Here it would be nice to test that the serial console connects, but we
// can't mock websockets with MSW yet: https://github.com/mswjs/msw/pull/2011
await testSerialConsole(page)
})

test('links in instance actions', async ({ page }) => {
test('serial console for existing instance', async ({ page }) => {
await page.goto('/projects/mock-project/instances')
await clickRowAction(page, 'db1', 'View serial console')
await expect(page).toHaveURL('/projects/mock-project/instances/db1/serial-console')
Expand All @@ -42,4 +43,23 @@ test('links in instance actions', async ({ page }) => {
await page.getByRole('button', { name: 'Instance actions' }).click()
await page.getByRole('menuitem', { name: 'View serial console' }).click()
await expect(page).toHaveURL('/projects/mock-project/instances/db1/serial-console')

await testSerialConsole(page)
})

async function testSerialConsole(page: Page) {
const xterm = page.getByRole('application')

// MSW mocks a message. use first() because there are multiple copies on screen
await expect(xterm.getByText('Wake up Neo...').first()).toBeVisible()

// we need to do this for our keypresses to land
await page.locator('.xterm-helper-textarea').focus()

await xterm.pressSequentially('abc')
await expect(xterm.getByText('Wake up Neo...abc').first()).toBeVisible()
await xterm.press('Enter')
await xterm.pressSequentially('def')
await expect(xterm.getByText('Wake up Neo...abc').first()).toBeVisible()
await expect(xterm.getByText('def').first()).toBeVisible()
}
47 changes: 0 additions & 47 deletions tools/deno/mock-serial-console.ts

This file was deleted.

9 changes: 0 additions & 9 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,6 @@ export default defineConfig(({ mode }) => ({
apiMode === 'dogfood' ? `https://${DOGFOOD_HOST}` : 'http://localhost:12220',
changeOrigin: true,
},
'^/v1/instances/[^/]+/serial-console/stream': {
target:
// in msw mode, serial console is served by tools/deno/mock-serial-console.ts
apiMode === 'dogfood'
? `wss://${DOGFOOD_HOST}`
: 'ws://127.0.0.1:' + (apiMode === 'msw' ? 6036 : 12220),
changeOrigin: true,
ws: true,
},
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This didn't work unless you manually ran tools/deno/mock-serial-console.ts. Now it all just works.

},
},
preview: { headers },
Expand Down
Loading