From 4e3332304ead41d4d9af11e0a1542784d872083d Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 31 May 2023 12:52:51 +0200 Subject: [PATCH] fix(tracker): fix breaking q sender test --- .../tracker/src/main/vendors/finder/LICENSE | 21 - .../tracker/src/main/vendors/finder/README.md | 132 ------ .../tracker/src/main/vendors/finder/finder.ts | 432 ------------------ .../src/main/vendors/finder/package.json | 78 ---- .../src/webworker/QueueSender.unit.test.ts | 45 +- 5 files changed, 41 insertions(+), 667 deletions(-) delete mode 100644 tracker/tracker/src/main/vendors/finder/LICENSE delete mode 100644 tracker/tracker/src/main/vendors/finder/README.md delete mode 100644 tracker/tracker/src/main/vendors/finder/finder.ts delete mode 100644 tracker/tracker/src/main/vendors/finder/package.json diff --git a/tracker/tracker/src/main/vendors/finder/LICENSE b/tracker/tracker/src/main/vendors/finder/LICENSE deleted file mode 100644 index 1c1d11619..000000000 --- a/tracker/tracker/src/main/vendors/finder/LICENSE +++ /dev/null @@ -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. diff --git a/tracker/tracker/src/main/vendors/finder/README.md b/tracker/tracker/src/main/vendors/finder/README.md deleted file mode 100644 index 391bfee9a..000000000 --- a/tracker/tracker/src/main/vendors/finder/README.md +++ /dev/null @@ -1,132 +0,0 @@ -![finder](https://medv.io/assets/finder.png) - -# finder - -[![npm](https://img.shields.io/npm/v/@medv/finder?color=grightgreen)](https://www.npmjs.com/package/@medv/finder) -[![Build status](https://img.shields.io/travis/antonmedv/finder)](https://travis-ci.org/antonmedv/finder) -[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@medv/finder?label=size)](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 - -``` - -## 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 - -![Chrome Extension](https://user-images.githubusercontent.com/141232/36737287-4a999d84-1c0d-11e8-8a14-43bcf9baf7ca.png) - -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) diff --git a/tracker/tracker/src/main/vendors/finder/finder.ts b/tracker/tracker/src/main/vendors/finder/finder.ts deleted file mode 100644 index 431956595..000000000 --- a/tracker/tracker/src/main/vendors/finder/finder.ts +++ /dev/null @@ -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) { - 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(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[] { - return Array.from(paths).sort((a, b) => penalty(a) - penalty(b)) -} - -type Scope = { - counter: number - visited: Map -} - -function optimize( - path: Path, - input: Element, - scope: Scope = { - counter: 0, - visited: new Map(), - }, -): 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 = {}) { - 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 -} diff --git a/tracker/tracker/src/main/vendors/finder/package.json b/tracker/tracker/src/main/vendors/finder/package.json deleted file mode 100644 index 685be35f4..000000000 --- a/tracker/tracker/src/main/vendors/finder/package.json +++ /dev/null @@ -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" -} diff --git a/tracker/tracker/src/webworker/QueueSender.unit.test.ts b/tracker/tracker/src/webworker/QueueSender.unit.test.ts index 0f76c29dd..79fba9e71 100644 --- a/tracker/tracker/src/webworker/QueueSender.unit.test.ts +++ b/tracker/tracker/src/webworker/QueueSender.unit.test.ts @@ -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) { 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 = {}) { + 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()