Skip to content

Commit 329f292

Browse files
authored
chore(#330): Adds e2e tests for default geopoint question type (#313)
- Adds minimal README file to guide contributors when adding new tests - Adds a new folder structure - Moves the content of wf-preview.test.ts to /e2e/test-cases/build/web-forms-preview-forms.test.ts. The original name wasn't descriptive. - Adds new basic tests for geopoint and note question types - Adds new page objects for fill form page, preview page and geopoint control
1 parent c3716bf commit 329f292

File tree

10 files changed

+459
-27
lines changed

10 files changed

+459
-27
lines changed

.changeset/sad-pens-wear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@getodk/web-forms': patch
3+
---
4+
5+
Adds E2E tests for Geopoint and Notes question types

packages/web-forms/e2e/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# E2E tests for `@getodk/web-forms`
2+
3+
This directory contains end-to-end (E2E) tests for the `@getodk/web-forms` package, which powers form filling and submission editing of ODK forms in a web browser. These tests use [Playwright](https://playwright.dev/) to simulate user interactions and ensure the package works as expected in real-world scenarios.
4+
5+
Review the [best practices recommended by Playwright](https://playwright.dev/docs/best-practices#best-practices) before you start writing tests.
6+
7+
## Folder structure
8+
9+
The E2E tests are located in `packages/web-forms/e2e/`. Here's the structure and purpose of each directory:
10+
11+
```
12+
e2e/
13+
├── page-objects/ # Page Object Model structure for e2e tests, organizing UI abstractions into pages and reusable controls.
14+
├── controls/ # Reusable controls such as form fields, UI elements, etc.
15+
├── GeopointControl.ts # Example of a reusable control for the geopoint question type.
16+
├── pages/ # Full page representations.
17+
├── FillFormPage.ts # Example of a full page representation for a form.
18+
├── test-cases/ # Test specification files.
19+
├── geopoint.test.ts # Example of a test file for the geopoint question type.
20+
```
21+
22+
## Key concepts
23+
24+
- **Page Objects**: Implements the [Page Object Model](https://playwright.dev/docs/pom) pattern to encapsulate UI interactions, enhancing test readability and maintainability.
25+
- **Test Specification File**: Test files structured by feature (e.g., rendering, form submission), each holding a suite of tests targeting a specific application aspect. Use clear, descriptive names to highlight their purpose and ensure test coverage is easily identifiable.
26+
- **Fixtures**: Reusable test data (e.g., sample XForms) to simulate real-world use cases. Find fixtures in the [common package](../../common/src/fixtures)
27+
28+
## Getting started
29+
30+
1. **Build the project**
31+
In the root folder run:
32+
33+
```bash
34+
yarn build
35+
```
36+
37+
2. **Run tests**
38+
Execute all E2E tests:
39+
40+
```bash
41+
yarn workspace @getodk/web-forms test:e2e
42+
```
43+
44+
Or run specific tests:
45+
46+
```bash
47+
yarn workspace @getodk/web-forms test:e2e <filepath, e.g. e2e/specs/geopoint.test.ts>
48+
```
49+
50+
## Contributing
51+
52+
- Keep tests focused.
53+
- Use page object methods in `pages-objects/` for UI actions.
54+
- Add [fixtures](../../common/src/fixtures) for new scenarios instead of hardcoding data.

packages/web-forms/e2e/build/wf-preview.test.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { expect, Page } from '@playwright/test';
2+
3+
export class GeopointControl {
4+
private readonly page: Page;
5+
6+
constructor(page: Page) {
7+
this.page = page;
8+
}
9+
10+
async expectGeopointDialog(expectedTitle: string, expectedQualityText: string) {
11+
const dialogTitle = this.page
12+
.locator('.geo-dialog-header-title')
13+
.getByText(expectedTitle, { exact: true });
14+
await expect(dialogTitle).toBeVisible();
15+
16+
const accuracyQuality = this.page
17+
.locator('.geopoint-information .geo-quality')
18+
.getByText(expectedQualityText, { exact: true });
19+
await expect(accuracyQuality).toBeVisible();
20+
}
21+
22+
async expectGeopointFormattedValue(expectedLocation: string[], expectedQuality?: string) {
23+
for (const location of expectedLocation) {
24+
const formattedValue = this.page
25+
.locator('.geopoint-formatted-value')
26+
.getByText(location, { exact: true });
27+
await expect(formattedValue).toBeVisible();
28+
}
29+
30+
if (expectedQuality) {
31+
const quality = this.page
32+
.locator('.geopoint-value .geo-quality')
33+
.getByText(expectedQuality, { exact: true });
34+
await expect(quality).toBeVisible();
35+
}
36+
}
37+
38+
async expectGeopointErrorMessage(expectedMessage: string[]) {
39+
const message = this.page
40+
.locator('.geopoint-error')
41+
.getByText(expectedMessage, { exact: true });
42+
await expect(message).toBeVisible();
43+
}
44+
45+
async openDialog(index = 0) {
46+
const buttons = this.page
47+
.locator('.geopoint-control')
48+
.getByText('Get location', { exact: true });
49+
const button = buttons.nth(index);
50+
await button.scrollIntoViewIfNeeded();
51+
await expect(button).toBeVisible();
52+
await button.click();
53+
}
54+
55+
async saveLocation() {
56+
const button = this.page
57+
.locator('.geo-dialog-footer')
58+
.getByText('Save location', { exact: true });
59+
await expect(button).toBeVisible();
60+
await button.click();
61+
}
62+
63+
async retryCapture() {
64+
const button = this.page.locator('.geopoint-value').getByText('Try again', { exact: true });
65+
await expect(button).toBeVisible();
66+
await button.click();
67+
}
68+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect, Page } from '@playwright/test';
2+
import { GeopointControl } from '../controls/GeopointControl.js';
3+
4+
export class FillFormPage {
5+
private readonly page: Page;
6+
7+
public readonly geopoint: GeopointControl;
8+
9+
constructor(page: Page) {
10+
this.page = page;
11+
this.geopoint = new GeopointControl(page);
12+
}
13+
14+
private async expectTextAtIndex(
15+
locator: string,
16+
expectedText: string,
17+
index: number,
18+
expectVisible = true
19+
) {
20+
const texts = this.page.locator(locator);
21+
const text = texts.nth(index).getByText(expectedText, { exact: true });
22+
23+
if (expectVisible) {
24+
await text.scrollIntoViewIfNeeded();
25+
return expect(text).toBeVisible();
26+
}
27+
28+
return expect(text).not.toBeVisible();
29+
}
30+
31+
private async expectText(locator: string, expectedText: string) {
32+
const text = this.page.locator(locator).getByText(expectedText, { exact: true });
33+
await text.scrollIntoViewIfNeeded();
34+
await expect(text).toBeVisible();
35+
}
36+
37+
async expectNoteAtIndex(expectedNoteText: string, index: number, expectVisible?: boolean) {
38+
await this.expectTextAtIndex(
39+
'.note-control .note-value',
40+
expectedNoteText,
41+
index,
42+
expectVisible
43+
);
44+
}
45+
46+
async expectNote(expectedNoteText: string) {
47+
await this.expectText('.note-control .note-value', expectedNoteText);
48+
}
49+
50+
async expectLabelAtIndex(expectedLabelText: string, index: number, expectVisible?: boolean) {
51+
await this.expectTextAtIndex('.control-text label', expectedLabelText, index, expectVisible);
52+
}
53+
54+
async expectLabel(expectedLabelText: string) {
55+
await this.expectText('.control-text label', expectedLabelText);
56+
}
57+
58+
async expectHintAtIndex(expectedHintText: string, index: number, expectVisible?: boolean) {
59+
await this.expectTextAtIndex('.control-text .hint', expectedHintText, index, expectVisible);
60+
}
61+
62+
async expectHint(expectedHintText: string) {
63+
await this.expectText('.control-text .hint', expectedHintText);
64+
}
65+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { expect, Page } from '@playwright/test';
2+
3+
const DEV_BASE_URL = 'http://localhost:5173';
4+
const BUILD_BASE_URL = 'http://localhost:5174';
5+
6+
export class PreviewPage {
7+
private readonly page: Page;
8+
9+
constructor(page: Page) {
10+
this.page = page;
11+
}
12+
13+
async goToDevPage() {
14+
await this.page.goto(DEV_BASE_URL);
15+
}
16+
17+
async goToBuildPage() {
18+
await this.page.goto(BUILD_BASE_URL);
19+
}
20+
21+
/**
22+
* Opens a preexisting demo form by navigating through the demo forms UI.
23+
*
24+
* @param {string} accordionName - The name of the demo forms category accordion
25+
* @param {string} formLinkName - The exact name of the demo form link to open
26+
* @param {string} formTitle - The expected title of the form, used to verify successful loading
27+
* @returns {Promise<void>} Resolves when the form is successfully loaded and verified
28+
*/
29+
async openDemoForm(accordionName: string, formLinkName: string, formTitle: string) {
30+
const demoSection = this.page
31+
.locator('.dev-form-list-component')
32+
.getByText('Demo Forms for DEV');
33+
await demoSection.scrollIntoViewIfNeeded();
34+
await expect(demoSection).toBeVisible();
35+
36+
const accordion = this.page.locator('.category-list').getByText(accordionName, { exact: true });
37+
await accordion.scrollIntoViewIfNeeded();
38+
await expect(accordion).toBeVisible();
39+
await accordion.click();
40+
41+
const link = this.page.locator('.form-list').getByText(formLinkName, { exact: true });
42+
await link.scrollIntoViewIfNeeded();
43+
await expect(link).toBeVisible();
44+
await link.click();
45+
46+
// Wait for form to load and verify the form title ensuring the form is ready.
47+
const title = this.page.locator('.form-title').getByRole('heading', { name: formTitle });
48+
await expect(title).toBeVisible();
49+
}
50+
}

packages/web-forms/e2e/build/style.test.ts renamed to packages/web-forms/e2e/specs/build/style.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { expect, test } from '@playwright/test';
2+
import { PreviewPage } from '../../page-objects/pages/PreviewPage.js';
23

34
test('Build includes component-defined styles', async ({ page }) => {
4-
await page.goto('http://localhost:5174/');
5+
const previewPage = new PreviewPage(page);
6+
await previewPage.goToBuildPage();
57

68
// This ensures that the application is loaded before proceeding forward.
79
await expect(page.getByText('ODK Web Forms Preview').first()).toBeVisible();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { expect, test } from '@playwright/test';
2+
import { PreviewPage } from '../../page-objects/pages/PreviewPage.js';
3+
4+
test.describe('Web Forms Preview Demo Forms', () => {
5+
test('demo forms load', async ({ context, page }) => {
6+
const previewPage = new PreviewPage(page);
7+
await previewPage.goToBuildPage();
8+
9+
const formPreviewLinks = await page.locator('.form-preview-link').all();
10+
11+
expect(formPreviewLinks.length).toBeGreaterThan(0);
12+
13+
for (const link of formPreviewLinks) {
14+
const [formPreviewPage] = await Promise.all([context.waitForEvent('page'), link.click()]);
15+
16+
await formPreviewPage.waitForSelector(
17+
'.form-initialization-status.error, .form-initialization-status.ready',
18+
{
19+
state: 'attached',
20+
}
21+
);
22+
23+
const [failureDialog] = await formPreviewPage.locator('.form-load-failure-dialog').all();
24+
25+
expect(failureDialog).toBeUndefined();
26+
27+
await formPreviewPage.close();
28+
}
29+
});
30+
});

0 commit comments

Comments
 (0)