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();`);
+ });
});