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
Prev Previous commit
Next Next commit
test: added required tests
  • Loading branch information
felis2803 committed Feb 10, 2023
commit 31264da355b831e2928c0646da44c0e74d5a5d76
85 changes: 85 additions & 0 deletions test/functional/fixtures/ui/pages/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!DOCTYPE html>
felis2803 marked this conversation as resolved.
Show resolved Hide resolved
<html><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>TestCafe Example Page</title>
</head>
<body>

<form id="main-form" action="#">
<div class="main-content">
<header>
<h1>Example</h1>
<p>This webpage is used as a sample in TestCafe tutorials.</p>
<div id="shadow-host"></div>
</header>
<div class="row">
<div class="column col-1">
<fieldset>
<legend>Your name:</legend>
<input id="developer-name" name="name" type="text" data-testid="name-input">
<input type="button" id="populate" data-testid="populate-button" value="Populate">
</fieldset>
<fieldset>
<legend>Which features are important to you:</legend>
<p><label for="remote-testing"><input type="checkbox" name="remote" id="remote-testing" data-testid="remote-testing-checkbox">Support for testing on remote devices</label></p>
<p><label for="reusing-js-code"><input type="checkbox" name="re-using" id="reusing-js-code" data-testid="reusing-js-code-checkbox">Re-using existing JavaScript code for testing</label></p>
<p><label for="background-parallel-testing"><input type="checkbox" name="background" id="background-parallel-testing" data-testid="parallel-testing-checkbox">Running tests in background and/or in parallel in multiple browsers</label></p>
<p><label for="continuous-integration-embedding"><input type="checkbox" name="CI" id="continuous-integration-embedding" data-testid="ci-checkbox">Easy embedding into a Continuous integration system</label></p>
<p><label for="traffic-markup-analysis"><input type="checkbox" name="analysis" id="traffic-markup-analysis" data-testid="analysis-checkbox">Advanced traffic and markup analysis</label></p>
</fieldset>
</div>

<div class="column col-2">
<fieldset>
<legend>What is your primary Operating System:</legend>
<p><label for="windows"><input type="radio" name="os" id="windows" value="Windows" data-testid="windows-radio">Windows</label></p>
<p><label for="macos"><input type="radio" name="os" id="macos" value="MacOS" data-testid="macos-radio">MacOS</label></p>
<p><label for="linux"><input type="radio" name="os" id="linux" value="Linux" data-testid="linux-radio">Linux</label></p>
</fieldset>

<fieldset>
<legend>Which TestCafe interface do you use:</legend>
<select id="preferred-interface" name="preferred-interface" data-testid="preferred-interface-select">
<option selected="selected">Command Line</option>
<option>JavaScript API</option>
<option>Both</option>
</select>
</fieldset>
</div>
</div>
<div class="form-bottom">
<fieldset id="tried-section">
<label for="tried-test-cafe"><input type="checkbox" name="tried-test-cafe" id="tried-test-cafe" data-testid="tried-testcafe-checkbox">I have tried TestCafe</label>
</fieldset>

<fieldset>
<legend>How would you rate TestCafe on a scale from 1 to 10</legend>
<div class="slider-container">
<div id="slider" class="ui-slider ui-corner-all ui-slider-horizontal ui-widget ui-widget-content ui-slider-disabled ui-state-disabled"><span tabindex="0" class="ui-slider-handle ui-corner-all ui-state-default" style="left: 0%;"></span></div>
<div class="slider-values">
<div class="slider-value">1</div>
<div class="slider-value">2</div>
<div class="slider-value">3</div>
<div class="slider-value">4</div>
<div class="slider-value">5</div>
<div class="slider-value">6</div>
<div class="slider-value">7</div>
<div class="slider-value">8</div>
<div class="slider-value">9</div>
<div class="slider-value">10</div>
</div>
</div>
</fieldset>

<fieldset>
<legend>Please let us know what you think:</legend>
<textarea id="comments" name="comments" data-testid="comments-area" disabled="disabled"></textarea>
</fieldset>

