Skip to content

feat(promo-codes): bulk domain input Tier 1 (paste + modal + compact summary)#947

Merged
smarcet merged 12 commits into
masterfrom
feature/domain-authorized-promo-codes-bulk-input
May 20, 2026
Merged

feat(promo-codes): bulk domain input Tier 1 (paste + modal + compact summary)#947
smarcet merged 12 commits into
masterfrom
feature/domain-authorized-promo-codes-bulk-input

Conversation

@caseylocker
Copy link
Copy Markdown

@caseylocker caseylocker commented May 18, 2026

ref: https://app.clickup.com/t/86b9z4d3d

Summary

Ships Tier 1 + Tier 1.1 of the Domain-Authorized Promo Codes bulk-input frontend per SDS PR #15. Admins editing a DOMAIN_AUTHORIZED_PROMO_CODE or DOMAIN_AUTHORIZED_DISCOUNT_CODE can now paste 700–1,400 allowed_email_domains entries into a single modal without melting the chip wall — and the bulk-paste entry point is reachable from any list size, including a fresh empty form.

  • New ManageAllowedEmailDomainsModal opened from a Manage List button rendered next to the field label at all counts (Tier 1.1 — threshold-independent). Modal hosts a paste textarea + post-add toast tally ("Added X · Y invalid · Z duplicates") + read-only virtualized list (react-window FixedSizeList) + Cancel/Done modal-scoped semantics.
  • New bulk-input-parser.js pure utility: parseTextBlob (newline/comma/tab/semicolon), normalizeEntry (preserves user-typed casing, auto-prefixes @ for bare domains including single-char SLDs like q.io), classifyEntries (case-insensitive dedup against existing + within input).
  • AllowedEmailDomainsRow grows a compact-summary branch above the 50-entry threshold (count badge + type-mix hint + example); chip wall is preserved byte-identical at ≤ 50. Tightens existing single-entry chip-input dedup to case-insensitive (Locked Decision 6).
  • Tier 1 i18n keys under edit_promocode.manage_modal.* (7) and edit_promocode.large_list.* (3). Tier 1.1 adds edit_promocode.manage_button (renamed from edit_promocode.large_list.manage_button since the button is no longer compact-summary-specific). No Tier 2 keys.
  • One new dep: react-window@^1.8.10 (~6 KB gzipped, lives behind Manage List static-import chain — see SDS Gotcha "react-window bundle cost").

Tier 1.1 (commit 28ea322d) — reconciliation with JP pass-1 framing: field testing surfaced a UX cliff at ≤ 50 entries where the inline + add one chip input was the only add path (and <input type="text"> flattens pasted multi-line lists). JP's pass-1 SDS directive treats "Paste-to-add" as a top-level Tier 1 capability separate from "Compact summary above threshold" — they were meant to be parallel affordances, not nested. The Manage List button is lifted out of the compact-summary block to a sibling next to the field label, rendered at all counts. Tier 1 read-only modal scope is unchanged. SDS amendment in companion docs PR captures the reconciliation; Locked Decision #1 relaxed from "modal opened from Manage List above threshold" to "modal opened from Manage List, threshold-independent."

Companion PR: OpenStackweb/summit-api#546 (Track 1 telemetry + lookup-build) — same prod push, week of 2026-05-19.

Out of scope (Tier 2, deferred): search input, type filter, per-row checkbox, Select All, Delete Selected. Per JP scope decision pass-3.

ClickUp: https://app.clickup.com/t/86b9z4d3d

Test plan

  • CI: full Jest suite green (yarn test) — verified locally 607/607 passing; promocode-form bundle re-verified 2026-05-19 at 113/113 (post-Tier 1.1; +1 new assertion for empty-form button presence) in 1.6s
  • Open a DOMAIN_AUTHORIZED_* promo code edit form with < 50 entries — chip wall renders unchanged (regression check) — verified 2026-05-19 against api.dev.fnopen.com summit 69
  • Open same form with > 50 entries — compact summary renders; chip wall hidden — verified 2026-05-19 (saved code id 3909 with 54 entries)
  • Tier 1.1: Manage List button visible at all counts including length === 0 — verified 2026-05-19 on a fresh new-promo-code form against api.dev.fnopen.com summit 69; modal opens empty, paste → Add Domains → Done populates the chip wall
  • Click Manage List — modal opens with current allowed_email_domains as the initial working copy — verified 2026-05-19
  • Paste mixed input (one valid, one duplicate, one invalid, one bare domain) — toast tally accurate; valid entries appear in list — verified 2026-05-19 (test blob result: 6 added · 1 invalid · 2 duplicates)
  • Cmd/Ctrl+Enter inside the textarea fires Add Domains — not exercised locally; defer to staging
  • Cancel — modal closes, form state unchanged — verified 2026-05-19 (post 700-domain DevTools soak)
  • Done — modal closes, form's allowed_email_domains reflects working copy — verified 2026-05-19 via Save round-trip
  • Save the form — entity persists with the new array — verified 2026-05-19 (POST → 200, code id 3909 with 54 entries on api.dev.fnopen.com/api/v1/summits/69/promo-codes)
  • Re-open the form — compact-summary count matches saved length — verified 2026-05-19 (54/54 round-trip via subsequent GET with expand=allowed_email_domains)
  • Single-entry chip-input now blocks @ACME.com when @acme.com is already present (case-insensitive dedup) — pinned by unit test __tests__/allowed-email-domains-row.test.jsx; UI path not exercised locally

