diff --git a/examples/angular_sample_app/package.json b/examples/angular_sample_app/package.json index b1e17a14..1253290f 100644 --- a/examples/angular_sample_app/package.json +++ b/examples/angular_sample_app/package.json @@ -29,7 +29,7 @@ "@angular/cli": "^16.1.6", "@angular/compiler-cli": "^16.1.7", "@ngx-env/builder": "^16.1.2", - "@types/google.maps": "~3.55.8", + "@types/google.maps": "~3.65.0", "typescript": "~5.0.2" } } diff --git a/package-lock.json b/package-lock.json index 7a4317a1..75b8a16e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-terser": "^0.4.3", - "@types/google.maps": "~3.55.8", + "@types/google.maps": "~3.65.0", "@types/jasmine": "^4.3.6", "@types/react": "^18.2.24", "@web/test-runner": "^0.17.1", @@ -609,9 +609,9 @@ } }, "node_modules/@types/google.maps": { - "version": "3.55.8", - "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.55.8.tgz", - "integrity": "sha512-aSyvlCRXzF9Jtjqq4zmA24sczKZ0QWJnn4zRrkufCoohHulS6LCf4KsF22eAlnHBuVYwEhQoMXIufUS7kXF5uA==", + "version": "3.65.0", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.65.0.tgz", + "integrity": "sha512-u4SHiRC3m27lPa4vDBxh2AI7mDcHcheX6GSHn1Mwi0Gap8/uhM2kFppiFTnWASXLHZO+1ahHciLeEIV+Sjqk/A==", "dev": true }, "node_modules/@types/http-assert": { diff --git a/package.json b/package.json index 597706df..aef18aea 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@rollup/plugin-node-resolve": "^15.2.1", "@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-terser": "^0.4.3", - "@types/google.maps": "~3.55.8", + "@types/google.maps": "~3.65.0", "@types/jasmine": "^4.3.6", "@types/react": "^18.2.24", "@web/test-runner": "^0.17.1", @@ -100,42 +100,86 @@ "!**/*_test.ts", "!src/testing/*.ts" ], - "output": ["custom-elements.json"] + "output": [ + "custom-elements.json" + ] }, "docs": { "command": "node build/make_docs.js", - "dependencies": ["cem"], - "files": ["**/doc_src/*", "build/make_docs.js", "custom-elements.json"], - "output": ["README.md", "src/**/README.md", "!src/react/README.md"] + "dependencies": [ + "cem" + ], + "files": [ + "**/doc_src/*", + "build/make_docs.js", + "custom-elements.json" + ], + "output": [ + "README.md", + "src/**/README.md", + "!src/react/README.md" + ] }, "build": { - "dependencies": ["build:ts"] + "dependencies": [ + "build:ts" + ] }, "build:react": { "command": "node build/make_react.js", - "dependencies": ["cem"], - "files": ["build/make_react.js", "custom-elements.json"], - "output": ["src/react/index.ts"] + "dependencies": [ + "cem" + ], + "files": [ + "build/make_react.js", + "custom-elements.json" + ], + "output": [ + "src/react/index.ts" + ] }, "build:ts": { "command": "tsc", - "dependencies": ["build:react"], - "files": ["tsconfig.json", "src/**/*.ts"], - "output": [".tsbuildinfo", "lib/**/*"], + "dependencies": [ + "build:react" + ], + "files": [ + "tsconfig.json", + "src/**/*.ts" + ], + "output": [ + ".tsbuildinfo", + "lib/**/*" + ], "clean": "if-file-deleted" }, "build:package": { "command": ". build/finalize_package.sh", - "dependencies": ["build:ts"], - "files": ["build/finalize_package.sh", "lib/base/constants.js"], - "output": ["lib/**/*.js", "lib/**/*.md"], + "dependencies": [ + "build:ts" + ], + "files": [ + "build/finalize_package.sh", + "lib/base/constants.js" + ], + "output": [ + "lib/**/*.js", + "lib/**/*.md" + ], "clean": false }, "build:bundle": { "command": "rollup -c build/rollup.config.js", - "dependencies": ["build:package"], - "files": ["build/rollup.config.js", "lib/**/*.js"], - "output": ["dist/index.min.js"] + "dependencies": [ + "build:package" + ], + "files": [ + "build/rollup.config.js", + "lib/**/*.js" + ], + "output": [ + "dist/index.min.js" + ] }, "example:prepare": { "command": "chmod +x build/start_example.sh" @@ -145,7 +189,10 @@ "env": { "PORT": "8001" }, - "dependencies": ["build:package", "example:prepare"], + "dependencies": [ + "build:package", + "example:prepare" + ], "service": { "readyWhen": { "lineMatches": "You can now view \\S+ in the browser" @@ -157,7 +204,10 @@ "env": { "PORT": "8002" }, - "dependencies": ["build:bundle", "example:prepare"], + "dependencies": [ + "build:bundle", + "example:prepare" + ], "service": { "readyWhen": { "lineMatches": "Web Dev Server started" @@ -169,7 +219,10 @@ "env": { "PORT": "8003" }, - "dependencies": ["build:package", "example:prepare"], + "dependencies": [ + "build:package", + "example:prepare" + ], "service": { "readyWhen": { "lineMatches": "Angular Live Development Server is listening on localhost" @@ -183,13 +236,19 @@ "example:js_sample_app", "example:angular_sample_app" ], - "files": ["e2e/**/*.js"], + "files": [ + "e2e/**/*.js" + ], "output": [] }, "test": { "command": "wtr", - "dependencies": ["build:ts"], - "files": ["web-test-runner.config.js"], + "dependencies": [ + "build:ts" + ], + "files": [ + "web-test-runner.config.js" + ], "output": [] } } diff --git a/src/address_validation/suggest_validation_action.ts b/src/address_validation/suggest_validation_action.ts index ac369b68..5797f58f 100644 --- a/src/address_validation/suggest_validation_action.ts +++ b/src/address_validation/suggest_validation_action.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {Address, AddressValidation, Granularity} from '../utils/googlemaps_types.js'; +import {Address, AddressValidation} from '../utils/googlemaps_types.js'; /** Suggested action to take for this validation result. */ @@ -46,7 +46,7 @@ function isMissingNonSubpremiseComponent(result: AddressValidation): boolean { */ function hasValidationGranularityOther(result: AddressValidation): boolean { return !result.verdict?.validationGranularity || - result.verdict.validationGranularity === Granularity.OTHER; + result.verdict.validationGranularity === 'OTHER'; } function hasSuspiciousComponent(result: AddressValidation): boolean { @@ -70,7 +70,9 @@ function hasMajorInference(result: AddressValidation): boolean { ]); return !!result.address && result.address.components.some( - c => c.isInferred && !minorComponents.has(c.componentType)) + // TODO: Align with 3.65 typings + // @ts-ignore + c => c.isInferred && !minorComponents.has(c.componentType!)) } function hasReplacement(result: AddressValidation): boolean { @@ -158,4 +160,4 @@ export function suggestValidationAction(response: AddressValidation): return {suggestedAction: SuggestedAction.ADD_SUBPREMISES}; } return {suggestedAction: SuggestedAction.ACCEPT}; -} \ No newline at end of file +} diff --git a/src/address_validation/suggest_validation_action_test.ts b/src/address_validation/suggest_validation_action_test.ts index 6c1cb4cd..94c2cf26 100644 --- a/src/address_validation/suggest_validation_action_test.ts +++ b/src/address_validation/suggest_validation_action_test.ts @@ -4,16 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ +// TODO: Align with 3.65 typings +// @ts-nocheck + // import 'jasmine'; (google3-only) -import {Address, AddressComponent, AddressValidation, ConfirmationLevel, Granularity, Verdict} from '../utils/googlemaps_types.js'; +import {Address, AddressComponent, AddressValidation, Verdict} from '../utils/googlemaps_types.js'; import {SuggestedAction, suggestValidationAction} from './suggest_validation_action.js'; const GOOD_VERDICT: Verdict = { - inputGranularity: Granularity.PREMISE, - validationGranularity: Granularity.PREMISE, - geocodeGranularity: Granularity.PREMISE, + inputGranularity: 'PREMISE', + validationGranularity: 'PREMISE', + geocodeGranularity: 'PREMISE', isAddressComplete: true, hasUnconfirmedComponents: false, hasInferredComponents: false, @@ -67,7 +70,7 @@ describe('SuggestValidationAction', () => { it('returns FIX when the validation granularity is OTHER', () => { const suggestion = suggestValidationAction(makeFakeValidationResponse( - {}, {...GOOD_VERDICT, validationGranularity: Granularity.OTHER})); + {}, {...GOOD_VERDICT, validationGranularity: 'OTHER'})); expect(suggestion.suggestedAction).toBe(SuggestedAction.FIX); }); @@ -76,7 +79,7 @@ describe('SuggestValidationAction', () => { { components: [{ ...LOCALITY_COMPONENT, - confirmationLevel: ConfirmationLevel.UNCONFIRMED_AND_SUSPICIOUS + confirmationLevel: 'UNCONFIRMED_AND_SUSPICIOUS' }] }, GOOD_VERDICT)); diff --git a/src/place_building_blocks/place_attribution/place_attribution_test.ts b/src/place_building_blocks/place_attribution/place_attribution_test.ts index c7017321..e3771800 100644 --- a/src/place_building_blocks/place_attribution/place_attribution_test.ts +++ b/src/place_building_blocks/place_attribution/place_attribution_test.ts @@ -10,6 +10,7 @@ import {html, TemplateResult} from 'lit'; import {Environment} from '../../testing/environment.js'; import {makeFakePlace} from '../../testing/fake_place.js'; +import {mapsJsData} from '../../utils/googlemaps_types.js'; import {PlaceAttribution} from './place_attribution.js'; @@ -64,8 +65,8 @@ describe('place attribution test', () => { const place = makeFakePlace({ id: '1234567890', attributions: [ - {provider: 'Foo', providerURI: 'https://foo.com'}, - {provider: 'Bar', providerURI: null} + mapsJsData({provider: 'Foo', providerURI: 'https://foo.com'}), + mapsJsData({provider: 'Bar', providerURI: null}) ] }); diff --git a/src/place_building_blocks/place_data_provider/place_data_provider_test.ts b/src/place_building_blocks/place_data_provider/place_data_provider_test.ts index 4cd27a00..b50e3bdf 100644 --- a/src/place_building_blocks/place_data_provider/place_data_provider_test.ts +++ b/src/place_building_blocks/place_data_provider/place_data_provider_test.ts @@ -63,10 +63,15 @@ describe('PlaceDataProvider', () => { fetchFieldsSpy.and.callFake( async ({fields}: google.maps.places.FetchFieldsRequest) => { if (fields.includes('displayName')) { - place.displayName = 'Fake Place'; + // Properties of Place are getter-only in the typings + Object.defineProperty(place, 'displayName', { + get: () => 'Fake Place', + }); } if (fields.includes('rating')) { - place.rating = 5; + Object.defineProperty(place, 'rating', { + get: () => 5, + }); } return {place}; }); diff --git a/src/place_building_blocks/place_field_boolean/place_field_boolean_test.ts b/src/place_building_blocks/place_field_boolean/place_field_boolean_test.ts index 127ffae1..7e754db6 100644 --- a/src/place_building_blocks/place_field_boolean/place_field_boolean_test.ts +++ b/src/place_building_blocks/place_field_boolean/place_field_boolean_test.ts @@ -12,7 +12,7 @@ import {map} from 'lit/directives/map.js'; import {Environment} from '../../testing/environment.js'; import {makeFakePlace} from '../../testing/fake_place.js'; import {LifecycleSpyController} from '../../testing/lifecycle_spy.js'; -import type {PlaceResult} from '../../utils/googlemaps_types.js'; +import {mapsJsData, type PlaceResult} from '../../utils/googlemaps_types.js'; import {BooleanField, PLACE_BOOLEAN_FIELDS, PlaceFieldBoolean} from './place_field_boolean.js'; @@ -67,8 +67,8 @@ describe('place field boolean test', () => { servesVegetarianFood: true, servesWine: true, - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, - regularOpeningHours: {periods: [], weekdayDescriptions: []}, + businessStatus: 'OPERATIONAL', + regularOpeningHours: mapsJsData({periods: [], weekdayDescriptions: [], specialDays: []}), utcOffsetMinutes: 0, isOpen: async () => true, }); @@ -103,8 +103,8 @@ describe('place field boolean test', () => { servesVegetarianFood: false, servesWine: false, - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, - regularOpeningHours: {periods: [], weekdayDescriptions: []}, + businessStatus: 'OPERATIONAL', + regularOpeningHours: mapsJsData({periods: [], weekdayDescriptions: [], specialDays: []}), utcOffsetMinutes: 0, isOpen: async () => false, }); @@ -209,7 +209,7 @@ describe('place field boolean test', () => { // behavior for `isOpen()`, in combination with PlaceBooleanField's own // field checking. const openPlaceResult: PlaceResult = { - business_status: 'OPERATIONAL' as google.maps.places.BusinessStatus, + business_status: 'OPERATIONAL', utc_offset_minutes: 0, opening_hours: { // This gets discarded and replaced with the fake implementation. @@ -217,8 +217,7 @@ describe('place field boolean test', () => { } }; const closedPlaceResult: PlaceResult = { - business_status: 'CLOSED_TEMPORARILY' as - google.maps.places.BusinessStatus, + business_status: 'CLOSED_TEMPORARILY', utc_offset_minutes: 0, opening_hours: { // This gets discarded and replaced with the fake implementation. @@ -226,7 +225,7 @@ describe('place field boolean test', () => { } }; const undefinedPlaceResult: PlaceResult = { - business_status: 'OPERATIONAL' as google.maps.places.BusinessStatus, + business_status: 'OPERATIONAL', }; const [openEl, closedEl, undefinedEl] = await prepareState(html` @@ -246,13 +245,13 @@ describe('place field boolean test', () => { it('handles the field value opening_hours.isOpen', async () => { const openPlace = makeFakePlace({ id: '1234567890', - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, - regularOpeningHours: {periods: [], weekdayDescriptions: []}, + businessStatus: 'OPERATIONAL', + regularOpeningHours: mapsJsData({periods: [], weekdayDescriptions: [], specialDays: []}), utcOffsetMinutes: 0, isOpen: async () => true, }); const openPlaceResult: PlaceResult = { - business_status: 'OPERATIONAL' as google.maps.places.BusinessStatus, + business_status: 'OPERATIONAL', utc_offset_minutes: 0, opening_hours: { // This gets discarded and replaced with the fake implementation. @@ -277,19 +276,19 @@ describe('place field boolean test', () => { const isOpenSpy = jasmine.createSpy('isOpen'); const noBusinessStatusPlace = makeFakePlace({ id: '1234567890', - regularOpeningHours: {periods: [], weekdayDescriptions: []}, + regularOpeningHours: mapsJsData({periods: [], weekdayDescriptions: [], specialDays: []}), isOpen: isOpenSpy, }); const noOpeningHoursPlace = makeFakePlace({ id: '1234567890', - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, + businessStatus: 'OPERATIONAL', utcOffsetMinutes: 0, isOpen: isOpenSpy, }); const noUtcOffsetPlace = makeFakePlace({ id: '1234567890', - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, - regularOpeningHours: {periods: [], weekdayDescriptions: []}, + businessStatus: 'OPERATIONAL', + regularOpeningHours: mapsJsData({periods: [], weekdayDescriptions: [], specialDays: []}), isOpen: isOpenSpy, }); @@ -310,8 +309,8 @@ describe('place field boolean test', () => { const isOpenSpy = jasmine.createSpy('isOpen').and.resolveTo(true); const place = makeFakePlace({ id: '1234567890', - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, - regularOpeningHours: {periods: [], weekdayDescriptions: []}, + businessStatus: 'OPERATIONAL', + regularOpeningHours: mapsJsData({periods: [], weekdayDescriptions: [], specialDays: []}), utcOffsetMinutes: 0, isOpen: isOpenSpy, }); @@ -350,7 +349,7 @@ describe('place field boolean test', () => { const place = makeFakePlace({ id: '1234567890', businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, - regularOpeningHours: {periods: [], weekdayDescriptions: []}, + regularOpeningHours: mapsJsData({periods: [], weekdayDescriptions: [], specialDays: []}), utcOffsetMinutes: 0, }); const [el] = await prepareState(html` diff --git a/src/place_building_blocks/place_field_text/place_field_text.ts b/src/place_building_blocks/place_field_text/place_field_text.ts index cb2821f2..faae708b 100644 --- a/src/place_building_blocks/place_field_text/place_field_text.ts +++ b/src/place_building_blocks/place_field_text/place_field_text.ts @@ -286,15 +286,16 @@ export class PlaceFieldText extends PlaceDataConsumer { } } - private renderBusinessStatus(status: google.maps.places.BusinessStatus|null| - undefined): string|null|undefined { + private renderBusinessStatus( + status: google.maps.places.BusinessStatusString|null|undefined): string + |null|undefined { if (!status) return status; switch (status) { - case 'CLOSED_PERMANENTLY' as google.maps.places.BusinessStatus: + case 'CLOSED_PERMANENTLY': return this.getMsg('PLACE_CLOSED_PERMANENTLY'); - case 'CLOSED_TEMPORARILY' as google.maps.places.BusinessStatus: + case 'CLOSED_TEMPORARILY': return this.getMsg('PLACE_CLOSED_TEMPORARILY'); - case 'OPERATIONAL' as google.maps.places.BusinessStatus: + case 'OPERATIONAL': return this.getMsg('PLACE_OPERATIONAL'); default: return undefined; diff --git a/src/place_building_blocks/place_field_text/place_field_text_test.ts b/src/place_building_blocks/place_field_text/place_field_text_test.ts index 0b16d1a8..cd2fd1c6 100644 --- a/src/place_building_blocks/place_field_text/place_field_text_test.ts +++ b/src/place_building_blocks/place_field_text/place_field_text_test.ts @@ -12,7 +12,7 @@ import {map} from 'lit/directives/map.js'; import {Environment} from '../../testing/environment.js'; import {FakeLatLng} from '../../testing/fake_lat_lng.js'; import {makeFakePlace} from '../../testing/fake_place.js'; -import type {PlaceResult} from '../../utils/googlemaps_types.js'; +import {mapsJsData, type PlaceResult} from '../../utils/googlemaps_types.js'; import {PLACE_RESULT_TEXT_FIELDS, PLACE_TEXT_FIELDS, PlaceFieldText, TextField} from './place_field_text.js'; @@ -26,24 +26,24 @@ function getText(root: HTMLElement, field?: TextField): string|null|undefined { } const fakePlace = makeFakePlace({ - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, + businessStatus: 'OPERATIONAL', displayName: 'Name', formattedAddress: '123 Main Street', id: '1234567890', internationalPhoneNumber: '+1 234-567-8910', location: new FakeLatLng(1, 2), nationalPhoneNumber: '(234) 567-8910', - plusCode: { + plusCode: mapsJsData({ compoundCode: '1234+AB Some Place', globalCode: 'ABCD1234+AB', - }, + }), rating: 4.5, types: ['restaurant'], userRatingCount: 123, }); const placeResult: PlaceResult = { - business_status: 'OPERATIONAL' as google.maps.places.BusinessStatus, + business_status: 'OPERATIONAL', name: 'Name', formatted_address: '123 Main Street', place_id: '1234567890', @@ -215,15 +215,15 @@ describe('PlaceFieldText', () => { it('renders all business statuses correctly', async () => { const operationalPlace = makeFakePlace({ id: '1234567890', - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus + businessStatus: 'OPERATIONAL' }); const permanentPlace = makeFakePlace({ id: '1234567890', - businessStatus: 'CLOSED_PERMANENTLY' as google.maps.places.BusinessStatus + businessStatus: 'CLOSED_PERMANENTLY' }); const temporaryPlace = makeFakePlace({ id: '1234567890', - businessStatus: 'CLOSED_TEMPORARILY' as google.maps.places.BusinessStatus + businessStatus: 'CLOSED_TEMPORARILY' }); const [operationalField, permanentField, temporaryField] = await prepareState(html` diff --git a/src/place_building_blocks/place_opening_hours/place_opening_hours_test.ts b/src/place_building_blocks/place_opening_hours/place_opening_hours_test.ts index e5ae89d8..9ac1f19c 100644 --- a/src/place_building_blocks/place_opening_hours/place_opening_hours_test.ts +++ b/src/place_building_blocks/place_opening_hours/place_opening_hours_test.ts @@ -11,7 +11,7 @@ import {html} from 'lit'; import {Environment} from '../../testing/environment.js'; import {makeFakePlace} from '../../testing/fake_place.js'; import {LifecycleSpyController} from '../../testing/lifecycle_spy.js'; -import type {Place, PlaceResult} from '../../utils/googlemaps_types.js'; +import {mapsJsData, type Place, type PlaceResult} from '../../utils/googlemaps_types.js'; import {PlaceFieldBoolean} from '../place_field_boolean/place_field_boolean.js'; import {PlaceFieldText} from '../place_field_text/place_field_text.js'; @@ -20,17 +20,17 @@ import {PlaceOpeningHours} from './place_opening_hours.js'; const FAKE_PLACE_PROPS: Pick&Partial = { id: '1234567890', - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, - regularOpeningHours: { + businessStatus: 'OPERATIONAL', + regularOpeningHours: mapsJsData({ periods: [ - { - open: {day: 0, hour: 10, minute: 0}, - close: {day: 0, hour: 20, minute: 0}, - }, - { - open: {day: 6, hour: 10, minute: 0}, - close: {day: 6, hour: 21, minute: 30}, - }, + mapsJsData({ + open: mapsJsData({day: 0, hour: 10, minute: 0}), + close: mapsJsData({day: 0, hour: 20, minute: 0}), + }), + mapsJsData({ + open: mapsJsData({day: 6, hour: 10, minute: 0}), + close: mapsJsData({day: 6, hour: 21, minute: 30}), + }), ], weekdayDescriptions: [ 'Monday: Closed', @@ -41,7 +41,8 @@ const FAKE_PLACE_PROPS: Pick&Partial = { 'Saturday: 10:00 AM - 9:30 PM', 'Sunday: 10:00 AM - 8:00 PM', ], - }, + specialDays: [], + }), utcOffsetMinutes: 0, // Important! Specifies when regularOpeningHours occur. }; @@ -92,7 +93,7 @@ describe('PlaceOpeningHours', () => { it('renders business status when place is temporarily closed', async () => { const place = makeFakePlace({ ...FAKE_PLACE_PROPS, - businessStatus: 'CLOSED_TEMPORARILY' as google.maps.places.BusinessStatus, + businessStatus: 'CLOSED_TEMPORARILY', regularOpeningHours: undefined, }); const el = await prepareState({place}); @@ -143,15 +144,16 @@ describe('PlaceOpeningHours', () => { it('labels place as open 24 hours when close time is null', async () => { const place = makeFakePlace({ ...FAKE_PLACE_PROPS, - regularOpeningHours: { + regularOpeningHours: mapsJsData({ periods: [ - { - open: {day: 0, hour: 0, minute: 0}, + mapsJsData({ + open: mapsJsData({day: 0, hour: 0, minute: 0}), close: null, - }, + }), ], weekdayDescriptions: [], - }, + specialDays: [], + }), }); const el = await prepareState({place}); @@ -161,10 +163,11 @@ describe('PlaceOpeningHours', () => { it('omits closing time when data is insufficient', async () => { const place = makeFakePlace({ ...FAKE_PLACE_PROPS, - regularOpeningHours: { + regularOpeningHours: mapsJsData({ periods: [], weekdayDescriptions: [], - }, + specialDays: [], + }), }); const el = await prepareState({place}); diff --git a/src/place_building_blocks/place_photo_gallery/place_photo_gallery_test.ts b/src/place_building_blocks/place_photo_gallery/place_photo_gallery_test.ts index 3a7603cb..075a0050 100644 --- a/src/place_building_blocks/place_photo_gallery/place_photo_gallery_test.ts +++ b/src/place_building_blocks/place_photo_gallery/place_photo_gallery_test.ts @@ -12,7 +12,7 @@ import {ifDefined} from 'lit/directives/if-defined.js'; import {Environment} from '../../testing/environment.js'; import {makeFakePhoto, makeFakePlace, makeFakePlacePhoto} from '../../testing/fake_place.js'; import {getDeepActiveElement} from '../../utils/deep_element_access.js'; -import type {Place, PlaceResult} from '../../utils/googlemaps_types.js'; +import {mapsJsData, type Place, type PlaceResult} from '../../utils/googlemaps_types.js'; import {PlacePhotoGallery} from './place_photo_gallery.js'; @@ -23,32 +23,36 @@ const fakePlace = makeFakePlace({ makeFakePhoto( { authorAttributions: [ - { + mapsJsData({ displayName: 'Author A1', photoURI: '', uri: 'https://www.google.com/maps/contrib/A1', - }, - { + }), + mapsJsData({ displayName: 'Author A2', photoURI: '', uri: '', - }, + }), ], heightPx: 1000, widthPx: 2000, + googleMapsURI: null, + flagContentURI: null, }, 'https://lh3.googleusercontent.com/places/A'), makeFakePhoto( { authorAttributions: [ - { + mapsJsData({ displayName: 'Author B1', photoURI: '', uri: 'https://www.google.com/maps/contrib/B1', - }, + }), ], heightPx: 2000, widthPx: 1000, + googleMapsURI: null, + flagContentURI: null, }, 'https://lh3.googleusercontent.com/places/B'), makeFakePhoto( @@ -56,6 +60,8 @@ const fakePlace = makeFakePlace({ authorAttributions: [], heightPx: 1340, widthPx: 1420, + googleMapsURI: null, + flagContentURI: null, }, 'https://lh3.googleusercontent.com/places/C'), ], diff --git a/src/place_building_blocks/place_reviews/place_reviews_test.ts b/src/place_building_blocks/place_reviews/place_reviews_test.ts index 2a08119d..bb2cdefb 100644 --- a/src/place_building_blocks/place_reviews/place_reviews_test.ts +++ b/src/place_building_blocks/place_reviews/place_reviews_test.ts @@ -11,6 +11,7 @@ import {map} from 'lit/directives/map.js'; import {Environment} from '../../testing/environment.js'; import {makeFakePlace, SAMPLE_FAKE_PLACE} from '../../testing/fake_place.js'; +import {mapsJsData} from '../../utils/googlemaps_types.js'; import {PlaceReviews} from './place_reviews.js'; @@ -63,18 +64,24 @@ describe('PlaceReviews', () => { it('renders the right URIs', async () => { const place = makeFakePlace({ id: '', - reviews: [{ - authorAttribution: { + reviews: [mapsJsData({ + authorAttribution: mapsJsData({ displayName: 'Author', photoURI: 'https://lh3.googlusercontent.com/a/1', uri: 'https://www.google.com/maps/contrib/1/reviews', - }, + }), publishTime: new Date(1234567890), rating: 5, relativePublishTimeDescription: '1 month ago', text: '', textLanguageCode: 'en', - }], + flagContentURI: null, + googleMapsURI: null, + originalText: null, + originalTextLanguageCode: null, + visitDateMonth: null, + visitDateYear: null, + })], }); const [reviews] = await prepareState(html` diff --git a/src/store_locator/distances.ts b/src/store_locator/distances.ts index d9a8b08a..69d256a1 100644 --- a/src/store_locator/distances.ts +++ b/src/store_locator/distances.ts @@ -19,9 +19,8 @@ function makeDistanceMatrixRequestCache() { // Requests with a transient error of OVER_QUERY_LIMIT // and UNKNOWN_ERROR should be retried. See full list of statuses // https://developers.google.com/maps/documentation/javascript/reference/distance-matrix#DistanceMatrixStatus - return error.code === - 'OVER_QUERY_LIMIT' as google.maps.DistanceMatrixStatus || - error.code === 'UNKNOWN_ERROR' as google.maps.DistanceMatrixStatus; + return error.code === 'OVER_QUERY_LIMIT' || + error.code === 'UNKNOWN_ERROR'; }); } @@ -93,7 +92,7 @@ export class DistanceMeasurer { const request: google.maps.DistanceMatrixRequest = { origins: [origin], destinations: destinationsForLookup, - travelMode: 'DRIVING' as google.maps.TravelMode, + travelMode: 'DRIVING', unitSystem: units, }; let responsePromise = DistanceMeasurer.cache.get(request); diff --git a/src/store_locator/store_locator_test.ts b/src/store_locator/store_locator_test.ts index 0691f69a..b19af2c2 100644 --- a/src/store_locator/store_locator_test.ts +++ b/src/store_locator/store_locator_test.ts @@ -16,7 +16,7 @@ import {makeFakeDistanceMatrixResponse} from '../testing/fake_distance_matrix.js import type {FakeMapElement} from '../testing/fake_gmp_components.js'; import {FakeLatLng, FakeLatLngBounds} from '../testing/fake_lat_lng.js'; import {Deferred} from '../utils/deferred.js'; -import type {LatLngLiteral, Place} from '../utils/googlemaps_types.js'; +import {mapsJsData, type LatLngLiteral, type Place} from '../utils/googlemaps_types.js'; import {DistanceMeasurer} from './distances.js'; import {FeatureSet, StoreLocatorListing} from './interfaces.js'; @@ -179,11 +179,11 @@ describe('StoreLocator', () => { spyOnProperty(placePicker!, 'value').and.returnValue({ id: 'foo_origin_id', location: origin, - addressComponents: [{ + addressComponents: [mapsJsData({ types: ['foo', 'country'], shortText: 'US', longText: 'United States' - }], + })], } as Partialas Place); // Distance Matrix will set Location B as closer @@ -222,8 +222,8 @@ describe('StoreLocator', () => { spyOnProperty(placePicker!, 'value').and.returnValue({ id: 'foo_origin_id', location: new FakeLatLng(10, 10), - addressComponents: - [{types: ['foo', 'country'], shortText: 'CA', longText: 'Canada'}], + addressComponents: [mapsJsData( + {types: ['foo', 'country'], shortText: 'CA', longText: 'Canada'})], } as Partialas Place); // Distance Matrix will set Location B as closer diff --git a/src/testing/fake_gmp_components.ts b/src/testing/fake_gmp_components.ts index f49e22d8..9457fcbe 100644 --- a/src/testing/fake_gmp_components.ts +++ b/src/testing/fake_gmp_components.ts @@ -31,13 +31,16 @@ declare global { /** A fake `google.maps.MapElement` class for testing purposes. */ export class FakeMapElement extends LitElement { - center: LatLng|LatLngLiteral|null = null; - readonly innerMap = jasmine.createSpyObj( 'Map', ['fitBounds', 'panTo', 'setOptions']); + center: LatLng|LatLngLiteral|null = null; mapId: string|null = null; zoom: number|null = null; + headingInteractionDisabled: boolean|null = null; + internalUsageAttributionIds: Iterable|null = null; + renderingType: google.maps.RenderingTypeString|null = null; + tiltInteractionDisabled: boolean|null = null; } /** @@ -58,4 +61,15 @@ export class FakeAdvancedMarkerElement extends LitElement { get content(): Element|null|undefined { return this.innerContent; } + + get element(): HTMLElement { + return this; + } + + addListener(eventName: string, handler: Function): + google.maps.MapsEventListener { + return { + remove: () => {}, + }; + } } diff --git a/src/testing/fake_place.ts b/src/testing/fake_place.ts index af241cb4..49bc8e0f 100644 --- a/src/testing/fake_place.ts +++ b/src/testing/fake_place.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {Photo, Place} from '../utils/googlemaps_types.js'; +import {mapsJsData, type Photo, type Place} from '../utils/googlemaps_types.js'; import {FakeLatLng} from './fake_lat_lng.js'; @@ -16,10 +16,9 @@ type PlacePhoto = google.maps.places.PlacePhoto; * loading the API. It is *not* recognized as an `instanceof` the `Place` * constructor loaded with the API. * - * @param fields - An object of fields of the `Place`. The `id` field is - * required and the rest are optional. + * @param fields - An object of fields of the `Place`. */ -export function makeFakePlace(fields: Pick&Partial): Place { +export function makeFakePlace(fields: Partial): Place { return { // Fake version of isOpen() simply checks whether the business is // operational. @@ -45,8 +44,8 @@ export function makeFakePlace(fields: Pick&Partial): Place { * @param uri - The URI to return when `getURI()` is called. */ export function makeFakePhoto( - fields: Omit, uri: string): Photo { - return {getURI: () => uri, ...fields}; + fields: Omit, uri: string): Photo { + return mapsJsData({getURI: () => uri, ...fields}); } /** @@ -63,15 +62,19 @@ export function makeFakePlacePhoto( /** A sample `google.maps.places.Place` object for testing purposes. */ export const SAMPLE_FAKE_PLACE = makeFakePlace({ addressComponents: [ - {longText: '123', shortText: '123', types: ['street_number']}, - {longText: 'Main Street', shortText: 'Main St', types: ['route']}, + mapsJsData({longText: '123', shortText: '123', types: ['street_number']}), + mapsJsData( + {longText: 'Main Street', shortText: 'Main St', types: ['route']}), ], adrFormatAddress: '123 Main Street', attributions: [ - {provider: 'Provider 1', providerURI: 'https://www.someprovider.com/1'}, - {provider: 'Provider 2', providerURI: null}, + mapsJsData({ + provider: 'Provider 1', + providerURI: 'https://www.someprovider.com/1' + }), + mapsJsData({provider: 'Provider 2', providerURI: null}), ], - businessStatus: 'OPERATIONAL' as google.maps.places.BusinessStatus, + businessStatus: 'OPERATIONAL', displayName: 'Place Name', googleMapsURI: 'https://maps.google.com/', formattedAddress: '123 Main Street', @@ -80,16 +83,16 @@ export const SAMPLE_FAKE_PLACE = makeFakePlace({ internationalPhoneNumber: '+1 234-567-8910', location: new FakeLatLng(1, 2), nationalPhoneNumber: '(234) 567-8910', - regularOpeningHours: { + regularOpeningHours: mapsJsData({ periods: [ - { - close: {day: 0, hour: 18, minute: 0}, - open: {day: 0, hour: 11, minute: 0}, - }, - { - close: {day: 6, hour: 18, minute: 0}, - open: {day: 6, hour: 12, minute: 30}, - }, + mapsJsData({ + close: mapsJsData({day: 0, hour: 18, minute: 0}), + open: mapsJsData({day: 0, hour: 11, minute: 0}), + }), + mapsJsData({ + close: mapsJsData({day: 6, hour: 18, minute: 0}), + open: mapsJsData({day: 6, hour: 12, minute: 30}), + }), ], weekdayDescriptions: [ 'Monday: Closed', @@ -100,90 +103,116 @@ export const SAMPLE_FAKE_PLACE = makeFakePlace({ 'Saturday: 11:00 AM - 6:00 PM', 'Sunday: 12:30 PM - 6:00 PM', ], - }, - photos: [ - makeFakePhoto( - { - authorAttributions: [ + specialDays: [], + }), + photos: + [ + makeFakePhoto( { - displayName: 'Author A1', - photoURI: '', - uri: 'https://www.google.com/maps/contrib/A1', + authorAttributions: [ + mapsJsData({ + displayName: 'Author A1', + photoURI: '', + uri: 'https://www.google.com/maps/contrib/A1', + }), + mapsJsData({ + displayName: 'Author A2', + photoURI: '', + uri: '', + }), + ], + heightPx: 1000, + widthPx: 2000, + googleMapsURI: null, + flagContentURI: null, }, + 'https://lh3.googlusercontent.com/places/A'), + makeFakePhoto( { - displayName: 'Author A2', - photoURI: '', - uri: '', + authorAttributions: [ + mapsJsData({ + displayName: 'Author B1', + photoURI: '', + uri: 'https://www.google.com/maps/contrib/B1', + }), + ], + heightPx: 1000, + widthPx: 2000, + googleMapsURI: null, + flagContentURI: null, }, - ], - heightPx: 1000, - widthPx: 2000, - }, - 'https://lh3.googlusercontent.com/places/A'), - makeFakePhoto( - { - authorAttributions: [ + 'https://lh3.googlusercontent.com/places/B'), + makeFakePhoto( { - displayName: 'Author B1', - photoURI: '', - uri: 'https://www.google.com/maps/contrib/B1', + authorAttributions: [], + heightPx: 1000, + widthPx: 2000, + googleMapsURI: null, + flagContentURI: null, }, - ], - heightPx: 1000, - widthPx: 2000, - }, - 'https://lh3.googlusercontent.com/places/B'), - makeFakePhoto( - { - authorAttributions: [], - heightPx: 1000, - widthPx: 2000, - }, - 'https://lh3.googlusercontent.com/places/C'), - ], - plusCode: { + 'https://lh3.googlusercontent.com/places/C'), + ], + plusCode: mapsJsData({ compoundCode: '1234+AB Some Place', globalCode: 'ABCD1234+AB', - }, - priceLevel: 'INEXPENSIVE' as google.maps.places.PriceLevel, + }), + priceLevel: 'INEXPENSIVE', rating: 4.5, reviews: [ - { - authorAttribution: { + mapsJsData({ + authorAttribution: mapsJsData({ displayName: 'Author 1', uri: 'https://www.google.com/maps/contrib/1/reviews', photoURI: 'https://lh3.googlusercontent.com/a/1', - }, + }), publishTime: new Date(1234567890), rating: 5, relativePublishTimeDescription: '1 month ago', text: 'it\'s lit!', textLanguageCode: 'en', - }, - { - authorAttribution: { + flagContentURI: null, + googleMapsURI: null, + originalText: null, + originalTextLanguageCode: null, + visitDateMonth: null, + visitDateYear: null, + }), + mapsJsData({ + authorAttribution: mapsJsData({ displayName: 'Author 2', uri: 'https://www.google.com/maps/contrib/2/reviews', photoURI: 'https://lh3.googlusercontent.com/a/2', - }, + }), publishTime: new Date(1234567890), rating: null, relativePublishTimeDescription: '2 months ago', text: '¡Que bacano!', textLanguageCode: 'es', - }, - { - authorAttribution: { + flagContentURI: null, + googleMapsURI: null, + originalText: null, + originalTextLanguageCode: null, + visitDateMonth: null, + visitDateYear: null, + }), + mapsJsData({ + authorAttribution: mapsJsData({ displayName: 'Author 3', uri: '', photoURI: 'https://lh3.googlusercontent.com/a/3', - }, + }), publishTime: new Date(1234567890), rating: 4, relativePublishTimeDescription: '3 months ago', text: '', textLanguageCode: 'en', - }, + flagContentURI: null, + googleMapsURI: null, + originalText: null, + originalTextLanguageCode: null, + visitDateMonth: null, + visitDateYear: null, + }), ], svgIconMaskURI: 'https://maps.gstatic.com/mapfiles/mask.png', types: ['restaurant', 'food', 'establishment'], @@ -199,7 +228,7 @@ export const SAMPLE_FAKE_PLACE_RESULT: google.maps.places.PlaceResult = { {long_name: 'Main Street', short_name: 'Main St', types: ['route']}, ], adr_address: '123 Main Street', - business_status: 'OPERATIONAL' as google.maps.places.BusinessStatus, + business_status: 'OPERATIONAL', formatted_address: '123 Main Street', formatted_phone_number: '(234) 567-8910', geometry: { diff --git a/src/testing/fake_route.ts b/src/testing/fake_route.ts index f4806a58..4360013e 100644 --- a/src/testing/fake_route.ts +++ b/src/testing/fake_route.ts @@ -41,7 +41,7 @@ const EMPTY_FAKE_STEP: DirectionsStep = { maneuver: '', start_location: new FakeLatLng(0, 0), start_point: new FakeLatLng(0, 0), - travel_mode: 'DRIVING' as google.maps.TravelMode, + travel_mode: 'DRIVING', }; /** diff --git a/src/utils/googlemaps_types.ts b/src/utils/googlemaps_types.ts index 061788a9..6dc6d033 100644 --- a/src/utils/googlemaps_types.ts +++ b/src/utils/googlemaps_types.ts @@ -43,8 +43,8 @@ export type MapElement = google.maps.MapElement; /** google.maps.places.PlaceResult */ export type PlaceResult = google.maps.places.PlaceResult; -/** google.maps.places.PriceLevel */ -export type PriceLevel = google.maps.places.PriceLevel; +/** google.maps.places.PriceLevelString */ +export type PriceLevelString = google.maps.places.PriceLevelString; /** HTML tag names for Maps JS web components. */ export interface HTMLElementTagNameMap { @@ -52,89 +52,22 @@ export interface HTMLElementTagNameMap { 'gmp-advanced-marker': AdvancedMarkerElement; } -/** google.maps.addressValidation.ComponentName */ -export declare interface ComponentName { - text: string; - languageCode: string; -} - -/** google.maps.addressValidation.ConfirmationLevel */ -export enum ConfirmationLevel { - CONFIRMATION_LEVEL_UNSPECIFIED = 'CONFIRMATION_LEVEL_UNSPECIFIED', - CONFIRMED = 'CONFIRMED', - UNCONFIRMED_BUT_PLAUSIBLE = 'UNCONFIRMED_BUT_PLAUSIBLE', - UNCONFIRMED_AND_SUSPICIOUS = 'UNCONFIRMED_AND_SUSPICIOUS' -} - /** google.maps.addressValidation.AddressComponent */ -export declare interface AddressComponent { - componentName: ComponentName; - componentType: string; - confirmationLevel: ConfirmationLevel|null; - isInferred: boolean; - isSpellCorrected: boolean; - isReplaced: boolean; - isUnexpected: boolean; -} - -/** google.maps.addressValidation.PostalAddress */ -export declare interface PostalAddress { - revision?: number; - regionCode?: string; - languageCode?: string; - postalCode?: string; - sortingCode?: string; - administrativeArea?: string; - locality?: string; - sublocality?: string; - addressLines?: string[]; - recipients?: string[]; - organization?: string; -} +export type AddressComponent = google.maps.addressValidation.AddressComponent; /** google.maps.addressValidation.Address */ -export declare interface Address { - formattedAddress: string|null; - postalAddress: PostalAddress|null; - components: AddressComponent[]; - missingComponentTypes?: string[]; - unconfirmedComponentTypes?: string[]; - unresolvedTokens?: string[]; -} - -/** google.maps.addressValidation.Granularity */ -export enum Granularity { - GRANULARITY_UNSPECIFIED = 'GRANULARITY_UNSPECIFIED', - SUB_PREMISE = 'SUB_PREMISE', - PREMISE = 'PREMISE', - PREMISE_PROXIMITY = 'PREMISE_PROXIMITY', - BLOCK = 'BLOCK', - ROUTE = 'ROUTE', - OTHER = 'OTHER' -} +export type Address = google.maps.addressValidation.Address; /** google.maps.addressValidation.Verdict */ -export declare interface Verdict { - inputGranularity: Granularity|null; - validationGranularity: Granularity|null; - geocodeGranularity: Granularity|null; - isAddressComplete: boolean; - hasUnconfirmedComponents: boolean; - hasInferredComponents: boolean; - hasReplacedComponents: boolean; -} +export type Verdict = google.maps.addressValidation.Verdict; /** google.maps.addressValidation.AddressValidation */ -export declare interface AddressValidation { - responseId: string|null; - verdict: Verdict|null; - address: Address|null; - - // These properties exist but are not needed for the ECL. - // geocode: Geocode; - // metadata: AddressMetadata; - // uspsData: UspsData; - - // This property is not yet published. - // englishLatinAddress: Address; +export type AddressValidation = google.maps.addressValidation.AddressValidation; + +/** + * Adds a toJSON() method to a plain object, making it assignable to the data + * types in the Google Maps JS API typings (google.maps.places.Photo, etc.) + */ +export function mapsJsData(data: T): T&{toJSON(): T} { + return {...data, toJSON: () => data}; } diff --git a/src/utils/opening_hours_test.ts b/src/utils/opening_hours_test.ts index 4597c2ad..9920ecd7 100644 --- a/src/utils/opening_hours_test.ts +++ b/src/utils/opening_hours_test.ts @@ -7,16 +7,18 @@ // import 'jasmine'; (google3-only) import {makeFakePlace} from '../testing/fake_place.js'; +import {mapsJsData} from './googlemaps_types.js'; import {formatTimeWithWeekdayMaybe, getUpcomingCloseTime, getUpcomingOpenTime, isOpen, isSoon, NextCloseTimeStatus, NextOpenTimeStatus} from './opening_hours.js'; type OpeningHours = google.maps.places.OpeningHours; type OpeningHoursPeriod = google.maps.places.OpeningHoursPeriod; -const ALWAYS_OPEN_HOURS: OpeningHours = { +const ALWAYS_OPEN_HOURS: OpeningHours = mapsJsData({ periods: [makePeriod(0, 0)], - weekdayDescriptions: [] -}; + weekdayDescriptions: [], + specialDays: [], +}); const SF_OFFSET = -7 * 60; const NYC_OFFSET = -4 * 60; @@ -33,12 +35,12 @@ const SAT = 6; function makePeriod( startDay: number, startHour: number, endDay?: number, endHour?: number): OpeningHoursPeriod { - const open = {day: startDay, hour: startHour, minute: 0}; + const open = mapsJsData({day: startDay, hour: startHour, minute: 0}); if ((endDay == null) || (endHour == null)) { - return {open, close: null}; + return mapsJsData({open, close: null}); } - const close = {day: endDay, hour: endHour, minute: 0}; - return {open, close}; + const close = mapsJsData({day: endDay, hour: endHour, minute: 0}); + return mapsJsData({open, close}); } function makeDateInLocale( @@ -56,7 +58,7 @@ describe('Opening hours utilities', () => { describe('formatTimeWithWeekdayMaybe', () => { it('formats a relative time with a weekday when the absolute point is over 24 hours in the future', () => { - const point = {day: SAT, hour: 17, minute: 0}; // Saturday, 5:00 PM + const point = mapsJsData({day: SAT, hour: 17, minute: 0}); // Saturday, 5:00 PM const pointDate = new Date( '2023-04-08T17:00-04:00'); // Saturday, 5:00 PM Eastern Time @@ -69,7 +71,7 @@ describe('Opening hours utilities', () => { it('formats a relative time without a weekday when the absolute point is less than 24 hours in the future', () => { - const point = {day: SAT, hour: 17, minute: 0}; // Saturday, 5:00 PM + const point = mapsJsData({day: SAT, hour: 17, minute: 0}); // Saturday, 5:00 PM const pointDate = new Date( '2023-04-08T17:00-04:00'); // Saturday, 5:00 PM Eastern Time @@ -126,8 +128,8 @@ describe('Opening hours utilities', () => { const monday8AmSf = makeDateInLocale('2023-04-10T08:00', SF_OFFSET); const place = makeFakePlace({ id: '123', - regularOpeningHours: - {periods: [mondayNineToFive], weekdayDescriptions: []}, + regularOpeningHours: mapsJsData( + {periods: [mondayNineToFive], weekdayDescriptions: [], specialDays: []}), utcOffsetMinutes: SF_OFFSET }); @@ -142,8 +144,8 @@ describe('Opening hours utilities', () => { const monday4PmSf = makeDateInLocale('2023-04-10T16:00', SF_OFFSET); const place = makeFakePlace({ id: '123', - regularOpeningHours: - {periods: [mondayNineToFive], weekdayDescriptions: []}, + regularOpeningHours: mapsJsData( + {periods: [mondayNineToFive], weekdayDescriptions: [], specialDays: []}), utcOffsetMinutes: NYC_OFFSET }); @@ -154,14 +156,15 @@ describe('Opening hours utilities', () => { it('returns a closing time for status for a Place closing soon', () => { const thursday3PmSf = makeDateInLocale('2023-04-13T15:00', SF_OFFSET); - const regularOpeningHours: OpeningHours = { + const regularOpeningHours: OpeningHours = mapsJsData({ periods: [ makePeriod(WED, 9, WED, 17), // Wed 9am - 5pm makePeriod(THU, 9, THU, 17), // Thurs 9am - 5pm makePeriod(FRI, 9, SAT, 2), // Friday 9am - Sat 2am ], - weekdayDescriptions: [] - }; + weekdayDescriptions: [], + specialDays: [], + }); const place = makeFakePlace( {id: '123', regularOpeningHours, utcOffsetMinutes: SF_OFFSET}); @@ -175,12 +178,13 @@ describe('Opening hours utilities', () => { it('returns a closing time for a Place closing in over 24 hours from now', () => { const monday1pmSf = makeDateInLocale('2023-04-10T13:00', SF_OFFSET); - const regularOpeningHours: OpeningHours = { + const regularOpeningHours: OpeningHours = mapsJsData({ periods: [ makePeriod(MON, 9, FRI, 17), // Mon 9am - Fri 5pm ], - weekdayDescriptions: [] - }; + weekdayDescriptions: [], + specialDays: [], + }); const place = makeFakePlace( {id: '123', regularOpeningHours, utcOffsetMinutes: SF_OFFSET}); @@ -195,14 +199,15 @@ describe('Opening hours utilities', () => { it('handles a period which wraps the week', () => { // Sequence is (week start) -> (now) -> (period end) -> (period start) const monday8amSf = makeDateInLocale('2023-04-10T08:00', SF_OFFSET); - const regularOpeningHours: OpeningHours = { + const regularOpeningHours: OpeningHours = mapsJsData({ periods: [ makePeriod(WED, 9, WED, 17), // Wed 9am - 5pm makePeriod(THU, 9, THU, 17), // Thurs 9am - 5pm makePeriod(FRI, 18, MON, 9), // Friday 6pm - Mon 9am ], - weekdayDescriptions: [] - }; + weekdayDescriptions: [], + specialDays: [], + }); const place = makeFakePlace( {id: '123', regularOpeningHours, utcOffsetMinutes: SF_OFFSET}); @@ -216,14 +221,15 @@ describe('Opening hours utilities', () => { it('handles a period which wraps the week in the other direction', () => { // Sequence is (week start) -> (period end) -> (period start) -> (now) const saturday11pmSf = makeDateInLocale('2023-04-15T23:00', SF_OFFSET); - const regularOpeningHours: OpeningHours = { + const regularOpeningHours: OpeningHours = mapsJsData({ periods: [ makePeriod(WED, 9, WED, 17), // Wed 9am - 5pm makePeriod(THU, 9, THU, 17), // Thurs 9am - 5pm makePeriod(FRI, 18, SUN, 9), // Friday 6pm - Sun 9am ], - weekdayDescriptions: [] - }; + weekdayDescriptions: [], + specialDays: [], + }); const place = makeFakePlace( {id: '123', regularOpeningHours, utcOffsetMinutes: SF_OFFSET}); @@ -239,10 +245,11 @@ describe('Opening hours utilities', () => { it('returns a status if there is not enough information', () => { const place = makeFakePlace({ id: '123', - regularOpeningHours: { + regularOpeningHours: mapsJsData({ periods: [], weekdayDescriptions: [], - }, + specialDays: [], + }), utcOffsetMinutes: undefined }); @@ -254,10 +261,11 @@ describe('Opening hours utilities', () => { it('returns a status if the place is never open', () => { const place = makeFakePlace({ id: '123', - regularOpeningHours: { + regularOpeningHours: mapsJsData({ periods: [], weekdayDescriptions: [], - }, + specialDays: [], + }), utcOffsetMinutes: 0 }); @@ -270,10 +278,11 @@ describe('Opening hours utilities', () => { const tuesdayNoonSf = makeDateInLocale('2023-04-11T12:00', SF_OFFSET); const place = makeFakePlace({ id: '123', - regularOpeningHours: { + regularOpeningHours: mapsJsData({ periods: [makePeriod(TUE, 9, TUE, 17)], // Tuesday 9am - 5pm weekdayDescriptions: [], - }, + specialDays: [], + }), utcOffsetMinutes: SF_OFFSET }); @@ -284,14 +293,15 @@ describe('Opening hours utilities', () => { it('returns the next time the place will open', () => { const thursday6PmSf = makeDateInLocale('2023-04-13T18:00', SF_OFFSET); - const regularOpeningHours: OpeningHours = { + const regularOpeningHours: OpeningHours = mapsJsData({ periods: [ makePeriod(WED, 9, WED, 17), // Wed 9am - 5pm makePeriod(THU, 9, THU, 17), // Thurs 9am - 5pm makePeriod(FRI, 9, SAT, 2), // Friday 9am - Sat 2am ], - weekdayDescriptions: [] - }; + weekdayDescriptions: [], + specialDays: [], + }); const place = makeFakePlace( {id: '123', regularOpeningHours, utcOffsetMinutes: SF_OFFSET}); @@ -309,14 +319,15 @@ describe('Opening hours utilities', () => { // // Sequence is (now) -> (week start) -> (period start) const saturdayNoonSf = makeDateInLocale('2023-04-15T12:00', SF_OFFSET); - const regularOpeningHours: OpeningHours = { + const regularOpeningHours: OpeningHours = mapsJsData({ periods: [ makePeriod(WED, 9, WED, 17), // Wed 9am - 5pm makePeriod(THU, 9, THU, 17), // Thurs 9am - 5pm makePeriod(FRI, 9, SAT, 2), // Friday 9am - Sat 2am ], - weekdayDescriptions: [] - }; + weekdayDescriptions: [], + specialDays: [], + }); const place = makeFakePlace( {id: '123', regularOpeningHours, utcOffsetMinutes: SF_OFFSET}); @@ -345,10 +356,11 @@ describe('Opening hours utilities', () => { it('returns true if the place is open now', () => { const mondayNoonSf = makeDateInLocale('2023-08-07T12:00', SF_OFFSET); - const regularOpeningHours: OpeningHours = { + const regularOpeningHours: OpeningHours = mapsJsData({ periods: [makePeriod(MON, 9, MON, 17)], // Wed 9am - 5pm - weekdayDescriptions: [] - }; + weekdayDescriptions: [], + specialDays: [], + }); const place = makeFakePlace( {id: '123', regularOpeningHours, utcOffsetMinutes: SF_OFFSET}); @@ -357,10 +369,11 @@ describe('Opening hours utilities', () => { it('returns false if the place is not open now', () => { const mondayEarlySf = makeDateInLocale('2023-08-07T06:00', SF_OFFSET); - const regularOpeningHours: OpeningHours = { + const regularOpeningHours: OpeningHours = mapsJsData({ periods: [makePeriod(MON, 9, MON, 17)], // Wed 9am - 5pm - weekdayDescriptions: [] - }; + weekdayDescriptions: [], + specialDays: [], + }); const place = makeFakePlace( {id: '123', regularOpeningHours, utcOffsetMinutes: SF_OFFSET}); diff --git a/src/utils/place_utils.ts b/src/utils/place_utils.ts index 24cfaa24..3b95f96c 100644 --- a/src/utils/place_utils.ts +++ b/src/utils/place_utils.ts @@ -9,7 +9,8 @@ import {html} from 'lit'; import {APILoader} from '../api_loader/api_loader.js'; import {extractTextAndURL} from './dom_utils.js'; -import type {LatLng, LatLngLiteral, Place, PlaceResult, PriceLevel} from './googlemaps_types.js'; +import {mapsJsData} from './googlemaps_types.js'; +import type {LatLng, LatLngLiteral, Place, PlaceResult, PriceLevelString} from './googlemaps_types.js'; import {isOpen} from './opening_hours.js'; /** @@ -58,15 +59,16 @@ function isLatLng(data: LatLng|LatLngLiteral): data is LatLng { return typeof data.lat === 'function'; } -const PRICE_LEVEL_CONVERSIONS: Record = Object.freeze({ - 'FREE': 0, - 'INEXPENSIVE': 1, - 'MODERATE': 2, - 'EXPENSIVE': 3, - 'VERY_EXPENSIVE': 4, -}); +const PRICE_LEVEL_CONVERSIONS: Record = + Object.freeze({ + 'FREE': 0, + 'INEXPENSIVE': 1, + 'MODERATE': 2, + 'EXPENSIVE': 3, + 'VERY_EXPENSIVE': 4, + }); -const REVERSE_PRICE_LEVEL_CONVERSIONS: Record = +const REVERSE_PRICE_LEVEL_CONVERSIONS: Record = Object.freeze(Object.fromEntries( Object.entries(PRICE_LEVEL_CONVERSIONS).map(tup => tup.reverse()))); @@ -74,7 +76,8 @@ const REVERSE_PRICE_LEVEL_CONVERSIONS: Record = * Converts an enum price level to the corresponding numeric value. If passed a * numeric value, it will return it unchanged. */ -export function priceLevelToNumeric(level: number|PriceLevel): number|null { +export function priceLevelToNumeric(level: number|PriceLevelString): number| + null { if (typeof level === 'number') return level; return PRICE_LEVEL_CONVERSIONS[level] ?? null; } @@ -83,7 +86,8 @@ export function priceLevelToNumeric(level: number|PriceLevel): number|null { * Converts a numeric price level to the corresponding enum value. If passed an * enum value, it will return it unchanged. */ -export function numericToPriceLevel(level: number|PriceLevel): PriceLevel|null { +export function numericToPriceLevel(level: number|PriceLevelString): + PriceLevelString|null { if (typeof level !== 'number') return level; return REVERSE_PRICE_LEVEL_CONVERSIONS[level] ?? null; } @@ -176,124 +180,119 @@ export function hasDataForOpeningCalculations(place: Place): boolean { /** Converts `PlaceResult` data to `Place`-compatible field values. */ function convertToPlaceFields(placeResult: PlaceResult): Partial { - const place: Partial = {}; - if (placeResult.address_components !== undefined) { - place.addressComponents = placeResult.address_components.map( - (component: google.maps.GeocoderAddressComponent) => ({ - longText: component.long_name, - shortText: component.short_name, - types: component.types, - })); - } - if (placeResult.adr_address !== undefined) { - place.adrFormatAddress = placeResult.adr_address; - } - if (placeResult.business_status !== undefined) { - place.businessStatus = placeResult.business_status; - } - if (placeResult.formatted_address !== undefined) { - place.formattedAddress = placeResult.formatted_address; - } - if (placeResult.formatted_phone_number !== undefined) { - place.nationalPhoneNumber = placeResult.formatted_phone_number; - } - if (placeResult.geometry !== undefined) { - const geometry = placeResult.geometry; - if (geometry.location) place.location = geometry.location; - if (geometry.viewport) place.viewport = geometry.viewport; - } - if (placeResult.html_attributions !== undefined) { - place.attributions = placeResult.html_attributions.map((html: string) => { - const {text, url} = extractTextAndURL(html); - return {provider: text ?? '', providerURI: url ?? null}; - }); - } - if (placeResult.icon_background_color !== undefined) { - place.iconBackgroundColor = placeResult.icon_background_color; - } - if (placeResult.icon_mask_base_uri !== undefined) { - place.svgIconMaskURI = placeResult.icon_mask_base_uri; - } - if (placeResult.international_phone_number !== undefined) { - place.internationalPhoneNumber = placeResult.international_phone_number; - } - if (placeResult.name !== undefined) { - place.displayName = placeResult.name; - } - if (placeResult.opening_hours !== undefined) { - const periods = placeResult.opening_hours.periods?.map( - (period: google.maps.places.PlaceOpeningHoursPeriod) => ({ - open: makeOpeningHoursPoint(period.open), - // A place that is open 24/7 does not return a close period. - close: period.close ? makeOpeningHoursPoint(period.close) : null, - })); - place.regularOpeningHours = { - periods: periods ?? [], - weekdayDescriptions: placeResult.opening_hours.weekday_text ?? [], - }; - } - if (placeResult.photos !== undefined) { - place.photos = - placeResult.photos.map((photo: google.maps.places.PlacePhoto) => { - const attributions = photo.html_attributions.map((html) => { - const {text, url} = extractTextAndURL(html); - return {displayName: text ?? '', photoURI: '', uri: url || ''}; + return { + ...(placeResult.address_components !== undefined && { + addressComponents: placeResult.address_components.map( + (component: google.maps.GeocoderAddressComponent) => mapsJsData({ + longText: component.long_name, + shortText: component.short_name, + types: component.types, + })) + }), + ...(placeResult.adr_address !== undefined && + {adrFormatAddress: placeResult.adr_address}), + ...(placeResult.business_status !== undefined && + {businessStatus: placeResult.business_status}), + ...(placeResult.formatted_address !== undefined && + {formattedAddress: placeResult.formatted_address}), + ...(placeResult.formatted_phone_number !== undefined && + {nationalPhoneNumber: placeResult.formatted_phone_number}), + ...(placeResult.geometry !== undefined && { + location: placeResult.geometry.location, + viewport: placeResult.geometry.viewport, + }), + ...(placeResult.html_attributions !== undefined && { + attributions: placeResult.html_attributions.map((html: string) => { + const {text, url} = extractTextAndURL(html); + return mapsJsData({ + provider: text ?? '', + providerURI: url ?? null, + }); + }) + }), + ...(placeResult.icon_background_color !== undefined && + {iconBackgroundColor: placeResult.icon_background_color}), + ...(placeResult.icon_mask_base_uri !== undefined && + {svgIconMaskURI: placeResult.icon_mask_base_uri}), + ...(placeResult.international_phone_number !== undefined && + {internationalPhoneNumber: placeResult.international_phone_number}), + ...(placeResult.name !== undefined && {displayName: placeResult.name}), + ...(placeResult.opening_hours !== undefined && { + regularOpeningHours: mapsJsData({ + periods: + placeResult.opening_hours.periods?.map( + (period: + google.maps.places.PlaceOpeningHoursPeriod) => mapsJsData({ + open: makeOpeningHoursPoint(period.open), + // A place that is open 24/7 does not return a close period. + close: period.close ? makeOpeningHoursPoint(period.close) : + null, + })) ?? + [], + weekdayDescriptions: placeResult.opening_hours.weekday_text ?? [], + specialDays: [], + }) + }), + ...(placeResult.photos !== undefined && { + photos: placeResult.photos.map((photo: google.maps.places.PlacePhoto) => { + const attributions = photo.html_attributions.map((html) => { + const {text, url} = extractTextAndURL(html); + return mapsJsData({ + displayName: text ?? '', + photoURI: '', + uri: url || '', }); - return { - authorAttributions: attributions, - getURI: photo.getUrl, - heightPx: photo.height, - widthPx: photo.width, - }; }); - } - if (placeResult.place_id !== undefined) { - place.id = placeResult.place_id; - } - if (placeResult.plus_code !== undefined) { - place.plusCode = { - compoundCode: placeResult.plus_code.compound_code ?? null, - globalCode: placeResult.plus_code.global_code, - }; - } - if (placeResult.price_level !== undefined) { - place.priceLevel = numericToPriceLevel(placeResult.price_level); - } - if (placeResult.rating !== undefined) { - place.rating = placeResult.rating; - } - if (placeResult.reviews !== undefined) { - place.reviews = placeResult.reviews.map( - (review: google.maps.places.PlaceReview) => ({ - authorAttribution: { - displayName: review.author_name, - photoURI: review.profile_photo_url, - uri: review.author_url || '', - }, - // Convert publish time from milliseconds to a Date object. - publishTime: new Date(review.time), - rating: review.rating ?? null, - relativePublishTimeDescription: review.relative_time_description, - text: review.text, - textLanguageCode: review.language, - })); - } - if (placeResult.types !== undefined) { - place.types = placeResult.types; - } - if (placeResult.url !== undefined) { - place.googleMapsURI = placeResult.url; - } - if (placeResult.user_ratings_total !== undefined) { - place.userRatingCount = placeResult.user_ratings_total; - } - if (placeResult.utc_offset_minutes !== undefined) { - place.utcOffsetMinutes = placeResult.utc_offset_minutes; - } - if (placeResult.website !== undefined) { - place.websiteURI = placeResult.website; - } - return place; + return mapsJsData({ + authorAttributions: attributions, + getURI: photo.getUrl, + heightPx: photo.height, + widthPx: photo.width, + flagContentURI: null, + googleMapsURI: null, + }); + }) + }), + ...(placeResult.place_id !== undefined && {id: placeResult.place_id}), + ...(placeResult.plus_code !== undefined && { + plusCode: mapsJsData({ + compoundCode: placeResult.plus_code.compound_code ?? null, + globalCode: placeResult.plus_code.global_code, + }) + }), + ...(placeResult.price_level !== undefined && + {priceLevel: numericToPriceLevel(placeResult.price_level)}), + ...(placeResult.rating !== undefined && {rating: placeResult.rating}), + ...(placeResult.reviews !== undefined && { + reviews: placeResult.reviews.map( + (review: google.maps.places.PlaceReview) => mapsJsData({ + authorAttribution: mapsJsData({ + displayName: review.author_name, + photoURI: review.profile_photo_url, + uri: review.author_url || '', + }), + // Convert publish time from milliseconds to a Date object. + publishTime: new Date(review.time), + rating: review.rating ?? null, + relativePublishTimeDescription: review.relative_time_description, + text: review.text, + textLanguageCode: review.language, + flagContentURI: null, + googleMapsURI: null, + originalText: null, + originalTextLanguageCode: null, + visitDateMonth: null, + visitDateYear: null, + })) + }), + ...(placeResult.types !== undefined && {types: placeResult.types}), + ...(placeResult.url !== undefined && {googleMapsURI: placeResult.url}), + ...(placeResult.user_ratings_total !== undefined && + {userRatingCount: placeResult.user_ratings_total}), + ...(placeResult.utc_offset_minutes !== undefined && + {utcOffsetMinutes: placeResult.utc_offset_minutes}), + ...(placeResult.website !== undefined && {websiteURI: placeResult.website}), + }; } /** @@ -303,7 +302,7 @@ function convertToPlaceFields(placeResult: PlaceResult): Partial { function makeOpeningHoursPoint( {day, hours, minutes}: google.maps.places.PlaceOpeningHoursTime): google.maps.places.OpeningHoursPoint { - return {day, hour: hours, minute: minutes}; + return mapsJsData({day, hour: hours, minute: minutes}); } const PLACE_TO_PLACE_RESULT_FIELDS: @@ -369,4 +368,4 @@ async function fetchFromPlaceDetails( } }); }); -} \ No newline at end of file +} diff --git a/src/utils/place_utils_test.ts b/src/utils/place_utils_test.ts index 4f00f317..8d97900f 100644 --- a/src/utils/place_utils_test.ts +++ b/src/utils/place_utils_test.ts @@ -15,7 +15,13 @@ import {makeFakePlace, SAMPLE_FAKE_PLACE, SAMPLE_FAKE_PLACE_RESULT} from '../tes import type {Place, PlaceResult} from './googlemaps_types.js'; import {isPlaceResult, makePlaceFromPlaceResult, makeWaypoint, numericToPriceLevel, priceLevelToNumeric, renderAttribution} from './place_utils.js'; -type PriceLevel = google.maps.places.PriceLevel; + +/** + * Recursively removes toJSON() methods from Maps JS API-style data objects. + */ +function stripToJSON(obj: unknown): unknown { + return obj ? JSON.parse(JSON.stringify(obj)) : obj; +} describe('isPlaceResult', () => { it('says that the empty object is a PlaceResult', () => { @@ -47,11 +53,11 @@ describe('isPlaceResult', () => { describe('priceLevelToNumeric', () => { it('converts all price levels', () => { - expect(priceLevelToNumeric('FREE' as PriceLevel)).toEqual(0); - expect(priceLevelToNumeric('INEXPENSIVE' as PriceLevel)).toEqual(1); - expect(priceLevelToNumeric('MODERATE' as PriceLevel)).toEqual(2); - expect(priceLevelToNumeric('EXPENSIVE' as PriceLevel)).toEqual(3); - expect(priceLevelToNumeric('VERY_EXPENSIVE' as PriceLevel)).toEqual(4); + expect(priceLevelToNumeric('FREE')).toEqual(0); + expect(priceLevelToNumeric('INEXPENSIVE')).toEqual(1); + expect(priceLevelToNumeric('MODERATE')).toEqual(2); + expect(priceLevelToNumeric('EXPENSIVE')).toEqual(3); + expect(priceLevelToNumeric('VERY_EXPENSIVE')).toEqual(4); }); it('returns numbers unchanged', () => { @@ -59,22 +65,22 @@ describe('priceLevelToNumeric', () => { }); it('returns null on invalid values', () => { - expect(priceLevelToNumeric('INVALID' as PriceLevel)).toEqual(null); + expect(priceLevelToNumeric('INVALID' as any)).toEqual(null); }); }); describe('numericToPriceLevel', () => { it('converts all valid numbers', () => { - expect(numericToPriceLevel(0)).toEqual('FREE' as PriceLevel); - expect(numericToPriceLevel(1)).toEqual('INEXPENSIVE' as PriceLevel); - expect(numericToPriceLevel(2)).toEqual('MODERATE' as PriceLevel); - expect(numericToPriceLevel(3)).toEqual('EXPENSIVE' as PriceLevel); - expect(numericToPriceLevel(4)).toEqual('VERY_EXPENSIVE' as PriceLevel); + expect(numericToPriceLevel(0)).toEqual('FREE'); + expect(numericToPriceLevel(1)).toEqual('INEXPENSIVE'); + expect(numericToPriceLevel(2)).toEqual('MODERATE'); + expect(numericToPriceLevel(3)).toEqual('EXPENSIVE'); + expect(numericToPriceLevel(4)).toEqual('VERY_EXPENSIVE'); }); it('returns enum values unchanged', () => { - expect(numericToPriceLevel('FREE' as PriceLevel)) - .toEqual('FREE' as PriceLevel); + expect(numericToPriceLevel('FREE')) + .toEqual('FREE'); }); it('returns null on invalid numbers', () => { @@ -164,10 +170,13 @@ describe('makePlaceFromPlaceResult', () => { it('copies all equivalent fields from PlaceResult to Place', async () => { const place = await makePlaceFromPlaceResult(SAMPLE_FAKE_PLACE_RESULT); - expect(place.addressComponents) - .toEqual(SAMPLE_FAKE_PLACE.addressComponents); + // Since distinct toJSON() function instances are considered unequal, they + // need to be stripped before comparison. + expect(stripToJSON(place.addressComponents)) + .toEqual(stripToJSON(SAMPLE_FAKE_PLACE.addressComponents)); expect(place.adrFormatAddress).toEqual(SAMPLE_FAKE_PLACE.adrFormatAddress); - expect(place.attributions).toEqual(SAMPLE_FAKE_PLACE.attributions); + expect(stripToJSON(place.attributions)) + .toEqual(stripToJSON(SAMPLE_FAKE_PLACE.attributions)); expect(place.businessStatus).toEqual(SAMPLE_FAKE_PLACE.businessStatus); expect(place.displayName).toEqual(SAMPLE_FAKE_PLACE.displayName); expect(place.googleMapsURI).toEqual(SAMPLE_FAKE_PLACE.googleMapsURI); @@ -177,10 +186,12 @@ describe('makePlaceFromPlaceResult', () => { expect(place.id).toEqual(SAMPLE_FAKE_PLACE.id); expect(place.internationalPhoneNumber) .toEqual(SAMPLE_FAKE_PLACE.internationalPhoneNumber); - expect(place.location).toEqual(SAMPLE_FAKE_PLACE.location); + expect(stripToJSON(place.location)) + .toEqual(stripToJSON(SAMPLE_FAKE_PLACE.location)); expect(place.nationalPhoneNumber) .toEqual(SAMPLE_FAKE_PLACE.nationalPhoneNumber); - expect(place.plusCode).toEqual(SAMPLE_FAKE_PLACE.plusCode); + expect(stripToJSON(place.plusCode)) + .toEqual(stripToJSON(SAMPLE_FAKE_PLACE.plusCode)); expect(place.priceLevel).toEqual(SAMPLE_FAKE_PLACE.priceLevel); expect(place.rating).toEqual(SAMPLE_FAKE_PLACE.rating); expect(place.svgIconMaskURI).toEqual(SAMPLE_FAKE_PLACE.svgIconMaskURI); @@ -192,16 +203,16 @@ describe('makePlaceFromPlaceResult', () => { expect(place.reviews!.length).toEqual(SAMPLE_FAKE_PLACE.reviews!.length); SAMPLE_FAKE_PLACE.reviews!.forEach((expectedReview, i) => { const review = place.reviews![i]; - expect(review.authorAttribution) - .toEqual(expectedReview.authorAttribution); - expect(review).toEqual(expectedReview); + expect(stripToJSON(review.authorAttribution)) + .toEqual(stripToJSON(expectedReview.authorAttribution)); + expect(stripToJSON(review)).toEqual(stripToJSON(expectedReview)); }); expect(place.photos!.length).toEqual(SAMPLE_FAKE_PLACE.photos!.length); SAMPLE_FAKE_PLACE.photos!.forEach((expectedPhoto, i) => { const photo = place.photos![i]; - expect(photo.authorAttributions) - .toEqual(expectedPhoto.authorAttributions); + expect(stripToJSON(photo.authorAttributions)) + .toEqual(stripToJSON(expectedPhoto.authorAttributions)); expect(photo.heightPx).toEqual(expectedPhoto.heightPx); expect(photo.widthPx).toEqual(expectedPhoto.widthPx); expect(photo.getURI()).toEqual(expectedPhoto.getURI());