Skip to content

Commit

Permalink
Added block selection utils for remote control
Browse files Browse the repository at this point in the history
commit_hash:9800f8c104370b217547e5f42cdeff3c0ea0fb35
  • Loading branch information
Stanislavsky34200 committed Sep 17, 2024
1 parent 6e54872 commit 7a13012
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 19 deletions.
1 change: 1 addition & 0 deletions .mapping.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@
"src/utils/dom/__tests__/nodeName.spec.ts":"metrika/frontend/watch/public/src/utils/dom/__tests__/nodeName.spec.ts",
"src/utils/dom/__tests__/nodeText.spec.ts":"metrika/frontend/watch/public/src/utils/dom/__tests__/nodeText.spec.ts",
"src/utils/dom/__tests__/waitForBody.spec.ts":"metrika/frontend/watch/public/src/utils/dom/__tests__/waitForBody.spec.ts",
"src/utils/dom/block.ts":"metrika/frontend/watch/public/src/utils/dom/block.ts",
"src/utils/dom/button.ts":"metrika/frontend/watch/public/src/utils/dom/button.ts",
"src/utils/dom/closest.ts":"metrika/frontend/watch/public/src/utils/dom/closest.ts",
"src/utils/dom/dom.ts":"metrika/frontend/watch/public/src/utils/dom/dom.ts",
Expand Down
4 changes: 4 additions & 0 deletions features.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@
"path": "remoteControl",
"desc": "Enables metrika inpage scripts work (i.g. selecting buttons for quick goals in the metrika interface)"
},
{
"code": "REMOTE_CONTROL_BLOCK_HELPERS_FEATURE",
"desc": "Enables metrika block selection helpers for inpage scripts to work"
},
{
"code": "CSRF_TOKEN_FEATURE",
"desc": "Add API token required for authorized traffic",
Expand Down
29 changes: 28 additions & 1 deletion src/providers/remoteControl/remoteControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SUBMIT_TRACKING_FEATURE,
CHECK_STATUS_FEATURE,
REMOTE_CONTROL_FEATURE,
REMOTE_CONTROL_BLOCK_HELPERS_FEATURE,
} from 'generated/features';
import {
memo,
Expand All @@ -34,6 +35,11 @@ import { closestForm, getFormData, selectForms } from 'src/utils/dom/form';
import { checkStatusFn } from 'src/providers/statusCheck/statusCheckFn';
import { isNumber, parseDecimalInt } from 'src/utils/number';
import { AnyFunc } from 'src/utils/function/types';
import {
getClosestTextContainer,
getTextContainerData,
selectTextContainer,
} from 'src/utils/dom/block';

/* eslint-disable camelcase */
type ExtendedWindow = Window & {
Expand Down Expand Up @@ -144,6 +150,8 @@ export type Message = {
inpageMode: string;
/** Feature init data */
initMessage: string;
/** Are block utils needed */
isBlockUtilsEnabled?: boolean;
} & InlineMessageProps;

export const UTILS_KEY = '_u';
Expand Down Expand Up @@ -200,6 +208,7 @@ export const setupUtilsAndLoadScript = (
ctx: ExtendedWindow,
src?: string,
counterId = '',
isBlockUtilsEnabled?: boolean,
) => {
if (
flags[CLICK_TRACKING_FEATURE] ||
Expand All @@ -221,6 +230,19 @@ export const setupUtilsAndLoadScript = (
[UTILS_GET_DATA_KEY]: bindArg(ctx, getFormData),
};
}
if (
flags[REMOTE_CONTROL_BLOCK_HELPERS_FEATURE] &&
(flags[REMOTE_CONTROL_FEATURE] ||
flags[CLICK_TRACKING_FEATURE] ||
flags[SUBMIT_TRACKING_FEATURE]) &&
isBlockUtilsEnabled
) {
utils['block'] = {
[UTILS_CLOSEST_KEY]: bindArg(ctx, getClosestTextContainer),
[UTILS_SELECT_KEY]: selectTextContainer,
[UTILS_GET_DATA_KEY]: bindArg(ctx, getTextContainerData),
};
}
if (flags[CLICK_TRACKING_FEATURE] || flags[REMOTE_CONTROL_FEATURE]) {
utils['button'] = {
[UTILS_CLOSEST_KEY]: bindArg(ctx, closestButton),
Expand Down Expand Up @@ -256,7 +278,12 @@ export const handleMessage = memo(
if (message['inline']) {
const src = getResourceUrl(ctx, message);
const { id = '' } = message;
setupUtilsAndLoadScript(ctx, src, id);
setupUtilsAndLoadScript(
ctx,
src,
id,
message['isBlockUtilsEnabled'],
);
} else if (
message['resource'] &&
isAllowedResource(message['resource'])
Expand Down
70 changes: 54 additions & 16 deletions src/utils/dom/__tests__/element.spec.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,65 @@
import { getElementPath } from 'src/utils/dom';
import { getElementPath, getElementCSSSelector } from 'src/utils/dom';
import { JSDOMWrapper } from 'src/__tests__/utils/jsdom';
import chai from 'chai';

describe('Element', () => {
const { window } = new JSDOMWrapper(`<span id="ignored"></span>
<span>
<div>
<b id="id">Hello!</b>
</div>
</span>`);
describe('getElementPath', () => {
const { window } = new JSDOMWrapper(`<span id="ignored"></span>
<span>
<div>
<b id="id">Hello!</b>
</div>
</span>`);

it('get path', () => {
const el = window.document.querySelector('#id') as HTMLElement;
it('get path', () => {
const el = window.document.querySelector('#id') as HTMLElement;

chai.expect(getElementPath(window, el)).eq('<AW1');
chai.expect(getElementPath(window, el)).eq('<AW1');
});

it('ignore element', () => {
const el = window.document.querySelector('#id') as HTMLElement;
const ignored = window.document.querySelector(
'#ignored',
) as HTMLElement;

chai.expect(getElementPath(window, el, ignored)).eq('<AW');
});
});

it('ignore element', () => {
const el = window.document.querySelector('#id') as HTMLElement;
const ignored = window.document.querySelector(
'#ignored',
) as HTMLElement;
describe('getElementCSSSelector', () => {
it('returns null if it is impossible to get unique selector', () => {
const { window } = new JSDOMWrapper(`
<div>
<div class="non-unique">
</div>
<div class="non-unique">
</div>
</div>
`);

const element = window.document.querySelector(
'.non-unique',
) as HTMLElement;
const result = getElementCSSSelector(window, element);
chai.expect(result).to.be.null;
});

chai.expect(getElementPath(window, el, ignored)).eq('<AW');
it('returns unique multi-component selector with id', () => {
const { window } = new JSDOMWrapper(`
<div>
<div id="unique">
<div class="non-unique"></div>
</div>
<div class="non-unique">
</div>
</div>
`);
const element = window.document.querySelector(
'#unique .non-unique',
) as HTMLElement;
const result = getElementCSSSelector(window, element);
chai.expect(result).to.equal('#unique .non-unique');
});
});
});
15 changes: 15 additions & 0 deletions src/utils/dom/block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { bindArg } from '../function';
import { closest } from './closest';
import { CSS, PATH, getData } from './identifiers';
import { select } from './select';

const BLOCK_SELECTOR =
'div,span,main,section,p,b,h1,h2,h3,h4,h5,h6,td,small,a,i,td,li,q';

export const getClosestTextContainer = bindArg(BLOCK_SELECTOR, closest);
export const selectTextContainer = bindArg(BLOCK_SELECTOR, select);
export const getTextContainerData = (
ctx: Window,
form: HTMLElement,
ignored?: HTMLElement,
) => getData(ctx, form, [PATH, CSS], undefined, ignored);
51 changes: 51 additions & 0 deletions src/utils/dom/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
hasClass,
} from './dom';
import { isRemovedFromDoc } from './isRemovedFromDoc';
import { arrayFrom } from '../array/arrayFrom';
import { select } from './select';
/* eslint-disable */

export const getElementXY = (ctx: Window, el: HTMLElement | null) => {
Expand Down Expand Up @@ -141,6 +143,55 @@ export const getCachedTags = memo(() => {
return cacheTags;
});

export const getElementCSSSelector = (ctx: Window, el: HTMLElement | null) => {
let selector = '';
let element = el;
const body = getBody(ctx)!;
let elementSelector: string[] = [];
const getFullSelector = () => {
const lastComponentSelector = arrayJoin('', elementSelector);
return selector
? `${lastComponentSelector} ${selector}`
: lastComponentSelector;
};
const getUniqueSelector = () => {
const fullSelector = getFullSelector();
const selectedNodes = select(fullSelector, body);
if (selectedNodes.length === 1) {
return fullSelector;
}
return null;
};

while (element && element.parentElement) {
if (element.id) {
elementSelector.push(`#${element.id}`);
const uniqueSelector = getUniqueSelector();
if (uniqueSelector) {
return uniqueSelector;
}
}
if (element.classList.length) {
cForEach(
(className) => elementSelector.push(`.${className}`),
arrayFrom(element.classList),
);
const uniqueSelector = getUniqueSelector();
if (uniqueSelector) {
return uniqueSelector;
}
}

if (elementSelector.length) {
selector = getFullSelector();
}
elementSelector = [];
element = element.parentElement;
}

return null;
};

export const getElementPath = (
ctx: Window,
el: HTMLElement | null,
Expand Down
18 changes: 16 additions & 2 deletions src/utils/dom/identifiers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { cReduce, cSome } from 'src/utils/array';
import { ctxPath, getPath } from 'src/utils/object';
import { getElementPathCached } from 'src/utils/dom/element';
import {
getElementCSSSelector,
getElementPathCached,
} from 'src/utils/dom/element';
import { convertToString } from 'src/utils/string';
import { trimText } from 'src/utils/string/remove';
import { isInputElement } from 'src/utils/dom/dom';
Expand All @@ -9,6 +12,7 @@ import {
CLICK_TRACKING_FEATURE,
LOCAL_FEATURE,
PREPROD_FEATURE,
REMOTE_CONTROL_BLOCK_HELPERS_FEATURE,
REMOTE_CONTROL_FEATURE,
SUBMIT_TRACKING_FEATURE,
} from 'generated/features';
Expand All @@ -22,8 +26,9 @@ export const PATH = 'p';
export const CONTENT = 'c';
export const HREF = 'h';
export const TYPE = 'ty';
export const CSS = 'cs';

const IDENTIFIERS = [ID, NAME, HREF, PATH, CONTENT, TYPE] as const;
const IDENTIFIERS = [ID, NAME, HREF, PATH, CONTENT, TYPE, CSS] as const;

export type Identifier = typeof IDENTIFIERS[number];
type GenericGetter = (ctx: Window, element: HTMLElement) => string | null;
Expand Down Expand Up @@ -84,6 +89,15 @@ if (
GETTERS_MAP[PATH] = getElementPathCached;
}

if (
flags[REMOTE_CONTROL_BLOCK_HELPERS_FEATURE] &&
(flags[REMOTE_CONTROL_FEATURE] ||
flags[CLICK_TRACKING_FEATURE] ||
flags[SUBMIT_TRACKING_FEATURE])
) {
GETTERS_MAP[CSS] = getElementCSSSelector;
}

if (flags[CLICK_TRACKING_FEATURE] || flags[REMOTE_CONTROL_FEATURE]) {
GETTERS_MAP[CONTENT] = (ctx, element, selectFn) => {
let result = trimText(getPath(element, 'textContent'));
Expand Down

0 comments on commit 7a13012

Please sign in to comment.