Skip to content

feat(e2e): contractor domain — scenarios + spec rewrites#1833

Merged
jeffredodd merged 18 commits into
mainfrom
e2e/contractor-domain
May 21, 2026
Merged

feat(e2e): contractor domain — scenarios + spec rewrites#1833
jeffredodd merged 18 commits into
mainfrom
e2e/contractor-domain

Conversation

@jeffredodd
Copy link
Copy Markdown
Contributor

@jeffredodd jeffredodd commented May 14, 2026

Summary

  • Add contractor-onboarding and contractor-payment scenarios
  • Rewrite contractor spec files to use scenario fixture
  • Move to e2e/tests/contractor/ directory

Videos

canary-03-payment-create-submit-PASSED.webm
canary-02-onboarding-business-PASSED.webm
canary-01-onboarding-individual-PASSED.webm

Base: PR #1829 (e2e/infrastructure)

Notion tasks: #74-079

Made with Cursor

@jeffredodd jeffredodd force-pushed the e2e/contractor-domain branch 2 times, most recently from 85dfbda to dd77938 Compare May 19, 2026 17:44
@jeffredodd
Copy link
Copy Markdown
Contributor Author

Changes below this comment are for the API upgrade to 11-15-2025

@jeffredodd jeffredodd force-pushed the e2e/contractor-domain branch from dd77938 to c767f6c Compare May 19, 2026 18:43
@jeffredodd jeffredodd force-pushed the e2e/contractor-domain branch 5 times, most recently from fe9d1d5 to 1538505 Compare May 19, 2026 20:11
@jeffredodd jeffredodd force-pushed the e2e/infrastructure branch from cd91cb8 to a133f07 Compare May 19, 2026 21:59
@jeffredodd jeffredodd force-pushed the e2e/contractor-domain branch from 1538505 to b784e95 Compare May 19, 2026 22:00
@jeffredodd jeffredodd force-pushed the e2e/infrastructure branch from 1a995fd to 034555e Compare May 20, 2026 00:04
@jeffredodd jeffredodd force-pushed the e2e/contractor-domain branch 2 times, most recently from 8f28bec to 15c4029 Compare May 20, 2026 04:52
Copy link
Copy Markdown

@boostsecurity-io-ai boostsecurity-io-ai Bot left a comment

Choose a reason for hiding this comment

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

⚠️  1 New Suppressed Finding

The latest commit contains 1 new suppressed finding.

Findings
CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
Original Rule ID: rules_lgpl_javascript_crypto_rule-node-insecure-random-generator
Remediation: Replace all uses of crypto.pseudoRandomBytes() and Math.random() withcrypto.randomBytes() for any security-sensitive random number generation.The crypto.randomBytes() function uses the operating system'scryptographically secure random number generator (e.g., /dev/urandom on Linux,CryptGenRandom on Windows). For generating random integers within a specificrange, use crypto.randomInt(min, max) which is available in Node.js 14.10.0and later. When generating tokens or identifiers, consider usingcrypto.randomUUID() for RFC 4122 compliant UUIDs, or encode random bytesusing base64url encoding with randomBytes(32).toString('base64url') for URL-safe tokens. Note that crypto.pseudoRandomBytes() was deprecated in Node.jsv6.0.0 and should never be used in modern applications.
The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong.

The application uses cryptographically weak random number generators such as
crypto.pseudoRandomBytes() or Math.random() for security-sensitive
operations. The crypto.pseudoRandomBytes() function uses a non-cryptographic
PRNG that may be...
 📘 Learn More
const ein = `${Math.floor(Math.random() * 89 + 10)}-${Math.floor(Math.random() * 8999999 + 1000000)}` // noboost

Scanner: boostsecurity - Semgrep

@jeffredodd jeffredodd marked this pull request as ready for review May 20, 2026 05:26
@jeffredodd jeffredodd requested a review from a team as a code owner May 20, 2026 05:26
@jeffredodd
Copy link
Copy Markdown
Contributor Author

@cursoragent can we investigate? Is this issue caused by a specific change in this branch?

⚠️  1 New Suppressed Finding