<button type="submit" id="submit-button" data-testid="submit-button" disabled="disabled">Submit</button>
</div>
</div>
</form>

</body></html>
218 changes: 218 additions & 0 deletions test/functional/fixtures/ui/selector-inspector-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
Object.assign(window, {
isVisible (element) {
return element.style.visibility !== 'hidden' && element.style.display !== 'none';
},

canBeShownInPicking (element) {
return [
'element-frame-hammerhead-shadow-ui',
'tooltip-hammerhead-shadow-ui',
'arrow-hammerhead-shadow-ui',
].includes(element.className);
},

isElementsRectsEql (firstElement, secondElement) {
const firstRect = firstElement.getBoundingClientRect();
const secondRect = secondElement.getBoundingClientRect();

for (const key in firstElement) {
if (firstRect[key] !== secondRect[key])
return false;
}

return true;
},

getSelectedValue () {
const { nativeMethods } = window['%hammerhead%'];

const activeElement = nativeMethods.documentActiveElementGetter.call(document);

return activeElement.value.substring(activeElement.selectionStart, activeElement.selectionend);
},

simulateEvent (element, eventName, options) {
const { eventSandbox } = window['%hammerhead%'];

eventSandbox.eventSimulator[eventName](element, options);
},

click (element) {
this.simulateEvent(element, 'click');
},

mousedown (element) {
this.simulateEvent(element, 'mousedown');
},

mousemove (element) {
const { x, y } = element.getBoundingClientRect();

this.simulateEvent(element, 'mousemove', { clientX: x + 1, clientY: y + 1 });
},

input (element, value) {
this.simulateEvent(element, 'input', value);
},

focus (element) {
this.simulateEvent(element, 'focus');
},

getShadowUIElements () {
return window['%hammerhead%'].shadowUI.root.firstChild.children;
},

pickElement (element) {
this.mousemove(element);
this.click(element);
},

getGeneratedSelectors () {
const { elementPicker } = window['%testCafeDriverInstance%'].selectorInspectorPanel;

return elementPicker.actualSelectors.map(selector => selector.value);
},

querySelector (cssSelector, element = document) {
const { nativeMethods } = window['%hammerhead%'];

return nativeMethods.querySelector.call(element, cssSelector);
},

querySelectorAll (cssSelector, element = document) {
const { nativeMethods } = window['%hammerhead%'];

return nativeMethods.querySelectorAll.call(element, cssSelector);
},

async retryExecute (fn, retryTimeout = 80) {
return new Promise(resolve => {
const intervalId = setInterval(() => fn(result => {
clearInterval(intervalId);
resolve(result);
}), retryTimeout);
});
},

async getElement (cssSelector) {
return this.retryExecute(resolve => {
const element = this.querySelector(cssSelector);

if (element)
resolve(element);
});
},

async getElements (cssSelector) {
return this.retryExecute(resolve => {
const elements = this.querySelectorAll(cssSelector);

if (elements && elements.length)
resolve(elements);
});
},

async resumeTest () {
const resumeButton = await this.getElement('.resume-hammerhead-shadow-ui');

this.mousedown(resumeButton);
},

async startPicking () {
const pickButton = await this.getElement('.pick-button-hammerhead-shadow-ui');

this.click(pickButton);
},

async getSelectorInput () {
return this.getElement('.selector-input-hammerhead-shadow-ui');
},

async getSelectorInputValue () {
return this.getSelectorInput().then(input => input.value);
},

async typeSelector (value) {
const selectorInput = await this.getSelectorInput();

selectorInput.value = value;

selectorInput.focus();
},

async getMatchIndicator () {
return this.getElement('.match-indicator-hammerhead-shadow-ui');
},

async getMatchIndicatorInnerText () {
return this.getMatchIndicator().then(indicator => indicator.innerText);
},

async expandSelectorsList () {
const expandButton = await this.getElement('.expand-selector-list-hammerhead-shadow-ui');

this.click(expandButton);
},

async getSelectorsList () {
return this.getElement('.selectors-list-hammerhead-shadow-ui');
},

async getSelectorsListValues () {
const selectorsList = await this.getSelectorsList();
const values = [];

for (const selectorValueElement of selectorsList.children)
values.push(selectorValueElement.innerText);

return values;
},

async selectSelectorFromList (index) {
await this.expandSelectorsList();

const selectorsList = await this.getSelectorsList();
const selectorValueElement = selectorsList.children[index];

this.click(selectorValueElement);
},

async getElementFrames () {
const RENDERING_DELAY = 200;
const CSS_SELECTOR = '.element-frame-hammerhead-shadow-ui';

await this.getElement(CSS_SELECTOR);

await new Promise(resolve => setTimeout(resolve, RENDERING_DELAY));

return this.querySelectorAll(CSS_SELECTOR);
},

async mockOnceCopyCommand () {
const originExecCommand = document.execCommand;

return new Promise(resolve => {
document.execCommand = cmd => {
if (cmd !== 'copy')
return originExecCommand.call(document, cmd);

document.execCommand = originExecCommand;

resolve(this.getSelectedValue());

return document.execCommand(cmd);
};
});
},

async copySelector () {
const copyButton = await this.getElement('input[value="Copy"]');

const promise = this.mockOnceCopyCommand();

this.click(copyButton);

return promise;
},
});
69 changes: 58 additions & 11 deletions test/functional/fixtures/ui/test.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,67 @@
const config = require('../../config');


