Campground Data-Layer Self-Tests
Deterministic tests for map mode, site numbering, the campsite attribute resolver, overview hotspots, and the path-based site generator.
Data-Layer Self-Tests
✓ ALL PASS100 passed · 0 failed · 100 total
✓
mode: missing mode defaults to standard
✓
mode: campground mode preserved and detected
✓
mode: switching never deletes objects
✓
numbering: sequential unique numbers
✓
numbering: prefix, suffix, padding, increment
✓
numbering: duplicates are skipped, never emitted
✓
numbering: both-sides generation stays unique
✓
path: arc length + position sampling
✓
path: straight line positions + tangent rotation
✓
path: left and right offsets are perpendicular
✓
path: vertical path tangent + curved polyline rotations
✓
path: rotation modes (follow/face/fixed)
✓
path: boundary skip reports and preserves numbering
✓
path: sites that do not fit are SKIPPED, never stacked
✓
path: rotation-aware SAT never false-positives on rotated sites
✓
path: placement colliding with an EXISTING site is skipped
✓
path: both sides sequential completes one side then the other
✓
resolver: type defaults apply when the site has no override
✓
resolver: area defaults are the fallback level
✓
resolver: individual overrides supersede all defaults
✓
resolver: never mutates inputs; price/geometry resolve in order
✓
resolver: findBoothTypeForBooth reads raw show booth_types
✓
overview: absent config normalizes to disabled defaults
✓
overview: unknown keys preserved through normalize round-trip
✓
overview: rectangle hotspot hit test + bounds
✓
overview: polygon hotspot hit test (ray casting)
✓
overview: validation warns on missing image and unlinked hotspots
✓
overview: hotspot defaults (public, enabled, colors, order)
✓
renumber: plans sequential numbers by position (top→bottom, left→right)
✓
renumber: skips numbers reserved by out-of-scope sites
✓
renumber: reverse order + increment + immutability
✓
commit: preview coordinates equal commit payload coordinates (exact)
✓
commit: payload carries every geometry field + room + derived percents
✓
commit: JSON save/reload round-trip keeps positions nonzero
✓
commit: per-site rotation persists (straight and curved paths)
✓
commit: left / right / both offset geometry persists
✓
commit: both-sides numbering unique through commit
✓
commit: road and campsites commit together as one batch
✓
commit: invalid site geometry blocks the whole batch — no partial records
✓
commit: blank road name blocks campsite creation
✓
commit: empty or cancelled batch creates no records
✓
commit: save-road OFF creates sites without a road
✓
roads: rename preserves the relationship (id stable, name resolves)
✓
roads: deletion removes the road, never campsites
✓
roads: bend projection maps the CURVED road back to the correct ANCHOR segment
✓
roads: public-hidden road filtered from public rendering and name lookup
✓
roads: public display resolves the NAME, never the internal id
✓
roads: config round-trip through roadsFromConfig
✓
standard: commit records match the standard booth field contract; no roads
✓
whiteboard roads: two-click creation record (straight, no handles)
✓
whiteboard roads: dragging the body inserts a bend anchor with smooth handles
✓
whiteboard roads: multiple control points (S-curve via a second insert)
✓
whiteboard roads: handle drag mirrors the opposite handle; endpoint handles inert
✓
whiteboard roads: anchor removal keeps ≥2 anchors and re-smooths the joint
✓
whiteboard roads: save/reload geometry round-trip is exact
✓
road sites: horizontal segment — perpendicular orientation, left side
✓
road sites: vertical segment — rotation and side follow the road
✓
road sites: curved bend — positions AND rotations progressively follow the curve
✓
road sites: S-curve — spacing measured as distance traveled along the road
✓
road sites: both sides — continue numbering vs separate prefixes
✓
road sites: perpendicular vs parallel rotation rule
✓
road sites: committed geometry equals the preview exactly; road id persists; rotation survives reload
✓
road sites: roadEdgeCenterOffset formula — road half + setback + rotated half-extent
✓
road sites: flush at 0, exact clearance, road width + depth move the center
✓
road sites: left-only and right-only continuous rows
✓
road sites: both sides — two complete rows, 10 per side = 20, never alternating
✓
road sites: continue numbering completes side one (1–10) then side two (11–20)
✓
road sites: curved + S-curve — consistent sides, exact clearance, no flips or duplicates
✓
road sites: parallel orientation uses the projected footprint for clearance
✓
road sites: tight bend never STACKS sites on the inner curve
✓
translateRoad: moves every anchor and handle by the delta, purely
✓
roadBoundsFt: bounds cover anchors and control handles
✓
linked sites: update plan follows the road — offsets preserved, no 180° flips
✓
linked sites: update happens only after confirmation — the planner never mutates
✓
linked sites: deleting the road keeps every site and its reference
✓
hit targets: road hit path is wider than the visible road at any zoom
✓
hit targets: control-point grab radii meet the minimum screen size
✓
roads: renaming changes only the name — geometry and linked sites untouched
✓
linked sites: Keep Existing Site Positions discards the plan — sites never move
✓
linked sites: shortening a road below capacity never clamps sites to the endpoint
✓
linked sites: insufficient capacity produces the exact fit count
✓
linked sites: duplicate campsite coordinates block Apply
✓
linked sites: significant campsite overlap blocks Apply
✓
linked sites: Keep Existing Site Positions leaves every site unchanged even at insufficient capacity
✓
linked sites: Cancel Road Edit restores the pre-edit road and leaves sites unchanged
✓
linked sites: lengthening the road makes Apply available again
✓
shape roads: circle geometry is a closed exact Bézier circle
✓
shape roads: rectangle geometry is a closed rectangular loop
✓
shape roads: oval geometry is a closed exact Bézier ellipse
✓
shape roads: bending a loop inserts into the correct segment
✓
merge: findMergeTarget picks the nearest open endpoint
✓
merge: dragging an endpoint onto another road merges end-to-end
✓
merge: merged geometry survives normalization exactly
✓
pieces: straight piece extends the road along its end tangent
✓
pieces: straight piece continues a curve smoothly along its tangent
✓
pieces: curve piece is an exact quarter arc, left or right, joined smoothly
✓
pieces: dragging the endpoint resizes the arc and flips its direction
✓
pieces: attach at START extends the road's other end
✓
pieces: straights + curves snap into a full rounded loop (train set)
✓
pieces: closed loops cannot be extended
Public Map View Self-Tests
✓ ALL PASS20 passed · 0 failed · 20 total
✓
public filter: persisted public campsite included
✓
public filter: explicitly hidden campsite excluded
✓
public filter: non-reservable public campsite still included
✓
public filter: undefined legacy flag follows compatibility rule (stays public)
✓
new road site: explicit public+reservable defaults, true 15×40 geometry unchanged
✓
version: save increments the map/data version
✓
cache: key includes event + area + version; changed version invalidates
✓
fit campsites: bounds contain all public sites (x_ft based, not legacy x)
✓
fit campsites: rotated campsite included correctly in content bounds
✓
fit campsites: hidden sites and invalid coordinates ignored
✓
fit campsites: empty content falls back (null → Fit Full Area)
✓
fit campsites: associated public road included in bounds
✓
fit full area: whole 2000×1500 area fits, centered, scale = fit
✓
fit campsites: small content cluster zooms in on a large area
✓
search zoom: result centers and zooms to the correct site even when tiny
✓
stroke: minimum screen-space border width in feet math
✓
hit target: minimum 16px screen target without changing the displayed footprint
✓
labels: readable check drives the zoom-in cue and label reveal
✓
states: no premature empty state during lazy load
✓
states: ready / error / unavailable / empty
Property Overview Authoring Self-Tests
✓ ALL PASS52 passed · 0 failed · 52 total
✓
image file validation accepts png under the size limit
✓
image metadata normalization stores url + intrinsic size
✓
low-resolution quality warning (non-blocking)
✓
rectangle hotspot normalization fills stable defaults
✓
polygon hotspot normalization keeps ordered vertices
✓
polygon with too few vertices falls back to a valid quad
✓
Fit/Contain letterbox rect: wide image in square box is centered
✓
responsive coordinate round-trip survives letterboxing
✓
hotspot feet coordinates are renderer-size independent
✓
same-aspect-ratio replacement keeps hotspots and gives no warning
✓
different-aspect-ratio replacement warns for alignment review
✓
removing the image never removes areas or hotspots
✓
stable hotspot IDs: no collisions across many generations
✓
stable area IDs: no collisions against existing rooms
✓
create new area is idempotent (double click / retry safe)
✓
link existing area updates the hotspot by stable id
✓
area rename preserves the hotspot link (links are by id, not name)
✓
deleting an area also deletes its linked hotspots (one action, both sides)
✓
deleting a hotspot never deletes its linked area
✓
area delete confirmation counts campsites, roads, landmarks and links
✓
duplicate hotspot gets a new stable id and no silent area link
✓
polygon vertex add/remove keeps at least 3 vertices
✓
public/private filtering in publish validation
✓
invalid area link detection (missing room)
✓
self-intersecting polygon detection
✓
duplicate hotspot ID detection
✓
zero-size and outside-image hotspot detection
✓
multiple hotspots on one area produce a confirm-needed warning
✓
overview save preserves every unknown config key
✓
existing campground map config is unchanged outside the overview key
✓
standard-mode maps remain unchanged (merge never alters other keys)
✓
overview-only save creates zero campsite writes (diff engine)
✓
overview-to-area navigation resolves a hotspot to its area
✓
campsite search resolves directly to the area (bypasses the overview)
✓
accessible hotspot label format
✓
regression: rect drawn after earlier edits appends instead of wiping
✓
regression: moving a rect patches only that hotspot (latest state)
✓
regression: resizing a rect patches only that hotspot
✓
regression: moving a polygon keeps vertex structure and other hotspots
✓
regression: single-vertex drag changes one point, others byte-identical
✓
regression: link change then revert round-trips byte-identical
✓
regression: composed add+move+resize+link keeps every hotspot
✓
rotation: defaults to 0, survives normalize round-trips, normalizes to 0-360
✓
rotation: rotated rect renders, bounds, and hit-tests through its rotated corners
✓
rotation: unrotated rect hit test stays edge-inclusive (legacy behavior)
✓
resize: dragging a corner keeps the opposite corner fixed (unrotated)
✓
resize: rotated rect resizes along its own axes (opposite corner stays exactly in place)
✓
corner-pull: rect converts to a 4-vertex polygon so each corner drags individually
✓
corner-pull: a rotated rect keeps its exact outline through conversion
✓
corner-pull: conversion is a no-op for polygons and unknown ids
✓
rotate: polygon vertices rotate around the center; zero degrees is a no-op
✓
rotate: rotation drag patches only the target hotspot (drag-path purity)
Campsite Number Uniqueness Self-Tests
✓ ALL PASS21 passed · 0 failed · 21 total
✓
uniqueness: normalization trims, collapses whitespace, uppercases
✓
uniqueness: numeric padding is NOT normalized (A-01 ≠ A-1)
✓
uniqueness: two keyless creates with the same number still conflict (batch-internal)
✓
uniqueness: keyless creates with distinct numbers pass
✓
uniqueness: same number in same area rejected
✓
uniqueness: same number in different areas allowed
✓
uniqueness: case/whitespace variants of an existing number rejected
✓
uniqueness: batch-internal conflicts detected
✓
uniqueness: conflicts against persisted records (existing dups present)
✓
uniqueness: update retains its own number (legacy duplicates compatible)
✓
uniqueness: existing duplicates do not block unrelated edits (move/amenities)
✓
uniqueness: update renumbered onto another site's number rejected
✓
uniqueness: persistence-key retry remains idempotent
✓
uniqueness: deletes free numbers within the same save
✓
uniqueness: guard applies only to campground saves (standard compatibility)
✓
generation: conflicts detected against persisted and batch-internal numbers
✓
numbering: skip-existing comparison is normalized, display formatting preserved
✓
list: duplicateNumberGroups groups by area + normalized number
✓
list: duplicate filter shows only duplicated rows
✓
list: aggregate counts per-area duplicates (normalized)
✓
publish: within-area duplicate campsite numbers block publishing
Campsite Selection Self-Tests
✓ ALL PASS18 passed · 0 failed · 18 total
✓
utilities: 30 Amp Water & Electric derives Electric/Water and explicit Sewer: No
✓
utilities: 50 Amp Water & Electric derives Electric/Water and explicit Sewer: No
✓
utilities: 30 Amp Full Hookup derives Electric/Water/Sewer
✓
utilities: 50 Amp Full Hookup derives Electric/Water/Sewer
✓
utilities: explicit site values override type-derived values
✓
utilities: unknown campsite with a populated explicit value shows only that value
✓
utilities: blank data with unknown type → Not specified (never 'No')
✓
utilities: resolvedUtilities keeps unknown fields null
✓
utilities: full hookup type never implies missing sewer
✓
utilities: amperage 'none' stays explicit Electric: None
✓
utilities: empty-string amperage falls through to type derivation
✓
typeNameForTypeKey resolves booking type-key snapshots
✓
availability: booked site is Unavailable and must not be reservable
✓
availability: informational-only site (reservable=false) is Info only
✓
availability: open reservable site is Available
✓
informational dimensions: effective carries map geometry + display string, geometry untouched
✓
informational dimensions: actual_* never becomes effective width/height
✓
informational dimensions: editing display strings never rewrites geometry fields
Deep-Link & Type-Label Self-Tests
✓ ALL PASS13 passed · 0 failed · 13 total
✓
type label: resolves the friendly name from the show's type config
✓
type label: never exposes a raw internal type key
✓
type label: unknown key falls back — never a title-cased raw key
✓
type label: empty or missing key falls back
✓
type label: missing/unparseable show config falls back
✓
typeNameForTypeKey stays null for unknown keys (existing behavior)
✓
deep link: no action when the target id is unchanged
✓
deep link: waits for the search index — no failure, no loop
✓
deep link: first load with a valid id applies it
✓
deep link: a new valid id applies the NEW site (query change)
✓
deep link: invalid or inaccessible id is rejected — nothing selected
✓
deep link: removing the query clears the selection
✓
deep link: normal in-page selection without a query is untouched