From bda4b50e2a7e7e60e8d8f232b482571eeacb310f Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 29 May 2026 19:31:00 +0200 Subject: [PATCH] test(web): add e2e tests for examples from guide Build-bot: skip build:web Test-bot: skip --- web/build.sh | 15 + .../engine/guide/examples/__auto-control.html | 4 +- .../guide/examples/__full-manual-control.html | 8 +- .../guide/examples/__manual-control.html | 6 +- .../guide/examples/full-manual-control.md | 8 +- .../engine/guide/examples/manual-control.md | 7 +- web/src/test/auto/e2e/e2eUtils.ts | 64 +++++ web/src/test/auto/e2e/guide-examples.tests.ts | 256 ++++++++++++++++++ 8 files changed, 353 insertions(+), 15 deletions(-) create mode 100644 web/src/test/auto/e2e/e2eUtils.ts create mode 100644 web/src/test/auto/e2e/guide-examples.tests.ts diff --git a/web/build.sh b/web/build.sh index ca253ab82c7..4154c6203cd 100755 --- a/web/build.sh +++ b/web/build.sh @@ -114,6 +114,21 @@ build_tests_action() { cp "${KEYMAN_ROOT}/web/src/test/auto/dom/cases/attachment/textStoreForElement.tests.html" \ "${KEYMAN_ROOT}/web/build/test/dom/cases/attachment/" + + # Copy and update guide examples - for local, PR, and test builds we + # replace the CDN URL with the local build path, so that we can test + # against the current build + mkdir -p "${KEYMAN_ROOT}/web/build/docs/engine/guide" + cp -r "${KEYMAN_ROOT}/web/docs/engine/guide/examples" \ + "${KEYMAN_ROOT}/web/build/docs/engine/guide/" + + # shellcheck disable=SC2310 + if ! builder_is_ci_release_build; then + for f in "${KEYMAN_ROOT}/web/build/docs/engine/guide/examples"/*.html; do + sed "s|https://s\.keyman\.com/kmw/engine/[0-9]*\.[0-9]*\.[0-9]*/|/build/publish/${config}/|g" \ + "${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}" + done + fi } coverage_action() { diff --git a/web/docs/engine/guide/examples/__auto-control.html b/web/docs/engine/guide/examples/__auto-control.html index c2a8b472b09..76ff91cf98d 100644 --- a/web/docs/engine/guide/examples/__auto-control.html +++ b/web/docs/engine/guide/examples/__auto-control.html @@ -19,8 +19,8 @@

Automatic Mode Example

-

-

+

+

Back to Document diff --git a/web/docs/engine/guide/examples/__full-manual-control.html b/web/docs/engine/guide/examples/__full-manual-control.html index 6313c135184..b862184ff0e 100644 --- a/web/docs/engine/guide/examples/__full-manual-control.html +++ b/web/docs/engine/guide/examples/__full-manual-control.html @@ -24,13 +24,13 @@ } document.f.multilingual.focus(); - keyman.setActiveKeyboard('', ''); + await keyman.setActiveKeyboard('', ''); }); -function KWControlChange() { +async function KWControlChange() { var name = KWControl.value.substr(0, KWControl.value.indexOf("$$")); var languageCode = KWControl.value.substr(KWControl.value.indexOf("$$") + 2); - keyman.setActiveKeyboard(name, languageCode); + await keyman.setActiveKeyboard(name, languageCode); document.f.multilingual.focus(); } @@ -40,7 +40,7 @@

Manual Control - Custom Interface

-

Keyboard:

+

diff --git a/web/docs/engine/guide/examples/__manual-control.html b/web/docs/engine/guide/examples/__manual-control.html index a1cfa72c83c..f3c1b7a80e9 100644 --- a/web/docs/engine/guide/examples/__manual-control.html +++ b/web/docs/engine/guide/examples/__manual-control.html @@ -11,7 +11,7 @@ languages: { id: 'lo', name: 'Lao' }, filename: "./js/laokeys.js" }); - keyman.setActiveKeyboard('laokeys'); + await keyman.setActiveKeyboard('laokeys'); keyman.osk.hide(); }); @@ -30,8 +30,8 @@

Manual Mode Example

KeymanWeb

-

-

+

+

