-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: add E2E Playwright tests for billing settings pages #16230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| import { faker } from "@faker-js/faker"; | ||
| import { expect, test } from "@playwright/test"; | ||
| import { getFacilityId } from "tests/support/facilityId"; | ||
|
|
||
| import en from "@/public/locale/en.json"; | ||
|
|
||
| test.use({ storageState: "tests/.auth/user.json" }); | ||
|
|
||
| const NO_MATCH_TERM = "ZZZNODATA_XYZ_12345"; | ||
|
|
||
| // ── Shared setup ──────────────────────────────────────────────────────────── | ||
|
|
||
| let facilityId: string; | ||
|
|
||
| test.beforeAll(async () => { | ||
| facilityId = getFacilityId(); | ||
| }); | ||
|
|
||
| // ── Search / Filter helper ────────────────────────────────────────────────── | ||
| // Generates 4 tests for any read-only table page with a search input. | ||
|
|
||
| function searchFilterSuite( | ||
| label: string, | ||
| route: string, | ||
| placeholder: string, | ||
| emptyStateText: string, | ||
| ) { | ||
| test.describe(`Billing Settings - ${label}`, () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto(`/facility/${facilityId}${route}`); | ||
| // Wait for the search input to appear — proves auth + facility data loaded | ||
| await page | ||
| .getByPlaceholder(placeholder) | ||
| .waitFor({ state: "visible", timeout: 60000 }); | ||
| }); | ||
|
Comment on lines
+29
to
+35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
fd 'playwright\.config\.(ts|js|mjs|cjs)$' . -x sh -c '
printf "\n--- %s ---\n" "$1"
sed -n "1,220p" "$1"
' sh {}
printf "\n--- timeout usage in tests/facility/settings/billing/billingSetting.spec.ts ---\n"
sed -n '1,260p' tests/facility/settings/billing/billingSetting.spec.ts \
| nl -ba \
| rg -n 'setTimeout|waitFor\(|timeout:'Repository: ohcnetwork/care_fe Length of output: 2995 🏁 Script executed: #!/bin/bash
# Check for waitFor and setTimeout in the spec file
echo "=== Looking for waitFor calls with timeout values ==="
rg 'waitFor\(' tests/facility/settings/billing/billingSetting.spec.ts -A 2 -B 1
echo ""
echo "=== Looking for test.setTimeout calls ==="
rg 'test\.setTimeout|test\.use' tests/facility/settings/billing/billingSetting.spec.tsRepository: ohcnetwork/care_fe Length of output: 535 Increase the test timeout before the beforeEach hooks consume the entire budget. The global Playwright timeout is 60 seconds. Each test.use({ storageState: "tests/.auth/user.json" });
+test.setTimeout(120000);Also applies to: lines 116-124, 183-189 🤖 Prompt for AI Agents |
||
|
|
||
| test("shows table with rows on navigation", async ({ page }) => { | ||
| await expect(page.getByRole("table")).toBeVisible(); | ||
| // nth(1) is the first data row (nth(0) is the header row) | ||
| await expect( | ||
| page.getByRole("table").getByRole("row").nth(1), | ||
| ).toBeVisible(); | ||
| }); | ||
|
|
||
| test("matching search shows relevant rows", async ({ page }) => { | ||
| // Read the first data-row's first cell at runtime to avoid hardcoding | ||
| const firstDataRow = page.getByRole("table").getByRole("row").nth(1); | ||
| const cellText = | ||
| (await firstDataRow.getByRole("cell").first().textContent()) ?? ""; | ||
| const searchTerm = cellText.trim().slice(0, 4); | ||
| if (!searchTerm) { | ||
| throw new Error( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why? does it make sense to throw an error here? |
||
| `Could not read cell text to derive a search term on route ${route}`, | ||
| ); | ||
| } | ||
|
|
||
| await page.getByPlaceholder(placeholder).fill(searchTerm); | ||
|
|
||
| await expect(firstDataRow).toBeVisible(); | ||
| await expect( | ||
| page.getByText(emptyStateText, { exact: true }), | ||
| ).not.toBeVisible(); | ||
| }); | ||
|
|
||
| test("non-matching search shows empty state", async ({ page }) => { | ||
| await page.getByPlaceholder(placeholder).fill(NO_MATCH_TERM); | ||
|
|
||
| await expect( | ||
| page.getByText(emptyStateText, { exact: true }), | ||
| ).toBeVisible(); | ||
| }); | ||
|
|
||
| test("clearing search restores all rows", async ({ page }) => { | ||
| const searchInput = page.getByPlaceholder(placeholder); | ||
| await searchInput.fill(NO_MATCH_TERM); | ||
| await expect( | ||
| page.getByText(emptyStateText, { exact: true }), | ||
| ).toBeVisible(); | ||
|
|
||
| await searchInput.clear(); | ||
|
|
||
| await expect( | ||
| page.getByRole("table").getByRole("row").nth(1), | ||
| ).toBeVisible(); | ||
| await expect( | ||
| page.getByText(emptyStateText, { exact: true }), | ||
| ).not.toBeVisible(); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| searchFilterSuite( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. refer to how other tests are handled; write tests that call a common search function, rather than writing a common fn that wraps tests. |
||
| "Tax Codes", | ||
| "/settings/billing/tax_codes", | ||
| en.search_tax_codes, | ||
| en.no_matching_tax_codes, | ||
| ); | ||
|
|
||
| searchFilterSuite( | ||
| "Tax Components", | ||
| "/settings/billing/tax_components", | ||
| en.search_tax_components, | ||
| en.no_matching_tax_components, | ||
| ); | ||
|
|
||
| searchFilterSuite( | ||
| "Informational Codes", | ||
| "/settings/billing/informational_codes", | ||
| en.search_informational_codes, | ||
| en.no_matching_informational_codes, | ||
| ); | ||
|
|
||
| // ── Edit / Save Pages ─────────────────────────────────────────────────────── | ||
|
|
||
| test.describe("Billing Settings - Discount Configuration", () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto( | ||
| `/facility/${facilityId}/settings/billing/discount_configuration`, | ||
| ); | ||
| // Wait for the Edit button to appear — proves auth + facility data loaded | ||
| await page | ||
| .getByRole("button", { name: en.edit }) | ||
| .waitFor({ state: "visible", timeout: 60000 }); | ||
| }); | ||
|
|
||
| test("shows current values in read-only view on navigation", async ({ | ||
| page, | ||
| }) => { | ||
| await expect(page.getByRole("button", { name: en.edit })).toBeVisible(); | ||
| await expect( | ||
| page.getByText(en.max_applicable_discounts, { exact: true }), | ||
| ).toBeVisible(); | ||
| await expect( | ||
| page.getByText(en.applicability_order, { exact: true }), | ||
| ).toBeVisible(); | ||
| }); | ||
|
|
||
| test("clicking Edit shows pre-filled input fields", async ({ page }) => { | ||
| await page.getByRole("button", { name: en.edit }).click(); | ||
|
|
||
| const maxInput = page.getByLabel(en.max_applicable_discounts); | ||
| await expect(maxInput).toBeVisible(); | ||
|
|
||
| // Value must be a whole number pre-filled from current config | ||
| await expect(maxInput).toHaveValue(/^\d+$/); | ||
|
|
||
| await expect(page.getByLabel(en.applicability_order)).toBeVisible(); | ||
| await expect(page.getByRole("button", { name: en.save })).toBeVisible(); | ||
| await expect(page.getByRole("button", { name: en.cancel })).toBeVisible(); | ||
| }); | ||
|
|
||
| test("modifying values and saving shows success toast", async ({ page }) => { | ||
| await page.getByRole("button", { name: en.edit }).click(); | ||
|
|
||
| const maxInput = page.getByLabel(en.max_applicable_discounts); | ||
| const newValue = String(faker.number.int({ min: 1, max: 10 })); | ||
| await maxInput.fill(newValue); | ||
|
|
||
| await page.getByRole("button", { name: en.save }).click(); | ||
|
|
||
| await expect( | ||
| page.getByText(en.discount_configuration_saved, { exact: true }), | ||
| ).toBeVisible({ timeout: 5000 }); | ||
| // Read-only view restored | ||
| await expect(page.getByRole("button", { name: en.edit })).toBeVisible(); | ||
| }); | ||
|
Comment on lines
+152
to
+166
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restore the settings you persist in the save-path tests.
Also applies to: 212-232 🤖 Prompt for AI Agents |
||
|
|
||
| test("cancelling edit restores original values", async ({ page }) => { | ||
| await page.getByRole("button", { name: en.edit }).click(); | ||
|
|
||
| const maxInput = page.getByLabel(en.max_applicable_discounts); | ||
| await maxInput.fill("999"); | ||
|
|
||
| await page.getByRole("button", { name: en.cancel }).click(); | ||
|
|
||
| // Edit form must be gone — edit button back, inputs hidden | ||
| await expect(page.getByRole("button", { name: en.edit })).toBeVisible(); | ||
| await expect(maxInput).not.toBeVisible(); | ||
| }); | ||
|
Comment on lines
+126
to
+179
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discount Configuration needs a real state oracle. This suite mostly checks labels and mode switches. It never proves that the read-only view shows the current values, that ♻️ Suggested pattern test("modifying values and saving shows success toast", async ({ page }) => {
await page.getByRole("button", { name: en.edit }).click();
const maxInput = page.getByLabel(en.max_applicable_discounts);
- const newValue = String(faker.number.int({ min: 1, max: 10 }));
+ const originalValue = await maxInput.inputValue();
+ const newValue = originalValue === "1" ? "2" : "1";
await maxInput.fill(newValue);
await page.getByRole("button", { name: en.save }).click();
await expect(
page.getByText(en.discount_configuration_saved, { exact: true }),
).toBeVisible({ timeout: 5000 });
- // Read-only view restored
- await expect(page.getByRole("button", { name: en.edit })).toBeVisible();
+ await page.getByRole("button", { name: en.edit }).click();
+ await expect(page.getByLabel(en.max_applicable_discounts)).toHaveValue(newValue);
});
test("cancelling edit restores original values", async ({ page }) => {
await page.getByRole("button", { name: en.edit }).click();
const maxInput = page.getByLabel(en.max_applicable_discounts);
- await maxInput.fill("999");
+ const originalValue = await maxInput.inputValue();
+ const draftValue = originalValue === "999" ? "998" : "999";
+ await maxInput.fill(draftValue);
await page.getByRole("button", { name: en.cancel }).click();
- // Edit form must be gone — edit button back, inputs hidden
- await expect(page.getByRole("button", { name: en.edit })).toBeVisible();
- await expect(maxInput).not.toBeVisible();
+ await page.getByRole("button", { name: en.edit }).click();
+ await expect(page.getByLabel(en.max_applicable_discounts)).toHaveValue(originalValue);
});Do the same for 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| test.describe("Billing Settings - Invoice Number Expression", () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto(`/facility/${facilityId}/settings/billing/settings`); | ||
| // Wait for the Edit button to appear — proves auth + facility data loaded | ||
| await page | ||
| .getByRole("button", { name: en.edit }) | ||
| .waitFor({ state: "visible", timeout: 60000 }); | ||
| }); | ||
|
|
||
| test("shows current expression in read-only view on navigation", async ({ | ||
| page, | ||
| }) => { | ||
| await expect(page.getByRole("button", { name: en.edit })).toBeVisible(); | ||
| await expect( | ||
| page.getByRole("heading", { name: en.invoice_number_expression }).first(), | ||
| ).toBeVisible(); | ||
| }); | ||
|
|
||
| test("clicking Edit shows pre-filled input field", async ({ page }) => { | ||
| await page.getByRole("button", { name: en.edit }).click(); | ||
|
|
||
| const input = page.getByRole("textbox", { | ||
| name: en.invoice_number_expression, | ||
| }); | ||
| await expect(input).toBeVisible(); | ||
| await expect(input).toBeEnabled(); | ||
| await expect(page.getByRole("button", { name: en.save })).toBeVisible(); | ||
| await expect(page.getByRole("button", { name: en.cancel })).toBeVisible(); | ||
| }); | ||
|
Comment on lines
+191
to
+210
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assert the existing invoice expression, not just the heading. The current checks only prove that the section renders and that a draft value disappears. If cancel blanks the stored expression instead of restoring it, this suite still passes. Capture the original expression, assert the textbox is prefilled with it, and after cancel verify that same value is shown again. 🧪 Minimal improvement test("clicking Edit shows pre-filled input field", async ({ page }) => {
await page.getByRole("button", { name: en.edit }).click();
const input = page.getByRole("textbox", {
name: en.invoice_number_expression,
});
await expect(input).toBeVisible();
await expect(input).toBeEnabled();
+ await expect(input).toHaveValue(/\S+/);
await expect(page.getByRole("button", { name: en.save })).toBeVisible();
await expect(page.getByRole("button", { name: en.cancel })).toBeVisible();
});
test("cancelling edit restores original expression", async ({ page }) => {
await page.getByRole("button", { name: en.edit }).click();
const input = page.getByRole("textbox", {
name: en.invoice_number_expression,
});
+ const originalExpression = await input.inputValue();
await input.fill("TEMP_EXPR_XYZ");
await page.getByRole("button", { name: en.cancel }).click();
await expect(page.getByRole("button", { name: en.edit })).toBeVisible();
await expect(input).not.toBeVisible();
await expect(page.getByText("TEMP_EXPR_XYZ")).not.toBeVisible();
+ await expect(page.getByText(originalExpression, { exact: true })).toBeVisible();
});Also applies to: 234-249 🤖 Prompt for AI Agents |
||
|
|
||
| test("modifying expression and saving shows success message", async ({ | ||
| page, | ||
| }) => { | ||
| await page.getByRole("button", { name: en.edit }).click(); | ||
|
|
||
| const input = page.getByRole("textbox", { | ||
| name: en.invoice_number_expression, | ||
| }); | ||
| // Use the exact documented expression format (f-string with supported variables) | ||
| const counter = faker.number.int({ min: 1000, max: 9999 }); | ||
| const newExpression = `f'#INV-{invoice_count + ${counter}}-{current_year_yy}'`; | ||
| await input.fill(newExpression); | ||
|
|
||
| await page.getByRole("button", { name: en.save }).click(); | ||
|
|
||
| await expect( | ||
| page.getByText(en.saved_successfully, { exact: true }), | ||
| ).toBeVisible(); | ||
| // Read-only view must reflect the saved expression | ||
| await expect(page.getByText(newExpression, { exact: true })).toBeVisible(); | ||
| }); | ||
|
|
||
| test("cancelling edit restores original expression", async ({ page }) => { | ||
| await page.getByRole("button", { name: en.edit }).click(); | ||
|
|
||
| const input = page.getByRole("textbox", { | ||
| name: en.invoice_number_expression, | ||
| }); | ||
| await input.fill("TEMP_EXPR_XYZ"); | ||
|
|
||
| await page.getByRole("button", { name: en.cancel }).click(); | ||
|
|
||
| // Edit form must be gone — edit button back, input hidden | ||
| await expect(page.getByRole("button", { name: en.edit })).toBeVisible(); | ||
| await expect(input).not.toBeVisible(); | ||
| // Unsaved value must not appear anywhere on the page | ||
| await expect(page.getByText("TEMP_EXPR_XYZ")).not.toBeVisible(); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why?