Skip to content
Open
Changes from all commits
Commits
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
250 changes: 250 additions & 0 deletions tests/facility/settings/billing/billingSetting.spec.ts
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";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why?


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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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.ts

Repository: 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 beforeEach hook waits up to 60 seconds, exhausting the entire timeout before any assertions run. This spec fails to override the timeout.

 test.use({ storageState: "tests/.auth/user.json" });
+test.setTimeout(120000);

Also applies to: lines 116-124, 183-189

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/facility/settings/billing/billingSetting.spec.ts` around lines 29 - 35,
The beforeEach hooks (test.beforeEach) are each waiting up to the global 60s
Playwright timeout (via .waitFor with timeout: 60000) which exhausts the test
budget; add a longer test timeout for this spec by calling test.setTimeout(...)
near the top of the file (before the beforeEach blocks) or increase timeouts on
the specific hooks/tests; update the file to call test.setTimeout(120000) (or
another suitable value) before the test.beforeEach usages so the beforeEach
waits don't consume the entire test timeout (also apply the same change for the
other beforeEach blocks referenced).


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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore the settings you persist in the save-path tests.

tests/support/facilityId.ts:1-43 reuses a shared facility, so both save tests leave backend state changed for retries and later specs. If one of these tests fails after saving, the retry starts from already-edited billing settings. Restore the original values in a finally block after the success assertions.

Also applies to: 212-232

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/facility/settings/billing/billingSetting.spec.ts` around lines 152 -
166, Capture the original billing value before editing in the test "modifying
values and saving shows success toast" (e.g., read current value from
page.getByLabel(en.max_applicable_discounts) into a variable), perform the
fill/click/asserts as written, and then add a finally block that restores the
original value by refilling the saved original and clicking the save button (use
the same selectors: page.getByLabel(en.max_applicable_discounts),
page.getByRole("button", { name: en.save })). Apply the same pattern to the
other save test (the one around lines 212-232) so both tests always revert
backend state even on failures.


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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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 applicability_order is prefilled correctly, that save actually persisted a different value, or that cancel restored the previous one. A blank read-only section or a no-op save can still pass here, especially because the random 1..10 can equal the current value.

♻️ 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 applicability_order, not just its visibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/facility/settings/billing/billingSetting.spec.ts` around lines 126 -
179, The tests lack a state oracle: in the "shows current values in read-only
view on navigation" and subsequent tests (functions referencing
en.max_applicable_discounts, en.applicability_order, maxInput, and
en.discount_configuration_saved) assert only visibility; update them to read the
current displayed values (capture the read-only text for
max_applicable_discounts and applicability_order), assert those match the
expected seed/state, then when clicking Edit assert the inputs (maxInput and the
applicability_order input) are pre-filled with those exact values; for the save
test, choose a new value deterministically different from the current value
(e.g., read current value and increment or toggle) before filling and saving,
then assert the read-only view now shows the new value and the success toast
(en.discount_configuration_saved); for the cancel test, record the original
read-only values, change inputs, click cancel, and assert the read-only view
still shows the original values and the inputs are hidden.

});

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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
Verify each finding against the current code and only fix it if needed.

In `@tests/facility/settings/billing/billingSetting.spec.ts` around lines 191 -
210, Capture the current invoice expression text from the read-only view before
entering edit mode, then assert the textbox (obtained via
page.getByRole("textbox", { name: en.invoice_number_expression }) assigned to
input) is prefilled with that exact value after clicking the Edit button; after
clicking Cancel assert the read-only heading/field again shows the original
captured expression. Apply the same change to the similar test that checks
cancel behavior (the second test for invoice_number_expression) so both verify
preservation of the stored expression rather than only presence of
headings/buttons.


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();
});
});
Loading