domain project / marketplace

C2C Marketplace

Test strategies for peer-to-peer selling, listing creation, buyer/seller flows, dispute resolution, and escrow payments.

Listing creationSearch & discoveryBuyer & seller flowsEscrow & paymentsReviews & ratingsDispute resolution

Domain Overview

C2C marketplaces connect buyers and sellers in real time, requiring dual-persona test flows, financial escrow integrity, and trust mechanisms like reviews and dispute resolution. Testing must cover concurrent bidding/buying races, search index consistency, and payout correctness.

Typical stack: React frontend, Elasticsearch search index, Stripe Connect escrow, S3 photo storage, background job for index sync, email/push notifications.

Key Test Areas

AreaWhat to testRisk
ListingCreate, edit, photo upload, pricing, category selection, publish/unpublishListing invisible, wrong category
SearchKeyword, location radius, price range, saved searches, sort orderItems not found, stale results
Purchase flowMake offer, buy-now, checkout, escrow hold, release on receiptDouble-charge, premature payout
Seller toolsDashboard metrics, payout schedule, inventory statusWrong payout, confused seller
ReviewsPost-transaction review, seller reply, flag as abusiveFake reviews, review bombing
DisputesOpen dispute, upload evidence, escalate to support, resolveFunds held indefinitely, no recourse

Test Pyramid for C2C Marketplace

Unit (60%):   Offer validation, escrow state machine, payout fee calculation, search query builder
Integration (30%): Search index sync, Stripe Connect payout API, photo upload pipeline
E2E (10%):    Seller lists item → buyer purchases → escrow holds → receipt confirmed → payout released

Critical E2E tests (must pass before every deploy):

  1. Seller creates listing and it appears in search results
  2. Buyer completes purchase and funds go into escrow
  3. Buyer confirms receipt and escrow releases to seller
  4. Dispute blocks escrow release until resolved
  5. Review only available after confirmed transaction

Common Failure Scenarios

1. Listing price update doesn't propagate to search index
   Test: update price via seller dashboard, re-search, assert new price shown (allow up to 2s for index sync)

2. Buyer and seller both believe they've won a race condition bid
   Test: fire two concurrent buy-now requests for same single-quantity listing, assert only one HTTP 200

3. Escrow release triggered before item marked received
   Test: complete purchase, do NOT confirm receipt, assert payout API not called after 24h window mock

4. Review left by non-buyer (no purchase verification)
   Test: attempt to POST review as a user with no completed transaction for that listing, assert 403

5. Photo upload succeeds but image not visible in listing
   Test: upload photo, publish listing, load listing page as anonymous user, assert img element loads (no 404)

Playwright Patterns for C2C Marketplace

// Test search debounce — type quickly, verify only one API call
test('search debounce fires single API call on rapid typing', async ({ page }) => {
  let searchCallCount = 0;
  await page.route('**/api/search*', route => {
    searchCallCount++;
    route.continue();
  });
  await page.goto('/marketplace');
  const searchInput = page.getByRole('searchbox', { name: 'Search listings' });
  // Type quickly without pausing
  await searchInput.pressSequentially('vintage lamp', { delay: 30 });
  // Wait for debounce window to settle (typically 300ms)
  await page.waitForTimeout(500);
  expect(searchCallCount).toBe(1);
});

// Dual-persona flow — buyer and seller simultaneously
test('seller sees listing purchased by buyer in real time', async ({ browser }) => {
  const sellerContext = await browser.newContext({ storageState: 'seller.json' });
  const buyerContext  = await browser.newContext({ storageState: 'buyer.json' });
  const sellerPage   = await sellerContext.newPage();
  const buyerPage    = await buyerContext.newPage();

  await sellerPage.goto('/seller/listings/99');
  await buyerPage.goto('/listing/99');
  await buyerPage.getByRole('button', { name: 'Buy now' }).click();
  await buyerPage.getByRole('button', { name: 'Confirm purchase' }).click();

  // Seller dashboard should reflect sale
  await sellerPage.reload();
  await expect(sellerPage.getByTestId('listing-status')).toHaveText('Sold');

  await sellerContext.close();
  await buyerContext.close();
});