Local manual test pass — 2026-05-19

Exercised against api.dev.fnopen.com summit 69 from localhost:8080 (webpack-dev-server). Tier 1 verification at HEAD e1566ac1 then Tier 1.1 verification at HEAD 28ea322d.

Tier 1 (HEAD e1566ac1)

Threshold temporarily lowered to 3 to reach the modal flow without manually entering 51 chips; reverted to 50 before final jest run. Working tree clean post-revert.

Items verified beyond the Test plan checklist above:

  • 700-domain virtualization soak. Pasted a synthetic 700-entry blob (@company{0..699}.example) into the Manage List modal via a DevTools setter snippet on the textarea, clicked Add Domains. Toast: Added 700 · skipped 0 invalid · 0 duplicates. DevTools Elements panel confirmed react-window virtualization — DOM stayed at ~10 rendered row divs while scrolling 754 entries (saved 54 + soak 700), not 754. Smooth scroll. Cancel discarded the working copy as expected.
  • CodeRabbit fix e1566ac1 verified end-to-end. With the inline error showing (typed garbage into + add one → Enter → red "Each entry must be an exact domain..." text + garbage retained in input per commit() early-return), opening the modal and clicking Done without modifications cleared both the local domainsError (red text) and draft (inline input value). Pre-fix path would have left both stale.
  • Save → reload round-trip. POST /api/v1/summits/69/promo-codes returned 200 with the new entity, frontend immediately re-fetched via GET ?expand=...allowed_email_domains, page re-rendered the compact summary with 54 domains configured + correct type mix + first-entry example.

Tier 1.1 (HEAD 28ea322d)

Manage List button now always-on. Codex second-pass review completed before commit (Major: clean; Minor: stale ref in untracked plan file, not committed; Nit: SDS line-number drift fixed).

  • Existing 54-domain compact mode at code 3909. Reloaded the edit page; Manage List button rendered as btn-xs next to the field label, no longer inside the compact summary block. Click → modal opens with all 54 entries in the virtualized list.
  • Empty-form entry point. Fresh /promocodes/new form, Class Name DOMAIN_AUTHORIZED_PROMO_CODE, code TEST-DA-EMPTY, no domains entered. Manage List button visible. Click → modal opens empty. Paste 3 domains → Add Domains → Done → 3 chips appear in the parent chip wall. Save → POST round-trip green, navigates to the new code's edit page.
  • Button label text. Renders literal "Manage List" (not a raw i18n key), confirming the rename of edit_promocode.large_list.manage_buttonedit_promocode.manage_button resolved in both branches.

Real-world failure mode worth noting (legacy, not in scope for Tier 1): the validator passes @three.cmo-style typos because it only requires @... shape with no real TLD check. The Tier 1 modal is read-only, so a customer pasting a 700-domain list can't spot/fix these from inside the modal — they have to drop back to chip-wall mode (≤ 50 entries) to remove. Filed under "Tier 2 search/filter would surface this."

Pre-prod follow-ups status:

  • ✅ Tier 1 + Tier 1.1 local exercise complete (paste, virtualize, save, round-trip, e1566ac inline-clear, always-on entry point).
  • ⏳ Staging soak with the actual customer ~700-domain list still owed — synthetic 700-entry soak proves the virtualization + add-domains path, not customer data validity.
  • ⏳ Companion summit-api#546 T1-Sanity measurement still owed (needs staging deploy of fix: extra questions read only conditions #546).

Manual staging soak (REQUIRED before prod merge)