The latest commit contains 1 new suppressed finding.

Findings
CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
Original Rule ID: rules_lgpl_javascript_crypto_rule-node-insecure-random-generator
Remediation: Replace all uses of crypto.pseudoRandomBytes() and Math.random() withcrypto.randomBytes() for any security-sensitive random number generation.The crypto.randomBytes() function uses the operating system'scryptographically secure random number generator (e.g., /dev/urandom on Linux,CryptGenRandom on Windows). For generating random integers within a specificrange, use crypto.randomInt(min, max) which is available in Node.js 14.10.0and later. When generating tokens or identifiers, consider usingcrypto.randomUUID() for RFC 4122 compliant UUIDs, or encode random bytesusing base64url encoding with randomBytes(32).toString('base64url') for URL-safe tokens. Note that crypto.pseudoRandomBytes() was deprecated in Node.jsv6.0.0 and should never be used in modern applications.
The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong.

The application uses cryptographically weak random number generators such as
crypto.pseudoRandomBytes() or Math.random() for security-sensitive
operations. The crypto.pseudoRandomBytes() function uses a non-cryptographic
PRNG that may be...
 📘 Learn More

const ein = `${Math.floor(Math.random() * 89 + 10)}-${Math.floor(Math.random() * 8999999 + 1000000)}` // noboost

Scanner: boostsecurity - Semgrep

@cursor
Copy link
Copy Markdown

cursor Bot commented May 20, 2026

Investigation Complete

I investigated the security finding and here's what I found:

Yes, this issue was caused by changes in this branch. The file e2e/utils/contractorFlowDrivers.ts was added in commit b3382233 as part of this PR, and it used Math.random() to generate test EINs (Employer Identification Numbers).

What I Fixed

I replaced all instances of Math.random() with Node's cryptographically secure crypto.randomInt() function:

  1. Created reusable helpers — Added generateUniqueEIN() to e2e/utils/helpers.ts (similar to the existing generateUniqueSSN() function)
  2. Updated both helpers — Converted generateUniqueSSN() to also use crypto.randomInt() for consistency
  3. Replaced all usages — Updated three files that were generating EINs inline:
    • e2e/utils/contractorFlowDrivers.ts (line 67, flagged by scanner)
    • e2e/tests/contractor/contractor-business-create-lifecycle.spec.ts
    • e2e/tests/company-onboarding.spec.ts

Why This Matters

While this is E2E test code (not production), using cryptographically secure random number generation is the right approach and eliminates the security scanner warning. The crypto.randomInt() function uses the OS's secure random number generator, satisfying the CWE-338 security requirement without needing suppression comments.

The changes have been committed and pushed to the e2e/contractor-domain branch.

Open in Web Open in Cursor 

Copy link
Copy Markdown

@boostsecurity-io-ai boostsecurity-io-ai Bot left a comment

Choose a reason for hiding this comment

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

🚀 1 New Security Fix

You just committed 1 security fix. 😎 Keep up the great work!

🎯 Take a look at what findings you fixed.
Findings
CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
Original Rule ID: rules_lgpl_javascript_crypto_rule-node-insecure-random-generator
The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong.

The application uses cryptographically weak random number generators such as
crypto.pseudoRandomBytes() or Math.random() for security-sensitive
operations. The crypto.pseudoRandomBytes() function uses a non-cryptographic
PRNG that may be...
 📘 Learn More
const ein = `${Math.floor(Math.random() * 89 + 10)}-${Math.floor(Math.random() * 8999999 + 1000000)}` // noboost

Scanner: boostsecurity - Semgrep

@gusto-fresh-eyes
Copy link
Copy Markdown

gusto-fresh-eyes Bot commented May 20, 2026

Fresh Eyes Review

Found 6 issues in this PR.

