feat(recorder): in-page overlay (#27904)
This commit is contained in:
parent
d983941447
commit
3dedbced13
|
|
@ -122,7 +122,7 @@ export class DebugController extends SdkObject {
|
|||
// Toggle the mode.
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
recorder.hideHighlightedSelector();
|
||||
if (params.mode === 'recording')
|
||||
if (params.mode !== 'inspecting')
|
||||
recorder.setOutput(this._codegenId, params.file);
|
||||
recorder.setMode(params.mode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class Highlight {
|
|||
this._glassPaneElement.style.right = '0';
|
||||
this._glassPaneElement.style.bottom = '0';
|
||||
this._glassPaneElement.style.left = '0';
|
||||
this._glassPaneElement.style.zIndex = '2147483647';
|
||||
this._glassPaneElement.style.zIndex = '2147483646';
|
||||
this._glassPaneElement.style.pointerEvents = 'none';
|
||||
this._glassPaneElement.style.display = 'flex';
|
||||
this._glassPaneElement.style.backgroundColor = 'transparent';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import type * as actions from '../recorder/recorderActions';
|
|||
import type { InjectedScript } from '../injected/injectedScript';
|
||||
import { generateSelector } from '../injected/selectorGenerator';
|
||||
import type { Point } from '../../common/types';
|
||||
import type { UIState, Mode, RecordingTool } from '@recorder/recorderTypes';
|
||||
import type { Mode, UIState } from '@recorder/recorderTypes';
|
||||
import { Highlight } from '../injected/highlight';
|
||||
import { enclosingElement, isInsideScope, parentElementOrShadowHost } from './domUtils';
|
||||
import { elementText } from './selectorUtils';
|
||||
|
|
@ -28,10 +28,12 @@ interface RecorderDelegate {
|
|||
performAction?(action: actions.Action): Promise<void>;
|
||||
recordAction?(action: actions.Action): Promise<void>;
|
||||
setSelector?(selector: string): Promise<void>;
|
||||
setMode?(mode: Mode): Promise<void>;
|
||||
highlightUpdated?(): void;
|
||||
}
|
||||
|
||||
interface RecorderTool {
|
||||
cursor(): string;
|
||||
disable?(): void;
|
||||
onClick?(event: MouseEvent): void;
|
||||
onInput?(event: Event): void;
|
||||
|
|
@ -46,6 +48,9 @@ interface RecorderTool {
|
|||
}
|
||||
|
||||
class NoneTool implements RecorderTool {
|
||||
cursor() {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
class InspectTool implements RecorderTool {
|
||||
|
|
@ -55,6 +60,10 @@ class InspectTool implements RecorderTool {
|
|||
constructor(private _recorder: Recorder) {
|
||||
}
|
||||
|
||||
cursor() {
|
||||
return 'pointer';
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._hoveredModel = null;
|
||||
this._hoveredElement = null;
|
||||
|
|
@ -75,13 +84,13 @@ class InspectTool implements RecorderTool {
|
|||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
consumeEvent(event);
|
||||
let target: HTMLElement | null = deepEventTarget(event);
|
||||
let target: HTMLElement | null = this._recorder.deepEventTarget(event);
|
||||
if (!target.isConnected)
|
||||
target = null;
|
||||
if (this._hoveredElement === target)
|
||||
return;
|
||||
this._hoveredElement = target;
|
||||
const model = this._hoveredElement ? generateSelector(this._recorder.injectedScript, this._hoveredElement, { testIdAttributeName: this._recorder.testIdAttributeName }) : null;
|
||||
const model = this._hoveredElement ? generateSelector(this._recorder.injectedScript, this._hoveredElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null;
|
||||
if (this._hoveredModel?.selector === model?.selector)
|
||||
return;
|
||||
this._hoveredModel = model;
|
||||
|
|
@ -92,7 +101,7 @@ class InspectTool implements RecorderTool {
|
|||
consumeEvent(event);
|
||||
const window = this._recorder.injectedScript.window;
|
||||
// Leaving iframe.
|
||||
if (window.top !== window && deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
|
||||
if (window.top !== window && this._recorder.deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
|
||||
this._hoveredElement = null;
|
||||
this._hoveredModel = null;
|
||||
this._recorder.updateHighlight(null, true);
|
||||
|
|
@ -124,6 +133,10 @@ class RecordActionTool implements RecorderTool {
|
|||
constructor(private _recorder: Recorder) {
|
||||
}
|
||||
|
||||
cursor() {
|
||||
return 'pointer';
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._hoveredModel = null;
|
||||
this._hoveredElement = null;
|
||||
|
|
@ -139,7 +152,7 @@ class RecordActionTool implements RecorderTool {
|
|||
if (this._consumedDueToNoModel(event, this._hoveredModel))
|
||||
return;
|
||||
|
||||
const checkbox = asCheckbox(deepEventTarget(event));
|
||||
const checkbox = asCheckbox(this._recorder.deepEventTarget(event));
|
||||
if (checkbox) {
|
||||
// Interestingly, inputElement.checked is reversed inside this event handler.
|
||||
this._performAction({
|
||||
|
|
@ -177,7 +190,7 @@ class RecordActionTool implements RecorderTool {
|
|||
}
|
||||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
const target = deepEventTarget(event);
|
||||
const target = this._recorder.deepEventTarget(event);
|
||||
if (this._hoveredElement === target)
|
||||
return;
|
||||
this._hoveredElement = target;
|
||||
|
|
@ -187,7 +200,7 @@ class RecordActionTool implements RecorderTool {
|
|||
onMouseLeave(event: MouseEvent) {
|
||||
const window = this._recorder.injectedScript.window;
|
||||
// Leaving iframe.
|
||||
if (window.top !== window && deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
|
||||
if (window.top !== window && this._recorder.deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
|
||||
this._hoveredElement = null;
|
||||
this._updateModelForHoveredElement();
|
||||
}
|
||||
|
|
@ -198,7 +211,7 @@ class RecordActionTool implements RecorderTool {
|
|||
}
|
||||
|
||||
onInput(event: Event) {
|
||||
const target = deepEventTarget(event);
|
||||
const target = this._recorder.deepEventTarget(event);
|
||||
|
||||
if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') {
|
||||
this._recorder.delegate.recordAction?.({
|
||||
|
|
@ -251,7 +264,7 @@ class RecordActionTool implements RecorderTool {
|
|||
return;
|
||||
// Similarly to click, trigger checkbox on key event, not input.
|
||||
if (event.key === ' ') {
|
||||
const checkbox = asCheckbox(deepEventTarget(event));
|
||||
const checkbox = asCheckbox(this._recorder.deepEventTarget(event));
|
||||
if (checkbox) {
|
||||
this._performAction({
|
||||
name: checkbox.checked ? 'uncheck' : 'check',
|
||||
|
|
@ -295,7 +308,7 @@ class RecordActionTool implements RecorderTool {
|
|||
// We'd like to ignore this stray event.
|
||||
if (userGesture && activeElement === this._recorder.document.body)
|
||||
return;
|
||||
const result = activeElement ? generateSelector(this._recorder.injectedScript, activeElement, { testIdAttributeName: this._recorder.testIdAttributeName }) : null;
|
||||
const result = activeElement ? generateSelector(this._recorder.injectedScript, activeElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null;
|
||||
this._activeModel = result && result.selector ? result : null;
|
||||
if (userGesture)
|
||||
this._hoveredElement = activeElement as HTMLElement | null;
|
||||
|
|
@ -303,7 +316,7 @@ class RecordActionTool implements RecorderTool {
|
|||
}
|
||||
|
||||
private _shouldIgnoreMouseEvent(event: MouseEvent): boolean {
|
||||
const target = deepEventTarget(event);
|
||||
const target = this._recorder.deepEventTarget(event);
|
||||
const nodeName = target.nodeName;
|
||||
if (nodeName === 'SELECT' || nodeName === 'OPTION')
|
||||
return true;
|
||||
|
|
@ -329,7 +342,7 @@ class RecordActionTool implements RecorderTool {
|
|||
}
|
||||
|
||||
private _consumedDueWrongTarget(event: Event): boolean {
|
||||
if (this._activeModel && this._activeModel.elements[0] === deepEventTarget(event))
|
||||
if (this._activeModel && this._activeModel.elements[0] === this._recorder.deepEventTarget(event))
|
||||
return false;
|
||||
consumeEvent(event);
|
||||
return true;
|
||||
|
|
@ -359,7 +372,7 @@ class RecordActionTool implements RecorderTool {
|
|||
|
||||
private _shouldGenerateKeyPressFor(event: KeyboardEvent): boolean {
|
||||
// Enter aka. new line is handled in input event.
|
||||
if (event.key === 'Enter' && (deepEventTarget(event).nodeName === 'TEXTAREA' || deepEventTarget(event).isContentEditable))
|
||||
if (event.key === 'Enter' && (this._recorder.deepEventTarget(event).nodeName === 'TEXTAREA' || this._recorder.deepEventTarget(event).isContentEditable))
|
||||
return false;
|
||||
// Backspace, Delete, AltGraph are changing input, will handle it there.
|
||||
if (['Backspace', 'Delete', 'AltGraph'].includes(event.key))
|
||||
|
|
@ -381,7 +394,7 @@ class RecordActionTool implements RecorderTool {
|
|||
return false;
|
||||
const hasModifier = event.ctrlKey || event.altKey || event.metaKey;
|
||||
if (event.key.length === 1 && !hasModifier)
|
||||
return !!asCheckbox(deepEventTarget(event));
|
||||
return !!asCheckbox(this._recorder.deepEventTarget(event));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -392,7 +405,7 @@ class RecordActionTool implements RecorderTool {
|
|||
this._recorder.updateHighlight(null, true);
|
||||
return;
|
||||
}
|
||||
const { selector, elements } = generateSelector(this._recorder.injectedScript, this._hoveredElement, { testIdAttributeName: this._recorder.testIdAttributeName });
|
||||
const { selector, elements } = generateSelector(this._recorder.injectedScript, this._hoveredElement, { testIdAttributeName: this._recorder.state.testIdAttributeName });
|
||||
if (this._hoveredModel && this._hoveredModel.selector === selector)
|
||||
return;
|
||||
this._hoveredModel = selector ? { selector, elements } : null;
|
||||
|
|
@ -406,6 +419,10 @@ class TextAssertionTool implements RecorderTool {
|
|||
constructor(private _recorder: Recorder) {
|
||||
}
|
||||
|
||||
cursor() {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._selectionModel = null;
|
||||
this._syncDocumentSelection();
|
||||
|
|
@ -415,7 +432,7 @@ class TextAssertionTool implements RecorderTool {
|
|||
consumeEvent(event);
|
||||
if (event.detail !== 1 || this._getSelectionText())
|
||||
return;
|
||||
const target = deepEventTarget(event);
|
||||
const target = this._recorder.deepEventTarget(event);
|
||||
const text = target ? elementText(new Map(), target).full : '';
|
||||
if (text) {
|
||||
this._selectionModel = { anchor: { node: target, offset: 0 }, focus: { node: target, offset: target.childNodes.length }, highlight: null };
|
||||
|
|
@ -517,7 +534,7 @@ class TextAssertionTool implements RecorderTool {
|
|||
let lcaElement = focusElement ? enclosingElement(this._selectionModel.anchor.node) : undefined;
|
||||
while (lcaElement && !isInsideScope(lcaElement, focusElement))
|
||||
lcaElement = parentElementOrShadowHost(lcaElement);
|
||||
const highlight = lcaElement ? generateSelector(this._recorder.injectedScript, lcaElement, { testIdAttributeName: this._recorder.testIdAttributeName, forTextExpect: true }) : null;
|
||||
const highlight = lcaElement ? generateSelector(this._recorder.injectedScript, lcaElement, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }) : null;
|
||||
if (highlight?.selector === this._selectionModel.highlight?.selector)
|
||||
return;
|
||||
this._selectionModel.highlight = highlight;
|
||||
|
|
@ -525,21 +542,190 @@ class TextAssertionTool implements RecorderTool {
|
|||
}
|
||||
}
|
||||
|
||||
class Overlay {
|
||||
private _overlayElement: HTMLElement;
|
||||
private _tools: Record<Mode, HTMLElement>;
|
||||
private _position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
private _dragState: { position: { x: number, y: number }, dragStart: { x: number, y: number } } | undefined;
|
||||
private _measure: { width: number, height: number } = { width: 0, height: 0 };
|
||||
|
||||
constructor(private _recorder: Recorder) {
|
||||
const document = this._recorder.injectedScript.document;
|
||||
this._overlayElement = document.createElement('x-pw-overlay');
|
||||
|
||||
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;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
x-pw-tools-list {
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 0.25em 0.5em;
|
||||
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;
|
||||
flex-direction: column;
|
||||
margin: 1em;
|
||||
padding: 0px;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
x-pw-tool-item {
|
||||
cursor: pointer;
|
||||
height: 2.25em;
|
||||
width: 2.25em;
|
||||
margin: 0.05em 0.25em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
}
|
||||
x-pw-tool-item:first-child {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
x-pw-tool-item:last-child {
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
x-pw-tool-item:hover {
|
||||
background-color: hsl(0, 0%, 95%);
|
||||
}
|
||||
x-pw-tool-item.active {
|
||||
background-color: hsl(0, 0%, 100%);
|
||||
}
|
||||
x-pw-tool-item > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: 20px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 20px;
|
||||
}
|
||||
x-pw-tool-item.active > div {
|
||||
background-color: #ff4ca5;
|
||||
}
|
||||
x-pw-tool-item.none > div {
|
||||
/* codicon: close */
|
||||
-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='M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z'/></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='M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z'/></svg>");
|
||||
}
|
||||
x-pw-tool-item.inspecting > div {
|
||||
/* codicon: target */
|
||||
-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 9C8.55228 9 9 8.55228 9 8C9 7.44772 8.55228 7 8 7C7.44772 7 7 7.44772 7 8C7 8.55228 7.44772 9 8 9Z'/><path d='M12 8C12 10.2091 10.2091 12 8 12C5.79086 12 4 10.2091 4 8C4 5.79086 5.79086 4 8 4C10.2091 4 12 5.79086 12 8ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z'/><path d='M15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8ZM8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z'/></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 9C8.55228 9 9 8.55228 9 8C9 7.44772 8.55228 7 8 7C7.44772 7 7 7.44772 7 8C7 8.55228 7.44772 9 8 9Z'/><path d='M12 8C12 10.2091 10.2091 12 8 12C5.79086 12 4 10.2091 4 8C4 5.79086 5.79086 4 8 4C10.2091 4 12 5.79086 12 8ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z'/><path d='M15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8ZM8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z'/></svg>");
|
||||
}
|
||||
x-pw-tool-item.recording > div {
|
||||
/* codicon: record */
|
||||
-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 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z'/></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 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z'/></svg>");
|
||||
}
|
||||
x-pw-tool-item.assertingText > div {
|
||||
/* codicon: text-size */
|
||||
-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='M3.36 7L1 13h1.34l.51-1.47h2.26L5.64 13H7L4.65 7H3.36zm-.15 3.53l.78-2.14.78 2.14H3.21zM11.82 4h-1.6L7 13h1.56l.75-2.29h3.36l.77 2.29H15l-3.18-9zM9.67 9.5l1.18-3.59c.059-.185.1-.376.12-.57.027.192.064.382.11.57l1.25 3.59H9.67z'/></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='M3.36 7L1 13h1.34l.51-1.47h2.26L5.64 13H7L4.65 7H3.36zm-.15 3.53l.78-2.14.78 2.14H3.21zM11.82 4h-1.6L7 13h1.56l.75-2.29h3.36l.77 2.29H15l-3.18-9zM9.67 9.5l1.18-3.59c.059-.185.1-.376.12-.57.027.192.064.382.11.57l1.25 3.59H9.67z'/></svg>");
|
||||
}
|
||||
`;
|
||||
shadow.appendChild(styleElement);
|
||||
|
||||
const toolsListElement = document.createElement('x-pw-tools-list');
|
||||
shadow.appendChild(toolsListElement);
|
||||
|
||||
this._tools = {
|
||||
none: this._createToolElement(toolsListElement, 'none', 'Disable'),
|
||||
inspecting: this._createToolElement(toolsListElement, 'inspecting', 'Pick locator'),
|
||||
recording: this._createToolElement(toolsListElement, 'recording', 'Record actions'),
|
||||
assertingText: this._createToolElement(toolsListElement, 'assertingText', 'Assert text'),
|
||||
};
|
||||
|
||||
this._overlayElement.addEventListener('mousedown', event => {
|
||||
this._dragState = { position: this._position, dragStart: { x: event.clientX, y: event.clientY } };
|
||||
});
|
||||
|
||||
if (this._recorder.injectedScript.isUnderTest) {
|
||||
// Most of our tests put elements at the top left, so get out of the way.
|
||||
this._position = { x: 350, y: 350 };
|
||||
}
|
||||
this._updateVisualPosition();
|
||||
}
|
||||
|
||||
private _createToolElement(parent: Element, mode: Mode, title: string) {
|
||||
const element = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
|
||||
element.title = title;
|
||||
element.classList.add(mode);
|
||||
element.appendChild(this._recorder.injectedScript.document.createElement('div'));
|
||||
element.addEventListener('click', () => this._recorder.delegate.setMode?.(mode));
|
||||
parent.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
install() {
|
||||
this._recorder.injectedScript.document.documentElement.appendChild(this._overlayElement);
|
||||
this._measure = this._overlayElement.getBoundingClientRect();
|
||||
}
|
||||
|
||||
contains(element: Element) {
|
||||
return isInsideScope(this._overlayElement, element);
|
||||
}
|
||||
|
||||
setUIState(state: UIState) {
|
||||
for (const [mode, tool] of Object.entries(this._tools))
|
||||
tool.classList.toggle('active', state.mode === mode);
|
||||
}
|
||||
|
||||
private _updateVisualPosition() {
|
||||
this._overlayElement.style.left = this._position.x + 'px';
|
||||
this._overlayElement.style.top = this._position.y + 'px';
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
if (!event.buttons) {
|
||||
this._dragState = undefined;
|
||||
return false;
|
||||
}
|
||||
if (this._dragState) {
|
||||
this._position = {
|
||||
x: this._dragState.position.x + event.clientX - this._dragState.dragStart.x,
|
||||
y: this._dragState.position.y + event.clientY - this._dragState.dragStart.y,
|
||||
};
|
||||
this._position.x = Math.max(0, Math.min(this._recorder.injectedScript.window.innerWidth - this._measure.width, this._position.x));
|
||||
this._position.y = Math.max(0, Math.min(this._recorder.injectedScript.window.innerHeight - this._measure.height, this._position.y));
|
||||
this._updateVisualPosition();
|
||||
consumeEvent(event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onMouseUp(event: MouseEvent) {
|
||||
if (this._dragState) {
|
||||
this._dragState = undefined;
|
||||
consumeEvent(event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class Recorder {
|
||||
readonly injectedScript: InjectedScript;
|
||||
private _listeners: (() => void)[] = [];
|
||||
private _mode: Mode = 'none';
|
||||
private _tool: RecordingTool = 'action';
|
||||
private _currentTool: RecorderTool;
|
||||
private _noneTool: NoneTool;
|
||||
private _inspectTool: InspectTool;
|
||||
private _recordActionTool: RecordActionTool;
|
||||
private _textAssertionTool: TextAssertionTool;
|
||||
private _actionPoint: Point | undefined;
|
||||
private _tools: Record<Mode, RecorderTool>;
|
||||
private _actionSelectorModel: HighlightModel | null = null;
|
||||
private _highlightModel: HighlightModel | null = null;
|
||||
private _highlight: Highlight;
|
||||
testIdAttributeName: string = 'data-testid';
|
||||
private _overlay: Overlay | undefined;
|
||||
private _styleElement: HTMLStyleElement;
|
||||
state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript' };
|
||||
readonly document: Document;
|
||||
delegate: RecorderDelegate = {};
|
||||
|
||||
|
|
@ -547,11 +733,23 @@ export class Recorder {
|
|||
this.document = injectedScript.document;
|
||||
this.injectedScript = injectedScript;
|
||||
this._highlight = new Highlight(injectedScript);
|
||||
this._noneTool = new NoneTool();
|
||||
this._inspectTool = new InspectTool(this);
|
||||
this._recordActionTool = new RecordActionTool(this);
|
||||
this._textAssertionTool = new TextAssertionTool(this);
|
||||
this._currentTool = this._noneTool;
|
||||
this._tools = {
|
||||
none: new NoneTool(),
|
||||
inspecting: new InspectTool(this),
|
||||
recording: new RecordActionTool(this),
|
||||
assertingText: new TextAssertionTool(this),
|
||||
};
|
||||
this._currentTool = this._tools.none;
|
||||
if (injectedScript.window.top === injectedScript.window) {
|
||||
this._overlay = new Overlay(this);
|
||||
this._overlay.setUIState(this.state);
|
||||
}
|
||||
this._styleElement = this.document.createElement('style');
|
||||
this._styleElement.textContent = `
|
||||
body[data-pw-cursor=pointer] *, body[data-pw-cursor=pointer] *::after { cursor: pointer !important; }
|
||||
body[data-pw-cursor=text] *, body[data-pw-cursor=text] *::after { cursor: text !important; }
|
||||
`;
|
||||
this.installListeners();
|
||||
|
||||
if (injectedScript.isUnderTest)
|
||||
console.error('Recorder script ready for test'); // eslint-disable-line no-console
|
||||
|
|
@ -576,61 +774,45 @@ export class Recorder {
|
|||
addEventListener(this.document, 'scroll', event => this._onScroll(event), true),
|
||||
];
|
||||
this._highlight.install();
|
||||
}
|
||||
|
||||
uninstallListeners() {
|
||||
removeEventListeners(this._listeners);
|
||||
this._highlight.uninstall();
|
||||
this._overlay?.install();
|
||||
this.injectedScript.document.head.appendChild(this._styleElement);
|
||||
}
|
||||
|
||||
private _switchCurrentTool() {
|
||||
const newTool = this._tools[this.state.mode];
|
||||
if (newTool === this._currentTool)
|
||||
return;
|
||||
this._currentTool.disable?.();
|
||||
this.clearHighlight();
|
||||
if (this._mode === 'none')
|
||||
this._currentTool = this._noneTool;
|
||||
else if (this._mode === 'inspecting')
|
||||
this._currentTool = this._inspectTool;
|
||||
else if (this._tool === 'action')
|
||||
this._currentTool = this._recordActionTool;
|
||||
else
|
||||
this._currentTool = this._textAssertionTool;
|
||||
this._currentTool = newTool;
|
||||
this.injectedScript.document.body.setAttribute('data-pw-cursor', newTool.cursor());
|
||||
}
|
||||
|
||||
setUIState(state: UIState, delegate: RecorderDelegate) {
|
||||
this.delegate = delegate;
|
||||
|
||||
if (state.mode !== 'none' || state.actionSelector)
|
||||
this.installListeners();
|
||||
else
|
||||
this.uninstallListeners();
|
||||
|
||||
const { mode, tool, actionPoint, actionSelector, language, testIdAttributeName } = state;
|
||||
this.testIdAttributeName = testIdAttributeName;
|
||||
this._highlight.setLanguage(language);
|
||||
if (mode !== this._mode || this._tool !== tool) {
|
||||
this._mode = mode;
|
||||
this._tool = tool;
|
||||
this._switchCurrentTool();
|
||||
}
|
||||
|
||||
if (actionPoint && this._actionPoint && actionPoint.x === this._actionPoint.x && actionPoint.y === this._actionPoint.y) {
|
||||
if (state.actionPoint && this.state.actionPoint && state.actionPoint.x === this.state.actionPoint.x && state.actionPoint.y === this.state.actionPoint.y) {
|
||||
// All good.
|
||||
} else if (!actionPoint && !this._actionPoint) {
|
||||
} else if (!state.actionPoint && !this.state.actionPoint) {
|
||||
// All good.
|
||||
} else {
|
||||
if (actionPoint)
|
||||
this._highlight.showActionPoint(actionPoint.x, actionPoint.y);
|
||||
if (state.actionPoint)
|
||||
this._highlight.showActionPoint(state.actionPoint.x, state.actionPoint.y);
|
||||
else
|
||||
this._highlight.hideActionPoint();
|
||||
this._actionPoint = actionPoint;
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
this._highlight.setLanguage(state.language);
|
||||
this._switchCurrentTool();
|
||||
this._overlay?.setUIState(state);
|
||||
|
||||
// Race or scroll.
|
||||
if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length)
|
||||
this._actionSelectorModel = null;
|
||||
if (actionSelector !== this._actionSelectorModel?.selector)
|
||||
this._actionSelectorModel = actionSelector ? querySelector(this.injectedScript, actionSelector, this.document) : null;
|
||||
if (this._mode === 'none')
|
||||
if (state.actionSelector !== this._actionSelectorModel?.selector)
|
||||
this._actionSelectorModel = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null;
|
||||
if (this.state.mode === 'none')
|
||||
this.updateHighlight(this._actionSelectorModel, false);
|
||||
}
|
||||
|
||||
|
|
@ -642,36 +824,52 @@ export class Recorder {
|
|||
private _onClick(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._ignoreOverlayEvent(event))
|
||||
return;
|
||||
this._currentTool.onClick?.(event);
|
||||
}
|
||||
|
||||
private _onMouseDown(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._ignoreOverlayEvent(event))
|
||||
return;
|
||||
this._currentTool.onMouseDown?.(event);
|
||||
}
|
||||
|
||||
private _onMouseUp(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._overlay?.onMouseUp(event))
|
||||
return;
|
||||
if (this._ignoreOverlayEvent(event))
|
||||
return;
|
||||
this._currentTool.onMouseUp?.(event);
|
||||
}
|
||||
|
||||
private _onMouseMove(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._overlay?.onMouseMove(event))
|
||||
return;
|
||||
if (this._ignoreOverlayEvent(event))
|
||||
return;
|
||||
this._currentTool.onMouseMove?.(event);
|
||||
}
|
||||
|
||||
private _onMouseLeave(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._ignoreOverlayEvent(event))
|
||||
return;
|
||||
this._currentTool.onMouseLeave?.(event);
|
||||
}
|
||||
|
||||
private _onFocus(event: Event) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._ignoreOverlayEvent(event))
|
||||
return;
|
||||
this._currentTool.onFocus?.(event);
|
||||
}
|
||||
|
||||
|
|
@ -683,27 +881,44 @@ export class Recorder {
|
|||
}
|
||||
|
||||
private _onInput(event: Event) {
|
||||
if (this._ignoreOverlayEvent(event))
|
||||
return;
|
||||
this._currentTool.onInput?.(event);
|
||||
}
|
||||
|
||||
private _onKeyDown(event: KeyboardEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._ignoreOverlayEvent(event))
|
||||
return;
|
||||
this._currentTool.onKeyDown?.(event);
|
||||
}
|
||||
|
||||
private _onKeyUp(event: KeyboardEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._ignoreOverlayEvent(event))
|
||||
return;
|
||||
this._currentTool.onKeyUp?.(event);
|
||||
}
|
||||
|
||||
updateHighlight(model: HighlightModel | null, userGesture: boolean, color?: string) {
|
||||
this._highlightModel = model;
|
||||
this._highlight.updateHighlight(model?.elements || [], model?.selector || '', color);
|
||||
if (userGesture)
|
||||
this.delegate.highlightUpdated?.();
|
||||
}
|
||||
|
||||
private _ignoreOverlayEvent(event: Event) {
|
||||
return this._overlay?.contains(event.composedPath()[0] as Element);
|
||||
}
|
||||
|
||||
deepEventTarget(event: Event): HTMLElement {
|
||||
for (const element of event.composedPath()) {
|
||||
if (!this._overlay?.contains(element as Element))
|
||||
return element as HTMLElement;
|
||||
}
|
||||
return event.composedPath()[0] as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
function deepActiveElement(document: Document): Element | null {
|
||||
|
|
@ -713,10 +928,6 @@ function deepActiveElement(document: Document): Element | null {
|
|||
return activeElement;
|
||||
}
|
||||
|
||||
function deepEventTarget(event: Event): HTMLElement {
|
||||
return event.composedPath()[0] as HTMLElement;
|
||||
}
|
||||
|
||||
function modifiersForEvent(event: MouseEvent | KeyboardEvent): number {
|
||||
return (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0);
|
||||
}
|
||||
|
|
@ -798,6 +1009,7 @@ interface Embedder {
|
|||
__pw_recorderRecordAction(action: actions.Action): Promise<void>;
|
||||
__pw_recorderState(): Promise<UIState>;
|
||||
__pw_recorderSetSelector(selector: string): Promise<void>;
|
||||
__pw_recorderSetMode(mode: Mode): Promise<void>;
|
||||
__pw_refreshOverlay(): void;
|
||||
}
|
||||
|
||||
|
|
@ -853,6 +1065,10 @@ export class PollingRecorder implements RecorderDelegate {
|
|||
async setSelector(selector: string): Promise<void> {
|
||||
await this._embedder.__pw_recorderSetSelector(selector);
|
||||
}
|
||||
|
||||
async setMode(mode: Mode): Promise<void> {
|
||||
await this._embedder.__pw_recorderSetMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
export default PollingRecorder;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import type { IRecorderApp } from './recorder/recorderApp';
|
|||
import { RecorderApp } from './recorder/recorderApp';
|
||||
import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
|
||||
import type { Point } from '../common/types';
|
||||
import type { CallLog, CallLogStatus, EventData, Mode, RecordingTool, Source, UIState } from '@recorder/recorderTypes';
|
||||
import type { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from '@recorder/recorderTypes';
|
||||
import { createGuid, isUnderTest, monotonicTime } from '../utils';
|
||||
import { metadataToCallLog } from './recorder/recorderUtils';
|
||||
import { Debugger } from './debugger';
|
||||
|
|
@ -54,7 +54,6 @@ const recorderSymbol = Symbol('recorderSymbol');
|
|||
export class Recorder implements InstrumentationListener {
|
||||
private _context: BrowserContext;
|
||||
private _mode: Mode;
|
||||
private _tool: RecordingTool = 'action';
|
||||
private _highlightedSelector = '';
|
||||
private _recorderApp: IRecorderApp | null = null;
|
||||
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
||||
|
|
@ -118,10 +117,6 @@ export class Recorder implements InstrumentationListener {
|
|||
this.setMode(data.params.mode);
|
||||
return;
|
||||
}
|
||||
if (data.event === 'setRecordingTool') {
|
||||
this.setRecordingTool(data.params.tool);
|
||||
return;
|
||||
}
|
||||
if (data.event === 'selectorUpdated') {
|
||||
this.setHighlightedSelector(this._currentLanguage, data.params.selector);
|
||||
return;
|
||||
|
|
@ -181,7 +176,6 @@ export class Recorder implements InstrumentationListener {
|
|||
}
|
||||
const uiState: UIState = {
|
||||
mode: this._mode,
|
||||
tool: this._tool,
|
||||
actionPoint,
|
||||
actionSelector,
|
||||
language: this._currentLanguage,
|
||||
|
|
@ -202,6 +196,12 @@ export class Recorder implements InstrumentationListener {
|
|||
await this._recorderApp?.setSelector(fullSelector.join(' >> internal:control=enter-frame >> '), true);
|
||||
});
|
||||
|
||||
await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => {
|
||||
if (frame.parentFrame())
|
||||
return;
|
||||
this.setMode(mode);
|
||||
});
|
||||
|
||||
await this._context.exposeBinding('__pw_resume', false, () => {
|
||||
this._debugger.resume(false);
|
||||
});
|
||||
|
|
@ -233,21 +233,13 @@ export class Recorder implements InstrumentationListener {
|
|||
this._highlightedSelector = '';
|
||||
this._mode = mode;
|
||||
this._recorderApp?.setMode(this._mode);
|
||||
this._contextRecorder.setEnabled(this._mode === 'recording');
|
||||
this._debugger.setMuted(this._mode === 'recording');
|
||||
this._contextRecorder.setEnabled(this._mode === 'recording' || this._mode === 'assertingText');
|
||||
this._debugger.setMuted(this._mode === 'recording' || this._mode === 'assertingText');
|
||||
if (this._mode !== 'none' && this._context.pages().length === 1)
|
||||
this._context.pages()[0].bringToFront().catch(() => {});
|
||||
this._refreshOverlay();
|
||||
}
|
||||
|
||||
setRecordingTool(tool: RecordingTool) {
|
||||
if (this._tool === tool)
|
||||
return;
|
||||
this._tool = tool;
|
||||
this._recorderApp?.setRecordingTool(this._tool);
|
||||
this._refreshOverlay();
|
||||
}
|
||||
|
||||
resume() {
|
||||
this._debugger.resume(false);
|
||||
}
|
||||
|
|
@ -272,7 +264,7 @@ export class Recorder implements InstrumentationListener {
|
|||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._omitCallTracking || this._mode === 'recording')
|
||||
if (this._omitCallTracking || this._mode === 'recording' || this._mode === 'assertingText')
|
||||
return;
|
||||
this._currentCallsMetadata.set(metadata, sdkObject);
|
||||
this._updateUserSources();
|
||||
|
|
@ -286,7 +278,7 @@ export class Recorder implements InstrumentationListener {
|
|||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._omitCallTracking || this._mode === 'recording')
|
||||
if (this._omitCallTracking || this._mode === 'recording' || this._mode === 'assertingText')
|
||||
return;
|
||||
if (!metadata.error)
|
||||
this._currentCallsMetadata.delete(metadata);
|
||||
|
|
@ -336,7 +328,7 @@ export class Recorder implements InstrumentationListener {
|
|||
}
|
||||
|
||||
updateCallLog(metadatas: CallMetadata[]) {
|
||||
if (this._mode === 'recording')
|
||||
if (this._mode === 'recording' || this._mode === 'assertingText')
|
||||
return;
|
||||
const logs: CallLog[] = [];
|
||||
for (const metadata of metadatas) {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import type { Page } from '../page';
|
|||
import { ProgressController } from '../progress';
|
||||
import { EventEmitter } from 'events';
|
||||
import { serverSideCallMetadata } from '../instrumentation';
|
||||
import type { CallLog, EventData, Mode, RecordingTool, Source } from '@recorder/recorderTypes';
|
||||
import type { CallLog, EventData, Mode, Source } from '@recorder/recorderTypes';
|
||||
import { isUnderTest } from '../../utils';
|
||||
import { mime } from '../../utilsBundle';
|
||||
import { syncLocalStorageWithSettings } from '../launchApp';
|
||||
|
|
@ -45,7 +45,6 @@ export interface IRecorderApp extends EventEmitter {
|
|||
close(): Promise<void>;
|
||||
setPaused(paused: boolean): Promise<void>;
|
||||
setMode(mode: Mode): Promise<void>;
|
||||
setRecordingTool(tool: RecordingTool): Promise<void>;
|
||||
setFileIfNeeded(file: string): Promise<void>;
|
||||
setSelector(selector: string, focus?: boolean): Promise<void>;
|
||||
updateCallLogs(callLogs: CallLog[]): Promise<void>;
|
||||
|
|
@ -56,7 +55,6 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
|
|||
async close(): Promise<void> {}
|
||||
async setPaused(paused: boolean): Promise<void> {}
|
||||
async setMode(mode: Mode): Promise<void> {}
|
||||
async setRecordingTool(tool: RecordingTool): Promise<void> {}
|
||||
async setFileIfNeeded(file: string): Promise<void> {}
|
||||
async setSelector(selector: string, focus?: boolean): Promise<void> {}
|
||||
async updateCallLogs(callLogs: CallLog[]): Promise<void> {}
|
||||
|
|
@ -146,12 +144,6 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
|||
}).toString(), { isFunction: true }, mode).catch(() => {});
|
||||
}
|
||||
|
||||
async setRecordingTool(tool: RecordingTool): Promise<void> {
|
||||
await this._page.mainFrame().evaluateExpression(((tool: RecordingTool) => {
|
||||
window.playwrightSetRecordingTool(tool);
|
||||
}).toString(), { isFunction: true }, tool).catch(() => {});
|
||||
}
|
||||
|
||||
async setFileIfNeeded(file: string): Promise<void> {
|
||||
await this._page.mainFrame().evaluateExpression(((file: string) => {
|
||||
window.playwrightSetFileIfNeeded(file);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { CallLog, Mode, RecordingTool, Source } from './recorderTypes';
|
||||
import type { CallLog, Mode, Source } from './recorderTypes';
|
||||
import * as React from 'react';
|
||||
import { Recorder } from './recorder';
|
||||
import './recorder.css';
|
||||
|
|
@ -25,10 +25,8 @@ export const Main: React.FC = ({
|
|||
const [paused, setPaused] = React.useState(false);
|
||||
const [log, setLog] = React.useState(new Map<string, CallLog>());
|
||||
const [mode, setMode] = React.useState<Mode>('none');
|
||||
const [tool, setTool] = React.useState<RecordingTool>('action');
|
||||
|
||||
window.playwrightSetMode = setMode;
|
||||
window.playwrightSetRecordingTool = setTool;
|
||||
window.playwrightSetSources = setSources;
|
||||
window.playwrightSetPaused = setPaused;
|
||||
window.playwrightUpdateLogs = callLogs => {
|
||||
|
|
@ -41,5 +39,5 @@ export const Main: React.FC = ({
|
|||
};
|
||||
|
||||
window.playwrightSourcesEchoForTest = sources;
|
||||
return <Recorder sources={sources} paused={paused} log={log} mode={mode} tool={tool}/>;
|
||||
return <Recorder sources={sources} paused={paused} log={log} mode={mode}/>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,11 +28,13 @@
|
|||
min-width: 100px;
|
||||
}
|
||||
|
||||
.recorder .toolbar-button.toggled.record {
|
||||
.recorder .toolbar-button.toggled.record,
|
||||
.recorder .toolbar-button.toggled.text-size {
|
||||
color: #a1260d;
|
||||
}
|
||||
|
||||
body.dark-mode .recorder .toolbar-button.toggled.record {
|
||||
body.dark-mode .recorder .toolbar-button.toggled.record,
|
||||
body.dark-mode .recorder .toolbar-button.toggled.text-size {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { CallLog, Mode, RecordingTool, Source } from './recorderTypes';
|
||||
import type { CallLog, Mode, Source } from './recorderTypes';
|
||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||
import { SplitView } from '@web/components/splitView';
|
||||
import { TabbedPane } from '@web/components/tabbedPane';
|
||||
|
|
@ -40,7 +40,6 @@ export interface RecorderProps {
|
|||
paused: boolean,
|
||||
log: Map<string, CallLog>,
|
||||
mode: Mode,
|
||||
tool: RecordingTool,
|
||||
}
|
||||
|
||||
export const Recorder: React.FC<RecorderProps> = ({
|
||||
|
|
@ -48,7 +47,6 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
paused,
|
||||
log,
|
||||
mode,
|
||||
tool,
|
||||
}) => {
|
||||
const [fileId, setFileId] = React.useState<string | undefined>();
|
||||
const [selectedTab, setSelectedTab] = React.useState<string>('log');
|
||||
|
|
@ -115,11 +113,11 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
|
||||
return <div className='recorder'>
|
||||
<Toolbar>
|
||||
<ToolbarButton icon='record' title='Record' toggled={mode === 'recording'} onClick={() => {
|
||||
<ToolbarButton icon='record' title='Record actions' toggled={mode === 'recording'} onClick={() => {
|
||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'recording' ? 'none' : 'recording' } });
|
||||
}}>Record</ToolbarButton>
|
||||
<ToolbarButton icon='check-all' title={tool === 'action' ? 'Recording actions' : 'Recording assertions'} toggled={tool === 'assert'} disabled={mode !== 'recording'} onClick={() => {
|
||||
window.dispatch({ event: 'setRecordingTool', params: { tool: tool === 'assert' ? 'action' : 'assert' } });
|
||||
<ToolbarButton icon='text-size' title='Assert text' toggled={mode === 'assertingText'} onClick={() => {
|
||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingText' ? 'none' : 'assertingText' } });
|
||||
}}>Assert</ToolbarButton>
|
||||
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
|
||||
copy(source.text);
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ import type { Language } from '../../playwright-core/src/utils/isomorphic/locato
|
|||
|
||||
export type Point = { x: number, y: number };
|
||||
|
||||
export type Mode = 'inspecting' | 'recording' | 'none';
|
||||
|
||||
export type RecordingTool = 'action' | 'assert';
|
||||
export type Mode = 'inspecting' | 'recording' | 'none' | 'assertingText';
|
||||
|
||||
export type EventData = {
|
||||
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'setRecordingTool' | 'selectorUpdated' | 'fileChanged';
|
||||
|
|
@ -29,7 +27,6 @@ export type EventData = {
|
|||
|
||||
export type UIState = {
|
||||
mode: Mode;
|
||||
tool: RecordingTool;
|
||||
actionPoint?: Point;
|
||||
actionSelector?: string;
|
||||
language: 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
||||
|
|
@ -75,7 +72,6 @@ export type Source = {
|
|||
declare global {
|
||||
interface Window {
|
||||
playwrightSetMode: (mode: Mode) => void;
|
||||
playwrightSetRecordingTool: (tool: RecordingTool) => void;
|
||||
playwrightSetPaused: (paused: boolean) => void;
|
||||
playwrightSetSources: (sources: Source[]) => void;
|
||||
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
|||
pointElement.style.height = '20px';
|
||||
pointElement.style.borderRadius = '10px';
|
||||
pointElement.style.margin = '-10px 0 0 -10px';
|
||||
pointElement.style.zIndex = '2147483647';
|
||||
pointElement.style.zIndex = '2147483646';
|
||||
const box = target.getBoundingClientRect();
|
||||
pointElement.style.left = (box.left + box.width / 2) + 'px';
|
||||
pointElement.style.top = (box.top + box.height / 2) + 'px';
|
||||
|
|
|
|||
|
|
@ -239,7 +239,6 @@ export const InspectModeController: React.FunctionComponent<{
|
|||
const actionSelector = locatorOrSelectorAsSelector(sdkLanguage, highlightedLocator, testIdAttributeName);
|
||||
recorder.setUIState({
|
||||
mode: isInspecting ? 'inspecting' : 'none',
|
||||
tool: 'action',
|
||||
actionSelector: actionSelector.startsWith(frameSelector) ? actionSelector.substring(frameSelector.length).trim() : undefined,
|
||||
language: sdkLanguage,
|
||||
testIdAttributeName,
|
||||
|
|
|
|||
Loading…
Reference in a new issue