domain project / real-estate

Real Estate

Test strategies for property search, virtual tours, mortgage calculators, listing management, and agent/buyer portals.

Property search & filtersVirtual tours & mediaMortgage calculatorListing managementAgent & buyer portalsLead management

Domain Overview

Real estate portals handle high-value decisions with complex search (geo, price, bedrooms, amenities), media-heavy listings (photos, floor plans, virtual tours), and lead capture. Mortgage calculators have complex financial logic. Agent and buyer have different permissions and views of the same listing.

Typical stack: React / Next.js frontend, mapping via Mapbox or Google Maps, property data APIs, CRM for lead routing, third-party virtual tour embeds (Matterport).

Key Test Areas

AreaWhat to testRisk
SearchGeo radius, price range, property type, multi-filter, map view, save searchWrong results, lost leads
Listing detailPhotos gallery, floor plan, virtual tour embed, agent contact, save/favouriteBroken media, missed contact
Mortgage calculatorPrincipal, rate, term, monthly payment, amortisationWrong financial advice
Agent portalCreate/edit listing, lead inbox, analyticsListing errors, missed leads
Buyer portalSaved searches, favourites, viewing requestsLost user engagement
Lead managementCapture, routing, follow-up statusRevenue loss

Test Pyramid for Real Estate

Unit (60%):        Mortgage calculation, geo distance, search scoring
Integration (30%): Listing API, geo search, lead routing
E2E (10%):         Search → listing detail → contact agent, mortgage calc output

Common Failure Scenarios

1. Map search returns properties outside drawn boundary
   Test: draw boundary polygon, assert all returned properties have coords within polygon

2. Mortgage calc shows monthly payment without including PMI (below 20% down)
   Test: enter < 20% down payment, assert PMI line item present and total reflects it

3. Virtual tour iframe blocked by CSP header
   Test: navigate to listing, check for CSP errors in page.on('console'), assert iframe loads

4. Listing marked sold still appears in search results (cache)
   Test: mark listing sold via API, trigger search, assert listing absent (retry with cache TTL wait)

5. Agent receives lead but buyer email not confirmed (unverified contact)
   Test: submit contact form with unverified email, assert agent lead inbox shows unverified flag

Playwright Patterns for Real Estate

// Mortgage calculator with known inputs/outputs
test('mortgage calculator produces correct monthly payment', async ({ page }) => {
  await page.goto('/mortgage-calculator');
  await page.getByLabel('Home price').fill('400000');
  await page.getByLabel('Down payment').fill('80000');
  await page.getByLabel('Interest rate').fill('6.5');
  await page.getByLabel('Loan term (years)').selectOption('30');
  await page.getByRole('button', { name: 'Calculate' }).click();
  // 320000 at 6.5% over 30 years = ~$2,023/month
  await expect(page.getByTestId('monthly-payment')).toHaveText(/\$2,02[0-9]/);
});

// Map boundary drawing with page.mouse
test('map search respects drawn boundary', async ({ page }) => {
  await page.goto('/search?view=map');
  await page.getByRole('button', { name: 'Draw area' }).click();
  const mapCanvas = page.locator('#map-canvas');
  const box = await mapCanvas.boundingBox();
  // Draw a small rectangle in the centre of the map
  await page.mouse.move(box.x + 100, box.y + 100);
  await page.mouse.down();
  await page.mouse.move(box.x + 300, box.y + 300);
  await page.mouse.up();
  await expect(page.locator('.property-card')).toHaveCount(await page.locator('.property-card').count());
});

// Saved search notification trigger
test('saved search triggers notification on new listing', async ({ page }) => {
  await page.goto('/search?city=London&minPrice=200000&maxPrice=400000');
  await page.getByRole('button', { name: 'Save search' }).click();
  await expect(page.getByText('Search saved')).toBeVisible();
  // Verify saved search appears in buyer portal
  await page.goto('/portal/saved-searches');
  await expect(page.locator('.saved-search-card')).toHaveCount(1);
});