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
1 change: 1 addition & 0 deletions src/ViperIDE.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<button title="Run File [F5]" onclick="app.runCurrentFile()" id="btn-run"><i class="fa-solid fa-circle-play" id="btn-run-icon"></i></button>
</div>
<div>
<span id="boot-status" role="status" hidden><i class="fa-solid fa-spinner fa-spin"></i> <span id="boot-status-text">Starting…</span></span>
<button title="Simulator (virtual device)" onclick="app.connectDevice('sim')" id="btn-conn-sim" disabled><i class="fa-solid fa-display"></i></button>
<button title="Connect WebREPL" onclick="app.connectDevice('ws')" id="btn-conn-ws" disabled ><i class="fa-solid fa-link"></i></button>
<button title="Connect Bluetooth" onclick="app.connectDevice('ble')" id="btn-conn-ble" disabled><i class="fa-brands fa-bluetooth-b"></i></button>
Expand Down
36 changes: 30 additions & 6 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?'
Expand All @@ -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()
Expand Down
13 changes: 12 additions & 1 deletion src/app_common.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
37 changes: 37 additions & 0 deletions test/browser/booting-indicator.spec.js
Original file line number Diff line number Diff line change
@@ -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('>>>')
})
Loading