mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-12 00:52:05 +03:00
feat(recorder): assert value as a separate tool (#28145)
This commit is contained in:
parent
f76c261b16
commit
557f3afd74
@ -55,6 +55,13 @@ x-pw-dialog-body {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
x-pw-dialog-body label {
|
||||
margin: 5px 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
x-pw-highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -129,6 +136,14 @@ x-pw-tool-item:not(.disabled):hover {
|
||||
background-color: hsl(0, 0%, 86%);
|
||||
}
|
||||
|
||||
x-pw-tool-item.active {
|
||||
background-color: rgba(138, 202, 228, 0.5);
|
||||
}
|
||||
|
||||
x-pw-tool-item.active:not(.disabled):hover {
|
||||
background-color: #8acae4c4;
|
||||
}
|
||||
|
||||
x-pw-tool-item > x-div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -146,8 +161,12 @@ x-pw-tool-item.disabled > x-div {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
x-pw-tool-item.active > x-div {
|
||||
background-color: #006ab1;
|
||||
x-pw-tool-item.record.active {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
x-pw-tool-item.record.active:hover {
|
||||
background-color: hsl(0, 0%, 86%);
|
||||
}
|
||||
|
||||
x-pw-tool-item.record.active > x-div {
|
||||
@ -182,6 +201,12 @@ x-pw-tool-item.visibility > x-div {
|
||||
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='M7.99993 6.00316C9.47266 6.00316 10.6666 7.19708 10.6666 8.66981C10.6666 10.1426 9.47266 11.3365 7.99993 11.3365C6.52715 11.3365 5.33324 10.1426 5.33324 8.66981C5.33324 7.19708 6.52715 6.00316 7.99993 6.00316ZM7.99993 7.00315C7.07946 7.00315 6.33324 7.74935 6.33324 8.66981C6.33324 9.59028 7.07946 10.3365 7.99993 10.3365C8.9204 10.3365 9.6666 9.59028 9.6666 8.66981C9.6666 7.74935 8.9204 7.00315 7.99993 7.00315ZM7.99993 3.66675C11.0756 3.66675 13.7307 5.76675 14.4673 8.70968C14.5344 8.97755 14.3716 9.24908 14.1037 9.31615C13.8358 9.38315 13.5643 9.22041 13.4973 8.95248C12.8713 6.45205 10.6141 4.66675 7.99993 4.66675C5.38454 4.66675 3.12664 6.45359 2.50182 8.95555C2.43491 9.22341 2.16348 9.38635 1.89557 9.31948C1.62766 9.25255 1.46471 8.98115 1.53162 8.71321C2.26701 5.76856 4.9229 3.66675 7.99993 3.66675Z'/></svg>");
|
||||
}
|
||||
|
||||
x-pw-tool-item.value > x-div {
|
||||
/* codicon: symbol-constant */
|
||||
-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='M4 6h8v1H4V6zm8 3H4v1h8V9z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z'/></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='M4 6h8v1H4V6zm8 3H4v1h8V9z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z'/></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>");
|
||||
|
@ -80,10 +80,14 @@ class NoneTool implements RecorderTool {
|
||||
}
|
||||
|
||||
class InspectTool implements RecorderTool {
|
||||
private _recorder: Recorder;
|
||||
private _hoveredModel: HighlightModel | null = null;
|
||||
private _hoveredElement: HTMLElement | null = null;
|
||||
private _assertVisibility: boolean;
|
||||
|
||||
constructor(private _recorder: Recorder, private _assertVisibility: boolean) {
|
||||
constructor(recorder: Recorder, assertVisibility: boolean) {
|
||||
this._recorder = recorder;
|
||||
this._assertVisibility = assertVisibility;
|
||||
}
|
||||
|
||||
cursor() {
|
||||
@ -104,6 +108,7 @@ class InspectTool implements RecorderTool {
|
||||
selector: this._hoveredModel.selector,
|
||||
signals: [],
|
||||
});
|
||||
this._recorder.delegate.setMode?.('recording');
|
||||
}
|
||||
} else {
|
||||
this._recorder.delegate.setSelector?.(this._hoveredModel ? this._hoveredModel.selector : '');
|
||||
@ -138,7 +143,7 @@ class InspectTool implements RecorderTool {
|
||||
if (this._hoveredModel?.selector === model?.selector)
|
||||
return;
|
||||
this._hoveredModel = model;
|
||||
this._recorder.updateHighlight(model, true);
|
||||
this._recorder.updateHighlight(model, true, { color: this._assertVisibility ? '#8acae480' : undefined });
|
||||
}
|
||||
|
||||
onMouseLeave(event: MouseEvent) {
|
||||
@ -170,13 +175,15 @@ class InspectTool implements RecorderTool {
|
||||
}
|
||||
|
||||
class RecordActionTool implements RecorderTool {
|
||||
private _recorder: Recorder;
|
||||
private _performingAction = false;
|
||||
private _hoveredModel: HighlightModel | null = null;
|
||||
private _hoveredElement: HTMLElement | null = null;
|
||||
private _activeModel: HighlightModel | null = null;
|
||||
private _expectProgrammaticKeyUp = false;
|
||||
|
||||
constructor(private _recorder: Recorder) {
|
||||
constructor(recorder: Recorder) {
|
||||
this._recorder = recorder;
|
||||
}
|
||||
|
||||
cursor() {
|
||||
@ -474,6 +481,7 @@ class RecordActionTool implements RecorderTool {
|
||||
}
|
||||
|
||||
class TextAssertionTool implements RecorderTool {
|
||||
private _recorder: Recorder;
|
||||
private _hoverHighlight: HighlightModel | null = null;
|
||||
private _action: actions.AssertAction | null = null;
|
||||
private _dialogElement: HTMLElement | null = null;
|
||||
@ -481,8 +489,12 @@ class TextAssertionTool implements RecorderTool {
|
||||
private _cancelButton: HTMLElement;
|
||||
private _keyboardListener: ((event: KeyboardEvent) => void) | undefined;
|
||||
private _textCache = new Map<Element | ShadowRoot, ElementText>();
|
||||
private _kind: 'text' | 'value';
|
||||
|
||||
constructor(recorder: Recorder, kind: 'text' | 'value') {
|
||||
this._recorder = recorder;
|
||||
this._kind = kind;
|
||||
|
||||
constructor(private _recorder: Recorder) {
|
||||
this._acceptButton = this._recorder.document.createElement('x-pw-tool-item');
|
||||
this._acceptButton.title = 'Accept';
|
||||
this._acceptButton.classList.add('accept');
|
||||
@ -523,7 +535,10 @@ class TextAssertionTool implements RecorderTool {
|
||||
const target = this._recorder.deepEventTarget(event);
|
||||
if (this._hoverHighlight?.elements[0] === target)
|
||||
return;
|
||||
this._hoverHighlight = target.nodeName === 'INPUT' || target.nodeName === 'TEXTAREA' || elementText(new Map(), target).full ? { elements: [target], selector: '' } : null;
|
||||
if (this._kind === 'text')
|
||||
this._hoverHighlight = elementText(this._textCache, target).full ? { elements: [target], selector: '' } : null;
|
||||
else
|
||||
this._hoverHighlight = this._elementHasValue(target) ? generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null;
|
||||
this._recorder.updateHighlight(this._hoverHighlight, true, { color: '#8acae480' });
|
||||
}
|
||||
|
||||
@ -533,12 +548,22 @@ class TextAssertionTool implements RecorderTool {
|
||||
consumeEvent(event);
|
||||
}
|
||||
|
||||
onScroll(event: Event) {
|
||||
this._recorder.updateHighlight(this._hoverHighlight, false, { color: '#8acae480' });
|
||||
}
|
||||
|
||||
private _elementHasValue(element: Element) {
|
||||
return element.nodeName === 'TEXTAREA' || element.nodeName === 'SELECT' || (element.nodeName === 'INPUT' && !['button', 'image', 'reset', 'submit'].includes((element as HTMLInputElement).type));
|
||||
}
|
||||
|
||||
private _generateAction(): actions.AssertAction | null {
|
||||
this._textCache.clear();
|
||||
const target = this._hoverHighlight?.elements[0];
|
||||
if (!target)
|
||||
return null;
|
||||
if (target.nodeName === 'INPUT' || target.nodeName === 'TEXTAREA' || target.nodeName === 'SELECT') {
|
||||
if (this._kind === 'value') {
|
||||
if (!this._elementHasValue(target))
|
||||
return null;
|
||||
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 {
|
||||
@ -553,7 +578,7 @@ class TextAssertionTool implements RecorderTool {
|
||||
name: 'assertValue',
|
||||
selector,
|
||||
signals: [],
|
||||
value: (target as (HTMLInputElement | HTMLSelectElement)).value,
|
||||
value: (target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).value,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@ -637,7 +662,11 @@ class TextAssertionTool implements RecorderTool {
|
||||
cm.on('change', () => {
|
||||
if (this._action) {
|
||||
const selector = locatorOrSelectorAsSelector(this._recorder.state.language, cm.getValue(), this._recorder.state.testIdAttributeName);
|
||||
const elements = this._recorder.injectedScript.querySelectorAll(parseSelector(selector), this._recorder.document);
|
||||
let elements: Element[] = [];
|
||||
try {
|
||||
elements = this._recorder.injectedScript.querySelectorAll(parseSelector(selector), this._recorder.document);
|
||||
} catch {
|
||||
}
|
||||
cmElement.classList.toggle('does-not-match', !elements.length);
|
||||
this._hoverHighlight = elements.length ? {
|
||||
selector,
|
||||
@ -735,16 +764,19 @@ class TextAssertionTool implements RecorderTool {
|
||||
}
|
||||
|
||||
class Overlay {
|
||||
private _recorder: Recorder;
|
||||
private _overlayElement: HTMLElement;
|
||||
private _recordToggle: HTMLElement;
|
||||
private _pickLocatorToggle: HTMLElement;
|
||||
private _assertVisibilityToggle: HTMLElement;
|
||||
private _assertTextToggle: HTMLElement;
|
||||
private _assertValuesToggle: HTMLElement;
|
||||
private _offsetX = 0;
|
||||
private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined;
|
||||
private _measure: { width: number, height: number } = { width: 0, height: 0 };
|
||||
|
||||
constructor(private _recorder: Recorder) {
|
||||
constructor(recorder: Recorder) {
|
||||
this._recorder = recorder;
|
||||
const document = this._recorder.injectedScript.document;
|
||||
this._overlayElement = document.createElement('x-pw-overlay');
|
||||
|
||||
@ -780,6 +812,7 @@ class Overlay {
|
||||
'recording-inspecting': 'recording',
|
||||
'assertingText': 'recording-inspecting',
|
||||
'assertingVisibility': 'recording-inspecting',
|
||||
'assertingValue': 'recording-inspecting',
|
||||
};
|
||||
this._recorder.delegate.setMode?.(newMode[this._recorder.state.mode]);
|
||||
});
|
||||
@ -805,6 +838,16 @@ class Overlay {
|
||||
});
|
||||
toolsListElement.appendChild(this._assertTextToggle);
|
||||
|
||||
this._assertValuesToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
|
||||
this._assertValuesToggle.title = 'Assert value';
|
||||
this._assertValuesToggle.classList.add('value');
|
||||
this._assertValuesToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
|
||||
this._assertValuesToggle.addEventListener('click', () => {
|
||||
if (!this._assertValuesToggle.classList.contains('disabled'))
|
||||
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue');
|
||||
});
|
||||
toolsListElement.appendChild(this._assertValuesToggle);
|
||||
|
||||
this._updateVisualPosition();
|
||||
}
|
||||
|
||||
@ -818,12 +861,14 @@ class Overlay {
|
||||
}
|
||||
|
||||
setUIState(state: UIState) {
|
||||
this._recordToggle.classList.toggle('active', state.mode === 'recording' || state.mode === 'assertingText' || state.mode === 'assertingVisibility' || state.mode === 'recording-inspecting');
|
||||
this._recordToggle.classList.toggle('active', state.mode === 'recording' || state.mode === 'assertingText' || state.mode === 'assertingVisibility' || state.mode === 'assertingValue' || state.mode === 'recording-inspecting');
|
||||
this._pickLocatorToggle.classList.toggle('active', state.mode === 'inspecting' || state.mode === 'recording-inspecting');
|
||||
this._assertVisibilityToggle.classList.toggle('active', state.mode === 'assertingVisibility');
|
||||
this._assertVisibilityToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
|
||||
this._assertTextToggle.classList.toggle('active', state.mode === 'assertingText');
|
||||
this._assertTextToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
|
||||
this._assertValuesToggle.classList.toggle('active', state.mode === 'assertingValue');
|
||||
this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
|
||||
if (this._offsetX !== state.overlay.offsetX) {
|
||||
this._offsetX = state.overlay.offsetX;
|
||||
this._updateVisualPosition();
|
||||
@ -896,8 +941,9 @@ export class Recorder {
|
||||
'inspecting': new InspectTool(this, false),
|
||||
'recording': new RecordActionTool(this),
|
||||
'recording-inspecting': new InspectTool(this, false),
|
||||
'assertingText': new TextAssertionTool(this),
|
||||
'assertingText': new TextAssertionTool(this, 'text'),
|
||||
'assertingVisibility': new InspectTool(this, true),
|
||||
'assertingValue': new TextAssertionTool(this, 'value'),
|
||||
};
|
||||
this._currentTool = this._tools.none;
|
||||
if (injectedScript.window.top === injectedScript.window) {
|
||||
|
@ -246,8 +246,8 @@ export class Recorder implements InstrumentationListener {
|
||||
this._highlightedSelector = '';
|
||||
this._mode = mode;
|
||||
this._recorderApp?.setMode(this._mode);
|
||||
this._contextRecorder.setEnabled(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility');
|
||||
this._debugger.setMuted(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility');
|
||||
this._contextRecorder.setEnabled(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue');
|
||||
this._debugger.setMuted(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue');
|
||||
if (this._mode !== 'none' && this._mode !== 'standby' && this._context.pages().length === 1)
|
||||
this._context.pages()[0].bringToFront().catch(() => {});
|
||||
this._refreshOverlay();
|
||||
@ -281,7 +281,7 @@ export class Recorder implements InstrumentationListener {
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._omitCallTracking || this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility')
|
||||
if (this._omitCallTracking || this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue')
|
||||
return;
|
||||
this._currentCallsMetadata.set(metadata, sdkObject);
|
||||
this._updateUserSources();
|
||||
@ -295,7 +295,7 @@ export class Recorder implements InstrumentationListener {
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._omitCallTracking || this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility')
|
||||
if (this._omitCallTracking || this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue')
|
||||
return;
|
||||
if (!metadata.error)
|
||||
this._currentCallsMetadata.delete(metadata);
|
||||
@ -345,7 +345,7 @@ export class Recorder implements InstrumentationListener {
|
||||
}
|
||||
|
||||
updateCallLog(metadatas: CallMetadata[]) {
|
||||
if (this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility')
|
||||
if (this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue')
|
||||
return;
|
||||
const logs: CallLog[] = [];
|
||||
for (const metadata of metadatas) {
|
||||
|
@ -128,15 +128,19 @@ export const Recorder: React.FC<RecorderProps> = ({
|
||||
'recording-inspecting': 'recording',
|
||||
'assertingText': 'recording-inspecting',
|
||||
'assertingVisibility': 'recording-inspecting',
|
||||
'assertingValue': 'recording-inspecting',
|
||||
}[mode];
|
||||
window.dispatch({ event: 'setMode', params: { mode: newMode } }).catch(() => { });
|
||||
}}></ToolbarButton>
|
||||
<ToolbarButton icon='eye' title='Assert visibility' toggled={mode === 'assertingVisibility'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
|
||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility' } });
|
||||
}}></ToolbarButton>
|
||||
<ToolbarButton icon='whole-word' title='Assert text and values' toggled={mode === 'assertingText'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
|
||||
<ToolbarButton icon='whole-word' title='Assert text' toggled={mode === 'assertingText'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
|
||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingText' ? 'recording' : 'assertingText' } });
|
||||
}}></ToolbarButton>
|
||||
<ToolbarButton icon='symbol-constant' title='Assert value' toggled={mode === 'assertingValue'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
|
||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingValue' ? 'recording' : 'assertingValue' } });
|
||||
}}></ToolbarButton>
|
||||
<ToolbarSeparator />
|
||||
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
|
||||
copy(source.text);
|
||||
|
@ -18,7 +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' | 'assertingText' | 'recording-inspecting' | 'standby' | 'assertingVisibility';
|
||||
export type Mode = 'inspecting' | 'recording' | 'none' | 'assertingText' | 'recording-inspecting' | 'standby' | 'assertingVisibility' | 'assertingValue';
|
||||
|
||||
export type EventData = {
|
||||
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'selectorUpdated' | 'fileChanged';
|
||||
|
Loading…
Reference in New Issue
Block a user