PR Description Issues

  • Major | [fresh_eyes]: description-check: Missing Approach: No explanation of design decisions — why scenario fixtures over other patterns, why the canary/lifecycle separation, why the specific scenario groupings (e.g. why contractor-mixed-roster is shared across non-mutating specs while contractor-onboarding is used by mutating specs).
  • Major | [fresh_eyes]: description-check: Missing Problem/Why: The description lists what was done (add scenarios, rewrite specs, move directory) but doesn't explain why. What motivated these changes — test reliability, speed, parallelization, or maintainability? The commit message on HEAD mentions 'consolidate contractor scenarios to speed up provisioning' which would be useful context in the description.
  • Major | [fresh_eyes]: description-check: Significant undocumented changes: The summary mentions only 'contractor-onboarding and contractor-payment scenarios' but the PR also adds contractor-mixed-roster.json and full-flow-canary.json scenarios, a 342-line contractorFlowDrivers.ts utility, helpers.ts additions (generateUniqueEIN, nextBusinessDay, crypto.randomInt refactor), canary specs (3 files), 5 new lifecycle specs, and a change to company-onboarding.spec.ts. The description understates the scope.
Info (1)
  • description-check: PR size: 20 files changed with well over 500 lines of meaningful additions. Consider whether the canary specs + flow drivers could be split from the scenario definitions + spec rewrites.

Download findings.json — drag the file into Claude or use /add to propose fixes


Please 👍🏽 👎🏽 if you found this useful. Generated by Fresh Eyes Reviewer. Get help in #ai-code-reviews

@jeffredodd jeffredodd force-pushed the e2e/contractor-domain branch from e7f6c9c to 7cb9add Compare May 20, 2026 06:12
@jeffredodd
Copy link
Copy Markdown
Contributor Author

@cursoragent Are all the comments in this PR up to date and resolved?

@cursor
Copy link
Copy Markdown

cursor Bot commented May 20, 2026

Summary: PR Comments Review

I've reviewed and addressed all code-related comments in the PR. Here's the status:

✅ Resolved Issues

  1. Security Finding (CWE-338) - Fixed in commit 7cb9adde

    • Replaced Math.random() with crypto.randomInt() in all test ID generation
    • Created reusable generateUniqueEIN() helper function
    • Updated generateUniqueSSN() to use secure random generation
    • Affected files: helpers.ts, contractorFlowDrivers.ts, contractor-business-create-lifecycle.spec.ts, company-onboarding.spec.ts
  2. Unnecessary Dynamic Imports - Fixed in commit bc135e44

    • Replaced await import('../utils/helpers') with static imports
    • Added generateUniqueEIN to the top-level import statements
    • Affected files: contractor-business-create-lifecycle.spec.ts, company-onboarding.spec.ts

ℹ️ Remaining Comments (Non-blocking)

The Fresh Eyes bot has feedback about the PR description (not code):

  • Incomplete scenario listing in summary
  • Missing problem/why context
  • Undocumented significant changes
  • PR size note

These are documentation suggestions for the PR description. Would you like me to update the PR description to address this feedback, or are you satisfied with the current description?

All code issues have been resolved and pushed to the branch.

Open in Web Open in Cursor 

@jeffredodd jeffredodd force-pushed the e2e/infrastructure branch from f7509d2 to 23258c3 Compare May 20, 2026 15:44
Base automatically changed from e2e/infrastructure to main May 20, 2026 16:12
@jeffredodd
Copy link
Copy Markdown
Contributor Author

@cursoragent can you resolve conflicts here and make sure we're rebased off main?

@cursor
Copy link
Copy Markdown

cursor Bot commented May 20, 2026

Perfect! I've successfully rebased the e2e/contractor-domain branch onto the latest main. Here's what was done:

Rebase Summary

Strategy: Instead of a full rebase (which caused many conflicts due to commits already in main), I cherry-picked only the contractor-domain-specific commits onto the latest main.

Result:

  • ✅ Branch is now based on latest main (commit 83932d20)
  • ✅ All conflicts resolved (package.json, helpers.ts)
  • ✅ Branch contains 13 contractor-domain commits
  • ✅ Force-pushed to remote successfully

