feat(codegen): add button to generate toHaveScreenshot statement (#29996)

Fixes #29250.
This commit is contained in:
Dmitry Gozman 2024-03-19 14:01:04 -07:00 committed by GitHub
parent 54aca430b0
commit 1bb463163b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 71 additions and 17 deletions

View File

@ -231,6 +231,12 @@ x-pw-tool-item.value > x-div {
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M4 6h8v1H4V6zm8 3H4v1h8V9z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z'/></svg>");
}
x-pw-tool-item.screenshot > x-div {
/* codicon: device-camera */
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M10.707 3H14.5l.5.5v9l-.5.5h-13l-.5-.5v-9l.5-.5h3.793l.853-.854L6.5 2h3l.354.146.853.854zM2 12h12V4h-3.5l-.354-.146L9.293 3H6.707l-.853.854L5.5 4H2v8zm1.5-7a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1zM8 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm0-1a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'/></svg>");
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M10.707 3H14.5l.5.5v9l-.5.5h-13l-.5-.5v-9l.5-.5h3.793l.853-.854L6.5 2h3l.354.146.853.854zM2 12h12V4h-3.5l-.354-.146L9.293 3H6.707l-.853.854L5.5 4H2v8zm1.5-7a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1zM8 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm0-1a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'/></svg>");
}
x-pw-tool-item.accept > x-div {
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/></svg>");
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/></svg>");

View File

@ -761,6 +761,7 @@ class Overlay {
private _assertVisibilityToggle: HTMLElement;
private _assertTextToggle: HTMLElement;
private _assertValuesToggle: HTMLElement;
private _assertScreenshotButton: HTMLElement;
private _offsetX = 0;
private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined;
private _measure: { width: number, height: number } = { width: 0, height: 0 };
@ -807,6 +808,12 @@ class Overlay {
this._assertValuesToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
toolsListElement.appendChild(this._assertValuesToggle);
this._assertScreenshotButton = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
this._assertScreenshotButton.title = 'Assert screenshot';
this._assertScreenshotButton.classList.add('screenshot');
this._assertScreenshotButton.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
toolsListElement.appendChild(this._assertScreenshotButton);
this._updateVisualPosition();
this._refreshListeners();
}
@ -845,6 +852,15 @@ class Overlay {
if (!this._assertValuesToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue');
}),
addEventListener(this._assertScreenshotButton, 'click', () => {
if (!this._assertScreenshotButton.classList.contains('disabled')) {
this._recorder.delegate.recordAction?.({
name: 'assertScreenshot',
signals: [],
});
this.flashToolSucceeded('assertScreenshot');
}
}),
];
}
@ -867,6 +883,7 @@ class Overlay {
this._assertTextToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
this._assertValuesToggle.classList.toggle('active', state.mode === 'assertingValue');
this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
this._assertScreenshotButton.classList.toggle('disabled', state.mode !== 'recording');
if (this._offsetX !== state.overlay.offsetX) {
this._offsetX = state.overlay.offsetX;
this._updateVisualPosition();
@ -877,8 +894,12 @@ class Overlay {
this._showOverlay();
}
flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') {
const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle;
flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue' | 'assertScreenshot') {
const element = {
'assertingVisibility': this._assertVisibilityToggle,
'assertingValue': this._assertValuesToggle,
'assertScreenshot': this._assertScreenshotButton,
}[tool];
element.classList.add('succeeded');
setTimeout(() => element.classList.remove('succeeded'), 2000);
}

View File

@ -164,6 +164,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmptyAsync()`;
return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
case 'assertScreenshot':
return `// AssertScreenshot(await ${subject}.ScreenshotAsync());`;
}
}

View File

@ -152,6 +152,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `hasValue(${quote(action.value)})` : `isEmpty()`;
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`;
}
case 'assertScreenshot':
return `// assertScreenshot(${subject}.screenshot());`;
}
}

View File

@ -135,6 +135,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
}
case 'assertScreenshot':
return `${this._isTest ? '' : '// '}await expect(${subject}).toHaveScreenshot();`;
}
}

View File

@ -73,7 +73,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
if (signals.dialog)
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`;
let code = this._generateActionCall(subject, action);
if (signals.popup) {
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
@ -99,7 +99,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
case 'openPage':
throw Error('Not reached');
case 'closePage':
return `${subject}.close()`;
return `${this._awaitPrefix}${subject}.close()`;
case 'click': {
let method = 'click';
if (action.clickCount === 2)
@ -115,35 +115,37 @@ export class PythonLanguageGenerator implements LanguageGenerator {
if (action.position)
options.position = action.position;
const optionsString = formatOptions(options, false);
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
}
case 'check':
return `${subject}.${this._asLocator(action.selector)}.check()`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.check()`;
case 'uncheck':
return `${subject}.${this._asLocator(action.selector)}.uncheck()`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.uncheck()`;
case 'fill':
return `${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)})`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)})`;
case 'setInputFiles':
return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
case 'press': {
const modifiers = toModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+');
return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
}
case 'navigate':
return `${subject}.goto(${quote(action.url)})`;
return `${this._awaitPrefix}${subject}.goto(${quote(action.url)})`;
case 'select':
return `${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
return `${this._awaitPrefix}${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)})`;
return `${this._awaitPrefix}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()'}`;
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${action.checked ? 'to_be_checked()' : 'not_to_be_checked()'}`;
case 'assertVisible':
return `expect(${subject}.${this._asLocator(action.selector)}).to_be_visible()`;
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).to_be_visible()`;
case 'assertValue': {
const assertion = action.value ? `to_have_value(${quote(action.value)})` : `to_be_empty()`;
return `expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${assertion}`;
}
case 'assertScreenshot':
return `# assert_screenshot(${this._awaitPrefix}${subject}.screenshot())`;
}
}

View File

@ -30,6 +30,7 @@ export type ActionName =
'assertText' |
'assertValue' |
'assertChecked' |
'assertScreenshot' |
'assertVisible';
export type ActionBase = {
@ -119,7 +120,11 @@ export type AssertVisibleAction = ActionBase & {
selector: string,
};
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction;
export type AssertScreenshotAction = ActionBase & {
name: 'assertScreenshot',
};
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertScreenshotAction;
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction;
// Signals.

View File

@ -648,4 +648,18 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
expect.soft(sources1.get('Java')!.text).toContain(`assertThat(page.getByRole(AriaRole.TEXTBOX)).isVisible()`);
expect.soft(sources1.get('C#')!.text).toContain(`await Expect(page.GetByRole(AriaRole.Textbox)).ToBeVisibleAsync()`);
});
test('should assert screenshot', async ({ openRecorder }) => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`<div>Hello, world</div>`);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'toHaveScreenshot'),
recorder.page.click('x-pw-tool-item.screenshot'),
]);
expect.soft(sources.get('JavaScript')!.text).toContain(`await expect(page).toHaveScreenshot()`);
expect.soft(sources.get('Python')!.text).toContain(`# assert_screenshot(page.screenshot())`);
expect.soft(sources.get('Python Async')!.text).toContain(`# assert_screenshot(await page.screenshot())`);
expect.soft(sources.get('Java')!.text).toContain(`// assertScreenshot(page.screenshot());`);
expect.soft(sources.get('C#')!.text).toContain(`// AssertScreenshot(await page.ScreenshotAsync())`);
});
});