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.