From 2a7b278c374cbc0459ef6215c42580c49f1b023f Mon Sep 17 00:00:00 2001 From: Noel Chou Date: Thu, 19 Feb 2026 14:32:00 -0500 Subject: [PATCH] fix: BL-15923 e2e tests and temp fix for BL-15919 --- components/language-chooser/AGENTS.md | 10 + .../find-language/languageSearch.spec.ts | 6 +- .../language-chooser-react-hook/README.md | 2 +- .../useLanguageChooser.ts | 20 +- .../language-chooser-react-mui/README.md | 5 +- .../e2e/cardSelection.e2e.ts | 82 +++- .../e2e/customizationDialog.e2e.ts | 201 +++++++--- .../e2e/e2eHelpers.ts | 32 +- .../e2e/initialData.e2e.ts | 358 ++++++++++++++++++ .../e2e/keyboardNavigation.e2e.ts | 212 +++++++++++ .../e2e/languageCardDisplay.e2e.ts | 164 ++++++++ .../e2e/languageSearch.e2e.ts | 204 ++++++++++ .../e2e/languageTag.e2e.ts | 155 ++++++++ .../e2e/localization.e2e.ts | 58 +++ .../e2e/rightPanel.e2e.ts | 100 +++++ .../e2e/tooltips.e2e.ts | 134 +++++++ .../src/LanguageCard.tsx | 30 +- .../src/LanguageChooser.tsx | 6 +- .../src/demos/DialogDemo.tsx | 12 +- .../language-chooser-react-mui/src/main.tsx | 15 +- 20 files changed, 1714 insertions(+), 92 deletions(-) create mode 100644 components/language-chooser/AGENTS.md create mode 100644 components/language-chooser/react/language-chooser-react-mui/e2e/initialData.e2e.ts create mode 100644 components/language-chooser/react/language-chooser-react-mui/e2e/keyboardNavigation.e2e.ts create mode 100644 components/language-chooser/react/language-chooser-react-mui/e2e/languageCardDisplay.e2e.ts create mode 100644 components/language-chooser/react/language-chooser-react-mui/e2e/localization.e2e.ts create mode 100644 components/language-chooser/react/language-chooser-react-mui/e2e/rightPanel.e2e.ts create mode 100644 components/language-chooser/react/language-chooser-react-mui/e2e/tooltips.e2e.ts diff --git a/components/language-chooser/AGENTS.md b/components/language-chooser/AGENTS.md new file mode 100644 index 00000000..5896cfb2 --- /dev/null +++ b/components/language-chooser/AGENTS.md @@ -0,0 +1,10 @@ +## Tests (Unit tests and E2E tests) + +- If you can't figure out why a test is failing, stop and inform the programmer. Never change the objectives nor expected outcomes of a test unless explicitly instructed to. +- There are several special case languages, handled in searchResultModifiers.ts and/or called out in the Anomalies and Special Situations section of macrolanguageNotes.md. Be sure not to use any of these languages in general-case tests. Whenever adding tests, also test these special cases if appropriate. +- Avoid running the same exact scenario multiple times. If one test scenario is useful for testing multiple behaviors/outcomes/properties, just run it once and list all of those behaviors in the description. +- Be careful to prevent falsely passing tests. Do sanity checks. +- In e2e tests, be careful to avoid adding tests for behavior which is already covered by tests in a different file. +- You can trust that libraries behave the way they are supposed to; don't write tests for behavior which is entirely handled by a library. E.g. no need to test that MUI components are behaving as specified. +- When adding tests, make sure they are in a test suite where they belong +- When tests involve language searching, search results may come back in batches with delays in between, and lazyload. So the immediate absence of a certain result is not enough to verify the absence of that result. And when checking for a result or checking the count of results, do something that will wait for the results to appear. diff --git a/components/language-chooser/common/find-language/languageSearch.spec.ts b/components/language-chooser/common/find-language/languageSearch.spec.ts index 5a7955bc..2512d786 100644 --- a/components/language-chooser/common/find-language/languageSearch.spec.ts +++ b/components/language-chooser/common/find-language/languageSearch.spec.ts @@ -8,10 +8,8 @@ import { describe, expect, it } from "vitest"; import { expectTypeOf } from "vitest"; import { codeMatches } from "./languageTagUtils"; import { stripDemarcation } from "./matchingSubstringDemarcation"; -import { - defaultSearchResultModifier, - LanguageSearcher, -} from "@ethnolib/find-language"; +import { defaultSearchResultModifier } from "./searchResultModifiers"; +import { LanguageSearcher } from "./languageSearcher"; // wait for all the results from asyncSearchForLanguage so we can check them export async function asyncGetAllLanguageResults( diff --git a/components/language-chooser/react/common/language-chooser-react-hook/README.md b/components/language-chooser/react/common/language-chooser-react-hook/README.md index bc80bb33..fe65e90f 100644 --- a/components/language-chooser/react/common/language-chooser-react-hook/README.md +++ b/components/language-chooser/react/common/language-chooser-react-hook/README.md @@ -46,7 +46,7 @@ Install with npm: `npm i @ethnolib/language-chooser-react-hook` - `saveLanguageDetails: (details: ICustomizableLanguageDetails, script: IScript | undefined) => void` - Sets `customizableLanguageDetails` and `selectedScript`. -- `resetTo: (searchString: string, selectionLanguageTag?: string, initialCustomDisplayName?: string) => void` - For restoring preexisting data when the LanguageChooser is first opened. Sets `selectedLanguage`, `selectedScript`, and `customizableLanguageDetails` to the values in `initialState`. +- `resetTo: (searchString?: string, selectionLanguageTag?: string, initialCustomDisplayName?: string) => void` - For restoring preexisting data when the LanguageChooser is first opened. Sets `selectedLanguage`, `selectedScript`, and `customizableLanguageDetails` to the values in `initialState`. ### Example diff --git a/components/language-chooser/react/common/language-chooser-react-hook/useLanguageChooser.ts b/components/language-chooser/react/common/language-chooser-react-hook/useLanguageChooser.ts index 1e811e81..d8aaa02c 100644 --- a/components/language-chooser/react/common/language-chooser-react-hook/useLanguageChooser.ts +++ b/components/language-chooser/react/common/language-chooser-react-hook/useLanguageChooser.ts @@ -38,7 +38,7 @@ export interface ILanguageChooser { script: IScript | undefined ) => void; resetTo: ( - searchString: string, + searchString?: string, selectionLanguageTag?: string, initialCustomDisplayName?: string ) => void; @@ -114,17 +114,16 @@ export const useLanguageChooser = ( })(); }, [searchString]); - // For reopening to a specific selection. We should then also set the search string - // such that the selected language is visible. + // For reopening to a specific selection function resetTo( - searchString: string, - // the language in selectionLanguageTag must be a result of this search string or selection won't display - // unless it is a manually entered tag, in which case there is never a search result anyway + searchString?: string, selectionLanguageTag?: string, initialCustomDisplayName?: string // all info can be captured in language tag except display name ) { - onSearchStringChange(searchString); - if (!selectionLanguageTag) return; + if (!selectionLanguageTag) { + onSearchStringChange(searchString || ""); + return; + } let initialSelections = parseLangtagFromLangChooser( selectionLanguageTag || "", @@ -140,6 +139,11 @@ export const useLanguageChooser = ( }, }; } + // If we have an initially selected language but no search string, might as well set the search string to something + // that will definitely show that selected language in the results + searchString = searchString || initialSelections?.language?.languageSubtag; + onSearchStringChange(searchString || ""); + if (initialSelections?.language) { selectLanguage(initialSelections?.language as ILanguage); } diff --git a/components/language-chooser/react/language-chooser-react-mui/README.md b/components/language-chooser/react/language-chooser-react-mui/README.md index e2bd6e0c..aff4ae50 100644 --- a/components/language-chooser/react/language-chooser-react-mui/README.md +++ b/components/language-chooser/react/language-chooser-react-mui/README.md @@ -31,15 +31,12 @@ The Language Chooser will adopt the primary color of the [MUI theme](https://mui results: FuseResult[], searchString: string ) => ILanguage[]` - Can be used to add, remove, and modify results. See [find-language](../../common/find-language/README.md) for details. -- `initialSearchString?: string` +- `initialSearchString?: string` **Currently (February 2026) the initialSearchString will not be displayed in the search bar, but the results shown will reflect it.** - `initialSelectionLanguageTag?: string` - The Language Chooser will open with the language information captured by this language tag being already selected. If the user has already previously selected a language and is using the LanguageChooser to modify their selection, use this to prepopulate with their preexisting selection. - - We expect this to be a language tag which was output either by this Language Chooser or by the [libPalasso](https://github.com/sillsdev/libpalaso)-based language picker. **The language subtag must be the default language subtag for the language** (the first part of the "tag" field of langtags.json), which may be a 2-letter code even if an equivalent ISO 639-3 code exists. May not corectly handle irregular codes, extension codes, langtags with both macrolanguage code and language code, and other comparably unusual nuances of BCP-47. - If the initialSelectionLanguageTag does not have an explicit script subtag, the Language Chooser will select the script implied by the language subtag and region subtag if present. For example, if initialSelectionLanguageTag is "uz" (Uzbek), Latin script will be selected because "uz" is an equivalent language tag to "uz-Latn". If initialSelectionLanguageTag is "uz-AF" (Uzbek, Afghanistan), Arabic script will be selected because "uz-AF" is an equivalent language tag to "uz-Arab-AF". - - **If an initialSelectionLanguageTag is provided, an initialSearchString must also be provided such that the initially selected language is a result of the search string in order for the selected card to be visible.** - - `initialCustomDisplayName?: string` - If using initialSelectionLanguageTag to prepopulate with a language, this field can be used to prepopulate a customized display name for the language. - `onSelectionChange?: (orthographyInfo: IOrthography | undefined, languageTag: string | undefined) => void` - Whenever the user makes or unselects a complete language selection, this function will be called with the selected language information or undefined, respectively. - `rightPanelComponent?: React.ReactNode` - A slot for a component to go in the space on the upper right side of the language chooser. See the Storybook Dialog Demo -> Additional Right Panel Component for an example. diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/cardSelection.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/cardSelection.e2e.ts index a4e4a4bc..8c7747ea 100644 --- a/components/language-chooser/react/language-chooser-react-mui/e2e/cardSelection.e2e.ts +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/cardSelection.e2e.ts @@ -6,11 +6,13 @@ import { findChechenCyrlCard, findChechenCard, scriptCardTestId, + search, + selectChechenCard, } from "./e2eHelpers"; let page: Page; // All the tests in this file use the same page object to save time; we only load the language chooser once. -test.describe("Selection toggle script card behavior", () => { +test.describe("Selection toggle card behavior", () => { test.beforeAll(async ({ browser }) => { page = await createPageAndLoadLanguageChooser(browser); }); @@ -54,4 +56,82 @@ test.describe("Selection toggle script card behavior", () => { await page.locator("#search-bar").fill("uzbek "); await expect(cyrlCard).not.toBeVisible(); }); + + test("Rapid clicking doesn't break selection state", async () => { + await page.locator("#search-bar").fill("chechen"); + + const chechenCard = page.getByTestId("language-card-che"); + + // Click rapidly multiple times + await chechenCard.click(); + await chechenCard.click(); + await chechenCard.click(); + await chechenCard.click(); + + // Wait a moment for state to settle + await page.waitForTimeout(300); + + // Card should be in a consistent state (unselected after even number of clicks) + const cardButton = page.locator( + `button:has([data-testid="language-card-che"])` + ); + + // Should be unselected (4 clicks = toggled 4 times) + await expect(cardButton).not.toHaveClass(/.*selected-option-card-button.*/); + }); + + test("Toggling script cards multiple times maintains consistency", async () => { + await selectChechenCard(page); + + const cyrlCard = page.getByTestId("script-card-Cyrl"); + const cardButton = page.locator( + `button:has([data-testid="script-card-Cyrl"])` + ); + + // Toggle on + await cyrlCard.click(); + await expect(cardButton).toHaveClass(/.*selected-option-card-button.*/); + + // Toggle off + await cyrlCard.click(); + await expect(cardButton).not.toHaveClass(/.*selected-option-card-button.*/); + + // Toggle on again + await cyrlCard.click(); + await expect(cardButton).toHaveClass(/.*selected-option-card-button.*/); + + // Toggle off again + await cyrlCard.click(); + await expect(cardButton).not.toHaveClass(/.*selected-option-card-button.*/); + + // Final state should be consistent + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("ce"); + await expect(tagPreview).not.toContainText("ce-Cyrl"); + }); + + test("Selected language card scrolls to top when clicked", async () => { + await search(page, "zebra"); + const cards = page.locator(".option-card-button"); + const thirdCard = cards.nth(2); + const initialThirdCardPosition = await thirdCard.evaluate( + (el) => el.getBoundingClientRect().top + ); + + // without any scrolling, third card should be visible but lower down without scrolling + await expect(thirdCard).toBeVisible(); + // click on third card, which should scroll it to top + await thirdCard.click(); + // Wait a bit for smooth scroll + await page.waitForTimeout(500); + + const newThirdCardPosition = await thirdCard.evaluate( + (el) => el.getBoundingClientRect().top + ); + + // the third card should have moved up by more than 100px + await expect(newThirdCardPosition).toBeLessThan( + initialThirdCardPosition - 100 + ); + }); }); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/customizationDialog.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/customizationDialog.e2e.ts index 60409b48..01c0c24c 100644 --- a/components/language-chooser/react/language-chooser-react-mui/e2e/customizationDialog.e2e.ts +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/customizationDialog.e2e.ts @@ -11,20 +11,16 @@ import { scriptCardTestId, search, selectChechenCard, + customizationDialogLocator, + cancelButtonLocator, + cleanupDialogHandlers, + resetBeforeEach, } from "./e2eHelpers"; -function customizationDialogLocator(page) { - return page.getByTestId("customization-dialog"); -} - function okButtonLocator(customizationDialog) { return customizationDialog.getByRole("button", { name: "OK" }); } -function cancelButtonLocator(customizationDialog) { - return customizationDialog.getByRole("button", { name: "Cancel" }); -} - function scriptFieldLocator(customizationDialog) { return customizationDialog.locator("#customize-script-field"); } @@ -71,34 +67,17 @@ function enterName(customizationDialog, name) { let page: Page; // All the tests in this file use the same page object to save time; we only reload the language chooser when necessary -async function cleanupDialogHandlers() { - await page.removeAllListeners("dialog"); -} - -async function resetBeforeEach() { - // In case some got left for some reason - await cleanupDialogHandlers(); - - // close customization dialog if open - const customizationDialog = await customizationDialogLocator(page); - if (await customizationDialog.isVisible()) { - await cancelButtonLocator(customizationDialog).click(); - } - // clear search - await clearSearch(page); -} - test.describe("Customization button and dialog", () => { test.beforeAll(async ({ browser }) => { page = await createPageAndLoadLanguageChooser(browser); }); test.beforeEach(async () => { - await resetBeforeEach(); + await resetBeforeEach(page); }); test.afterEach(async () => { - await cleanupDialogHandlers(); + await cleanupDialogHandlers(page); }); test("no language selected, button should say 'Create Unlisted Language'", async () => { @@ -137,19 +116,16 @@ test.describe("Customization button and dialog", () => { }); test("if manually entered language tag, clicking button should open windows prompt to edit the tag", async () => { - await clickCustomizationButton(page); - const customizationDialogTagPreview = await page.getByTestId( - "customization-dialog-tag-preview" - ); - await expect(customizationDialogTagPreview).toBeVisible(); + await manuallyEnterValidLanguageTag(page, "zzz-Foo-x-barbaz"); + const dialogHandled = page.waitForEvent("dialog").then(async (dialog) => { - expect(dialog.type()).toBe("prompt"); - expect(dialog.message()).toMatch( + await expect(dialog.type()).toBe("prompt"); + await expect(dialog.message()).toMatch( /.*If this user interface is not offering you a code that you know is valid ISO 639 code, you can enter it here.*/ ); await dialog.dismiss(); }); - await customizationDialogTagPreview.click({ modifiers: ["Control"] }); + await clickCustomizationButton(page); await dialogHandled; }); @@ -322,25 +298,25 @@ test.describe("Customization button and dialog", () => { const customizationDialog = await customizationDialogLocator(page); await expect(customizationDialog).toBeVisible(); const okButton = okButtonLocator(customizationDialog); - expect(okButton).toBeVisible(); + await expect(okButton).toBeVisible(); // nothing filled, ok button disabled - expect(okButton).toBeDisabled(); + await expect(okButton).toBeDisabled(); // fill in name, ok button still disabled await enterName(customizationDialog, "foo"); - expect(okButton).toBeDisabled(); + await expect(okButton).toBeDisabled(); // fill in country but clear name, ok button still disabled await enterRegion(customizationDialog, "Austria"); await enterName(customizationDialog, ""); - expect(okButton).toBeDisabled(); + await expect(okButton).toBeDisabled(); // invalid name should keep ok button disabled await enterName(customizationDialog, "!!!"); - expect(okButton).toBeDisabled(); + await expect(okButton).toBeDisabled(); // fill in name and country, ok button enabled await enterName(customizationDialog, "foo"); - expect(okButton).not.toBeDisabled(); + await expect(okButton).not.toBeDisabled(); // the word "required" should appear in labels twice, once for name and once for country await expect( @@ -364,6 +340,64 @@ test.describe("Customization button and dialog", () => { await expect(customizationDialog).toBeVisible(); await expect(customizationDialog.getByLabel("Name")).toHaveValue("foo"); }); + + test("Variant field accepts and formats text correctly", async () => { + await selectChechenCard(page); + await clickCustomizationButton(page); + + const dialog = customizationDialogLocator(page); + const variantField = dialog.locator("#customize-variant-field"); + + // Enter variant with spaces and special characters + await variantField.fill("test 123!!! foobar"); + + // Check dialog tag preview - should format it + const dialogTagPreview = dialog.getByTestId( + "customization-dialog-tag-preview" + ); + + // Should remove spaces and invalid chars and truncate + await expect(dialogTagPreview).toContainText("ce-x-test123f"); + }); + + test("Live BCP 47 tag preview updates as fields change in dialog", async () => { + await selectChechenCard(page); + await clickCustomizationButton(page); + + const dialog = customizationDialogLocator(page); + const dialogTagPreview = dialog.getByTestId( + "customization-dialog-tag-preview" + ); + + // Initially just "ce" + await expect(dialogTagPreview).toContainText("ce"); + + // Add script + await dialog + .locator("#customize-script-field-wrapper") + .getByLabel("Open") + .click(); + await page + .getByRole("option", { name: /Arabic/ }) + .first() + .click(); + + await expect(dialogTagPreview).toContainText("ce-Arab"); + + // Add region + await dialog + .locator("#customize-region-field-wrapper") + .getByLabel("Open") + .click(); + await page.getByRole("option", { name: /India/ }).first().click(); + + await expect(dialogTagPreview).toContainText("ce-Arab-IN"); + + // Add variant + await dialog.locator("#customize-variant-field").fill("dialectTest"); + + await expect(dialogTagPreview).toContainText("ce-Arab-IN-x-dialectT"); + }); }); test.describe("Manually entered language tag behavior", () => { @@ -372,28 +406,70 @@ test.describe("Manually entered language tag behavior", () => { }); test.beforeEach(async () => { - await resetBeforeEach(); + await resetBeforeEach(page); }); test.afterEach(async () => { - await cleanupDialogHandlers(); + await cleanupDialogHandlers(page); }); - test("invalid manually entered tag gets rejected, leaves selection untouched", async () => { + test("invalid manually entered tag gets rejected, shows alert, and leaves selection untouched", async () => { await selectChechenCard(page); await clickCustomizationButton(page); - const customizationDialogTagPreview = await page.getByTestId( + const customizationDialog = page.getByTestId("customization-dialog"); + const customizationDialogTagPreview = customizationDialog.getByTestId( "customization-dialog-tag-preview" ); const chechenTag = "ce"; await expect(customizationDialogTagPreview).toContainText(chechenTag); - // clicking the tag preview will trigger a windows.prompt dialog, enter an invalid tag - const dialogHandled = page - .waitForEvent("dialog") - .then((dialog) => dialog.accept("invalid-tag!")); + + // Track dialogs and create promises for each + let promptSeen = false; + let alertSeen = false; + let alertMessage = ""; + let resolvePrompt: () => void; + let resolveAlert: () => void; + + const promptPromise = new Promise((resolve) => { + resolvePrompt = resolve; + }); + const alertPromise = new Promise((resolve) => { + resolveAlert = resolve; + }); + + // Set up a general dialog handler + const dialogHandler = async (dialog) => { + if (dialog.type() === "prompt" && !promptSeen) { + promptSeen = true; + await dialog.accept("invalid-tag!@#$"); + resolvePrompt(); + } else if (dialog.type() === "alert" && !alertSeen) { + alertSeen = true; + alertMessage = dialog.message(); + await dialog.accept(); + resolveAlert(); + } + }; + + page.on("dialog", dialogHandler); + + // Click to trigger the dialogs await customizationDialogTagPreview.click({ modifiers: ["Control"] }); - await dialogHandled; + + // Wait for both dialogs to have been handled + await promptPromise; + await alertPromise; + + // Clean up the handler + page.off("dialog", dialogHandler); + + // Check that we got an alert about invalid format + await expect(alertMessage).toContain( + "This is not in a valid IETF BCP 47 format" + ); + // Dialog should still be up with chechen tag + await expect(customizationDialog).toBeVisible(); await expect(customizationDialogTagPreview).toContainText(chechenTag); }); @@ -412,4 +488,29 @@ test.describe("Manually entered language tag behavior", () => { await expect(page.locator("#search-bar")).toBeEmpty(); await expect(page.locator(".option-card-button")).not.toBeVisible(); }); + + test("Empty manual tag entry dismissed gracefully", async () => { + await selectChechenCard(page); + await clickCustomizationButton(page); + + const customizationDialog = page.getByTestId("customization-dialog"); + const tagPreview = customizationDialog.getByTestId( + "customization-dialog-tag-preview" + ); + + // Handle the prompt by entering empty string + const dialogHandled = page.waitForEvent("dialog").then(async (dialog) => { + await expect(dialog.type()).toBe("prompt"); + await dialog.accept(""); + }); + + await tagPreview.click({ modifiers: ["Control"] }); + await dialogHandled; + + // Dialog should still be open (empty input doesn't crash) + await expect(customizationDialog).toBeVisible(); + + // Original tag should still be there + await expect(tagPreview).toContainText("ce"); + }); }); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/e2eHelpers.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/e2eHelpers.ts index a7a665ab..be064bda 100644 --- a/components/language-chooser/react/language-chooser-react-mui/e2e/e2eHelpers.ts +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/e2eHelpers.ts @@ -19,7 +19,8 @@ export function languageCardTestId(languageCode: string) { } export async function clearSearch(page) { - await page.getByTestId("clear-search-X-button").click(); + const clearButton = page.getByTestId("clear-search-X-button"); + await clearButton.click(); } export async function search(page, searchString: string) { @@ -91,8 +92,8 @@ export async function manuallyEnterValidLanguageTag(page, tag) { ); await expect(customizationDialogTagPreview).toBeVisible(); // clicking the tag preview will trigger a windows.prompt dialog, enter tag into it - const dialogHandled = page.waitForEvent("dialog").then((dialog) => { - expect(dialog.type()).toBe("prompt"); + const dialogHandled = page.waitForEvent("dialog").then(async (dialog) => { + await expect(dialog.type()).toBe("prompt"); return dialog.accept(tag); }); await customizationDialogTagPreview.click({ modifiers: ["Control"] }); @@ -100,3 +101,28 @@ export async function manuallyEnterValidLanguageTag(page, tag) { // Check that the tag was accepted. Not a robust check, but see that it is at least visible somewhere await expect(page.getByText(/.*tag.*/)).toBeVisible(); } + +export function customizationDialogLocator(page) { + return page.getByTestId("customization-dialog"); +} + +export function cancelButtonLocator(customizationDialog) { + return customizationDialog.getByRole("button", { name: "Cancel" }); +} + +export async function cleanupDialogHandlers(page) { + await page.removeAllListeners("dialog"); +} + +export async function resetBeforeEach(page) { + // In case some got left for some reason + await cleanupDialogHandlers(page); + + // close customization dialog if open + const customizationDialog = await customizationDialogLocator(page); + if (await customizationDialog.isVisible()) { + await cancelButtonLocator(customizationDialog).click(); + } + // clear search + await clearSearch(page); +} diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/initialData.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/initialData.e2e.ts new file mode 100644 index 00000000..d2bc1787 --- /dev/null +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/initialData.e2e.ts @@ -0,0 +1,358 @@ +import { test, expect } from "@playwright/test"; +import { + languageCardTestId, + scriptCardTestId, + customizationDialogLocator, + clickCustomizationButton, +} from "./e2eHelpers"; + +async function createPageWithParams(browser, params: Record) { + const page = await browser.newPage(); + const queryString = new URLSearchParams(params).toString(); + await page.goto(`/?${queryString}`, { waitUntil: "load" }); + return page; +} + +test.describe("Initial data via URL parameters", () => { + let page; + + test.afterEach(async () => { + if (page) { + await page.close(); + page = undefined; + } + }); + + test("initialLanguageTag with language only selects the language card", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "jpn", + }); + + // Check that the Japanese language card is selected + const japaneseCardButton = page.locator( + `button:has([data-testid="${languageCardTestId("jpn")}"])` + ); + await expect(japaneseCardButton).toBeVisible(); + await expect(japaneseCardButton).toHaveClass( + /.*selected-option-card-button.*/ + ); + + // Verify tag preview shows "ja" + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toBeVisible(); + await expect(tagPreview).toContainText("ja"); + }); + + test("initialLanguageTag with explicit script selects the language and script cards", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "sr-Latn", // Serbian with Latin (Cyrillic is default) + }); + + // Check that Serbian language card is selected + const serbianCardButton = page.locator( + `button:has([data-testid="${languageCardTestId("srp")}"])` + ); + await expect(serbianCardButton).toBeVisible(); + await expect(serbianCardButton).toHaveClass( + /.*selected-option-card-button.*/ + ); + + // Check that Latin script card is selected + const latnCardButton = page.locator( + `button:has([data-testid="${scriptCardTestId("Latn")}"])` + ); + await expect(latnCardButton).toBeVisible(); + await expect(latnCardButton).toHaveClass(/.*selected-option-card-button.*/); + + // Verify tag preview shows "sr-Latn" + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("sr-Latn"); + }); + + test("initialLanguageTag with implied script selects language and script (ce implies Cyrl)", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "ce", // Chechen, Cyrillic is the default/suppressed script + }); + + // Check that Chechen language card is selected + const chechenCardButton = page.locator( + `button:has([data-testid="${languageCardTestId("che")}"])` + ); + await expect(chechenCardButton).toBeVisible(); + await expect(chechenCardButton).toHaveClass( + /.*selected-option-card-button.*/ + ); + + // Check that Cyrillic script card is visible and selected (implied script) + const cyrlCardButton = page.locator( + `button:has([data-testid="${scriptCardTestId("Cyrl")}"])` + ); + await expect(cyrlCardButton).toBeVisible(); + await expect(cyrlCardButton).toHaveClass(/.*selected-option-card-button.*/); + + // Verify tag preview shows "ce" (script is suppressed) + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("ce"); + await expect(tagPreview).not.toContainText("ce-Cyrl"); + }); + + test("initialLanguageTag with implied script selects language and script (uzn-AF implies Arab)", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "uzn-AF", + }); + + // Check that Northern Uzbek language card is selected + const uzbCardButton = page.locator( + `button:has([data-testid="${languageCardTestId("uzn")}"])` + ); + await expect(uzbCardButton).toBeVisible(); + await expect(uzbCardButton).toHaveClass(/.*selected-option-card-button.*/); + + // Check that Arabic script card is selected + const arabCardButton = page.locator( + `button:has([data-testid="${scriptCardTestId("Arab")}"])` + ); + await expect(arabCardButton).toBeVisible(); + await expect(arabCardButton).toHaveClass(/.*selected-option-card-button.*/); + + // Verify tag preview + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("uzn-AF"); + }); + + test("initialLanguageTag with region populates region field", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "en-GB", + }); + + // Check that English language card is selected + const englishCardButton = page.locator( + `button:has([data-testid="${languageCardTestId("eng")}"])` + ); + await expect(englishCardButton).toBeVisible(); + await expect(englishCardButton).toHaveClass( + /.*selected-option-card-button.*/ + ); + + // Open customization dialog to check region + const customizeButton = page.getByTestId("customization-button"); + await customizeButton.click(); + + const customizationDialog = customizationDialogLocator(page); + await expect(customizationDialog).toBeVisible(); + + // Check that region field has "GB" or "United Kingdom" + const regionField = customizationDialog.locator("#customize-region-field"); + await expect(regionField).toHaveValue(/GB|United Kingdom/); + + // Verify tag preview shows "en-GB" + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("en-GB"); + }); + + test("initialLanguageTag with variant populates variant field", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "sr-Latn-x-foobar", + }); + + // Check that Serbian language card is selected + const serbianCardButton = page.locator( + `button:has([data-testid="${languageCardTestId("srp")}"])` + ); + await expect(serbianCardButton).toBeVisible(); + await expect(serbianCardButton).toHaveClass( + /.*selected-option-card-button.*/ + ); + + // Open customization dialog to check variant + const customizeButton = page.getByTestId("customization-button"); + await customizeButton.click(); + + const customizationDialog = customizationDialogLocator(page); + await expect(customizationDialog).toBeVisible(); + + // Check that variant field has "foobar" + const variantField = customizationDialog.locator( + "#customize-variant-field" + ); + await expect(variantField).toHaveValue("foobar"); + + // Verify tag preview shows the variant + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("sr-Latn-x-foobar"); + }); + + test("initialLanguageTag with complex tag (script, region, variant)", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "uzn-Cyrl-AT-x-dialect1", + }); + + // Check that Northern Uzbek language card is selected + const uzbCardButton = page.locator( + `button:has([data-testid="${languageCardTestId("uzn")}"])` + ); + await expect(uzbCardButton).toBeVisible(); + await expect(uzbCardButton).toHaveClass(/.*selected-option-card-button.*/); + + // Check that Cyrillic script card is selected + const cyrlCardButton = page.locator( + `button:has([data-testid="${scriptCardTestId("Cyrl")}"])` + ); + await expect(cyrlCardButton).toBeVisible(); + await expect(cyrlCardButton).toHaveClass(/.*selected-option-card-button.*/); + + // Open customization dialog to check region and variant + const customizeButton = page.getByTestId("customization-button"); + await customizeButton.click(); + + const customizationDialog = customizationDialogLocator(page); + await expect(customizationDialog).toBeVisible(); + + // Check region field + const regionField = customizationDialog.locator("#customize-region-field"); + await expect(regionField).toHaveValue(/AT|Austria/); + + // Check variant field + const variantField = customizationDialog.locator( + "#customize-variant-field" + ); + await expect(variantField).toHaveValue("dialect1"); + + // Verify tag preview + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("uzn-Cyrl-AT-x-dialect1"); + }); + + test("initialCustomDisplayName sets the display name field", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "jpn", + initialCustomDisplayName: "My Custom Japanese Name", + }); + + // Check that display name field has the custom name + const displayNameField = page.locator("#language-name-bar"); + await expect(displayNameField).toBeVisible(); + await expect(displayNameField).toHaveValue("My Custom Japanese Name"); + }); + + test("invalid initialLanguageTag is handled gracefully", async ({ + browser, + }) => { + const invalidTag = "&&&&&&&&&&&&&&&¬-a-valid-tag-12345"; + page = await createPageWithParams(browser, { + initialLanguageTag: invalidTag, + }); + + // Application should load without errors + await expect(page.getByText("Choose Language")).toBeVisible(); + + // No language card should be selected + const selectedCards = page.locator( + 'button[class*="selected-option-card-button"]' + ); + await expect(selectedCards).toHaveCount(0); + + // ok button should be disabled + const okButton = page.getByTestId("lang-chooser-dialog-ok-button"); + await expect(okButton).toBeDisabled(); + }); + + test("invalid script in initialLanguageTag is handled gracefully", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "eng-Xyzt", // Invalid script code + }); + + // Application should load + await expect(page.getByText("Choose Language")).toBeVisible(); + }); + + test("all URL parameters combined work together", async ({ browser }) => { + page = await createPageWithParams(browser, { + uiLanguage: "en", + initialLanguageTag: "sr-Latn-US", + initialSearchString: "serbian", + initialCustomDisplayName: "My Serbian Variant", + }); + + // Check language selection + const serbianCardButton = page.locator( + `button:has([data-testid="${languageCardTestId("srp")}"])` + ); + await expect(serbianCardButton).toBeVisible(); + await expect(serbianCardButton).toHaveClass( + /.*selected-option-card-button.*/ + ); + + // Check script selection + const latnCardButton = page.locator( + `button:has([data-testid="${scriptCardTestId("Latn")}"])` + ); + await expect(latnCardButton).toBeVisible(); + await expect(latnCardButton).toHaveClass(/.*selected-option-card-button.*/); + + // Check display name + const displayNameField = page.locator("#language-name-bar"); + await expect(displayNameField).toHaveValue("My Serbian Variant"); + + // Check region in customization dialog + const customizeButton = page.getByTestId("customization-button"); + await customizeButton.click(); + + const customizationDialog = customizationDialogLocator(page); + const regionField = customizationDialog.locator("#customize-region-field"); + await expect(regionField).toHaveValue(/US|United States/); + + // Verify tag preview + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("sr-Latn-US"); + }); + + test("initialLanguageTag with manually entered language tag", async ({ + browser, + }) => { + page = await createPageWithParams(browser, { + initialLanguageTag: "zzz-Foo-x-barbaz", + }); + + // Application should load without errors + await expect(page.getByText("Choose Language")).toBeVisible(); + + // Verify tag preview shows the manually entered tag + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toBeVisible(); + await expect(tagPreview).toContainText("zzz-Foo-x-barbaz"); + + // Verify customization button says "Edit Language Tag" for manually entered tags + const customizeButton = page.getByTestId("customization-button"); + await expect(customizeButton).toHaveText(/Edit Language Tag.*/); + + // Open customization dialog + const dialogHandled = page.waitForEvent("dialog").then(async (dialog) => { + await expect(dialog.type()).toBe("prompt"); + await expect(dialog.message()).toMatch( + /.*If this user interface is not offering you a code that you know is valid ISO 639 code, you can enter it here.*/ + ); + await dialog.dismiss(); + }); + await clickCustomizationButton(page); + await dialogHandled; + }); +}); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/keyboardNavigation.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/keyboardNavigation.e2e.ts new file mode 100644 index 00000000..f651d502 --- /dev/null +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/keyboardNavigation.e2e.ts @@ -0,0 +1,212 @@ +import { test, expect, Page } from "@playwright/test"; +import { + clickCustomizationButton, + createPageAndLoadLanguageChooser, + languageCardTestId, + search, + selectChechenCard, + resetBeforeEach, + cleanupDialogHandlers, +} from "./e2eHelpers"; + +let page: Page; + +async function tabToElement( + page: Page, + checkElement: (element: Element) => boolean, + maxTries: number, + elementDescription: string +): Promise { + for (let i = 0; i < maxTries; i++) { + await page.keyboard.press("Tab"); + + // Get the currently focused element and check with the function + const focused = await page.evaluateHandle(() => document.activeElement); + const element = await focused.asElement(); + if (element) { + const meetsCriteria = await element.evaluate(checkElement); + if (meetsCriteria) { + return; + } + } + } + throw new Error( + `${elementDescription} was not reachable via Tab key within ${maxTries} attempts` + ); +} + +test.describe("initial focus", () => { + test.beforeAll(async ({ browser }) => { + page = await createPageAndLoadLanguageChooser(browser); + }); + test("Search bar receives focus on mount", async () => { + // Reload the page + await page.reload(); + await page.waitForLoadState("load"); + + const searchBar = page.locator("#search-bar"); + + // Search bar should be focused + await expect(searchBar).toBeFocused(); + + // sanity check expect clear button not to be focused + const clearButton = page.getByTestId("clear-search-X-button"); + await expect(clearButton).toBeVisible(); + await expect(clearButton).not.toBeFocused(); + }); +}); + +test.describe("Keyboard navigation", () => { + test.beforeAll(async ({ browser }) => { + page = await createPageAndLoadLanguageChooser(browser); + }); + + test.beforeEach(async () => { + await resetBeforeEach(page); + }); + + test.afterEach(async () => { + await cleanupDialogHandlers(page); + }); + + test("Tab and enter keys search and selection", async () => { + await search(page, "chechen"); + const chechenCard = page.getByTestId(languageCardTestId("che")); + await expect(chechenCard).toBeVisible(); + + // Search should be focused initially + const searchBar = page.locator("#search-bar"); + await searchBar.focus(); + await expect(searchBar).toBeFocused(); + + // Press Tab to move to next element + await page.keyboard.press("Tab"); + + // Should move to the clear button + const clearButton = page.getByTestId("clear-search-X-button"); + const isClearButtonFocused = await clearButton.evaluate((el) => + el.matches(":focus") + ); + expect(isClearButtonFocused).toBe(true); + + // Enhance: Fix up the check for the Chechen card receiving focus. Currently it takes one tab to get from the + // clear button to the first card, but we should make the test more flexible/robust against that changing. But right now + // it is taking me too much time trying to get the below approach to work, where we try tabbing until we detect that + // the chechen card has received focus. + // function isOnChechenCard(element: Element): boolean { + // // element is a descendant of the element with id="language-card-list" not including the element with id="language-card-list" + // // and is a ancestor of the chechenCard or is the chechenCard itself + // const languageCardList = document.getElementById("language-card-list"); + // if (!languageCardList) return false; + // const isDescendantOfList = + // languageCardList.contains(element) && element !== languageCardList; + // const chechenCardElement = document.querySelector( + // `[data-testid="${languageCardTestId("che")}"]` + // ); + // if (!chechenCardElement) return false; + // const isAncestorOfChechenCard = + // element.contains(chechenCardElement) || element === chechenCardElement; + // return isDescendantOfList && isAncestorOfChechenCard; + // } + // + // await tabToElement(page, isOnChechenCard, 10, "Chechen card"); + + // Enhance: see comment above. + await page.keyboard.press("Tab"); + + // Press Enter to select the language card + await page.keyboard.press("Enter"); + + // Chechen Card should now be selected + // chechen card should have an ancestor that has class selected-option-card-button + const selectedChechenCardButton = chechenCard.locator( + 'xpath=ancestor::*[contains(@class, "selected-option-card-button")]' + ); + await expect(selectedChechenCardButton).toBeVisible(); + + // Tab again to the first script card, which is currently Cyrl + // Enhance: Cyrl is currently the first script card but it doesn't need to be. Make this test more robust for other + // possible script orderings + await page.keyboard.press("Tab"); + + // Press Enter to select the script card + await page.keyboard.press("Enter"); + + // Script Card should now be selected + const cyrlCard = page.locator("[data-testid='script-card-Cyrl']"); + const scriptCardButton = cyrlCard.locator( + 'xpath=ancestor::*[contains(@class, "selected-option-card-button")]' + ); + await expect(scriptCardButton).toBeVisible(); + }); + + test("Tab key navigates to customization button", async () => { + // Focus the search bar first + const searchBar = page.locator("#search-bar"); + await searchBar.focus(); + + // Tab to the customization button + await tabToElement( + page, + (element) => + element.getAttribute("data-testid") === "customization-button", + 10, + "Customization button" + ); + }); + + test("Escape key dismisses customization dialog", async () => { + await selectChechenCard(page); + await clickCustomizationButton(page); + + const customizationDialog = page.getByTestId("customization-dialog"); + await expect(customizationDialog).toBeVisible(); + + // Press Escape + await page.keyboard.press("Escape"); + + // Dialog should close + await expect(customizationDialog).not.toBeVisible(); + }); + + test("Arrow keys navigate within dropdowns in customization dialog", async () => { + await selectChechenCard(page); + await clickCustomizationButton(page); + + const customizationDialog = page.getByTestId("customization-dialog"); + await expect(customizationDialog).toBeVisible(); + + // Open the script dropdown + const scriptField = customizationDialog.locator( + "#customize-script-field-wrapper" + ); + await scriptField.click(); + + // Wait for dropdown to open + await page.waitForTimeout(200); + + // Press ArrowDown to navigate in the dropdown + await page.keyboard.press("ArrowDown"); + await page.keyboard.press("ArrowDown"); + + // Should be able to select with Enter + await page.keyboard.press("Enter"); + + // The script field should now have a value + const scriptInput = customizationDialog.locator("#customize-script-field"); + const value = await scriptInput.inputValue(); + expect(value).toBeTruthy(); + expect(value.length).toBeGreaterThan(0); + }); + + test("Search bar receives focus on clearing search", async () => { + await search(page, "chechen"); + + // Click clear button + await page.getByTestId("clear-search-X-button").click(); + + // Search bar should be focused + const searchBar = page.locator("#search-bar"); + await expect(searchBar).toBeFocused(); + }); +}); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/languageCardDisplay.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/languageCardDisplay.e2e.ts new file mode 100644 index 00000000..78f26582 --- /dev/null +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/languageCardDisplay.e2e.ts @@ -0,0 +1,164 @@ +import { test, expect, Page } from "@playwright/test"; +import { + clearSearch, + createPageAndLoadLanguageChooser, + languageCardTestId, + scriptCardTestId, + search, +} from "./e2eHelpers"; + +let page: Page; + +test.describe("Language card display elements", () => { + test.beforeAll(async ({ browser }) => { + page = await createPageAndLoadLanguageChooser(browser); + }); + + test.beforeEach(async () => { + await clearSearch(page); + }); + + test("Autonym displays when available", async () => { + await search(page, "japanese"); + + const japaneseCard = page.getByTestId(languageCardTestId("jpn")); + await japaneseCard.scrollIntoViewIfNeeded(); + + // Japanese has an autonym: 日本語 + await expect(japaneseCard.locator("..")).toContainText(/日本語/); + }); + + test("Exonym displays when different from autonym", async () => { + await search(page, "russian"); + + const russianCard = page.getByTestId(languageCardTestId("rus")); + await russianCard.scrollIntoViewIfNeeded(); + + // Should show both autonym and exonym since they're different + await expect(russianCard.locator("..")).toContainText(/русский/); + await expect(russianCard.locator("..")).toContainText("Russian"); + }); + + test("Exonym not duplicated when same as autonym", async () => { + // Sãotomense has autonym = exonym + await search(page, "Sãotomense"); + + const saoTomeCard = page.getByTestId(languageCardTestId("cri")); + await saoTomeCard.scrollIntoViewIfNeeded(); + + // Should show the language name, but not duplicate it + const cardContent = await saoTomeCard.textContent(); + + // Should have content (autonym or exonym) + expect(cardContent).toBeTruthy(); + // If it has "Sãotomense" in English, it shouldn't be duplicated + const matches = cardContent?.match(/Sãotomense/g); + if (matches) { + expect(matches.length).toBeLessThanOrEqual(1); + } + }); + + test("Region names display for language with known regions", async () => { + await search(page, "tok pisin"); + + const tpiCard = page.getByTestId(languageCardTestId("tpi")); + await tpiCard.scrollIntoViewIfNeeded(); + + // Tok Pisin is spoken in Papua New Guinea + await expect(tpiCard.locator("..")).toContainText(/Papua New Guinea/); + await expect(tpiCard.locator("..")).toContainText(/A language of/); + }); + + test("Macrolanguage warning icon and text appear on macrolanguage cards", async () => { + await search(page, "kanuri"); + + const kauCard = page.getByTestId(languageCardTestId("kau")); + await kauCard.scrollIntoViewIfNeeded(); + + // Should show macrolanguage indicator + await expect(kauCard.locator("..")).toContainText(/macrolanguage/); + + // Should show warning icon + const warningIcon = kauCard.locator("..").getByTestId("WarningIcon"); + await expect(warningIcon).toBeVisible(); + + // Should show the warning message + await expect(kauCard.locator("..")).toContainText( + /It is usually better to pick a specific language instead of a macrolanguage/ + ); + }); + + test("Alternative names display as comma-separated list", async () => { + // Find a language with multiple alternative names + await search(page, "swiss german"); + + const gswCard = page.getByTestId(languageCardTestId("gsw")); + await gswCard.scrollIntoViewIfNeeded(); + + const cardContent = await gswCard.locator("..").textContent(); + + // Should contain comma separators if there are alternative names + // Swiss German has various dialectal names + if (cardContent && cardContent.includes(",")) { + expect(cardContent).toContain(","); + } + }); + + test("Script sample text displays on script cards", async () => { + await search(page, "chechen"); + const chechenCard = page.getByTestId(languageCardTestId("che")); + await chechenCard.click(); + + // Find the Cyrillic script card + const cyrlCard = page.getByTestId(scriptCardTestId("Cyrl")); + await expect(cyrlCard).toBeVisible(); + + // Should contain script name + await expect(cyrlCard.locator("..")).toContainText(/Cyr/); + + // Cyrillic should have sample text (like "Аа Бб Вв") + const cardContent = await cyrlCard.locator("..").textContent(); + expect(cardContent).toBeTruthy(); + expect(cardContent?.length).toBeGreaterThan(10); // Has more than just the script name + }); + + test("Empty script sample handled gracefully", async () => { + // Find a language with a script that might not have samples + await search(page, "hindi"); + const hindiCard = page.getByTestId(languageCardTestId("hin")); + await hindiCard.click(); + + // Look at one of the script cards (Devanagari should have samples, but testing the structure) + const scriptCards = page.locator("[data-testid^='script-card-']"); + const firstScriptCard = scriptCards.first(); + + if (await firstScriptCard.isVisible()) { + // Script card should still be visible and properly formatted + await expect(firstScriptCard).toBeVisible(); + + // Should have at least the script name + const cardContent = await firstScriptCard.locator("..").textContent(); + expect(cardContent).toBeTruthy(); + } + }); + + test("Region names truncate with ellipsis after 2 lines", async () => { + // Find a language spoken in many regions + await search(page, "arabic"); + + const arabicCard = page.getByTestId(languageCardTestId("arb")); + await arabicCard.scrollIntoViewIfNeeded(); + + // Check that the region display has the ellipsis CSS applied + const regionText = arabicCard + .locator("..") + .locator("h6", { hasText: "A language of" }); + + // Verify the CSS properties for text truncation + const overflow = await regionText.evaluate((el) => { + return window.getComputedStyle(el).overflow; + }); + + expect(overflow).toBe("hidden"); + }); +}); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/languageSearch.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/languageSearch.e2e.ts index e466fbe4..8a21113b 100644 --- a/components/language-chooser/react/language-chooser-react-mui/e2e/languageSearch.e2e.ts +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/languageSearch.e2e.ts @@ -138,4 +138,208 @@ test.describe("Search", () => { // no results await expect(page.locator(".option-card-button")).not.toBeVisible(); }); + + test("Search by alternative name (synonym)", async () => { + // Search for a language using an alternative name + await search(page, "barbadian creole english"); + + // Should find Bajan language + const bajanCard = page.getByTestId(languageCardTestId("bjs")); + await bajanCard.scrollIntoViewIfNeeded(); + await expect(bajanCard).toBeVisible(); + }); + + test("Search with special characters and diacritics", async () => { + // Search with diacritics for Portuguese + await search(page, "português"); + + const portugueseCard = page.getByTestId(languageCardTestId("por")); + await portugueseCard.scrollIntoViewIfNeeded(); + await expect(portugueseCard).toBeVisible(); + + // Try without diacritics + await search(page, "portugues"); + await portugueseCard.scrollIntoViewIfNeeded(); + await expect(portugueseCard).toBeVisible(); + }); + + test("Search with typos gets fuzzy matches", async () => { + // Search with a typo for Russian + await search(page, "rusian"); + const russianCard = page.getByTestId(languageCardTestId("rus")); + await russianCard.scrollIntoViewIfNeeded(); + await expect(russianCard).toBeVisible(); + + await search(page, "jxpanese"); + const japaneseCard = page.getByTestId(languageCardTestId("jpn")); + await japaneseCard.scrollIntoViewIfNeeded(); + await expect(japaneseCard).toBeVisible(); + }); + + test("Search with partial matching shows results", async () => { + await search(page, "port"); + + // Should match Portuguese (Português) + const portugueseCard = page.getByTestId(languageCardTestId("por")); + await portugueseCard.scrollIntoViewIfNeeded(); + await expect(portugueseCard).toBeVisible(); + }); + + test("Empty search shows no results", async () => { + await search(page, ""); + + // No results should be visible + const cards = page.locator(".option-card-button"); + + // Cards may take time to lazy load, so wait a bit to be sure + await page.waitForTimeout(200); + await expect(cards).not.toBeVisible(); + }); + + test("Search with no matches clears all cards", async () => { + // First search for something that has results + await search(page, "german"); + + let cards = page.locator(".option-card-button"); + // Wait for lazy loading + await expect(cards.first()).toBeVisible(); + let count = await cards.count(); + expect(count).toBeGreaterThan(0); + + // Now search for something with no matches + await search(page, "xyzabc123notarealanguage"); + + // If there are cards they may take time to lazyload + await page.waitForTimeout(500); + + // Should have no visible cards + cards = page.locator(".option-card-button"); + await expect(cards).not.toBeVisible(); + }); + + test("Multiple word search terms work", async () => { + await search(page, "swiss german"); + + const gswCard = page.getByTestId(languageCardTestId("gsw")); + await gswCard.scrollIntoViewIfNeeded(); + await expect(gswCard).toBeVisible(); + await expect(gswCard.locator("..")).toContainText( + /Schwiizerdütsch|Swiss German/ + ); + }); + + test("Search is case insensitive", async () => { + // All uppercase + await search(page, "GERMAN"); + let germanCard = page.getByTestId(languageCardTestId("deu")); + await germanCard.scrollIntoViewIfNeeded(); + await expect(germanCard).toBeVisible(); + + // All lowercase + await search(page, "german"); + germanCard = page.getByTestId(languageCardTestId("deu")); + await expect(germanCard).toBeVisible(); + + // Mixed case + await search(page, "GeRmAn"); + germanCard = page.getByTestId(languageCardTestId("deu")); + await expect(germanCard).toBeVisible(); + }); + + test("Search by region shows multiple languages from that region", async () => { + await search(page, "Afghanistan"); + + // Should show multiple languages spoken in Afghanistan + const cards = page.locator(".option-card-button"); + await expect(cards.nth(3)).toBeVisible(); + }); + + test("Search maintains results while scrolling", async () => { + await search(page, "language"); + + let cards = page.locator(".option-card-button"); + await expect(cards.first()).toBeVisible(); + const initialCount = await cards.count(); + + // wait until we have at least 10 results + await expect(cards.nth(9)).toBeDefined(); + + // Scroll down + const languageCardList = page.locator("#language-card-list"); + await languageCardList.evaluate((el) => { + el.scrollTop = 500; + }); + + // Should still have results (possibly more due to lazy loading) + cards = page.locator(".option-card-button"); + const newCount = await cards.count(); + expect(newCount).toBeGreaterThanOrEqual(initialCount); + }); + + test("Searching for language code in different case finds language", async () => { + // Try lowercase + await search(page, "deu"); + let germanCard = page.getByTestId(languageCardTestId("deu")); + await germanCard.scrollIntoViewIfNeeded(); + await expect(germanCard).toBeVisible(); + + // Try uppercase + await search(page, "DEU"); + germanCard = page.getByTestId(languageCardTestId("deu")); + await expect(germanCard).toBeVisible(); + }); + + test("Search by autonym (native script) finds language", async () => { + // Search for Japanese using native script + await search(page, "日本"); + + const japaneseCard = page.getByTestId(languageCardTestId("jpn")); + await japaneseCard.scrollIntoViewIfNeeded(); + await expect(japaneseCard).toBeVisible(); + }); + + test("Debounced search updates results correctly", async () => { + // Type quickly (simulating rapid typing) + await page.locator("#search-bar").fill("g"); + await page.waitForTimeout(50); + await page.locator("#search-bar").fill("ge"); + await page.waitForTimeout(50); + await page.locator("#search-bar").fill("ger"); + await page.waitForTimeout(50); + await page.locator("#search-bar").fill("germ"); + await page.waitForTimeout(50); + await page.locator("#search-bar").fill("germa"); + await page.waitForTimeout(50); + await page.locator("#search-bar").fill("german"); + + const germanCard = page.getByTestId(languageCardTestId("deu")); + await germanCard.scrollIntoViewIfNeeded(); + await expect(germanCard).toBeVisible(); + }); + + test("Language list scrolls to top when search changes", async () => { + // Search for something that produces many results + await search(page, "language"); + + const languageCardList = page.locator("#language-card-list"); + + // Scroll down in the list + await languageCardList.evaluate((el) => { + el.scrollTop = 500; + }); + + // Verify we've scrolled + let scrollTop = await languageCardList.evaluate((el) => el.scrollTop); + expect(scrollTop).toBeGreaterThan(400); + + // Change the search + await search(page, "russian"); + + // Wait for results to update + await page.waitForTimeout(300); + + // Should have scrolled back to top + scrollTop = await languageCardList.evaluate((el) => el.scrollTop); + expect(scrollTop).toBeLessThan(100); + }); }); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/languageTag.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/languageTag.e2e.ts index cc35a419..bcf5687e 100644 --- a/components/language-chooser/react/language-chooser-react-mui/e2e/languageTag.e2e.ts +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/languageTag.e2e.ts @@ -220,4 +220,159 @@ test.describe("Language tag creation and preview", () => { await expect(simpTagPreview).toBeVisible(); await expect(simpTagPreview).toContainText("zh-CN"); }); + + test("Tag preview updates when customizations applied via dialog", async () => { + await search(page, "uzbek"); + + const uzbCard = page.getByTestId(languageCardTestId("uzn")); + await uzbCard.scrollIntoViewIfNeeded(); + await expect(uzbCard).toBeVisible(); + await uzbCard.click(); + + // Select a script first + const cyrlCard = page.getByTestId(scriptCardTestId("Cyrl")); + await cyrlCard.click(); + + let tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("uzn-Cyrl"); + + // Open customization dialog and add region + const customizationButton = page.getByTestId("customization-button"); + await customizationButton.click(); + + const customizationDialog = page.getByTestId("customization-dialog"); + await expect(customizationDialog).toBeVisible(); + + // Add a region + const regionField = customizationDialog.locator( + "#customize-region-field-wrapper" + ); + await regionField.getByLabel("Open").click(); + await page + .getByRole("option", { name: /Afghanistan/ }) + .first() + .click(); + + // Dialog tag preview should update + const dialogTagPreview = customizationDialog.getByTestId( + "customization-dialog-tag-preview" + ); + await expect(dialogTagPreview).toContainText("uzn-Cyrl-AF"); + + // Click OK + await customizationDialog.getByRole("button", { name: "OK" }).click(); + + // Main tag preview should now show the region + tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("uzn-Cyrl-AF"); + }); + + test("Tag preview updates when variant/dialect added", async () => { + await search(page, "chechen"); + + const chechenCard = page.getByTestId(languageCardTestId("che")); + await chechenCard.click(); + + // Open customization dialog + const customizationButton = page.getByTestId("customization-button"); + await customizationButton.click(); + + const customizationDialog = page.getByTestId("customization-dialog"); + await expect(customizationDialog).toBeVisible(); + + // Add a variant + const variantField = customizationDialog.locator( + "#customize-variant-field" + ); + await variantField.fill("foobar"); + + // Dialog tag preview should update + const dialogTagPreview = customizationDialog.getByTestId( + "customization-dialog-tag-preview" + ); + await expect(dialogTagPreview).toContainText("ce-x-foobar"); + + // Click OK + await customizationDialog.getByRole("button", { name: "OK" }).click(); + + // Main tag preview should include the variant + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("ce-x-foobar"); + }); + + test("Tag preview updates correctly when switching between languages", async () => { + // Select first language + await search(page, "chechen"); + const chechenCard = page.getByTestId(languageCardTestId("che")); + await chechenCard.click(); + + let tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("ce"); + + // Switch to different language + await search(page, "japanese"); + const japaneseCard = page.getByTestId(languageCardTestId("jpn")); + await japaneseCard.click(); + + // Tag preview should update to Japanese + tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("ja"); + await expect(tagPreview).not.toContainText("ce"); + + // Switch again + await search(page, "german"); + const germanCard = page.getByTestId(languageCardTestId("deu")); + await germanCard.click(); + + // Tag preview should update to German + tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("de"); + await expect(tagPreview).not.toContainText("ja"); + }); + + test("Tag preview shows complex tag with script, region, and variant", async () => { + await search(page, "serbian"); + + const serbianCard = page.getByTestId(languageCardTestId("srp")); + await serbianCard.click(); + + // Select Latin script + const latnCard = page.getByTestId(scriptCardTestId("Latn")); + await latnCard.click(); + + // Open customization dialog + const customizationButton = page.getByTestId("customization-button"); + await customizationButton.click(); + + const customizationDialog = page.getByTestId("customization-dialog"); + + // Add region + const regionField = customizationDialog.locator( + "#customize-region-field-wrapper" + ); + await regionField.getByLabel("Open").click(); + await page + .getByRole("option", { name: /Bosnia/ }) + .first() + .click(); + + // Add variant + const variantField = customizationDialog.locator( + "#customize-variant-field" + ); + await variantField.fill("variant1"); + + // Dialog should show complete tag + const dialogTagPreview = customizationDialog.getByTestId( + "customization-dialog-tag-preview" + ); + await expect(dialogTagPreview).toContainText("sr-Latn-BA-x-variant1"); + + // Click OK + await customizationDialog.getByRole("button", { name: "OK" }).click(); + + // Main tag preview should show complete tag + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toContainText("sr-Latn-BA-x-variant1"); + }); }); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/localization.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/localization.e2e.ts new file mode 100644 index 00000000..7f862f97 --- /dev/null +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/localization.e2e.ts @@ -0,0 +1,58 @@ +import { test, expect, Page } from "@playwright/test"; +import { + clickCustomizationButton, + search, + selectChechenCard, +} from "./e2eHelpers"; + +test.describe("UI Localization", () => { + test("French translations are present and English is absent", async ({ + page, + }) => { + await page.goto("/?uiLanguage=fr", { waitUntil: "load" }); + + // Test 1: "Choose Language" -> "Sélectionnez une langue" + await expect(page.getByText("Sélectionnez une langue")).toBeVisible(); + await expect( + page.getByText("Choose Language", { exact: true }) + ).toHaveCount(0); + + // Select a language to access the Customize button + await search(page, "chechen"); + await selectChechenCard(page); + + // Test 2: "Customize" -> "Personnaliser" + const customizeButton = page.getByTestId("customization-button"); + await expect(customizeButton).toContainText("Personnaliser"); + await expect(customizeButton).not.toContainText("Customize"); + + // Open customization dialog to access more labels + await clickCustomizationButton(page); + const dialog = page.getByTestId("customization-dialog"); + + // Test 3: "Cancel" -> "Annuler" + const cancelButton = dialog.getByRole("button", { name: "Annuler" }); + await expect(cancelButton).toBeVisible(); + await expect( + dialog.getByRole("button", { name: "Cancel", exact: true }) + ).toHaveCount(0); + }); + + test("English translations are present", async ({ page }) => { + await page.goto("/?uiLanguage=en", { waitUntil: "load" }); + + // Verify English versions are present + await expect(page.getByText("Choose Language")).toBeVisible(); + + await search(page, "chechen"); + await selectChechenCard(page); + + const customizeButton = page.getByTestId("customization-button"); + await expect(customizeButton).toContainText("Customize"); + + await clickCustomizationButton(page); + const dialog = page.getByTestId("customization-dialog"); + const cancelButton = dialog.getByRole("button", { name: "Cancel" }); + await expect(cancelButton).toBeVisible(); + }); +}); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/rightPanel.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/rightPanel.e2e.ts new file mode 100644 index 00000000..d0ab84b9 --- /dev/null +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/rightPanel.e2e.ts @@ -0,0 +1,100 @@ +import { test, expect, Page } from "@playwright/test"; +import { + clearSearch, + createPageAndLoadLanguageChooser, + search, + selectChechenCard, + selectChechenCyrlCard, + toggleScriptCard, +} from "./e2eHelpers"; + +let page: Page; + +test.describe("Right panel functionality", () => { + test.beforeAll(async ({ browser }) => { + page = await createPageAndLoadLanguageChooser(browser); + }); + + test.beforeEach(async () => { + await clearSearch(page); + }); + + test("Display name field shows default name when no custom name entered", async () => { + await selectChechenCard(page); + + const displayNameField = page.locator("#language-name-bar"); + await expect(displayNameField).toBeVisible(); + + await expect(displayNameField).toHaveValue("Нохчийн мотт"); + }); + + test("Display name field edits update the value", async () => { + await selectChechenCard(page); + + const displayNameField = page.locator("#language-name-bar"); + const customName = "My Custom Chechen Name"; + await displayNameField.fill(customName); + + await expect(displayNameField).toHaveValue(customName); + }); + + test("Display name field shows 'required' label when empty and blocking submission", async () => { + await selectChechenCyrlCard(page); + + const displayNameField = page.locator("#language-name-bar"); + + // Clear the display name + await displayNameField.fill(""); + + // The "required" label should appear + const requiredLabel = page.getByText("required"); + await expect(requiredLabel).toBeVisible(); + + // And the OK button should be disabled + const okButton = page.getByTestId("lang-chooser-dialog-ok-button"); + await expect(okButton).toBeDisabled(); + }); + + test("Display name clears when selecting different language", async () => { + await selectChechenCard(page); + + const displayNameField = page.locator("#language-name-bar"); + + // Set a custom display name + const customName = "Custom Chechen Name"; + await displayNameField.fill(customName); + await expect(displayNameField).toHaveValue(customName); + + await search(page, "english"); + const englishCard = page.getByTestId("language-card-eng"); + await englishCard.click(); + await expect(displayNameField).toHaveValue("English"); + await expect(displayNameField).not.toHaveValue(customName); + }); + + test("Tag preview updates when language selected", async () => { + await selectChechenCard(page); + + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + await expect(tagPreview).toBeVisible(); + + // Should show "ce" for Chechen + await expect(tagPreview).toContainText("ce"); + }); + + test("Tag preview updates when script selected", async () => { + await selectChechenCyrlCard(page); + + const tagPreview = page.getByTestId("right-panel-langtag-preview"); + + // Should still show "ce" (Cyrillic is suppressed for Chechen as it's the default) + await expect(tagPreview).toContainText("ce"); + await expect(tagPreview).not.toContainText("ce-Cyrl"); + + // Now select Arabic script + await toggleScriptCard(page, "Arab"); + + // Should show "ce-Arab" + await expect(tagPreview).toContainText("ce-Arab"); + }); +}); diff --git a/components/language-chooser/react/language-chooser-react-mui/e2e/tooltips.e2e.ts b/components/language-chooser/react/language-chooser-react-mui/e2e/tooltips.e2e.ts new file mode 100644 index 00000000..c2e590e6 --- /dev/null +++ b/components/language-chooser/react/language-chooser-react-mui/e2e/tooltips.e2e.ts @@ -0,0 +1,134 @@ +import { test, expect, Page } from "@playwright/test"; +import { + clearSearch, + clickCustomizationButton, + createPageAndLoadLanguageChooser, + search, + selectChechenCard, + resetBeforeEach, + cleanupDialogHandlers, +} from "./e2eHelpers"; + +let page: Page; + +// Don't do our usual beforeEach clear search +test.describe("Initial tooltip behavior", () => { + test.beforeAll(async ({ browser }) => { + page = await createPageAndLoadLanguageChooser(browser); + }); + test("Initial 'Start typing' prompt tooltip appears on first load", async () => { + // The initial prompt tooltip should be visible + const tooltipText = page.getByText("Start typing to find your language."); + await expect(tooltipText).toBeVisible(); + }); + + test("Initial tooltip disappears when user types in search", async () => { + // Reload to get the initial state + await page.reload(); + await page.waitForLoadState("load"); + + // Verify tooltip is visible + const tooltipText = page.getByText("Start typing to find your language."); + await expect(tooltipText).toBeVisible(); + + // Start typing + await search(page, "che"); + + // Tooltip should now be hidden + await expect(tooltipText).not.toBeVisible(); + }); +}); + +test.describe("Tooltip behavior", () => { + test.beforeAll(async ({ browser }) => { + page = await createPageAndLoadLanguageChooser(browser); + }); + + test.beforeEach(async () => { + await resetBeforeEach(page); + }); + + test.afterEach(async () => { + await cleanupDialogHandlers(page); + }); + + test("'Select a script' tooltip appears when language with multiple scripts selected and no script chosen", async () => { + // Select a language with multiple scripts (Chechen has Cyrillic, Latin, and Arabic) + await selectChechenCard(page); + + // The "Select a script" tooltip should appear + const scriptTooltip = page.getByText("Select a script"); + await expect(scriptTooltip).toBeVisible(); + }); + + test("'Select a script' tooltip hides when script card scrolled out of view", async () => { + await selectChechenCard(page); + + // Verify tooltip is visible + const scriptTooltip = page.getByText("Select a script"); + await expect(scriptTooltip).toBeVisible(); + + // Search for something else to scroll the script cards out of view + await search(page, "japanese"); + + // Tooltip should be hidden (the script cards are no longer visible) + await expect(scriptTooltip).not.toBeVisible(); + }); + + test("Info icon tooltip on customization button shows correct text for unlisted language state", async () => { + // With no language selected, we're in "Create Unlisted Language" mode + await clearSearch(page); + + const customizationButton = page.getByTestId("customization-button"); + const infoIcon = customizationButton.getByTestId("InfoOutlinedIcon"); + + // Hover over the info icon to trigger tooltip + await infoIcon.hover(); + + // Check for unlisted language tooltip text + const unlistedTooltip = page.getByText( + /If you cannot find a language and it does not appear in ethnologue.com/ + ); + await expect(unlistedTooltip).toBeVisible(); + }); + + test("Info icon tooltip on customization button shows correct text for customize state", async () => { + // Select a language to enter "Customize" mode + await selectChechenCard(page); + + const customizationButton = page.getByTestId("customization-button"); + const infoIcon = customizationButton.getByTestId("InfoOutlinedIcon"); + + // Hover over the info icon + await infoIcon.hover(); + + // Check for customize tooltip text + const customizeTooltip = page.getByText( + /If you found the main language but need to change some of the specifics/ + ); + await expect(customizeTooltip).toBeVisible(); + }); + + test("Manual tag entry tooltip appears on tag preview in customization dialog", async () => { + await selectChechenCard(page); + await clickCustomizationButton(page); + + const tagPreview = page.getByTestId("customization-dialog-tag-preview"); + await expect(tagPreview).toBeVisible(); + + const infoIcon = tagPreview.getByTestId("InfoOutlinedIcon"); + + // Hover over the info icon + await infoIcon.hover(); + + // Look for the advanced/manual entry tooltip + const manualEntryTooltip = page.getByText(/Advanced/); + await expect(manualEntryTooltip).toBeVisible(); + + // Should also show the warning/instruction about CTRL+click + const instructionText = page.getByText( + /If this user interface is not offering you a code/ + ); + await expect(instructionText).toBeVisible(); + }); +}); diff --git a/components/language-chooser/react/language-chooser-react-mui/src/LanguageCard.tsx b/components/language-chooser/react/language-chooser-react-mui/src/LanguageCard.tsx index 2d3c55cb..a633b63f 100644 --- a/components/language-chooser/react/language-chooser-react-mui/src/LanguageCard.tsx +++ b/components/language-chooser/react/language-chooser-react-mui/src/LanguageCard.tsx @@ -1,7 +1,11 @@ /** @jsxImportSource @emotion/react */ import { css } from "@emotion/react"; import { OptionCard, OptionCardProps } from "./OptionCard"; -import { ILanguage, rawIsoCode } from "@ethnolib/find-language"; +import { + ILanguage, + rawIsoCode, + stripDemarcation, +} from "@ethnolib/find-language"; import { PartiallyBoldedTypography } from "./PartiallyBoldedTypography"; import WarningIcon from "@mui/icons-material/Warning"; import { lighten, Typography, useTheme } from "@mui/material"; @@ -56,16 +60,20 @@ export const LanguageCard: React.FunctionComponent< {languageCardData.autonym} )} - {languageCardData.exonym !== languageCardData.autonym && ( - - {languageCardData.exonym} - - )} + { + // TODO temporary fix for BL-15919. Probably will be changed by BL-15922 + stripDemarcation(languageCardData.exonym) !== + stripDemarcation(languageCardData.autonym) && ( + + {languageCardData.exonym} + + ) + } { // This can take a bit, so push it to the end of the event queue so that we paint the rest of the component first lp.resetTo( - props.initialSearchString || "", + props.initialSearchString, props.initialSelectionLanguageTag, props.initialCustomDisplayName ); @@ -166,7 +166,7 @@ export const LanguageChooserInner: React.FunctionComponent< // Show a tooltip prompting user to start typing, only on first load, until they start typing const [showInitialPrompt, setShowInitialPrompt] = useState( - !props.initialSearchString + !(props.initialSearchString || props.initialSelectionLanguageTag) ); useEffect(() => { if (showInitialPrompt && (lp.searchString || customizeLanguageDialogOpen)) { diff --git a/components/language-chooser/react/language-chooser-react-mui/src/demos/DialogDemo.tsx b/components/language-chooser/react/language-chooser-react-mui/src/demos/DialogDemo.tsx index 3265574c..4b379e58 100644 --- a/components/language-chooser/react/language-chooser-react-mui/src/demos/DialogDemo.tsx +++ b/components/language-chooser/react/language-chooser-react-mui/src/demos/DialogDemo.tsx @@ -33,12 +33,15 @@ export const DialogDemo: React.FunctionComponent<{ initialLanguageTag, demoRightPanelComponent, primaryColor, - initialSearchString: demoInitialSearchString, + initialSearchString, ...languageChooserDialogProps }) => { // To demonstrate the ability to reopen to a desired state const initialSelection: IOrthography | undefined = - parseLangtagFromLangChooser(initialLanguageTag || "", defaultSearchResultModifier); + parseLangtagFromLangChooser( + initialLanguageTag || "", + defaultSearchResultModifier + ); if (initialSelection?.language) { initialSelection.customDetails = { ...(initialSelection.customDetails || []), @@ -49,9 +52,6 @@ export const DialogDemo: React.FunctionComponent<{ } const [open, setOpen] = React.useState(true); - if (initialSelection && demoInitialSearchString) { - console.warn("When both initialLanguageTag and initialSearchString are entered, initialSearchString will be ignored.") - } const [selectedValue, setSelectedValue] = React.useState( initialSelection || ({} as IOrthography) ); @@ -153,7 +153,7 @@ export const DialogDemo: React.FunctionComponent<{ rightPanelComponent={ demoRightPanelComponent ? : undefined } - initialSearchString={selectedValue?.language?.languageSubtag || demoInitialSearchString} + initialSearchString={initialSearchString} /> diff --git a/components/language-chooser/react/language-chooser-react-mui/src/main.tsx b/components/language-chooser/react/language-chooser-react-mui/src/main.tsx index 26a23ab9..395b1b34 100644 --- a/components/language-chooser/react/language-chooser-react-mui/src/main.tsx +++ b/components/language-chooser/react/language-chooser-react-mui/src/main.tsx @@ -2,9 +2,22 @@ import ReactDOM from "react-dom"; import DialogDemo from "./demos/DialogDemo"; import React from "react"; +// Read parameters from URL query parameters (for e2e testing) +const urlParams = new URLSearchParams(window.location.search); +const uiLanguage = urlParams.get("uiLanguage") || undefined; +const initialLanguageTag = urlParams.get("initialLanguageTag") || undefined; +const initialSearchString = urlParams.get("initialSearchString") || undefined; +const initialCustomDisplayName = + urlParams.get("initialCustomDisplayName") || undefined; + ReactDOM.render( - + , document.getElementById("root") );