From 4d7867e61cbe41e51432ebf678685a1d62e2e326 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 27 Jul 2026 14:13:03 +0200 Subject: [PATCH] fix(web): allow to set default keyboard to 'off' The parameters for `setKeyboardForControl` basically have three different modes: - an id of a keyboard/language to set that keyboard, enabling independent keyboard mode - empty string to set the system keyboard, enabling independent keyboard mode - `null` to disable independent keyboard mode. Our previous code didn't properly handle the last two modes. This change fixes the problems and also clarifies and updates the documentation. Also included is an improvement to the guide-examples e2e tests that now wait until all keyboards are loaded (which may fix #16167). Partially drafted by kilo.ai. Fixes: #16080 Build-bot: skip release:web --- .../reference/core/setKeyboardForControl.md | 15 ++-- web/src/app/browser/src/contextManager.ts | 38 ++++++---- web/src/app/browser/src/keymanEngine.ts | 36 ++++++--- web/src/app/browser/src/languageMenu.ts | 5 +- .../engine/src/attachment/attachmentInfo.ts | 9 ++- web/src/engine/src/main/contextManagerBase.ts | 15 ++-- .../dom/cases/browser/contextManager.tests.ts | 30 +++++++- .../dom/cases/browser/keymanEngine.tests.ts | 58 +++++++++++++++ web/src/test/auto/e2e/e2eUtils.ts | 60 ++++++++++----- web/src/test/auto/e2e/guide-examples.tests.ts | 73 ++++++++++++++----- 10 files changed, 254 insertions(+), 85 deletions(-) create mode 100644 web/src/test/auto/dom/cases/browser/keymanEngine.tests.ts diff --git a/web/docs/engine/reference/core/setKeyboardForControl.md b/web/docs/engine/reference/core/setKeyboardForControl.md index 007c1e6fb13..b088d137a72 100644 --- a/web/docs/engine/reference/core/setKeyboardForControl.md +++ b/web/docs/engine/reference/core/setKeyboardForControl.md @@ -9,21 +9,21 @@ Associate control with independent keyboard settings initialized to a specific k ## Syntax ```js -keyman.setDefaultKeyboardForControl(Pelem, keyboard, languageCode); +keyman.setKeyboardForControl(elem, keyboard, languageCode); ``` ### Parameters -`Pelem` +`elem` : Type: `Element` : The control element to be managed manually. `keyboard` -: Type: `string` *optional* +: Type: `string | null` *optional* : The ID (internal name) of a keyboard. `languageCode` -: Type: `string` *optional* +: Type: `string | null` *optional* : The three-letter language code for the keyboard. ### Return Value @@ -34,14 +34,17 @@ keyman.setDefaultKeyboardForControl(Pelem, keyboard, languageCode); This function establishes the control with separately-managed keyboard settings from other, non-specialized controls on the page. This may be -undone by setting both `keyboard` and `languageCode` to null, reverting -the control back to default keyboard-management behavior. +undone by setting both `keyboard` and `languageCode` to null, +reverting the control back to default keyboard-management behavior. Note that either **both** parameters should be set or **neither**. In particular, if `languageCode` is specified but not `keyboard`, this method will not automatically select an appropriate keyboard, even if one has previously been registered with that language code. +Set `keyboard` and `languageCode` to the empty string (`''`) to select the system +keyboard (on non-touch devices), or the first installed keyboard (on touch devices). + Due to system limitations, this function will fail if called on an IFRAME element. See also [Control-by-Control Example (guide)](../../guide/examples/control-by-control) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index d1d428c1ae9..8e171a77fe4 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -366,21 +366,22 @@ export class ContextManager extends ContextManagerBase { protected currentKeyboardSrcTextStore(): AbstractElementTextStore | null { const textStore = this.currentTextStore || this.mostRecentTextStore; - if(this.isTextStoreKeyboardIndependent(textStore)) { + if(this.isElementInIndependentMode(textStore?.getElement())) { return textStore; } return null; } - private isTextStoreKeyboardIndependent(textStore: AbstractElementTextStore): boolean { - const attachment = textStore?.getElement()._kmwAttachment; + public isElementInIndependentMode(element: HTMLElement | null): boolean { + const attachment = element?._kmwAttachment; // If null or undefined, we're in 'global' mode. - return !!(attachment?.keyboard || attachment?.keyboard === ''); + return !!(attachment && + (attachment.keyboard !== undefined && attachment.keyboard !== null)); } // Note: is part of the keyboard activation process. Not to be called directly by published API. - public activateKeyboardForTextStore(kbd: KeyboardInfoPair, textStore: AbstractElementTextStore): void { + protected activateKeyboardForTextStore(kbd: KeyboardInfoPair, textStore: AbstractElementTextStore): void { const attachment = textStore?.getElement()._kmwAttachment; if(!attachment) { @@ -421,7 +422,7 @@ export class ContextManager extends ContextManagerBase { * @param kbdId * @param langId */ - public setKeyboardForTextStore(textStore: AbstractElementTextStore, kbdId: string, langId: string): void { + public setKeyboardForTextStore(textStore: AbstractElementTextStore, kbdId: string | null, langId: string | null): void { if(textStore instanceof DesignIFrameElementTextStore) { console.warn("'keymanweb.setKeyboardForControl' cannot set keyboard on iframes."); return; @@ -436,20 +437,25 @@ export class ContextManager extends ContextManagerBase { if(!attachment) { return; } else { + if(wasPriorTextStore && kbdId === null) { + this.findAndPopActivation(textStore); + } + // Either establishes or cancels independent-keyboard mode by setting the // associated metadata. This will have direct effects on the results // of .currentKeyboardSrcTextStore(). - attachment.keyboard = kbdId || null; - attachment.languageCode = langId || null; + attachment.keyboard = kbdId ?? null; + attachment.languageCode = langId ?? null; // If it has just entered independent-keyboard mode, we need the second check. if(wasPriorTextStore || this.currentKeyboardSrcTextStore() == textStore) { const globalKbd = this.globalKeyboard.metadata; - // The `||` bits below - in case we're cancelling independent-keyboard mode. + // `??` preserves empty-string values for an explicit "system keyboard" state, + // while falling back to the global keyboard only when the control is truly unset. this.activateKeyboard( - attachment.keyboard || globalKbd.id, - attachment.languageCode || globalKbd.langId, + attachment.keyboard ?? globalKbd.id, + attachment.languageCode ?? globalKbd.langId, true ); } @@ -457,7 +463,7 @@ export class ContextManager extends ContextManagerBase { } public getKeyboardStubForTextStore(textStore: AbstractElementTextStore) { - if(!this.isTextStoreKeyboardIndependent(textStore)) { + if(!this.isElementInIndependentMode(textStore?.getElement())) { return this.globalKeyboard.metadata; } else { const attachment = textStore.getElement()._kmwAttachment; @@ -578,7 +584,7 @@ export class ContextManager extends ContextManagerBase { langCode = lgCode; } - if(lastElem && lastElem._kmwAttachment.keyboard != null) { + if (lastElem && this.isElementInIndependentMode(lastElem)) { lastElem._kmwAttachment.keyboard = keyboardID; lastElem._kmwAttachment.languageCode = langCode; } else { @@ -598,8 +604,10 @@ export class ContextManager extends ContextManagerBase { const attachment = lastElem._kmwAttachment; const global = this.globalKeyboard; - if(attachment.keyboard != null) { - this.activateKeyboard(attachment.keyboard, attachment.languageCode, true); + if (this.isElementInIndependentMode(lastElem)) { + const keyboardId = attachment.keyboard ?? global?.metadata.id ?? ''; + const languageCode = attachment.languageCode ?? global?.metadata.langId ?? ''; + this.activateKeyboard(keyboardId, languageCode, true); } else if(!blockGlobalChange && (global?.metadata != this._activeKeyboard?.metadata)) { // TODO: can we drop `!blockGlobalChange` in favor of the latter check? this.activateKeyboard(global?.metadata.id, global?.metadata.langId, true); diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 9b56247cdd1..531e0970a77 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -292,7 +292,10 @@ export class KeymanEngine extends KeymanEngineBase, public keyboard: string) {} + constructor( + public readonly textStore: AbstractElementTextStore, + public keyboard: string | null) { } } diff --git a/web/src/engine/src/main/contextManagerBase.ts b/web/src/engine/src/main/contextManagerBase.ts index 931c18c039b..887fb0564ff 100644 --- a/web/src/engine/src/main/contextManagerBase.ts +++ b/web/src/engine/src/main/contextManagerBase.ts @@ -161,13 +161,16 @@ export abstract class ContextManagerBase protected abstract activateKeyboardForTextStore(kbd: KeyboardInfoPair, textStore: TextStore): void; /** - * Checks the pending keyboard-activation array for an entry corresponding to the specified - * TextStore. If found, also removes the entry for bookkeeping purposes. - * @param textStore The specific TextStore affected by the pending Keyboard activation. - * May be `null`, which corresponds to the global default Keyboard. - * @returns `true` if pending activation is still valid, `false` otherwise. + * Checks the pending keyboard-activation array for an entry + * corresponding to the specified TextStore. If found, also removes + * the entry for bookkeeping purposes. + * @param textStore The specific TextStore affected by the pending + * Keyboard activation. May be `null`, which + * corresponds to the global default Keyboard. + * @returns the pending activation for the specified TextStore, or + * `null` if no such activation exists. */ - private findAndPopActivation(textStore: TextStore): PendingActivation { + protected findAndPopActivation(textStore: TextStore): PendingActivation { // Array.findIndex requires Chrome 45+. :( let activationIndex; for(activationIndex = 0; activationIndex < this.pendingActivations.length; activationIndex++) { diff --git a/web/src/test/auto/dom/cases/browser/contextManager.tests.ts b/web/src/test/auto/dom/cases/browser/contextManager.tests.ts index ce96c2a3561..a86e0a20f71 100644 --- a/web/src/test/auto/dom/cases/browser/contextManager.tests.ts +++ b/web/src/test/auto/dom/cases/browser/contextManager.tests.ts @@ -969,7 +969,7 @@ describe('app/browser: ContextManager', function () { // Actual test: transitioning focus from an independent-mode target // to a global-mode target. - contextManager.setKeyboardForTextStore(textStore, '', ''); + contextManager.setKeyboardForTextStore(textStore, null, null); const beforekeyboardchange = sinon.fake(); const keyboardchange = sinon.fake(); @@ -1017,7 +1017,7 @@ describe('app/browser: ContextManager', function () { // Actual test: transitioning focus from an independent-mode target // to a global-mode target. - contextManager.setKeyboardForTextStore(textStore, '', ''); + contextManager.setKeyboardForTextStore(textStore, null, null); // Allow the indirect keyboard-change operation to resolve. await timedPromise(10); @@ -1250,6 +1250,32 @@ describe('app/browser: ContextManager', function () { assert.isTrue(keyboardasyncload.calledTwice); assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.test_chirality.metadata); }); + + it('cancels pending activation when leaving independent mode', async () => { + const FETCH_DELAY = 50; + + keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard); + + await contextManager.activateKeyboard('khmer_angkor', 'km'); + + const textarea = document.getElementById('textarea'); + const textStore = textStoreForElement(textarea); + + withDelayedFetching(keyboardLoader, FETCH_DELAY, () => { + contextManager.setKeyboardForTextStore(textStore, 'lao_2008_basic', 'lo'); + }); + + await Promise.resolve(); + + contextManager.setKeyboardForTextStore(textStore, null, null); + + await timedPromise(FETCH_DELAY + 10); + + const attachment = textStore.getElement()._kmwAttachment; + assert.isNull(attachment.keyboard); + assert.equal((contextManager as any).currentKeyboardSrcTextStore(), null); + assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata); + }); }); }); diff --git a/web/src/test/auto/dom/cases/browser/keymanEngine.tests.ts b/web/src/test/auto/dom/cases/browser/keymanEngine.tests.ts new file mode 100644 index 00000000000..845858ce70b --- /dev/null +++ b/web/src/test/auto/dom/cases/browser/keymanEngine.tests.ts @@ -0,0 +1,58 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import { KeymanEngine } from 'keyman/app/browser'; +import { StubAndKeyboardCache } from 'keyman/engine/keyboard-storage'; +import { assert } from 'chai'; + +const mockWorkerFactory = { + constructInstance: (): null => null +}; + +describe('KeymanEngine.getKeyboardForControl', () => { + let engine: KeymanEngine; + let keyboardCache: StubAndKeyboardCache; + + beforeEach(() => { + engine = new KeymanEngine(mockWorkerFactory, ''); + keyboardCache = new StubAndKeyboardCache(); + (engine as any).keyboardRequisitioner = { + cache: keyboardCache + }; + }); + + it('returns null for controls in global mode', () => { + const input = document.createElement('input'); + document.body.appendChild(input); + engine.attachToControl(input); + + assert.isNull(engine.getKeyboardForControl(input)); + }); + + it('returns empty string for explicit system-keyboard mode', () => { + const input = document.createElement('input'); + document.body.appendChild(input); + engine.attachToControl(input); + + engine.setKeyboardForControl(input, '', ''); + assert.equal(engine.getKeyboardForControl(input), ''); + }); + + it('returns canonical prefixed ID after setting an unprefixed ID', () => { + const stub = { + KI: 'Keyboard_lao_2008_basic', + KN: 'Lao 2008 Basic', + KL: 'Lao', + KLC: 'lo', + KF: 'resources/keyboards/lao_2008_basic.js', + } as any; + keyboardCache.addStub(stub); + + const input = document.createElement('input'); + document.body.appendChild(input); + engine.attachToControl(input); + + engine.setKeyboardForControl(input, 'lao_2008_basic', 'lo'); + assert.equal(engine.getKeyboardForControl(input), 'Keyboard_lao_2008_basic'); + }); +}); diff --git a/web/src/test/auto/e2e/e2eUtils.ts b/web/src/test/auto/e2e/e2eUtils.ts index 0ef45ffec00..ee9ffa191da 100644 --- a/web/src/test/auto/e2e/e2eUtils.ts +++ b/web/src/test/auto/e2e/e2eUtils.ts @@ -4,34 +4,64 @@ import { type Locator, type Page } from "@playwright/test"; +declare const keyman: any; + +/** + * Wait until the keyboard menu is updated and shows the active keyboard + */ +export async function waitForKeyboardSelection(page: Page): Promise { + await page.waitForFunction(() => { + const activeKbd = keyman.getActiveKeyboard(); + const activeLang = keyman.getActiveLanguage(); + const selectedElem = document.querySelector('#kmwico .selected'); + if (!selectedElem) { + return false; + } + const selectedText = (selectedElem.textContent || '').trim(); + + if (!activeKbd) { + return selectedText === '(System keyboard)'; + } + + const keyboards = keyman.getKeyboards(); + for (const kbd of keyboards) { + if (kbd.InternalName === activeKbd && kbd.LanguageCode === activeLang) { + return selectedText === `${kbd.LanguageName} - ${kbd.Name}`; + } + } + return false; + }, { timeout: 5000 }); +} + /** * Expands the keyboard selection menu and returns the text content of the * currently selected keyboard. */ export async function getSelectedKeyboardMenuText(page: Page): Promise { - const watchDog = page.waitForFunction(() => !!document.getElementById('KeymanWeb_KbdList')); - await page.getByRole('img', { name: 'Use Web Keyboard' }).click(); - await watchDog; + await page.waitForFunction(() => document.getElementById('kmwico')); + await page.locator('#kmwico').hover(); + await page.waitForFunction(() => document.querySelector('#KeymanWeb_KbdList.sfhover')); + await waitForKeyboardSelection(page); return page.evaluate(() => { const selectedKbd = document.querySelector('#kmwico .selected'); - return selectedKbd?.textContent; + return selectedKbd?.textContent?.trim(); }); -}; +} /** * Expands the keyboard selection menu and returns the menu items as an array */ export async function getAllKeyboardMenuText(page: Page): Promise<(string|undefined)[]> { - const watchDog = page.waitForFunction(() => !!document.getElementById('KeymanWeb_KbdList')); - await page.getByRole('img', { name: 'Use Web Keyboard' }).hover(); - await watchDog; + await page.waitForFunction(() => document.getElementById('kmwico')); + await page.locator('#kmwico').hover(); + await page.waitForFunction(() => document.querySelector('#KeymanWeb_KbdList.sfhover')); return page.evaluate(() => { - const menuItems = []; + const menuItems: (string | undefined)[] = []; const menuDiv = document.querySelector('#kmwico'); const kbdList = menuDiv?.lastElementChild; for (let i = 0; i < (kbdList ? kbdList.children.length : 0); i++) { const item = kbdList?.children[i]; - menuItems.push(item?.textContent); + menuItems.push(item?.textContent?.trim()); } return menuItems; }); @@ -51,14 +81,8 @@ export async function loadPage(page: Page, url: string): Promise { * locator for the OSK title bar. */ export async function clickFieldAndWaitForOSK(page: Page, fieldLocator: Locator): Promise { - const keyboardchangePromise = page.evaluate(async () => { - return new Promise((resolve) => { - keyman.addEventListener('keyboardchange', function (kbd) { - resolve(kbd); - }); - }); - }); await fieldLocator.click(); - await keyboardchangePromise; + await waitForKeyboardSelection(page); + await page.waitForFunction(() => keyman.osk.isVisible()); return page.locator('#keymanweb_title_bar'); } diff --git a/web/src/test/auto/e2e/guide-examples.tests.ts b/web/src/test/auto/e2e/guide-examples.tests.ts index 574ddf6276e..ee449daca54 100644 --- a/web/src/test/auto/e2e/guide-examples.tests.ts +++ b/web/src/test/auto/e2e/guide-examples.tests.ts @@ -4,14 +4,35 @@ import { test, expect, type Page } from '@playwright/test'; import { clickFieldAndWaitForOSK, getAllKeyboardMenuText, getSelectedKeyboardMenuText, loadPage } from './e2eUtils'; -async function setTimeoutAndLoadPage(page: Page, url: string): Promise { +declare const keyman: any; + +async function setTimeoutAndLoadPage(page: Page, url: string, numKeyboards: number): Promise { test.setTimeout(5000); + await loadPage(page, url); + + await page.waitForFunction( + (num: number) => typeof keyman !== 'undefined' && keyman.getKeyboards().length >= num, + numKeyboards + ); + + // Now that we know that the expected number of keyboards were loaded, we can + // force trigger updateKeyboardList() instead of waiting for the timeout which + // might come in the middle of the tests + await page.evaluate(() => { + if (keyman.ui && keyman.ui.updateTimer) { + clearTimeout(keyman.ui.updateTimer); + } + keyman.ui?.updateKeyboardList(); + }); } -test.describe.skip('First example from the guide', function () { +test.describe('First example from the guide', function () { const beforeEach = async (page: Page) => { - await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__first-example.html'); + // output messages from the browser console to the test output, for debugging + page.on('console', msg => console.log(msg.text())); + + await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__first-example.html', 2); } test('Input field shows US keyboard', async ({ page } : { page: Page }) => { @@ -22,8 +43,8 @@ test.describe.skip('First example from the guide', function () { // Verify OSK shows US keyboard await expect(page.getByRole('img', { name: 'Use Web Keyboard' })).toBeVisible(); await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).toBeVisible(); - await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); - await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy(); + await expect.poll(() => page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); + await expect.poll(() => page.evaluate(() => keyman.osk.isVisible())).toBeTruthy(); await expect(oskTitleBar).toContainText('US'); await expect(await getSelectedKeyboardMenuText(page)).toBe('English - US'); @@ -41,9 +62,12 @@ test.describe.skip('First example from the guide', function () { }); }); -test.describe.skip('Auto-control example from the guide', function () { +test.describe('Auto-control example from the guide', function () { const beforeEach = async (page: Page) => { - await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__auto-control.html'); + // output messages from the browser console to the test output, for debugging + page.on('console', msg => console.log(msg.text())); + + await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__auto-control.html', 1); } test('Input field shows Lao keyboard', async ({ page } : { page: Page }) => { @@ -52,8 +76,8 @@ test.describe.skip('Auto-control example from the guide', function () { await page.getByTestId('multilingual' ).click(); // Verify OSK is shown - await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); - await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy(); + await expect.poll(() => page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); + await expect.poll(() => page.evaluate(() => keyman.osk.isVisible())).toBeTruthy(); await expect(page.locator('#keymanweb_title_bar')).toContainText('Lao (Phonetic)'); }); @@ -69,9 +93,12 @@ test.describe.skip('Auto-control example from the guide', function () { }); }); -test.describe.skip('Control-by-control example from the guide', function () { +test.describe('Control-by-control example from the guide', function () { const beforeEach = async (page: Page) => { - await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__control-by-control.html'); + // output messages from the browser console to the test output, for debugging + page.on('console', msg => console.log(msg.text())); + + await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__control-by-control.html', 6); } test('address field does not have KeymanWeb enabled', async ({ page } : { page: Page }) => { @@ -86,17 +113,17 @@ test.describe.skip('Control-by-control example from the guide', function () { await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).not.toBeVisible(); }); - // TODO: #16080 - test.skip('subject field does not show keyboard and defaults to system keyboard', async ({ page } : { page: Page }) => { + test('subject field does not show keyboard and defaults to system keyboard', async ({ page } : { page: Page }) => { // Setup await beforeEach(page); await page.getByPlaceholder('id = subject').click(); - // Verify OSK is shown + // Verify the control is in system-keyboard mode: the OSK stays hidden, + // while the toggle UI remains available for switching keyboards. await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); - await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeFalsy(); await expect(page.getByRole('img', { name: 'Use Web Keyboard' })).toBeVisible(); - await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).not.toBeVisible(); + await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' }).isHidden()).toBeTruthy(); await expect(await getSelectedKeyboardMenuText(page)).toBe('(System keyboard)'); }); @@ -119,9 +146,12 @@ test.describe.skip('Control-by-control example from the guide', function () { }); }); -test.describe.skip('Full manual control example from the guide', function () { +test.describe('Full manual control example from the guide', function () { const beforeEach = async (page: Page) => { - await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__full-manual-control.html'); + // output messages from the browser console to the test output, for debugging + page.on('console', msg => console.log(msg.text())); + + await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__full-manual-control.html', 6); } test('Shows English and no OSK after loading page', async ({ page } : { page: Page }) => { @@ -203,9 +233,12 @@ test.describe.skip('Full manual control example from the guide', function () { }); }); -test.describe.skip('Manual control example from the guide', function () { +test.describe('Manual control example from the guide', function () { const beforeEach = async (page: Page) => { - await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__manual-control.html'); + // output messages from the browser console to the test output, for debugging + page.on('console', msg => console.log(msg.text())); + + await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__manual-control.html', 1); } test('Does not show OSK after loading', async ({ page } : { page: Page }) => {