Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/parks/universal/__tests__/places.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,102 @@ describe('placeToEntity', () => {
expect(e?.parentId).toBe('uor.ioa');
});

// USH Upper/Lower Lot are sub-areas of the single `ush.ush` park (not surfaced
// as parks themselves), so their children must reparent onto `ush.ush` or they
// strand in the sync's unresolved-parent queue forever.
test('USH Upper Lot child → reparented to ush.ush', () => {
const upperLotDining: UniversalPlace = {
place_id: 'ush.dining.krusty_burger',
name: 'Krusty Burger',
resort_area_code: 'ush',
venue_id: 'ush.upper_lot',
place_type: {type: 'Dining'},
};
expect(placeToEntity(upperLotDining, DESTINATION, TZ)?.parentId).toBe('ush.ush');
});

test('USH Lower Lot child → reparented to ush.ush', () => {
const lowerLotRide: UniversalPlace = {
place_id: 'ush.lower_lot.rides.jurassic_world_the_ride',
name: 'Jurassic World — The Ride',
resort_area_code: 'ush',
venue_id: 'ush.lower_lot',
place_type: {type: 'Ride'},
};
expect(placeToEntity(lowerLotRide, DESTINATION, TZ)?.parentId).toBe('ush.ush');
});

test('CityWalk child → null (district not surfaced as a park; excluded)', () => {
const ushCityWalk: UniversalPlace = {
place_id: 'ush.cw.dining.voodoo_doughnut',
name: 'Voodoo Doughnut',
resort_area_code: 'ush',
venue_id: 'ush.cw',
place_type: {type: 'Dining'},
};
const uorCityWalk: UniversalPlace = {
place_id: 'uor.cw.dining.voodoo_doughnut',
name: 'Voodoo Doughnut',
resort_area_code: 'uor',
venue_id: 'uor.cw',
place_type: {type: 'Dining'},
};
expect(placeToEntity(ushCityWalk, DESTINATION, TZ)).toBeNull();
expect(placeToEntity(uorCityWalk, DESTINATION, TZ)).toBeNull();
});

test('Surfaced-park venue is left unchanged (no accidental remap)', () => {
// ridePlace.venue_id === 'uor.usf' (a real park) must pass through as-is.
expect(placeToEntity(ridePlace, DESTINATION, TZ)?.parentId).toBe('uor.usf');
});

// Event-flagged variants that alias ANOTHER canonical POI (language / last-tram
// duplicates) are dropped; genuine event entities that link to themselves stay.
const evented = (
placeId: string,
name: string,
type: 'Ride' | 'Show',
shareTargetId: string,
isEvent = 'true',
): UniversalPlace => ({
place_id: placeId,
name,
resort_area_code: 'ush',
venue_id: 'ush.upper_lot',
place_type: {
type,
attributes: [
{name: 'is_event', value: isEvent},
{
name: 'social_sharing_link',
value: `https://www.universalstudioshollywood.com/applinks/poi?id=${shareTargetId}`,
},
],
},
});

test('Studio Tour - Mandarin (event, shares WaterWorld id) → null', () => {
const p = evented('ush.upper_lot.shows.studio_tour_mandarin', 'Studio Tour - Mandarin', 'Show', 'ush.upper_lot.shows.waterworld');
expect(placeToEntity(p, DESTINATION, TZ)).toBeNull();
});

test('Studio Tour Last Tram (event, shares canonical Studio Tour id) → null', () => {
const p = evented('ush.upper_lot.rides.studio_tour_last_tram', 'Studio Tour Last Tram', 'Ride', 'ush.upper_lot.rides.studio_tour');
expect(placeToEntity(p, DESTINATION, TZ)).toBeNull();
});

test('Event entity whose share link points at itself → kept (Bowser Jr. Challenge)', () => {
const p = evented('ush.lower_lot.rides.bowser.jr.challenge', 'Bowser Jr. Challenge', 'Ride', 'ush.lower_lot.rides.bowser.jr.challenge');
const e = placeToEntity(p, DESTINATION, TZ);
expect(e).not.toBeNull();
expect(e?.parentId).toBe('ush.ush'); // and reparented off upper_lot
});

