fix(tracker): fix breaking q sender test
This commit is contained in:
parent
5ecd7b86f8
commit
4e3332304e
5 changed files with 41 additions and 667 deletions
21
tracker/tracker/src/main/vendors/finder/LICENSE
vendored
21
tracker/tracker/src/main/vendors/finder/LICENSE
vendored
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018–2020 Anton Medvedev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
132
tracker/tracker/src/main/vendors/finder/README.md
vendored
132
tracker/tracker/src/main/vendors/finder/README.md
vendored
|
|
@ -1,132 +0,0 @@
|
|||

|
||||
|
||||
# finder
|
||||
|
||||
[](https://www.npmjs.com/package/@medv/finder)
|
||||
[](https://travis-ci.org/antonmedv/finder)
|
||||
[](https://bundlephobia.com/result?p=@medv/finder)
|
||||
|
||||
> CSS Selector Generator
|
||||
|
||||
## Features
|
||||
|
||||
* Generates **shortest** selectors
|
||||
* **Unique** selectors per page
|
||||
* Stable and **robust** selectors
|
||||
* **2.1 kB** gzip and minify size
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @medv/finder
|
||||
```
|
||||
|
||||
Finder can be used via modules:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import {finder} from 'https://medv.io/finder/finder.js'
|
||||
</script>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import {finder} from '@medv/finder'
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
const selector = finder(event.target)
|
||||
console.log(selector)
|
||||
})
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Example of generated selector:
|
||||
|
||||
```css
|
||||
.blog > article:nth-child(3) .add-comment
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`finder` takes configuration object as second parameters. Here is example of all params with default values:
|
||||
|
||||
```js
|
||||
const selector = finder(event.target, {
|
||||
root: document.body,
|
||||
className: (name) => true,
|
||||
tagName: (name) => true,
|
||||
attr: (name, value) => false,
|
||||
seedMinLength: 1,
|
||||
optimizedMinLength: 2,
|
||||
threshold: 1000,
|
||||
maxNumberOfTries: 10_000,
|
||||
})
|
||||
```
|
||||
|
||||
#### `root: Element`
|
||||
|
||||
Root of search, defaults to `document.body`.
|
||||
|
||||
#### `idName: (name: string) => boolean`
|
||||
|
||||
Check if this ID can be used. For example you can restrict using framework specific IDs:
|
||||
|
||||
```js
|
||||
const selector = finder(event.target, {
|
||||
idName: name => !name.startsWith('ember')
|
||||
})
|
||||
```
|
||||
|
||||
#### `className: (name: string) => boolean`
|
||||
|
||||
Check if this class name can be used. For example you can restrict using _is-*_ class names:
|
||||
|
||||
```js
|
||||
const selector = finder(event.target, {
|
||||
className: name => !name.startsWith('is-')
|
||||
})
|
||||
```
|
||||
|
||||
#### `tagName: (name: string) => boolean`
|
||||
|
||||
Check if tag name can be used, same as `className`.
|
||||
|
||||
#### `attr: (name: string, value: string) => boolean`
|
||||
|
||||
Check if attr name can be used.
|
||||
|
||||
#### `seedMinLength: number`
|
||||
|
||||
Minimum length of levels in fining selector. Starts from `1`.
|
||||
For more robust selectors give this param value around 4-5 depending on depth of you DOM tree.
|
||||
If `finder` hits `root` this param is ignored.
|
||||
|
||||
#### `optimizedMinLength: number`
|
||||
|
||||
Minimum length for optimising selector. Starts from `2`.
|
||||
For example selector `body > div > div > p` can be optimized to `body p`.
|
||||
|
||||
#### `threshold: number`
|
||||
|
||||
Max number of selectors to check before falling into `nth-child` usage.
|
||||
Checking for uniqueness of selector is very costs operation, if you have DOM tree depth of 5, with 5 classes on each level,
|
||||
that gives you more than 3k selectors to check.
|
||||
`finder` uses two step approach so it's reaching this threshold in some cases twice.
|
||||
Default `1000` is good enough in most cases.
|
||||
|
||||
#### `maxNumberOfTries: number`
|
||||
|
||||
Max number of tries when we do the optimization. It is a trade-off between optimization and efficiency.
|
||||
Default `10_000` is good enough in most cases.
|
||||
|
||||
### Google Chrome Extension
|
||||
|
||||

|
||||
|
||||
Generate the unique selectors in your browser by using [Chrome Extension](https://chrome.google.com/webstore/detail/get-unique-css-selector/lkfaghhbdebclkklgjhhonadomejckai)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
432
tracker/tracker/src/main/vendors/finder/finder.ts
vendored
432
tracker/tracker/src/main/vendors/finder/finder.ts
vendored
|
|
@ -1,432 +0,0 @@
|
|||
type Node = {
|
||||
name: string
|
||||
penalty: number
|
||||
level?: number
|
||||
}
|
||||
|
||||
type Path = Node[]
|
||||
|
||||
enum Limit {
|
||||
All,
|
||||
Two,
|
||||
One,
|
||||
}
|
||||
|
||||
export type Options = {
|
||||
root: Element
|
||||
idName: (name: string) => boolean
|
||||
className: (name: string) => boolean
|
||||
tagName: (name: string) => boolean
|
||||
attr: (name: string, value: string) => boolean
|
||||
seedMinLength: number
|
||||
optimizedMinLength: number
|
||||
threshold: number
|
||||
maxNumberOfTries: number
|
||||
}
|
||||
|
||||
let config: Options
|
||||
|
||||
let rootDocument: Document | Element
|
||||
export function finder(input: Element, options?: Partial<Options>) {
|
||||
if (input.nodeType !== Node.ELEMENT_NODE) {
|
||||
throw new Error("Can't generate CSS selector for non-element node type.")
|
||||
}
|
||||
|
||||
if ('html' === input.tagName.toLowerCase()) {
|
||||
return 'html'
|
||||
}
|
||||
|
||||
const defaults: Options = {
|
||||
root: document.body,
|
||||
idName: (name: string) => true,
|
||||
className: (name: string) => true,
|
||||
tagName: (name: string) => true,
|
||||
attr: (name: string, value: string) => false,
|
||||
seedMinLength: 1,
|
||||
optimizedMinLength: 2,
|
||||
threshold: 1000,
|
||||
maxNumberOfTries: 10000,
|
||||
}
|
||||
|
||||
config = { ...defaults, ...options }
|
||||
|
||||
rootDocument = findRootDocument(config.root, defaults)
|
||||
|
||||
let path = bottomUpSearch(input, Limit.All, () =>
|
||||
bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)),
|
||||
)
|
||||
|
||||
if (path) {
|
||||
const optimized = sort(optimize(path, input))
|
||||
|
||||
if (optimized.length > 0) {
|
||||
path = optimized[0]
|
||||
}
|
||||
|
||||
return selector(path)
|
||||
} else {
|
||||
throw new Error('Selector was not found.')
|
||||
}
|
||||
}
|
||||
|
||||
function findRootDocument(rootNode: Element | Document, defaults: Options) {
|
||||
if (rootNode.nodeType === Node.DOCUMENT_NODE) {
|
||||
return rootNode
|
||||
}
|
||||
if (rootNode === defaults.root) {
|
||||
return rootNode.ownerDocument
|
||||
}
|
||||
return rootNode
|
||||
}
|
||||
|
||||
function bottomUpSearch(input: Element, limit: Limit, fallback?: () => Path | null): Path | null {
|
||||
let path: Path | null = null
|
||||
const stack: Node[][] = []
|
||||
let current: Element | null = input
|
||||
let i = 0
|
||||
|
||||
while (current && current !== config.root.parentElement) {
|
||||
let level: Node[] = maybe(id(current)) ||
|
||||
maybe(...attr(current)) ||
|
||||
maybe(...classNames(current)) ||
|
||||
maybe(tagName(current)) || [any()]
|
||||
|
||||
const nth = index(current)
|
||||
|
||||
if (limit === Limit.All) {
|
||||
if (nth) {
|
||||
level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth)))
|
||||
}
|
||||
} else if (limit === Limit.Two) {
|
||||
level = level.slice(0, 1)
|
||||
|
||||
if (nth) {
|
||||
level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth)))
|
||||
}
|
||||
} else if (limit === Limit.One) {
|
||||
const [node] = (level = level.slice(0, 1))
|
||||
|
||||
if (nth && dispensableNth(node)) {
|
||||
level = [nthChild(node, nth)]
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of level) {
|
||||
node.level = i
|
||||
}
|
||||
|
||||
stack.push(level)
|
||||
|
||||
if (stack.length >= config.seedMinLength) {
|
||||
path = findUniquePath(stack, fallback)
|
||||
if (path) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
current = current.parentElement
|
||||
i++
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
path = findUniquePath(stack, fallback)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
function findUniquePath(stack: Node[][], fallback?: () => Path | null): Path | null {
|
||||
const paths = sort(combinations(stack))
|
||||
|
||||
if (paths.length > config.threshold) {
|
||||
return fallback ? fallback() : null
|
||||
}
|
||||
|
||||
for (const candidate of paths) {
|
||||
if (unique(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function selector(path: Path): string {
|
||||
let node = path[0]
|
||||
let query = node.name
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const level = path[i].level || 0
|
||||
|
||||
if (node.level === level - 1) {
|
||||
query = `${path[i].name} > ${query}`
|
||||
} else {
|
||||
query = `${path[i].name} ${query}`
|
||||
}
|
||||
|
||||
node = path[i]
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
function penalty(path: Path): number {
|
||||
return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0)
|
||||
}
|
||||
|
||||
function unique(path: Path) {
|
||||
switch (rootDocument.querySelectorAll(selector(path)).length) {
|
||||
case 0:
|
||||
throw new Error(`Can't select any node with this selector: ${selector(path)}`)
|
||||
case 1:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function id(input: Element): Node | null {
|
||||
const elementId = input.getAttribute('id')
|
||||
if (elementId && config.idName(elementId)) {
|
||||
return {
|
||||
name: '#' + cssesc(elementId, { isIdentifier: true }),
|
||||
penalty: 0,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function attr(input: Element): Node[] {
|
||||
const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value))
|
||||
|
||||
return attrs.map(
|
||||
(attr): Node => ({
|
||||
name: '[' + cssesc(attr.name, { isIdentifier: true }) + '="' + cssesc(attr.value) + '"]',
|
||||
penalty: 0.5,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function classNames(input: Element): Node[] {
|
||||
const names = Array.from(input.classList).filter(config.className)
|
||||
|
||||
return names.map(
|
||||
(name): Node => ({
|
||||
name: '.' + cssesc(name, { isIdentifier: true }),
|
||||
penalty: 1,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function tagName(input: Element): Node | null {
|
||||
const name = input.tagName.toLowerCase()
|
||||
if (config.tagName(name)) {
|
||||
return {
|
||||
name,
|
||||
penalty: 2,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function any(): Node {
|
||||
return {
|
||||
name: '*',
|
||||
penalty: 3,
|
||||
}
|
||||
}
|
||||
|
||||
function index(input: Element): number | null {
|
||||
const parent = input.parentNode
|
||||
if (!parent) {
|
||||
return null
|
||||
}
|
||||
|
||||
let child = parent.firstChild
|
||||
if (!child) {
|
||||
return null
|
||||
}
|
||||
|
||||
let i = 0
|
||||
while (child) {
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
i++
|
||||
}
|
||||
|
||||
if (child === input) {
|
||||
break
|
||||
}
|
||||
|
||||
child = child.nextSibling
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
function nthChild(node: Node, i: number): Node {
|
||||
return {
|
||||
name: node.name + `:nth-child(${i})`,
|
||||
penalty: node.penalty + 1,
|
||||
}
|
||||
}
|
||||
|
||||
function dispensableNth(node: Node) {
|
||||
return node.name !== 'html' && !node.name.startsWith('#')
|
||||
}
|
||||
|
||||
function maybe(...level: (Node | null)[]): Node[] | null {
|
||||
const list = level.filter(notEmpty)
|
||||
if (list.length > 0) {
|
||||
return list
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function notEmpty<T>(value: T | null | undefined): value is T {
|
||||
return value !== null && value !== undefined
|
||||
}
|
||||
|
||||
function combinations(stack: Node[][], path: Node[] = []): Node[][] {
|
||||
const paths: Node[][] = []
|
||||
if (stack.length > 0) {
|
||||
for (const node of stack[0]) {
|
||||
paths.push(...combinations(stack.slice(1, stack.length), path.concat(node)))
|
||||
}
|
||||
} else {
|
||||
paths.push(path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function sort(paths: Iterable<Path>): Path[] {
|
||||
return Array.from(paths).sort((a, b) => penalty(a) - penalty(b))
|
||||
}
|
||||
|
||||
type Scope = {
|
||||
counter: number
|
||||
visited: Map<string, boolean>
|
||||
}
|
||||
|
||||
function optimize(
|
||||
path: Path,
|
||||
input: Element,
|
||||
scope: Scope = {
|
||||
counter: 0,
|
||||
visited: new Map<string, boolean>(),
|
||||
},
|
||||
): Node[][] {
|
||||
const paths: Node[][] = []
|
||||
if (path.length > 2 && path.length > config.optimizedMinLength) {
|
||||
for (let i = 1; i < path.length - 1; i++) {
|
||||
if (scope.counter > config.maxNumberOfTries) {
|
||||
return paths // Okay At least I tried!
|
||||
}
|
||||
scope.counter += 1
|
||||
const newPath = [...path]
|
||||
newPath.splice(i, 1)
|
||||
const newPathKey = selector(newPath)
|
||||
if (scope.visited.has(newPathKey)) {
|
||||
return paths
|
||||
}
|
||||
if (unique(newPath) && same(newPath, input)) {
|
||||
paths.push(newPath)
|
||||
scope.visited.set(newPathKey, true)
|
||||
paths.push(...optimize(newPath, input, scope))
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function same(path: Path, input: Element) {
|
||||
return rootDocument.querySelector(selector(path)) === input
|
||||
}
|
||||
|
||||
const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/
|
||||
const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/
|
||||
const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g
|
||||
|
||||
const defaultOptions = {
|
||||
escapeEverything: false,
|
||||
isIdentifier: false,
|
||||
quotes: 'single',
|
||||
wrap: false,
|
||||
}
|
||||
|
||||
function cssesc(string: string, opt: Partial<typeof defaultOptions> = {}) {
|
||||
const options = { ...defaultOptions, ...opt }
|
||||
if (options.quotes != 'single' && options.quotes != 'double') {
|
||||
options.quotes = 'single'
|
||||
}
|
||||
const quote = options.quotes == 'double' ? '"' : "'"
|
||||
const isIdentifier = options.isIdentifier
|
||||
|
||||
const firstChar = string.charAt(0)
|
||||
let output = ''
|
||||
let counter = 0
|
||||
const length = string.length
|
||||
while (counter < length) {
|
||||
const character = string.charAt(counter++)
|
||||
let codePoint = character.charCodeAt(0)
|
||||
let value: string | undefined = void 0
|
||||
// If it’s not a printable ASCII character…
|
||||
if (codePoint < 0x20 || codePoint > 0x7e) {
|
||||
if (codePoint >= 0xd800 && codePoint <= 0xdbff && counter < length) {
|
||||
// It’s a high surrogate, and there is a next character.
|
||||
const extra = string.charCodeAt(counter++)
|
||||
if ((extra & 0xfc00) == 0xdc00) {
|
||||
// next character is low surrogate
|
||||
codePoint = ((codePoint & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000
|
||||
} else {
|
||||
// It’s an unmatched surrogate; only append this code unit, in case
|
||||
// the next code unit is the high surrogate of a surrogate pair.
|
||||
counter--
|
||||
}
|
||||
}
|
||||
value = '\\' + codePoint.toString(16).toUpperCase() + ' '
|
||||
} else {
|
||||
if (options.escapeEverything) {
|
||||
if (regexAnySingleEscape.test(character)) {
|
||||
value = '\\' + character
|
||||
} else {
|
||||
value = '\\' + codePoint.toString(16).toUpperCase() + ' '
|
||||
}
|
||||
} else if (/[\t\n\f\r\x0B]/.test(character)) {
|
||||
value = '\\' + codePoint.toString(16).toUpperCase() + ' '
|
||||
} else if (
|
||||
character == '\\' ||
|
||||
(!isIdentifier &&
|
||||
((character == '"' && quote == character) || (character == "'" && quote == character))) ||
|
||||
(isIdentifier && regexSingleEscape.test(character))
|
||||
) {
|
||||
value = '\\' + character
|
||||
} else {
|
||||
value = character
|
||||
}
|
||||
}
|
||||
output += value
|
||||
}
|
||||
|
||||
if (isIdentifier) {
|
||||
if (/^-[-\d]/.test(output)) {
|
||||
output = '\\-' + output.slice(1)
|
||||
} else if (/\d/.test(firstChar)) {
|
||||
output = '\\3' + firstChar + ' ' + output.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove spaces after `\HEX` escapes that are not followed by a hex digit,
|
||||
// since they’re redundant. Note that this is only possible if the escape
|
||||
// sequence isn’t preceded by an odd number of backslashes.
|
||||
output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) {
|
||||
if ($1 && $1.length % 2) {
|
||||
// It’s not safe to remove the space, so don’t.
|
||||
return $0
|
||||
}
|
||||
// Strip the space.
|
||||
return ($1 || '') + $2
|
||||
})
|
||||
|
||||
if (!isIdentifier && options.wrap) {
|
||||
return quote + output + quote
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
{
|
||||
"_from": "@medv/finder",
|
||||
"_id": "@medv/finder@2.0.0",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-gV4jOsGpiWNDGd8Dw7tod1Fc9Gc7StaOT4oZ/6srHRWtsHU+HYWzmkYsa3Qy/z0e9tY1WpJ9wWdBFGskfbzoug==",
|
||||
"_location": "/@medv/finder",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "tag",
|
||||
"registry": true,
|
||||
"raw": "@medv/finder",
|
||||
"name": "@medv/finder",
|
||||
"escapedName": "@medv%2ffinder",
|
||||
"scope": "@medv",
|
||||
"rawSpec": "",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "latest"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"#USER",
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/@medv/finder/-/finder-2.0.0.tgz",
|
||||
"_shasum": "699b7141393aa815f120b38f54f92ad212225902",
|
||||
"_spec": "@medv/finder",
|
||||
"_where": "/Users/shikhu/work/openreplay/tracker/tracker",
|
||||
"author": {
|
||||
"name": "Anton Medvedev",
|
||||
"email": "anton@medv.io"
|
||||
},
|
||||
"ava": {
|
||||
"require": [
|
||||
"esm",
|
||||
"./test/helpers/setup-browser-env.js"
|
||||
]
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/antonmedv/finder/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"deprecated": false,
|
||||
"description": "CSS Selector Generator",
|
||||
"devDependencies": {
|
||||
"ava": "^3.8.2",
|
||||
"babel-minify": "*",
|
||||
"browser-env": "^3.3.0",
|
||||
"esm": "^3.2.25",
|
||||
"gzip-size-cli": "*",
|
||||
"release-it": "^13.6.1",
|
||||
"typescript": "3.9.3"
|
||||
},
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.js"
|
||||
],
|
||||
"homepage": "https://github.com/antonmedv/finder",
|
||||
"keywords": [
|
||||
"css",
|
||||
"selector",
|
||||
"generator"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "finder.js",
|
||||
"name": "@medv/finder",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/antonmedv/finder.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "tsc",
|
||||
"release": "release-it --access public",
|
||||
"size": "minify finder.js --sourceType module | gzip-size",
|
||||
"start": "tsc -w",
|
||||
"test": "tsc && ava"
|
||||
},
|
||||
"types": "finder.d.ts",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
|
|
@ -3,20 +3,38 @@ import QueueSender from './QueueSender.js'
|
|||
|
||||
global.fetch = () => Promise.resolve(new Response()) // jsdom does not have it
|
||||
|
||||
function mockFetch(status: number) {
|
||||
function mockFetch(status: number, headers?: Record<string, string>) {
|
||||
return jest
|
||||
.spyOn(global, 'fetch')
|
||||
.mockImplementation(() => Promise.resolve({ status } as Response))
|
||||
.mockImplementation((request) =>
|
||||
Promise.resolve({ status, headers, request } as unknown as Response & {
|
||||
request: RequestInfo
|
||||
}),
|
||||
)
|
||||
}
|
||||
const baseURL = 'MYBASEURL'
|
||||
const sampleArray = new Uint8Array(1)
|
||||
const randomToken = 'abc'
|
||||
|
||||
const requestMock = {
|
||||
body: sampleArray,
|
||||
headers: { Authorization: 'Bearer abc' },
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
const gzipRequestMock = {
|
||||
...requestMock,
|
||||
headers: { ...requestMock.headers, 'Content-Encoding': 'gzip' },
|
||||
}
|
||||
|
||||
function defaultQueueSender({
|
||||
url = baseURL,
|
||||
onUnauthorised = () => {},
|
||||
onFailed = () => {},
|
||||
} = {}) {
|
||||
return new QueueSender(baseURL, onUnauthorised, onFailed, 10, 1000)
|
||||
onCompress = undefined,
|
||||
}: Record<string, any> = {}) {
|
||||
return new QueueSender(baseURL, onUnauthorised, onFailed, 10, 1000, onCompress)
|
||||
}
|
||||
|
||||
describe('QueueSender', () => {
|
||||
|
|
@ -42,6 +60,25 @@ describe('QueueSender', () => {
|
|||
expect(fetchMock).toBeCalledTimes(0)
|
||||
queueSender.push(sampleArray)
|
||||
expect(fetchMock).toBeCalledTimes(1)
|
||||
expect(fetchMock.mock.calls[0][1]).toMatchObject(requestMock)
|
||||
})
|
||||
test('Sends compressed request if onCompress is provided and compressed batch is included', () => {
|
||||
const queueSender = defaultQueueSender({ onCompress: () => true })
|
||||
const fetchMock = mockFetch(200)
|
||||
|
||||
// @ts-ignore
|
||||
const spyOnCompress = jest.spyOn(queueSender, 'onCompress')
|
||||
// @ts-ignore
|
||||
const spyOnSendNext = jest.spyOn(queueSender, 'sendNext')
|
||||
|
||||
queueSender.authorise(randomToken)
|
||||
queueSender.push(sampleArray)
|
||||
expect(spyOnCompress).toBeCalledTimes(1)
|
||||
queueSender.sendCompressed(sampleArray)
|
||||
expect(fetchMock).toBeCalledTimes(1)
|
||||
expect(spyOnSendNext).toBeCalledTimes(1)
|
||||
expect(spyOnCompress).toBeCalledTimes(1)
|
||||
expect(fetchMock.mock.calls[0][1]).toMatchObject(gzipRequestMock)
|
||||
})
|
||||
test('Calls fetch on authorisation if there was a push() call before', () => {
|
||||
const queueSender = defaultQueueSender()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue