get-browser-fingerprint
Advanced tools
Comparing version 2.0.1 to 2.1.1
{ | ||
"name": "get-browser-fingerprint", | ||
"version": "2.0.1", | ||
"author": "Damiano Barbati <damiano.barbati@gmail.com> (http://github.com/damianobarbati)", | ||
"repository": "https://github.com/damianobarbati/get-browser-fingerprint", | ||
"license": "MIT", | ||
"main": "src/index.js", | ||
"type": "module", | ||
"scripts": { | ||
"eslint": "eslint --ignore-path .gitignore", | ||
"prettier": "prettier --ignore-unknown", | ||
"test": "node src/index.spec.js || echo 'test failed'" | ||
}, | ||
"devDependencies": { | ||
"@babel/eslint-parser": "^7.13.14", | ||
"eslint": "^7.26.0", | ||
"eslint-config-prettier": "^8.1.0", | ||
"eslint-plugin-editorconfig": "^3.0.2", | ||
"eslint-plugin-prettier": "^3.3.1", | ||
"http-server": "^0.12.3", | ||
"prettier": "^2.2.1", | ||
"puppeteer": "^9.1.1" | ||
} | ||
"name": "get-browser-fingerprint", | ||
"version": "2.1.1", | ||
"author": "Damiano Barbati <damiano.barbati@gmail.com> (https://github.com/damianobarbati)", | ||
"repository": "https://github.com/damianobarbati/get-browser-fingerprint", | ||
"license": "MIT", | ||
"main": "src/index.js", | ||
"type": "module", | ||
"scripts": { | ||
"eslint": "eslint --ignore-path .gitignore --fix", | ||
"test": "NODE_OPTIONS='--experimental-vm-modules' jest --runInBand --no-cache" | ||
}, | ||
"devDependencies": { | ||
"eslint-config-xs": "^1.3.0", | ||
"http-server": "^14.1.1", | ||
"jest": "^28.1.3", | ||
"puppeteer": "^16.1.1" | ||
} | ||
} |
# get-browser-fingerprint | ||
Zero dependencies package exporting a single function which computes a browser fingerprint. | ||
Zero dependencies package exporting a single, fast (<15ms) and synchronous function which computes a browser fingerprint, without requiring any permission to the user. | ||
@@ -8,3 +8,3 @@ ## Usage | ||
Get browser fingerprint: | ||
```javascript | ||
```js | ||
import getBrowserFingerprint from 'get-browser-fingerprint'; | ||
@@ -16,8 +16,20 @@ const fingerprint = getBrowserFingerprint(); | ||
Options available: | ||
- `enableWebgl`: enable webgl renderer, 5x times slower but deadly powerful (default `false`) | ||
- `debug`: log data used to generate fingerprint to console (default `false`) | ||
- `hardwareOnly` (default `false`): leverage only hardware info about device | ||
- `enableWebgl` (default `false`): enable webgl renderer, ~4x times slower but adds another deadly powerful hardware detection layer on top of canvas | ||
- `debug`: log data used to generate fingerprint to console and add canvas/webgl canvas to body to see rendered image (default `false`) | ||
## Disclaimer | ||
⚠️ Be careful: the strongest discriminating factor is canvas token which can't be computed on old devices (eg: iPhone 6), deal accordingly ⚠️ | ||
Be careful: | ||
- strongest discriminating factor is canvas token which can't be computed on old devices (eg: iPhone 6) | ||
## Development | ||
To test locally: | ||
```sh | ||
nvm install | ||
yarn install | ||
yarn test | ||
``` | ||
To run example locally: | ||
```sh | ||
yarn http-server src -o -c-1 -p 80 | ||
``` |
347
src/index.js
@@ -1,237 +0,248 @@ | ||
export default ({ enableWebgl = false, debug = false } = {}) => { | ||
let { devicePixelRatio } = window; | ||
// weird behaviour when getting value from localhost vs ip!!! | ||
devicePixelRatio = +parseInt(devicePixelRatio); | ||
const getBrowserFingerprint = ({ hardwareOnly = false, enableWebgl = false, debug = false } = {}) => { | ||
const devicePixelRatio = +parseInt(window.devicePixelRatio); | ||
const { | ||
appName, | ||
appCodeName, | ||
appVersion, | ||
cookieEnabled, | ||
const { | ||
appName, | ||
appCodeName, | ||
appVersion, | ||
cookieEnabled, | ||
deviceMemory, | ||
doNotTrack, | ||
hardwareConcurrency, | ||
language, | ||
languages, | ||
maxTouchPoints, | ||
platform, | ||
product, | ||
productSub, | ||
userAgent, | ||
vendor, | ||
vendorSub, | ||
} = window.navigator; | ||
const { width, height, colorDepth, pixelDepth } = window.screen; | ||
const timezoneOffset = new Date().getTimezoneOffset(); | ||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; | ||
const touchSupport = 'ontouchstart' in window; | ||
const canvas = getCanvasID(debug); | ||
const webgl = enableWebgl ? getWebglID(debug) : undefined; // undefined will remove this from the stringify down here | ||
const webglInfo = enableWebgl ? getWebglInfo(debug) : undefined; // undefined will remove this from the stringify down here | ||
const data = hardwareOnly | ||
? JSON.stringify({ | ||
canvas, | ||
colorDepth, | ||
deviceMemory, | ||
doNotTrack, | ||
devicePixelRatio, | ||
hardwareConcurrency, | ||
language, | ||
languages, | ||
height, | ||
maxTouchPoints, | ||
pixelDepth, | ||
platform, | ||
product, | ||
productSub, | ||
userAgent, | ||
vendor, | ||
vendorSub, | ||
webdriver, | ||
} = window.navigator; | ||
const plugins = Object.entries(window.navigator.plugins).map(([, plugin]) => plugin.name); | ||
const mimeTypes = Object.entries(window.navigator.mimeTypes).map(([, mimeType]) => mimeType.type); | ||
const { width, height, colorDepth, pixelDepth } = window.screen; | ||
const timezoneOffset = new Date().getTimezoneOffset(); | ||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; | ||
const touchSupport = 'ontouchstart' in window; | ||
const canvas = getCanvasID(debug); | ||
const webgl = enableWebgl ? getWebglID(debug) : null; | ||
const webglInfo = getWebglInfo(debug); | ||
const data = { | ||
devicePixelRatio, | ||
touchSupport, | ||
webgl, | ||
webglInfo, | ||
width, | ||
}) | ||
: JSON.stringify({ | ||
appCodeName, | ||
appName, | ||
appCodeName, | ||
appVersion, | ||
canvas, | ||
colorDepth, | ||
cookieEnabled, | ||
deviceMemory, | ||
devicePixelRatio, | ||
doNotTrack, | ||
hardwareConcurrency, | ||
height, | ||
language, | ||
languages, | ||
maxTouchPoints, | ||
mimeTypes, | ||
pixelDepth, | ||
platform, | ||
plugins, | ||
product, | ||
productSub, | ||
timezone, | ||
timezoneOffset, | ||
touchSupport, | ||
userAgent, | ||
vendor, | ||
vendorSub, | ||
webdriver, | ||
width, | ||
height, | ||
colorDepth, | ||
pixelDepth, | ||
timezoneOffset, | ||
timezone, | ||
touchSupport, | ||
canvas, | ||
webgl, | ||
webglInfo, | ||
}; | ||
width, | ||
}); | ||
const datastring = JSON.stringify(data, null, 4); | ||
const datastring = JSON.stringify(data, null, 4); | ||
if (debug) console.log('fingerprint data', datastring); | ||
if (debug) console.log('fingerprint data', datastring); | ||
const result = murmurhash3_32_gc(datastring); | ||
return result; | ||
const result = murmurhash3_32_gc(datastring); | ||
return result; | ||
}; | ||
export const getCanvasID = (debug) => { | ||
try { | ||
const canvas = document.createElement('canvas'); | ||
const ctx = canvas.getContext('2d'); | ||
const text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~1!2@3#4$5%6^7&8*9(0)-_=+[{]}|;:',<.>/?"; | ||
ctx.textBaseline = 'top'; | ||
ctx.font = "14px 'Arial'"; | ||
ctx.textBaseline = 'alphabetic'; | ||
ctx.fillStyle = '#f60'; | ||
ctx.fillRect(125, 1, 62, 20); | ||
ctx.fillStyle = '#069'; | ||
ctx.fillText(text, 2, 15); | ||
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; | ||
ctx.fillText(text, 4, 17); | ||
try { | ||
const canvas = document.createElement('canvas'); | ||
const ctx = canvas.getContext('2d'); | ||
const text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~1!2@3#4$5%6^7&8*9(0)-_=+[{]}|;:',<.>/?"; | ||
ctx.textBaseline = 'top'; | ||
ctx.font = "14px 'Arial'"; | ||
ctx.textBaseline = 'alphabetic'; | ||
ctx.fillStyle = '#f60'; | ||
ctx.fillRect(125, 1, 62, 20); | ||
ctx.fillStyle = '#069'; | ||
ctx.fillText(text, 2, 15); | ||
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; | ||
ctx.fillText(text, 4, 17); | ||
const result = canvas.toDataURL(); | ||
const result = canvas.toDataURL(); | ||
if (debug) { | ||
document.body.appendChild(canvas); | ||
} else { | ||
ctx.clearRect(0, 0, canvas.width, canvas.height); | ||
} | ||
if (debug) { | ||
document.body.appendChild(canvas); | ||
} else { | ||
ctx.clearRect(0, 0, canvas.width, canvas.height); | ||
} | ||
return murmurhash3_32_gc(result); | ||
} catch { | ||
return null; | ||
} | ||
return murmurhash3_32_gc(result); | ||
} catch { | ||
return null; | ||
} | ||
}; | ||
export const getWebglID = (debug) => { | ||
try { | ||
const canvas = document.createElement('canvas'); | ||
const ctx = canvas.getContext('webgl'); | ||
canvas.width = 256; | ||
canvas.height = 128; | ||
try { | ||
const canvas = document.createElement('canvas'); | ||
const ctx = canvas.getContext('webgl'); | ||
canvas.width = 256; | ||
canvas.height = 128; | ||
const f = 'attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}'; | ||
const g = 'precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}'; | ||
const h = ctx.createBuffer(); | ||
const f = | ||
'attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}'; | ||
const g = 'precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}'; | ||
const h = ctx.createBuffer(); | ||
ctx.bindBuffer(ctx.ARRAY_BUFFER, h); | ||
ctx.bindBuffer(ctx.ARRAY_BUFFER, h); | ||
const i = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.7321, 0]); | ||
const i = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.7321, 0]); | ||
ctx.bufferData(ctx.ARRAY_BUFFER, i, ctx.STATIC_DRAW), (h.itemSize = 3), (h.numItems = 3); | ||
ctx.bufferData(ctx.ARRAY_BUFFER, i, ctx.STATIC_DRAW), (h.itemSize = 3), (h.numItems = 3); | ||
const j = ctx.createProgram(); | ||
const k = ctx.createShader(ctx.VERTEX_SHADER); | ||
const j = ctx.createProgram(); | ||
const k = ctx.createShader(ctx.VERTEX_SHADER); | ||
ctx.shaderSource(k, f); | ||
ctx.compileShader(k); | ||
ctx.shaderSource(k, f); | ||
ctx.compileShader(k); | ||
const l = ctx.createShader(ctx.FRAGMENT_SHADER); | ||
const l = ctx.createShader(ctx.FRAGMENT_SHADER); | ||
ctx.shaderSource(l, g); | ||
ctx.compileShader(l); | ||
ctx.attachShader(j, k); | ||
ctx.attachShader(j, l); | ||
ctx.linkProgram(j); | ||
ctx.useProgram(j); | ||
ctx.shaderSource(l, g); | ||
ctx.compileShader(l); | ||
ctx.attachShader(j, k); | ||
ctx.attachShader(j, l); | ||
ctx.linkProgram(j); | ||
ctx.useProgram(j); | ||
j.vertexPosAttrib = ctx.getAttribLocation(j, 'attrVertex'); | ||
j.offsetUniform = ctx.getUniformLocation(j, 'uniformOffset'); | ||
j.vertexPosAttrib = ctx.getAttribLocation(j, 'attrVertex'); | ||
j.offsetUniform = ctx.getUniformLocation(j, 'uniformOffset'); | ||
ctx.enableVertexAttribArray(j.vertexPosArray); | ||
ctx.vertexAttribPointer(j.vertexPosAttrib, h.itemSize, ctx.FLOAT, !1, 0, 0); | ||
ctx.uniform2f(j.offsetUniform, 1, 1); | ||
ctx.drawArrays(ctx.TRIANGLE_STRIP, 0, h.numItems); | ||
ctx.enableVertexAttribArray(j.vertexPosArray); | ||
ctx.vertexAttribPointer(j.vertexPosAttrib, h.itemSize, ctx.FLOAT, !1, 0, 0); | ||
ctx.uniform2f(j.offsetUniform, 1, 1); | ||
ctx.drawArrays(ctx.TRIANGLE_STRIP, 0, h.numItems); | ||
const n = new Uint8Array(canvas.width * canvas.height * 4); | ||
ctx.readPixels(0, 0, canvas.width, canvas.height, ctx.RGBA, ctx.UNSIGNED_BYTE, n); | ||
const n = new Uint8Array(canvas.width * canvas.height * 4); | ||
ctx.readPixels(0, 0, canvas.width, canvas.height, ctx.RGBA, ctx.UNSIGNED_BYTE, n); | ||
const result = JSON.stringify(n).replace(/,?"[0-9]+":/g, ''); | ||
const result = JSON.stringify(n).replace(/,?"[0-9]+":/g, ''); | ||
if (debug) { | ||
document.body.appendChild(canvas); | ||
} else { | ||
ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT | ctx.STENCIL_BUFFER_BIT); | ||
} | ||
if (debug) { | ||
document.body.appendChild(canvas); | ||
} else { | ||
ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT | ctx.STENCIL_BUFFER_BIT); | ||
} | ||
return murmurhash3_32_gc(result); | ||
} catch { | ||
return null; | ||
} | ||
return murmurhash3_32_gc(result); | ||
} catch { | ||
return null; | ||
} | ||
}; | ||
export const getWebglInfo = () => { | ||
try { | ||
const ctx = document.createElement('canvas').getContext('webgl'); | ||
try { | ||
const ctx = document.createElement('canvas').getContext('webgl'); | ||
const result = { | ||
VERSION: ctx.getParameter(ctx.VERSION), | ||
SHADING_LANGUAGE_VERSION: ctx.getParameter(ctx.SHADING_LANGUAGE_VERSION), | ||
VENDOR: ctx.getParameter(ctx.VENDOR), | ||
SUPORTED_EXTENSIONS: ctx.getSupportedExtensions(), | ||
}; | ||
const result = { | ||
VERSION: ctx.getParameter(ctx.VERSION), | ||
SHADING_LANGUAGE_VERSION: ctx.getParameter(ctx.SHADING_LANGUAGE_VERSION), | ||
VENDOR: ctx.getParameter(ctx.VENDOR), | ||
SUPORTED_EXTENSIONS: ctx.getSupportedExtensions(), | ||
}; | ||
return result; | ||
} catch { | ||
return null; | ||
} | ||
return result; | ||
} catch { | ||
return null; | ||
} | ||
}; | ||
export const murmurhash3_32_gc = (key) => { | ||
const remainder = key.length & 3; // key.length % 4 | ||
const bytes = key.length - remainder; | ||
const c1 = 0xcc9e2d51; | ||
const c2 = 0x1b873593; | ||
const remainder = key.length & 3; // key.length % 4 | ||
const bytes = key.length - remainder; | ||
const c1 = 0xcc9e2d51; | ||
const c2 = 0x1b873593; | ||
let h1, h1b, k1; | ||
let h1, h1b, k1; | ||
for (let i = 0; i < bytes; i++) { | ||
k1 = (key.charCodeAt(i) & 0xff) | ((key.charCodeAt(++i) & 0xff) << 8) | ((key.charCodeAt(++i) & 0xff) << 16) | ((key.charCodeAt(++i) & 0xff) << 24); | ||
++i; | ||
for (let i = 0; i < bytes; i++) { | ||
k1 = (key.charCodeAt(i) & 0xff) | ((key.charCodeAt(++i) & 0xff) << 8) | ((key.charCodeAt(++i) & 0xff) << 16) | ((key.charCodeAt(++i) & 0xff) << 24); | ||
++i; | ||
k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; | ||
k1 = (k1 << 15) | (k1 >>> 17); | ||
k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; | ||
k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; | ||
k1 = (k1 << 15) | (k1 >>> 17); | ||
k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; | ||
h1 ^= k1; | ||
h1 = (h1 << 13) | (h1 >>> 19); | ||
h1b = ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff; | ||
h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16); | ||
} | ||
h1 ^= k1; | ||
h1 = (h1 << 13) | (h1 >>> 19); | ||
h1b = ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff; | ||
h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16); | ||
} | ||
const i = bytes - 1; | ||
const i = bytes - 1; | ||
k1 = 0; | ||
k1 = 0; | ||
switch (remainder) { | ||
case 3: { | ||
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; | ||
break; | ||
} | ||
case 2: { | ||
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; | ||
break; | ||
} | ||
case 1: { | ||
k1 ^= key.charCodeAt(i) & 0xff; | ||
break; | ||
} | ||
switch (remainder) { | ||
case 3: { | ||
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; | ||
break; | ||
} | ||
case 2: { | ||
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; | ||
break; | ||
} | ||
case 1: { | ||
k1 ^= key.charCodeAt(i) & 0xff; | ||
break; | ||
} | ||
} | ||
k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; | ||
k1 = (k1 << 15) | (k1 >>> 17); | ||
k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; | ||
h1 ^= k1; | ||
k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; | ||
k1 = (k1 << 15) | (k1 >>> 17); | ||
k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; | ||
h1 ^= k1; | ||
h1 ^= key.length; | ||
h1 ^= key.length; | ||
h1 ^= h1 >>> 16; | ||
h1 = ((h1 & 0xffff) * 0x85ebca6b + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; | ||
h1 ^= h1 >>> 13; | ||
h1 = ((h1 & 0xffff) * 0xc2b2ae35 + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 0xffffffff; | ||
h1 ^= h1 >>> 16; | ||
h1 ^= h1 >>> 16; | ||
h1 = ((h1 & 0xffff) * 0x85ebca6b + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; | ||
h1 ^= h1 >>> 13; | ||
h1 = ((h1 & 0xffff) * 0xc2b2ae35 + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 0xffffffff; | ||
h1 ^= h1 >>> 16; | ||
return h1 >>> 0; | ||
return h1 >>> 0; | ||
}; | ||
window.getBrowserFingerprint = getBrowserFingerprint; | ||
export default getBrowserFingerprint; |
@@ -1,28 +0,52 @@ | ||
import { strict as assert } from 'assert'; | ||
import puppeteer from 'puppeteer'; | ||
import getBrowserFingerprint from './index.js'; | ||
(async () => { | ||
await (async () => { | ||
const browser = await puppeteer.launch(); | ||
const page = await browser.newPage(); | ||
const result = await page.evaluate(getBrowserFingerprint); | ||
await browser.close(); | ||
describe('getBrowserFingerprint', () => { | ||
let browser, page; | ||
assert.deepStrictEqual(Number.isInteger(result), true, 'fingerprint is not an integer'); | ||
assert.deepStrictEqual(String(result).length > 7, true, 'fingerprint is not long enough'); | ||
})(); | ||
beforeAll(async () => { | ||
browser = await puppeteer.launch({ | ||
// headless: false, | ||
// devtools: true, | ||
}); | ||
page = await browser.newPage(); | ||
await (async () => { | ||
const browser = await puppeteer.launch(); | ||
const page = await browser.newPage(); | ||
const result = await page.evaluate(getBrowserFingerprint, true); | ||
await browser.close(); | ||
await page.addScriptTag({ | ||
type: 'module', | ||
path: './src/index.js', | ||
}); | ||
}); | ||
assert.deepStrictEqual(Number.isInteger(result), true, 'fingerprint is not an integer'); | ||
assert.deepStrictEqual(String(result).length > 7, true, 'fingerprint is not long enough'); | ||
})(); | ||
})() | ||
.then(console.log) | ||
.catch(console.error) | ||
.finally(process.exit); | ||
afterAll(async () => { | ||
await browser.close(); | ||
}); | ||
it('works without args', async () => { | ||
const result = await page.evaluate(() => { | ||
const result = window.getBrowserFingerprint(); | ||
return result; | ||
}); | ||
expect(typeof result).toBe('number'); | ||
expect(String(result).length).toBeGreaterThanOrEqual(7); | ||
}); | ||
it('works without hardwareOnly=true', async () => { | ||
const result = await page.evaluate(() => { | ||
const result = window.getBrowserFingerprint(); | ||
return result; | ||
}); | ||
expect(typeof result).toBe('number'); | ||
expect(String(result).length).toBeGreaterThanOrEqual(7); | ||
}); | ||
it('works with enableWebgl=true', async () => { | ||
const result = await page.evaluate(() => { | ||
const result = window.getBrowserFingerprint({ enableWebgl: true }); | ||
return result; | ||
}); | ||
expect(typeof result).toBe('number'); | ||
expect(String(result).length).toBeGreaterThanOrEqual(7); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
13921
4
13
255
33