|
1 | 1 | import { render } from '@testing-library/react'; |
2 | 2 | import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; |
3 | | -import type { ReactNode } from 'react'; |
| 3 | +import { createRef, type ReactNode } from 'react'; |
4 | 4 | import { createMockNaverMaps, type MockKVO } from './test-utils.js'; |
5 | 5 |
|
6 | 6 | vi.mock('../hooks/use-navermaps.js', () => ({ |
@@ -80,37 +80,53 @@ describe('CustomOverlay 스펙 테스트', () => { |
80 | 80 | }); |
81 | 81 |
|
82 | 82 | test('position 변경 시 draw 재호출 (left/top 업데이트)', async () => { |
| 83 | + const ref = createRef<naver.maps.OverlayView>(); |
| 84 | + |
83 | 85 | const { rerender } = render( |
84 | 86 | <Wrapper> |
85 | | - <CustomOverlay position={{ lat: 37.5, lng: 127.0 }}> |
| 87 | + <CustomOverlay position={{ lat: 37.5, lng: 127.0 }} ref={ref}> |
86 | 88 | <span data-testid="pos-child">content</span> |
87 | 89 | </CustomOverlay> |
88 | 90 | </Wrapper>, |
89 | 91 | ); |
90 | 92 |
|
91 | 93 | await vi.waitFor(() => { |
| 94 | + expect(ref.current).not.toBeNull(); |
92 | 95 | const child = document.querySelector('[data-testid="pos-child"]'); |
93 | 96 | expect(child).not.toBeNull(); |
94 | 97 | const container = child!.parentElement!; |
95 | | - // fromCoordToOffset mock은 항상 { x: 100, y: 200 }을 반환 |
| 98 | + // fromCoordToOffset mock: lat=37.5,lng=127 → { x: 100, y: 200 } |
96 | 99 | expect(container.style.left).toBe('100px'); |
97 | 100 | expect(container.style.top).toBe('200px'); |
98 | 101 | }); |
99 | 102 |
|
| 103 | + // 실제 draw 재호출을 인스턴스 메서드 spy로 단언 (setPosition → draw 경로). |
| 104 | + const setPositionSpy = vi.spyOn( |
| 105 | + ref.current as unknown as { setPosition: (p: unknown) => void }, |
| 106 | + 'setPosition', |
| 107 | + ); |
| 108 | + |
100 | 109 | rerender( |
101 | 110 | <Wrapper> |
102 | | - <CustomOverlay position={{ lat: 38.0, lng: 128.0 }}> |
| 111 | + <CustomOverlay position={{ lat: 38.0, lng: 128.0 }} ref={ref}> |
103 | 112 | <span data-testid="pos-child">content</span> |
104 | 113 | </CustomOverlay> |
105 | 114 | </Wrapper>, |
106 | 115 | ); |
107 | 116 |
|
| 117 | + // setPosition이 새 좌표로 호출되어야 함 (draw가 재실행되는 트리거) |
| 118 | + await vi.waitFor(() => { |
| 119 | + expect(setPositionSpy).toHaveBeenCalledWith({ lat: 38.0, lng: 128.0 }); |
| 120 | + }); |
| 121 | + |
| 122 | + // left/top이 새 좌표 기반 값으로 갱신 (draw 미재호출이면 100/200 그대로 → FAIL) |
| 123 | + // lng=128 → x=100+(128-127)=101, lat=38 → y=200+(38-37.5)=200.5 |
108 | 124 | await vi.waitFor(() => { |
109 | 125 | const child = document.querySelector('[data-testid="pos-child"]'); |
110 | 126 | expect(child).not.toBeNull(); |
111 | 127 | const container = child!.parentElement!; |
112 | | - expect(container.style.left).toBe('100px'); |
113 | | - expect(container.style.top).toBe('200px'); |
| 128 | + expect(container.style.left).toBe('101px'); |
| 129 | + expect(container.style.top).toBe('200.5px'); |
114 | 130 | }); |
115 | 131 | }); |
116 | 132 |
|
@@ -226,4 +242,104 @@ describe('CustomOverlay 스펙 테스트', () => { |
226 | 242 | expect(pane).not.toBeNull(); |
227 | 243 | }); |
228 | 244 | }); |
| 245 | + |
| 246 | + /** |
| 247 | + * fix-10: position에 .equals()를 가진 객체(LatLng 인스턴스)를 같은 좌표로 다시 전달하면 |
| 248 | + * 참조가 다르더라도 kvoEquals(equals 호출)로 변경 없음으로 판정해 setPosition을 호출하지 않아야 한다. |
| 249 | + * |
| 250 | + * test-utils의 MockLatLng은 .equals()가 없으므로, 이 테스트에서는 .equals()를 가진 |
| 251 | + * 별도 LatLng-like 객체로 검증한다. |
| 252 | + */ |
| 253 | + test('동일 좌표 LatLng(.equals) 재전달 시 setPosition 미호출 (fix-10)', async () => { |
| 254 | + class LatLngLike { |
| 255 | + constructor( |
| 256 | + public lat: number, |
| 257 | + public lng: number, |
| 258 | + ) {} |
| 259 | + equals(other: { lat: number; lng: number }) { |
| 260 | + return this.lat === other.lat && this.lng === other.lng; |
| 261 | + } |
| 262 | + } |
| 263 | + |
| 264 | + const ref = createRef<naver.maps.OverlayView>(); |
| 265 | + |
| 266 | + const p1 = new LatLngLike(37.5, 127.0) as unknown as naver.maps.LatLng; |
| 267 | + |
| 268 | + const { rerender } = render( |
| 269 | + <Wrapper> |
| 270 | + <CustomOverlay position={p1} ref={ref}> |
| 271 | + <span data-testid="eq-child">content</span> |
| 272 | + </CustomOverlay> |
| 273 | + </Wrapper>, |
| 274 | + ); |
| 275 | + |
| 276 | + await vi.waitFor(() => { |
| 277 | + expect(ref.current).not.toBeNull(); |
| 278 | + }); |
| 279 | + |
| 280 | + // ref로 받은 인스턴스에 spy 설치 (prototype 메서드를 instance level로 가로챔) |
| 281 | + const setPositionSpy = vi.spyOn( |
| 282 | + ref.current as unknown as { setPosition: (p: unknown) => void }, |
| 283 | + 'setPosition', |
| 284 | + ); |
| 285 | + |
| 286 | + // 같은 좌표를 가진 새 LatLngLike 인스턴스 (참조는 다름, equals는 true) |
| 287 | + const p2 = new LatLngLike(37.5, 127.0) as unknown as naver.maps.LatLng; |
| 288 | + |
| 289 | + rerender( |
| 290 | + <Wrapper> |
| 291 | + <CustomOverlay position={p2} ref={ref}> |
| 292 | + <span data-testid="eq-child">content</span> |
| 293 | + </CustomOverlay> |
| 294 | + </Wrapper>, |
| 295 | + ); |
| 296 | + |
| 297 | + // kvoEquals(p1, p2) === true이므로 setPosition 미호출 |
| 298 | + expect(setPositionSpy).not.toHaveBeenCalled(); |
| 299 | + }); |
| 300 | + |
| 301 | + test('실제 좌표가 다른 LatLng 전달 시 setPosition 호출 (fix-10 회귀 검증)', async () => { |
| 302 | + class LatLngLike { |
| 303 | + constructor( |
| 304 | + public lat: number, |
| 305 | + public lng: number, |
| 306 | + ) {} |
| 307 | + equals(other: { lat: number; lng: number }) { |
| 308 | + return this.lat === other.lat && this.lng === other.lng; |
| 309 | + } |
| 310 | + } |
| 311 | + |
| 312 | + const ref = createRef<naver.maps.OverlayView>(); |
| 313 | + |
| 314 | + const p1 = new LatLngLike(37.5, 127.0) as unknown as naver.maps.LatLng; |
| 315 | + |
| 316 | + const { rerender } = render( |
| 317 | + <Wrapper> |
| 318 | + <CustomOverlay position={p1} ref={ref}> |
| 319 | + <span data-testid="diff-child">content</span> |
| 320 | + </CustomOverlay> |
| 321 | + </Wrapper>, |
| 322 | + ); |
| 323 | + |
| 324 | + await vi.waitFor(() => { |
| 325 | + expect(ref.current).not.toBeNull(); |
| 326 | + }); |
| 327 | + |
| 328 | + const setPositionSpy = vi.spyOn( |
| 329 | + ref.current as unknown as { setPosition: (p: unknown) => void }, |
| 330 | + 'setPosition', |
| 331 | + ); |
| 332 | + |
| 333 | + const p2 = new LatLngLike(38.0, 128.0) as unknown as naver.maps.LatLng; |
| 334 | + |
| 335 | + rerender( |
| 336 | + <Wrapper> |
| 337 | + <CustomOverlay position={p2} ref={ref}> |
| 338 | + <span data-testid="diff-child">content</span> |
| 339 | + </CustomOverlay> |
| 340 | + </Wrapper>, |
| 341 | + ); |
| 342 | + |
| 343 | + expect(setPositionSpy).toHaveBeenCalledWith(p2); |
| 344 | + }); |
229 | 345 | }); |
0 commit comments