Commits included (in order):

  1. ci(e2e): discover matrix domains from e2e/tests subfolders
  2. feat(e2e): contractor domain — scenarios + spec rewrites
  3. fix(e2e): stabilize contractor scenario provisioning in CI
  4. feat(e2e): expand contractor domain with business and mixed roster scenarios
  5. fix(e2e): assert real terminal states in contractor lifecycle tests
  6. feat(e2e): cover contractor onboarding state-machine branches
  7. feat(e2e): cover contractor payment submit validation terminal state
  8. feat(e2e): cover contractor payment create-to-summary full lifecycle
  9. feat(e2e): cover contractor edit re-entry from list lifecycle
  10. fix(e2e): anchor fillDate spinbutton matchers to /^month|^day|^year/
  11. feat(e2e): add contractor canary suite covering all 3 contractor-domain flow paths
  12. fix(e2e): replace Math.random() with crypto.randomInt() for test ID generation
  13. fix(e2e): replace dynamic imports with static imports for generateUniqueEIN
  14. fix(e2e): format company-onboarding imports

The branch is now clean and ready for review!

Open in Web Open in Cursor 

@cursor cursor Bot force-pushed the e2e/contractor-domain branch from a153696 to cd4f8df Compare May 20, 2026 20:15
@serikjensen
Copy link
Copy Markdown
Member

serikjensen commented May 20, 2026

From claude, with a grain of salt:

1. Inconsistent field-visibility guards in flow drivers

fillIndividualProfile (line 881) and fillBusinessProfile (line 900) in contractorFlowDrivers.ts fill SSN and EIN unconditionally, but the parallel lifecycle specs guard both fields with isVisible().catch(() => false). If a flow variant ever skips those fields, the canary will throw while the lifecycle spec silently passes.

// Guard consistently with the lifecycle specs:
const ssnField = page.getByLabel(/social security number/i)
if (await ssnField.isVisible({ timeout: 5_000 }).catch(() => false)) {
  await ssnField.fill(generateUniqueSSN())
}
// Same pattern for EIN in fillBusinessProfile

2. Silent pass on empty-contractor state hides provisioning failures

In contractor-payment-create-full-lifecycle.spec.ts (line 671) and contractor-payment-submit-lifecycle.spec.ts (line 754), when the "no contractors available" heading is visible the test does a redundant expect then returns — making the test appear green when the scenario didn't provision contractors. A CI provisioning failure would go undetected.

// Surface the failure explicitly instead:
if (await empty.isVisible().catch(() => false)) {
  test.skip(true, 'No contractors provisioned — skipping payment flow')
}

Posted by Claude on behalf of Steve

@@ -0,0 +1,313 @@
import { expect, type Page } from '@playwright/test'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Helpers in this file make me a little worried about how extensible this is going to be. Will every domain have a similar list of drivers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah thats a fair point. I think each domain has its own unique drivers currently. We might be able to create repeatable patterns here but right now I think AI's metric is just to get through the test and meet the need and this is what some QAs do. I don't love it either and I think we can iterate here.

@jeffredodd
Copy link
Copy Markdown
Contributor Author

@cursoragent Can you investigate the feedback and see if we should address Steve's cursor feedback?

@cursor
Copy link
Copy Markdown

cursor Bot commented May 20, 2026

Investigation Complete ✅

I investigated Steve's feedback and both concerns are valid. I've implemented fixes for both issues.

Issue 1: Inconsistent Field Visibility Guards

Problem: The flow driver functions (fillIndividualProfile and fillBusinessProfile) filled SSN/EIN fields unconditionally, while the lifecycle specs guarded them with isVisible().catch(() => false). This meant:

  • Canary tests (using drivers) would fail if fields became optional
  • Lifecycle tests would silently pass

Fix Applied: Added visibility guards to both functions in contractorFlowDrivers.ts:

const ssnField = page.getByLabel(/social security number/i)
if (await ssnField.isVisible({ timeout: 5_000 }).catch(() => false)) {
  await ssnField.fill(generateUniqueSSN())
}

Issue 2: Silent Pass on Empty Contractor State ⚠️ More Critical

Problem: Both payment lifecycle specs checked for "no contractors available" and did:

if (await empty.isVisible().catch(() => false)) {
  await expect(empty).toBeVisible()  // redundant assertion
  return  // test passes green
}

This masked provisioning failures — tests appeared successful when the scenario didn't provision contractors properly.

