feat(recorder): generate toHaveValue/toBeEmpty/toBeChecked (#27913)

This commit is contained in:
Dmitry Gozman 2023-11-01 21:17:25 -07:00 committed by GitHub
parent 0f2de59b7c
commit 07da88dcf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 74 additions and 5 deletions

View file

@ -433,6 +433,29 @@ class TextAssertionTool implements RecorderTool {
if (event.detail !== 1 || this._getSelectionText())
return;
const target = this._recorder.deepEventTarget(event);
if (['INPUT', 'TEXTAREA'].includes(target.nodeName) || target.isContentEditable) {
const highlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes((target as HTMLInputElement).type.toLowerCase())) {
this._recorder.delegate.recordAction?.({
name: 'assertChecked',
selector: highlight.selector,
signals: [],
// Interestingly, inputElement.checked is reversed inside this event handler.
checked: !(target as HTMLInputElement).checked,
});
} else {
this._recorder.delegate.recordAction?.({
name: 'assertValue',
selector: highlight.selector,
signals: [],
value: target.isContentEditable ? target.innerText : (target as HTMLInputElement).value,
});
}
this._recorder.updateHighlight(highlight, true, '#6fdcbd38');
return;
}
const text = target ? elementText(new Map(), target).full : '';
if (text) {
this._selectionModel = { anchor: { node: target, offset: 0 }, focus: { node: target, offset: target.childNodes.length }, highlight: null };
@ -443,6 +466,14 @@ class TextAssertionTool implements RecorderTool {
onMouseDown(event: MouseEvent) {
consumeEvent(event);
const target = this._recorder.deepEventTarget(event);
if (['INPUT', 'TEXTAREA'].includes(target.nodeName) || target.isContentEditable) {
this._selectionModel = null;
this._syncDocumentSelection();
const highlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName });
this._recorder.updateHighlight(highlight, true, '#6fdcbd38');
return;
}
const pos = this._selectionPosition(event);
if (pos && event.detail <= 1) {
this._selectionModel = { anchor: pos, focus: pos, highlight: null };
@ -538,7 +569,7 @@ class TextAssertionTool implements RecorderTool {
if (highlight?.selector === this._selectionModel.highlight?.selector)
return;
this._selectionModel.highlight = highlight;
this._recorder.updateHighlight(highlight, false, '#6fdcbd38');
this._recorder.updateHighlight(highlight, true, '#6fdcbd38');
}
}
@ -644,7 +675,7 @@ class Overlay {
none: this._createToolElement(toolsListElement, 'none', 'Disable'),
inspecting: this._createToolElement(toolsListElement, 'inspecting', 'Pick locator'),
recording: this._createToolElement(toolsListElement, 'recording', 'Record actions'),
assertingText: this._createToolElement(toolsListElement, 'assertingText', 'Assert text'),
assertingText: this._createToolElement(toolsListElement, 'assertingText', 'Assert text and values'),
};
this._overlayElement.addEventListener('mousedown', event => {

View file

@ -156,6 +156,12 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
return `await ${subject}.${this._asLocator(action.selector)}.SelectOptionAsync(${formatObject(action.options)});`;
case 'assertText':
return `await Expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'ToContainTextAsync' : 'ToHaveTextAsync'}(${quote(action.text)});`;
case 'assertChecked':
return `await Expect(${subject}.${this._asLocator(action.selector)})${action.checked ? '' : '.Not'}.ToBeCheckedAsync();`;
case 'assertValue': {
const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmpty()`;
return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
}
}

View file

@ -124,6 +124,12 @@ export class JavaLanguageGenerator implements LanguageGenerator {
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.selectOption(${formatSelectOption(action.options.length > 1 ? action.options : action.options[0])});`;
case 'assertText':
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${action.substring ? 'containsText' : 'hasText'}(${quote(action.text)});`;
case 'assertChecked':
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)})${action.checked ? '' : '.not()'}.isChecked();`;
case 'assertValue': {
const assertion = action.value ? `hasValue(${quote(action.value)})` : `isEmpty()`;
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`;
}
}
}

View file

@ -127,6 +127,12 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return `await ${subject}.${this._asLocator(action.selector)}.selectOption(${formatObject(action.options.length > 1 ? action.options : action.options[0])});`;
case 'assertText':
return `await expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'toContainText' : 'toHaveText'}(${quote(action.text)});`;
case 'assertChecked':
return `await expect(${subject}.${this._asLocator(action.selector)})${action.checked ? '' : '.not'}.toBeChecked();`;
case 'assertValue': {
const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
return `await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
}
}

View file

@ -136,6 +136,12 @@ export class PythonLanguageGenerator implements LanguageGenerator {
return `${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
case 'assertText':
return `expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'to_contain_text' : 'to_have_text'}(${quote(action.text)})`;
case 'assertChecked':
return `expect(${subject}.${this._asLocator(action.selector)}).${action.checked ? 'to_be_checked()' : 'not_to_be_checked()'}`;
case 'assertValue': {
const assertion = action.value ? `to_have_value(${quote(action.value)})` : `to_be_empty()`;
return `expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
}
}

View file

@ -27,7 +27,9 @@ export type ActionName =
'select' |
'uncheck' |
'setInputFiles' |
'assertText';
'assertText' |
'assertValue' |
'assertChecked';
export type ActionBase = {
name: ActionName,
@ -99,7 +101,19 @@ export type AssertTextAction = ActionBase & {
substring: boolean,
};
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction;
export type AssertValueAction = ActionBase & {
name: 'assertValue',
selector: string,
value: string,
};
export type AssertCheckedAction = ActionBase & {
name: 'assertChecked',
selector: string,
checked: boolean,
};
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction;
// Signals.

View file

@ -116,7 +116,7 @@ export const Recorder: React.FC<RecorderProps> = ({
<ToolbarButton icon='record' title='Record actions' toggled={mode === 'recording'} onClick={() => {
window.dispatch({ event: 'setMode', params: { mode: mode === 'recording' ? 'none' : 'recording' } });
}}>Record</ToolbarButton>
<ToolbarButton icon='text-size' title='Assert text' toggled={mode === 'assertingText'} onClick={() => {
<ToolbarButton icon='text-size' title='Assert text and values' toggled={mode === 'assertingText'} onClick={() => {
window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingText' ? 'none' : 'assertingText' } });
}}>Assert</ToolbarButton>
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {