admin管理员组

文章数量:1133745

I have a React component ( Next JS ) :

"use client"

import { getEnv } from "@/actions/env"
import Layers from "@/features/solar-analytics/panel-installation/map/components/layers"
import mapboxgl, { LngLatLike } from "mapbox-gl"
import "mapbox-gl/dist/mapbox-gl.css"
import { useQueryState } from "nuqs"
import React, { useEffect, useRef, useState } from "react"
import { useSelector } from "react-redux"

import { RootState } from "@/redux/store"

import Compass from "@/components/core/compass"

import { createBuildingModelsLayers } from "./layers/3d-building-model"
import { createFootPrintLayers } from "./layers/foot-print"
import { createParcelLayers } from "./layers/parcel"

/* eslint-disable no-console */

type MapViewerProps = {
  zoom?: number
  className?: string
}

type MapFlyOptions = {
  center: [number, number]
  zoom: number
  pitch: number
  essential: boolean
  duration: number
  speed: number
  screenSpeed: number
  maxDuration: number
}

export const defaultLocation: [number, number] = [10.4515, 51.1657]

export const MapViewer: React.FC<MapViewerProps> = ({ zoom = 5, className = "h-full w-full" }) => {
  const [lng] = useQueryState("lng")

  const [lat] = useQueryState("lat")

  const [bearing, setBearing] = useQueryState("bearing")

  const [layer] = useQueryState("layer")

  const mapContainer = useRef<HTMLDivElement>(null)

  const mapRef = useRef<mapboxgl.Map | null>(null)

  const [mapLoaded, setMapLoaded] = useState(false)

  const footPrintGeometry = useSelector((state: RootState) => state.geometry.footPrintGeometry)

  const parcelGeometry = useSelector((state: RootState) => state.geometry.parcelGeometry)

  const buildingModels = useSelector((state: RootState) => state.geometry.buildingModels)

  const footPrintLayersRef = useRef<{
    sourceIds: string[]
    layerIds: string[]
    labelSourceIds: string[]
    labelIds: string[]
    areaLabelSourceIds: string[]
    areaLabelIds: string[]
  }>({
    sourceIds: [],
    layerIds: [],
    labelSourceIds: [],
    labelIds: [],
    areaLabelSourceIds: [],
    areaLabelIds: [],
  })

  useEffect(() => {
    const init = async () => {
      try {
        const env = await getEnv()
        const token = env.NEXT_PUBLIC_DOCKER_MAPBOX_ACCESS_TOKEN!
        await initializeMap(token)
      } catch (error) {
        console.error("Failed to initialize map:", error)
      }
    }

    init()

    return () => {
      if (mapRef.current) {
        mapRef.current.remove()
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  useEffect(() => {
    navigateToLocation(mapRef, lng as string, lat as string)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lng, lat])

  useEffect(() => {
    if (!mapRef.current || !parcelGeometry || parcelGeometry.length === 0) return

    // Get the ID of the first footPrintGeometry layer
    const firstFootPrintGeometrylayer =
      footPrintLayersRef.current.layerIds.length > 0 ? footPrintLayersRef.current.layerIds[0] : undefined

    const { bounds, cleanup } = createParcelLayers(mapRef.current, parcelGeometry, firstFootPrintGeometrylayer)

    if (!bounds.isEmpty()) {
      mapRef.current.fitBounds(bounds, {
        maxZoom: 20,
        pitch: 60,
      })
    }

    return cleanup
  }, [parcelGeometry])

  useEffect(() => {
    if (!mapRef.current || !footPrintGeometry || footPrintGeometry.length === 0) return

    const { bounds, layerIds, cleanup } = createFootPrintLayers(mapRef.current, footPrintGeometry)

    // Update footPrintLayersRef with the new layer IDs
    footPrintLayersRef.current = {
      sourceIds: layerIds.map((layer) => layer.sourceId),
      layerIds: layerIds.map((layer) => layer.layerId),
      labelSourceIds: [],
      labelIds: [],
      areaLabelSourceIds: [],
      areaLabelIds: [],
    }

    if (!bounds.isEmpty()) {
      mapRef.current.fitBounds(bounds, {
        maxZoom: 20,
        pitch: 60,
      })
    }

    return cleanup
  }, [footPrintGeometry])

  useEffect(() => {
    let layerIds: string[] = []

    const init = async () => {
      layerIds = await createBuildingModelsLayers(mapRef, buildingModels)
    }

    init()

    return () => {
      const map = mapRef.current
      if (!map) return
      layerIds.forEach((id) => {
        // Check if map and its style still exist before cleanup
        // This prevents "Cannot read properties of undefined" errors that occur
        // When trying to navigate to the next page after the map component has been destroyed
        if (map.style) {
          if (map.getLayer(id)) map.removeLayer(id)
        }
      })
    }
  }, [buildingModels])

  // Set the layer based on the URL query parameter (layer)
  useEffect(() => {
    if (!mapRef.current || !mapLoaded) return

    if (layer === "wms") {
      addWMS()
    } else {
      removeWMSLayer()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [layer, mapLoaded])

  const initializeMap = async (token: string) => {
    if (!mapContainer.current) return

    const initialCenter = lng && lat ? [parseFloat(lng), parseFloat(lat)] : defaultLocation

    mapboxgl.accessToken = token

    const map = new mapboxgl.Map({
      container: mapContainer.current,
      style: "mapbox://styles/mapbox/satellite-streets-v12",
      center: initialCenter as LngLatLike,
      zoom,
      antialias: true,
      bearing: +bearing!,
    })

    map.on("rotate", () => {
      const bearing = map.getBearing();
      setBearing(`${bearing}`);
    });

    map.on("load", () => {
      setMapLoaded(true)
    })

    mapRef.current = map
  }

  const navigateToLocation = (mapRef: React.MutableRefObject<mapboxgl.Map | null>, lng: string, lat: string) => {
    if (!lng || !lat || !mapRef.current) return

    const newLocation: [number, number] = [parseFloat(lng), parseFloat(lat)]
    const isDefaultLocation = newLocation[0] === defaultLocation[0] && newLocation[1] === defaultLocation[1]

    const options: MapFlyOptions = {
      center: newLocation,
      zoom: isDefaultLocation ? 5 : 20,
      pitch: isDefaultLocation ? 0 : 60,
      essential: true,
      duration: 2000,
      speed: 1.2,
      screenSpeed: 1.5,
      maxDuration: 3000,
    }

    mapRef.current.flyTo(options)
  }

  const addWMS = () => {
    if (!mapRef.current?.getSource("wms-source")) {
      mapRef.current?.addSource("wms-source", {
        type: "raster",
        tiles: [
          ";VERSION=1.1.0&REQUEST=GetMap&LAYERS=orthophoto_germany&STYLES=default&SRS=EPSG:3857&WIDTH=256&HEIGHT=256&FORMAT=image/jpeg&BBOX={bbox-epsg-3857}",
        ],
        tileSize: 256,
      })

      mapRef.current?.addLayer({
        id: "wms-layer",
        type: "raster",
        source: "wms-source",
        paint: {},
      })
    }

    moveWMSLayerToTop()
  }

  const moveWMSLayerToTop = () => {
    const map = mapRef.current

    if (map) {
      const style = map.getStyle()

      if (style) {
        // Move the WMS layer to the top of all layers
        const lastLayerId = style?.layers[style?.layers.length - 1].id

        if (lastLayerId) {
          map.moveLayer("wms-layer", lastLayerId)
        }
      }
    }
  }

  const removeWMSLayer = () => {
    const map = mapRef.current

    if (map) {
      if (map.getLayer("wms-layer")) {
        map.removeLayer("wms-layer")
        map.removeSource("wms-source")
      }
    }
  }

  return (
    <>
      <div ref={mapContainer} className={className} />
      <div className='z-50 absolute bottom-20 right-2 flex flex-col gap-2 items-center'>
        <Compass bearing={+bearing!} />
        <Layers />
      </div>
    </>
  )
}

and I have e2e test with playwright:

import test, { expect } from "@playwright/test"

test.describe("Map Viewer (Home Page)", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/")
    await page.waitForLoadState("networkidle")
  })

  test("should exist on the page", async ({ page }) => {
    await expect(page).toHaveURL("/?bearing=0&layer=mapbox")
  })

  test("should render map container", async ({ page }) => {
    await expect(page.locator(".mapboxgl-canvas")).toBeVisible()
  })

  test("should display compass", async ({ page }) => {
    await expect(page.getByTestId("map-compass")).toBeVisible()
  })

  test("should display layers", async ({ page }) => {
    await expect(page.getByTestId("map-layers")).toBeVisible()
  })

  test("should update URL when selecting wms layer", async ({ page }) => {
    await page.getByTestId("wms-layer-card").click()
    await expect(page).toHaveURL("/?bearing=0&layer=wms")
  })

  test("should update URL when selecting mapbox layer", async ({ page }) => {
    await page.getByTestId("mapbox-layer-card").click()
    await expect(page).toHaveURL("/?bearing=0&layer=mapbox")
  })
})

These tests are passed without any issue.

No, I wanted to add a case when the map is rotated ( bearing changes ) , it should be changed in the URL.

I tried with Claude and GPT but it did not work.

test("should update URL when rotating the map to 45 degrees", async ({ page }) => {
  // Use page.evaluate to set the bearing to 45
  await page.evaluate(() => {
    if (window.mapRef) {
      window.mapRef.setBearing(45); // Rotate the map to 45 degrees
    }
  });

  // Wait for the URL to update
  await page.waitForTimeout(500);

  // Assert the URL includes bearing=45
  await expect(page).toHaveURL(/bearing=45/);
});

The mapRef is not exposed globally.

本文标签: typescriptHow to simulate mapbox rotation ( bearing ) in Playwright E2E testStack Overflow