Fix Applied: Replaced with test.skip() to surface provisioning issues:

if (await empty.isVisible().catch(() => false)) {
  test.skip(true, 'No contractors provisioned — skipping payment flow')
}

Now CI will show these as skipped (yellow) rather than passed (green), alerting us to provisioning problems.

Changes Committed

  • Commit d5e4b9c3: "fix(e2e): add field guards to flow drivers and skip on provisioning failure"
  • Files modified:
    • e2e/utils/contractorFlowDrivers.ts
    • e2e/tests/contractor/contractor-payment-submit-lifecycle.spec.ts
    • e2e/tests/contractor/contractor-payment-create-full-lifecycle.spec.ts

The branch has been pushed and is ready for review.

Open in Web Open in Cursor 

@jeffredodd jeffredodd force-pushed the e2e/contractor-domain branch 2 times, most recently from 6fb4950 to 0df77c6 Compare May 21, 2026 01:45
jeffredodd and others added 18 commits May 20, 2026 21:21
Co-authored-by: Cursor <cursoragent@cursor.com>
Merge shared infrastructure CI stabilization, switch onboarding to a stable demo,
and format the contractor payment scenario for consistent format checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
…enarios

Adds two new contractor scenarios:
- contractor-business: Business contractor (Acme Consulting LLC) so
  the business profile branch is reachable.
- contractor-mixed-roster: Individual + business contractor list view.

Adds two new specs covering each scenario plus an additional
contractor-payment test that clicks New payment and asserts the payment
composition surface mounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
contractor-onboarding `can fill out the contractor profile form` was
silently passing — it filled a first name with digits ("Jane123456")
which triggers a regex validation error that the test never noticed
because it asserted only that an `<article>` was visible. Use a clean
"Jane" / "TestContractor" name (uniqueness comes from the SSN
generator already in use) and assert the next step heading
(Address/New Hire) is visible after Create Contractor.

contractor-payment `clicking new payment` was also weak (article only).
Now asserts the "Pay contractors" composition heading and the payment
date input are both visible — proving we actually entered the create
payment surface.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds two lifecycle specs to round out contractor onboarding paths:

- contractor-business-create-lifecycle: drives the Business contractor
  branch (business name, EIN, Fixed wage type, start date) through
  the profile step, asserts advancement to address or new-hire step.
- contractor-cancel-lifecycle: opens the profile step from the list,
  uses the progress-bar "Back to contractors" CTA, and asserts return
  to the contractor list (covers the cancelTransition path that drops
  contractorId and selfOnboarding flags).

Combined with the existing Individual create + back-from-profile
tests, all major contractor onboarding state-machine branches now
land at a terminal UI state in their video.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds contractor-payment-submit-lifecycle which drives:
list -> click New payment -> Pay contractors composition page ->
fill payment date -> click Continue with no contractor payments
entered -> assert the "Please add at least one contractor payment"
validation alert is visible.

This exercises the createPayment state's submit-with-empty-rows
validation branch in the paymentMachine, which is a documented
terminal state until a contractor row is edited with wage/hours.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drives the full create-payment path:
- New payment → Pay contractors composition
- Enter payment date
- Open Edit contractor payment hamburger → menu → Edit modal
- Fill wage (Fixed) or hours (Hourly)
- Save → modal closes
- Continue → assert payment summary heading or success alert

Covers the CONTRACTOR_PAYMENT_CREATED transition from createPayment
to paymentSummary terminal in the paymentMachine.

Co-authored-by: Cursor <cursoragent@cursor.com>
Opens an existing Business contractor's hamburger menu, clicks Edit,
and asserts the profile form renders pre-filled with Business radio
checked, business name populated, and Update Contractor CTA visible.

Covers the CONTRACTOR_UPDATE re-entry transition from the list state
back into profile with contractorId preserved in context.

Co-authored-by: Cursor <cursoragent@cursor.com>
…in flow paths

The contractor domain owns two flows reachable through the SDK
(`flow=contractor-onboarding` for both Individual and Business profile
branches, and `flow=contractor-payment` from list through Payment
Summary). Existing specs in e2e/tests/contractor/ cover landing-page
and single-screen surface checks, but no spec drives any of them all
the way to their terminal success state.

