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>"); 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 { 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>"); -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>"); 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 _assertVisibilityToggle: HTMLElement;
private _assertTextToggle: HTMLElement; private _assertTextToggle: HTMLElement;
private _assertValuesToggle: HTMLElement; private _assertValuesToggle: HTMLElement;
private _assertScreenshotButton: HTMLElement;
private _offsetX = 0; private _offsetX = 0;
private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined; private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined;
private _measure: { width: number, height: number } = { width: 0, height: 0 }; 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')); this._assertValuesToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
toolsListElement.appendChild(this._assertValuesToggle); 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._updateVisualPosition();
this._refreshListeners(); this._refreshListeners();
} }
@ -845,6 +852,15 @@ class Overlay {
if (!this._assertValuesToggle.classList.contains('disabled')) if (!this._assertValuesToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue'); 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._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('active', state.mode === 'assertingValue');
this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting'); 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) { if (this._offsetX !== state.overlay.offsetX) {
this._offsetX = state.overlay.offsetX; this._offsetX = state.overlay.offsetX;
this._updateVisualPosition(); this._updateVisualPosition();
@ -877,8 +894,12 @@ class Overlay {
this._showOverlay(); this._showOverlay();
} }
flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') { flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue' | 'assertScreenshot') {
const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle; const element = {
'assertingVisibility': this._assertVisibilityToggle,
'assertingValue': this._assertValuesToggle,
'assertScreenshot': this._assertScreenshotButton,
}[tool];
element.classList.add('succeeded'); element.classList.add('succeeded');
setTimeout(() => element.classList.remove('succeeded'), 2000); 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()`; const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmptyAsync()`;
return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; 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()`; const assertion = action.value ? `hasValue(${quote(action.value)})` : `isEmpty()`;
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`; 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()`; const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; 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) if (signals.dialog)
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`); 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) { if (signals.popup) {
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info { code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
@ -99,7 +99,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
case 'openPage': case 'openPage':
throw Error('Not reached'); throw Error('Not reached');
case 'closePage': case 'closePage':
return `${subject}.close()`; return `${this._awaitPrefix}${subject}.close()`;
case 'click': { case 'click': {
let method = 'click'; let method = 'click';
if (action.clickCount === 2) if (action.clickCount === 2)
@ -115,35 +115,37 @@ export class PythonLanguageGenerator implements LanguageGenerator {
if (action.position) if (action.position)
options.position = action.position; options.position = action.position;
const optionsString = formatOptions(options, false); 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': case 'check':
return `${subject}.${this._asLocator(action.selector)}.check()`; return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.check()`;
case 'uncheck': case 'uncheck':
return `${subject}.${this._asLocator(action.selector)}.uncheck()`; return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.uncheck()`;
case 'fill': 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': 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': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); 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': case 'navigate':
return `${subject}.goto(${quote(action.url)})`; return `${this._awaitPrefix}${subject}.goto(${quote(action.url)})`;
case 'select': 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': 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': 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': 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': { case 'assertValue': {
const assertion = action.value ? `to_have_value(${quote(action.value)})` : `to_be_empty()`; 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' | 'assertText' |
'assertValue' | 'assertValue' |
'assertChecked' | 'assertChecked' |
'assertScreenshot' |
'assertVisible'; 'assertVisible';
export type ActionBase = { export type ActionBase = {
@ -119,7 +120,11 @@ export type AssertVisibleAction = ActionBase & {
selector: string, 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; export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction;
// Signals. // 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('Java')!.text).toContain(`assertThat(page.getByRole(AriaRole.TEXTBOX)).isVisible()`);
expect.soft(sources1.get('C#')!.text).toContain(`await Expect(page.GetByRole(AriaRole.Textbox)).ToBeVisibleAsync()`); 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())`);
});
}); });