diff --git a/package-lock.json b/package-lock.json index f9f313a..ce640b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@fontsource/noto-sans": "^5.2.10", - "@napi-rs/canvas": "^0.1.58", + "@napi-rs/canvas": "^0.1.84", "jsdom": "^27.0.0", "undici": "^6.22.0" }, @@ -2044,9 +2044,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.81.tgz", - "integrity": "sha512-ReCjd5SYI/UKx/olaQLC4GtN6wUQGjlgHXs1lvUvWGXfBMR3Fxnik3cL+OxKN5ithNdoU0/GlCrdKcQDFh2XKQ==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.84.tgz", + "integrity": "sha512-88FTNFs4uuiFKP0tUrPsEXhpe9dg7za9ILZJE08pGdUveMIDeana1zwfVkqRHJDPJFAmGY3dXmJ99dzsy57YnA==", "license": "MIT", "workspaces": [ "e2e/*" @@ -2055,22 +2055,22 @@ "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.81", - "@napi-rs/canvas-darwin-arm64": "0.1.81", - "@napi-rs/canvas-darwin-x64": "0.1.81", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.81", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.81", - "@napi-rs/canvas-linux-arm64-musl": "0.1.81", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.81", - "@napi-rs/canvas-linux-x64-gnu": "0.1.81", - "@napi-rs/canvas-linux-x64-musl": "0.1.81", - "@napi-rs/canvas-win32-x64-msvc": "0.1.81" + "@napi-rs/canvas-android-arm64": "0.1.84", + "@napi-rs/canvas-darwin-arm64": "0.1.84", + "@napi-rs/canvas-darwin-x64": "0.1.84", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.84", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.84", + "@napi-rs/canvas-linux-arm64-musl": "0.1.84", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.84", + "@napi-rs/canvas-linux-x64-gnu": "0.1.84", + "@napi-rs/canvas-linux-x64-musl": "0.1.84", + "@napi-rs/canvas-win32-x64-msvc": "0.1.84" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.81.tgz", - "integrity": "sha512-78Lz+AUi+MsWupyZjXwpwQrp1QCwncPvRZrdvrROcZ9Gq9grP7LfQZiGdR8LKyHIq3OR18mDP+JESGT15V1nXw==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.84.tgz", + "integrity": "sha512-pdvuqvj3qtwVryqgpAGornJLV6Ezpk39V6wT4JCnRVGy8I3Tk1au8qOalFGrx/r0Ig87hWslysPpHBxVpBMIww==", "cpu": [ "arm64" ], @@ -2084,9 +2084,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.81.tgz", - "integrity": "sha512-omejuKgHWKDGoh8rsgsyhm/whwxMaryTQjJTd9zD7hiB9/rzcEEJLHnzXWR5ysy4/tTjHaQotE6k2t8eodTLnA==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.84.tgz", + "integrity": "sha512-A8IND3Hnv0R6abc6qCcCaOCujTLMmGxtucMTZ5vbQUrEN/scxi378MyTLtyWg+MRr6bwQJ6v/orqMS9datIcww==", "cpu": [ "arm64" ], @@ -2100,9 +2100,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.81.tgz", - "integrity": "sha512-EYfk+co6BElq5DXNH9PBLYDYwc4QsvIVbyrsVHsxVpn4p6Y3/s8MChgC69AGqj3vzZBQ1qx2CRCMtg5cub+XuQ==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.84.tgz", + "integrity": "sha512-AUW45lJhYWwnA74LaNeqhvqYKK/2hNnBBBl03KRdqeCD4tKneUSrxUqIv8d22CBweOvrAASyKN3W87WO2zEr/A==", "cpu": [ "x64" ], @@ -2116,9 +2116,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.81.tgz", - "integrity": "sha512-teh6Q74CyAcH31yLNQGR9MtXSFxlZa5CI6vvNUISI14gWIJWrhOwUAOly+KRe1aztWR0FWTVSPxM4p5y+06aow==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.84.tgz", + "integrity": "sha512-8zs5ZqOrdgs4FioTxSBrkl/wHZB56bJNBqaIsfPL4ZkEQCinOkrFF7xIcXiHiKp93J3wUtbIzeVrhTIaWwqk+A==", "cpu": [ "arm" ], @@ -2132,9 +2132,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.81.tgz", - "integrity": "sha512-AGEopHFYRzJOjxY+2G1RmHPRnuWvO3Qdhq7sIazlSjxb3Z6dZHg7OB/4ZimXaimPjDACm9qWa6t5bn9bhXvkcw==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.84.tgz", + "integrity": "sha512-i204vtowOglJUpbAFWU5mqsJgH0lVpNk/Ml4mQtB4Lndd86oF+Otr6Mr5KQnZHqYGhlSIKiU2SYnUbhO28zGQA==", "cpu": [ "arm64" ], @@ -2148,9 +2148,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.81.tgz", - "integrity": "sha512-Bj3m1cl4GIhsigkdwOxii4g4Ump3/QhNpx85IgAlCCYXpaly6mcsWpuDYEabfIGWOWhDUNBOndaQUPfWK1czOQ==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.84.tgz", + "integrity": "sha512-VyZq0EEw+OILnWk7G3ZgLLPaz1ERaPP++jLjeyLMbFOF+Tr4zHzWKiKDsEV/cT7btLPZbVoR3VX+T9/QubnURQ==", "cpu": [ "arm64" ], @@ -2164,9 +2164,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.81.tgz", - "integrity": "sha512-yg/5NkHykVdwPlD3XObwCa/EswkOwLHswJcI9rHrac+znHsmCSj5AMX/RTU9Z9F6lZTwL60JM2Esit33XhAMiw==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.84.tgz", + "integrity": "sha512-PSMTh8DiThvLRsbtc/a065I/ceZk17EXAATv9uNvHgkgo7wdEfTh2C3aveNkBMGByVO3tvnvD5v/YFtZL07cIg==", "cpu": [ "riscv64" ], @@ -2180,9 +2180,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.81.tgz", - "integrity": "sha512-tPfMpSEBuV5dJSKexO/UZxpOqnYTaNbG8aKa1ek8QsWu+4SJ/foWkaxscra/RUv85vepx6WWDjzBNbNJsTnO0w==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.84.tgz", + "integrity": "sha512-N1GY3noO1oqgEo3rYQIwY44kfM11vA0lDbN0orTOHfCSUZTUyiYCY0nZ197QMahZBm1aR/vYgsWpV74MMMDuNA==", "cpu": [ "x64" ], @@ -2196,9 +2196,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.81.tgz", - "integrity": "sha512-1L0xnYgzqn8Baef+inPvY4dKqdmw3KCBoe0NEDgezuBZN7MA5xElwifoG8609uNdrMtJ9J6QZarsslLRVqri7g==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.84.tgz", + "integrity": "sha512-vUZmua6ADqTWyHyei81aXIt9wp0yjeNwTH0KdhdeoBb6azHmFR8uKTukZMXfLCC3bnsW0t4lW7K78KNMknmtjg==", "cpu": [ "x64" ], @@ -2212,9 +2212,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.81.tgz", - "integrity": "sha512-57ryVbhm/z7RE9/UVcS7mrLPdlayLesy+9U0Uf6epCoeSGrs99tfieCcgZWFbIgmByQ1AZnNtFI2N6huqDLlWQ==", + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.84.tgz", + "integrity": "sha512-YSs8ncurc1xzegUMNnQUTYrdrAuaXdPMOa+iYYyAxydOtg0ppV386hyYMsy00Yip1NlTgLCseRG4sHSnjQx6og==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 537738b..2988403 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "dependencies": { "@fontsource/noto-sans": "^5.2.10", - "@napi-rs/canvas": "^0.1.58", + "@napi-rs/canvas": "^0.1.84", "jsdom": "^27.0.0", "undici": "^6.22.0" }, diff --git a/src/index.ts b/src/index.ts index 9d6baf2..db09fab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -375,15 +375,42 @@ function patchMapPrototype( }; const mapInstance = originalInit.call(this, id, headlessOpts); - (this as any)._headlessSize = { ...options.mapSize }; + // Initialize headless size with default or user-provided mapSize + const initialSize = (opts as any)?.mapSize || options.mapSize; + (this as any)._headlessSize = { ...initialSize }; return mapInstance; }; // Override getSize since jsdom doesn't support clientWidth/clientHeight L.Map.prototype.getSize = function (this: any): LeafletModule.Point { if (!this._size || this._sizeChanged) { - const size = (this as any)._headlessSize ?? options.mapSize; - this._size = new L.Point(size.width, size.height); + let width: number | undefined; + let height: number | undefined; + + // Try to get dimensions from the container first + // This allows tests to mock dimensions on the container element + const container = this.getContainer(); + if (container) { + if (container.clientWidth > 0 && container.clientHeight > 0) { + width = container.clientWidth; + height = container.clientHeight; + } else if (container.getBoundingClientRect) { + const rect = container.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + width = rect.width; + height = rect.height; + } + } + } + + // Fallback to headless size + if (width === undefined || height === undefined) { + const size = (this as any)._headlessSize ?? options.mapSize; + width = size.width; + height = size.height; + } + + this._size = new L.Point(width as number, height as number); this._sizeChanged = false; } return this._size.clone(); diff --git a/src/polyfills/dom-event.ts b/src/polyfills/dom-event.ts index 3bab04c..3af5124 100644 --- a/src/polyfills/dom-event.ts +++ b/src/polyfills/dom-event.ts @@ -17,6 +17,7 @@ export function patchDomEvent(L: typeof LeafletModule): void { } const originalOff = L.DomEvent.off; + const originalGetMousePosition = L.DomEvent.getMousePosition; // Store original on the object to avoid closure issues in some environments (like Jest) // This seems to prevent a silent crash during module import in specific test configurations @@ -38,6 +39,30 @@ export function patchDomEvent(L: typeof LeafletModule): void { } } as typeof originalOff; + // Patch getMousePosition to support JSDOM environments where getBoundingClientRect works + // but offsetParent/clientLeft/clientTop layout properties might not be perfect + // Use 'any' for the event type to avoid TypeScript issues if Touch is not defined globally in strict environments + L.DomEvent.getMousePosition = function(e: any, container?: HTMLElement): LeafletModule.Point { + if (container && container.getBoundingClientRect) { + const rect = container.getBoundingClientRect(); + const clientLeft = container.clientLeft || 0; + const clientTop = container.clientTop || 0; + + return new L.Point( + e.clientX - rect.left - clientLeft, + e.clientY - rect.top - clientTop + ); + } + + // Fallback to original implementation if container doesn't have getBoundingClientRect + // or if it's not provided (though Leaflet usually provides it) + return originalGetMousePosition(e, container); + }; + + // Note: We don't need to patch addListener/removeListener because standard Leaflet + // already checks for addEventListener/removeEventListener on the object. + // Since we run in JSDOM (which has these), standard Leaflet works fine. + // Mark as patched (L.DomEvent as any)._leafletNodePatched = true; diff --git a/tests/interaction.test.ts b/tests/interaction.test.ts new file mode 100644 index 0000000..957730c --- /dev/null +++ b/tests/interaction.test.ts @@ -0,0 +1,150 @@ + +import L from '../src/index'; +import { describe, it, expect, vi, afterEach } from 'vitest'; + +describe('JSDOM Interaction Support', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('L.DomEvent.getMousePosition', () => { + it('should calculate position using getBoundingClientRect if available', () => { + const container = document.createElement('div'); + + // Mock getBoundingClientRect + container.getBoundingClientRect = vi.fn(() => ({ + left: 10, + top: 20, + right: 110, + bottom: 120, + width: 100, + height: 100, + x: 10, + y: 20, + toJSON: () => {} + })); + + // JSDOM defaults clientLeft/Top to 0, which is fine + + // Create a mock event relative to the viewport (page) + // clientX = left + 30 = 40 + // clientY = top + 40 = 60 + const event = { + clientX: 40, + clientY: 60, + type: 'click' + } as unknown as MouseEvent; + + const pos = L.DomEvent.getMousePosition(event, container); + + expect(pos.x).toBe(30); + expect(pos.y).toBe(40); + }); + + it('should respect container clientLeft/clientTop (borders)', () => { + const container = document.createElement('div'); + + // Mock properties that JSDOM usually sets to 0 + Object.defineProperty(container, 'clientLeft', { value: 5 }); + Object.defineProperty(container, 'clientTop', { value: 5 }); + + container.getBoundingClientRect = vi.fn(() => ({ + left: 10, + top: 10, + right: 110, + bottom: 110, + width: 100, + height: 100, + x: 10, + y: 10, + toJSON: () => {} + })); + + // Event at 20, 20 + // Expected: 20 - 10 (rect) - 5 (border) = 5 + const event = { + clientX: 20, + clientY: 20, + type: 'click' + } as unknown as MouseEvent; + + const pos = L.DomEvent.getMousePosition(event, container); + + expect(pos.x).toBe(5); + expect(pos.y).toBe(5); + }); + }); + + describe('L.Map.getSize', () => { + it('should respect mocked clientWidth/clientHeight', () => { + const map = L.map(document.createElement('div')); + const container = map.getContainer(); + + // Mock dimensions + Object.defineProperty(container, 'clientWidth', { value: 800, configurable: true }); + Object.defineProperty(container, 'clientHeight', { value: 600, configurable: true }); + + map.invalidateSize(); + + const size = map.getSize(); + expect(size.x).toBe(800); + expect(size.y).toBe(600); + }); + + it('should respect mocked getBoundingClientRect if client dimensions are zero', () => { + const map = L.map(document.createElement('div')); + const container = map.getContainer(); + + // clientWidth/Height are 0 by default in JSDOM + + container.getBoundingClientRect = vi.fn(() => ({ + left: 0, top: 0, right: 500, bottom: 400, + width: 500, height: 400, + x: 0, y: 0, + toJSON: () => {} + })); + + map.invalidateSize(); + + const size = map.getSize(); + expect(size.x).toBe(500); + expect(size.y).toBe(400); + }); + + it('should fall back to headless size if no DOM dimensions available', () => { + const map = L.map(document.createElement('div'), { + mapSize: { width: 200, height: 200 } + }); + + // No mocks applied + + map.invalidateSize(); + const size = map.getSize(); + expect(size.x).toBe(200); + expect(size.y).toBe(200); + }); + }); + + describe('L.DomEvent.on (Event Listeners)', () => { + it('should use native addEventListener', () => { + const element = document.createElement('div'); + const spy = vi.spyOn(element, 'addEventListener'); + const handler = () => {}; + + L.DomEvent.on(element, 'click', handler); + + expect(spy).toHaveBeenCalledWith('click', expect.any(Function), false); + }); + + it('should trigger handler when event is dispatched', () => { + const element = document.createElement('div'); + const handler = vi.fn(); + + L.DomEvent.on(element, 'click', handler); + + element.dispatchEvent(new window.Event('click')); + + expect(handler).toHaveBeenCalled(); + }); + }); +});