This change adds a canary suite (e2e/tests/contractor/canary/) that
walks each flow end-to-end against the demo backend and asserts on
the real terminal landmark:

- 01-onboarding-individual: Contractor list → Add → Profile (Individual,
  SSN + Hourly + Start Date) → Home address → Payment method (Check) →
  New hire report (No) → Submit → "Contractor has been onboarded!"
- 02-onboarding-business: Contractor list → Add → Profile (Business,
  EIN + Fixed + Start Date) → Business address → Payment method (Check)
  → New hire report (No) → Submit → "Contractor has been onboarded!"
- 03-payment-create-submit: Payment list → New payment → Pay
  contractors (date + edit Hours on the seeded payable Hourly
  contractor, force Check) → Continue → Review and submit → Submit →
  Payment Summary ("Payment group created successfully")

Implementation notes:

- One shared scenario file, e2e/scenarios/contractor/full-flow-canary.json,
  provisions react_sdk_demo_company_onboarded plus a single pre-decorated
  Individual Hourly contractor ("payable"). The onboarding canaries
  ignore him and create their own through the UI; the payment canary
  needs an onboarded contractor immediately payable, which he provides.
- Drivers live in e2e/utils/contractorFlowDrivers.ts and import only
  fillDate + waitForLoadingComplete from e2e/utils/helpers.ts (no
  re-implementation of shared helpers).
- The Payment Method step picks Check and the payment Edit modal forces
  the Check radio to avoid fast-ACH funding submission blockers that
  fresh demo companies hit on Direct Deposit.
- The New Hire Report step picks "No, I have already filed" to keep
  the canary off the state-filing path.
- The Address step's State picker is a React Aria Select trigger, not a
  native combobox — driven by clicking the "Select state..." button and
  then the matching option, rather than getByLabel(/state/i) which
  resolves to the hidden combobox element.

All 3 specs verified PASSED against demo (workers=1, matching CI's
serial mode): 3 passed (55.8s).

Videos available locally at ~/Desktop/contractor-videos/canary-*-PASSED.webm
for the PR description.

Co-authored-by: Cursor <cursoragent@cursor.com>
…eneration

Co-authored-by: Jeffrey D Johnson <jeffredodd@users.noreply.github.com>
…queEIN

Co-authored-by: Jeffrey D Johnson <jeffredodd@users.noreply.github.com>
…ailure

- Add visibility guards to SSN/EIN fields in contractorFlowDrivers
- Replace silent pass with test.skip() when no contractors provisioned
- Ensures canary and lifecycle tests handle optional fields consistently
- Makes CI provisioning failures visible instead of silently passing
The contractor payment canary's setPaymentDate helper used today + 14d as
the payment date with a "clear weekend rules" comment, but it didn't
account for US federal holidays. CI run on 2026-05-20 produced today + 14d
= Mon May 25, 2026 = Memorial Day. The demo backend rejected it with a
non-actionable form alert ("Mon, May 25th is not a business day"), the
spec couldn't reach Review and submit, and the 8-min test timeout fired
3x in a row.

Adds a reusable nextBusinessDay(from, minOffsetDays) helper in
e2e/utils/helpers.ts that returns the first weekday after the offset that
is also not a US federal holiday. Encoded 2026-2027 holidays with a TODO
to refresh annually. Operates in UTC so results are stable regardless of
the runner timezone.

setPaymentDate now calls nextBusinessDay(new Date(), 14). Future payment
drivers can reuse the helper from helpers.ts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two related fixes for the contractor payment canary (spec 03):

1. Drop the `payable` Casey Canary decoration from
   contractor/full-flow-canary.json. The scenario runner's contractor
   decoration only calls POST /contractors with profile fields; it does
   not set an address, payment method, or mark onboarding complete. As a
   result the decorated contractor never appeared in "Pay contractors"
   on fresh demo provisions. The base demo
   react_sdk_demo_company_onboarded already includes payment-ready
   contractors (Ella Fitzgerald, Louis Armstrong), so the canary scenario
   no longer needs to add one.