Per SDS Rollout Plan:

  • Coordinate deploy with the companion summit-api Track 1 PR
  • Paste the actual ~700-domain customer list from the OCP source data into a DOMAIN_AUTHORIZED_* code on staging
  • Verify Add Domains tally, Done bubble-up, Save persistence, re-open count
  • Capture any surprises as tribal-knowledge/ entries in the fn-skills vault

Summary by CodeRabbit

  • New Features
    • Modal for bulk managing allowed email domains with paste-and-parse controls and toast summaries
    • Large lists show a compact summary with a persistent “Manage” button and inline add input
    • Domain entry deduplication is case-insensitive; bare-domain normalization applied
    • Improved validation behavior and scroll-to-error reliability for large domain lists
    • Added localization strings for the manage flow and summaries

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

This PR adds react-window, a ManageAllowedEmailDomainsModal, bulk-input parsing/classification (parseTextBlob/normalizeEntry/classifyEntries), a compact large-list UI in AllowedEmailDomainsRow, i18n strings, and unit/integration tests exercising virtualization, parsing, deduplication, and scroll-to-error behavior.

Changes

Virtualized email domain list management

Layer / File(s) Summary
Dependencies and localization
package.json, src/i18n/en.json
Adds react-window@^1.8.10 and i18n strings for the manage modal and large-list summary.
Bulk domain parsing and classification
src/components/forms/promocode-form/forms/domain-authorized/bulk-input-parser.js, src/components/forms/promocode-form/forms/domain-authorized/__tests__/bulk-input-parser.test.js
Adds parseTextBlob, normalizeEntry, classifyEntries, and tests for separators, trimming, sourceRow indexing, auto-prefixing, case-insensitive dedup, and classification into valid/invalid/duplicates.
Modal component for domain management
src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx, src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx
New modal that snapshots existing domains, parses/classifies pasted text on Add/Cmd+Enter, shows add/skip/duplicate counts, renders a react-window virtualized list, and applies working via onApply on Done.
Row component refactor and modal integration
src/components/forms/promocode-form/forms/domain-authorized/AllowedEmailDomainsRow.jsx, src/components/forms/promocode-form/forms/domain-authorized/__tests__/allowed-email-domains-row.test.jsx
Refactors row to show chip-wall for ≤50 entries or compact summary with Manage button for >50; keeps inline add input in both modes; uses case-insensitive dedup on commit; wires modal open/apply; tests threshold, dedup, Enter-commit, and #allowed_email_domains presence.
Integration tests with virtualization support
src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js
Adds react-window FixedSizeList mock to enable virtualized-list assertions in jsdom; adds regression test for scrollToError targeting compact wrapper and an end-to-end bulk-edit flow verifying merged/deduplicated submission.

Sequence Diagram

sequenceDiagram
  participant User
  participant AllowedEmailDomainsRow
  participant ManageModal as ManageAllowedEmailDomainsModal
  participant Parser as parseTextBlob/classifyEntries
  participant List as FixedSizeList

  User->>AllowedEmailDomainsRow: view domain list (>50 entries)
  AllowedEmailDomainsRow->>AllowedEmailDomainsRow: render compact summary + Manage button
  User->>AllowedEmailDomainsRow: click Manage button
  AllowedEmailDomainsRow->>ManageModal: open(show=true, existing)
  ManageModal->>ManageModal: snapshot existing → working
  User->>ManageModal: paste text + Add or Cmd+Enter
  ManageModal->>Parser: parse & classify entries
  Parser->>ManageModal: return valid/invalid/dup counts
  ManageModal->>ManageModal: append valid to working
  ManageModal->>List: render virtualized entries
  User->>ManageModal: click Done
  ManageModal->>AllowedEmailDomainsRow: onApply(merged domains)
  AllowedEmailDomainsRow->>AllowedEmailDomainsRow: update & fireChange
  AllowedEmailDomainsRow->>User: update summary display
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • fntechgit/summit-admin#915: Modifies the domain-authorized promo-code UI and overlaps with AllowedEmailDomainsRow behavior and validation.

Suggested reviewers

  • santipalenque
  • smarcet

Poem

🐰 A modal springs forth with windows so wide,
Parsing domains with virtualized pride,
Sixty lines pasted, duplicates spurned,
The rabbit taps Done — new domains are earned.
Bulk edits, clean lists — the form's satisfied.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing Tier 1 bulk domain input with paste, modal, and compact summary features for promo code domain management.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/domain-authorized-promo-codes-bulk-input

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@caseylocker caseylocker self-assigned this May 18, 2026
@caseylocker caseylocker marked this pull request as ready for review May 18, 2026 21:37
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/forms/promocode-form/forms/domain-authorized/__tests__/bulk-input-parser.test.js (1)

