diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index c9413e0a72..3432f159dd 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -552,28 +552,14 @@ class TextAssertionTool implements RecorderTool { private _recorder: Recorder; private _hoverHighlight: 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; + private _dialog: Dialog; private _textCache = new Map(); private _kind: 'text' | 'value'; constructor(recorder: Recorder, kind: 'text' | 'value') { this._recorder = recorder; this._kind = kind; - - 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._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._closeDialog()); + this._dialog = new Dialog(recorder); } cursor() { @@ -581,7 +567,7 @@ class TextAssertionTool implements RecorderTool { } cleanup() { - this._closeDialog(); + this._dialog.close(); this._hoverHighlight = null; } @@ -590,7 +576,7 @@ class TextAssertionTool implements RecorderTool { if (this._kind === 'value') { this._commitAssertValue(); } else { - if (!this._dialogElement) + if (!this._dialog.isShowing()) this._showDialog(); } } @@ -611,7 +597,7 @@ class TextAssertionTool implements RecorderTool { } onMouseMove(event: MouseEvent) { - if (this._dialogElement) + if (this._dialog.isShowing()) return; const target = this._recorder.deepEventTarget(event); if (this._hoverHighlight?.elements[0] === target) @@ -691,9 +677,9 @@ class TextAssertionTool implements RecorderTool { } private _commit() { - if (!this._action || !this._dialogElement) + if (!this._action || !this._dialog.isShowing()) return; - this._closeDialog(); + this._dialog.close(); this._recorder.delegate.recordAction?.(this._action); this._recorder.delegate.setMode?.('recording'); } @@ -705,31 +691,6 @@ class TextAssertionTool implements RecorderTool { if (!this._action || this._action.name !== 'assertText') 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; - } - }; - - this._recorder.document.addEventListener('keydown', this._keyboardListener, true); - const toolbarElement = this._recorder.document.createElement('x-pw-tools-list'); - const labelElement = this._recorder.document.createElement('label'); - labelElement.textContent = 'Assert that element contains text'; - toolbarElement.appendChild(labelElement); - 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 action = this._action; const textElement = this._recorder.document.createElement('textarea'); textElement.setAttribute('spellcheck', 'false'); @@ -747,24 +708,18 @@ class TextAssertionTool implements RecorderTool { textElement.classList.toggle('does-not-match', !matches); }; textElement.addEventListener('input', updateAndValidate); - 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'; + const label = 'Assert that element contains text'; + const dialogElement = this._dialog.show({ + label, + body: textElement, + onCommit: () => this._commit(), + }); + const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, dialogElement); + this._dialog.moveTo(position.anchorTop, position.anchorLeft); textElement.focus(); } - private _closeDialog() { - if (!this._dialogElement) - return; - this._dialogElement.remove(); - this._recorder.document.removeEventListener('keydown', this._keyboardListener!); - this._dialogElement = null; - } - private _commitAssertValue() { if (this._kind !== 'value') return; @@ -1219,6 +1174,87 @@ export class Recorder { } } +class Dialog { + private _recorder: Recorder; + private _dialogElement: HTMLElement | null = null; + private _keyboardListener: ((event: KeyboardEvent) => void) | undefined; + + constructor(recorder: Recorder) { + this._recorder = recorder; + } + + isShowing(): boolean { + return !!this._dialogElement; + } + + show(options: { + label: string; + body: Element; + onCommit: () => void; + onCancel?: () => void; + }) { + const acceptButton = this._recorder.document.createElement('x-pw-tool-item'); + acceptButton.title = 'Accept'; + acceptButton.classList.add('accept'); + acceptButton.appendChild(this._recorder.document.createElement('x-div')); + acceptButton.addEventListener('click', () => options.onCommit()); + + const cancelButton = this._recorder.document.createElement('x-pw-tool-item'); + cancelButton.title = 'Close'; + cancelButton.classList.add('cancel'); + cancelButton.appendChild(this._recorder.document.createElement('x-div')); + cancelButton.addEventListener('click', () => { + this.close(); + options.onCancel?.(); + }); + + this._dialogElement = this._recorder.document.createElement('x-pw-dialog'); + this._keyboardListener = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + this.close(); + options.onCancel?.(); + return; + } + if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { + if (this._dialogElement) + options.onCommit(); + return; + } + }; + + this._recorder.document.addEventListener('keydown', this._keyboardListener, true); + const toolbarElement = this._recorder.document.createElement('x-pw-tools-list'); + const labelElement = this._recorder.document.createElement('label'); + labelElement.textContent = options.label; + toolbarElement.appendChild(labelElement); + toolbarElement.appendChild(this._recorder.document.createElement('x-spacer')); + toolbarElement.appendChild(acceptButton); + toolbarElement.appendChild(cancelButton); + + this._dialogElement.appendChild(toolbarElement); + const bodyElement = this._recorder.document.createElement('x-pw-dialog-body'); + bodyElement.appendChild(options.body); + this._dialogElement.appendChild(bodyElement); + this._recorder.highlight.appendChild(this._dialogElement); + return this._dialogElement; + } + + moveTo(top: number, left: number) { + if (!this._dialogElement) + return; + this._dialogElement.style.top = top + 'px'; + this._dialogElement.style.left = left + 'px'; + } + + close() { + if (!this._dialogElement) + return; + this._dialogElement.remove(); + this._recorder.document.removeEventListener('keydown', this._keyboardListener!); + this._dialogElement = null; + } +} + function deepActiveElement(document: Document): Element | null { let activeElement = document.activeElement; while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)