2. Add an empty-state guard at the top of editFirstContractorPayment in
   contractorFlowDrivers.ts. Previously, if "No contractors available
   for payment" rendered, the spec silently sat at its 8-min test
   timeout waiting for an Edit button that would never appear (this is
   how the failing CI run burned 17.7m across 3 retries). The driver
   now probes the empty-state text with a 3s timeout and throws a
   clear, actionable error if it sees it, then explicitly waits up to
   LONG_WAIT for the Edit button before clicking.

Also drops the stale "decorated payable contractor is Hourly" comment;
the driver already handles both Hourly and Fixed wage types via the
hoursField / wageField fallback.

As a side benefit, since all 3 canary specs share
contractor/full-flow-canary, the worker-scoped scenario cache now
collapses provisioning for the whole canary suite into a single demo
creation per worker.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mirror the slimming that landed with the employee + payroll domain PRs:
collapse what was 5 contractor scenarios down to 4 by merging
overlapping shapes and dropping decorations the specs don't actually
read. The remaining set is the minimum needed to keep each spec working.

- Delete contractor/contractor-business.json. The business-profile branch
  is now covered by contractor-mixed-roster, which already provisions an
  individual + business contractor pair. The business contractor in the
  shared roster is renamed from "Bright Services Inc" to
  "Acme Consulting LLC" so contractor-business.spec.ts and
  contractor-onboarding-edit-lifecycle.spec.ts (which both assert on the
  Acme name) can target the shared roster instead of a dedicated scenario.

- Drop the Charlie decoration from contractor/contractor-payment.json.
  The recent fix to rely on the demo's payable contractors
  (Ella Fitzgerald, Louis Armstrong) instead of decorated ones already
  made the contractor decoration dead weight here — the scenario runner
  only POSTs profile fields, so the decorated contractor never appears in
  "Pay contractors" anyway. The scenario stays as a distinct id from
  contractor/full-flow-canary so the canary suite's mutating tests remain
  cache-isolated from these read-only lifecycle specs.

- Update consumer specs:
  - contractor-business.spec.ts → contractor/contractor-mixed-roster
  - contractor-onboarding-edit-lifecycle.spec.ts → contractor/contractor-mixed-roster
  - contractor-mixed-roster.spec.ts → assert "Acme Consulting" + "Charlie"
    (instead of "Bright Services" + "Charlie")

Net effect on a fresh CI run: contractor-domain provisioning drops from
5 demo companies (with 4 decorated contractors total) to 4 demo companies
(with 2 decorated contractors total). Both contractor-onboarding and
contractor-payment scenarios provision no contractors at all now, so
their provisioning collapses to a single demo creation per worker — the
same shape the payroll-domain biweekly-shared scenario uses.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jeffredodd jeffredodd force-pushed the e2e/contractor-domain branch from 05e9217 to e1272f1 Compare May 21, 2026 04:31
Comment on lines +19 to +41
await page.goto('/?flow=contractor-payment')
await waitForLoadingComplete(page, 60000)

await page
.getByRole('button', { name: /new payment/i })
.first()
.click()
await waitForLoadingComplete(page, 60000)

const composition = page.getByRole('heading', { name: /pay contractors/i })
const empty = page.getByRole('heading', { name: /no contractors available/i })
await expect(composition.or(empty)).toBeVisible({ timeout: 30000 })

if (await empty.isVisible().catch(() => false)) {
test.skip(true, 'No contractors provisioned — skipping payment flow')
return
}

const dateInput = page.getByLabel(/payment date/i)
await expect(dateInput).toBeVisible()
const futureDate = new Date()
futureDate.setDate(futureDate.getDate() + 7)
await dateInput.fill(futureDate.toISOString().slice(0, 10))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Minor | code-duplication

Payment setup sequence duplicated with contractor-payment-submit-lifecycle.spec.ts (lines 19-41 are nearly identical: goto, click new payment, empty-state skip, fill date). The PR already introduces shared helpers for this exact flow in contractorFlowDrivers.ts (landOnPaymentList, startNewPayment, setPaymentDate) but the lifecycle specs inline the logic instead of reusing them. The inlined version also uses a naive +7 day offset that may hit holidays, while the driver uses the more robust nextBusinessDay helper.


