chore: place overlay inside the glass pane (#28026)

This commit is contained in:
Pavel Feldman 2023-11-07 19:36:12 -08:00 committed by GitHub
parent 1a8b61199f
commit 061ded19b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 252 additions and 200 deletions

View file

@ -0,0 +1,172 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const highlightCSS = `
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;
line-height: 1.5;
max-width: 600px;
position: absolute;
top: 0;
}
x-pw-tooltip-body {
align-items: center;
padding: 3.2px 5.12px 3.2px;
}
x-pw-highlight {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
}
x-pw-action-point {
position: absolute;
width: 20px;
height: 20px;
background: red;
border-radius: 10px;
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%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: 20px;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 16px;
-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='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>");
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-tools-list {
display: flex;
width: 100%;
border-bottom: 1px solid #dddddd;
}
x-pw-tool-item {
pointer-events: auto;
cursor: pointer;
height: 28px;
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%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: 20px;
mask-repeat: no-repeat;
mask-position: center;
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;
max-width: min-content;
z-index: 2147483647;
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;
}
x-div {
display: block;
}
* {
box-sizing: border-box;
}
*[hidden] {
display: none !important;
}`;

View file

@ -19,6 +19,7 @@ import type { ParsedSelector } from '../../utils/isomorphic/selectorParser';
import type { InjectedScript } from './injectedScript'; import type { InjectedScript } from './injectedScript';
import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators';
import { highlightCSS } from './highlight.css';
type HighlightEntry = { type HighlightEntry = {
targetElement: Element, targetElement: Element,
@ -34,7 +35,8 @@ export type HighlightOptions = {
tooltipText?: string; tooltipText?: string;
color?: string; color?: string;
anchorGetter?: (element: Element) => DOMRect; anchorGetter?: (element: Element) => DOMRect;
decorateTooltip?: (tooltip: Element) => void; toolbar?: Element[];
interactive?: boolean;
}; };
export class Highlight { export class Highlight {
@ -67,44 +69,7 @@ export class Highlight {
this._glassPaneShadow = this._glassPaneElement.attachShadow({ mode: this._isUnderTest ? 'open' : 'closed' }); this._glassPaneShadow = this._glassPaneElement.attachShadow({ mode: this._isUnderTest ? 'open' : 'closed' });
this._glassPaneShadow.appendChild(this._actionPointElement); this._glassPaneShadow.appendChild(this._actionPointElement);
const styleElement = document.createElement('style'); const styleElement = document.createElement('style');
styleElement.textContent = ` styleElement.textContent = highlightCSS;
x-pw-tooltip {
align-items: center;
backdrop-filter: blur(5px);
background-color: rgba(0, 0, 0, 0.7);
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 3.6px 3.7px,
rgba(0, 0, 0, 0.15) 0px 12.1px 12.3px,
rgba(0, 0, 0, 0.1) 0px -2px 4px,
rgba(0, 0, 0, 0.15) 0px -12.1px 24px,
rgba(0, 0, 0, 0.25) 0px 54px 55px;
color: rgb(204, 204, 204);
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;
line-height: 1.5;
max-width: 600px;
padding: 3.2px 5.12px 3.2px;
position: absolute;
top: 0;
}
x-pw-action-point {
position: absolute;
width: 20px;
height: 20px;
background: red;
border-radius: 10px;
pointer-events: none;
margin: -10px 0 0 -10px;
z-index: 2;
}
*[hidden] {
display: none !important;
}
`;
this._glassPaneShadow.appendChild(styleElement); this._glassPaneShadow.appendChild(styleElement);
} }
@ -180,13 +145,26 @@ export class Highlight {
let tooltipElement; let tooltipElement;
if (options.tooltipText) { if (options.tooltipText) {
tooltipElement = this._injectedScript.document.createElement('x-pw-tooltip'); 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.top = '0';
tooltipElement.style.left = '0'; tooltipElement.style.left = '0';
tooltipElement.style.display = 'flex'; tooltipElement.style.display = 'flex';
options.decorateTooltip?.(tooltipElement); 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 }); this._highlightEntries.push({ targetElement: elements[i], tooltipElement, highlightElement, tooltipText: options.tooltipText });
} }
@ -260,13 +238,10 @@ export class Highlight {
} }
private _createHighlightElement(): HTMLElement { private _createHighlightElement(): HTMLElement {
const highlightElement = this._injectedScript.document.createElement('x-pw-highlight'); return this._injectedScript.document.createElement('x-pw-highlight');
highlightElement.style.position = 'absolute'; }
highlightElement.style.top = '0';
highlightElement.style.left = '0'; appendChild(element: HTMLElement) {
highlightElement.style.width = '0'; this._glassPaneShadow.appendChild(element);
highlightElement.style.height = '0';
highlightElement.style.boxSizing = 'border-box';
return highlightElement;
} }
} }

