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: English
+ Keyboard: English
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
-
-
+
+
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();
+
+ });
+});
+