test('Canonical Studio Tour (not an event) → kept', () => {
const p = evented('ush.upper_lot.rides.studio_tour', 'Studio Tour', 'Ride', 'ush.upper_lot.rides.studio_tour', 'false');
expect(placeToEntity(p, DESTINATION, TZ)).not.toBeNull();
});

test('Shop → null (not in PLACE_TYPE_TO_ENTITY)', () => {
expect(placeToEntity(shopPlace, DESTINATION, TZ)).toBeNull();
});
Expand Down
61 changes: 60 additions & 1 deletion src/parks/universal/universal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,58 @@ const PARK_PLACE_ID_TO_LEGACY_VENUE_ID: Record<string, string> = {
'ush.ush': '13825',
};

/**
* Park-type places we deliberately do NOT surface as parks
* (PARK_PLACE_ID_TO_LEGACY_VENUE_ID omits them) still appear as the `venue_id`
* of real child places. Left alone, those children reference a parent entity
* that is never emitted, so the sync strands them in its unresolved-parent
* queue every cycle — never pushing them (observed on USH: 73 dining / rides /
* shows held indefinitely, incl. Fast & Furious: Hollywood Drift). Reconcile
* each such venue here, keyed by its sanitized venue_id:
* - a park id → reparent the child onto that surfaced park
* - null → the venue has no wiki representation; drop the child
*/
const NON_SURFACED_VENUE_PARENT: Record<string, string | null> = {
// USH — Upper/Lower Lot are sub-areas of the single `ush.ush` park.
'ush.upper_lot': 'ush.ush',
'ush.lower_lot': 'ush.ush',
// CityWalk is a dining/shopping district, not a park on the wiki — exclude.
'ush.cw': null,
'uor.cw': null,
};

/** Read a single attribute value from a place's place_type.attributes[]. */
function attr(place: UniversalPlace, name: string): string | undefined {
return place.place_type.attributes?.find((a) => a.name === name)?.value;
}

/**
* True for event-flagged language / operational *variants* of another POI —
* e.g. USH's "Studio Tour - Mandarin/Spanish" and "Studio Tour Last Tram",
* which are alternate-language / last-departure duplicates of the canonical
* Studio Tour (their share links even point at WaterWorld). The feed marks each
* `is_event=true` AND aims its `social_sharing_link?id=` at a DIFFERENT
* canonical place_id. A genuine standalone entity's share link points at itself
* — Bowser Jr. Challenge is also `is_event=true` but links to its own id, so it
* is kept. Dropping these variants keeps the park from filling with per-language
* / operational duplicates of a ride that's already listed.
*/
function isEventVariantAlias(place: UniversalPlace): boolean {
if (attr(place, 'is_event') !== 'true') return false;
const link = attr(place, 'social_sharing_link');
const match = typeof link === 'string' ? link.match(/[?&]id=([^&]+)/) : null;
if (!match) return false;
let sharedId: string;
try {
sharedId = decodeURIComponent(match[1]);
} catch {
sharedId = match[1];
}
// Compare sanitized ids — the share-link id and place_id are the same raw
// scheme, but normalise defensively (matches how entity ids are formed).
return sanitizeId(sharedId) !== sanitizeId(place.place_id);
}

/**
* Map a UniversalPlace to a wiki Entity. Returns null for place types we
* don't expose (Park is emitted separately by buildEntityList; Shop /
Expand All @@ -150,6 +197,9 @@ export function placeToEntity(
const entityType = PLACE_TYPE_TO_ENTITY[place.place_type.type];
if (!entityType) return null;

// Drop per-language / operational variants of another POI (see helper).
if (isEventVariantAlias(place)) return null;

const entity: Entity = {
id: sanitizeId(place.place_id),
name: place.name,
Expand All @@ -159,7 +209,16 @@ export function placeToEntity(
} as Entity;

if (place.venue_id) {
(entity as any).parentId = sanitizeId(place.venue_id);
const venue = sanitizeId(place.venue_id);
if (venue in NON_SURFACED_VENUE_PARENT) {
const reparent = NON_SURFACED_VENUE_PARENT[venue];
// null → venue isn't represented on the wiki (e.g. CityWalk): drop the
// child rather than strand it under a parent that is never emitted.
if (reparent === null) return null;
(entity as any).parentId = reparent;
} else {
(entity as any).parentId = venue;
}
}

const mapLoc = place.geometry?.locations?.find((l) => l.location_type === 'map');
Expand Down
Loading