describe('TestCafe UI', () => {
it('Should display correct status', () => {
return runTests('./testcafe-fixtures/status-bar-test.js', 'Show status prefix', { assertionTimeout: 3000 });
});
describe('Status Bar', () => {
it('Should display correct status', () => {
return runTests('./testcafe-fixtures/status-bar-test.js', 'Show status prefix', { assertionTimeout: 3000 });
});

it('Hide elements when resizing the window', () => {
return runTests('./testcafe-fixtures/status-bar-test.js', 'Hide elements when resizing the window', { skip: ['android', 'ipad', 'iphone', 'edge', 'safari'] });
});

it('Should hide the status bar even if document was hidden during initialization (GH-7384)', function () {
// NOTE: the test needs direct access to the CDP client through the test controller
if (config.experimentalDebug)
this.skip();

it('Hide elements when resizing the window', () => {
return runTests('./testcafe-fixtures/status-bar-test.js', 'Hide elements when resizing the window', { skip: ['android', 'ipad', 'iphone', 'edge', 'safari'] });
return runTests('./testcafe-fixtures/status-bar-test.js', 'Hide status bar after mouse move', { only: ['chrome'] });
});
});

it('Should hide the status bar even if document was hidden during initialization (GH-7384)', function () {
// NOTE: the test needs direct access to the CDP client through the test controller
if (config.experimentalDebug)
this.skip();
describe('Selector Inspector', () => {
function runTest (testName) {
return runTests('./testcafe-fixtures/selector-inspector-test.js', testName);
}

it('panel should be shown in debug mode', () => {
return runTest('Show panel');
});

it('should hide TestCafe elements while piking', () => {
return runTest('Hide TestCafe element while picking');
});

it('should generate valid selector', () => {
return runTest('Generate selector');
});

it('should fill the selectors list with the generated selectors', () => {
return runTest('Fill the selectors list');
});

it('should indicate the correct number of elements matching the selector', () => {
return runTest('Indicate matching');
});

it('should indicate if the selector is invalid on input', () => {
return runTest('Indicate invalid');
});

it('should indicate that no matches on input', () => {
return runTest('Indicate no matching');
});

it('should highlight matches elements on input', () => {
return runTest('Highlight elements');
});

it('should place a selector selected from the list in the input field', () => {
return runTest('Select selector');
});

return runTests('./testcafe-fixtures/status-bar-test.js', 'Hide status bar after mouse move', { only: ['chrome'] });
it('should copy selector', () => {
return runTest('Copy selector');
});
});
});
Loading