Please 👍🏽 👎🏽 if you found this useful. Generated by Fresh Eyes Reviewer. Get help in #ai-code-reviews

Comment on lines +41 to +43
await dateInput.fill(futureDate.toISOString().slice(0, 10))

const editTrigger = page.getByRole('button', { name: /edit contractor payment/i }).first()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Major | logic-errors

Payment date uses naive new Date() + 7 days which can land on a weekend or US holiday, causing the backend to reject it. The canary driver (contractorFlowDrivers.ts setPaymentDate) explicitly uses nextBusinessDay(new Date(), 14) to avoid this exact failure mode — its comment states: 'The demo backend rejects e.g. Memorial Day with "Mon, May 25th is not a business day"'. This spec goes all the way to submission, so a non-business date will cause intermittent CI failures (~28% chance of hitting a weekend alone).


Please 👍🏽 👎🏽 if you found this useful. Generated by Fresh Eyes Reviewer. Get help in #ai-code-reviews

Comment thread e2e/utils/helpers.ts
Comment on lines +59 to 113
'2026-05-25', // Memorial Day
'2026-06-19', // Juneteenth
'2026-07-03', // Independence Day (observed, July 4 is Saturday)
'2026-09-07', // Labor Day
'2026-10-12', // Columbus Day
'2026-11-11', // Veterans Day
'2026-11-26', // Thanksgiving
'2026-11-27', // Day after Thanksgiving (banking holiday)
'2026-12-25', // Christmas Day
'2027-01-01', // New Year's Day
'2027-01-18', // MLK Day
'2027-02-15', // Presidents Day
'2027-05-31', // Memorial Day
'2027-06-18', // Juneteenth (observed, June 19 is Saturday)
'2027-07-05', // Independence Day (observed, July 4 is Sunday)
'2027-09-06', // Labor Day
'2027-10-11', // Columbus Day
'2027-11-11', // Veterans Day
'2027-11-25', // Thanksgiving
'2027-11-26', // Day after Thanksgiving
'2027-12-24', // Christmas Day (observed, Dec 25 is Saturday)
])

function toIsoDate(date: Date): string {
return date.toISOString().slice(0, 10)
}

function isBusinessDay(date: Date): boolean {
const day = date.getUTCDay()
if (day === 0 || day === 6) return false
return !US_FEDERAL_HOLIDAYS_ISO.has(toIsoDate(date))
}

/**
* Return the first business day (weekday + not a US federal holiday) that is
* at least `minOffsetDays` after `from`. Operates in UTC to keep results
* stable regardless of the test runner's local timezone — the demo backend
* also validates against UTC dates.
*
* Designed for payment / pay-date pickers where the backend rejects
* non-business days. Without this, a +14 offset that lands on Memorial Day
* surfaces as a non-actionable form-validation alert in CI.
*/
export function nextBusinessDay(from: Date, minOffsetDays: number): Date {
const candidate = new Date(
Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), from.getUTCDate() + minOffsetDays),
)
while (!isBusinessDay(candidate)) {
candidate.setUTCDate(candidate.getUTCDate() + 1)
}
return candidate
}

export async function waitForLoadingComplete(page: Page, timeout = 30_000) {
const suspenseFallback = page.getByRole('region', { name: /^loading/i })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Minor | test-coverage

nextBusinessDay() contains non-trivial date logic (weekend detection, US federal holiday exclusion, UTC date advancement) with no unit test. A bug in the holiday set or the advancement loop would surface only as a flaky CI failure on a specific calendar date. Consider adding a unit test covering: offset landing on a weekend, offset landing on a holiday, and consecutive non-business days.


Please 👍🏽 👎🏽 if you found this useful. Generated by Fresh Eyes Reviewer. Get help in #ai-code-reviews

@jeffredodd jeffredodd merged commit e5c214c into main May 21, 2026
24 checks passed
@jeffredodd jeffredodd deleted the e2e/contractor-domain branch May 21, 2026 04:43
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.

3 participants