diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 9be88a9f13..192f647472 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -225,6 +225,10 @@ class RecordActionTool implements RecorderTool { } onClick(event: MouseEvent) { + // in webkit, sliding a range element may trigger a click event with a different target if the mouse is released outside the element bounding box. + // So we check the hovered element instead, and if it is a range input, we skip click handling + if (isRangeInput(this._hoveredElement)) + return; if (this._shouldIgnoreMouseEvent(event)) return; if (this._actionInProgress(event)) @@ -317,6 +321,17 @@ class RecordActionTool implements RecorderTool { return; } + if (isRangeInput(target)) { + this._recorder.delegate.recordAction?.({ + name: 'fill', + // must use hoveredModel instead of activeModel for it to work in webkit + selector: this._hoveredModel!.selector, + signals: [], + text: target.value, + }); + return; + } + if (['INPUT', 'TEXTAREA'].includes(target.nodeName) || target.isContentEditable) { if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes((target as HTMLInputElement).type.toLowerCase())) { // Checkbox is handled in click, we can't let input trigger on checkbox - that would mean we dispatched click events while recording. @@ -414,7 +429,7 @@ class RecordActionTool implements RecorderTool { const nodeName = target.nodeName; if (nodeName === 'SELECT' || nodeName === 'OPTION') return true; - if (nodeName === 'INPUT' && ['date'].includes((target as HTMLInputElement).type)) + if (nodeName === 'INPUT' && ['date', 'range'].includes((target as HTMLInputElement).type)) return true; return false; } @@ -1226,6 +1241,13 @@ function asCheckbox(node: Node | null): HTMLInputElement | null { return ['checkbox', 'radio'].includes(inputElement.type) ? inputElement : null; } +function isRangeInput(node: Node | null): node is HTMLInputElement { + if (!node || node.nodeName !== 'INPUT') + return false; + const inputElement = node as HTMLInputElement; + return inputElement.type.toLowerCase() === 'range'; +} + function addEventListener(target: EventTarget, eventName: string, listener: EventListener, useCapture?: boolean): () => void { target.addEventListener(eventName, listener, useCapture); const remove = () => { diff --git a/tests/library/inspector/cli-codegen-1.spec.ts b/tests/library/inspector/cli-codegen-1.spec.ts index 3688bb2053..a0c5596eea 100644 --- a/tests/library/inspector/cli-codegen-1.spec.ts +++ b/tests/library/inspector/cli-codegen-1.spec.ts @@ -746,4 +746,75 @@ await page.GetByText("Click me").ClickAsync(new LocatorClickOptions Button = MouseButton.Middle, });`); }); + + test('should record slider', async ({ page, openRecorder }) => { + const recorder = await openRecorder(); + + await recorder.setContentAndWait(``); + + const dragSlider = async () => { + const { x, y, width, height } = await page.locator('input').boundingBox(); + await page.mouse.move(x + width / 2, y + height / 2); + await page.mouse.down(); + await page.mouse.move(x + width, y + height / 2); + await page.mouse.up(); + }; + + const [sources] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'fill'), + dragSlider(), + ]); + + await expect(page.locator('input')).toHaveValue('10'); + + expect(sources.get('JavaScript')!.text).not.toContain(` + await page.getByRole('slider').click();`); + + expect(sources.get('JavaScript')!.text).toContain(` + await page.getByRole('slider').fill('10');`); + + expect.soft(sources.get('Python')!.text).toContain(` + page.get_by_role("slider").fill("10")`); + + expect.soft(sources.get('Python Async')!.text).toContain(` + await page.get_by_role("slider").fill("10")`); + + expect.soft(sources.get('Java')!.text).toContain(` + page.getByRole(AriaRole.SLIDER).fill("10")`); + + expect.soft(sources.get('C#')!.text).toContain(` +await page.GetByRole(AriaRole.Slider).FillAsync("10");`); + }); + + test('should click button with nested div', async ({ page, openRecorder }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29067' }); + + const recorder = await openRecorder(); + + await recorder.setContentAndWait(``); + + // we hover the nested div, but it must record the button + const locator = await recorder.hoverOverElement('div'); + expect(locator).toBe(`getByRole('button', { name: 'Submit' })`); + + const [sources] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'Submit'), + recorder.trustedClick(), + ]); + + expect.soft(sources.get('JavaScript')!.text).toContain(` + await page.getByRole('button', { name: 'Submit' }).click();`); + + expect.soft(sources.get('Python')!.text).toContain(` + page.get_by_role("button", name="Submit").click()`); + + expect.soft(sources.get('Python Async')!.text).toContain(` + await page.get_by_role("button", name="Submit").click()`); + + expect.soft(sources.get('Java')!.text).toContain(` + page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click()`); + + expect.soft(sources.get('C#')!.text).toContain(` +await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`); + }); });