View file

@ -36,8 +36,7 @@ interface RecorderDelegate {
interface RecorderTool { interface RecorderTool {
cursor(): string; cursor(): string;
enable?(): void; cleanup?(): void;
disable?(): void;
onClick?(event: MouseEvent): void; onClick?(event: MouseEvent): void;
onDragStart?(event: DragEvent): void; onDragStart?(event: DragEvent): void;
onInput?(event: Event): void; onInput?(event: Event): void;
@ -70,7 +69,7 @@ class InspectTool implements RecorderTool {
return 'pointer'; return 'pointer';
} }
disable() { cleanup() {
this._hoveredModel = null; this._hoveredModel = null;
this._hoveredElement = null; this._hoveredElement = null;
} }
@ -151,7 +150,7 @@ class RecordActionTool implements RecorderTool {
return 'pointer'; return 'pointer';
} }
disable() { cleanup() {
this._hoveredModel = null; this._hoveredModel = null;
this._hoveredElement = null; this._hoveredElement = null;
this._activeModel = null; this._activeModel = null;
@ -447,25 +446,25 @@ class TextAssertionTool implements RecorderTool {
private _selectionText: { selectedText: string, fullText: string } | null = null; private _selectionText: { selectedText: string, fullText: string } | null = null;
private _inputHighlight: HighlightModel | null = null; private _inputHighlight: HighlightModel | null = null;
private _acceptButton: HTMLElement; private _acceptButton: HTMLElement;
private _cancelButton: HTMLElement;
constructor(private _recorder: Recorder) { constructor(private _recorder: Recorder) {
this._acceptButton = this._recorder.document.createElement('button'); this._acceptButton = this._recorder.document.createElement('x-pw-tool-item');
this._acceptButton.textContent = 'Accept'; this._acceptButton.classList.add('accept');
this._acceptButton.style.cursor = 'pointer'; this._acceptButton.appendChild(this._recorder.document.createElement('x-div'));
this._acceptButton.style.pointerEvents = 'auto';
this._acceptButton.addEventListener('click', () => this._commitAction()); this._acceptButton.addEventListener('click', () => this._commitAction());
this._cancelButton = this._recorder.document.createElement('x-pw-tool-item');
this._cancelButton.classList.add('cancel');
this._cancelButton.appendChild(this._recorder.document.createElement('x-div'));
this._cancelButton.addEventListener('click', () => this._cancelAction());
} }
cursor() { cursor() {
return 'text'; return 'text';
} }
enable() { cleanup() {
this._recorder.injectedScript.document.designMode = 'on';
}
disable() {
this._recorder.injectedScript.document.designMode = 'off';
this._hoverHighlight = null; this._hoverHighlight = null;
this._selectionHighlight = null; this._selectionHighlight = null;
this._selectionText = null; this._selectionText = null;
@ -473,11 +472,6 @@ class TextAssertionTool implements RecorderTool {
} }
onClick(event: MouseEvent) { onClick(event: MouseEvent) {
// Hack: work around highlight's glass pane having a closed shadow root.
const box = this._acceptButton.getBoundingClientRect();
if (box.left <= event.clientX && event.clientX <= box.right && box.top <= event.clientY && event.clientY <= box.bottom)
return;
consumeEvent(event); consumeEvent(event);
const selection = this._recorder.document.getSelection(); const selection = this._recorder.document.getSelection();
if (event.detail === 1 && selection && !selection.toString() && !this._inputHighlight) { if (event.detail === 1 && selection && !selection.toString() && !this._inputHighlight) {
@ -612,6 +606,10 @@ class TextAssertionTool implements RecorderTool {
} }
} }
private _cancelAction() {
this._resetSelectionAndHighlight();
}
private _resetSelectionAndHighlight() { private _resetSelectionAndHighlight() {
this._selectionHighlight = null; this._selectionHighlight = null;
this._selectionText = null; this._selectionText = null;
@ -643,7 +641,12 @@ class TextAssertionTool implements RecorderTool {
} }
private _showHighlight(userGesture: boolean) { private _showHighlight(userGesture: boolean) {
const options: HighlightOptions = { color: '#6fdcbd38', tooltipText: this._generateActionPreview(), decorateTooltip: tooltip => tooltip.appendChild(this._acceptButton) }; const options: HighlightOptions = {
color: '#6fdcbd38',
tooltipText: this._generateActionPreview(),
toolbar: [this._acceptButton, this._cancelButton],
interactive: true,
};
if (this._inputHighlight) { if (this._inputHighlight) {
this._recorder.updateHighlight(this._inputHighlight, userGesture, options); this._recorder.updateHighlight(this._inputHighlight, userGesture, options);
} else { } else {
@ -665,119 +668,21 @@ class Overlay {
constructor(private _recorder: Recorder) { constructor(private _recorder: Recorder) {
const document = this._recorder.injectedScript.document; const document = this._recorder.injectedScript.document;
this._overlayElement = document.createElement('x-pw-overlay'); this._overlayElement = document.createElement('x-pw-overlay');
this._overlayElement.style.top = '0';
this._overlayElement.style.position = 'absolute';
const shadow = this._overlayElement.attachShadow({ mode: 'closed' });
const styleElement = document.createElement('style');
styleElement.textContent = `
:host {
position: fixed;
max-width: min-content;
z-index: 2147483647;
background: transparent;
}
x-pw-tools-list {
box-shadow: rgba(0, 0, 0, 0.1) 0px 5px 5px;
backdrop-filter: blur(5px);
background-color: hsla(0 0% 100% / .9);
font-family: 'Dank Mono', 'Operator Mono', Inconsolata, 'Fira Mono', 'SF Mono', Monaco, 'Droid Sans Mono', 'Source Code Pro', monospace;
display: flex;
border-radius: 3px;
}
x-pw-separator {
height: 1px;
margin: 6px 9px;
background: rgb(148 148 148 / 90%);
}
x-pw-tool-item {
cursor: pointer;
height: 28px;
width: 28px;
margin: 2px 4px;
border-radius: 3px;
}
x-pw-tool-item:not(.disabled):hover {
background-color: hsl(0, 0%, 86%);
}
x-pw-tool-item > div {
width: 100%;
height: 100%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: 20px;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 16px;
background-color: #3a3a3a;
}
x-pw-tool-item.disabled > div {
background-color: rgba(97, 97, 97, 0.5);
cursor: default;
}
x-pw-tool-item.active > div {
background-color: #006ab1;
}
x-pw-tool-item.record.active > div {
background-color: #a1260d;
}
x-pw-tool-gripper {
height: 28px;
width: 24px;
margin: 2px 0;
cursor: grab;
}
x-pw-tool-gripper:active {
cursor: grabbing;
}
x-pw-tool-gripper > div {
width: 100%;
height: 100%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: 20px;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 16px;
-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='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>");
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-item.record > 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 > 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 > 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>");
}
`;
shadow.appendChild(styleElement);
const toolsListElement = document.createElement('x-pw-tools-list'); const toolsListElement = document.createElement('x-pw-tools-list');
shadow.appendChild(toolsListElement); this._overlayElement.appendChild(toolsListElement);
const dragHandle = document.createElement('x-pw-tool-gripper'); const dragHandle = document.createElement('x-pw-tool-gripper');
dragHandle.addEventListener('mousedown', event => { dragHandle.addEventListener('mousedown', event => {
this._dragState = { offsetX: this._offsetX, dragStart: { x: event.clientX, y: 0 } }; this._dragState = { offsetX: this._offsetX, dragStart: { x: event.clientX, y: 0 } };
}); });
dragHandle.appendChild(document.createElement('div')); dragHandle.appendChild(document.createElement('x-div'));
toolsListElement.appendChild(dragHandle); toolsListElement.appendChild(dragHandle);
this._recordToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item'); this._recordToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
this._recordToggle.title = 'Record'; this._recordToggle.title = 'Record';
this._recordToggle.classList.add('record'); this._recordToggle.classList.add('record');
this._recordToggle.appendChild(this._recorder.injectedScript.document.createElement('div')); this._recordToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
this._recordToggle.addEventListener('click', () => { this._recordToggle.addEventListener('click', () => {
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'none'); this._recorder.delegate.setMode?.(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'none');
}); });
@ -786,7 +691,7 @@ class Overlay {
this._pickLocatorToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item'); this._pickLocatorToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
this._pickLocatorToggle.title = 'Pick locator'; this._pickLocatorToggle.title = 'Pick locator';
this._pickLocatorToggle.classList.add('pick-locator'); this._pickLocatorToggle.classList.add('pick-locator');
this._pickLocatorToggle.appendChild(this._recorder.injectedScript.document.createElement('div')); this._pickLocatorToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
this._pickLocatorToggle.addEventListener('click', () => { this._pickLocatorToggle.addEventListener('click', () => {
const newMode: Record<Mode, Mode> = { const newMode: Record<Mode, Mode> = {
'inspecting': 'none', 'inspecting': 'none',
@ -802,7 +707,7 @@ class Overlay {
this._assertToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item'); this._assertToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
this._assertToggle.title = 'Assert text and values'; this._assertToggle.title = 'Assert text and values';
this._assertToggle.classList.add('assert'); this._assertToggle.classList.add('assert');
this._assertToggle.appendChild(this._recorder.injectedScript.document.createElement('div')); this._assertToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
this._assertToggle.addEventListener('click', () => { this._assertToggle.addEventListener('click', () => {
if (!this._assertToggle.classList.contains('disabled')) if (!this._assertToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText'); this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText');
@ -813,7 +718,7 @@ class Overlay {
} }
install() { install() {
this._recorder.injectedScript.document.documentElement.appendChild(this._overlayElement); this._recorder.highlight.appendChild(this._overlayElement);
this._measure = this._overlayElement.getBoundingClientRect(); this._measure = this._overlayElement.getBoundingClientRect();
} }
@ -877,7 +782,7 @@ export class Recorder {
private _currentTool: RecorderTool; private _currentTool: RecorderTool;
private _tools: Record<Mode, RecorderTool>; private _tools: Record<Mode, RecorderTool>;
private _actionSelectorModel: HighlightModel | null = null; private _actionSelectorModel: HighlightModel | null = null;
private _highlight: Highlight; readonly highlight: Highlight;
private _overlay: Overlay | undefined; private _overlay: Overlay | undefined;
private _styleElement: HTMLStyleElement; private _styleElement: HTMLStyleElement;
state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', overlay: { offsetX: 0 } }; state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', overlay: { offsetX: 0 } };
@ -887,7 +792,7 @@ export class Recorder {
constructor(injectedScript: InjectedScript) { constructor(injectedScript: InjectedScript) {
this.document = injectedScript.document; this.document = injectedScript.document;
this.injectedScript = injectedScript; this.injectedScript = injectedScript;
this._highlight = new Highlight(injectedScript); this.highlight = new Highlight(injectedScript);
this._tools = { this._tools = {
'none': new NoneTool(), 'none': new NoneTool(),
'inspecting': new InspectTool(this), 'inspecting': new InspectTool(this),
@ -913,7 +818,7 @@ export class Recorder {
installListeners() { installListeners() {
// Ensure we are attached to the current document, and we are on top (last element); // Ensure we are attached to the current document, and we are on top (last element);
if (this._highlight.isInstalled()) if (this.highlight.isInstalled())
return; return;
removeEventListeners(this._listeners); removeEventListeners(this._listeners);
this._listeners = [ this._listeners = [
@ -932,7 +837,7 @@ export class Recorder {
addEventListener(this.document, 'focus', event => this._onFocus(event), true), addEventListener(this.document, 'focus', event => this._onFocus(event), true),
addEventListener(this.document, 'scroll', event => this._onScroll(event), true), addEventListener(this.document, 'scroll', event => this._onScroll(event), true),
]; ];
this._highlight.install(); this.highlight.install();
this._overlay?.install(); this._overlay?.install();
this.injectedScript.document.head.appendChild(this._styleElement); this.injectedScript.document.head.appendChild(this._styleElement);
} }
@ -941,10 +846,9 @@ export class Recorder {
const newTool = this._tools[this.state.mode]; const newTool = this._tools[this.state.mode];
if (newTool === this._currentTool) if (newTool === this._currentTool)
return; return;
this._currentTool.disable?.(); this._currentTool.cleanup?.();
this.clearHighlight(); this.clearHighlight();
this._currentTool = newTool; this._currentTool = newTool;
this._currentTool.enable?.();
this.injectedScript.document.body?.setAttribute('data-pw-cursor', newTool.cursor()); this.injectedScript.document.body?.setAttribute('data-pw-cursor', newTool.cursor());
} }
@ -957,13 +861,13 @@ export class Recorder {
// All good. // All good.
} else { } else {
if (state.actionPoint) if (state.actionPoint)
this._highlight.showActionPoint(state.actionPoint.x, state.actionPoint.y); this.highlight.showActionPoint(state.actionPoint.x, state.actionPoint.y);
else else
this._highlight.hideActionPoint(); this.highlight.hideActionPoint();
} }
this.state = state; this.state = state;
this._highlight.setLanguage(state.language); this.highlight.setLanguage(state.language);
this._switchCurrentTool(); this._switchCurrentTool();
this._overlay?.setUIState(state); this._overlay?.setUIState(state);
@ -977,7 +881,7 @@ export class Recorder {
} }
clearHighlight() { clearHighlight() {
this._currentTool.disable?.(); this._currentTool.cleanup?.();
this.updateHighlight(null, false); this.updateHighlight(null, false);
} }
@ -1062,7 +966,7 @@ export class Recorder {
private _onScroll(event: Event) { private _onScroll(event: Event) {
if (!event.isTrusted) if (!event.isTrusted)
return; return;
this._highlight.hideActionPoint(); this.highlight.hideActionPoint();
this._currentTool.onScroll?.(event); this._currentTool.onScroll?.(event);
} }
@ -1091,13 +995,14 @@ export class Recorder {
updateHighlight(model: HighlightModel | null, userGesture: boolean, options: HighlightOptions = {}) { updateHighlight(model: HighlightModel | null, userGesture: boolean, options: HighlightOptions = {}) {
if (options.tooltipText === undefined && model?.selector) if (options.tooltipText === undefined && model?.selector)
options.tooltipText = asLocator(this.state.language, model.selector); options.tooltipText = asLocator(this.state.language, model.selector);
this._highlight.updateHighlight(model?.elements || [], options); this.highlight.updateHighlight(model?.elements || [], options);
if (userGesture) if (userGesture)
this.delegate.highlightUpdated?.(); this.delegate.highlightUpdated?.();
} }
private _ignoreOverlayEvent(event: Event) { private _ignoreOverlayEvent(event: Event) {
return this._overlay?.contains(event.composedPath()[0] as Element); const target = event.composedPath()[0] as Element;
return target.nodeName.toLowerCase() === 'x-pw-glass';
} }
deepEventTarget(event: Event): HTMLElement { deepEventTarget(event: Event): HTMLElement {

View file

@ -176,9 +176,9 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
this._recorder.setMode('recording'); this._recorder.setMode('recording');
} }
} }
await this._page.mainFrame().evaluateExpression(((selector: string) => { await this._page.mainFrame().evaluateExpression(((data: { selector: string, userGesture?: boolean }) => {
window.playwrightSetSelector(selector); window.playwrightSetSelector(data.selector, data.userGesture);
}).toString(), { isFunction: true }, selector).catch(() => {}); }).toString(), { isFunction: true }, { selector, userGesture }).catch(() => {});
} }
async updateCallLogs(callLogs: CallLog[]): Promise<void> { async updateCallLogs(callLogs: CallLog[]): Promise<void> {

View file

@ -66,8 +66,10 @@ export const Recorder: React.FC<RecorderProps> = ({
}; };
const [locator, setLocator] = React.useState(''); const [locator, setLocator] = React.useState('');
window.playwrightSetSelector = (selector: string) => { window.playwrightSetSelector = (selector: string, focus?: boolean) => {
const language = source.language; const language = source.language;
if (focus)
setSelectedTab('locator');
setLocator(asLocator(language, selector)); setLocator(asLocator(language, selector));
}; };
@ -126,7 +128,6 @@ export const Recorder: React.FC<RecorderProps> = ({
'assertingText': 'recording-inspecting', 'assertingText': 'recording-inspecting',
}[mode]; }[mode];
window.dispatch({ event: 'setMode', params: { mode: newMode } }).catch(() => { }); window.dispatch({ event: 'setMode', params: { mode: newMode } }).catch(() => { });
setSelectedTab('locator');
}}>Pick locator</ToolbarButton> }}>Pick locator</ToolbarButton>
<ToolbarButton icon='check-all' title='Assert text and values' toggled={mode === 'assertingText'} disabled={mode === 'none' || mode === 'inspecting'} onClick={() => { <ToolbarButton icon='check-all' title='Assert text and values' toggled={mode === 'assertingText'} disabled={mode === 'none' || mode === 'inspecting'} onClick={() => {
window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingText' ? 'recording' : 'assertingText' } }); window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingText' ? 'recording' : 'assertingText' } });
@ -155,7 +156,7 @@ export const Recorder: React.FC<RecorderProps> = ({
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton> <ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
</Toolbar> </Toolbar>
<SplitView sidebarSize={200} sidebarHidden={mode === 'recording'}> <SplitView sidebarSize={200}>
<CodeMirrorWrapper text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine} readOnly={true} lineNumbers={true}/> <CodeMirrorWrapper text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine} readOnly={true} lineNumbers={true}/>
<TabbedPane <TabbedPane
rightToolbar={selectedTab === 'locator' ? [<ToolbarButton icon='files' title='Copy' onClick={() => copy(locator)} />] : []} rightToolbar={selectedTab === 'locator' ? [<ToolbarButton icon='files' title='Copy' onClick={() => copy(locator)} />] : []}
@ -163,7 +164,7 @@ export const Recorder: React.FC<RecorderProps> = ({
{ {
id: 'locator', id: 'locator',
title: 'Locator', title: 'Locator',
render: () => <CodeMirrorWrapper text={locator} language={source.language} readOnly={false} focusOnChange={true} onChange={onEditorChange} /> render: () => <CodeMirrorWrapper text={locator} language={source.language} readOnly={false} focusOnChange={true} onChange={onEditorChange} wrapLines={true}/>
}, },
{ {
id: 'log', id: 'log',

View file

@ -16,6 +16,10 @@
@import '../third_party/vscode/colors.css'; @import '../third_party/vscode/colors.css';
.cm-wrapper {
line-height: 18px;
}
.cm-wrapper, .cm-wrapper > div { .cm-wrapper, .cm-wrapper > div {
width: 100%; width: 100%;
height: 100%; height: 100%;

View file

@ -93,7 +93,6 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
// Either configuration is different or we don't have a codemirror yet. // Either configuration is different or we don't have a codemirror yet.
codemirrorRef.current?.cm?.getWrapperElement().remove(); codemirrorRef.current?.cm?.getWrapperElement().remove();
const cm = CodeMirror(element, { const cm = CodeMirror(element, {
value: '', value: '',
mode, mode,

View file

@ -43,10 +43,6 @@
color: var(--vscode-notificationLink-foreground); color: var(--vscode-notificationLink-foreground);
} }
.toolbar-button.toggled .codicon {
font-weight: bold;
}
.toolbar-separator { .toolbar-separator {
flex: none; flex: none;
background-color: var(--vscode-menu-separatorBackground); background-color: var(--vscode-menu-separatorBackground);