Skip to content

Commit

Permalink
[Issue #1757]: E2E tests for search page (#1974)
Browse files Browse the repository at this point in the history
## Summary
Fixes #1757 

## Changes proposed
- Add e2e tests for search page
- Add e2e test (with FE and API) to its own CI job
(`ci-frontend-ci.yml`)
    - invokes shell script to wait until API is loaded
  • Loading branch information
rylew1 authored May 14, 2024
1 parent 1979a7d commit 85f3e0a
Show file tree
Hide file tree
Showing 12 changed files with 421 additions and 28 deletions.
61 changes: 61 additions & 0 deletions .github/workflows/ci-frontend-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Frontend E2E Tests

on:
workflow_call:
pull_request:
paths:
- frontend/**
- .github/workflows/ci-frontend-e2e.yml

defaults:
run:
working-directory: ./frontend

env:
NODE_VERSION: 18
LOCKFILE_PATH: ./frontend/package-lock.json
PACKAGE_MANAGER: npm

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
e2e-tests:
name: Run E2E Tests
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: ${{ env.PACKAGE_MANAGER }}
cache-dependency-path: ${{ env.LOCKFILE_PATH }}

- run: npm ci

- name: Install Playwright Browsers
run: npx playwright install --with-deps

- name: Start API Server for e2e tests
run: |
cd ../api
make init db-seed-local start &
cd ../frontend
# Ensure the API wait script is executable
chmod +x ../api/bin/wait-for-api.sh
../api/bin/wait-for-api.sh
shell: bash

- name: Run E2E Tests
run: npm run test:e2e

- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: ./frontend/playwright-report/
retention-days: 30
15 changes: 1 addition & 14 deletions .github/workflows/ci-frontend.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Front-end Checks
name: Frontend Checks

on:
workflow_call:
Expand Down Expand Up @@ -34,9 +34,6 @@ jobs:
cache: ${{ env.PACKAGE_MANAGER }}
- run: npm ci

- name: Install Playwright Browsers
run: npx playwright install --with-deps

- name: Run lint
run: npm run lint

Expand All @@ -58,16 +55,6 @@ jobs:
skip-step: none
output: comment

- name: Run e2e tests
run: npm run test:e2e

- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: ./frontend/playwright-report/
retention-days: 30

# Confirms the front end still builds successfully
check-frontend-builds:
name: FE Build Check
Expand Down
3 changes: 3 additions & 0 deletions api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ coverage.*
# VSCode Workspace
*.code-workspace
.vscode

#e2e
/test-results/
30 changes: 30 additions & 0 deletions api/bin/wait-for-api.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash
# wait-for-api.sh

set -e

# Color formatting for readability
GREEN='\033[0;32m'
RED='\033[0;31m'
NO_COLOR='\033[0m'

MAX_WAIT_TIME=800 # seconds, adjust as necessary
WAIT_TIME=0

echo "Waiting for API server to become ready..."

# Use curl to check the API server health endpoint
until curl --output /dev/null --silent --head --fail http://localhost:8080/health;
do
printf '.'
sleep 5

WAIT_TIME=$(($WAIT_TIME + 5))
if [ $WAIT_TIME -gt $MAX_WAIT_TIME ]
then
echo -e "${RED}ERROR: API server did not become ready within ${MAX_WAIT_TIME} seconds.${NO_COLOR}"
exit 1
fi
done

echo -e "${GREEN}API server is ready after ~${WAIT_TIME} seconds.${NO_COLOR}"
2 changes: 2 additions & 0 deletions frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ module.exports = {
"@typescript-eslint/no-unused-vars": "error",
// The usage of `any` defeats the purpose of typescript. Consider using `unknown` type instead instead.
"@typescript-eslint/no-explicit-any": "error",
// Just warn since playwright tests may not use screen the way jest would
"testing-library/prefer-screen-queries": "warn",
},
},
],
Expand Down
6 changes: 6 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ npm-debug.log*

# uswds assets
/public/uswds

# playwright e2e
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
24 changes: 12 additions & 12 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.0.2",
"@playwright/test": "^1.42.0",
"@playwright/test": "^1.44.0",
"@storybook/addon-designs": "^7.0.1",
"@storybook/addon-essentials": "^7.1.0",
"@storybook/nextjs": "^7.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const SearchFilterCheckbox: React.FC<SearchFilterCheckboxProps> = ({
onChange={handleChange}
disabled={!mounted}
checked={option.isChecked === true}
// value={option.id} // TODO: consider poassing explicit value
// value={option.id} // TODO: consider passing explicit value
/>
);
};
Expand Down
170 changes: 170 additions & 0 deletions frontend/tests/e2e/search/search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
clickAccordionWithTitle,
clickMobileNavMenu,
clickSearchNavLink,
expectCheckboxIDIsChecked,
expectSortBy,
expectURLContainsQueryParam,
fillSearchInputAndSubmit,
getMobileMenuButton,
getSearchInput,
hasMobileMenu,
refreshPageWithCurrentURL,
selectSortBy,
toggleCheckboxes,
waitForSearchResultsLoaded,
} from "./searchUtil";
import { expect, test } from "@playwright/test";

test("should navigate from index to search page", async ({ page }) => {
// Start from the index page with feature flag set
await page.goto("/?_ff=showSearchV0:true");

// Mobile chrome must first click the menu button
if (await hasMobileMenu(page)) {
const menuButton = getMobileMenuButton(page);
await clickMobileNavMenu(menuButton);
}

await clickSearchNavLink(page);

// Verify that the new URL is correct
expectURLContainsQueryParam(page, "status", "forecasted,posted");

// Verify the presence of "Search" content on the page
await expect(page.locator("h1")).toContainText(
"Search funding opportunities",
);

// Verify that the 'forecasted' and 'posted' are checked
await expectCheckboxIDIsChecked(page, "#status-forecasted");
await expectCheckboxIDIsChecked(page, "#status-posted");
});

test.describe("Search page tests", () => {
test.beforeEach(async ({ page }) => {
// Navigate to the search page with the feature flag set
await page.goto("/search?_ff=showSearchV0:true");
});

test("should return 0 results when searching for obscure term", async ({
page,
browserName,
}) => {
// TODO (Issue #2005): fix test for webkit
test.skip(
browserName === "webkit",
"Skipping test for WebKit due to a query param issue.",
);

const searchTerm = "0resultearch";

await fillSearchInputAndSubmit(searchTerm, page);

expectURLContainsQueryParam(page, "query", searchTerm);

const resultsHeading = page.getByRole("heading", {
name: /0 Opportunities/i,
});
await expect(resultsHeading).toBeVisible();

await expect(page.locator("div.usa-prose h2")).toHaveText(
"Your search did not return any results.",
);
});

test("should show and hide loading state", async ({ page, browserName }) => {
// TODO (Issue #2005): fix test for webkit
test.skip(
browserName === "webkit",
"Skipping test for WebKit due to a query param issue.",
);
const searchTerm = "advanced";
await fillSearchInputAndSubmit(searchTerm, page);

const loadingIndicator = page.locator("text='Loading results...'");
await expect(loadingIndicator).toBeVisible();
await expect(loadingIndicator).toBeHidden();

const searchTerm2 = "agency";
await fillSearchInputAndSubmit(searchTerm2, page);
await expect(loadingIndicator).toBeVisible();
await expect(loadingIndicator).toBeHidden();
});
test("should retain filters in a new tab", async ({ page }) => {
// Set all inputs, then refresh the page. Those same inputs should be
// set from query params.
const searchTerm = "education";
const statusCheckboxes = {
"status-forecasted": "forecasted",
"status-posted": "posted",
};
const fundingInstrumentCheckboxes = {
"funding-instrument-cooperative_agreement": "cooperative_agreement",
"funding-instrument-grant": "grant",
};

const eligibilityCheckboxes = {
"eligibility-state_governments": "state_governments",
"eligibility-county_governments": "county_governments",
};
const agencyCheckboxes = {
ARPAH: "ARPAH",
AC: "AC",
};
const categoryCheckboxes = {
"category-recovery_act": "recovery_act",
"category-agriculture": "agriculture",
};

await selectSortBy(page, "agencyDesc");

await waitForSearchResultsLoaded(page);
await fillSearchInputAndSubmit(searchTerm, page);
await toggleCheckboxes(page, statusCheckboxes, "status");

await clickAccordionWithTitle(page, "Funding instrument");
await toggleCheckboxes(
page,
fundingInstrumentCheckboxes,
"fundingInstrument",
);

await clickAccordionWithTitle(page, "Eligibility");
await toggleCheckboxes(page, eligibilityCheckboxes, "eligibility");

await clickAccordionWithTitle(page, "Agency");
await toggleCheckboxes(page, agencyCheckboxes, "agency");

await clickAccordionWithTitle(page, "Category");
await toggleCheckboxes(page, categoryCheckboxes, "category");

/***********************************************************/
/* Page refreshed should have all the same inputs selected
/***********************************************************/

await refreshPageWithCurrentURL(page);

// Expect search inputs are retained in the new tab
await expectSortBy(page, "agencyDesc");
const searchInput = getSearchInput(page);
await expect(searchInput).toHaveValue(searchTerm);

for (const [checkboxID] of Object.entries(statusCheckboxes)) {
await expectCheckboxIDIsChecked(page, `#${checkboxID}`);
}

for (const [checkboxID] of Object.entries(fundingInstrumentCheckboxes)) {
await expectCheckboxIDIsChecked(page, `#${checkboxID}`);
}
for (const [checkboxID] of Object.entries(eligibilityCheckboxes)) {
await expectCheckboxIDIsChecked(page, `#${checkboxID}`);
}
for (const [checkboxID] of Object.entries(agencyCheckboxes)) {
await expectCheckboxIDIsChecked(page, `#${checkboxID}`);
}
for (const [checkboxID] of Object.entries(categoryCheckboxes)) {
await expectCheckboxIDIsChecked(page, `#${checkboxID}`);
}
});
});
Loading

1 comment on commit 85f3e0a

@github-actions
Copy link

Choose a reason for hiding this comment

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

Coverage report for ./frontend

St.
Category Percentage Covered / Total
🟢 Statements 84.14% 870/1034
🟡 Branches 65.01% 223/343
🟡 Functions 75.58% 164/217
🟢 Lines 84.18% 809/961

Test suite run success

164 tests passing in 56 suites.

Report generated by 🧪jest coverage report action from 85f3e0a

Please sign in to comment.