diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index c338d3b8f0164..cd148f74aaa44 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -547,12 +547,7 @@ class TextAssertionTool implements RecorderTool { onClick(event: MouseEvent) { consumeEvent(event); if (this._kind === 'value') { - const action = this._generateAction(); - if (action) { - this._recorder.delegate.recordAction?.(action); - this._recorder.delegate.setMode?.('recording'); - this._recorder.overlay?.flashToolSucceeded('assertingValue'); - } + this._commitAssertValue(); } else { if (!this._dialogElement) this._showDialog(); @@ -565,6 +560,15 @@ class TextAssertionTool implements RecorderTool { event.preventDefault(); } + onPointerUp(event: PointerEvent) { + const target = this._hoverHighlight?.elements[0]; + if (this._kind === 'value' && target && target.nodeName === 'INPUT' && (target as HTMLInputElement).disabled) { + // Click on a disabled input does not produce a "click" event, but we still want + // to assert the value. + this._commitAssertValue(); + } + } + onMouseMove(event: MouseEvent) { if (this._dialogElement) return; @@ -719,6 +723,17 @@ class TextAssertionTool implements RecorderTool { this._recorder.document.removeEventListener('keydown', this._keyboardListener!); this._dialogElement = null; } + + private _commitAssertValue() { + if (this._kind !== 'value') + return; + const action = this._generateAction(); + if (!action) + return; + this._recorder.delegate.recordAction?.(action); + this._recorder.delegate.setMode?.('recording'); + this._recorder.overlay?.flashToolSucceeded('assertingValue'); + } } class Overlay { @@ -1138,8 +1153,10 @@ export class Recorder { } private _ignoreOverlayEvent(event: Event) { - const target = event.composedPath()[0] as Element; - return target.nodeName.toLowerCase() === 'x-pw-glass'; + return event.composedPath().some(e => { + const nodeName = (e as Element).nodeName || ''; + return nodeName.toLowerCase() === 'x-pw-glass'; + }); } deepEventTarget(event: Event): HTMLElement { diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/recorder/csharp.ts index 1dcc6a331adc6..46fadc244aa98 100644 --- a/packages/playwright-core/src/server/recorder/csharp.ts +++ b/packages/playwright-core/src/server/recorder/csharp.ts @@ -161,7 +161,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { case 'assertVisible': return `await Expect(${subject}.${this._asLocator(action.selector)}).ToBeVisibleAsync();`; case 'assertValue': { - const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmpty()`; + const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmptyAsync()`; return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; } } diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 77d91bb0e2233..96a6295f13a26 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -558,4 +558,94 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`); 'click', ]); }); + + test('should assert value', async ({ openRecorder }) => { + const recorder = await openRecorder(); + + await recorder.setContentAndWait(` + + + + + `); + + await recorder.page.click('x-pw-tool-item.value'); + await recorder.hoverOverElement('#first'); + const [sources1] = await Promise.all([ + recorder.waitForOutput('JavaScript', '#first'), + recorder.trustedClick(), + ]); + expect.soft(sources1.get('JavaScript')!.text).toContain(`await expect(page.locator('#first')).toHaveValue('foo')`); + expect.soft(sources1.get('Python')!.text).toContain(`expect(page.locator("#first")).to_have_value("foo")`); + expect.soft(sources1.get('Python Async')!.text).toContain(`await expect(page.locator("#first")).to_have_value("foo")`); + expect.soft(sources1.get('Java')!.text).toContain(`assertThat(page.locator("#first")).hasValue("foo")`); + expect.soft(sources1.get('C#')!.text).toContain(`await Expect(page.Locator("#first")).ToHaveValueAsync("foo")`); + + await recorder.page.click('x-pw-tool-item.value'); + await recorder.hoverOverElement('#third'); + const [sources3] = await Promise.all([ + recorder.waitForOutput('JavaScript', '#third'), + recorder.trustedClick(), + ]); + expect.soft(sources3.get('JavaScript')!.text).toContain(`await expect(page.locator('#third')).toBeEmpty()`); + expect.soft(sources3.get('Python')!.text).toContain(`expect(page.locator("#third")).to_be_empty()`); + expect.soft(sources3.get('Python Async')!.text).toContain(`await expect(page.locator("#third")).to_be_empty()`); + expect.soft(sources3.get('Java')!.text).toContain(`assertThat(page.locator("#third")).isEmpty()`); + expect.soft(sources3.get('C#')!.text).toContain(`await Expect(page.Locator("#third")).ToBeEmptyAsync()`); + + await recorder.page.click('x-pw-tool-item.value'); + await recorder.hoverOverElement('#fourth'); + const [sources4] = await Promise.all([ + recorder.waitForOutput('JavaScript', '#fourth'), + recorder.trustedClick(), + ]); + expect.soft(sources4.get('JavaScript')!.text).toContain(`await expect(page.locator('#fourth')).toBeChecked()`); + expect.soft(sources4.get('Python')!.text).toContain(`expect(page.locator("#fourth")).to_be_checked()`); + expect.soft(sources4.get('Python Async')!.text).toContain(`await expect(page.locator("#fourth")).to_be_checked()`); + expect.soft(sources4.get('Java')!.text).toContain(`assertThat(page.locator("#fourth")).isChecked()`); + expect.soft(sources4.get('C#')!.text).toContain(`await Expect(page.Locator("#fourth")).ToBeCheckedAsync()`); + }); + + test('should assert value on disabled input', async ({ openRecorder, browserName }) => { + test.fixme(browserName === 'firefox', 'pointerup event is not dispatched on a disabled input'); + + const recorder = await openRecorder(); + + await recorder.setContentAndWait(` + + + + + `); + + await recorder.page.click('x-pw-tool-item.value'); + await recorder.hoverOverElement('#second'); + const [sources2] = await Promise.all([ + recorder.waitForOutput('JavaScript', '#second'), + recorder.trustedClick(), + ]); + expect.soft(sources2.get('JavaScript')!.text).toContain(`await expect(page.locator('#second')).toHaveValue('bar')`); + expect.soft(sources2.get('Python')!.text).toContain(`expect(page.locator("#second")).to_have_value("bar")`); + expect.soft(sources2.get('Python Async')!.text).toContain(`await expect(page.locator("#second")).to_have_value("bar")`); + expect.soft(sources2.get('Java')!.text).toContain(`assertThat(page.locator("#second")).hasValue("bar")`); + expect.soft(sources2.get('C#')!.text).toContain(`await Expect(page.Locator("#second")).ToHaveValueAsync("bar")`); + }); + + test('should assert visibility', async ({ openRecorder }) => { + const recorder = await openRecorder(); + + await recorder.setContentAndWait(``); + + await recorder.page.click('x-pw-tool-item.visibility'); + await recorder.hoverOverElement('input'); + const [sources1] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'textbox'), + recorder.trustedClick(), + ]); + expect.soft(sources1.get('JavaScript')!.text).toContain(`await expect(page.getByRole('textbox')).toBeVisible()`); + expect.soft(sources1.get('Python')!.text).toContain(`expect(page.get_by_role("textbox")).to_be_visible()`); + expect.soft(sources1.get('Python Async')!.text).toContain(`await expect(page.get_by_role("textbox")).to_be_visible()`); + 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()`); + }); });