+ Starting…
diff --git a/src/app.js b/src/app.js
index f91634b..3a7a01f 100644
--- a/src/app.js
+++ b/src/app.js
@@ -491,6 +491,16 @@ function wirePort(new_port) {
new_port.onDisconnect(handlePortDisconnect)
}
+/* The "starting" sign: a spinner and a word beside the connect buttons, and
+ the clicked button pulsing. Cleared by the caller on success and failure. */
+function setBooting(on, type) {
+ QID('boot-status').hidden = !on
+ QID('boot-status-text').textContent = (type === 'sim') ? 'Starting simulator…' : 'Connecting…'
+ for (const t of ['sim', 'ws', 'ble', 'usb']) {
+ QID(`btn-conn-${t}`).classList.toggle('booting', on && t === type)
+ }
+}
+
export async function connectDevice(type) {
if (port) {
const msg = (deviceState === 'reconnecting') ? 'Stop reconnecting and disconnect?'
@@ -500,12 +510,26 @@ export async function connectDevice(type) {
return
}
- const new_port = await prepareNewPort(type)
- if (!new_port) { return }
- /* A ?vm=1 link arrives as type 'ws' but hands back the simulator; from
- here on everything - connType, the stage, audio, the button highlight -
- should treat it as what it is. */
- if (new_port.isSimulator) { type = 'sim' }
+ /* Booting the simulator takes a while, and most of it is inside
+ prepareNewPort (requestAccess starts the VM): say so from the click
+ until the prompt is up, whether that ends in a session or an error.
+ Other connections only start once their picker has been answered. */
+ setBooting(type === 'sim', type)
+ try {
+ const new_port = await prepareNewPort(type)
+ if (!new_port) { return }
+ /* A ?vm=1 link arrives as type 'ws' but hands back the simulator; from
+ here on everything - connType, the stage, audio, the button highlight -
+ should treat it as what it is. */
+ if (new_port.isSimulator) { type = 'sim' }
+ setBooting(true, type)
+ await bringUpPort(new_port, type)
+ } finally {
+ setBooting(false)
+ }
+}
+
+async function bringUpPort(new_port, type) {
// Connect new port
try {
await new_port.connect()
diff --git a/src/app_common.css b/src/app_common.css
index 6ec114e..d992252 100644
--- a/src/app_common.css
+++ b/src/app_common.css
@@ -152,12 +152,23 @@ button.reconnecting {
animation: reconnecting-pulse 1.2s ease-in-out infinite;
}
+/* The simulator or a board is coming up after a click on Connect */
+button.booting {
+ animation: reconnecting-pulse 1.2s ease-in-out infinite;
+}
+
+#boot-status {
+ font-size: 0.9rem;
+ opacity: 0.85;
+ white-space: nowrap;
+}
+
@keyframes reconnecting-pulse {
50% { opacity: 0.25; }
}
@media (prefers-reduced-motion: reduce) {
- button.reconnecting {
+ button.reconnecting, button.booting {
animation: none;
opacity: 0.6;
}
diff --git a/test/browser/booting-indicator.spec.js b/test/browser/booting-indicator.spec.js
new file mode 100644
index 0000000..e835bbb
--- /dev/null
+++ b/test/browser/booting-indicator.spec.js
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: MIT
+//
+// Launching the simulator used to show nothing until the board appeared, so
+// the click looked ignored. The page now shows a "Starting" status from the
+// click until the prompt is up, and clears it when the boot fails too.
+import { test, expect } from '@playwright/test'
+
+test('a booting indicator shows from the click until the prompt is up', async ({ page }) => {
+ await page.goto('/')
+ const status = page.locator('#boot-status')
+ await expect(status).toBeHidden()
+ await page.click('#btn-conn-sim')
+ await expect(status).toBeVisible({ timeout: 2000 })
+ await expect(status).toContainText('Starting')
+ await expect(page.locator('#btn-conn-sim')).toHaveClass(/booting/)
+ await expect(page.locator('#xterm')).toContainText('>>>', { timeout: 45_000 })
+ await expect(status).toBeHidden()
+ await expect(page.locator('#btn-conn-sim')).not.toHaveClass(/booting/)
+})
+
+test('the booting indicator shows, then clears, when the simulator fails to start', async ({ page }) => {
+ await page.route('**/micropython.wasm', (route) => route.abort())
+ await page.goto('/')
+ // A failed boot can be over in well under a poll, so watch for the sign
+ // from inside the page instead of racing it.
+ await page.evaluate(() => {
+ const el = document.getElementById('boot-status')
+ window.__sawBooting = false
+ new MutationObserver(() => { if (!el.hidden) window.__sawBooting = true })
+ .observe(el, { attributes: true, attributeFilter: ['hidden'] })
+ })
+ await page.click('#btn-conn-sim')
+ await expect.poll(() => page.evaluate(() => window.__sawBooting), { timeout: 30_000 }).toBe(true)
+ await expect(page.locator('#boot-status')).toBeHidden({ timeout: 30_000 })
+ await expect(page.locator('#btn-conn-sim')).not.toHaveClass(/booting/)
+ await expect(page.locator('#xterm')).not.toContainText('>>>')
+})