├── .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 | [](http://badge.fury.io/js/element-size-reporter)
4 | 
5 | [](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 |
--------------------------------------------------------------------------------