mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(recorder): use designMode for text selection tool (#27936)
This allows us to barely support shadow dom, compared to no support at all.
This commit is contained in:
parent
c59483c5fb
commit
a7fd515626
@ -34,8 +34,10 @@ interface RecorderDelegate {
|
|||||||
|
|
||||||
interface RecorderTool {
|
interface RecorderTool {
|
||||||
cursor(): string;
|
cursor(): string;
|
||||||
|
enable?(): void;
|
||||||
disable?(): void;
|
disable?(): void;
|
||||||
onClick?(event: MouseEvent): void;
|
onClick?(event: MouseEvent): void;
|
||||||
|
onDragStart?(event: DragEvent): void;
|
||||||
onInput?(event: Event): void;
|
onInput?(event: Event): void;
|
||||||
onKeyDown?(event: KeyboardEvent): void;
|
onKeyDown?(event: KeyboardEvent): void;
|
||||||
onKeyUp?(event: KeyboardEvent): void;
|
onKeyUp?(event: KeyboardEvent): void;
|
||||||
@ -414,7 +416,8 @@ class RecordActionTool implements RecorderTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TextAssertionTool implements RecorderTool {
|
class TextAssertionTool implements RecorderTool {
|
||||||
private _selectionModel: SelectionModel | null = null;
|
private _selectionHighlight: HighlightModel | null = null;
|
||||||
|
private _inputIsFocused = false;
|
||||||
|
|
||||||
constructor(private _recorder: Recorder) {
|
constructor(private _recorder: Recorder) {
|
||||||
}
|
}
|
||||||
@ -423,18 +426,21 @@ class TextAssertionTool implements RecorderTool {
|
|||||||
return 'text';
|
return 'text';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this._recorder.injectedScript.document.designMode = 'on';
|
||||||
|
}
|
||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
this._selectionModel = null;
|
this._recorder.injectedScript.document.designMode = 'off';
|
||||||
this._syncDocumentSelection();
|
this._selectionHighlight = null;
|
||||||
|
this._inputIsFocused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(event: MouseEvent) {
|
onClick(event: MouseEvent) {
|
||||||
consumeEvent(event);
|
consumeEvent(event);
|
||||||
if (event.detail !== 1 || this._getSelectionText())
|
|
||||||
return;
|
|
||||||
const target = this._recorder.deepEventTarget(event);
|
|
||||||
|
|
||||||
if (['INPUT', 'TEXTAREA'].includes(target.nodeName) || target.isContentEditable) {
|
const target = this._recorder.deepEventTarget(event);
|
||||||
|
if (event.detail === 1 && ['INPUT', 'TEXTAREA'].includes(target.nodeName)) {
|
||||||
const highlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
|
const highlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
|
||||||
if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes((target as HTMLInputElement).type.toLowerCase())) {
|
if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes((target as HTMLInputElement).type.toLowerCase())) {
|
||||||
this._recorder.delegate.recordAction?.({
|
this._recorder.delegate.recordAction?.({
|
||||||
@ -452,71 +458,72 @@ class TextAssertionTool implements RecorderTool {
|
|||||||
value: target.isContentEditable ? target.innerText : (target as HTMLInputElement).value,
|
value: target.isContentEditable ? target.innerText : (target as HTMLInputElement).value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._recorder.updateHighlight(highlight, true, '#6fdcbd38');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = target ? elementText(new Map(), target).full : '';
|
const selection = this._recorder.document.getSelection();
|
||||||
if (text) {
|
if (event.detail === 1 && selection && !selection.toString()) {
|
||||||
this._selectionModel = { anchor: { node: target, offset: 0 }, focus: { node: target, offset: target.childNodes.length }, highlight: null };
|
selection.selectAllChildren(target);
|
||||||
this._syncDocumentSelection();
|
|
||||||
this._updateSelectionHighlight();
|
this._updateSelectionHighlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown(event: MouseEvent) {
|
onMouseDown(event: MouseEvent) {
|
||||||
consumeEvent(event);
|
|
||||||
const target = this._recorder.deepEventTarget(event);
|
const target = this._recorder.deepEventTarget(event);
|
||||||
if (['INPUT', 'TEXTAREA'].includes(target.nodeName) || target.isContentEditable) {
|
if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) {
|
||||||
this._selectionModel = null;
|
this._recorder.injectedScript.window.getSelection()?.empty();
|
||||||
this._syncDocumentSelection();
|
this._selectionHighlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
|
||||||
const highlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
|
this._inputIsFocused = true;
|
||||||
this._recorder.updateHighlight(highlight, true, '#6fdcbd38');
|
this._recorder.updateHighlight(this._selectionHighlight, true, '#6fdcbd38');
|
||||||
|
consumeEvent(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pos = this._selectionPosition(event);
|
|
||||||
if (pos && event.detail <= 1) {
|
this._inputIsFocused = false;
|
||||||
this._selectionModel = { anchor: pos, focus: pos, highlight: null };
|
this._updateSelectionHighlight();
|
||||||
this._syncDocumentSelection();
|
|
||||||
this._updateSelectionHighlight();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseUp(event: MouseEvent) {
|
onMouseUp(event: MouseEvent) {
|
||||||
consumeEvent(event);
|
this._updateSelectionHighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseMove(event: MouseEvent) {
|
onMouseMove(event: MouseEvent) {
|
||||||
|
this._updateSelectionHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragStart(event: DragEvent) {
|
||||||
consumeEvent(event);
|
consumeEvent(event);
|
||||||
if (!event.buttons)
|
|
||||||
return;
|
|
||||||
const pos = this._selectionPosition(event);
|
|
||||||
if (pos && this._selectionModel) {
|
|
||||||
this._selectionModel.focus = pos;
|
|
||||||
this._syncDocumentSelection();
|
|
||||||
this._updateSelectionHighlight();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown(event: KeyboardEvent) {
|
onKeyDown(event: KeyboardEvent) {
|
||||||
consumeEvent(event);
|
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
this._selectionModel = null;
|
this._resetSelectionAndHighlight();
|
||||||
this._syncDocumentSelection();
|
consumeEvent(event);
|
||||||
this._recorder.updateHighlight(null, false);
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Enter' && this._selectionModel?.highlight) {
|
|
||||||
const text = this._getSelectionText();
|
if (event.key === 'Enter') {
|
||||||
this._recorder.delegate.recordAction?.({
|
const selection = this._recorder.document.getSelection();
|
||||||
name: 'assertText',
|
if (selection && this._selectionHighlight) {
|
||||||
selector: this._selectionModel.highlight.selector,
|
const selectedText = normalizeWhiteSpace(selection.toString());
|
||||||
signals: [],
|
const fullText = normalizeWhiteSpace(elementText(new Map(), this._selectionHighlight.elements[0]).full);
|
||||||
text,
|
this._recorder.delegate.recordAction?.({
|
||||||
substring: normalizeWhiteSpace(elementText(new Map(), this._selectionModel.highlight.elements[0]).full) !== text,
|
name: 'assertText',
|
||||||
});
|
selector: this._selectionHighlight.selector,
|
||||||
this._selectionModel = null;
|
signals: [],
|
||||||
this._syncDocumentSelection();
|
text: selectedText,
|
||||||
this._recorder.updateHighlight(null, false);
|
substring: fullText !== selectedText,
|
||||||
|
});
|
||||||
|
this._resetSelectionAndHighlight();
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -525,50 +532,30 @@ class TextAssertionTool implements RecorderTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onScroll(event: Event) {
|
onScroll(event: Event) {
|
||||||
this._recorder.updateHighlight(this._selectionModel ? this._selectionModel.highlight : null, false, '#6fdcbd38');
|
this._recorder.updateHighlight(this._selectionHighlight, false, '#6fdcbd38');
|
||||||
}
|
}
|
||||||
|
|
||||||
private _selectionPosition(event: MouseEvent) {
|
private _resetSelectionAndHighlight() {
|
||||||
if ((this._recorder.document as any).caretPositionFromPoint) {
|
this._selectionHighlight = null;
|
||||||
const range = (this._recorder.document as any).caretPositionFromPoint(event.clientX, event.clientY);
|
this._recorder.injectedScript.window.getSelection()?.empty();
|
||||||
return range ? { node: range.offsetNode, offset: range.offset } : undefined;
|
this._recorder.updateHighlight(null, false);
|
||||||
}
|
|
||||||
if ((this._recorder.document as any).caretRangeFromPoint) {
|
|
||||||
const range = this._recorder.document.caretRangeFromPoint(event.clientX, event.clientY);
|
|
||||||
return range ? { node: range.startContainer, offset: range.startOffset } : undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _syncDocumentSelection() {
|
|
||||||
if (!this._selectionModel) {
|
|
||||||
this._recorder.document.getSelection()?.empty();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._recorder.document.getSelection()?.setBaseAndExtent(
|
|
||||||
this._selectionModel.anchor.node,
|
|
||||||
this._selectionModel.anchor.offset,
|
|
||||||
this._selectionModel.focus.node,
|
|
||||||
this._selectionModel.focus.offset,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getSelectionText() {
|
|
||||||
this._syncDocumentSelection();
|
|
||||||
// TODO: use elementText() passing |range=selection.getRangeAt(0)| for proper text.
|
|
||||||
return normalizeWhiteSpace(this._recorder.document.getSelection()?.toString() || '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateSelectionHighlight() {
|
private _updateSelectionHighlight() {
|
||||||
if (!this._selectionModel)
|
if (this._inputIsFocused)
|
||||||
return;
|
return;
|
||||||
const focusElement = enclosingElement(this._selectionModel.focus.node);
|
const selection = this._recorder.document.getSelection();
|
||||||
let lcaElement = focusElement ? enclosingElement(this._selectionModel.anchor.node) : undefined;
|
let highlight: HighlightModel | null = null;
|
||||||
while (lcaElement && !isInsideScope(lcaElement, focusElement))
|
if (selection && selection.focusNode && selection.anchorNode && selection.toString()) {
|
||||||
lcaElement = parentElementOrShadowHost(lcaElement);
|
const focusElement = enclosingElement(selection.focusNode);
|
||||||
const highlight = lcaElement ? generateSelector(this._recorder.injectedScript, lcaElement, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }) : null;
|
let lcaElement = focusElement ? enclosingElement(selection.anchorNode) : undefined;
|
||||||
if (highlight?.selector === this._selectionModel.highlight?.selector)
|
while (lcaElement && !isInsideScope(lcaElement, focusElement))
|
||||||
|
lcaElement = parentElementOrShadowHost(lcaElement);
|
||||||
|
highlight = lcaElement ? generateSelector(this._recorder.injectedScript, lcaElement, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }) : null;
|
||||||
|
}
|
||||||
|
if (highlight?.selector === this._selectionHighlight?.selector)
|
||||||
return;
|
return;
|
||||||
this._selectionModel.highlight = highlight;
|
this._selectionHighlight = highlight;
|
||||||
this._recorder.updateHighlight(highlight, true, '#6fdcbd38');
|
this._recorder.updateHighlight(highlight, true, '#6fdcbd38');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -794,6 +781,7 @@ export class Recorder {
|
|||||||
this._listeners = [
|
this._listeners = [
|
||||||
addEventListener(this.document, 'click', event => this._onClick(event as MouseEvent), true),
|
addEventListener(this.document, 'click', event => this._onClick(event as MouseEvent), true),
|
||||||
addEventListener(this.document, 'auxclick', event => this._onClick(event as MouseEvent), true),
|
addEventListener(this.document, 'auxclick', event => this._onClick(event as MouseEvent), true),
|
||||||
|
addEventListener(this.document, 'dragstart', event => this._onDragStart(event as DragEvent), true),
|
||||||
addEventListener(this.document, 'input', event => this._onInput(event), true),
|
addEventListener(this.document, 'input', event => this._onInput(event), true),
|
||||||
addEventListener(this.document, 'keydown', event => this._onKeyDown(event as KeyboardEvent), true),
|
addEventListener(this.document, 'keydown', event => this._onKeyDown(event as KeyboardEvent), true),
|
||||||
addEventListener(this.document, 'keyup', event => this._onKeyUp(event as KeyboardEvent), true),
|
addEventListener(this.document, 'keyup', event => this._onKeyUp(event as KeyboardEvent), true),
|
||||||
@ -816,7 +804,8 @@ export class Recorder {
|
|||||||
this._currentTool.disable?.();
|
this._currentTool.disable?.();
|
||||||
this.clearHighlight();
|
this.clearHighlight();
|
||||||
this._currentTool = newTool;
|
this._currentTool = newTool;
|
||||||
this.injectedScript.document.body.setAttribute('data-pw-cursor', newTool.cursor());
|
this._currentTool.enable?.();
|
||||||
|
this.injectedScript.document.body?.setAttribute('data-pw-cursor', newTool.cursor());
|
||||||
}
|
}
|
||||||
|
|
||||||
setUIState(state: UIState, delegate: RecorderDelegate) {
|
setUIState(state: UIState, delegate: RecorderDelegate) {
|
||||||
@ -860,6 +849,14 @@ export class Recorder {
|
|||||||
this._currentTool.onClick?.(event);
|
this._currentTool.onClick?.(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _onDragStart(event: DragEvent) {
|
||||||
|
if (!event.isTrusted)
|
||||||
|
return;
|
||||||
|
if (this._ignoreOverlayEvent(event))
|
||||||
|
return;
|
||||||
|
this._currentTool.onDragStart?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
private _onMouseDown(event: MouseEvent) {
|
private _onMouseDown(event: MouseEvent) {
|
||||||
if (!event.isTrusted)
|
if (!event.isTrusted)
|
||||||
return;
|
return;
|
||||||
@ -993,12 +990,6 @@ type HighlightModel = {
|
|||||||
elements: Element[];
|
elements: Element[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type SelectionModel = {
|
|
||||||
anchor: { node: Node, offset: number };
|
|
||||||
focus: { node: Node, offset: number };
|
|
||||||
highlight: HighlightModel | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function asCheckbox(node: Node | null): HTMLInputElement | null {
|
function asCheckbox(node: Node | null): HTMLInputElement | null {
|
||||||
if (!node || node.nodeName !== 'INPUT')
|
if (!node || node.nodeName !== 'INPUT')
|
||||||
return null;
|
return null;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user