mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 05:46:28 +03:00
chore: rework assert dialog (#28043)
This commit is contained in:
parent
5f527fedb1
commit
b004c1a0a7
@ -14,16 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const highlightCSS = `
|
||||
:host {
|
||||
font-size: 13px;
|
||||
font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
x-pw-tooltip {
|
||||
backdrop-filter: blur(5px);
|
||||
background-color: white;
|
||||
color: #222;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0.5rem 1.2rem rgba(0,0,0,.3);
|
||||
display: none;
|
||||
font-family: 'Dank Mono', 'Operator Mono', Inconsolata, 'Fira Mono',
|
||||
'SF Mono', Monaco, 'Droid Sans Mono', 'Source Code Pro', monospace;
|
||||
font-size: 12.8px;
|
||||
font-weight: normal;
|
||||
left: 0;
|
||||
@ -31,11 +33,27 @@ x-pw-tooltip {
|
||||
max-width: 600px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
x-pw-tooltip-body {
|
||||
align-items: center;
|
||||
padding: 3.2px 5.12px 3.2px;
|
||||
|
||||
x-pw-dialog {
|
||||
background-color: white;
|
||||
pointer-events: auto;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0.5rem 1.2rem rgba(0,0,0,.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
min-width: 500px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
x-pw-dialog-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
x-pw-highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -43,6 +61,7 @@ x-pw-highlight {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
x-pw-action-point {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
@ -52,20 +71,24 @@ x-pw-action-point {
|
||||
margin: -10px 0 0 -10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
x-pw-separator {
|
||||
height: 1px;
|
||||
margin: 6px 9px;
|
||||
background: rgb(148 148 148 / 90%);
|
||||
}
|
||||
|
||||
x-pw-tool-gripper {
|
||||
height: 28px;
|
||||
width: 24px;
|
||||
margin: 2px 0;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
x-pw-tool-gripper:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
x-pw-tool-gripper > x-div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -79,11 +102,20 @@ x-pw-tool-gripper > x-div {
|
||||
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z' /></svg>");
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
x-pw-tool-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
x-pw-tools-list {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
x-pw-tool-item {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
@ -91,9 +123,11 @@ x-pw-tool-item {
|
||||
width: 28px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
x-pw-tool-item:not(.disabled):hover {
|
||||
background-color: hsl(0, 0%, 86%);
|
||||
}
|
||||
|
||||
x-pw-tool-item > x-div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -105,45 +139,52 @@ x-pw-tool-item > x-div {
|
||||
mask-size: 16px;
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
|
||||
x-pw-tool-item.disabled > x-div {
|
||||
background-color: rgba(97, 97, 97, 0.5);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
x-pw-tool-item.active > x-div {
|
||||
background-color: #006ab1;
|
||||
}
|
||||
|
||||
x-pw-tool-item.record.active > x-div {
|
||||
background-color: #a1260d;
|
||||
}
|
||||
|
||||
x-pw-tool-item.accept > x-div {
|
||||
background-color: #388a34;
|
||||
}
|
||||
x-pw-tool-item.cancel > x-div {
|
||||
background-color: #e51400;
|
||||
}
|
||||
|
||||
x-pw-tool-item.record > x-div {
|
||||
/* codicon: circle-large-filled */
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M8 1a6.8 6.8 0 0 1 1.86.253 6.899 6.899 0 0 1 3.083 1.805 6.903 6.903 0 0 1 1.804 3.083C14.916 6.738 15 7.357 15 8s-.084 1.262-.253 1.86a6.9 6.9 0 0 1-.704 1.674 7.157 7.157 0 0 1-2.516 2.509 6.966 6.966 0 0 1-1.668.71A6.984 6.984 0 0 1 8 15a6.984 6.984 0 0 1-1.86-.246 7.098 7.098 0 0 1-1.674-.711 7.3 7.3 0 0 1-1.415-1.094 7.295 7.295 0 0 1-1.094-1.415 7.098 7.098 0 0 1-.71-1.675A6.985 6.985 0 0 1 1 8c0-.643.082-1.262.246-1.86a6.968 6.968 0 0 1 .711-1.667 7.156 7.156 0 0 1 2.509-2.516 6.895 6.895 0 0 1 1.675-.704A6.808 6.808 0 0 1 8 1z'/></svg>");
|
||||
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M8 1a6.8 6.8 0 0 1 1.86.253 6.899 6.899 0 0 1 3.083 1.805 6.903 6.903 0 0 1 1.804 3.083C14.916 6.738 15 7.357 15 8s-.084 1.262-.253 1.86a6.9 6.9 0 0 1-.704 1.674 7.157 7.157 0 0 1-2.516 2.509 6.966 6.966 0 0 1-1.668.71A6.984 6.984 0 0 1 8 15a6.984 6.984 0 0 1-1.86-.246 7.098 7.098 0 0 1-1.674-.711 7.3 7.3 0 0 1-1.415-1.094 7.295 7.295 0 0 1-1.094-1.415 7.098 7.098 0 0 1-.71-1.675A6.985 6.985 0 0 1 1 8c0-.643.082-1.262.246-1.86a6.968 6.968 0 0 1 .711-1.667 7.156 7.156 0 0 1 2.509-2.516 6.895 6.895 0 0 1 1.675-.704A6.808 6.808 0 0 1 8 1z'/></svg>");
|
||||
}
|
||||
|
||||
x-pw-tool-item.pick-locator > x-div {
|
||||
/* codicon: inspect */
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z'/></svg>");
|
||||
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z'/></svg>");
|
||||
}
|
||||
|
||||
x-pw-tool-item.assert > x-div {
|
||||
/* codicon: check-all */
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M15.62 3.596L7.815 12.81l-.728-.033L4 8.382l.754-.53 2.744 3.907L14.917 3l.703.596z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M7.234 8.774l4.386-5.178L10.917 3l-4.23 4.994.547.78zm-1.55.403l.548.78-.547-.78zm-1.617 1.91l.547.78-.799.943-.728-.033L0 8.382l.754-.53 2.744 3.907.57-.672z'/></svg>");
|
||||
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M15.62 3.596L7.815 12.81l-.728-.033L4 8.382l.754-.53 2.744 3.907L14.917 3l.703.596z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M7.234 8.774l4.386-5.178L10.917 3l-4.23 4.994.547.78zm-1.55.403l.548.78-.547-.78zm-1.617 1.91l.547.78-.799.943-.728-.033L0 8.382l.754-.53 2.744 3.907.57-.672z'/></svg>");
|
||||
}
|
||||
|
||||
x-pw-tool-item.accept > x-div {
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/></svg>");
|
||||
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/></svg>");
|
||||
}
|
||||
|
||||
x-pw-tool-item.cancel > x-div {
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/></svg>");
|
||||
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/></svg>");
|
||||
}
|
||||
|
||||
x-pw-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -152,21 +193,52 @@ x-pw-overlay {
|
||||
background: transparent;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
x-pw-overlay x-pw-tools-list {
|
||||
background-color: #ffffffdd;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 5px 5px;
|
||||
border-radius: 3px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
x-pw-overlay x-pw-tool-item {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
input.locator-editor {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
flex: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
input.locator-editor:focus,
|
||||
textarea.text-editor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea.text-editor {
|
||||
font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif;
|
||||
flex: auto;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
x-div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
x-spacer {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*[hidden] {
|
||||
display: none !important;
|
||||
}`;
|
||||
}
|
@ -19,7 +19,7 @@ import type { ParsedSelector } from '../../utils/isomorphic/selectorParser';
|
||||
import type { InjectedScript } from './injectedScript';
|
||||
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
||||
import type { Language } from '../../utils/isomorphic/locatorGenerators';
|
||||
import { highlightCSS } from './highlight.css';
|
||||
import highlightCSS from './highlight.css?inline';
|
||||
|
||||
type HighlightEntry = {
|
||||
targetElement: Element,
|
||||
@ -34,9 +34,6 @@ type HighlightEntry = {
|
||||
export type HighlightOptions = {
|
||||
tooltipText?: string;
|
||||
color?: string;
|
||||
anchorGetter?: (element: Element) => DOMRect;
|
||||
toolbar?: Element[];
|
||||
interactive?: boolean;
|
||||
};
|
||||
|
||||
export class Highlight {
|
||||
@ -63,7 +60,12 @@ export class Highlight {
|
||||
this._glassPaneElement.style.pointerEvents = 'none';
|
||||
this._glassPaneElement.style.display = 'flex';
|
||||
this._glassPaneElement.style.backgroundColor = 'transparent';
|
||||
|
||||
for (const eventName of ['click', 'auxclick', 'dragstart', 'input', 'keydown', 'keyup', 'pointerdown', 'pointerup', 'mousedown', 'mouseup', 'mousemove', 'mouseleave', 'focus', 'scroll']) {
|
||||
this._glassPaneElement.addEventListener(eventName, e => {
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
});
|
||||
}
|
||||
this._actionPointElement = document.createElement('x-pw-action-point');
|
||||
this._actionPointElement.setAttribute('hidden', 'true');
|
||||
this._glassPaneShadow = this._glassPaneElement.attachShadow({ mode: this._isUnderTest ? 'open' : 'closed' });
|
||||
@ -145,26 +147,12 @@ export class Highlight {
|
||||
let tooltipElement;
|
||||
if (options.tooltipText) {
|
||||
tooltipElement = this._injectedScript.document.createElement('x-pw-tooltip');
|
||||
this._glassPaneShadow.appendChild(tooltipElement);
|
||||
const suffix = elements.length > 1 ? ` [${i + 1} of ${elements.length}]` : '';
|
||||
tooltipElement.textContent = options.tooltipText + suffix;
|
||||
tooltipElement.style.top = '0';
|
||||
tooltipElement.style.left = '0';
|
||||
tooltipElement.style.display = 'flex';
|
||||
tooltipElement.style.flexDirection = 'column';
|
||||
tooltipElement.style.alignItems = 'start';
|
||||
if (options.interactive)
|
||||
tooltipElement.style.pointerEvents = 'auto';
|
||||
|
||||
if (options.toolbar) {
|
||||
const toolbar = this._injectedScript.document.createElement('x-pw-tools-list');
|
||||
tooltipElement.appendChild(toolbar);
|
||||
for (const toolbarElement of options.toolbar)
|
||||
toolbar.appendChild(toolbarElement);
|
||||
}
|
||||
const bodyElement = this._injectedScript.document.createElement('x-pw-tooltip-body');
|
||||
tooltipElement.appendChild(bodyElement);
|
||||
|
||||
this._glassPaneShadow.appendChild(tooltipElement);
|
||||
const suffix = elements.length > 1 ? ` [${i + 1} of ${elements.length}]` : '';
|
||||
bodyElement.textContent = options.tooltipText + suffix;
|
||||
}
|
||||
this._highlightEntries.push({ targetElement: elements[i], tooltipElement, highlightElement, tooltipText: options.tooltipText });
|
||||
}
|
||||
@ -176,25 +164,7 @@ export class Highlight {
|
||||
continue;
|
||||
|
||||
// Position tooltip, if any.
|
||||
const tooltipWidth = entry.tooltipElement.offsetWidth;
|
||||
const tooltipHeight = entry.tooltipElement.offsetHeight;
|
||||
const totalWidth = this._glassPaneElement.offsetWidth;
|
||||
const totalHeight = this._glassPaneElement.offsetHeight;
|
||||
|
||||
const anchorBox = options.anchorGetter ? options.anchorGetter(entry.targetElement) : entry.box;
|
||||
let anchorLeft = anchorBox.left;
|
||||
if (anchorLeft + tooltipWidth > totalWidth - 5)
|
||||
anchorLeft = totalWidth - tooltipWidth - 5;
|
||||
let anchorTop = anchorBox.bottom + 5;
|
||||
if (anchorTop + tooltipHeight > totalHeight - 5) {
|
||||
// If can't fit below, either position above...
|
||||
if (anchorBox.top > tooltipHeight + 5) {
|
||||
anchorTop = anchorBox.top - tooltipHeight - 5;
|
||||
} else {
|
||||
// Or on top in case of large element
|
||||
anchorTop = totalHeight - 5 - tooltipHeight;
|
||||
}
|
||||
}
|
||||
const { anchorLeft, anchorTop } = this.tooltipPosition(entry.box, entry.tooltipElement);
|
||||
entry.tooltipTop = anchorTop;
|
||||
entry.tooltipLeft = anchorLeft;
|
||||
}
|
||||
@ -219,6 +189,33 @@ export class Highlight {
|
||||
console.error('Highlight box for test: ' + JSON.stringify({ x: box.x, y: box.y, width: box.width, height: box.height })); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
firstBox(): DOMRect | undefined {
|
||||
return this._highlightEntries[0]?.box;
|
||||
}
|
||||
|
||||
tooltipPosition(box: DOMRect, tooltipElement: HTMLElement) {
|
||||
const tooltipWidth = tooltipElement.offsetWidth;
|
||||
const tooltipHeight = tooltipElement.offsetHeight;
|
||||
const totalWidth = this._glassPaneElement.offsetWidth;
|
||||
const totalHeight = this._glassPaneElement.offsetHeight;
|
||||
|
||||
let anchorLeft = box.left;
|
||||
if (anchorLeft + tooltipWidth > totalWidth - 5)
|
||||
anchorLeft = totalWidth - tooltipWidth - 5;
|
||||
let anchorTop = box.bottom + 5;
|
||||
if (anchorTop + tooltipHeight > totalHeight - 5) {
|
||||
// If can't fit below, either position above...
|
||||
if (box.top > tooltipHeight + 5) {
|
||||
anchorTop = box.top - tooltipHeight - 5;
|
||||
} else {
|
||||
// Or on top in case of large element
|
||||
anchorTop = totalHeight - 5 - tooltipHeight;
|
||||
}
|
||||
}
|
||||
return { anchorLeft, anchorTop };
|
||||
}
|
||||
|
||||
private _highlightIsUpToDate(elements: Element[], tooltipText: string | undefined): boolean {
|
||||
if (elements.length !== this._highlightEntries.length)
|
||||
return false;
|
||||
|
@ -20,10 +20,12 @@ import { generateSelector } from '../injected/selectorGenerator';
|
||||
import type { Point } from '../../common/types';
|
||||
import type { Mode, OverlayState, UIState } from '@recorder/recorderTypes';
|
||||
import { Highlight, type HighlightOptions } from '../injected/highlight';
|
||||
import { enclosingElement, isInsideScope, parentElementOrShadowHost } from './domUtils';
|
||||
import { isInsideScope } from './domUtils';
|
||||
import { elementText } from './selectorUtils';
|
||||
import { escapeWithQuotes, normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
|
||||
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
||||
import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
|
||||
import { parseSelector } from '@isomorphic/selectorParser';
|
||||
import { normalizeWhiteSpace } from '@isomorphic/stringUtils';
|
||||
|
||||
interface RecorderDelegate {
|
||||
performAction?(action: actions.Action): Promise<void>;
|
||||
@ -442,217 +444,187 @@ class RecordActionTool implements RecorderTool {
|
||||
|
||||
class TextAssertionTool implements RecorderTool {
|
||||
private _hoverHighlight: HighlightModel | null = null;
|
||||
private _selectionHighlight: HighlightModel | null = null;
|
||||
private _selectionText: { selectedText: string, fullText: string } | null = null;
|
||||
private _inputHighlight: HighlightModel | null = null;
|
||||
private _action: actions.AssertAction | null = null;
|
||||
private _dialogElement: HTMLElement | null = null;
|
||||
private _acceptButton: HTMLElement;
|
||||
private _cancelButton: HTMLElement;
|
||||
private _keyboardListener: ((event: KeyboardEvent) => void) | undefined;
|
||||
|
||||
constructor(private _recorder: Recorder) {
|
||||
this._acceptButton = this._recorder.document.createElement('x-pw-tool-item');
|
||||
this._acceptButton.title = 'Accept';
|
||||
this._acceptButton.classList.add('accept');
|
||||
this._acceptButton.appendChild(this._recorder.document.createElement('x-div'));
|
||||
this._acceptButton.addEventListener('click', () => this._commitAction());
|
||||
this._acceptButton.addEventListener('click', () => this._commit());
|
||||
|
||||
this._cancelButton = this._recorder.document.createElement('x-pw-tool-item');
|
||||
this._cancelButton.title = 'Close';
|
||||
this._cancelButton.classList.add('cancel');
|
||||
this._cancelButton.appendChild(this._recorder.document.createElement('x-div'));
|
||||
this._cancelButton.addEventListener('click', () => this._cancelAction());
|
||||
this._cancelButton.addEventListener('click', () => this._closeDialog());
|
||||
}
|
||||
|
||||
cursor() {
|
||||
return 'text';
|
||||
return 'pointer';
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this._closeDialog();
|
||||
this._hoverHighlight = null;
|
||||
this._selectionHighlight = null;
|
||||
this._selectionText = null;
|
||||
this._inputHighlight = null;
|
||||
}
|
||||
|
||||
onClick(event: MouseEvent) {
|
||||
if (!this._dialogElement)
|
||||
this._showDialog();
|
||||
consumeEvent(event);
|
||||
const selection = this._recorder.document.getSelection();
|
||||
if (event.detail === 1 && selection && !selection.toString() && !this._inputHighlight) {
|
||||
const target = this._recorder.deepEventTarget(event);
|
||||
selection.selectAllChildren(target);
|
||||
this._updateSelectionHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent) {
|
||||
const target = this._recorder.deepEventTarget(event);
|
||||
if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) {
|
||||
this._recorder.injectedScript.window.getSelection()?.empty();
|
||||
this._inputHighlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
|
||||
this._showHighlight(true);
|
||||
consumeEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
this._inputHighlight = null;
|
||||
this._hoverHighlight = null;
|
||||
this._updateSelectionHighlight();
|
||||
}
|
||||
|
||||
onMouseUp(event: MouseEvent) {
|
||||
this._updateSelectionHighlight();
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
const selection = this._recorder.document.getSelection();
|
||||
if (selection && selection.toString()) {
|
||||
this._updateSelectionHighlight();
|
||||
return;
|
||||
}
|
||||
if (this._inputHighlight || event.buttons)
|
||||
if (this._dialogElement)
|
||||
return;
|
||||
const target = this._recorder.deepEventTarget(event);
|
||||
if (this._hoverHighlight?.elements[0] === target)
|
||||
return;
|
||||
this._hoverHighlight = elementText(new Map(), target).full ? { elements: [target], selector: '' } : null;
|
||||
this._hoverHighlight = target.nodeName === 'INPUT' || target.nodeName === 'TEXTAREA' || elementText(new Map(), target).full ? { elements: [target], selector: '' } : null;
|
||||
this._recorder.updateHighlight(this._hoverHighlight, true, { color: '#8acae480' });
|
||||
}
|
||||
|
||||
onDragStart(event: DragEvent) {
|
||||
consumeEvent(event);
|
||||
}
|
||||
|
||||
onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
const selection = this._recorder.document.getSelection();
|
||||
if (selection && selection.toString())
|
||||
this._resetSelectionAndHighlight();
|
||||
else
|
||||
this._recorder.delegate.setMode?.('recording');
|
||||
consumeEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
this._commitAction();
|
||||
consumeEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow keys that control text selection.
|
||||
if (!['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'Shift', 'Control', 'Meta', 'Alt', 'AltGraph'].includes(event.key)) {
|
||||
consumeEvent(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onKeyUp(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape')
|
||||
this._recorder.delegate.setMode?.('recording');
|
||||
consumeEvent(event);
|
||||
}
|
||||
|
||||
onScroll(event: Event) {
|
||||
this._hoverHighlight = null;
|
||||
this._showHighlight(false);
|
||||
}
|
||||
|
||||
private _generateAction(): actions.Action | null {
|
||||
if (this._inputHighlight) {
|
||||
const target = this._inputHighlight.elements[0] as HTMLInputElement;
|
||||
if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes(target.type.toLowerCase())) {
|
||||
private _generateAction(): actions.AssertAction | null {
|
||||
const target = this._hoverHighlight?.elements[0];
|
||||
if (!target)
|
||||
return null;
|
||||
if (target.nodeName === 'INPUT' || target.nodeName === 'TEXTAREA') {
|
||||
const { selector } = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
|
||||
if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes((target as HTMLInputElement).type.toLowerCase())) {
|
||||
return {
|
||||
name: 'assertChecked',
|
||||
selector: this._inputHighlight.selector,
|
||||
selector,
|
||||
signals: [],
|
||||
// Interestingly, inputElement.checked is reversed inside this event handler.
|
||||
checked: !(target as HTMLInputElement).checked,
|
||||
checked: (target as HTMLInputElement).checked,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: 'assertValue',
|
||||
selector: this._inputHighlight.selector,
|
||||
selector,
|
||||
signals: [],
|
||||
value: target.value,
|
||||
value: (target as HTMLInputElement).value,
|
||||
};
|
||||
}
|
||||
} else if (this._selectionText && this._selectionHighlight) {
|
||||
} else {
|
||||
const { selector } = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true });
|
||||
return {
|
||||
name: 'assertText',
|
||||
selector: this._selectionHighlight.selector,
|
||||
selector,
|
||||
signals: [],
|
||||
text: this._selectionText.selectedText,
|
||||
substring: this._selectionText.fullText !== this._selectionText.selectedText,
|
||||
text: target.textContent!,
|
||||
substring: true,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _generateActionPreview() {
|
||||
const action = this._generateAction();
|
||||
// TODO: support other languages, maybe unify with code generator?
|
||||
private _renderValue(action: actions.Action) {
|
||||
if (action?.name === 'assertText')
|
||||
return `expect(${asLocator(this._recorder.state.language, action.selector)}).${action.substring ? 'toContainText' : 'toHaveText'}(${escapeWithQuotes(action.text)})`;
|
||||
return normalizeWhiteSpace(action.text);
|
||||
if (action?.name === 'assertChecked')
|
||||
return `expect(${asLocator(this._recorder.state.language, action.selector)})${action.checked ? '' : '.not'}.toBeChecked()`;
|
||||
if (action?.name === 'assertValue') {
|
||||
const assertion = action.value ? `toHaveValue(${escapeWithQuotes(action.value)})` : `toBeEmpty()`;
|
||||
return `expect(${asLocator(this._recorder.state.language, action.selector)}).${assertion}`;
|
||||
}
|
||||
return String(action.checked);
|
||||
if (action?.name === 'assertValue')
|
||||
return action.value;
|
||||
return '';
|
||||
}
|
||||
|
||||
private _commitAction() {
|
||||
const action = this._generateAction();
|
||||
if (action) {
|
||||
this._resetSelectionAndHighlight();
|
||||
this._recorder.delegate.recordAction?.(action);
|
||||
this._recorder.delegate.setMode?.('recording');
|
||||
}
|
||||
}
|
||||
|
||||
private _cancelAction() {
|
||||
this._resetSelectionAndHighlight();
|
||||
}
|
||||
|
||||
private _resetSelectionAndHighlight() {
|
||||
this._selectionHighlight = null;
|
||||
this._selectionText = null;
|
||||
this._inputHighlight = null;
|
||||
this._recorder.injectedScript.window.getSelection()?.empty();
|
||||
this._recorder.updateHighlight(null, false);
|
||||
}
|
||||
|
||||
private _updateSelectionHighlight() {
|
||||
if (this._inputHighlight)
|
||||
private _commit() {
|
||||
if (!this._action || !this._dialogElement)
|
||||
return;
|
||||
const selection = this._recorder.document.getSelection();
|
||||
const selectedText = normalizeWhiteSpace(selection?.toString() || '');
|
||||
let highlight: HighlightModel | null = null;
|
||||
if (selection && selection.focusNode && selection.anchorNode && selectedText) {
|
||||
const focusElement = enclosingElement(selection.focusNode);
|
||||
let lcaElement = focusElement ? enclosingElement(selection.anchorNode) : undefined;
|
||||
while (lcaElement && !isInsideScope(lcaElement, focusElement))
|
||||
lcaElement = parentElementOrShadowHost(lcaElement);
|
||||
highlight = lcaElement ? generateSelector(this._recorder.injectedScript, lcaElement, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }) : null;
|
||||
}
|
||||
const fullText = highlight ? normalizeWhiteSpace(elementText(new Map(), highlight.elements[0]).full) : '';
|
||||
const selectionText = highlight ? { selectedText, fullText } : null;
|
||||
if (highlight?.selector === this._selectionHighlight?.selector && this._selectionText?.fullText === selectionText?.fullText && this._selectionText?.selectedText === selectionText?.selectedText)
|
||||
return;
|
||||
this._selectionHighlight = highlight;
|
||||
this._selectionText = selectionText;
|
||||
this._showHighlight(true);
|
||||
this._closeDialog();
|
||||
this._recorder.delegate.recordAction?.(this._action);
|
||||
this._recorder.delegate.setMode?.('recording');
|
||||
}
|
||||
|
||||
private _showHighlight(userGesture: boolean) {
|
||||
const options: HighlightOptions = {
|
||||
color: '#6fdcbd38',
|
||||
tooltipText: this._generateActionPreview(),
|
||||
toolbar: [this._acceptButton, this._cancelButton],
|
||||
interactive: true,
|
||||
private _showDialog() {
|
||||
const target = this._hoverHighlight?.elements[0];
|
||||
if (!target)
|
||||
return;
|
||||
this._action = this._generateAction();
|
||||
if (!this._action)
|
||||
return;
|
||||
|
||||
this._dialogElement = this._recorder.document.createElement('x-pw-dialog');
|
||||
this._keyboardListener = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
this._closeDialog();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
||||
if (this._dialogElement)
|
||||
this._commit();
|
||||
return;
|
||||
}
|
||||
};
|
||||
if (this._inputHighlight) {
|
||||
this._recorder.updateHighlight(this._inputHighlight, userGesture, options);
|
||||
} else {
|
||||
options.anchorGetter = (e: Element) => this._recorder.document.getSelection()?.getRangeAt(0)?.getBoundingClientRect() || e.getBoundingClientRect();
|
||||
this._recorder.updateHighlight(this._selectionHighlight, userGesture, options);
|
||||
}
|
||||
this._recorder.document.addEventListener('keydown', this._keyboardListener, true);
|
||||
const toolbarElement = this._recorder.document.createElement('x-pw-tools-list');
|
||||
toolbarElement.appendChild(this._createLabel(this._action));
|
||||
toolbarElement.appendChild(this._recorder.document.createElement('x-spacer'));
|
||||
toolbarElement.appendChild(this._acceptButton);
|
||||
toolbarElement.appendChild(this._cancelButton);
|
||||
|
||||
this._dialogElement.appendChild(toolbarElement);
|
||||
const bodyElement = this._recorder.document.createElement('x-pw-dialog-body');
|
||||
const locatorElement = this._recorder.document.createElement('input');
|
||||
locatorElement.classList.add('locator-editor');
|
||||
locatorElement.value = asLocator(this._recorder.state.language, this._action.selector);
|
||||
locatorElement.addEventListener('input', () => {
|
||||
if (this._action) {
|
||||
const selector = locatorOrSelectorAsSelector(this._recorder.state.language, locatorElement.value, this._recorder.state.testIdAttributeName);
|
||||
const model: HighlightModel = {
|
||||
selector,
|
||||
elements: this._recorder.injectedScript.querySelectorAll(parseSelector(selector), this._recorder.document),
|
||||
};
|
||||
this._action.selector = selector;
|
||||
this._recorder.updateHighlight(model, true);
|
||||
}
|
||||
});
|
||||
const textElement = this._recorder.document.createElement('textarea');
|
||||
textElement.value = this._renderValue(this._action);
|
||||
textElement.classList.add('text-editor');
|
||||
|
||||
textElement.addEventListener('input', () => {
|
||||
if (this._action?.name === 'assertText')
|
||||
this._action.text = normalizeWhiteSpace(elementText(new Map(), textElement).full);
|
||||
if (this._action?.name === 'assertChecked')
|
||||
this._action.checked = textElement.value === 'true';
|
||||
if (this._action?.name === 'assertValue')
|
||||
this._action.value = textElement.value;
|
||||
});
|
||||
|
||||
bodyElement.appendChild(locatorElement);
|
||||
bodyElement.appendChild(textElement);
|
||||
this._dialogElement.appendChild(bodyElement);
|
||||
this._recorder.highlight.appendChild(this._dialogElement);
|
||||
const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement);
|
||||
this._dialogElement.style.top = position.anchorTop + 'px';
|
||||
this._dialogElement.style.left = position.anchorLeft + 'px';
|
||||
textElement.focus();
|
||||
}
|
||||
|
||||
private _createLabel(action: actions.AssertAction) {
|
||||
const labelElement = this._recorder.document.createElement('x-pw-tool-label');
|
||||
labelElement.textContent = action.name === 'assertText' ? 'Assert text' : action.name === 'assertValue' ? 'Assert value' : 'Assert checked';
|
||||
return labelElement;
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
if (!this._dialogElement)
|
||||
return;
|
||||
this._dialogElement.remove();
|
||||
this._recorder.document.removeEventListener('keydown', this._keyboardListener!);
|
||||
this._dialogElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,6 +114,7 @@ export type AssertCheckedAction = ActionBase & {
|
||||
};
|
||||
|
||||
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction;
|
||||
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction;
|
||||
|
||||
// Signals.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user