32-36: ⚡ Quick win

Strengthen the sourceRow assertion to catch regressions.

This currently only verifies sourceRow > 0, so incorrect numbering can still pass. Assert exact row indices instead.

♻️ Proposed test adjustment
   it("preserves 1-based source row numbers", () => {
     const rows = parseTextBlob("`@a.com`\n@b.com\n@c.com");
-    expect(rows[0].sourceRow).toBeGreaterThan(0);
+    expect(rows.map((r) => r.sourceRow)).toEqual([1, 2, 3]);
     expect(rows.map((r) => r.entry)).toEqual(["`@a.com`", "`@b.com`", "`@c.com`"]);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/components/forms/promocode-form/forms/domain-authorized/__tests__/bulk-input-parser.test.js`
around lines 32 - 36, The test for parseTextBlob should assert exact 1-based
sourceRow values instead of only >0; update the "preserves 1-based source row
numbers" test to verify rows.map(r => r.sourceRow) equals [1,2,3] (and keep the
existing check that rows.map(r => r.entry) equals ["`@a.com`","`@b.com`","`@c.com`"])
so any off-by-one regressions in parseTextBlob are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/components/forms/promocode-form/forms/domain-authorized/AllowedEmailDomainsRow.jsx`:
- Around line 271-273: The onApply handler (the line calling
fireChange(handleChange, "allowed_email_domains", next)) updates parent state
but doesn't clear the component's local validation state, so previous
domainsError and draft can persist; after applying changes call the local
setters (e.g., setDomainsError(null) and setDraft(next) or an appropriate
empty/updated draft) to clear the inline error and sync the draft with the
applied value so the modal shows the new state without lingering errors.

---

Nitpick comments:
In
`@src/components/forms/promocode-form/forms/domain-authorized/__tests__/bulk-input-parser.test.js`:
- Around line 32-36: The test for parseTextBlob should assert exact 1-based
sourceRow values instead of only >0; update the "preserves 1-based source row
numbers" test to verify rows.map(r => r.sourceRow) equals [1,2,3] (and keep the
existing check that rows.map(r => r.entry) equals ["`@a.com`","`@b.com`","`@c.com`"])
so any off-by-one regressions in parseTextBlob are caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53e675e2-3286-4ced-9595-595afeeb73c5

📥 Commits

Reviewing files that changed from the base of the PR and between 7f5ff69 and 85b3431.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (9)
  • package.json
  • src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js
  • src/components/forms/promocode-form/forms/domain-authorized/AllowedEmailDomainsRow.jsx
  • src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx
  • src/components/forms/promocode-form/forms/domain-authorized/__tests__/allowed-email-domains-row.test.jsx
  • src/components/forms/promocode-form/forms/domain-authorized/__tests__/bulk-input-parser.test.js
  • src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx
  • src/components/forms/promocode-form/forms/domain-authorized/bulk-input-parser.js
  • src/i18n/en.json

@caseylocker caseylocker requested a review from smarcet May 18, 2026 23:04
Reconcile implementation with JP's pass-1 framing: Paste-to-add is a
top-level Tier 1 capability, not nested behind compact summary. Lift
the Manage List button from the compact-summary block to a sibling
next to the field label so it renders at all counts (including
length=0). Tier 1 read-only modal scope is unchanged.

- AllowedEmailDomainsRow.jsx: wrap label + button in flex container,
  remove button from compact-summary branch, drop the now-unused inner
  space-between flex wrapper around the count/type-mix/example block.
- i18n: rename edit_promocode.large_list.manage_button ->
  edit_promocode.manage_button (the large_list namespace lied once the
  button left the compact-summary branch); other large_list.* keys
  stay where they are.
- Tests: flip 3 button-presence assertions; re-probe 50/51 boundary
  test via compact-summary-count (button is now threshold-invariant);
  new count=0 button-present test for the empty-form entry point.
- yarn jest src/components/forms/promocode-form -> 113/113.

SDS amendment in ftn-docsnsklz captures the reconciliation:
sds/domain-authorized-promo-codes-bulk-input.md "2026-05-19 — Tier 1.1
Manage List button always-on (reconciliation with JP pass-1 framing)".
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js`:
- Around line 554-557: Narrow the assertion to the specific
allowed_email_domains lookup by filtering getByIdSpy.mock.results for results
where the call argument equals "allowed_email_domains" and the result is a
successful return with a non-null value; replace the broad filter using only
r.type and r.value with one that checks the corresponding mock.calls or the
mock.results' associated args to ensure the getByIdSpy invocation for
"allowed_email_domains" returned a non-null value (use getByIdSpy to locate the
call for "allowed_email_domains" and assert that its return/result is not null).

In
`@src/components/forms/promocode-form/forms/domain-authorized/__tests__/allowed-email-domains-row.test.jsx`:
- Around line 506-509: The test currently filters handleChange.mock.calls then
asserts length 0 which can miss calls that passed unchanged arrays; replace that
logic by directly asserting that handleChange was never called (use
handleChange.mock and an assertion like
expect(handleChange).not.toHaveBeenCalled()) in the allowed-email-domains-row
test to ensure no duplicate-triggering calls occur; locate the existing usage of
handleChange and the variable calls in the test and remove the filtering-based
assertion, replacing it with a direct zero-call assertion on handleChange.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1de602b-00d8-488d-b2b0-7ff3388bc407

📥 Commits

Reviewing files that changed from the base of the PR and between 85b3431 and 28ea322.

📒 Files selected for processing (4)
  • src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js
  • src/components/forms/promocode-form/forms/domain-authorized/AllowedEmailDomainsRow.jsx
  • src/components/forms/promocode-form/forms/domain-authorized/__tests__/allowed-email-domains-row.test.jsx
  • src/i18n/en.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/i18n/en.json
  • src/components/forms/promocode-form/forms/domain-authorized/AllowedEmailDomainsRow.jsx

Comment on lines +554 to +557
const calls = getByIdSpy.mock.results.filter(
(r) => r.type === "return" && r.value !== null
);
expect(calls.length).toBeGreaterThan(0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Bind spy result assertion to the allowed_email_domains lookup call.

The current non-null check can pass from any other getElementById call. Validate the return value for the specific "allowed_email_domains" invocation.

Suggested assertion fix
-    const calls = getByIdSpy.mock.results.filter(
-      (r) => r.type === "return" && r.value !== null
-    );
-    expect(calls.length).toBeGreaterThan(0);
+    const idx = getByIdSpy.mock.calls.findIndex(
+      ([id]) => id === "allowed_email_domains"
+    );
+    expect(idx).toBeGreaterThanOrEqual(0);
+    expect(getByIdSpy.mock.results[idx]?.value).not.toBeNull();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/components/forms/promocode-form/__tests__/promocode-form.integration.test.js`
around lines 554 - 557, Narrow the assertion to the specific
allowed_email_domains lookup by filtering getByIdSpy.mock.results for results
where the call argument equals "allowed_email_domains" and the result is a
successful return with a non-null value; replace the broad filter using only
r.type and r.value with one that checks the corresponding mock.calls or the
mock.results' associated args to ensure the getByIdSpy invocation for
"allowed_email_domains" returned a non-null value (use getByIdSpy to locate the
call for "allowed_email_domains" and assert that its return/result is not null).

Comment on lines +506 to +509
const calls = handleChange.mock.calls.filter(
(c) => Array.isArray(c[0]?.target?.value) && c[0].target.value.length > 1
);
expect(calls).toHaveLength(0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen dedup regression by asserting no handleChange call at all.

This check can pass even if duplicate commits still call handleChange with an unchanged array. Assert zero calls directly to avoid false positives.

Suggested test tightening
     typeAndCommit(container, "`@ACME.COM`");
-    const calls = handleChange.mock.calls.filter(
-      (c) => Array.isArray(c[0]?.target?.value) && c[0].target.value.length > 1
-    );
-    expect(calls).toHaveLength(0);
+    expect(handleChange).not.toHaveBeenCalled();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const calls = handleChange.mock.calls.filter(
(c) => Array.isArray(c[0]?.target?.value) && c[0].target.value.length > 1
);
expect(calls).toHaveLength(0);
expect(handleChange).not.toHaveBeenCalled();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/components/forms/promocode-form/forms/domain-authorized/__tests__/allowed-email-domains-row.test.jsx`
around lines 506 - 509, The test currently filters handleChange.mock.calls then
asserts length 0 which can miss calls that passed unchanged arrays; replace that
logic by directly asserting that handleChange was never called (use
handleChange.mock and an assertion like
expect(handleChange).not.toHaveBeenCalled()) in the allowed-email-domains-row
test to ensure no duplicate-triggering calls occur; locate the existing usage of
handleChange and the variable calls in the test and remove the filtering-based
assertion, replacing it with a direct zero-call assertion on handleChange.

Copy link
Copy Markdown

@smarcet smarcet left a comment

Choose a reason for hiding this comment

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

LGTM

@smarcet smarcet merged commit 2e186d2 into master May 20, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants