Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ef2959e
Workflow 1: Basic Translation Builder Display
Bharath-K-Shetty Aug 15, 2025
f6514ea
Workflow 2: Language Selection and Switching
Bharath-K-Shetty Aug 15, 2025
3be06ac
Merge branch 'main' into test/O3-4968
Bharath-K-Shetty Aug 17, 2025
2bec8c3
Added remaining e2e tests to Translation Builder
Bharath-K-Shetty Aug 21, 2025
ed1dfe7
Merge branch 'main' of https://github.com/Bharath-K-Shetty/openmrs-es…
Bharath-K-Shetty Aug 21, 2025
90abacd
Merge branch 'test/O3-4968' of https://github.com/Bharath-K-Shetty/op…
Bharath-K-Shetty Aug 21, 2025
977c97f
Merge branch 'main' into test/O3-4968
Bharath-K-Shetty Aug 21, 2025
f93ebdc
Merge branch 'main' of https://github.com/Bharath-K-Shetty/openmrs-es…
Bharath-K-Shetty Aug 23, 2025
95d0bd9
Refined the e2e test workflows
Bharath-K-Shetty Aug 23, 2025
fae2941
Merge branch 'test/O3-4968' of https://github.com/Bharath-K-Shetty/op…
Bharath-K-Shetty Aug 23, 2025
dee15b2
Updated the step names
Bharath-K-Shetty Aug 23, 2025
4b007ba
Added workflow for upload and download translation file
Bharath-K-Shetty Aug 23, 2025
833dfdc
Merge branch 'main' into test/O3-4968
Bharath-K-Shetty Aug 24, 2025
7090561
Made all the tests pass
Bharath-K-Shetty Aug 24, 2025
49fe191
Merge branch 'test/O3-4968' of https://github.com/Bharath-K-Shetty/op…
Bharath-K-Shetty Aug 24, 2025
2581cb8
Merge branch 'main' into test/O3-4968
Bharath-K-Shetty Aug 25, 2025
9f73bc6
Merge branch 'main' into test/O3-4968
Bharath-K-Shetty Aug 25, 2025
5bf32e7
Merge branch 'main' into test/O3-4968
Bharath-K-Shetty Aug 26, 2025
03cf72e
Merge branch 'main' of https://github.com/Bharath-K-Shetty/openmrs-es…
Bharath-K-Shetty Aug 26, 2025
e9871f9
Merge branch 'main' into test/O3-4968
NethmiRodrigo Aug 27, 2025
4d2d42e
remove eslint-disable and update to use formBuilderPage.page queries
Bharath-K-Shetty Aug 27, 2025
cd025b2
Merge branch 'main' of https://github.com/Bharath-K-Shetty/openmrs-es…
Bharath-K-Shetty Aug 27, 2025
04820c4
Merge branch 'test/O3-4968' of https://github.com/Bharath-K-Shetty/op…
Bharath-K-Shetty Aug 27, 2025
76adcd2
Merge branch 'main' into test/O3-4968
Bharath-K-Shetty Aug 27, 2025
78a81e4
Merge branch 'main' into test/O3-4968
Bharath-K-Shetty Aug 28, 2025
6891076
Merge branch 'main' of https://github.com/Bharath-K-Shetty/openmrs-es…
Bharath-K-Shetty Aug 29, 2025
96485a8
Refined the step names of the workflows
Bharath-K-Shetty Aug 29, 2025
8eec6a6
Merge branch 'test/O3-4968' of https://github.com/Bharath-K-Shetty/op…
Bharath-K-Shetty Aug 29, 2025
fbbed14
Merge branch 'main' of https://github.com/Bharath-K-Shetty/openmrs-es…
Bharath-K-Shetty Aug 30, 2025
b1af41d
Update the step names of the upload translation workflow
Bharath-K-Shetty Aug 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions e2e/pages/form-builder-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,30 @@ export class FormBuilderPage {
readonly questionIdInput = () => this.page.getByRole('textbox', { name: /question id/i });
readonly questionCreatedMessage = () => this.page.getByText(/new question created/i);

readonly translationBuilderTab = () => this.page.getByRole('tab', { name: /translation builder/i });
readonly translationBuilderPanel = () =>
this.page
.getByRole('tabpanel')
.filter({ has: this.page.getByRole('button', { name: /upload translation/i }) })
.first();
readonly languageDropdown = () => this.translationBuilderPanel().locator('#target-language');
readonly downloadTranslationButton = () =>
this.translationBuilderPanel().getByRole('button', { name: /download translation/i });
readonly uploadTranslationButton = () =>
this.translationBuilderPanel().getByRole('button', { name: /upload translation/i });
readonly translationSearchInput = () => this.page.getByPlaceholder(/search translation keys/i);
readonly translationTabsList = () =>
this.translationBuilderPanel().getByRole('tablist', { name: 'Translation filter' });
readonly allTranslationsTab = () => this.translationTabsList().getByRole('tab', { name: 'All', exact: true });
readonly translatedTab = () => this.translationTabsList().getByRole('tab', { name: 'Translated', exact: true });
readonly untranslatedTab = () => this.translationTabsList().getByRole('tab', { name: 'Untranslated', exact: true });
readonly editTranslationButton = (index: number = 0) =>
this.page.locator('[data-testid="edit-translation-button"]').nth(index);
readonly translationModal = () => this.page.getByRole('dialog');
translationValueInput() {
return this.page.getByTestId('translation-value-input');
}
readonly saveTranslationButton = () => this.translationModal().getByRole('button', { name: 'Save' });
async gotoFormBuilder() {
await this.page.goto('form-builder');
}
Expand Down
178 changes: 178 additions & 0 deletions e2e/specs/translation-builder-workflows.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { test } from '../core';
import { expect } from '@playwright/test';
import { deleteForm, createForm, createValueReference, addFormResources } from '../commands/form-operations';
import { FormBuilderPage } from '../pages';

let formUuid = '';

const formDetails = {
name: 'Covid-19 Screening',
description: 'A test form for recording COVID-19 screening information',
version: '1.0',
published: true,
};
test.describe('Translation Builder', () => {
test.beforeEach(async ({ api }) => {
const form = await createForm(api, false, formDetails);
formUuid = form.uuid;

const valueReference = await createValueReference(api);
await addFormResources(api, valueReference, formUuid);
});
test('Manage translations: switch languages, filter, and search', async ({ page }) => {
const formBuilderPage = new FormBuilderPage(page);

await test.step('When I open a form in the translation builder', async () => {
await formBuilderPage.gotoFormBuilder();
await formBuilderPage.searchForForm(formDetails.name);
await formBuilderPage.page.getByRole('row', { name: formDetails.name }).getByLabel('Edit Schema').first().click();
await formBuilderPage.translationBuilderTab().click();
});

await test.step('When I open the language dropdown', async () => {
await formBuilderPage.languageDropdown().click();
});

await test.step('Then I should see the language list', async () => {
const dropdownMenu = formBuilderPage.translationBuilderPanel().locator('.cds--list-box__menu');
await expect(dropdownMenu).toBeVisible();
});

await test.step('And I should see "English (en)" in the language list', async () => {
await expect(formBuilderPage.page.getByRole('option', { name: 'English (en)' })).toBeVisible();
});

await test.step('When I select the "All" translations tab', async () => {
await formBuilderPage.allTranslationsTab().click();
});

await test.step('Then the "All" translations tab should be selected', async () => {
await expect(formBuilderPage.allTranslationsTab()).toHaveAttribute('aria-selected', 'true');
});

await test.step('When I select the "Translated" translations tab', async () => {
await formBuilderPage.translatedTab().click();
});

await test.step('Then I should see 0 "translated" entries', async () => {
await expect(page.locator('[data-status="translated"]')).toHaveCount(0);
});

await test.step('When I select the "Untranslated" translations tab', async () => {
await formBuilderPage.untranslatedTab().click();
});

await test.step('Then the "Untranslated" translations tab should be selected', async () => {
await expect(formBuilderPage.untranslatedTab()).toHaveAttribute('aria-selected', 'true');
});

await test.step('And I should see at least 1 "untranslated" entry', async () => {
await expect(page.locator('[data-status="untranslated"]').first()).toBeVisible();
});

await test.step('When I search translations for "Visit Details"', async () => {
const searchInput = formBuilderPage.translationSearchInput();
await searchInput.fill('Visit Details');
});

await test.step('Then the translation search input should contain "Visit Details"', async () => {
const searchInput = formBuilderPage.translationSearchInput();
await expect(searchInput).toHaveValue('Visit Details');
});

await test.step('And I should see at least 1 translation result', async () => {
const results = page.locator('[data-testid="translation-entry"]');
await expect(results).not.toHaveCount(0);
});

await test.step('And the first translation result should be visible', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: Shouldn't this include that the translation result should contain the word visit details?

const results = page.locator('[data-testid="translation-entry"]');
await expect(results.first()).toBeVisible();
});
});

test('Edit and update an individual translation', async ({ page }) => {
const formBuilderPage = new FormBuilderPage(page);

await test.step('When I open a form in the Translation Builder', async () => {
await formBuilderPage.gotoFormBuilder();
await formBuilderPage.searchForForm(formDetails.name);
await formBuilderPage.page.getByRole('row', { name: formDetails.name }).getByLabel('Edit Schema').first().click();
});

await test.step('And then I go to the translation builder', async () => {
await formBuilderPage.translationBuilderTab().click();
});

await test.step('And then I select the French language', async () => {
await formBuilderPage.languageDropdown().click();
const dropdownMenu = formBuilderPage.translationBuilderPanel().locator('.cds--list-box__menu');
await expect(dropdownMenu).toBeVisible();
await formBuilderPage.page.getByRole('option', { name: 'French (fr)' }).click();
});

await test.step('And then I edit a translation string and save it', async () => {
await formBuilderPage.editTranslationButton(0).click();
const translationInput = formBuilderPage.translationValueInput();
await translationInput.fill('Test Translation Updated');
});
await test.step('And then when I preview the form in the language, I should see the edited translation strings', async () => {
await formBuilderPage.saveTranslationButton().click();
await expect(formBuilderPage.translationModal()).toBeHidden();
await expect(formBuilderPage.editTranslationButton(0)).toBeEnabled();
});
});
test('Download a translation file from a form', async ({ page }) => {
const formBuilderPage = new FormBuilderPage(page);

await test.step('Given I have opened a form in the Form Builder', async () => {
await formBuilderPage.gotoFormBuilder();
await formBuilderPage.searchForForm(formDetails.name);
await formBuilderPage.page.getByRole('row', { name: formDetails.name }).getByLabel('Edit Schema').first().click();
});

await test.step('When I navigate to the Translation Builder', async () => {
await formBuilderPage.translationBuilderTab().click();
});

await test.step('Then I should be able to download the translation file', async () => {
const downloadButton = formBuilderPage.downloadTranslationButton();
const [download] = await Promise.all([
formBuilderPage.page.waitForEvent('download', { timeout: 15000 }),
downloadButton.click(),
]);
expect(download.suggestedFilename()).toMatch(/\.json$/);
});
});

test('Upload a translation file to backend', async ({ page }) => {
const formBuilderPage = new FormBuilderPage(page);

await test.step('Given I have opened a form in the Form Builder', async () => {
await formBuilderPage.gotoFormBuilder();
await formBuilderPage.searchForForm(formDetails.name);
await formBuilderPage.page.getByRole('row', { name: formDetails.name }).getByLabel('Edit Schema').first().click();
});

await test.step('When I navigate to the Translation Builder', async () => {
await formBuilderPage.translationBuilderTab().click();
});

await test.step('Then I should be able to upload a translation file successfully', async () => {
const uploadButton = formBuilderPage.uploadTranslationButton();
await expect(uploadButton).toBeEnabled();
await expect(uploadButton).toBeVisible();
await expect(formBuilderPage.translationBuilderPanel()).toBeVisible();

await uploadButton.click();
await expect(formBuilderPage.page.getByText(/Translations Uploaded./i)).toBeVisible();
await expect(formBuilderPage.page.getByText(/Translation file uploaded successfully/i)).toBeVisible();
});
});

test.afterEach(async ({ api }) => {
if (formUuid) {
await deleteForm(api, formUuid);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const EditTranslationModal: React.FC<EditTranslationModalProps> = ({ onClose, or
labelText={t('translationValue', 'Translated Value')}
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
data-testid="translation-value-input"
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: Is the data test id necessary? Can't we target the button from the label text?

/>
</ModalBody>
<ModalFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,21 +223,22 @@ const TranslationBuilder: React.FC<TranslationBuilderProps> = ({ formSchema, onU
onChange={({ selectedItem }) => selectedItem && languageChanger(selectedItem.code)}
/>
</div>

<div className={styles.translationActions}>
<button className={styles.linkButton} onClick={handleDownloadTranslation}>
{t('downloadTranslation', 'Download translation')}
<Download size={16} />
</button>
<button
className={styles.linkButton}
onClick={handleUploadTranslationFromSchema}
disabled={translationsUploading}
>
{t('uploadTranslation', 'Upload translation')}
{!translationsUploading ? <Upload size={16} /> : <InlineLoading />}
</button>
</div>
{formSchema ? (
<div className={styles.translationActions}>
<button className={styles.linkButton} onClick={handleDownloadTranslation}>
{t('downloadTranslation', 'Download translation')}
<Download size={16} />
</button>
<button
className={styles.linkButton}
onClick={handleUploadTranslationFromSchema}
disabled={translationsUploading}
>
{t('uploadTranslation', 'Upload translation')}
{!translationsUploading ? <Upload size={16} /> : <InlineLoading />}
</button>
</div>
) : null}
</div>

<div className={styles.translationTabs}>
Expand Down Expand Up @@ -287,16 +288,26 @@ const TranslationBuilder: React.FC<TranslationBuilderProps> = ({ formSchema, onU
<div className={styles.translationEditor}>
{filteredTranslations.length > 0 ? (
filteredTranslations.map(([key, value]) => (
<div key={key} className={styles.translationRow}>
<div className={styles.translationKey}>{key}</div>
<div className={styles.translatedKey}>{value}</div>
<div
key={key}
className={styles.translationRow}
data-testid="translation-entry"
data-status="untranslated"
>
<div className={styles.translationKey} data-testid="translation-key">
{key}
</div>
<div className={styles.translatedKey} data-testid="translation-value">
{value}
</div>
<div className={styles.inlineControls}>
<IconButton
kind="ghost"
label={t('editString', 'Edit string')}
onClick={() => handleEditClick(key)}
size="md"
className={styles.deleteButton}
data-testid="edit-translation-button"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same question here about the label

>
<Edit />
</IconButton>
Expand Down