Back to Document diff --git a/web/docs/engine/guide/examples/full-manual-control.md b/web/docs/engine/guide/examples/full-manual-control.md index 39be4d34b3d..98d4236f4d7 100644 --- a/web/docs/engine/guide/examples/full-manual-control.md +++ b/web/docs/engine/guide/examples/full-manual-control.md @@ -28,15 +28,15 @@ Include the following script in the HEAD of your page: } document.f.multilingual.focus(); - keyman.setActiveKeyboard('', ''); + await keyman.setActiveKeyboard('', ''); }); /* KWControlChange: Called when user selects an item in the KWControl SELECT */ - function KWControlChange() { + async function KWControlChange() { /* Select the keyboard in KeymanWeb */ var name = KWControl.value.substr(0, KWControl.value.indexOf("$$")); - var languageCode = KWControl.value.substr(KWControl.value.indexOf("$$"+2)); - keyman.setActiveKeyboard(name, languageCode); + var languageCode = KWControl.value.substr(KWControl.value.indexOf("$$") + 2); + await keyman.setActiveKeyboard(name, languageCode); /* Focus onto the multilingual field in the form */ document.f.multilingual.focus(); } diff --git a/web/docs/engine/guide/examples/manual-control.md b/web/docs/engine/guide/examples/manual-control.md index d67db736e96..86759499365 100644 --- a/web/docs/engine/guide/examples/manual-control.md +++ b/web/docs/engine/guide/examples/manual-control.md @@ -2,7 +2,10 @@ title: Manual Mode Example --- -In this example, the web page designer specifies when KeymanWeb's on-screen keyboard may be displayed on non-mobile devices. They have also specified that the LaoKeys keyboard should be activated by default. This example continues to use the KeymanWeb default interface. Please click [this link](__manual-control.html) to open the test page. +In this example, the web page designer specifies when KeymanWeb's on-screen keyboard may be +displayed on non-mobile devices. They have also specified that the LaoKeys keyboard should be +activated by default. This example continues to use the KeymanWeb default interface. Please click +[this link](__manual-control.html) to open the test page. ## Code Walkthrough @@ -17,7 +20,7 @@ Include the following script in the HEAD of your page: languages:{id:'lo',name:'Lao'}, filename: "./js/laokeys.js" }); - keyman.setActiveKeyboard('laokeys'); + await keyman.setActiveKeyboard('laokeys'); keyman.osk.hide(); }); diff --git a/web/src/test/auto/e2e/e2eUtils.ts b/web/src/test/auto/e2e/e2eUtils.ts new file mode 100644 index 00000000000..0ef45ffec00 --- /dev/null +++ b/web/src/test/auto/e2e/e2eUtils.ts @@ -0,0 +1,64 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { type Locator, type Page } from "@playwright/test"; + +/** + * 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; + return page.evaluate(() => { + const selectedKbd = document.querySelector('#kmwico .selected'); + return selectedKbd?.textContent; + }); +}; + +/** + * 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; + return page.evaluate(() => { + const menuItems = []; + 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); + } + return menuItems; + }); +} + +/** + * Loads the specified URL and waits for the page load event. + */ +export async function loadPage(page: Page, url: string): Promise { + const loadPromise = page.waitForEvent('load'); + await page.goto(url); + return loadPromise; +} + +/** + * Clicks the specified field and waits for the OSK to be shown, returning a + * 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; + 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 new file mode 100644 index 00000000000..19f2fb4bf82 --- /dev/null +++ b/web/src/test/auto/e2e/guide-examples.tests.ts @@ -0,0 +1,256 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import { test, expect, type Page } from '@playwright/test'; +import { clickFieldAndWaitForOSK, getAllKeyboardMenuText, getSelectedKeyboardMenuText, loadPage } from './e2eUtils'; + +async function setTimeoutAndLoadPage(page: Page, url: string): Promise { + test.setTimeout(5000); + await loadPage(page, url); +} + +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'); + } + + test('Input field shows US keyboard', async ({ page }) => { + // Setup + await beforeEach(page); + const oskTitleBar = await clickFieldAndWaitForOSK(page, page.getByPlaceholder('Hello World')); + + // 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(oskTitleBar).toContainText('US'); + + await expect(await getSelectedKeyboardMenuText(page)).toBe('English - US'); + }); + + test('Keyman menu has expected keyboards', async ({ page }) => { + // Setup + await beforeEach(page); + await clickFieldAndWaitForOSK(page, page.getByPlaceholder('Hello World')); + + // Verify OSK menu has expected entries + await expect(page.getByRole('img', { name: 'Use Web Keyboard' })).toBeVisible(); + await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).toBeVisible(); + await expect(await getAllKeyboardMenuText(page)).toEqual(['(System keyboard)', 'English - US', 'Thai - Thai Kedmanee Basic']) + }); +}); + +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'); + } + + test('Input field shows Lao keyboard', async ({ page }) => { + // Setup + await beforeEach(page); + 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(page.locator('#keymanweb_title_bar')).toContainText('Lao (Phonetic)'); + }); + + test('Textarea shows Lao keyboard', async ({ page }) => { + // Setup + await beforeEach(page); + await page.getByTestId('textarea').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(page.locator('#keymanweb_title_bar')).toContainText('Lao (Phonetic)'); + }); +}); + +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'); + } + + test('address field does not have KeymanWeb enabled', async ({ page }) => { + // Setup + await beforeEach(page); + await page.getByPlaceholder('id = address').click(); + + // Verify OSK is not shown + await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeFalsy(); + await expect(page.getByRole('img', { name: 'Use Web Keyboard' })).not.toBeVisible(); + 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 }) => { + // Setup + await beforeEach(page); + await page.getByPlaceholder('id = subject').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(page.getByRole('img', { name: 'Use Web Keyboard' })).toBeVisible(); + await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).not.toBeVisible(); + + await expect(await getSelectedKeyboardMenuText(page)).toBe('(System keyboard)'); + }); + + test('message body field shows Lao keyboard', async ({ page }) => { + // Setup + await beforeEach(page); + await page.getByPlaceholder('id = text').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(page.getByRole('img', { name: 'Use Web Keyboard' })).toBeVisible(); + await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).toBeVisible(); + + // Verify Lao (Phonetic) keyboard is active + await expect(page.locator('#keymanweb_title_bar')).toContainText('Lao (Phonetic)'); + // Verify "Lao - Lao (Phonetic)" is selected (bold) in the menu + await expect(await getSelectedKeyboardMenuText(page)).toBe('Lao - Lao (Phonetic)'); + }); +}); + +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'); + } + + test('Shows English and no OSK after loading page', async ({ page }) => { + // Setup + await beforeEach(page); + + // Verify 'English' selected (which has the value '') and no OSK showing + await expect(page.getByLabel('Keyboard')).toHaveValue(''); + await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).not.toBeTruthy(); + }); + + test('Selecting English keyboard shows no OSK', async ({ page }) => { + // Setup + await beforeEach(page); + // first switch to Hebrew + let keyboardchangePromise = page.evaluate(async () => { + return new Promise((resolve) => { + keyman.addEventListener('keyboardchange', function (kbd) { + resolve(kbd); + }); + }); + }); + await page.getByLabel('Keyboard').selectOption('Hebrew'); + await keyboardchangePromise; + + // then back to English + keyboardchangePromise = page.evaluate(async () => { + return new Promise((resolve) => { + keyman.addEventListener('keyboardchange', function (kbd) { + resolve(kbd); + }); + }); + }); + await page.getByLabel('Keyboard').selectOption('English'); + await keyboardchangePromise; + + // Verify no OSK showing + await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).not.toBeTruthy(); + }); + + test('Selecting Devanagari keyboard shows Devanagari OSK', async ({ page }) => { + // Setup + await beforeEach(page); + const keyboardchangePromise = page.evaluate(async () => { + return new Promise((resolve) => { + keyman.addEventListener('keyboardchange', function (kbd) { + resolve(kbd); + }); + }); + }); + await page.getByLabel('Keyboard').selectOption('Devanagari (INSCRIPT)'); + await keyboardchangePromise; + + // Verify Devanagari OSK showing + await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy(); + await expect(page.locator('#keymanweb_title_bar')).toContainText('Devanagari (INSCRIPT)'); + }); + + test('Selecting Hebrew shows Hebrew OSK', async ({ page }) => { + // Setup + await beforeEach(page); + const keyboardchangePromise = page.evaluate(async () => { + return new Promise((resolve) => { + keyman.addEventListener('keyboardchange', function (kbd) { + resolve(kbd); + }); + }); + }); + await page.getByLabel('Keyboard').selectOption('Hebrew'); + await keyboardchangePromise; + + // Verify Hebrew OSK showing + await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy(); + await expect(page.locator('#keymanweb_title_bar')).toContainText('Hebrew'); + }); +}); + +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'); + } + + test('Does not show OSK after loading', async ({ page }) => { + // Setup + await beforeEach(page); + await page.getByTestId('multilingual').click(); + + // Verify no OSK showing + await expect(await page.evaluate(() => keyman.osk.isEnabled())).not.toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).not.toBeTruthy(); + }); + + test('Shows Lao OSK after clicking button', async ({ page }) => { + // Setup + await beforeEach(page); + await page.getByAltText('KeymanWeb').click(); + await page.getByTestId('multilingual').click(); + + // Verify Lao OSK showing + await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy(); + await expect(page.locator('#keymanweb_title_bar')).toContainText('Lao'); + }); + + test('Hides Lao OSK after clicking button', async ({ page }) => { + // Setup + await beforeEach(page); + + // click button + await page.getByAltText('KeymanWeb').click(); + + // Verify Lao OSK showing + await page.getByTestId('multilingual').click(); + await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy(); + await expect(page.locator('#keymanweb_title_bar')).toContainText('Lao'); + + // Click button again to hide OSK + await page.getByAltText('KeymanWeb').click(); + + // Verify Lao OSK not showing + await page.getByTestId('multilingual').click(); + await expect(await page.evaluate(() => keyman.osk.isEnabled())).not.toBeTruthy(); + await expect(await page.evaluate(() => keyman.osk.isVisible())).not.toBeTruthy(); + + }); +}); +