├── .browserlistrc ├── .eslintignore ├── .babelrc ├── .c8rc.json ├── .npmignore ├── .gitignore ├── .eslintrc.json ├── .github └── workflows │ ├── deploy.yml │ └── verify.yml ├── LICENSE.md ├── package.json ├── src ├── tests │ ├── fixtures │ │ └── Simple.jsx │ ├── utils │ │ └── testdom.js │ └── unit │ │ └── index.js └── lib │ └── index.js └── README.md /.browserlistrc: -------------------------------------------------------------------------------- 1 | defaults -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .nyc_output/ 3 | coverage/ 4 | output/ 5 | node_modules/ 6 | index.js 7 | tmp 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "only": [ 4 | "src/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": ["lcov", "json"], 3 | "reports-dir": "coverage", 4 | "exclude": ["tmp", "coverage", "node_modules", "src/tests", ".github"] 5 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | coverage 3 | output 4 | src/tests 5 | tmp 6 | .jshint* 7 | .travis.yml 8 | *.tgz 9 | .eslint* 10 | .babel* 11 | .c8* 12 | .browser* 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Dependency directory 12 | node_modules 13 | 14 | # Optional npm cache directory 15 | .npm 16 | 17 | # Optional REPL history 18 | .node_repl_history 19 | 20 | # No jshint expected 21 | .jshintrc 22 | .jshintignore 23 | 24 | # Project specific 25 | .nyc_output/ 26 | /coverage 27 | /dist 28 | /output 29 | /package 30 | /index.js 31 | /index.mjs 32 | tmp 33 | *.tgz 34 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parser": "@babel/eslint-parser", 6 | "plugins": [ 7 | "react" 8 | ], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:react/recommended" 12 | ], 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | } 17 | }, 18 | "rules": { 19 | "indent": [2, 2, {"SwitchCase": 1}], 20 | "quotes": [2, "single"] 21 | }, 22 | "settings": { 23 | "react": { 24 | "version": "detect" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [ master ] 5 | 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '20.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - run: npm ci 19 | - name: Verify 20 | run: npm run lint && npm test 21 | - name: Publish 22 | if: ${{ success() }} 23 | run: npm publish --provenance --access public 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | pull_request: 4 | branches: [ master ] 5 | 6 | jobs: 7 | verify: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [18.x, 20.x, 22.x] 14 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - name: Lint, Test, And Coverage 24 | run: npm run lint && npm run test:cover 25 | - name: Coverage Upload 26 | if: ${{ success() }} 27 | uses: coverallsapp/github-action@master 28 | with: 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | path-to-lcov: ./coverage/lcov.info -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 - 2024 Alex Grant, LocalNerve LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of react-size-reporter nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element-size-reporter", 3 | "version": "0.14.6", 4 | "description": "Reports width, height, and top for selected DOM elements", 5 | "main": "dist/index.js", 6 | "exports": { 7 | "require": "./dist/index.js", 8 | "import": "./dist/index.mjs", 9 | "default": "./dist/index.js" 10 | }, 11 | "scripts": { 12 | "test": "mocha src/tests/unit --no-experimental-global-navigator --recursive --reporter spec --require @babel/register", 13 | "test:debug": "rimraf output/ && babel ./src -d output -s inline && mocha output/tests/unit --no-experimental-global-navigator --recursive --reporter spec --inspect-brk", 14 | "test:cover": "c8 -- npm test", 15 | "build": "rimraf ./dist && babel src/lib/index.js -o ./dist/index.js && node -e 'require(\"fs\").copyFileSync(\"src/lib/index.js\",\"./dist/index.mjs\");'", 16 | "lint": "eslint .", 17 | "prepublishOnly": "npm run build", 18 | "validate": "npm ls" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/localnerve/element-size-reporter.git" 23 | }, 24 | "keywords": [ 25 | "react", 26 | "size", 27 | "position", 28 | "width", 29 | "height", 30 | "top", 31 | "resize", 32 | "cloudinary" 33 | ], 34 | "browserslist": [ 35 | "defaults" 36 | ], 37 | "author": "Alex Grant (@localnerve)", 38 | "license": "BSD-3-Clause", 39 | "bugs": { 40 | "url": "https://github.com/localnerve/element-size-reporter/issues" 41 | }, 42 | "homepage": "https://github.com/localnerve/element-size-reporter#readme", 43 | "pre-commit": [ 44 | "test" 45 | ], 46 | "devDependencies": { 47 | "@babel/cli": "^7.25.9", 48 | "@babel/core": "^7.26.0", 49 | "@babel/eslint-parser": "7.25.9", 50 | "@babel/preset-env": "^7.26.0", 51 | "@babel/preset-react": "^7.25.9", 52 | "@babel/register": "^7.25.9", 53 | "c8": "^10.1.2", 54 | "cross-env": "^7.0.3", 55 | "eslint": "^8.57.1", 56 | "eslint-plugin-react": "^7.37.2", 57 | "jsdom": "^25.0.1", 58 | "lodash": "^4.17.21", 59 | "mocha": "^10.8.2", 60 | "precommit-hook": "^3.0.0", 61 | "react": "^18.3.1", 62 | "react-dom": "^18.3.1", 63 | "rimraf": "^5.0.10" 64 | }, 65 | "dependencies": {}, 66 | "engines": { 67 | "node": ">= 18" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/tests/fixtures/Simple.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2024 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * React usage example and test fixture. 6 | */ 7 | /* eslint-env browser */ 8 | import React from 'react'; 9 | import debounce from 'lodash/debounce'; 10 | import createSizeReporter from '../../lib/index.js'; 11 | 12 | let testReporter; 13 | function reporter (data) { 14 | if (testReporter) { 15 | testReporter(data); 16 | } 17 | } 18 | 19 | export function setMockReporter (reporter) { 20 | testReporter = reporter; 21 | } 22 | 23 | export class Simple extends React.Component { 24 | constructor (props) { 25 | super(props); 26 | 27 | this.state = { 28 | action: false 29 | }; 30 | 31 | this.executeAction = this.executeAction.bind(this); 32 | } 33 | 34 | /** 35 | * This just toggles a state bool for testing. 36 | * React binds this as written. 37 | * A conceptual placeholder for your flux flow action executor. 38 | * In this function, you would execute a size action. 39 | */ 40 | executeAction (data) { 41 | reporter(data); 42 | this.setState({ 43 | action: true 44 | }, () => { 45 | setTimeout(() => { 46 | if (global.window !== undefined) { 47 | this.setState({ 48 | action: false 49 | }); 50 | } 51 | }, 2000); 52 | }); 53 | } 54 | 55 | /** 56 | * Setup window resize handler and report this component size now 57 | */ 58 | componentDidMount () { 59 | const sizeReporter = createSizeReporter('.contained', this.executeAction, { 60 | reportWidth: true 61 | }); 62 | 63 | this.resizeHandler = debounce(sizeReporter, 100); 64 | window.addEventListener('resize', this.resizeHandler); 65 | 66 | // reportSize now. 67 | setTimeout(sizeReporter, 0); 68 | } 69 | 70 | /** 71 | * Remove the window resize handler. 72 | */ 73 | componentWillUnmount () { 74 | window.removeEventListener('resize', this.resizeHandler); 75 | } 76 | 77 | /** 78 | * render and reflect if executeAction ran. 79 | */ 80 | render () { 81 | return ( 82 |
83 | Simple Test {this.state.action ? 'Action' : ''} 84 |
85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/tests/utils/testdom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2024 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * Start/stop jsdom environment 6 | */ 7 | /* global document */ 8 | import { JSDOM } from 'jsdom'; 9 | 10 | /** 11 | * Wrap the JSDOM api. 12 | * 13 | * @param {String} markup - The markup to init JSDOM with. 14 | * @param {Object} options - The options to init JSDOM with. 15 | * @returns {Object} window, document, and navigator as { win, doc, nav }. 16 | */ 17 | function createJSDOM (markup, options) { 18 | const dom = new JSDOM(markup, options); 19 | return { 20 | win: dom.window, 21 | doc: dom.window.document, 22 | nav: dom.window.navigator 23 | }; 24 | } 25 | 26 | /** 27 | * Shim document, window, and navigator with jsdom if not defined. 28 | * Init document with markup if specified. 29 | * Add globals if specified. 30 | */ 31 | export function domStart (markup, addGlobals) { 32 | if (typeof document !== 'undefined') { 33 | return; 34 | } 35 | 36 | const globalKeys = []; 37 | 38 | const { win, doc, nav } = createJSDOM( 39 | markup || '' 40 | ); 41 | 42 | global.IS_REACT_ACT_ENVIRONMENT = true; 43 | global.document = doc; 44 | global.window = win; 45 | global.navigator = nav; 46 | 47 | if (addGlobals) { 48 | Object.keys(addGlobals).forEach(function (key) { 49 | global.window[key] = addGlobals[key]; 50 | globalKeys.push(key); 51 | }); 52 | } 53 | 54 | return globalKeys; 55 | } 56 | 57 | /** 58 | * Remove globals, stop and delete the window. 59 | */ 60 | export function domStop (globalKeys) { 61 | if (globalKeys) { 62 | globalKeys.forEach(function (key) { 63 | delete global.window[key]; 64 | }); 65 | } 66 | 67 | global.window.close(); 68 | 69 | delete global.document; 70 | delete global.window; 71 | delete global.navigator; 72 | } 73 | 74 | const savedItems = { 75 | window: { 76 | }, 77 | document: { 78 | } 79 | }; 80 | 81 | /** 82 | * patch document and window for the component under test 83 | * 84 | * @returns {Object} contains relevant dom properties to the calling test. 85 | */ 86 | export function mockStart (patch) { 87 | savedItems.window.pageYOffset = global.window.pageYOffset; 88 | savedItems.document.querySelector = global.document.querySelector; 89 | 90 | global.window.pageYOffset = patch.pageYOffset; 91 | global.document.querySelector = patch.querySelector; 92 | 93 | return { 94 | clientTop: global.document.documentElement.clientTop 95 | }; 96 | } 97 | 98 | /** 99 | * restore document and window for the component under test 100 | */ 101 | export function mockEnd () { 102 | global.window.pageYOffset = savedItems.window.pageYOffset; 103 | global.document.querySelector = savedItems.document.querySelector; 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Element Size Reporter 2 | 3 | [![npm version](https://badge.fury.io/js/element-size-reporter.svg)](http://badge.fury.io/js/element-size-reporter) 4 | ![Verify](https://github.com/localnerve/element-size-reporter/workflows/Verify/badge.svg) 5 | [![Coverage Status](https://coveralls.io/repos/localnerve/element-size-reporter/badge.svg?branch=master)](https://coveralls.io/r/localnerve/element-size-reporter?branch=master) 6 | 7 | > Reports rendered size (width, height, also top) for a DOM element or group of elements. 8 | > 9 | > No dependencies. 10 | 11 | Use in a component to report on a DOM element's size. Can use in concert with other components to calculate a region composed of multiple component's DOM elements. This is especially useful for calculating image dimensions that span multiple DOM elements in multiple components. 12 | 13 | ### Use with React/Flux 14 | I use this in a React flux flow to calculate background image dimensions and position spanning multiple components and elements for calls to an image service (Cloudinary) in conjunction with [react-element-size-reporter](https://github.com/localnerve/react-element-size-reporter). 15 | 16 | ## API 17 | 18 | ```javascript 19 | createSizeReporter (selector, reporter, options) 20 | ``` 21 | Returns a size reporter function that creates a [Size Report](#size-report) of the rendered element found for given `selector`. Sends the report to `reporter` when executed. 22 | 23 | ### Parameters 24 | 25 | `selector` - {String} A CSS selector that finds the DOM element to report size on. 26 | 27 | `reporter` - {Function} The function that receives the [Size Report](#size-report). 28 | 29 | `options` - {Object} See [Options](#Options). 30 | 31 | ### Size Report 32 | A single `object` that contains the following properties: 33 | 34 | `width` - {Number} The width of the DOM element selected. 35 | Computed as the difference of the selected element bounding client rect (left from right). 36 | 37 | `height` - {Number} The height of the DOM element selected. 38 | Computed as the difference of the selected element bounding client rect (top from bottom). 39 | 40 | `top` - {Number} The top to the DOM element selected. Computed as: 41 | ```javascript 42 | selectedElement.getBoundingClientRect().top + window.pageYOffset - document.documentElement.clientTop; 43 | ``` 44 | 45 | `accumulate` - {Boolean} True if the data should be combined with previous data sent. 46 | Ignore this flag if you are not using grouped, multiple DOM elements. 47 | 48 | ## Options 49 | 50 | `group` - {String} An identifier that uniquely names a group of size reporters. This allows widths, heights, and/or tops from multiple components to be accumulated. Element Size Reporter tracks these groups and sends along an `accumulate` flag in the report for multiple reporting. Defaults to 'global'. If you don't care about grouping multiple DOM elements, just ignore the `accumulate` flag in the report. 51 | 52 | `reportWidth` - {Boolean} True to have the reporter include width in the report data. 53 | 54 | `reportHeight` - {Boolean} True to have the reporter include height in the report data. 55 | 56 | `reportTop` - {Boolean} True to have the reporter include top in the report data. 57 | 58 | `grow` - {Object} Options that control the arbitrary expansion the reported sizes. Specifically, width and height are increased, top is decreased. Use to reduce/conform image requests, or just otherwise smooth or adjust the reported size. 59 | 60 | `grow.width` - {Number} The nearest multiple to expand the width to. 61 | Example: If this is 10, then 92 gets expanded to 100, the next highest multiple of 10. 62 | 63 | `grow.height` - {Number} The nearest multiple to expand the height to. 64 | 65 | `grow.top` - {Number} The nearest multiple to expand the top to. 66 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2024 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. 4 | * See the accompanying LICENSE file for terms. 5 | * 6 | * Report selected DOM element size and top. 7 | */ 8 | /* eslint-env browser */ 9 | const __DEV__ = process.env.NODE_ENV !== 'production'; 10 | 11 | /*** 12 | * A tracker contains a reporter count and a call count to determine the value 13 | * of the accumulate flag in the report. Each sizeReporter created within a 14 | * group (using the group option) updates that group's tracker. 15 | * This allows mulitple components to contribute to an overall size calculation. 16 | * 17 | * A count of reporters is accumulated each time the factory below is called to 18 | * create a size reporter. 19 | * When all the reporters finish reporting (and initially), 20 | * callCount % reporters === 0. 21 | * This is used to determine if data should be accumulated, which is 22 | * passed along in the report data property: accumulate === true. 23 | * They should be accumulated by the report receiver when reporters have not all 24 | * reported for a given group. 25 | */ 26 | const trackers = Object.create(null); 27 | 28 | /** 29 | * Round a number to nearest multiple according to rules. 30 | * Default multiple is 1.0. Supply nearest multiple in rules.grow parameter. 31 | * 32 | * Rounding rules: 33 | * type === 'top' then use floor rounding. 34 | * otherwise, use ceiling rounding. 35 | * 36 | * @param {Number} value - The raw value to round. 37 | * @param {Object} [rules] - Rounding rules. If omitted, just uses Math.round. 38 | * If supplied, must at least specify rules.type. 39 | * @param {String} [rules.type] - Can be one of 'top', 'width', or 'height'. 40 | * @param {Object} [rules.grow] - Options that control padding adjustments. 41 | * @param {Number} [rules.grow.top] - Nearest multiple to grow to. Used if 42 | * matches given type parameter. 43 | * @param {Number} [rules.grow.width] - Nearest multiple to grow to. Used if 44 | * matches given type parameter. 45 | * @param {Number} [rules.grow.height] - Nearest multiple to grow to. Used if 46 | * matches given type parameter. 47 | * @returns {Number} The rounded value according to rules derived from type and 48 | * grow. 49 | */ 50 | function round (value, rules) { 51 | let unit = 1.0, roundOp = Math.round; 52 | 53 | if (rules) { 54 | roundOp = Math[rules.type === 'top' ? 'floor' : 'ceil']; 55 | if ('object' === typeof rules.grow) { 56 | unit = rules.grow[rules.type] || 1.0; 57 | } 58 | } 59 | 60 | return roundOp(value / unit) * unit; 61 | } 62 | 63 | /** 64 | * Factory to create a function to report selected DOM element size and top. 65 | * 66 | * @param {String} selector - The selector used to find the DOM element to 67 | * report on. 68 | * @param {Function} reporter - The function called to report size updates. 69 | * @param {Object} [options] - Options to control what gets reported. 70 | * @param {String} [options.group] - An arbitrary group name under which size 71 | * reporters are tracked. This accumulate flag in the report is determined by 72 | * reporters in a group. Defaults to 'global'. 73 | * @param {Boolean} [options.reportWidth] - True to report width. 74 | * @param {Boolean} [options.reportHeight] - True to report height. 75 | * @param {Boolean} [options.reportTop] - True to report top. 76 | * @param {Object} [options.grow] - Options to control size inflations. 77 | * @param {Number} [options.grow.top] - Nearest multiple to grow size to. 78 | * @param {Number} [options.grow.width] - Nearest multiple grow size to. 79 | * @param {Number} [options.grow.height] - Nearest multiple to grow size to. 80 | * @returns {Function} The sizeReporter function, reports on DOM element found 81 | * at `selector`. 82 | */ 83 | export default 84 | function createSizeReporter (selector, reporter, options) { 85 | options = options || {}; 86 | options.group = options.group || 'global'; 87 | 88 | if (__DEV__) { 89 | if (options._clearTrackers) { 90 | Object.keys(trackers).forEach((key) => { 91 | delete trackers[key]; 92 | }); 93 | return; 94 | } 95 | } 96 | 97 | if (!selector) { 98 | throw new Error( 99 | 'Invalid selector supplied to createSizeReporter.' 100 | ); 101 | } 102 | 103 | if (typeof reporter !== 'function') { 104 | throw new Error ( 105 | 'Invalid reporter function supplied to createSizeReporter' 106 | ); 107 | } 108 | 109 | if (!trackers[options.group]) { 110 | trackers[options.group] = { 111 | reporters: 0, 112 | callCount: 0 113 | }; 114 | } 115 | 116 | const tracker = trackers[options.group]; 117 | 118 | tracker.reporters++; 119 | 120 | /** 121 | * Report the size of the DOM element found at `selector`. 122 | * Rounding is used on clientRect to reduce the differences and optionally 123 | * grow the reported sizes if requested. 124 | */ 125 | return function sizeReporter () { 126 | let width, height, top; 127 | 128 | const el = document.querySelector(selector); 129 | const rect = el ? el.getBoundingClientRect() : { 130 | top: 0, 131 | right: 0, 132 | bottom: 0, 133 | left: 0 134 | }; 135 | 136 | if (options.reportWidth) { 137 | width = round(rect.right - rect.left, { 138 | type: 'width', 139 | grow: options.grow 140 | }); 141 | } 142 | 143 | if (options.reportTop) { 144 | top = round( 145 | rect.top + window.pageYOffset - document.documentElement.clientTop, { 146 | type: 'top', 147 | grow: options.grow 148 | } 149 | ); 150 | } 151 | 152 | if (options.reportHeight) { 153 | height = round(rect.bottom - rect.top, { 154 | type: 'height', 155 | grow: options.grow 156 | }); 157 | } 158 | 159 | reporter({ 160 | width, 161 | height, 162 | top, 163 | accumulate: tracker.callCount % tracker.reporters !== 0 164 | }); 165 | 166 | tracker.callCount++; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/tests/unit/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2024 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | /* eslint-env browser */ 6 | /* global before, beforeEach, after, afterEach, describe, it */ 7 | import * as assert from 'node:assert'; 8 | import React from 'react'; 9 | import ReactDOM from 'react-dom/client'; 10 | import { act } from 'react-dom/test-utils'; 11 | import { domStart, domStop, mockStart, mockEnd } from '../utils/testdom.js'; 12 | import { Simple, setMockReporter } from '../fixtures/Simple.jsx'; 13 | import createSizeReporter from '../../lib/index.js'; 14 | 15 | describe('sizeReporter', () => { 16 | 17 | before('sizeReporter', () => { 18 | domStart(); 19 | }); 20 | 21 | after('sizeReporter', () => { 22 | domStop(); 23 | }); 24 | 25 | describe('structure', () => { 26 | it('should report and have expected report items with correct types', 27 | (done) => { 28 | const reporter = createSizeReporter('.nothing', (data) => { 29 | assert.equal(typeof data.width, 'number'); 30 | assert.equal(typeof data.height, 'number'); 31 | assert.equal(typeof data.top, 'number'); 32 | assert.equal(typeof data.accumulate, 'boolean'); 33 | done(); 34 | }, { 35 | reportWidth: true, 36 | reportHeight: true, 37 | reportTop: true 38 | }); 39 | 40 | reporter(); 41 | }); 42 | }); 43 | 44 | describe('bad args', () => { 45 | it('should throw if no selector supplied', () => { 46 | assert.throws(function () { 47 | createSizeReporter('', () => {}); 48 | }); 49 | }); 50 | 51 | it('should throw if no reporter function supplied', () => { 52 | assert.throws(function () { 53 | createSizeReporter('.nothing', 'bad'); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('react', () => { 59 | let container; 60 | 61 | beforeEach(() => { 62 | // DOM is already started 63 | container = document.createElement('div'); 64 | document.body.appendChild(container); 65 | 66 | // Clear the group trackers 67 | createSizeReporter(null, null, { 68 | _clearTrackers: true 69 | }); 70 | }); 71 | 72 | afterEach(() => { 73 | // DOM is already started 74 | document.body.removeChild(container); 75 | container = null; 76 | 77 | setMockReporter(null); 78 | }); 79 | 80 | it('should render and execute action', async () => { 81 | let result; 82 | await act(() => { 83 | const element = React.createElement(Simple); 84 | ReactDOM.createRoot(container).render(element); 85 | }); 86 | 87 | await act(() => { 88 | result = document.querySelector('.contained'); 89 | assert.match(result.textContent, /Simple Test/); 90 | }); 91 | 92 | return new Promise(res => { 93 | setTimeout(() => { 94 | // Action test. Executes on componentDidMount so action should've run. 95 | assert.match(result.textContent, /Action/); 96 | res(); 97 | }, 250); 98 | }); 99 | }); 100 | 101 | it('should accumulate if multiple reporters in same group', async () => { 102 | const reporters = 2; 103 | let reportCall = 0; 104 | 105 | const result = new Promise(res => { 106 | /** 107 | * @param {Object} data - the report data from size reporter under test. 108 | */ 109 | function handleReport (data) { 110 | if (reportCall === 0) { 111 | // First time, you want to overwrite the data. 112 | assert.equal(data.accumulate, false); 113 | } else { 114 | // After that, you're accumulating. 115 | assert.equal(data.accumulate, true); 116 | } 117 | 118 | reportCall++; 119 | if (reportCall === reporters) { 120 | res(); 121 | } 122 | } 123 | 124 | setMockReporter(handleReport); 125 | }); 126 | 127 | await act(() => { 128 | const children = [Simple, Simple].map((childClass, index) => { 129 | return React.createElement(childClass, { key: index }); 130 | }); 131 | const element = React.createElement('div', {}, children); 132 | ReactDOM.createRoot(container).render(element); 133 | }); 134 | 135 | return act(() => result); 136 | }); 137 | }); 138 | 139 | describe('mocked tests', () => { 140 | const top = 202.2, right = 600, bottom = 600, left = 202.2, 141 | pageYOffset = 200; 142 | let domProps; 143 | 144 | function mockQuerySelector () { 145 | return { 146 | getBoundingClientRect: function () { 147 | return { 148 | top, 149 | right, 150 | bottom, 151 | left 152 | }; 153 | } 154 | }; 155 | } 156 | 157 | before('mocks', () => { 158 | domProps = mockStart({ 159 | querySelector: mockQuerySelector, 160 | pageYOffset 161 | }); 162 | }); 163 | 164 | after('mocks', () => { 165 | mockEnd(); 166 | }); 167 | 168 | beforeEach(() => { 169 | // Clear the group trackers 170 | createSizeReporter(null, null, { 171 | _clearTrackers: true 172 | }); 173 | }); 174 | 175 | it('should compute values as expected', (done) => { 176 | const reporter = createSizeReporter('.mock', (data) => { 177 | assert.equal(data.width, Math.round(right - left)); 178 | assert.equal(data.height, Math.round(bottom - top)); 179 | assert.equal(data.top, Math.round( 180 | top + (pageYOffset - domProps.clientTop) 181 | )); 182 | assert.equal(data.accumulate, false); 183 | done(); 184 | }, { 185 | reportWidth: true, 186 | reportHeight: true, 187 | reportTop: true 188 | }); 189 | 190 | reporter(); 191 | }); 192 | 193 | it('should grow width on multiple as expected', (done) => { 194 | const multiple = 10; 195 | 196 | const reporter = createSizeReporter('.mock', (data) => { 197 | assert.equal(data.width, 198 | Math.ceil((right - left)/multiple) * multiple 199 | ); 200 | done(); 201 | }, { 202 | reportWidth: true, 203 | grow: { 204 | width: multiple 205 | } 206 | }); 207 | 208 | reporter(); 209 | }); 210 | 211 | it('should grow top on multiple as expected', (done) => { 212 | const multiple = 10; 213 | 214 | const reporter = createSizeReporter('.mock', (data) => { 215 | assert.equal(data.top, 216 | Math.floor((top + (pageYOffset - domProps.clientTop))/multiple) * multiple 217 | ); 218 | assert.equal((data.top - (pageYOffset - domProps.clientTop)) < top, true); 219 | done(); 220 | }, { 221 | reportTop: true, 222 | grow: { 223 | top: multiple 224 | } 225 | }); 226 | 227 | reporter(); 228 | }); 229 | 230 | it('should handle missing grow option with round', (done) => { 231 | const reporter = createSizeReporter('.mock', (data) => { 232 | assert.equal(data.width, 233 | Math.round(right - left) 234 | ); 235 | done(); 236 | }, { 237 | reportWidth: true 238 | }); 239 | 240 | reporter(); 241 | }); 242 | 243 | it('should handle missing grow option prop without round', (done) => { 244 | const reporter = createSizeReporter('.mock', (data) => { 245 | assert.equal(data.width, 246 | Math.ceil(right - left) 247 | ); 248 | done(); 249 | }, { 250 | reportWidth: true, 251 | grow: {} 252 | }); 253 | 254 | reporter(); 255 | }); 256 | }); 257 | }); 258 | --------------------------------------------------------------------------------