diff --git a/app/components/Terminal.tsx b/app/components/Terminal.tsx
index bd39a5a9a..1eadcbc23 100644
--- a/app/components/Terminal.tsx
+++ b/app/components/Terminal.tsx
@@ -104,7 +104,11 @@ export default function Terminal({ ws }: TerminalProps) {
return (
<>
-
+
term?.scrollToTop()} aria-label="Scroll to top">
diff --git a/app/msw-mock-api.ts b/app/msw-mock-api.ts
index 2aa6fc5b6..8348a392f 100644
--- a/app/msw-mock-api.ts
+++ b/app/msw-mock-api.ts
@@ -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
@@ -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
diff --git a/test/e2e/instance-serial.e2e.ts b/test/e2e/instance-serial.e2e.ts
index ca6000350..c5082e9a1 100644
--- a/test/e2e/instance-serial.e2e.ts
+++ b/test/e2e/instance-serial.e2e.ts
@@ -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
@@ -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')
@@ -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()
+}
diff --git a/tools/deno/mock-serial-console.ts b/tools/deno/mock-serial-console.ts
deleted file mode 100755
index 3796bffca..000000000
--- a/tools/deno/mock-serial-console.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-#! /usr/bin/env -S deno run --allow-run --allow-net
-
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, you can obtain one at https://mozilla.org/MPL/2.0/.
- *
- * Copyright Oxide Computer Company
- */
-import { delay } from 'https://deno.land/std@0.181.0/async/delay.ts'
-import { serve } from 'https://deno.land/std@0.181.0/http/server.ts'
-
-/*
- * This exists because MSW does not support websockets. So in MSW mode, we also
- * run this little server and configure Vite to proxy WS requests to it.
- */
-
-async function streamString(socket: WebSocket, s: string, delayMs = 50) {
- for (const c of s) {
- socket.send(new TextEncoder().encode(c))
- await delay(delayMs)
- }
-}
-
-async function serialConsole(req: Request) {
- await delay(500)
- const { socket, response } = Deno.upgradeWebSocket(req)
- socket.binaryType = 'arraybuffer'
-
- console.info(`New client connected`)
-
- // send hello as a binary frame for xterm to display
- socket.onopen = () => {
- setTimeout(() => {
- streamString(socket, 'Wake up Neo...')
- }, 200)
- }
-
- // echo back binary data
- socket.onmessage = (m) => socket.send(m.data)
-
- socket.onclose = () => console.info('Connection closed')
-
- return response
-}
-
-serve(serialConsole, { port: 6036 })
diff --git a/vite.config.ts b/vite.config.ts
index 68b66e281..9fe5d8eb9 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -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,
- },
},
},
preview: { headers },