Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: selector inspector panel (closes #7375) #7372

Merged
merged 27 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
005408d
feat: Selector Inspector
felis2803 Jan 20, 2023
631b389
fix: style of .selector-value
felis2803 Jan 20, 2023
e41847b
fix: highlight disappears on click
felis2803 Jan 20, 2023
4211af0
feat: added a shadow for the element frame
felis2803 Jan 20, 2023
ea2806c
fix: inspector panel does not overlap status bar
felis2803 Jan 22, 2023
c1b31a6
fix: element frames do not overlap panels
felis2803 Jan 22, 2023
a6e49f7
fix: match indicator text changed
felis2803 Jan 23, 2023
d446891
fix: match indicator text changed
felis2803 Jan 23, 2023
46adf0f
style: removed extra styles
felis2803 Jan 24, 2023
590a453
fix: prevented repeated generation of selectors for the same element
felis2803 Jan 25, 2023
25b9321
refactor: elementPicker is available from selectorInspectorPanel for …
felis2803 Jan 25, 2023
9ef6e76
fix: auxiliary input is no longer shown
felis2803 Jan 26, 2023
31264da
test: added required tests
felis2803 Jan 26, 2023
19d5402
chore: got rid of auxiliary file and scripts
felis2803 Jan 26, 2023
8d27cee
chore: new link to selector generator
felis2803 Jan 26, 2023
7937a36
test: moved selector inspector utils to 'utils' dir
felis2803 Jan 27, 2023
447272d
fix: Selector Inspector no longer causes an error in the iframe driver
felis2803 Jan 27, 2023
d0cda46
style: fixed linting errors
felis2803 Jan 27, 2023
2c08769
test: skipped tests in ie
felis2803 Jan 27, 2023
f3b5a3d
test: skipped tests in experimental debug mode
felis2803 Jan 27, 2023
966a39b
chore(deps): used published testcafe-selector-generator
felis2803 Jan 30, 2023
4c5657d
refactor: separate function _getParsedSelector
felis2803 Jan 30, 2023
2192415
refactor: parseSelectorUrl -> parseSelectorRelativeUrl
felis2803 Jan 30, 2023
3037dc9
refactor: parseSelectorUrl -> parseSelector
felis2803 Jan 30, 2023
413986b
fix: got rid of this in static method
felis2803 Jan 30, 2023
a557c6d
test: reduced the test page
felis2803 Jan 31, 2023
ef348a5
test: skipped on mobile
felis2803 Jan 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: Selector Inspector
  • Loading branch information
felis2803 committed Feb 10, 2023
commit 005408daa59aca3a02a8d74c2b108ed735b34526
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
"test-functional-local-headless-chrome-esm": "npm run build && npm run test-functional-local-headless-chrome-run-esm",
"publish-please-only": "publish-please",
"publish-please": "del-cli package-lock.json node_modules && npm i && publish-please",
"prepublishOnly": "publish-please guard"
"prepublishOnly": "publish-please guard",
"inspector-run": "node bin/testcafe.js chrome temp.js",
"inspector": "gulp fast-build && npm run inspector-run"
},
"dependencies": {
"@babel/core": "^7.12.1",
Expand Down Expand Up @@ -151,6 +153,7 @@
"testcafe-reporter-spec": "^2.1.1",
"testcafe-reporter-xunit": "^2.2.1",
"testcafe-safe-storage": "^1.1.1",
"testcafe-selector-generator": "git+https://github.com/devexpress/testcafe-selector-generator",
"time-limit-promise": "^1.0.2",
"tmp": "0.0.28",
"tree-kill": "^1.2.2",
Expand Down
26 changes: 26 additions & 0 deletions src/browser/connection/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { IncomingMessage, ServerResponse } from 'http';
import SERVICE_ROUTES from './service-routes';
import EMPTY_PAGE_MARKUP from '../../proxyless/empty-page-markup';
import PROXYLESS_ERROR_ROUTE from '../../proxyless/error-route';
import { initSelector } from '../../test-run/commands/validations/initializers';

export default class BrowserConnectionGateway {
private _connections: Dictionary<BrowserConnection> = {};
Expand Down Expand Up @@ -72,6 +73,7 @@ export default class BrowserConnectionGateway {
this._dispatch(`${SERVICE_ROUTES.closeWindow}/{id}`, proxy, BrowserConnectionGateway._onCloseWindowRequest, 'POST');
this._dispatch(`${SERVICE_ROUTES.openFileProtocol}/{id}`, proxy, BrowserConnectionGateway._onOpenFileProtocolRequest, 'POST');
this._dispatch(`${SERVICE_ROUTES.dispatchProxylessEvent}/{id}`, proxy, BrowserConnectionGateway._onDispatchProxylessEvent, 'POST', this.proxyless);
this._dispatch(`${SERVICE_ROUTES.parseSelector}/{id}`, proxy, BrowserConnectionGateway._parseSelector, 'POST');

proxy.GET(SERVICE_ROUTES.connect, (req: IncomingMessage, res: ServerResponse) => this._connectNextRemoteBrowser(req, res));
proxy.GET(SERVICE_ROUTES.connectWithTrailingSlash, (req: IncomingMessage, res: ServerResponse) => this._connectNextRemoteBrowser(req, res));
Expand Down Expand Up @@ -245,6 +247,30 @@ export default class BrowserConnectionGateway {
respond500(res, 'There are no available _connections to establish.');
}

private static _parseSelector (req: IncomingMessage, res: ServerResponse, connection: BrowserConnection): void {
if (BrowserConnectionGateway._ensureConnectionReady(res, connection)) {
BrowserConnectionGateway._fetchRequestData(req, data => {
try {
const options = {
felis2803 marked this conversation as resolved.
Show resolved Hide resolved
testRun: connection.getCurrentTestRun(),
skipVisibilityCheck: true,
collectionMode: true,
};

const rawSelector = JSON.parse(data).selector;
const value = rawSelector.trim().startsWith('Selector(') ? rawSelector : `'${rawSelector}'`;
const selector = { type: 'js-expr', value };
const parsedSelector = initSelector('selector', selector, options);

respondWithJSON(res, parsedSelector);
}
catch (error) {
respondWithJSON(res);
}
});
}
}

// API
public startServingConnection (connection: BrowserConnection): void {
this._connections[connection.id] = connection;
Expand Down
2 changes: 2 additions & 0 deletions src/browser/connection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export default class BrowserConnection extends EventEmitter {
public openFileProtocolRelativeUrl = '';
public openFileProtocolUrl = '';
public dispatchProxylessEventRelativeUrl = '';
public parseSelectorUrl = '';
felis2803 marked this conversation as resolved.
Show resolved Hide resolved
private readonly debugLogger: debug.Debugger;
private osInfo: OSInfo | null = null;

Expand Down Expand Up @@ -195,6 +196,7 @@ export default class BrowserConnection extends EventEmitter {
this.closeWindowUrl = `${SERVICE_ROUTES.closeWindow}/${this.id}`;
this.openFileProtocolRelativeUrl = `${SERVICE_ROUTES.openFileProtocol}/${this.id}`;
this.dispatchProxylessEventRelativeUrl = `${SERVICE_ROUTES.dispatchProxylessEvent}/${this.id}`;
this.parseSelectorUrl = `${SERVICE_ROUTES.parseSelector}/${this.id}`;

this.idleUrl = proxy.resolveRelativeServiceUrl(this.idleRelativeUrl);
this.heartbeatUrl = proxy.resolveRelativeServiceUrl(this.heartbeatRelativeUrl);
Expand Down
1 change: 1 addition & 0 deletions src/browser/connection/service-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default {
serviceWorker: '/service-worker.js',
openFileProtocol: '/browser/open-file-protocol',
dispatchProxylessEvent: '/browser/dispatch-proxyless-event',
parseSelector: '/parse-selector',

assets: {
index: '/browser/assets/index.js',
Expand Down
7 changes: 7 additions & 0 deletions src/client/browser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,10 @@ export async function dispatchProxylessEvent (dispatchProxylessEventUrl, testCaf

await testCafeUI.show();
}

export function parseSelector (parseSelectorUrl, createXHR, selector) {
return sendXHR(parseSelectorUrl, createXHR, {
method: 'POST',
data: JSON.stringify({ selector }), //eslint-disable-line no-restricted-globals
});
}
12 changes: 9 additions & 3 deletions src/client/driver/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ export default class Driver extends serviceUtils.EventEmitter {
this.childWindowDriverLinks = [];
this.parentWindowDriverLink = null;

this.statusBar = null;
this.statusBar = null;
this.selectorInspectorPanel = null;

this.windowId = this._getCurrentWindowId();
this.role = DriverRole.replica;
Expand Down Expand Up @@ -1449,6 +1450,8 @@ export default class Driver extends serviceUtils.EventEmitter {
_onSetBreakpointCommand ({ isTestError, inCompilerService }) {
const showDebuggingStatusPromise = this.statusBar.showDebuggingStatus(isTestError);

this.selectorInspectorPanel.show();

if (inCompilerService) {
showDebuggingStatusPromise.then(debug => {
this.debug = debug;
Expand Down Expand Up @@ -1653,6 +1656,8 @@ export default class Driver extends serviceUtils.EventEmitter {
_executeCommand (command) {
this.contextStorage.setItem(this.WINDOW_COMMAND_API_CALL_FLAG, false);

this.selectorInspectorPanel.hide();

if (this.customCommandHandlers[command.type])
this._onCustomCommand(command);

Expand Down Expand Up @@ -1898,8 +1903,9 @@ export default class Driver extends serviceUtils.EventEmitter {
proxyless,
});

this.nativeDialogsTracker = new NativeDialogTracker(this.contextStorage, { proxyless, dialogHandler });
this.statusBar = new testCafeUI.StatusBar(this.runInfo.userAgent, this.runInfo.fixtureName, this.runInfo.testName, this.contextStorage);
this.nativeDialogsTracker = new NativeDialogTracker(this.contextStorage, { proxyless, dialogHandler });
this.statusBar = new testCafeUI.StatusBar(this.runInfo.userAgent, this.runInfo.fixtureName, this.runInfo.testName, this.contextStorage);
this.selectorInspectorPanel = new testCafeUI.SelectorInspectorPanel(this.statusBar);

const self = this;

Expand Down
2 changes: 2 additions & 0 deletions src/client/test-run/index.js.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
var browserActiveWindowIdUrl = origin + {{{browserActiveWindowIdUrl}}};
var browserCloseWindowUrl = origin + {{{browserCloseWindowUrl}}};
var browserDispatchProxylessEventUrl = origin + {{{browserDispatchProxylessEventRelativeUrl}}};
var browserParseSelectorUrl = origin + {{{browserParseSelectorUrl}}};
var skipJsErrors = {{{skipJsErrors}}};
var dialogHandler = {{{dialogHandler}}};
var userAgent = {{{userAgent}}};
Expand All @@ -47,6 +48,7 @@
closeWindow: browserCloseWindowUrl,
openFileProtocolUrl: browserOpenFileProtocolUrl,
dispatchProxylessEvent: browserDispatchProxylessEventUrl,
parseSelectorUrl: browserParseSelectorUrl,
felis2803 marked this conversation as resolved.
Show resolved Hide resolved
},
{
userAgent: userAgent,
Expand Down
3 changes: 3 additions & 0 deletions src/client/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import cursorUI from './cursor';
import iframeCursorUI from './cursor/iframe-cursor';
import screenshotMark from './screenshot-mark';
import uiRoot from './ui-root';
import SelectorInspectorPanel from './selector-inspector-panel';


const Promise = hammerhead.Promise;
Expand Down Expand Up @@ -45,6 +46,8 @@ exports.ProgressPanel = ProgressPanel;
exports.StatusBar = StatusBar;
exports.IframeStatusBar = IframeStatusBar;

exports.SelectorInspectorPanel = SelectorInspectorPanel;


exports.hide = function (hideTopRoot) {
if (hideTopRoot)
Expand Down
54 changes: 54 additions & 0 deletions src/client/ui/selector-inspector-panel/copy-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import hammerhead from './../deps/hammerhead';
import testCafeCore from './../deps/testcafe-core';

import { createElementFromDescriptor } from './utils/create-element-from-descriptor';
import { setStyles } from './utils/set-styles';
import { copy } from './utils/copy';

import * as descriptors from './descriptors';

const nativeMethods = hammerhead.nativeMethods;

const eventUtils = testCafeCore.eventUtils;

const ANIMATION_TIMEOUT = 1200;

const VALUES = {
copy: 'Copy',
copied: 'Copied!',
};

export class CopyButton {
element;
sourceElement;

constructor (sourceElement) {
this.element = createElementFromDescriptor(descriptors.copyButton);
this.sourceElement = sourceElement;

eventUtils.bind(this.element, 'click', () => this._copySelector());
}

_copySelector () {
// eslint-disable-next-line no-restricted-properties
copy(this.sourceElement.value);

this._animate();
}

_animate () {
this._changeAppearance(VALUES.copied, 'bold');

nativeMethods.setTimeout.call(window, () => this._resetAppearance(), ANIMATION_TIMEOUT);
}

_resetAppearance () {
this._changeAppearance(VALUES.copy, '');
}

_changeAppearance (value, fontWeight) {
nativeMethods.inputValueSetter.call(this.element, value);

setStyles(this.element, { fontWeight });
}
}
57 changes: 57 additions & 0 deletions src/client/ui/selector-inspector-panel/descriptors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export const pickButton = {
tag: 'input',
type: 'button',
value: 'Pick',
class: 'pick-button',
};

export const selectorInput = {
tag: 'input',
type: 'text',
class: 'selector-input',
};

export const matchIndicator = {
class: 'match-indicator',
text: 'Not Found',
};

export const expandSelectorsList = {
class: 'expand-selector-list',
};

export const selectorInputContainer = {
class: 'selector-input-container',
};

export const copyButton = {
tag: 'input',
type: 'button',
value: 'Copy',
};

export const selectorsList = {
class: 'selectors-list',
};

export const panel = {
class: 'selector-inspector-panel',
};

export const elementFrame = {
class: 'element-frame',
};

export const tooltip = {
class: 'tooltip',
};

export const arrow = {
class: 'arrow',
};

export const auxiliaryCopyInput = {
tag: 'input',
type: 'text',
class: 'auxiliary',
};
95 changes: 95 additions & 0 deletions src/client/ui/selector-inspector-panel/element-picker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import hammerhead from './../deps/hammerhead';
import testCafeCore from './../deps/testcafe-core';

import { getChildren } from './utils/ui-root';

import { selectorGenerator } from './selector-generator';
import { highlighter } from './highlighter';
import { tooltip } from './tooltip';

const listeners = hammerhead.eventSandbox.listeners;

const styleUtils = testCafeCore.styleUtils;
const serviceUtils = testCafeCore.serviceUtils;

export const ELEMENT_PICKED = 'element-piked';

class ElementPicker extends serviceUtils.EventEmitter {
actualSelectors;
hiddenTestCafeElements = new Map();
handlers;

constructor () {
super();

this.handlers = {
onClick: this._getClickHandler(),
felis2803 marked this conversation as resolved.
Show resolved Hide resolved
onMouseMove: this._getMouseMoveHandler(),
};
}

_hideTestCafeElements () {
const children = getChildren();

for (const element of children) {
const visibilityValue = styleUtils.get(element, 'visibility');

this.hiddenTestCafeElements.set(element, visibilityValue);

styleUtils.set(element, 'visibility', 'hidden');
}
}

_showTestCafeElements () {
this.hiddenTestCafeElements.forEach((visibilityValue, element) => {
styleUtils.set(element, 'visibility', visibilityValue);
});

this.hiddenTestCafeElements.clear();
}

_getClickHandler () {
return () => {
this._showTestCafeElements();

listeners.removeInternalEventBeforeListener(window, ['mousemove'], this.handlers.onMouseMove);
listeners.removeInternalEventBeforeListener(window, ['click'], this.handlers.onClick);

this.emit(ELEMENT_PICKED, this.actualSelectors);

tooltip.hide();
};
}

_getMouseMoveHandler () {
return event => {
const x = event.clientX;
const y = event.clientY;

const target = document.elementFromPoint(x, y);

if (!target)
return;

this.actualSelectors = selectorGenerator.generate(target);

highlighter.stopHighlighting();
highlighter.highlight(target);

// eslint-disable-next-line no-restricted-properties
tooltip.show(this.actualSelectors[0].value, target);
};
}

start (startEvent) {
this._hideTestCafeElements();

listeners.initElementListening(window, ['mousemove', 'click']);
listeners.addFirstInternalEventBeforeListener(window, ['mousemove'], this.handlers.onMouseMove);
listeners.addFirstInternalEventBeforeListener(window, ['click'], this.handlers.onClick);

this.handlers.onMouseMove(startEvent);
}
}

export const elementPicker = new ElementPicker();
Loading