Skip to content
Merged
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
68 changes: 42 additions & 26 deletions tests/playwright/wasm_btle_api.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const APP_URL = 'https://maximumtrainer.github.io/MaximumTrainer_Redux/app/';
async function injectRecordingBluetoothMock(page) {
await page.addInitScript(() => {
const calls = window._btleApiCalls = {
requestDeviceFilters: null,
requestDeviceFilters: undefined,
getPrimaryService: [],
getCharacteristic: [],
startNotifications: [],
Expand Down Expand Up @@ -81,29 +81,45 @@ async function injectRecordingBluetoothMock(page) {
});
}

// Wait for the Qt canvas to become visible (app fully initialised), then
// trigger BLE scanning via the test helper registered by BtleHubWasm::ctor.
async function waitForCanvas(page) {
// Wait for the Qt canvas to become visible (app fully initialised).
async function waitForAppReady(page) {
await page.waitForFunction(
() => {
const canvas = document.querySelector('#qt-canvas-wrapper');
return canvas && canvas.style.visibility !== 'hidden';
},
{ timeout: 45000 }
);
// Trigger BLE scan via the test helper registered by BtleHubWasm::ctor.
// In a real browser this would require a user gesture; the mock injected by
// injectRecordingBluetoothMock() satisfies the call without any gesture.
await page.evaluate(() => {
if (typeof window.mt_startBleScan === 'function') {
window.mt_startBleScan();
}
});
// Wait until the full async GATT setup chain has completed.
// The last step recorded by the mock is writeValueWithResponse on the
// FTMS Control Point (0x2AD9), which only fires after subscribeAll and
// requestFtmsControl both finish. Polling for it is more reliable than
// a fixed delay.
}

// Trigger BLE scanning via the test helper registered by BtleHubWasm::ctor
// and wait for requestDevice to be called — the earliest reliable signal that
// the WASM bridge has initiated scanning.
async function triggerBleScanAndWaitForRequestDevice(page) {
// Fail fast if the deployed app didn't expose the expected test hook.
const hasHook = await page.evaluate(() => typeof window.mt_startBleScan === 'function');
expect(hasHook, 'window.mt_startBleScan was not registered by the app build').toBeTruthy();

await page.evaluate(() => window.mt_startBleScan());

// Earliest reliable signal: our injected mock recorded a requestDevice call.
// requestDeviceFilters starts as `undefined`; it becomes defined (even if null
// for a filter-less call) the moment requestDevice is invoked.
await page.waitForFunction(
() => {
const calls = window._btleApiCalls;
return calls && calls.requestDeviceFilters !== undefined;
},
{ timeout: 45000 }
Comment on lines +105 to +113
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

triggerBleScanAndWaitForRequestDevice() waits for calls.requestDeviceFilters !== null as a proxy for “requestDevice was called”, but requestDeviceFilters is initialized to null and can legitimately remain null if the app ever calls requestDevice() without filters. In that case the helper will hang until timeout rather than failing with a clear assertion. Consider recording an explicit requestDeviceCalled flag in the injected mock (or initializing requestDeviceFilters to undefined and waiting for it to become defined) and wait on that instead.

Copilot uses AI. Check for mistakes.
);
}

// Wait until the full async GATT setup chain has completed.
// The last step recorded by the mock is writeValueWithResponse on the
// FTMS Control Point (0x2AD9), which only fires after subscribeAll and
// requestFtmsControl both finish. Polling for it is more reliable than
// a fixed delay.
async function waitForFtmsWrite(page) {
await page.waitForFunction(
() => {
const calls = window._btleApiCalls;
Expand Down Expand Up @@ -132,15 +148,11 @@ test.describe('WASM BLE API — Web Bluetooth call verification', () => {
page.on('console', msg => consoleLogs.push({ type: msg.type(), text: msg.text() }));

await page.goto(APP_URL, { waitUntil: 'domcontentloaded' });
await waitForCanvas(page);
await waitForAppReady(page);
await triggerBleScanAndWaitForRequestDevice(page);

const recorded = await page.evaluate(() => window._btleApiCalls);

// Diagnostic: log the full BLE call record when requestDevice was not triggered
if (!recorded.requestDeviceFilters) {
console.error('BLE scan was not initiated. window._btleApiCalls:', JSON.stringify(recorded));
}

// requestDevice must have been called by js_scanAndConnect
expect(recorded.requestDeviceFilters,
'navigator.bluetooth.requestDevice() was not called — BLE scan was not initiated')
Expand All @@ -167,7 +179,8 @@ test.describe('WASM BLE API — Web Bluetooth call verification', () => {
test('getPrimaryService is called for each sensor profile', async ({ page }) => {
await injectRecordingBluetoothMock(page);
await page.goto(APP_URL, { waitUntil: 'domcontentloaded' });
await waitForCanvas(page);
await waitForAppReady(page);
await triggerBleScanAndWaitForRequestDevice(page);

const recorded = await page.evaluate(() => window._btleApiCalls);

Expand All @@ -184,7 +197,8 @@ test.describe('WASM BLE API — Web Bluetooth call verification', () => {
test('startNotifications is called for each sensor characteristic', async ({ page }) => {
await injectRecordingBluetoothMock(page);
await page.goto(APP_URL, { waitUntil: 'domcontentloaded' });
await waitForCanvas(page);
await waitForAppReady(page);
await triggerBleScanAndWaitForRequestDevice(page);

const recorded = await page.evaluate(() => window._btleApiCalls);

Expand All @@ -200,7 +214,9 @@ test.describe('WASM BLE API — Web Bluetooth call verification', () => {
test('FTMS Request Control (opcode 0x00) is written to characteristic 0x2AD9', async ({ page }) => {
await injectRecordingBluetoothMock(page);
await page.goto(APP_URL, { waitUntil: 'domcontentloaded' });
await waitForCanvas(page);
await waitForAppReady(page);
await triggerBleScanAndWaitForRequestDevice(page);
await waitForFtmsWrite(page);

const recorded = await page.evaluate(() => window._btleApiCalls);

Expand Down
Loading