diff --git a/packages/playwright-core/src/server/injected/highlight.css.ts b/packages/playwright-core/src/server/injected/highlight.css
similarity index 89%
rename from packages/playwright-core/src/server/injected/highlight.css.ts
rename to packages/playwright-core/src/server/injected/highlight.css
index 18955755ba..0d0a74e7b3 100644
--- a/packages/playwright-core/src/server/injected/highlight.css.ts
+++ b/packages/playwright-core/src/server/injected/highlight.css
@@ -14,16 +14,18 @@
* limitations under the License.
*/
-export const highlightCSS = `
+:host {
+ font-size: 13px;
+ font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif;
+ color: #333;
+}
+
x-pw-tooltip {
backdrop-filter: blur(5px);
background-color: white;
- color: #222;
border-radius: 6px;
box-shadow: 0 0.5rem 1.2rem rgba(0,0,0,.3);
display: none;
- font-family: 'Dank Mono', 'Operator Mono', Inconsolata, 'Fira Mono',
- 'SF Mono', Monaco, 'Droid Sans Mono', 'Source Code Pro', monospace;
font-size: 12.8px;
font-weight: normal;
left: 0;
@@ -31,11 +33,27 @@ x-pw-tooltip {
max-width: 600px;
position: absolute;
top: 0;
+ padding: 4px;
}
-x-pw-tooltip-body {
- align-items: center;
- padding: 3.2px 5.12px 3.2px;
+
+x-pw-dialog {
+ background-color: white;
+ pointer-events: auto;
+ border-radius: 6px;
+ box-shadow: 0 0.5rem 1.2rem rgba(0,0,0,.3);
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ min-width: 500px;
+ min-height: 200px;
}
+
+x-pw-dialog-body {
+ display: flex;
+ flex-direction: column;
+ flex: auto;
+}
+
x-pw-highlight {
position: absolute;
top: 0;
@@ -43,6 +61,7 @@ x-pw-highlight {
width: 0;
height: 0;
}
+
x-pw-action-point {
position: absolute;
width: 20px;
@@ -52,20 +71,24 @@ x-pw-action-point {
margin: -10px 0 0 -10px;
z-index: 2;
}
+
x-pw-separator {
height: 1px;
margin: 6px 9px;
background: rgb(148 148 148 / 90%);
}
+
x-pw-tool-gripper {
height: 28px;
width: 24px;
margin: 2px 0;
cursor: grab;
}
+
x-pw-tool-gripper:active {
cursor: grabbing;
}
+
x-pw-tool-gripper > x-div {
width: 100%;
height: 100%;
@@ -79,11 +102,20 @@ x-pw-tool-gripper > x-div {
mask-image: url("data:image/svg+xml;utf8,");
background-color: #555555;
}
+
+x-pw-tool-label {
+ display: flex;
+ align-items: center;
+ margin-left: 10px;
+ user-select: none;
+}
+
x-pw-tools-list {
display: flex;
width: 100%;
border-bottom: 1px solid #dddddd;
}
+
x-pw-tool-item {
pointer-events: auto;
cursor: pointer;
@@ -91,9 +123,11 @@ x-pw-tool-item {
width: 28px;
border-radius: 3px;
}
+
x-pw-tool-item:not(.disabled):hover {
background-color: hsl(0, 0%, 86%);
}
+
x-pw-tool-item > x-div {
width: 100%;
height: 100%;
@@ -105,45 +139,52 @@ x-pw-tool-item > x-div {
mask-size: 16px;
background-color: #3a3a3a;
}
+
x-pw-tool-item.disabled > x-div {
background-color: rgba(97, 97, 97, 0.5);
cursor: default;
}
+
x-pw-tool-item.active > x-div {
background-color: #006ab1;
}
+
x-pw-tool-item.record.active > x-div {
background-color: #a1260d;
}
+
x-pw-tool-item.accept > x-div {
background-color: #388a34;
}
-x-pw-tool-item.cancel > x-div {
- background-color: #e51400;
-}
+
x-pw-tool-item.record > x-div {
/* codicon: circle-large-filled */
-webkit-mask-image: url("data:image/svg+xml;utf8,");
mask-image: url("data:image/svg+xml;utf8,");
}
+
x-pw-tool-item.pick-locator > x-div {
/* codicon: inspect */
-webkit-mask-image: url("data:image/svg+xml;utf8,");
mask-image: url("data:image/svg+xml;utf8,");
}
+
x-pw-tool-item.assert > x-div {
/* codicon: check-all */
-webkit-mask-image: url("data:image/svg+xml;utf8,");
mask-image: url("data:image/svg+xml;utf8,");
}
+
x-pw-tool-item.accept > x-div {
-webkit-mask-image: url("data:image/svg+xml;utf8,");
mask-image: url("data:image/svg+xml;utf8,");
}
+
x-pw-tool-item.cancel > x-div {
-webkit-mask-image: url("data:image/svg+xml;utf8,");
mask-image: url("data:image/svg+xml;utf8,");
}
+
x-pw-overlay {
position: absolute;
top: 0;
@@ -152,21 +193,52 @@ x-pw-overlay {
background: transparent;
pointer-events: auto;
}
+
x-pw-overlay x-pw-tools-list {
background-color: #ffffffdd;
box-shadow: rgba(0, 0, 0, 0.1) 0px 5px 5px;
border-radius: 3px;
border-bottom: none;
}
+
x-pw-overlay x-pw-tool-item {
margin: 2px;
}
+
+input.locator-editor {
+ display: flex;
+ padding: 10px;
+ flex: none;
+ border: none;
+ border-bottom: 1px solid #dddddd;
+}
+
+input.locator-editor:focus,
+textarea.text-editor:focus {
+ outline: none;
+}
+
+textarea.text-editor {
+ font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif;
+ flex: auto;
+ border: none;
+ padding: 10px;
+ color: #333;
+}
+
+
x-div {
display: block;
}
+
+x-spacer {
+ flex: auto;
+}
+
* {
box-sizing: border-box;
}
+
*[hidden] {
display: none !important;
-}`;
+}
diff --git a/packages/playwright-core/src/server/injected/highlight.ts b/packages/playwright-core/src/server/injected/highlight.ts
index d6253d3292..c629bf092b 100644
--- a/packages/playwright-core/src/server/injected/highlight.ts
+++ b/packages/playwright-core/src/server/injected/highlight.ts
@@ -19,7 +19,7 @@ import type { ParsedSelector } from '../../utils/isomorphic/selectorParser';
import type { InjectedScript } from './injectedScript';
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators';
-import { highlightCSS } from './highlight.css';
+import highlightCSS from './highlight.css?inline';
type HighlightEntry = {
targetElement: Element,
@@ -34,9 +34,6 @@ type HighlightEntry = {
export type HighlightOptions = {
tooltipText?: string;
color?: string;
- anchorGetter?: (element: Element) => DOMRect;
- toolbar?: Element[];
- interactive?: boolean;
};
export class Highlight {
@@ -63,7 +60,12 @@ export class Highlight {
this._glassPaneElement.style.pointerEvents = 'none';
this._glassPaneElement.style.display = 'flex';
this._glassPaneElement.style.backgroundColor = 'transparent';
-
+ for (const eventName of ['click', 'auxclick', 'dragstart', 'input', 'keydown', 'keyup', 'pointerdown', 'pointerup', 'mousedown', 'mouseup', 'mousemove', 'mouseleave', 'focus', 'scroll']) {
+ this._glassPaneElement.addEventListener(eventName, e => {
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+ });
+ }
this._actionPointElement = document.createElement('x-pw-action-point');
this._actionPointElement.setAttribute('hidden', 'true');
this._glassPaneShadow = this._glassPaneElement.attachShadow({ mode: this._isUnderTest ? 'open' : 'closed' });
@@ -145,26 +147,12 @@ export class Highlight {
let tooltipElement;
if (options.tooltipText) {
tooltipElement = this._injectedScript.document.createElement('x-pw-tooltip');
+ this._glassPaneShadow.appendChild(tooltipElement);
+ const suffix = elements.length > 1 ? ` [${i + 1} of ${elements.length}]` : '';
+ tooltipElement.textContent = options.tooltipText + suffix;
tooltipElement.style.top = '0';
tooltipElement.style.left = '0';
tooltipElement.style.display = 'flex';
- tooltipElement.style.flexDirection = 'column';
- tooltipElement.style.alignItems = 'start';
- if (options.interactive)
- tooltipElement.style.pointerEvents = 'auto';
-
- if (options.toolbar) {
- const toolbar = this._injectedScript.document.createElement('x-pw-tools-list');
- tooltipElement.appendChild(toolbar);
- for (const toolbarElement of options.toolbar)
- toolbar.appendChild(toolbarElement);
- }
- const bodyElement = this._injectedScript.document.createElement('x-pw-tooltip-body');
- tooltipElement.appendChild(bodyElement);
-
- this._glassPaneShadow.appendChild(tooltipElement);
- const suffix = elements.length > 1 ? ` [${i + 1} of ${elements.length}]` : '';
- bodyElement.textContent = options.tooltipText + suffix;
}
this._highlightEntries.push({ targetElement: elements[i], tooltipElement, highlightElement, tooltipText: options.tooltipText });
}
@@ -176,25 +164,7 @@ export class Highlight {
continue;
// Position tooltip, if any.
- const tooltipWidth = entry.tooltipElement.offsetWidth;
- const tooltipHeight = entry.tooltipElement.offsetHeight;
- const totalWidth = this._glassPaneElement.offsetWidth;
- const totalHeight = this._glassPaneElement.offsetHeight;
-
- const anchorBox = options.anchorGetter ? options.anchorGetter(entry.targetElement) : entry.box;
- let anchorLeft = anchorBox.left;
- if (anchorLeft + tooltipWidth > totalWidth - 5)
- anchorLeft = totalWidth - tooltipWidth - 5;
- let anchorTop = anchorBox.bottom + 5;
- if (anchorTop + tooltipHeight > totalHeight - 5) {
- // If can't fit below, either position above...
- if (anchorBox.top > tooltipHeight + 5) {
- anchorTop = anchorBox.top - tooltipHeight - 5;
- } else {
- // Or on top in case of large element
- anchorTop = totalHeight - 5 - tooltipHeight;
- }
- }
+ const { anchorLeft, anchorTop } = this.tooltipPosition(entry.box, entry.tooltipElement);
entry.tooltipTop = anchorTop;
entry.tooltipLeft = anchorLeft;
}
@@ -219,6 +189,33 @@ export class Highlight {
console.error('Highlight box for test: ' + JSON.stringify({ x: box.x, y: box.y, width: box.width, height: box.height })); // eslint-disable-line no-console
}
}
+
+ firstBox(): DOMRect | undefined {
+ return this._highlightEntries[0]?.box;
+ }
+
+ tooltipPosition(box: DOMRect, tooltipElement: HTMLElement) {
+ const tooltipWidth = tooltipElement.offsetWidth;
+ const tooltipHeight = tooltipElement.offsetHeight;
+ const totalWidth = this._glassPaneElement.offsetWidth;
+ const totalHeight = this._glassPaneElement.offsetHeight;
+
+ let anchorLeft = box.left;
+ if (anchorLeft + tooltipWidth > totalWidth - 5)
+ anchorLeft = totalWidth - tooltipWidth - 5;
+ let anchorTop = box.bottom + 5;
+ if (anchorTop + tooltipHeight > totalHeight - 5) {
+ // If can't fit below, either position above...
+ if (box.top > tooltipHeight + 5) {
+ anchorTop = box.top - tooltipHeight - 5;
+ } else {
+ // Or on top in case of large element
+ anchorTop = totalHeight - 5 - tooltipHeight;
+ }
+ }
+ return { anchorLeft, anchorTop };
+ }
+
private _highlightIsUpToDate(elements: Element[], tooltipText: string | undefined): boolean {
if (elements.length !== this._highlightEntries.length)
return false;
diff --git a/packages/playwright-core/src/server/injected/recorder.ts b/packages/playwright-core/src/server/injected/recorder.ts
index 19545c15f5..3ab0845ea6 100644
--- a/packages/playwright-core/src/server/injected/recorder.ts
+++ b/packages/playwright-core/src/server/injected/recorder.ts
@@ -20,10 +20,12 @@ import { generateSelector } from '../injected/selectorGenerator';
import type { Point } from '../../common/types';
import type { Mode, OverlayState, UIState } from '@recorder/recorderTypes';
import { Highlight, type HighlightOptions } from '../injected/highlight';
-import { enclosingElement, isInsideScope, parentElementOrShadowHost } from './domUtils';
+import { isInsideScope } from './domUtils';
import { elementText } from './selectorUtils';
-import { escapeWithQuotes, normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
+import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
+import { parseSelector } from '@isomorphic/selectorParser';
+import { normalizeWhiteSpace } from '@isomorphic/stringUtils';
interface RecorderDelegate {
performAction?(action: actions.Action): Promise;
@@ -442,217 +444,187 @@ class RecordActionTool implements RecorderTool {
class TextAssertionTool implements RecorderTool {
private _hoverHighlight: HighlightModel | null = null;
- private _selectionHighlight: HighlightModel | null = null;
- private _selectionText: { selectedText: string, fullText: string } | null = null;
- private _inputHighlight: HighlightModel | null = null;
+ private _action: actions.AssertAction | null = null;
+ private _dialogElement: HTMLElement | null = null;
private _acceptButton: HTMLElement;
private _cancelButton: HTMLElement;
+ private _keyboardListener: ((event: KeyboardEvent) => void) | undefined;
constructor(private _recorder: Recorder) {
this._acceptButton = this._recorder.document.createElement('x-pw-tool-item');
+ this._acceptButton.title = 'Accept';
this._acceptButton.classList.add('accept');
this._acceptButton.appendChild(this._recorder.document.createElement('x-div'));
- this._acceptButton.addEventListener('click', () => this._commitAction());
+ this._acceptButton.addEventListener('click', () => this._commit());
this._cancelButton = this._recorder.document.createElement('x-pw-tool-item');
+ this._cancelButton.title = 'Close';
this._cancelButton.classList.add('cancel');
this._cancelButton.appendChild(this._recorder.document.createElement('x-div'));
- this._cancelButton.addEventListener('click', () => this._cancelAction());
+ this._cancelButton.addEventListener('click', () => this._closeDialog());
}
cursor() {
- return 'text';
+ return 'pointer';
}
cleanup() {
+ this._closeDialog();
this._hoverHighlight = null;
- this._selectionHighlight = null;
- this._selectionText = null;
- this._inputHighlight = null;
}
onClick(event: MouseEvent) {
+ if (!this._dialogElement)
+ this._showDialog();
consumeEvent(event);
- const selection = this._recorder.document.getSelection();
- if (event.detail === 1 && selection && !selection.toString() && !this._inputHighlight) {
- const target = this._recorder.deepEventTarget(event);
- selection.selectAllChildren(target);
- this._updateSelectionHighlight();
- }
- }
-
- onMouseDown(event: MouseEvent) {
- const target = this._recorder.deepEventTarget(event);
- if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) {
- this._recorder.injectedScript.window.getSelection()?.empty();
- this._inputHighlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
- this._showHighlight(true);
- consumeEvent(event);
- return;
- }
-
- this._inputHighlight = null;
- this._hoverHighlight = null;
- this._updateSelectionHighlight();
- }
-
- onMouseUp(event: MouseEvent) {
- this._updateSelectionHighlight();
}
onMouseMove(event: MouseEvent) {
- const selection = this._recorder.document.getSelection();
- if (selection && selection.toString()) {
- this._updateSelectionHighlight();
- return;
- }
- if (this._inputHighlight || event.buttons)
+ if (this._dialogElement)
return;
const target = this._recorder.deepEventTarget(event);
if (this._hoverHighlight?.elements[0] === target)
return;
- this._hoverHighlight = elementText(new Map(), target).full ? { elements: [target], selector: '' } : null;
+ this._hoverHighlight = target.nodeName === 'INPUT' || target.nodeName === 'TEXTAREA' || elementText(new Map(), target).full ? { elements: [target], selector: '' } : null;
this._recorder.updateHighlight(this._hoverHighlight, true, { color: '#8acae480' });
}
- onDragStart(event: DragEvent) {
- consumeEvent(event);
- }
-
onKeyDown(event: KeyboardEvent) {
- if (event.key === 'Escape') {
- const selection = this._recorder.document.getSelection();
- if (selection && selection.toString())
- this._resetSelectionAndHighlight();
- else
- this._recorder.delegate.setMode?.('recording');
- consumeEvent(event);
- return;
- }
-
- if (event.key === 'Enter') {
- this._commitAction();
- consumeEvent(event);
- return;
- }
-
- // Only allow keys that control text selection.
- if (!['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'Shift', 'Control', 'Meta', 'Alt', 'AltGraph'].includes(event.key)) {
- consumeEvent(event);
- return;
- }
- }
-
- onKeyUp(event: KeyboardEvent) {
+ if (event.key === 'Escape')
+ this._recorder.delegate.setMode?.('recording');
consumeEvent(event);
}
- onScroll(event: Event) {
- this._hoverHighlight = null;
- this._showHighlight(false);
- }
-
- private _generateAction(): actions.Action | null {
- if (this._inputHighlight) {
- const target = this._inputHighlight.elements[0] as HTMLInputElement;
- if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes(target.type.toLowerCase())) {
+ private _generateAction(): actions.AssertAction | null {
+ const target = this._hoverHighlight?.elements[0];
+ if (!target)
+ return null;
+ if (target.nodeName === 'INPUT' || target.nodeName === 'TEXTAREA') {
+ const { selector } = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
+ if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes((target as HTMLInputElement).type.toLowerCase())) {
return {
name: 'assertChecked',
- selector: this._inputHighlight.selector,
+ selector,
signals: [],
// Interestingly, inputElement.checked is reversed inside this event handler.
- checked: !(target as HTMLInputElement).checked,
+ checked: (target as HTMLInputElement).checked,
};
} else {
return {
name: 'assertValue',
- selector: this._inputHighlight.selector,
+ selector,
signals: [],
- value: target.value,
+ value: (target as HTMLInputElement).value,
};
}
- } else if (this._selectionText && this._selectionHighlight) {
+ } else {
+ const { selector } = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true });
return {
name: 'assertText',
- selector: this._selectionHighlight.selector,
+ selector,
signals: [],
- text: this._selectionText.selectedText,
- substring: this._selectionText.fullText !== this._selectionText.selectedText,
+ text: target.textContent!,
+ substring: true,
};
}
- return null;
}
- private _generateActionPreview() {
- const action = this._generateAction();
- // TODO: support other languages, maybe unify with code generator?
+ private _renderValue(action: actions.Action) {
if (action?.name === 'assertText')
- return `expect(${asLocator(this._recorder.state.language, action.selector)}).${action.substring ? 'toContainText' : 'toHaveText'}(${escapeWithQuotes(action.text)})`;
+ return normalizeWhiteSpace(action.text);
if (action?.name === 'assertChecked')
- return `expect(${asLocator(this._recorder.state.language, action.selector)})${action.checked ? '' : '.not'}.toBeChecked()`;
- if (action?.name === 'assertValue') {
- const assertion = action.value ? `toHaveValue(${escapeWithQuotes(action.value)})` : `toBeEmpty()`;
- return `expect(${asLocator(this._recorder.state.language, action.selector)}).${assertion}`;
- }
+ return String(action.checked);
+ if (action?.name === 'assertValue')
+ return action.value;
return '';
}
- private _commitAction() {
- const action = this._generateAction();
- if (action) {
- this._resetSelectionAndHighlight();
- this._recorder.delegate.recordAction?.(action);
- this._recorder.delegate.setMode?.('recording');
- }
- }
-
- private _cancelAction() {
- this._resetSelectionAndHighlight();
- }
-
- private _resetSelectionAndHighlight() {
- this._selectionHighlight = null;
- this._selectionText = null;
- this._inputHighlight = null;
- this._recorder.injectedScript.window.getSelection()?.empty();
- this._recorder.updateHighlight(null, false);
- }
-
- private _updateSelectionHighlight() {
- if (this._inputHighlight)
+ private _commit() {
+ if (!this._action || !this._dialogElement)
return;
- const selection = this._recorder.document.getSelection();
- const selectedText = normalizeWhiteSpace(selection?.toString() || '');
- let highlight: HighlightModel | null = null;
- if (selection && selection.focusNode && selection.anchorNode && selectedText) {
- const focusElement = enclosingElement(selection.focusNode);
- let lcaElement = focusElement ? enclosingElement(selection.anchorNode) : undefined;
- while (lcaElement && !isInsideScope(lcaElement, focusElement))
- lcaElement = parentElementOrShadowHost(lcaElement);
- highlight = lcaElement ? generateSelector(this._recorder.injectedScript, lcaElement, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }) : null;
- }
- const fullText = highlight ? normalizeWhiteSpace(elementText(new Map(), highlight.elements[0]).full) : '';
- const selectionText = highlight ? { selectedText, fullText } : null;
- if (highlight?.selector === this._selectionHighlight?.selector && this._selectionText?.fullText === selectionText?.fullText && this._selectionText?.selectedText === selectionText?.selectedText)
- return;
- this._selectionHighlight = highlight;
- this._selectionText = selectionText;
- this._showHighlight(true);
+ this._closeDialog();
+ this._recorder.delegate.recordAction?.(this._action);
+ this._recorder.delegate.setMode?.('recording');
}
- private _showHighlight(userGesture: boolean) {
- const options: HighlightOptions = {
- color: '#6fdcbd38',
- tooltipText: this._generateActionPreview(),
- toolbar: [this._acceptButton, this._cancelButton],
- interactive: true,
+ private _showDialog() {
+ const target = this._hoverHighlight?.elements[0];
+ if (!target)
+ return;
+ this._action = this._generateAction();
+ if (!this._action)
+ return;
+
+ this._dialogElement = this._recorder.document.createElement('x-pw-dialog');
+ this._keyboardListener = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ this._closeDialog();
+ return;
+ }
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
+ if (this._dialogElement)
+ this._commit();
+ return;
+ }
};
- if (this._inputHighlight) {
- this._recorder.updateHighlight(this._inputHighlight, userGesture, options);
- } else {
- options.anchorGetter = (e: Element) => this._recorder.document.getSelection()?.getRangeAt(0)?.getBoundingClientRect() || e.getBoundingClientRect();
- this._recorder.updateHighlight(this._selectionHighlight, userGesture, options);
- }
+ this._recorder.document.addEventListener('keydown', this._keyboardListener, true);
+ const toolbarElement = this._recorder.document.createElement('x-pw-tools-list');
+ toolbarElement.appendChild(this._createLabel(this._action));
+ toolbarElement.appendChild(this._recorder.document.createElement('x-spacer'));
+ toolbarElement.appendChild(this._acceptButton);
+ toolbarElement.appendChild(this._cancelButton);
+
+ this._dialogElement.appendChild(toolbarElement);
+ const bodyElement = this._recorder.document.createElement('x-pw-dialog-body');
+ const locatorElement = this._recorder.document.createElement('input');
+ locatorElement.classList.add('locator-editor');
+ locatorElement.value = asLocator(this._recorder.state.language, this._action.selector);
+ locatorElement.addEventListener('input', () => {
+ if (this._action) {
+ const selector = locatorOrSelectorAsSelector(this._recorder.state.language, locatorElement.value, this._recorder.state.testIdAttributeName);
+ const model: HighlightModel = {
+ selector,
+ elements: this._recorder.injectedScript.querySelectorAll(parseSelector(selector), this._recorder.document),
+ };
+ this._action.selector = selector;
+ this._recorder.updateHighlight(model, true);
+ }
+ });
+ const textElement = this._recorder.document.createElement('textarea');
+ textElement.value = this._renderValue(this._action);
+ textElement.classList.add('text-editor');
+
+ textElement.addEventListener('input', () => {
+ if (this._action?.name === 'assertText')
+ this._action.text = normalizeWhiteSpace(elementText(new Map(), textElement).full);
+ if (this._action?.name === 'assertChecked')
+ this._action.checked = textElement.value === 'true';
+ if (this._action?.name === 'assertValue')
+ this._action.value = textElement.value;
+ });
+
+ bodyElement.appendChild(locatorElement);
+ bodyElement.appendChild(textElement);
+ this._dialogElement.appendChild(bodyElement);
+ this._recorder.highlight.appendChild(this._dialogElement);
+ const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement);
+ this._dialogElement.style.top = position.anchorTop + 'px';
+ this._dialogElement.style.left = position.anchorLeft + 'px';
+ textElement.focus();
+ }
+
+ private _createLabel(action: actions.AssertAction) {
+ const labelElement = this._recorder.document.createElement('x-pw-tool-label');
+ labelElement.textContent = action.name === 'assertText' ? 'Assert text' : action.name === 'assertValue' ? 'Assert value' : 'Assert checked';
+ return labelElement;
+ }
+
+ private _closeDialog() {
+ if (!this._dialogElement)
+ return;
+ this._dialogElement.remove();
+ this._recorder.document.removeEventListener('keydown', this._keyboardListener!);
+ this._dialogElement = null;
}
}
diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts
index 5be43e9ea9..3a4bbab325 100644
--- a/packages/playwright-core/src/server/recorder/recorderActions.ts
+++ b/packages/playwright-core/src/server/recorder/recorderActions.ts
@@ -114,6 +114,7 @@ export type AssertCheckedAction = ActionBase & {
};
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction;
+export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction;
// Signals.