Compare commits
2 Commits
dev
...
fcfd35aa3f
| Author | SHA1 | Date | |
|---|---|---|---|
| fcfd35aa3f | |||
| 724115f2b3 |
64
__tests__/scraper/fixtures/sample-listing-call.html
Normal file
64
__tests__/scraper/fixtures/sample-listing-call.html
Normal file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Country Club Towers - Call for Pricing</title></head>
|
||||
<body>
|
||||
<section class="spaces__tab-unit">
|
||||
<article
|
||||
class="spaces-unit floor_2760 spaces-community-country-club-towers 1bed 1bath spaces-market-country-club-towers-gardens penthouse"
|
||||
data-spaces-id="9001"
|
||||
data-spaces-soonest="Now"
|
||||
data-spaces-sort-date="1706745600"
|
||||
data-spaces-sort-price="Call for pricing"
|
||||
data-spaces-sort-area="750"
|
||||
data-spaces-sort-bed="1"
|
||||
data-spaces-unit="P-101"
|
||||
data-spaces-unit-id="9001"
|
||||
data-spaces-unit-floor="1"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="500"
|
||||
data-spaces-sort-plan-name="Penthouse A"
|
||||
data-spaces-sort-bath="1"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=9001"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=9001"
|
||||
aria-label="Unit P-101">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">Call for pricing</span>
|
||||
<span class="spaces-unit__unit">P-101</span>
|
||||
</div>
|
||||
</article>
|
||||
<article
|
||||
class="spaces-unit floor_2760 spaces-community-country-club-towers 2bed 2bath spaces-market-country-club-towers-gardens penthouse"
|
||||
data-spaces-id="9002"
|
||||
data-spaces-soonest="Now"
|
||||
data-spaces-sort-date="1706745600"
|
||||
data-spaces-sort-price="Call for pricing"
|
||||
data-spaces-sort-area="900"
|
||||
data-spaces-sort-bed="2"
|
||||
data-spaces-unit="P-102"
|
||||
data-spaces-unit-id="9002"
|
||||
data-spaces-unit-floor="2"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="501"
|
||||
data-spaces-sort-plan-name="Penthouse B"
|
||||
data-spaces-sort-bath="2"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=9002"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=9002"
|
||||
aria-label="Unit P-102">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">Call for pricing</span>
|
||||
<span class="spaces-unit__unit">P-102</span>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
9
__tests__/scraper/fixtures/sample-listing-empty.html
Normal file
9
__tests__/scraper/fixtures/sample-listing-empty.html
Normal file
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Country Club Towers - No Units</title></head>
|
||||
<body>
|
||||
<section class="spaces__tab-unit">
|
||||
<!-- Empty - no units available -->
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,144 +1,288 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Country Club Towers - Available Units</title>
|
||||
</head>
|
||||
<html>
|
||||
<head><title>Country Club Towers - Apartments</title></head>
|
||||
<body>
|
||||
<div class="spaces-container">
|
||||
<section class="spaces__tab-unit">
|
||||
<!-- Unit 1: Fully populated with all attributes and an image -->
|
||||
<article
|
||||
data-spaces-id="1001"
|
||||
data-spaces-unit="CCT-101"
|
||||
data-spaces-unit-id="5001"
|
||||
data-spaces-unit-floor="1"
|
||||
data-spaces-sort-area="750"
|
||||
data-spaces-sort-bed="1"
|
||||
data-spaces-sort-bath="1"
|
||||
data-spaces-sort-price="1450"
|
||||
data-spaces-available="true"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-soonest="2026-03-01"
|
||||
data-spaces-sort-date="20260301"
|
||||
data-spaces-plan-id="201"
|
||||
data-spaces-sort-plan-name="Alpine"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="100"
|
||||
data-spaces-href="/units/CCT-101"
|
||||
data-spaces-inventory-href="/inventory/CCT-101"
|
||||
data-spaces-specials-content="First month free!"
|
||||
>
|
||||
<div class="unit-card">
|
||||
<img src="https://example.com/images/unit-101.jpg" alt="Unit 101" />
|
||||
<h3>Unit CCT-101</h3>
|
||||
<p>1 Bed / 1 Bath - $1,450/mo</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Unit 2: Different floor plan, no specials, with data-src image -->
|
||||
<article
|
||||
data-spaces-id="1002"
|
||||
data-spaces-unit="CCT-205"
|
||||
data-spaces-unit-id="5002"
|
||||
data-spaces-unit-floor="2"
|
||||
data-spaces-sort-area="950"
|
||||
class="spaces-unit price_3500plus floor_2755 spaces-community-country-club-towers 2bed 2bath spaces-market-country-club-towers-gardens"
|
||||
data-spaces-id="154842"
|
||||
data-spaces-soonest="2025-10-11"
|
||||
data-spaces-sort-date="1760140800"
|
||||
data-spaces-sort-price="5230"
|
||||
data-spaces-sort-area="1210"
|
||||
data-spaces-sort-bed="2"
|
||||
data-spaces-sort-bath="1.5"
|
||||
data-spaces-sort-price="1850"
|
||||
data-spaces-available="true"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-soonest="2026-02-15"
|
||||
data-spaces-sort-date="20260215"
|
||||
data-spaces-plan-id="202"
|
||||
data-spaces-sort-plan-name="Birchwood"
|
||||
data-spaces-unit="W2707"
|
||||
data-spaces-unit-id="154842"
|
||||
data-spaces-unit-floor="2755"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="100"
|
||||
data-spaces-href="/units/CCT-205"
|
||||
data-spaces-inventory-href="/inventory/CCT-205"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
>
|
||||
<div class="unit-card">
|
||||
<img data-src="https://example.com/images/unit-205.jpg" alt="Unit 205" />
|
||||
<h3>Unit CCT-205</h3>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Unit 3: Studio with some missing attributes -->
|
||||
<article
|
||||
data-spaces-id="1003"
|
||||
data-spaces-unit="CCT-310"
|
||||
data-spaces-unit-id="5003"
|
||||
data-spaces-unit-floor="3"
|
||||
data-spaces-sort-area="500"
|
||||
data-spaces-sort-bed="0"
|
||||
data-spaces-sort-bath="1"
|
||||
data-spaces-sort-price="1100"
|
||||
data-spaces-available="true"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-soonest="Available Now"
|
||||
data-spaces-sort-date="20260201"
|
||||
data-spaces-plan-id="203"
|
||||
data-spaces-sort-plan-name="Cedar"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="100"
|
||||
data-spaces-href="/units/CCT-310"
|
||||
data-spaces-inventory-href="/inventory/CCT-310"
|
||||
>
|
||||
<div class="unit-card">
|
||||
<h3>Unit CCT-310</h3>
|
||||
<p>Studio - $1,100/mo</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Unit 4: Unavailable unit -->
|
||||
<article
|
||||
data-spaces-id="1004"
|
||||
data-spaces-unit="CCT-415"
|
||||
data-spaces-unit-id="5004"
|
||||
data-spaces-unit-floor="4"
|
||||
data-spaces-sort-area="1200"
|
||||
data-spaces-sort-bed="3"
|
||||
data-spaces-plan-id="10270"
|
||||
data-spaces-sort-plan-name="Pyramid Peak - Terrace"
|
||||
data-spaces-sort-bath="2"
|
||||
data-spaces-sort-price="2500"
|
||||
data-spaces-available="false"
|
||||
data-spaces-unavailable="true"
|
||||
data-spaces-soonest=""
|
||||
data-spaces-sort-date=""
|
||||
data-spaces-plan-id="204"
|
||||
data-spaces-sort-plan-name="Dogwood"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="100"
|
||||
data-spaces-href="/units/CCT-415"
|
||||
data-spaces-inventory-href="/inventory/CCT-415"
|
||||
data-spaces-specials-content=""
|
||||
>
|
||||
<div class="unit-card">
|
||||
<img src="https://example.com/images/unit-415.jpg" alt="Unit 415" />
|
||||
<h3>Unit CCT-415</h3>
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154842"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154842"
|
||||
aria-label="Unit W2707">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">5230</span>
|
||||
<span class="spaces-unit__unit">W2707</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Unit 5: Minimal attributes - edge case -->
|
||||
<article
|
||||
data-spaces-id="1005"
|
||||
data-spaces-unit="CCT-520"
|
||||
data-spaces-unit-id="5005"
|
||||
data-spaces-sort-price="1300"
|
||||
data-spaces-available="true"
|
||||
data-spaces-unavailable="false"
|
||||
class="spaces-unit price_2500-3000 floor_2738 spaces-community-country-club-towers 1bed 1bath spaces-market-country-club-towers-gardens"
|
||||
data-spaces-id="154451"
|
||||
data-spaces-soonest="2026-03-08"
|
||||
data-spaces-sort-date="1772928000"
|
||||
data-spaces-sort-price="2767"
|
||||
data-spaces-sort-area="806"
|
||||
data-spaces-sort-bed="1"
|
||||
data-spaces-unit="E0901"
|
||||
data-spaces-unit-id="154451"
|
||||
data-spaces-unit-floor="2738"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="100"
|
||||
>
|
||||
<div class="unit-card">
|
||||
<h3>Unit CCT-520</h3>
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="8016"
|
||||
data-spaces-sort-plan-name="Sunshine Peak"
|
||||
data-spaces-sort-bath="1"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154451"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154451"
|
||||
aria-label="Unit E0901">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">2767</span>
|
||||
<span class="spaces-unit__unit">E0901</span>
|
||||
</div>
|
||||
</article>
|
||||
<article
|
||||
class="spaces-unit price_2000-2500 floor_2754 spaces-community-country-club-towers 0bed 1bath spaces-market-country-club-towers-gardens"
|
||||
data-spaces-id="154814"
|
||||
data-spaces-soonest="2025-12-18"
|
||||
data-spaces-sort-date="1766016000"
|
||||
data-spaces-sort-price="2255"
|
||||
data-spaces-sort-area="623"
|
||||
data-spaces-sort-bed="0"
|
||||
data-spaces-unit="W2603"
|
||||
data-spaces-unit-id="154814"
|
||||
data-spaces-unit-floor="2754"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="8018"
|
||||
data-spaces-sort-plan-name="Little Bear Peak"
|
||||
data-spaces-sort-bath="1"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154814"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154814"
|
||||
aria-label="Unit W2603">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">2255</span>
|
||||
<span class="spaces-unit__unit">W2603</span>
|
||||
</div>
|
||||
</article>
|
||||
<article
|
||||
class="spaces-unit price_3500plus floor_2737 spaces-community-country-club-towers 2bed 2bath spaces-market-country-club-towers-gardens"
|
||||
data-spaces-id="154428"
|
||||
data-spaces-soonest="2026-02-22"
|
||||
data-spaces-sort-date="1771718400"
|
||||
data-spaces-sort-price="4250"
|
||||
data-spaces-sort-area="1152"
|
||||
data-spaces-sort-bed="2"
|
||||
data-spaces-unit="W0802"
|
||||
data-spaces-unit-id="154428"
|
||||
data-spaces-unit-floor="2737"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="8017"
|
||||
data-spaces-sort-plan-name="Mt. Princeton"
|
||||
data-spaces-sort-bath="2"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154428"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154428"
|
||||
aria-label="Unit W0802">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">4250</span>
|
||||
<span class="spaces-unit__unit">W0802</span>
|
||||
</div>
|
||||
</article>
|
||||
<article
|
||||
class="spaces-unit price_2000-2500 floor_2752 spaces-community-country-club-towers 1bed 1bath spaces-market-country-club-towers-gardens"
|
||||
data-spaces-id="154778"
|
||||
data-spaces-soonest="2025-12-31"
|
||||
data-spaces-sort-date="1767139200"
|
||||
data-spaces-sort-price="2495"
|
||||
data-spaces-sort-area="764"
|
||||
data-spaces-sort-bed="1"
|
||||
data-spaces-unit="W2405"
|
||||
data-spaces-unit-id="154778"
|
||||
data-spaces-unit-floor="2752"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="8020"
|
||||
data-spaces-sort-plan-name="Grays Peak"
|
||||
data-spaces-sort-bath="1"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154778"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154778"
|
||||
aria-label="Unit W2405">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">2495</span>
|
||||
<span class="spaces-unit__unit">W2405</span>
|
||||
</div>
|
||||
</article>
|
||||
<article
|
||||
class="spaces-unit price_2000-2500 floor_2737 spaces-community-country-club-towers 1bed 1bath spaces-market-country-club-towers-gardens"
|
||||
data-spaces-id="154442"
|
||||
data-spaces-soonest="2025-02-01"
|
||||
data-spaces-sort-date="1738368000"
|
||||
data-spaces-sort-price="2495"
|
||||
data-spaces-sort-area="802"
|
||||
data-spaces-sort-bed="1"
|
||||
data-spaces-unit="W0809"
|
||||
data-spaces-unit-id="154442"
|
||||
data-spaces-unit-floor="2737"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="8022"
|
||||
data-spaces-sort-plan-name="Pikes Peak"
|
||||
data-spaces-sort-bath="1"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154442"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154442"
|
||||
aria-label="Unit W0809">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">2495</span>
|
||||
<span class="spaces-unit__unit">W0809</span>
|
||||
</div>
|
||||
</article>
|
||||
<article
|
||||
class="spaces-unit price_2000-2500 floor_2751 spaces-community-country-club-towers 1bed 1bath spaces-market-country-club-towers-gardens"
|
||||
data-spaces-id="154764"
|
||||
data-spaces-soonest="2026-02-17"
|
||||
data-spaces-sort-date="1771286400"
|
||||
data-spaces-sort-price="2435"
|
||||
data-spaces-sort-area="715"
|
||||
data-spaces-sort-bed="1"
|
||||
data-spaces-unit="W2308"
|
||||
data-spaces-unit-id="154764"
|
||||
data-spaces-unit-floor="2751"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="8023"
|
||||
data-spaces-sort-plan-name="Longs Peak"
|
||||
data-spaces-sort-bath="1"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154764"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154764"
|
||||
aria-label="Unit W2308">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">2435</span>
|
||||
<span class="spaces-unit__unit">W2308</span>
|
||||
</div>
|
||||
</article>
|
||||
<article
|
||||
class="spaces-unit price_2500-3000 floor_2744 spaces-community-country-club-towers 1bed 1bath spaces-market-country-club-towers-gardens"
|
||||
data-spaces-id="154612"
|
||||
data-spaces-soonest="2026-03-24"
|
||||
data-spaces-sort-date="1774310400"
|
||||
data-spaces-sort-price="2683"
|
||||
data-spaces-sort-area="797"
|
||||
data-spaces-sort-bed="1"
|
||||
data-spaces-unit="W1610"
|
||||
data-spaces-unit-id="154612"
|
||||
data-spaces-unit-floor="2744"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="8024"
|
||||
data-spaces-sort-plan-name="Torreys Peak"
|
||||
data-spaces-sort-bath="1"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154612"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154612"
|
||||
aria-label="Unit W1610">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">2683</span>
|
||||
<span class="spaces-unit__unit">W1610</span>
|
||||
</div>
|
||||
</article>
|
||||
<article
|
||||
class="spaces-unit price_3500plus floor_2739 spaces-community-country-club-towers 2bed 2bath spaces-market-country-club-towers-gardens"
|
||||
data-spaces-id="154495"
|
||||
data-spaces-soonest="2025-11-07"
|
||||
data-spaces-sort-date="1762473600"
|
||||
data-spaces-sort-price="4470"
|
||||
data-spaces-sort-area="1230"
|
||||
data-spaces-sort-bed="2"
|
||||
data-spaces-unit="E1011"
|
||||
data-spaces-unit-id="154495"
|
||||
data-spaces-unit-floor="2739"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="8025"
|
||||
data-spaces-sort-plan-name="Maroon Peak"
|
||||
data-spaces-sort-bath="2"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154495"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154495"
|
||||
aria-label="Unit E1011">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">4470</span>
|
||||
<span class="spaces-unit__unit">E1011</span>
|
||||
</div>
|
||||
</article>
|
||||
<article
|
||||
class="spaces-unit price_3500plus floor_2760 spaces-community-country-club-towers 2bed 2.5bath has_tour spaces-market-country-club-towers-gardens penthouse"
|
||||
data-spaces-id="154919"
|
||||
data-spaces-soonest="2025-10-27"
|
||||
data-spaces-sort-date="1761523200"
|
||||
data-spaces-sort-price="8581"
|
||||
data-spaces-sort-area="1532"
|
||||
data-spaces-sort-bed="2"
|
||||
data-spaces-unit="E3205"
|
||||
data-spaces-unit-id="154919"
|
||||
data-spaces-unit-floor="2760"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-unavailable="false"
|
||||
data-spaces-available="true"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="420"
|
||||
data-spaces-specials-content=""
|
||||
data-spaces-plan-id="8037"
|
||||
data-spaces-sort-plan-name="Mt. Antero"
|
||||
data-spaces-sort-bath="2.5"
|
||||
data-spaces-href="?spaces_tab=unit-detail&detail=154919"
|
||||
data-spaces-inventory-href="?spaces_tab=unit-detail&detail=154919"
|
||||
aria-label="Unit E3205">
|
||||
<div class="spaces-unit__inner">
|
||||
<span class="spaces-unit__price">8581</span>
|
||||
<span class="spaces-unit__unit">E3205</span>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -87,55 +87,55 @@ describe('parseUnits', () => {
|
||||
describe('attribute extraction', () => {
|
||||
it('should extract all expected attributes from a fully populated article', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
const unit1 = units.find(u => u.unit_code === 'CCT-101');
|
||||
const unit1 = units.find(u => u.unit_code === 'W2707');
|
||||
|
||||
expect(unit1).toBeDefined();
|
||||
|
||||
// Core identifiers
|
||||
expect(unit1.id).toBe('1001');
|
||||
expect(unit1.unit_code).toBe('CCT-101');
|
||||
expect(unit1.unit_id).toBe('5001');
|
||||
expect(unit1.id).toBe('154842');
|
||||
expect(unit1.unit_code).toBe('W2707');
|
||||
expect(unit1.unit_id).toBe('154842');
|
||||
|
||||
// Physical attributes
|
||||
expect(unit1.floor).toBe('1');
|
||||
expect(unit1.area).toBe('750');
|
||||
expect(unit1.bed_count).toBe('1');
|
||||
expect(unit1.bath_count).toBe('1');
|
||||
expect(unit1.floor).toBe('2755');
|
||||
expect(unit1.area).toBe('1210');
|
||||
expect(unit1.bed_count).toBe('2');
|
||||
expect(unit1.bath_count).toBe('2');
|
||||
|
||||
// Pricing
|
||||
expect(unit1.price).toBe('1450');
|
||||
expect(unit1.price).toBe('5230');
|
||||
|
||||
// Availability
|
||||
expect(unit1.available).toBe('true');
|
||||
expect(unit1.unavailable).toBe('false');
|
||||
expect(unit1.soonest).toBe('2026-03-01');
|
||||
expect(unit1.date_available).toBe('20260301');
|
||||
expect(unit1.soonest).toBe('2025-10-11');
|
||||
expect(unit1.date_available).toBe('1760140800');
|
||||
|
||||
// Plan information
|
||||
expect(unit1.plan_id).toBe('201');
|
||||
expect(unit1.plan_name).toBe('Alpine');
|
||||
expect(unit1.plan_id).toBe('10270');
|
||||
expect(unit1.plan_name).toBe('Pyramid Peak - Terrace');
|
||||
|
||||
// Property information
|
||||
expect(unit1.obj_type).toBe('unit');
|
||||
expect(unit1.community).toBe('Country Club Towers');
|
||||
expect(unit1.asset).toBe('100');
|
||||
expect(unit1.asset).toBe('420');
|
||||
|
||||
// URLs
|
||||
expect(unit1.href).toBe('/units/CCT-101');
|
||||
expect(unit1.inventory_href).toBe('/inventory/CCT-101');
|
||||
expect(unit1.href).toBe('?spaces_tab=unit-detail&detail=154842');
|
||||
expect(unit1.inventory_href).toBe('?spaces_tab=unit-detail&detail=154842');
|
||||
|
||||
// Specials
|
||||
expect(unit1.specials_content).toBe('First month free!');
|
||||
expect(unit1.specials_content).toBe('');
|
||||
});
|
||||
|
||||
it('should extract correct count of units from the fixture', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
expect(units).toHaveLength(5);
|
||||
expect(units).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('should return raw string values without type conversion', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
const unit = units.find(u => u.unit_code === 'CCT-101');
|
||||
const unit = units.find(u => u.unit_code === 'W2707');
|
||||
|
||||
// All values should be strings (raw extraction, no conversion)
|
||||
expect(typeof unit.id).toBe('string');
|
||||
@ -152,24 +152,48 @@ describe('parseUnits', () => {
|
||||
// ---------------------------------------------------------------
|
||||
describe('image URL extraction', () => {
|
||||
it('should extract image_url from img src attribute', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
const unit1 = units.find(u => u.unit_code === 'CCT-101');
|
||||
const html = `
|
||||
<html><body>
|
||||
<section class="spaces__tab-unit">
|
||||
<article data-spaces-id="1" data-spaces-unit="IMG-1" data-spaces-sort-price="1000">
|
||||
<img src="https://example.com/images/unit-1.jpg" />
|
||||
</article>
|
||||
</section>
|
||||
</body></html>
|
||||
`;
|
||||
const units = parseUnits(html, mockLogger);
|
||||
|
||||
expect(unit1.image_url).toBe('https://example.com/images/unit-101.jpg');
|
||||
expect(units[0].image_url).toBe('https://example.com/images/unit-1.jpg');
|
||||
});
|
||||
|
||||
it('should fall back to data-src when src is not present', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
const unit2 = units.find(u => u.unit_code === 'CCT-205');
|
||||
const html = `
|
||||
<html><body>
|
||||
<section class="spaces__tab-unit">
|
||||
<article data-spaces-id="1" data-spaces-unit="IMG-2" data-spaces-sort-price="1000">
|
||||
<img data-src="https://example.com/images/unit-2.jpg" />
|
||||
</article>
|
||||
</section>
|
||||
</body></html>
|
||||
`;
|
||||
const units = parseUnits(html, mockLogger);
|
||||
|
||||
expect(unit2.image_url).toBe('https://example.com/images/unit-205.jpg');
|
||||
expect(units[0].image_url).toBe('https://example.com/images/unit-2.jpg');
|
||||
});
|
||||
|
||||
it('should not set image_url when no img element exists', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
const unit3 = units.find(u => u.unit_code === 'CCT-310');
|
||||
const html = `
|
||||
<html><body>
|
||||
<section class="spaces__tab-unit">
|
||||
<article data-spaces-id="1" data-spaces-unit="IMG-3" data-spaces-sort-price="1000">
|
||||
<div>No image here</div>
|
||||
</article>
|
||||
</section>
|
||||
</body></html>
|
||||
`;
|
||||
const units = parseUnits(html, mockLogger);
|
||||
|
||||
expect(unit3.image_url).toBeUndefined();
|
||||
expect(units[0].image_url).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -178,37 +202,67 @@ describe('parseUnits', () => {
|
||||
// ---------------------------------------------------------------
|
||||
describe('missing attributes', () => {
|
||||
it('should return undefined for attributes not present on the article', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
// Unit 5 (CCT-520) is missing floor, area, bed_count, bath_count,
|
||||
// soonest, date_available, plan_id, plan_name, href, inventory_href, specials_content
|
||||
const unit5 = units.find(u => u.unit_code === 'CCT-520');
|
||||
const html = `
|
||||
<html><body>
|
||||
<section class="spaces__tab-unit">
|
||||
<article
|
||||
data-spaces-id="1005"
|
||||
data-spaces-unit="SPARSE-1"
|
||||
data-spaces-unit-id="5005"
|
||||
data-spaces-sort-price="1300"
|
||||
data-spaces-available="true"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="100">
|
||||
</article>
|
||||
</section>
|
||||
</body></html>
|
||||
`;
|
||||
const units = parseUnits(html, mockLogger);
|
||||
const unit = units[0];
|
||||
|
||||
expect(unit5).toBeDefined();
|
||||
expect(unit5.floor).toBeUndefined();
|
||||
expect(unit5.area).toBeUndefined();
|
||||
expect(unit5.bed_count).toBeUndefined();
|
||||
expect(unit5.bath_count).toBeUndefined();
|
||||
expect(unit5.soonest).toBeUndefined();
|
||||
expect(unit5.date_available).toBeUndefined();
|
||||
expect(unit5.plan_id).toBeUndefined();
|
||||
expect(unit5.plan_name).toBeUndefined();
|
||||
expect(unit5.href).toBeUndefined();
|
||||
expect(unit5.inventory_href).toBeUndefined();
|
||||
expect(unit5.specials_content).toBeUndefined();
|
||||
expect(unit).toBeDefined();
|
||||
expect(unit.floor).toBeUndefined();
|
||||
expect(unit.area).toBeUndefined();
|
||||
expect(unit.bed_count).toBeUndefined();
|
||||
expect(unit.bath_count).toBeUndefined();
|
||||
expect(unit.soonest).toBeUndefined();
|
||||
expect(unit.date_available).toBeUndefined();
|
||||
expect(unit.plan_id).toBeUndefined();
|
||||
expect(unit.plan_name).toBeUndefined();
|
||||
expect(unit.href).toBeUndefined();
|
||||
expect(unit.inventory_href).toBeUndefined();
|
||||
expect(unit.specials_content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should still extract the attributes that are present on a sparse article', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
const unit5 = units.find(u => u.unit_code === 'CCT-520');
|
||||
const html = `
|
||||
<html><body>
|
||||
<section class="spaces__tab-unit">
|
||||
<article
|
||||
data-spaces-id="1005"
|
||||
data-spaces-unit="SPARSE-1"
|
||||
data-spaces-unit-id="5005"
|
||||
data-spaces-sort-price="1300"
|
||||
data-spaces-available="true"
|
||||
data-spaces-obj="unit"
|
||||
data-spaces-community="Country Club Towers"
|
||||
data-spaces-asset="100">
|
||||
</article>
|
||||
</section>
|
||||
</body></html>
|
||||
`;
|
||||
const units = parseUnits(html, mockLogger);
|
||||
const unit = units[0];
|
||||
|
||||
expect(unit5.id).toBe('1005');
|
||||
expect(unit5.unit_code).toBe('CCT-520');
|
||||
expect(unit5.unit_id).toBe('5005');
|
||||
expect(unit5.price).toBe('1300');
|
||||
expect(unit5.available).toBe('true');
|
||||
expect(unit5.obj_type).toBe('unit');
|
||||
expect(unit5.community).toBe('Country Club Towers');
|
||||
expect(unit5.asset).toBe('100');
|
||||
expect(unit.id).toBe('1005');
|
||||
expect(unit.unit_code).toBe('SPARSE-1');
|
||||
expect(unit.unit_id).toBe('5005');
|
||||
expect(unit.price).toBe('1300');
|
||||
expect(unit.available).toBe('true');
|
||||
expect(unit.obj_type).toBe('unit');
|
||||
expect(unit.community).toBe('Country Club Towers');
|
||||
expect(unit.asset).toBe('100');
|
||||
});
|
||||
});
|
||||
|
||||
@ -313,7 +367,7 @@ describe('parseUnits', () => {
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
'Units parsed',
|
||||
expect.objectContaining({ count: 5 })
|
||||
expect.objectContaining({ count: 10 })
|
||||
);
|
||||
});
|
||||
|
||||
@ -334,27 +388,44 @@ describe('parseUnits', () => {
|
||||
// ---------------------------------------------------------------
|
||||
describe('varied unit data', () => {
|
||||
it('should handle unavailable units correctly', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
const unit4 = units.find(u => u.unit_code === 'CCT-415');
|
||||
const html = `
|
||||
<html><body>
|
||||
<section class="spaces__tab-unit">
|
||||
<article
|
||||
data-spaces-id="4001"
|
||||
data-spaces-unit="UNAVAIL-1"
|
||||
data-spaces-sort-price="2000"
|
||||
data-spaces-available="false"
|
||||
data-spaces-unavailable="true"
|
||||
data-spaces-soonest=""
|
||||
data-spaces-sort-date="">
|
||||
</article>
|
||||
</section>
|
||||
</body></html>
|
||||
`;
|
||||
const units = parseUnits(html, mockLogger);
|
||||
const unit = units[0];
|
||||
|
||||
expect(unit4.available).toBe('false');
|
||||
expect(unit4.unavailable).toBe('true');
|
||||
expect(unit4.soonest).toBe('');
|
||||
expect(unit4.date_available).toBe('');
|
||||
expect(unit.available).toBe('false');
|
||||
expect(unit.unavailable).toBe('true');
|
||||
expect(unit.soonest).toBe('');
|
||||
expect(unit.date_available).toBe('');
|
||||
});
|
||||
|
||||
it('should handle half bath counts', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
const unit2 = units.find(u => u.unit_code === 'CCT-205');
|
||||
// E3205 is the penthouse with 2.5 baths
|
||||
const unit = units.find(u => u.unit_code === 'E3205');
|
||||
|
||||
expect(unit2.bath_count).toBe('1.5');
|
||||
expect(unit.bath_count).toBe('2.5');
|
||||
});
|
||||
|
||||
it('should handle studio units (bed_count = 0)', () => {
|
||||
const units = parseUnits(sampleHtml, mockLogger);
|
||||
const unit3 = units.find(u => u.unit_code === 'CCT-310');
|
||||
// W2603 is the studio with bed_count 0
|
||||
const unit = units.find(u => u.unit_code === 'W2603');
|
||||
|
||||
expect(unit3.bed_count).toBe('0');
|
||||
expect(unit.bed_count).toBe('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user