diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b0ce0b5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + - name: Install dependencies + run: npm install + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + - name: Run tests + run: npm test diff --git a/.gitignore b/.gitignore index 0f98073..874a122 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,9 @@ node_modules # macOS .DS_Store + +# Test artifacts +test-results/ +playwright-report/ +*.png +.last-run.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..c50a885 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +test/ +.github/ +playwright-report/ diff --git a/bower.json b/bower.json index c89cf08..4b01506 100644 --- a/bower.json +++ b/bower.json @@ -2,7 +2,7 @@ "name": "googlemaps-js-api-stub", "description": "Google Maps Javascript API Stub", "main": "stub.js", - "version": "3.61.0", + "version": "3.64.0", "authors": [ "CodeMangler" ], @@ -27,6 +27,9 @@ "node_modules", "bower_components", "test", - "tests" + "tests", + "package.json", + "package-lock.json", + ".github" ] } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..eefc269 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,94 @@ +{ + "name": "googlemaps-js-api-stub", + "version": "3.64.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "googlemaps-js-api-stub", + "version": "3.64.0", + "license": "ISC", + "devDependencies": { + "@playwright/browser-chromium": "^1.59.1", + "@playwright/test": "^1.59.1" + } + }, + "node_modules/@playwright/browser-chromium": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.59.1.tgz", + "integrity": "sha512-XDwr0qOrzLXAuBAzg4WO/xctVMb+ldJ54yz9KCpFu8G8MVJzUVFO6BvK1tBtBl4DIoFcoFRKHgUGZT+8wOC8BQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json index e2d12aa..bc226f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "googlemaps-js-api-stub", - "version": "3.63.0", + "version": "3.64.0", "description": "Google Maps Javascript API Stub", "main": "stub.js", "repository": { @@ -18,7 +18,6 @@ "mock", "double", "fake", - "fake", "spy" ], "author": "CodeMangler", @@ -26,5 +25,12 @@ "bugs": { "url": "https://github.com/CodeMangler/googlemaps-js-api-stub/issues" }, - "homepage": "https://github.com/CodeMangler/googlemaps-js-api-stub#readme" + "homepage": "https://github.com/CodeMangler/googlemaps-js-api-stub#readme", + "devDependencies": { + "@playwright/browser-chromium": "^1.59.1", + "@playwright/test": "^1.59.1" + }, + "scripts": { + "test": "node test/verify_stub.js && npx playwright test test/verify_frontend.spec.js" + } } diff --git a/stub.js b/stub.js index 8ddf195..cd405ba 100644 --- a/stub.js +++ b/stub.js @@ -5,10 +5,28 @@ var fnEmptyObject = function() { return EMPTY_OBJECT; }; -window.google = { +var google = { maps: { __gjsload__: noop, - importLibrary: function() { return Promise.resolve(); }, + importLibrary: function(libraryName) { + switch (libraryName) { + case 'maps': + case 'core': + case 'marker': + case 'geometry': + case 'drawing': + case 'visualization': + case 'journeySharing': + case 'places': + case 'maps3d': + case 'routes': + case 'addressValidation': + case 'airQuality': + return Promise.resolve(google.maps[libraryName] || google.maps); + default: + return Promise.resolve(google.maps); + } + }, event: { addListener: noop, clearInstanceListeners: noop, @@ -18,6 +36,50 @@ window.google = { trigger: noop, }, places: { + Place: (function() { + var Place = function(options) { + this.id = (options && options.id) || null; + this.displayName = ''; + this.formattedAddress = ''; + this.location = null; + this.accessibilityOptions = null; + this.addressComponents = EMPTY_ARRAY; + this.adrFormatAddress = ''; + this.allowsDogs = null; + this.attributions = EMPTY_ARRAY; + this.businessStatus = null; + this.editorialSummary = null; + this.evChargeOptions = null; + this.fuelOptions = null; + this.googleMapsURI = ''; + this.iconBackgroundColor = ''; + this.internationalPhoneNumber = ''; + this.nationalPhoneNumber = ''; + this.openingHours = null; + this.regularOpeningHours = null; + this.parkingOptions = null; + this.paymentOptions = null; + this.photos = EMPTY_ARRAY; + this.plusCode = null; + this.priceLevel = null; + this.rating = null; + this.reviews = EMPTY_ARRAY; + this.servesBeer = null; + this.types = EMPTY_ARRAY; + this.userRatingCount = null; + this.utcOffsetMinutes = null; + this.viewport = null; + this.websiteURI = ''; + this.fetchFields = function(request) { return Promise.resolve(this); }; + this.getNextOpeningTime = function(){ return Promise.resolve(null); }; + this.isOpen = function(date){ return Promise.resolve(false); }; + this.toJSON = function() { return { id: this.id }; }; + return this; + }; + Place.searchByText = function(request) { return Promise.resolve({places: EMPTY_ARRAY}); }; + Place.searchNearby = function(request) { return Promise.resolve({places: EMPTY_ARRAY}); }; + return Place; + })(), AutocompleteService: function() { return { getPlacePredictions: function(request, callback) { @@ -56,7 +118,22 @@ window.google = { }; }, PlaceAutocompleteElement: function(options) { return { addEventListener: noop, getPlace: fnEmptyObject, setLocationBias: noop, setLocationRestriction: noop, setTypes: noop }; }, + BasicPlaceAutocompleteElement: function(options) { return { addEventListener: noop }; }, PlaceDetailsElement: function(options) { return { addEventListener: noop, place: null }; }, + PlaceDetailsCompactElement: function(options) { return { addEventListener: noop, place: null }; }, + PlaceDetailsOrientation: { HORIZONTAL: 'HORIZONTAL', VERTICAL: 'VERTICAL' }, + PlaceDetailsPlaceRequestElement: function(options) { return { addEventListener: noop }; }, + PlaceDetailsLocationRequestElement: function(options) { return { addEventListener: noop }; }, + PlaceSearchElement: function(options) { return { addEventListener: noop, places: EMPTY_ARRAY }; }, + PlaceSearchAttributionPosition: { BOTTOM: 'BOTTOM', TOP: 'TOP' }, + PlaceSearchOrientation: { HORIZONTAL: 'HORIZONTAL', VERTICAL: 'VERTICAL' }, + PlaceNearbySearchRequestElement: function(options) { return { addEventListener: noop }; }, + PlaceTextSearchRequestElement: function(options) { return { addEventListener: noop }; }, + PlacePredictionSelectEvent: function() { return { placePrediction: EMPTY_OBJECT }; }, + PlaceSelectEvent: function() { return { place: EMPTY_OBJECT }; }, + PlaceContextualElement: function(options) { return { addEventListener: noop }; }, + PlaceContextualListConfigElement: function(options) { return { addEventListener: noop }; }, + PlaceContextualListLayout: { COMPACT: 'COMPACT', VERTICAL: 'VERTICAL' }, PlaceDirectionsButton: function(options) { return { addEventListener: noop, map: null, place: null, travelMode: null }; }, PlaceMediaElement: function(options) { return { addEventListener: noop, photos: EMPTY_ARRAY }; }, PlaceReviewsElement: function(options) { return { addEventListener: noop, reviews: EMPTY_ARRAY }; }, @@ -342,60 +419,179 @@ window.google = { IMPERIAL: 1 }, ZoomControlStyle: EMPTY_OBJECT, - Settings: function() {}, + Settings: { + getInstance: fnEmptyObject, + experienceIds: EMPTY_ARRAY, + fetchAppCheckToken: noop + }, marker: { AdvancedMarkerElement: function(options) { return { - addListener: noop, - get gmpDraggable() { return false; }, - set gmpDraggable(value) {}, + collisionBehavior: (options && options.collisionBehavior) || null, + gmpClickable: (options && options.gmpClickable) || null, + gmpDraggable: (options && options.gmpDraggable) || false, + map: (options && options.map) || null, + position: (options && options.position) || null, + title: (options && options.title) || '', + zIndex: (options && options.zIndex) || null, + addListener: noop }; }, PinElement: function(options) { - return {}; + return { + background: (options && options.background) || null, + borderColor: (options && options.borderColor) || null, + element: (options && options.element) || null, + glyph: (options && options.glyph) || null, + glyphColor: (options && options.glyphColor) || null, + scale: (options && options.scale) || 1, + addEventListener: noop, + removeEventListener: noop + }; }, + CollisionBehavior: { + OPTIONAL_AND_HIDES_LOWER_PRIORITY: 'OPTIONAL_AND_HIDES_LOWER_PRIORITY', + REQUIRED: 'REQUIRED', + REQUIRED_AND_HIDES_OPTIONAL: 'REQUIRED_AND_HIDES_OPTIONAL' + } + }, + Place: (function() { + var Place = function(options) { + console.warn('google.maps.Place is deprecated. Use google.maps.places.Place instead.'); + return new google.maps.places.Place(options); + }; + Place.searchByText = function(request) { return google.maps.places.Place.searchByText(request); }; + Place.searchNearby = function(request) { return google.maps.places.Place.searchNearby(request); }; + return Place; + })(), + drawing: { + DrawingManager: function(options) { return { getDrawingMode:function(){return null;}, getMap:fnEmptyObject, setDrawingMode:noop, setMap:noop, setOptions:noop, addListener:noop }; }, + OverlayType: { MARKER: 'marker', POLYGON: 'polygon', POLYLINE: 'polyline', RECTANGLE: 'rectangle', CIRCLE: 'circle' } + }, + visualization: { + HeatmapLayer: function(options) { return { getData:function(){return new google.maps.MVCArray();}, getMap:fnEmptyObject, setData:noop, setMap:noop, setOptions:noop }; } }, - Place: function(options) { - console.warn('google.maps.Place is deprecated. Use google.maps.places.Place instead.'); - this.id = (options && options.id) || null; - this.displayName = ''; - this.formattedAddress = ''; - this.location = null; - this.accessibilityOptions = null; this.addressComponents = EMPTY_ARRAY; this.adrFormatAddress = ''; this.allowsDogs = null; this.attributions = EMPTY_ARRAY; this.businessStatus = null; this.editorialSummary = null; this.evChargeOptions = null; this.fuelOptions = null; this.googleMapsURI = ''; this.iconBackgroundColor = ''; this.internationalPhoneNumber = ''; this.nationalPhoneNumber = ''; this.openingHours = null; this.regularOpeningHours = null; this.parkingOptions = null; this.paymentOptions = null; this.photos = EMPTY_ARRAY; this.plusCode = null; this.priceLevel = null; this.rating = null; this.reviews = EMPTY_ARRAY; this.servesBeer = null; this.types = EMPTY_ARRAY; this.userRatingCount = null; this.utcOffsetMinutes = null; this.viewport = null; this.websiteURI = ''; - return this; + journeySharing: { + JourneySharingMapView: function(options) { return { automaticViewportMode: null, element: null, enableTraffic: false, locationProviders: EMPTY_ARRAY, map: null, mapOptions: null, addLocationProvider: noop, removeLocationProvider: noop, addListener: noop }; }, + FleetEngineServiceType: { DELIVERY_VEHICLE_SERVICE: 'DELIVERY_VEHICLE_SERVICE', TASK_SERVICE: 'TASK_SERVICE', TRIP_SERVICE: 'TRIP_SERVICE', UNKNOWN_SERVICE: 'UNKNOWN_SERVICE' }, + AutomaticViewportMode: { FIT_ANTICIPATED_ROUTE: 'FIT_ANTICIPATED_ROUTE', NONE: 'NONE'}, + vehicle: {}, + TripType: { SHARED: 'SHARED', EXCLUSIVE: 'EXCLUSIVE', UNKNOWN_TRIP_TYPE: 'UNKNOWN_TRIP_TYPE' } + }, + maps3d: { + MarkerElement: function(options) { return { addEventListener: noop, removeEventListener: noop, position: (options && options.position) || null }; }, + MarkerInteractiveElement: function(options) { return { addEventListener: noop, removeEventListener: noop, position: (options && options.position) || null }; }, + Marker3DElement: function(options) { return { addEventListener: noop, removeEventListener: noop, position: (options && options.position) || null }; }, + Marker3DInteractiveElement: function(options) { return { addEventListener: noop, removeEventListener: noop, position: (options && options.position) || null }; }, + Model3DElement: function(options) { return { addEventListener: noop, removeEventListener: noop, position: (options && options.position) || null }; }, + Model3DInteractiveElement: function(options) { return { addEventListener: noop, removeEventListener: noop, position: (options && options.position) || null }; }, + Polyline3DElement: function(options) { return { addEventListener: noop, removeEventListener: noop, path: (options && options.path) || EMPTY_ARRAY }; }, + Polyline3DInteractiveElement: function(options) { return { addEventListener: noop, removeEventListener: noop, path: (options && options.path) || EMPTY_ARRAY }; }, + Polygon3DElement: function(options) { return { addEventListener: noop, removeEventListener: noop, paths: (options && options.paths) || EMPTY_ARRAY }; }, + Polygon3DInteractiveElement: function(options) { return { addEventListener: noop, removeEventListener: noop, paths: (options && options.paths) || EMPTY_ARRAY }; }, + FlattenerElement: function(options) { return { addEventListener: noop, removeEventListener: noop }; }, + PopoverElement: function(options) { return { addEventListener: noop, removeEventListener: noop, position: (options && options.position) || null }; }, + AltitudeMode: { ABSOLUTE: 'ABSOLUTE', CLAMP_TO_GROUND: 'CLAMP_TO_GROUND', RELATIVE_TO_GROUND: 'RELATIVE_TO_GROUND', RELATIVE_TO_MESH: 'RELATIVE_TO_MESH' }, + CirclePathElement: function(options) { return { addEventListener: noop, removeEventListener: noop, center: (options && options.center) || null, radius: (options && options.radius) || 0 }; }, + Map3DElement: function(options) { return { addEventListener:noop, center:(options && options.center) || null, zoom:(options && options.zoom) || null, heading:(options && options.heading) || 0, tilt:(options && options.tilt) || 0, roll:(options && options.roll) || 0, flyTo:noop, flyCameraTo:noop, stopCameraAnimation:noop }; }, + MapMode: { HYBRID: 'HYBRID', SATELLITE: 'SATELLITE'} + }, + routes: { + Route: function() { return { toJSON: fnEmptyObject, create3DPolylines: function(options) { return Promise.resolve(EMPTY_ARRAY); }, createPolylines: function(options) { return EMPTY_ARRAY; }, createPopover: function() { return Promise.resolve(EMPTY_OBJECT); }, createWaypointAdvancedMarkers: function(options) { return Promise.resolve(EMPTY_ARRAY); } }; }, + RouteLeg: function() { return { toJSON: fnEmptyObject }; }, + RouteLegStep: function() { return { toJSON: fnEmptyObject }; }, + TransitLine: function() { return { toJSON: fnEmptyObject }; }, + TransitAgency: function() { return { toJSON: fnEmptyObject }; }, + TransitStop: function() { return { toJSON: fnEmptyObject }; }, + TransitVehicle: function() { return { toJSON: fnEmptyObject }; }, + DirectionalLocation: function() { return { toJSON: fnEmptyObject }; }, + FallbackInfo: function() { return { toJSON: fnEmptyObject }; }, + GeocodedWaypoint: function() { return { toJSON: fnEmptyObject }; }, + GeocodingResults: function() { return { toJSON: fnEmptyObject }; }, + MultiModalSegment: function() { return { toJSON: fnEmptyObject }; }, + PolylineDetailInfo: function() { return { toJSON: fnEmptyObject }; }, + PolylineDetails: function() { return { toJSON: fnEmptyObject }; }, + RouteLegLocalizedValues: function() { return { toJSON: fnEmptyObject }; }, + RouteLegStepLocalizedValues: function() { return { toJSON: fnEmptyObject }; }, + RouteLegTravelAdvisory: function() { return { toJSON: fnEmptyObject }; }, + RouteLocalizedValues: function() { return { toJSON: fnEmptyObject }; }, + RouteTravelAdvisory: function() { return { toJSON: fnEmptyObject }; }, + SpeedReadingInterval: function() { return { toJSON: fnEmptyObject }; }, + StepsOverview: function() { return { toJSON: fnEmptyObject }; }, + TollInfo: function() { return { toJSON: fnEmptyObject }; }, + ComputeRoutesExtraComputation: { FLYOVER_INFO_ON_POLYLINE: 'FLYOVER_INFO_ON_POLYLINE', FUEL_CONSUMPTION: 'FUEL_CONSUMPTION', HTML_FORMATTED_NAVIGATION_INSTRUCTIONS: 'HTML_FORMATTED_NAVIGATION_INSTRUCTIONS', NARROW_ROAD_INFO_ON_POLYLINE: 'NARROW_ROAD_INFO_ON_POLYLINE', TOLLS: 'TOLLS', TRAFFIC_ON_POLYLINE: 'TRAFFIC_ON_POLYLINE' }, + FallbackReason: { LATENCY_EXCEEDED: 'LATENCY_EXCEEDED', SERVER_ERROR: 'SERVER_ERROR' }, + FallbackRoutingMode: { TRAFFIC_AWARE: 'TRAFFIC_AWARE', TRAFFIC_UNAWARE: 'TRAFFIC_UNAWARE' }, + PolylineQuality: { HIGH_QUALITY: 'HIGH_QUALITY', OVERVIEW: 'OVERVIEW' }, + ReferenceRoute: { FUEL_EFFICIENT: 'FUEL_EFFICIENT', SHORTER_DISTANCE: 'SHORTER_DISTANCE' }, + RoadFeatureState: { DOES_NOT_EXIST: 'DOES_NOT_EXIST', EXISTS: 'EXISTS' }, + RouteLabel: { DEFAULT_ROUTE: 'DEFAULT_ROUTE', DEFAULT_ROUTE_ALTERNATE: 'DEFAULT_ROUTE_ALTERNATE', FUEL_EFFICIENT: 'FUEL_EFFICIENT', SHORTER_DISTANCE: 'SHORTER_DISTANCE' }, + RoutingPreference: { TRAFFIC_AWARE: 'TRAFFIC_AWARE', TRAFFIC_AWARE_OPTIMAL: 'TRAFFIC_AWARE_OPTIMAL', TRAFFIC_UNAWARE: 'TRAFFIC_UNAWARE' }, + Speed: { NORMAL: 'NORMAL', SLOW: 'SLOW', TRAFFIC_JAM: 'TRAFFIC_JAM' }, + VehicleEmissionType: { DIESEL: 'DIESEL', ELECTRIC: 'ELECTRIC', GASOLINE: 'GASOLINE', HYBRID: 'HYBRID' } + }, + addressValidation: { + fetchAddressValidation: function(request) { return Promise.resolve(EMPTY_OBJECT); }, + Address: function() { return { toJSON: fnEmptyObject }; }, + AddressComponent: function() { return { toJSON: fnEmptyObject }; }, + AddressMetadata: function() { return { toJSON: fnEmptyObject }; }, + ConfirmationLevel: { CONFIRMED: 'CONFIRMED', UNCONFIRMED_AND_SUSPICIOUS: 'UNCONFIRMED_AND_SUSPICIOUS', UNCONFIRMED_BUT_PLAUSIBLE: 'UNCONFIRMED_BUT_PLAUSIBLE' }, + Geocode: function() { return { fetchPlace: noop, toJSON: fnEmptyObject }; }, + Granularity: { BLOCK: 'BLOCK', OTHER: 'OTHER', PREMISE: 'PREMISE', PREMISE_PROXIMITY: 'PREMISE_PROXIMITY', ROUTE: 'ROUTE', SUB_PREMISE: 'SUB_PREMISE' }, + PossibleNextAction: { ACCEPT: 'ACCEPT', CONFIRM: 'CONFIRM', CONFIRM_ADD_SUBPREMISES: 'CONFIRM_ADD_SUBPREMISES', FIX: 'FIX' }, + USPSAddress: function() { return { toJSON: fnEmptyObject }; }, + USPSData: function() { return { toJSON: fnEmptyObject }; }, + Verdict: function() { return { toJSON: fnEmptyObject }; } + }, + airQuality: { + AirQualityMeterElement: function(options) { return { addEventListener: noop, removeEventListener: noop }; } + }, + geometry: { + encoding: { decodePath: function(encodedPath) { return []; }, encodePath: function(path) { return ''; } }, + spherical: { + computeArea: function(path, radius) { return 0; }, + computeDistanceBetween: function(from, to, radius) { return 0; }, + computeHeading: function(from, to) { return 0; }, + computeLength: function(path, radius) { return 0; }, + computeOffset: function(from, distance, heading, radius) { return null; }, + computeOffsetOrigin: function(to, distance, heading, radius) { return null; }, + computeSignedArea: function(loop, radius) { return 0; }, + interpolate: function(from, to, fraction) { return null; }, + traversePath: function(path, fraction) { return { lat: 0, lng: 0 }; } + }, + poly: { containsLocation: function(latLng, polygon) { return false; }, isLocationOnEdge: function(latLng, poly, tolerance) { return false; } } }, - drawing: {}, - visualization: {}, - journeySharing: {}, - maps3d: {}, - geometry: {}, ColorScheme: { LIGHT: 'LIGHT', DARK: 'DARK', FOLLOW_SYSTEM: 'FOLLOW_SYSTEM' }, RenderingType: { RASTER: 'RASTER', VECTOR: 'VECTOR', UNINITIALIZED: 'UNINITIALIZED' }, FeatureType: { ADMINISTRATIVE_AREA_LEVEL_1: 'ADMINISTRATIVE_AREA_LEVEL_1', ADMINISTRATIVE_AREA_LEVEL_2: 'ADMINISTRATIVE_AREA_LEVEL_2', COUNTRY: 'COUNTRY', DATASET: 'DATASET', LOCALITY: 'LOCALITY', POSTAL_CODE: 'POSTAL_CODE', SCHOOL_DISTRICT: 'SCHOOL_DISTRICT' }, - MapsNetworkErrorEndpoint: { DIRECTIONS_ROUTE: 'DIRECTIONS_ROUTE', DISTANCE_MATRIX: 'DISTANCE_MATRIX', ELEVATION_ALONG_PATH: 'ELEVATION_ALONG_PATH', ELEVATION_LOCATIONS: 'ELEVATION_LOCATIONS', FLEET_ENGINE_GET_DELIVERY_VEHICLE: 'FLEET_ENGINE_GET_DELIVERY_VEHICLE', FLEET_ENGINE_GET_TRIP: 'FLEET_ENGINE_GET_TRIP', FLEET_ENGINE_GET_VEHICLE: 'FLEET_ENGINE_GET_VEHICLE', FLEET_ENGINE_LIST_DELIVERY_VEHICLES: 'FLEET_ENGINE_LIST_DELIVERY_VEHICLES', FLEET_ENGINE_LIST_TASKS: 'FLEET_ENGINE_LIST_TASKS', FLEET_ENGINE_LIST_VEHICLES: 'FLEET_ENGINE_LIST_VEHICLES', FLEET_ENGINE_SEARCH_TASKS: 'FLEET_ENGINE_SEARCH_TASKS', GEOCODER_GEOCODE: 'GEOCODER_GEOCODE', MAPS_MAX_ZOOM: 'MAPS_MAX_ZOOM', PLACES_AUTOCOMPLETE: 'PLACES_AUTOCOMPLETE', PLACES_DETAILS: 'PLACES_DETAILS', PLACES_FIND_PLACE_FROM_PHONE_NUMBER: 'PLACES_FIND_PLACE_FROM_PHONE_NUMBER', PLACES_FIND_PLACE_FROM_QUERY: 'PLACES_FIND_PLACE_FROM_QUERY', PLACES_GATEWAY: 'PLACES_GATEWAY', PLACES_GET_PLACE: 'PLACES_GET_PLACE', PLACES_LOCAL_CONTEXT_SEARCH: 'PLACES_LOCAL_CONTEXT_SEARCH', PLACES_NEARBY_SEARCH: 'PLACES_NEARBY_SEARCH', PLACES_SEARCH_TEXT: 'PLACES_SEARCH_TEXT', STREETVIEW_GET_PANORAMA: 'STREETVIEW_GET_PANORAMA' }, + MapsNetworkErrorEndpoint: { + DIRECTIONS_ROUTE: 'DIRECTIONS_ROUTE', DISTANCE_MATRIX: 'DISTANCE_MATRIX', ELEVATION_ALONG_PATH: 'ELEVATION_ALONG_PATH', ELEVATION_LOCATIONS: 'ELEVATION_LOCATIONS', + FLEET_ENGINE_GET_DELIVERY_VEHICLE: 'FLEET_ENGINE_GET_DELIVERY_VEHICLE', FLEET_ENGINE_GET_TRIP: 'FLEET_ENGINE_GET_TRIP', FLEET_ENGINE_GET_VEHICLE: 'FLEET_ENGINE_GET_VEHICLE', + FLEET_ENGINE_LIST_DELIVERY_VEHICLES: 'FLEET_ENGINE_LIST_DELIVERY_VEHICLES', FLEET_ENGINE_LIST_TASKS: 'FLEET_ENGINE_LIST_TASKS', FLEET_ENGINE_LIST_VEHICLES: 'FLEET_ENGINE_LIST_VEHICLES', + FLEET_ENGINE_SEARCH_TASKS: 'FLEET_ENGINE_SEARCH_TASKS', GEOCODER_GEOCODE: 'GEOCODER_GEOCODE', MAPS_MAX_ZOOM: 'MAPS_MAX_ZOOM', PLACES_AUTOCOMPLETE: 'PLACES_AUTOCOMPLETE', + PLACES_DETAILS: 'PLACES_DETAILS', PLACES_FIND_PLACE_FROM_PHONE_NUMBER: 'PLACES_FIND_PLACE_FROM_PHONE_NUMBER', PLACES_FIND_PLACE_FROM_QUERY: 'PLACES_FIND_PLACE_FROM_QUERY', + PLACES_GATEWAY: 'PLACES_GATEWAY', PLACES_GET_PLACE: 'PLACES_GET_PLACE', PLACES_LOCAL_CONTEXT_SEARCH: 'PLACES_LOCAL_CONTEXT_SEARCH', PLACES_NEARBY_SEARCH: 'PLACES_NEARBY_SEARCH', + PLACES_SEARCH_TEXT: 'PLACES_SEARCH_TEXT', STREETVIEW_GET_PANORAMA: 'STREETVIEW_GET_PANORAMA' + }, + RPCStatus: { + ABORTED: 'ABORTED', ALREADY_EXISTS: 'ALREADY_EXISTS', CANCELLED: 'CANCELLED', DATA_LOSS: 'DATA_LOSS', DEADLINE_EXCEEDED: 'DEADLINE_EXCEEDED', + FAILED_PRECONDITION: 'FAILED_PRECONDITION', INTERNAL: 'INTERNAL', INVALID_ARGUMENT: 'INVALID_ARGUMENT', NOT_FOUND: 'NOT_FOUND', OK: 'OK', + OUT_OF_RANGE: 'OUT_OF_RANGE', PERMISSION_DENIED: 'PERMISSION_DENIED', RESOURCE_EXHAUSTED: 'RESOURCE_EXHAUSTED', UNAUTHENTICATED: 'UNAUTHENTICATED', + UNAVAILABLE: 'UNAVAILABLE', UNIMPLEMENTED: 'UNIMPLEMENTED', UNKNOWN: 'UNKNOWN' + }, + MapsNetworkError: function() {}, + MapsRequestError: function() {}, + MapsServerError: function() {} }, }; -google.maps.LatLngBounds.MAX_BOUNDS = new google.maps.LatLngBounds(new google.maps.LatLng(-90,-180), new google.maps.LatLng(90,180)); +if (typeof window !== 'undefined') { + window.google = google; +} else if (typeof global !== 'undefined') { + global.google = google; +} -google.maps.geometry.encoding = { decodePath: function(encodedPath) { return []; }, encodePath: function(path) { return ''; } }; -google.maps.geometry.spherical = { computeArea: function(path, radius) { return 0; }, computeDistanceBetween: function(from, to, radius) { return 0; }, computeHeading: function(from, to) { return 0; }, computeLength: function(path, radius) { return 0; }, computeOffset: function(from, distance, heading, radius) { return null; }, computeOffsetOrigin: function(to, distance, heading, radius) { return null; }, computeSignedArea: function(loop, radius) { return 0; }, interpolate: function(from, to, fraction) { return null; } }; -google.maps.geometry.poly = { containsLocation: function(latLng, polygon) { return false; }, isLocationOnEdge: function(latLng, poly, tolerance) { return false; } }; - -google.maps.drawing.DrawingManager = function(options) { return { getDrawingMode:function(){return null;}, getMap:fnEmptyObject, setDrawingMode:noop, setMap:noop, setOptions:noop, addListener:noop }; }; -google.maps.drawing.OverlayType = { MARKER: 'marker', POLYGON: 'polygon', POLYLINE: 'polyline', RECTANGLE: 'rectangle', CIRCLE: 'circle' }; - -google.maps.visualization.HeatmapLayer = function(options) { return { getData:function(){return new google.maps.MVCArray();}, getMap:fnEmptyObject, setData:noop, setMap:noop, setOptions:noop }; }; - -google.maps.journeySharing.JourneySharingMapView = function(options) { return { automaticViewportMode: null, element: null, enableTraffic: false, locationProviders: EMPTY_ARRAY, map: null, mapOptions: null, addLocationProvider: noop, removeLocationProvider: noop, addListener: noop }; }; -google.maps.journeySharing.FleetEngineServiceType = { DELIVERY_VEHICLE_SERVICE: 'DELIVERY_VEHICLE_SERVICE', TASK_SERVICE: 'TASK_SERVICE', TRIP_SERVICE: 'TRIP_SERVICE', UNKNOWN_SERVICE: 'UNKNOWN_SERVICE' }; -google.maps.journeySharing.AutomaticViewportMode = { FIT_ANTICIPATED_ROUTE: 'FIT_ANTICIPATED_ROUTE', NONE: 'NONE'}; -google.maps.journeySharing.vehicle = {}; -google.maps.journeySharing.TripType = { SHARED: 'SHARED', EXCLUSIVE: 'EXCLUSIVE', UNKNOWN_TRIP_TYPE: 'UNKNOWN_TRIP_TYPE' }; - -google.maps.maps3d.Map3DElement = function(options) { return { addEventListener:noop, center:null, zoom:null, heading:0, tilt:0, roll:0, flyTo:noop, flyCameraTo:noop, stopCameraAnimation:noop }; }; -google.maps.maps3d.AltitudeMode = { ABSOLUTE: 'ABSOLUTE', CLAMP_TO_GROUND: 'CLAMP_TO_GROUND', RELATIVE_TO_GROUND: 'RELATIVE_TO_GROUND', RELATIVE_TO_MESH: 'RELATIVE_TO_MESH' }; -google.maps.maps3d.MapMode = { HYBRID: 'HYBRID', SATELLITE: 'SATELLITE'}; +google.maps.LatLngBounds.MAX_BOUNDS = new google.maps.LatLngBounds(new google.maps.LatLng(-90,-180), new google.maps.LatLng(90,180)); google.maps.WebGLOverlayView = function() {}; google.maps.WebGLOverlayView.prototype.onAdd = noop; @@ -420,61 +616,6 @@ google.maps.OverlayView.preventMapHitsFrom = noop; google.maps.Marker.MAX_ZINDEX = 0; -google.maps.places.Place = function(options) { - return { - fetchFields: function(options) { - return Promise.resolve({ - place: { - name: 'Fake Place', - }, - }); - }, - }; -}; - -google.maps.Place.searchByText = function(request) { return Promise.resolve({places: EMPTY_ARRAY}); }; -google.maps.Place.searchNearby = function(request) { return Promise.resolve({places: EMPTY_ARRAY}); }; -google.maps.Place.prototype.fetchFields = function(request) { return Promise.resolve(this); }; -google.maps.Place.prototype.toJSON = function() { return { id: this.id }; }; -google.maps.Place.prototype.getNextOpeningTime = function(){ return Promise.resolve(null); }; -google.maps.Place.prototype.isOpen = function(date){ return Promise.resolve(false); }; - -google.maps.Settings.getInstance = fnEmptyObject; -google.maps.Settings.experienceIds = EMPTY_ARRAY; -google.maps.Settings.fetchAppCheckToken = noop; - -google.maps.MapsNetworkError = function() {}; -google.maps.MapsRequestError = function() {}; -google.maps.MapsServerError = function() {}; +google.maps.routes.computeRoutes = function(request) { return Promise.resolve({ routes: EMPTY_ARRAY }); }; google.maps.Data.Feature = function(options) { return { forEachProperty:noop, getGeometry:fnEmptyObject, getId:function(){return undefined;}, getProperty:function(name){return undefined;}, removeProperty:noop, setGeometry:noop, setProperty:noop, toGeoJson:function(callback){callback(EMPTY_OBJECT);} }; }; - -google.maps.marker.AdvancedMarkerElement = function(options) { - return { - collisionBehavior: (options && options.collisionBehavior) || null, - gmpClickable: (options && options.gmpClickable) || null, - gmpDraggable: (options && options.gmpDraggable) || false, - map: (options && options.map) || null, - position: (options && options.position) || null, - title: (options && options.title) || '', - zIndex: (options && options.zIndex) || null, - addListener: noop, - }; -}; - -google.maps.marker.PinElement = function(options) { - return { - background: (options && options.background) || null, - borderColor: (options && options.borderColor) || null, - element: (options && options.element) || null, - glyph: (options && options.glyph) || null, - glyphColor: (options && options.glyphColor) || null, - scale: (options && options.scale) || 1, - }; -}; - -google.maps.marker.CollisionBehavior = { - OPTIONAL_AND_HIDES_LOWER_PRIORITY: 'OPTIONAL_AND_HIDES_LOWER_PRIORITY', - REQUIRED: 'REQUIRED', - REQUIRED_AND_HIDES_OPTIONAL: 'REQUIRED_AND_HIDES_OPTIONAL' -}; diff --git a/test.html b/test.html deleted file mode 100644 index 8df2b2b..0000000 --- a/test.html +++ /dev/null @@ -1,66 +0,0 @@ - - -
-Open the console to see the test results.
- - - - diff --git a/test/test.html b/test/test.html new file mode 100644 index 0000000..a24f68b --- /dev/null +++ b/test/test.html @@ -0,0 +1,45 @@ + + + +Open the console to see the test results.
+ + + + diff --git a/test/verify_frontend.spec.js b/test/verify_frontend.spec.js new file mode 100644 index 0000000..128be5a --- /dev/null +++ b/test/verify_frontend.spec.js @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +test('verify google maps stub in browser', async ({ page }) => { + const filePath = `file://${path.resolve(__dirname, 'test.html')}`; + await page.goto(filePath); + + // Check if google object is defined + const isGoogleDefined = await page.evaluate(() => typeof google !== 'undefined'); + expect(isGoogleDefined).toBe(true); + + // Verify new namespaces and methods + const apiCheck = await page.evaluate(async () => { + const results = {}; + results.routesCompute = typeof google.maps.routes.computeRoutes === 'function'; + results.addressValidationFetch = typeof google.maps.addressValidation.fetchAddressValidation === 'function'; + results.placeClass = typeof google.maps.places.Place === 'function'; + results.placeSearchByText = typeof google.maps.places.Place.searchByText === 'function'; + results.map3dClass = typeof google.maps.maps3d.Map3DElement === 'function'; + + // Test importLibrary + const airQualityLib = await google.maps.importLibrary('airQuality'); + results.airQualityLibOk = airQualityLib === google.maps.airQuality; + + return results; + }); + + expect(apiCheck.routesCompute).toBe(true); + expect(apiCheck.addressValidationFetch).toBe(true); + expect(apiCheck.placeClass).toBe(true); + expect(apiCheck.placeSearchByText).toBe(true); + expect(apiCheck.map3dClass).toBe(true); + expect(apiCheck.airQualityLibOk).toBe(true); + + await page.screenshot({ path: 'frontend_verification.png' }); +}); diff --git a/test/verify_stub.js b/test/verify_stub.js new file mode 100644 index 0000000..95256ad --- /dev/null +++ b/test/verify_stub.js @@ -0,0 +1,49 @@ +const assert = require('assert'); +require('../stub.js'); + +async function runTests() { + console.log('Starting API structure verification...'); + + // 1. Routes + assert.ok(google.maps.routes, 'google.maps.routes should exist'); + assert.strictEqual(typeof google.maps.routes.computeRoutes, 'function', 'computeRoutes should be a function at google.maps.routes'); + console.log('✓ google.maps.routes.computeRoutes is correctly placed'); + + // 2. Address Validation + assert.ok(google.maps.addressValidation, 'google.maps.addressValidation should exist'); + assert.strictEqual(typeof google.maps.addressValidation.fetchAddressValidation, 'function', 'fetchAddressValidation should be a function at google.maps.addressValidation'); + console.log('✓ google.maps.addressValidation.fetchAddressValidation is correctly placed'); + + // 3. Place + const placeOptions = { id: 'test-id' }; + const place = new google.maps.places.Place(placeOptions); + assert.strictEqual(place.id, 'test-id', 'Place instance should have correct ID'); + assert.strictEqual(typeof place.fetchFields, 'function', 'Place instance should have fetchFields'); + assert.strictEqual(typeof google.maps.places.Place.searchByText, 'function', 'google.maps.places.Place.searchByText should exist'); + assert.strictEqual(typeof google.maps.places.Place.searchNearby, 'function', 'google.maps.places.Place.searchNearby should exist'); + + const legacyPlace = new google.maps.Place(placeOptions); + assert.strictEqual(legacyPlace.id, 'test-id', 'Legacy Place should delegate correctly'); + console.log('✓ google.maps.places.Place and legacy Place are correctly implemented'); + + // 4. Maps3D + const map3d = new google.maps.maps3d.Map3DElement({ center: {lat: 1, lng: 2}, zoom: 10, heading: 45 }); + assert.deepStrictEqual(map3d.center, {lat: 1, lng: 2}, 'Map3DElement should initialize center'); + assert.strictEqual(map3d.zoom, 10, 'Map3DElement should initialize zoom'); + assert.strictEqual(map3d.heading, 45, 'Map3DElement should initialize heading'); + console.log('✓ google.maps.maps3d.Map3DElement initializes from options correctly'); + + // 5. importLibrary + const mapsLib = await google.maps.importLibrary('maps'); + assert.strictEqual(mapsLib, google.maps, 'importLibrary("maps") should return google.maps'); + const routesLib = await google.maps.importLibrary('routes'); + assert.strictEqual(routesLib, google.maps.routes, 'importLibrary("routes") should return google.maps.routes'); + console.log('✓ google.maps.importLibrary works for new libraries'); + + console.log('All API structure assertions passed!'); +} + +runTests().catch(err => { + console.error('Assertion failed:', err); + process.exit(1); +});