diff --git a/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts-cache.json index 6a8123e86ac7a..1318880339e58 100644 --- a/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/completing-todos/should-complete-multiple-todos.spec.ts-cache.json @@ -38,7 +38,6 @@ { "method": "click", "selector": "internal:role=listitem >> internal:has-text=\"Buy milk\"i >> internal:label=\"Toggle Todo\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Buy milk' }).getByLabel('Toggle Todo').click();" } ] @@ -48,7 +47,6 @@ { "method": "click", "selector": "internal:role=listitem >> internal:has-text=\"Finish report\"i >> internal:label=\"Toggle Todo\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Finish report' }).getByLabel('Toggle Todo').click();" } ] @@ -104,9 +102,8 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:text=\"0 items left\"i", - "isNot": true, - "code": "await expect(page.getByText('0 items left')).not.toBeVisible();" + "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", + "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" } ] }, diff --git a/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts-cache.json index edb4f906cf59b..4263ae994422f 100644 --- a/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/completing-todos/should-complete-single-todo.spec.ts-cache.json @@ -15,7 +15,6 @@ { "method": "click", "selector": "internal:role=checkbox[name=\"Toggle Todo\"i]", - "options": {}, "code": "await page.getByRole('checkbox', { name: 'Toggle Todo' }).click();" } ] @@ -62,9 +61,8 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:role=list[name=\"Todo List\"i]", - "isNot": true, - "code": "await expect(page.getByRole('list', { name: 'Todo List' })).not.toBeVisible();" + "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", + "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" } ] }, @@ -72,8 +70,8 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:text=\"Buy groceries\"i", - "code": "await expect(page.getByText('Buy groceries')).toBeVisible();" + "selector": "internal:role=generic[name=\"Buy groceries\"i]", + "code": "await expect(page.getByRole('generic', { name: 'Buy groceries' })).toBeVisible();" } ] } diff --git a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts-cache.json index 14a93f8484935..3e45999276634 100644 --- a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-complete.spec.ts-cache.json @@ -58,7 +58,6 @@ { "method": "click", "selector": "internal:role=checkbox[name=\"❯Mark all as complete\"i]", - "options": {}, "code": "await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click();" } ] @@ -91,6 +90,12 @@ ] }, "The page loads with an empty todo list": { - "actions": [] + "actions": [ + { + "method": "expectVisible", + "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", + "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" + } + ] } } \ No newline at end of file diff --git a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts-cache.json index 4023b4bed369e..876cff3f56034 100644 --- a/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/completing-todos/should-toggle-all-todos-incomplete.spec.ts-cache.json @@ -23,10 +23,10 @@ "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Task 3');\nawait page.keyboard.press('Enter');" }, { - "method": "click", + "method": "setChecked", "selector": "internal:role=checkbox[name=\"❯Mark all as complete\"i]", - "options": {}, - "code": "await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click();" + "checked": true, + "code": "await page.getByRole('checkbox', { name: '❯Mark all as complete' }).check();" } ] }, @@ -38,15 +38,32 @@ "type": "checkbox", "value": "false", "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: false });" + }, + { + "method": "expectValue", + "selector": "internal:role=listitem >> internal:has-text=\"Task 2\"i >> internal:label=\"Toggle Todo\"i", + "type": "checkbox", + "value": "false", + "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 2' }).getByLabel('Toggle Todo')).toBeChecked({ checked: false });" + }, + { + "method": "expectValue", + "selector": "internal:role=listitem >> internal:has-text=\"Task 3\"i >> internal:label=\"Toggle Todo\"i", + "type": "checkbox", + "value": "false", + "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 3' }).getByLabel('Toggle Todo')).toBeChecked({ checked: false });" } ] }, "All todos are marked as active": { "actions": [ { - "method": "expectVisible", - "selector": "internal:text=\"3 items left\"i", - "code": "await expect(page.getByText('3 items left')).toBeVisible();" + "method": "expectValue", + "selector": "internal:role=listitem >> internal:has-text=\"Task 1\"i >> internal:label=\"Toggle Todo\"i", + "type": "checkbox", + "value": "false", + "isNot": false, + "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: false });" } ] }, @@ -58,6 +75,20 @@ "type": "checkbox", "value": "true", "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" + }, + { + "method": "expectValue", + "selector": "internal:role=listitem >> internal:has-text=\"Task 2\"i >> internal:label=\"Toggle Todo\"i", + "type": "checkbox", + "value": "true", + "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 2' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" + }, + { + "method": "expectValue", + "selector": "internal:role=listitem >> internal:has-text=\"Task 3\"i >> internal:label=\"Toggle Todo\"i", + "type": "checkbox", + "value": "true", + "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 3' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" } ] }, @@ -66,7 +97,6 @@ { "method": "click", "selector": "internal:role=checkbox[name=\"❯Mark all as complete\"i]", - "options": {}, "code": "await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click();" } ] diff --git a/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts-cache.json b/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts-cache.json index 44d36487c495d..bad79f7e96f30 100644 --- a/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/completing-todos/should-uncomplete-completed-todo.spec.ts-cache.json @@ -11,7 +11,6 @@ { "method": "click", "selector": "internal:role=checkbox[name=\"Toggle Todo\"i]", - "options": {}, "code": "await page.getByRole('checkbox', { name: 'Toggle Todo' }).click();" } ] @@ -21,7 +20,6 @@ { "method": "click", "selector": "internal:role=checkbox[name=\"Toggle Todo\"i]", - "options": {}, "code": "await page.getByRole('checkbox', { name: 'Toggle Todo' }).click();" } ] @@ -39,8 +37,8 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:text=\"item left\"i", - "code": "await expect(page.getByText('item left')).toBeVisible();" + "selector": "internal:text=\"1 item left\"i", + "code": "await expect(page.getByText('1 item left')).toBeVisible();" } ] }, @@ -69,8 +67,8 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" + "selector": "internal:text=\"What needs to be done?\"i", + "code": "await expect(page.getByText('What needs to be done?')).toBeVisible();" } ] }, diff --git a/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts-cache.json b/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts-cache.json index bd4c5060cbcbb..687b9196bb874 100644 --- a/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/deleting-todos/should-clear-all-completed-todos.spec.ts-cache.json @@ -47,7 +47,6 @@ { "method": "click", "selector": "internal:role=button[name=\"Clear completed\"i]", - "options": {}, "code": "await page.getByRole('button', { name: 'Clear completed' }).click();" } ] @@ -66,13 +65,11 @@ { "method": "click", "selector": "internal:role=listitem >> internal:has-text=\"Task 1\"i >> internal:label=\"Toggle Todo\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo').click();" }, { "method": "click", "selector": "internal:role=listitem >> internal:has-text=\"Task 3\"i >> internal:label=\"Toggle Todo\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Task 3' }).getByLabel('Toggle Todo').click();" } ] @@ -117,9 +114,18 @@ "Two todos are marked as complete": { "actions": [ { - "method": "expectVisible", - "selector": "internal:text=\"item left\"i", - "code": "await expect(page.getByText('item left')).toBeVisible();" + "method": "expectValue", + "selector": "internal:role=listitem >> internal:has-text=\"Task 1\"i >> internal:label=\"Toggle Todo\"i", + "type": "checkbox", + "value": "true", + "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" + }, + { + "method": "expectValue", + "selector": "internal:role=listitem >> internal:has-text=\"Task 3\"i >> internal:label=\"Toggle Todo\"i", + "type": "checkbox", + "value": "true", + "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Task 3' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" } ] } diff --git a/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts-cache.json b/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts-cache.json index dd8e0aa3bf97e..37f5a8b620960 100644 --- a/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/deleting-todos/should-delete-single-todo.spec.ts-cache.json @@ -24,7 +24,6 @@ { "method": "click", "selector": "internal:role=button[name=\"Delete\"i]", - "options": {}, "code": "await page.getByRole('button', { name: 'Delete' }).click();" } ] @@ -33,8 +32,8 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:text=\"1 item left\"i", - "code": "await expect(page.getByText('1 item left')).toBeVisible();" + "selector": "internal:text=\"item left\"i", + "code": "await expect(page.getByText('item left')).toBeVisible();" } ] }, @@ -42,9 +41,8 @@ "actions": [ { "method": "hover", - "selector": "internal:testid=[data-testid=\"todo-item\"s] >> div", - "options": {}, - "code": "await page.getByTestId('todo-item').locator('div').hover();" + "selector": "internal:testid=[data-testid=\"todo-title\"s]", + "code": "await page.getByTestId('todo-title').hover();" } ] }, @@ -59,7 +57,14 @@ ] }, "The list is empty": { - "actions": [] + "actions": [ + { + "method": "expectVisible", + "selector": "internal:text=\"Task to delete\"i", + "isNot": true, + "code": "await expect(page.getByText('Task to delete')).not.toBeVisible();" + } + ] }, "The page loads with an empty todo list": { "actions": [ @@ -83,8 +88,9 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" + "selector": "internal:text=\"Task to delete\"i", + "isNot": true, + "code": "await expect(page.getByText('Task to delete')).not.toBeVisible();" } ] } diff --git a/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts-cache.json b/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts-cache.json index 2b54ed47e3257..ae699824bb140 100644 --- a/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/deleting-todos/should-delete-specific-todo-from-multiple.spec.ts-cache.json @@ -75,13 +75,11 @@ { "method": "hover", "selector": "internal:role=listitem >> internal:has-text=\"Task 2\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Task 2' }).hover();" }, { "method": "click", "selector": "internal:role=button[name=\"Delete\"i]", - "options": {}, "code": "await page.getByRole('button', { name: 'Delete' }).click();" } ] @@ -89,10 +87,11 @@ "The page loads with an empty todo list": { "actions": [ { - "method": "expectVisible", + "method": "expectValue", "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "isNot": false, - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" + "type": "textbox", + "value": "", + "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue('');" } ] } diff --git a/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts-cache.json index 834cfb604b052..e2e3a57affc36 100644 --- a/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/editing-todos/should-cancel-edit-on-escape.spec.ts-cache.json @@ -12,17 +12,6 @@ }, "Change the text to 'Modified text' but press Escape instead of Enter": { "actions": [ - { - "method": "click", - "selector": "internal:role=textbox[name=\"Edit\"i]", - "options": {}, - "code": "await page.getByRole('textbox', { name: 'Edit' }).click();" - }, - { - "method": "pressKey", - "key": "Control+a", - "code": "await page.keyboard.press('Control+a');" - }, { "method": "fill", "selector": "internal:role=textbox[name=\"Edit\"i]", @@ -50,9 +39,7 @@ { "method": "click", "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "options": { - "clickCount": 2 - }, + "clickCount": 2, "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" } ] @@ -61,8 +48,8 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:text=\"Original text\"i", - "code": "await expect(page.getByText('Original text')).toBeVisible();" + "selector": "internal:role=generic[name=\"Original text\"i]", + "code": "await expect(page.getByRole('generic', { name: 'Original text' })).toBeVisible();" } ] }, diff --git a/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts-cache.json index e960139b1b6ac..b1a568c528aeb 100644 --- a/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/editing-todos/should-delete-todo-when-edited-to-empty.spec.ts-cache.json @@ -45,9 +45,7 @@ { "method": "click", "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "options": { - "clickCount": 2 - }, + "clickCount": 2, "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" } ] @@ -64,21 +62,18 @@ "The list is empty": { "actions": [ { - "method": "expectVisible", - "selector": "internal:role=listitem[name=\"Temporary task\"i]", - "isNot": true, - "code": "await expect(page.getByRole('listitem', { name: 'Temporary task' })).not.toBeVisible();" + "method": "expectAria", + "template": "- list:\n", + "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n\n`);" } ] }, "The page loads with an empty todo list": { "actions": [ { - "method": "expectValue", + "method": "expectVisible", "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "type": "textbox", - "value": "", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue('');" + "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" } ] }, @@ -95,9 +90,9 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:text=\"Temporary task\"i", - "isNot": true, - "code": "await expect(page.getByText('Temporary task')).not.toBeVisible();" + "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", + "isNot": false, + "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" } ] } diff --git a/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts-cache.json index 35428eca76dca..99e8380ae71a0 100644 --- a/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/editing-todos/should-edit-todo-by-double-clicking.spec.ts-cache.json @@ -37,9 +37,7 @@ { "method": "click", "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "options": { - "clickCount": 2 - }, + "clickCount": 2, "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" } ] @@ -48,9 +46,8 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:role=textbox[name=\"Buy organic milk\"i]", - "isNot": true, - "code": "await expect(page.getByRole('textbox', { name: 'Buy organic milk' })).not.toBeVisible();" + "selector": "internal:text=\"Buy organic milk\"i", + "code": "await expect(page.getByText('Buy organic milk')).toBeVisible();" } ] }, @@ -75,9 +72,9 @@ "The todo appears in the list": { "actions": [ { - "method": "expectAria", - "template": "- list:\n - listitem: Buy milk", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy milk\n`);" + "method": "expectVisible", + "selector": "internal:text=\"Buy milk\"i", + "code": "await expect(page.getByText('Buy milk')).toBeVisible();" } ] }, diff --git a/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts-cache.json index 834c6523ff127..9387c52cb1998 100644 --- a/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/editing-todos/should-save-edit-on-blur.spec.ts-cache.json @@ -21,7 +21,6 @@ { "method": "click", "selector": "internal:role=heading[name=\"todos\"i]", - "options": {}, "code": "await page.getByRole('heading', { name: 'todos' }).click();" } ] @@ -31,9 +30,7 @@ { "method": "click", "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "options": { - "clickCount": 2 - }, + "clickCount": 2, "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" } ] @@ -42,9 +39,9 @@ "actions": [ { "method": "expectVisible", - "selector": "internal:role=textbox[name=\"Schedule dentist appointment\"i]", - "isNot": true, - "code": "await expect(page.getByRole('textbox', { name: 'Schedule dentist appointment' })).not.toBeVisible();" + "selector": "internal:text=\"Schedule dentist appointment\"i", + "isNot": false, + "code": "await expect(page.getByText('Schedule dentist appointment')).toBeVisible();" } ] }, diff --git a/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts-cache.json b/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts-cache.json index 7a46e97a347d0..aab34f22d570b 100644 --- a/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/editing-todos/should-trim-whitespace-when-editing.spec.ts-cache.json @@ -15,9 +15,7 @@ { "method": "click", "selector": "internal:testid=[data-testid=\"todo-title\"s]", - "options": { - "clickCount": 2 - }, + "clickCount": 2, "code": "await page.getByTestId('todo-title').click({\nclickCount: 2\n});" }, { @@ -30,7 +28,13 @@ ] }, "Edit textbox shows the text with spaces": { - "actions": [] + "actions": [ + { + "method": "expectVisible", + "selector": "internal:text=\" Edited task \"i", + "code": "await expect(page.getByText(' Edited task ')).toBeVisible();" + } + ] }, "Press Enter to save": { "actions": [ @@ -44,9 +48,11 @@ "The page loads with an empty todo list": { "actions": [ { - "method": "expectVisible", + "method": "expectValue", "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible();" + "type": "textbox", + "value": "", + "code": "await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue('');" } ] }, diff --git a/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts-cache.json b/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts-cache.json index 585d182e079dd..ef978f6928c07 100644 --- a/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/filtering-todos/should-filter-active-todos.spec.ts-cache.json @@ -48,7 +48,6 @@ { "method": "click", "selector": "internal:role=link[name=\"Active\"i]", - "options": {}, "code": "await page.getByRole('link', { name: 'Active' }).click();" } ] @@ -67,7 +66,6 @@ { "method": "click", "selector": "internal:role=listitem >> internal:has-text=\"Will complete\"i >> internal:label=\"Toggle Todo\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Will complete' }).getByLabel('Toggle Todo').click();" } ] diff --git a/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts-cache.json b/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts-cache.json index d01527705b1ab..f9f5e770a3f42 100644 --- a/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/filtering-todos/should-filter-completed-todos.spec.ts-cache.json @@ -48,7 +48,6 @@ { "method": "click", "selector": "internal:role=link[name=\"Completed\"i]", - "options": {}, "code": "await page.getByRole('link', { name: 'Completed' }).click();" } ] @@ -58,13 +57,11 @@ { "method": "click", "selector": "internal:role=listitem >> internal:has-text=\"Completed 1\"i >> internal:label=\"Toggle Todo\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Completed 1' }).getByLabel('Toggle Todo').click();" }, { "method": "click", "selector": "internal:role=listitem >> internal:has-text=\"Completed 2\"i >> internal:label=\"Toggle Todo\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Completed 2' }).getByLabel('Toggle Todo').click();" } ] @@ -108,18 +105,9 @@ "Two todos are marked as complete": { "actions": [ { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Completed 1\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Completed 1' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" - }, - { - "method": "expectValue", - "selector": "internal:role=listitem >> internal:has-text=\"Completed 2\"i >> internal:label=\"Toggle Todo\"i", - "type": "checkbox", - "value": "true", - "code": "await expect(page.getByRole('listitem').filter({ hasText: 'Completed 2' }).getByLabel('Toggle Todo')).toBeChecked({ checked: true });" + "method": "expectVisible", + "selector": "internal:text=\"1\"i", + "code": "await expect(page.getByText('1')).toBeVisible();" } ] } diff --git a/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts-cache.json b/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts-cache.json index d8c34971414d3..1dc14c062088c 100644 --- a/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/filtering-todos/should-show-all-todos-with-all-filter.spec.ts-cache.json @@ -18,14 +18,13 @@ { "method": "fill", "selector": "internal:role=textbox[name=\"What needs to be done?\"i]", - "text": "Finish homework", + "text": "Read a book", "submit": true, - "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Finish homework');\nawait page.keyboard.press('Enter');" + "code": "await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Read a book');\nawait page.keyboard.press('Enter');" }, { "method": "click", "selector": "internal:role=listitem >> internal:has-text=\"Buy groceries\"i >> internal:label=\"Toggle Todo\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Buy groceries' }).getByLabel('Toggle Todo').click();" } ] @@ -34,8 +33,8 @@ "actions": [ { "method": "expectAria", - "template": "- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Finish homework", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Finish homework\n`);" + "template": "- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Read a book", + "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Read a book\n`);" } ] }, @@ -44,7 +43,6 @@ { "method": "click", "selector": "internal:role=link[name=\"All\"i]", - "options": {}, "code": "await page.getByRole('link', { name: 'All' }).click();" } ] @@ -54,7 +52,6 @@ { "method": "click", "selector": "internal:role=link[name=\"Active\"i]", - "options": {}, "code": "await page.getByRole('link', { name: 'Active' }).click();" } ] @@ -63,8 +60,8 @@ "actions": [ { "method": "expectAria", - "template": "- list:\n - listitem: Walk the dog\n - listitem: Finish homework", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Walk the dog\n - listitem: Finish homework\n`);" + "template": "- list:\n - listitem: Walk the dog\n - listitem: Read a book", + "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Walk the dog\n - listitem: Read a book\n`);" } ] }, @@ -99,8 +96,8 @@ "actions": [ { "method": "expectAria", - "template": "- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Finish homework", - "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Finish homework\n`);" + "template": "- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Read a book", + "code": "await expect(page.locator('body')).toMatchAria(`\n- list:\n - listitem: Buy groceries\n - listitem: Walk the dog\n - listitem: Read a book\n`);" } ] } diff --git a/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts-cache.json b/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts-cache.json index 62ead87f9f306..0dc39f0bf9c60 100644 --- a/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts-cache.json +++ b/examples/todomvc/tests/perform/persistence/should-persist-todos-after-page-reload.spec.ts-cache.json @@ -69,7 +69,6 @@ { "method": "click", "selector": "internal:role=listitem >> internal:has-text=\"Persistent 2\"i >> internal:label=\"Toggle Todo\"i", - "options": {}, "code": "await page.getByRole('listitem').filter({ hasText: 'Persistent 2' }).getByLabel('Toggle Todo').click();" } ] diff --git a/packages/playwright-core/src/server/agent/DEPS.list b/packages/playwright-core/src/server/agent/DEPS.list index e64ce4085af45..f0fade51be83d 100644 --- a/packages/playwright-core/src/server/agent/DEPS.list +++ b/packages/playwright-core/src/server/agent/DEPS.list @@ -1,8 +1,10 @@ [*] ../browserContext.ts +../errors.ts ../page.ts ../progress.ts ../utils/expectUtils.ts +../utils/crypto.ts ../../mcpBundle.ts ../../protocol/ ../../utilsBundle.ts diff --git a/packages/playwright-core/src/server/agent/actionRunner.ts b/packages/playwright-core/src/server/agent/actionRunner.ts index 106f121520090..af2994edac467 100644 --- a/packages/playwright-core/src/server/agent/actionRunner.ts +++ b/packages/playwright-core/src/server/agent/actionRunner.ts @@ -16,15 +16,19 @@ import { serializeExpectedTextValues } from '../utils/expectUtils'; import { monotonicTime } from '../../utils/isomorphic/time'; +import { createGuid } from '../utils/crypto'; import { parseAriaSnapshotUnsafe } from '../../utils/isomorphic/ariaSnapshot'; import { ProgressController } from '../progress'; import { yaml } from '../../utilsBundle'; +import { serializeError } from '../errors'; import type * as actions from './actions'; import type { Page } from '../page'; import type { Progress } from '../progress'; import type { NameValue } from '@protocol/channels'; -import type { ExpectResult } from '../frames'; +import type { ExpectResult, Frame } from '../frames'; +import type { CallMetadata } from '../instrumentation'; +import type * as channels from '@protocol/channels'; export async function runAction(parentProgress: Progress, mode: 'generate' | 'run', page: Page, action: actions.Action, secrets: NameValue[]) { const timeout = mode === 'generate' ? generateActionTimeout(action) : performActionTimeout(action); @@ -33,21 +37,44 @@ export async function runAction(parentProgress: Progress, mode: 'generate' | 'ru const minDeadline = parentProgress.deadline ? Math.min(parentProgress.deadline, deadline) : deadline; const pc = new ProgressController(); return await pc.run(async progress => { - return await innerRunAction(progress, page, action, secrets); + const frame = page.mainFrame(); + const callMetadata = callMetadataForAction(frame, action); + await frame.instrumentation.onBeforeCall(frame, callMetadata, parentProgress.metadata.id); + + let error: Error | undefined; + const result = await innerRunAction(progress, page, action, secrets).catch(e => error = e); + callMetadata.endTime = monotonicTime(); + callMetadata.error = error ? serializeError(error) : undefined; + callMetadata.result = error ? undefined : result; + await frame.instrumentation.onAfterCall(frame, callMetadata); + if (error) + throw error; + return result; }, minDeadline - mt); } async function innerRunAction(progress: Progress, page: Page, action: actions.Action, secrets: NameValue[]) { const frame = page.mainFrame(); switch (action.method) { + case 'navigate': + await frame.goto(progress, action.url); + break; case 'click': - await frame.click(progress, action.selector, { ...action.options, ...strictTrue }); + await frame.click(progress, action.selector, { + button: action.button, + clickCount: action.clickCount, + modifiers: action.modifiers, + ...strictTrue + }); break; case 'drag': await frame.dragAndDrop(progress, action.sourceSelector, action.targetSelector, { ...strictTrue }); break; case 'hover': - await frame.hover(progress, action.selector, { ...action.options, ...strictTrue }); + await frame.hover(progress, action.selector, { + modifiers: action.modifiers, + ...strictTrue + }); break; case 'selectOption': await frame.selectOption(progress, action.selector, [], action.labels.map(a => ({ label: a })), { ...strictTrue }); @@ -77,7 +104,7 @@ async function innerRunAction(progress: Progress, page: Page, action: actions.Ac break; case 'expectVisible': { const result = await frame.expect(progress, action.selector, { expression: 'to.be.visible', isNot: !!action.isNot }); - if (result.matches === !!action.isNot) + if (!result.matches === !action.isNot) throw new Error(result.errorMessage); break; } @@ -92,14 +119,14 @@ async function innerRunAction(progress: Progress, page: Page, action: actions.Ac } else { throw new Error(`Unsupported element type: ${action.type}`); } - if (result.matches === !!action.isNot) + if (!result.matches === !action.isNot) throw new Error(result.errorMessage); break; } case 'expectAria': { const expectedValue = parseAriaSnapshotUnsafe(yaml, action.template); const result = await frame.expect(progress, 'body', { expression: 'to.match.aria', expectedValue, isNot: !!action.isNot }); - if (result.matches === !!action.isNot) + if (!result.matches === !action.isNot) throw new Error(result.errorMessage); break; } @@ -108,6 +135,8 @@ async function innerRunAction(progress: Progress, page: Page, action: actions.Ac export function generateActionTimeout(action: actions.Action): number { switch (action.method) { + case 'navigate': + return 10000; case 'click': case 'drag': case 'hover': @@ -126,6 +155,7 @@ export function generateActionTimeout(action: actions.Action): number { export function performActionTimeout(action: actions.Action): number { switch (action.method) { + case 'navigate': case 'click': case 'drag': case 'hover': @@ -142,4 +172,157 @@ export function performActionTimeout(action: actions.Action): number { } } +export function traceParamsForAction(action: actions.Action): { method: string, title: string, params: any } { + const timeout = generateActionTimeout(action); + switch (action.method) { + case 'navigate': { + const params: channels.FrameGotoParams = { + url: action.url, + timeout, + }; + return { method: 'goto', title: 'Navigate', params }; + } + case 'click': { + const params: channels.FrameClickParams = { + selector: action.selector, + strict: true, + modifiers: action.modifiers, + button: action.button, + clickCount: action.clickCount, + timeout, + }; + return { method: 'click', title: 'Click', params }; + } + case 'drag': { + const params: channels.FrameDragAndDropParams = { + source: action.sourceSelector, + target: action.targetSelector, + timeout, + }; + return { method: 'dragAndDrop', title: 'Drag and Drop', params }; + } + case 'hover': { + const params: channels.FrameHoverParams = { + selector: action.selector, + modifiers: action.modifiers, + timeout, + }; + return { method: 'hover', title: 'Hover', params }; + } + case 'pressKey': { + const params: channels.PageKeyboardPressParams = { + key: action.key, + }; + return { method: 'press', title: 'Press', params }; + } + case 'pressSequentially': { + const params: channels.FrameTypeParams = { + selector: action.selector, + text: action.text, + timeout, + }; + return { method: 'type', title: 'Type', params }; + } + case 'fill': { + const params: channels.FrameFillParams = { + selector: action.selector, + strict: true, + value: action.text, + timeout, + }; + return { method: 'fill', title: 'Fill', params }; + } + case 'setChecked': { + if (action.checked) { + const params: channels.FrameCheckParams = { + selector: action.selector, + strict: true, + timeout, + }; + return { method: 'check', title: 'Check', params }; + } else { + const params: channels.FrameUncheckParams = { + selector: action.selector, + strict: true, + timeout, + }; + return { method: 'uncheck', title: 'Uncheck', params }; + } + } + case 'selectOption': { + const params: channels.FrameSelectOptionParams = { + selector: action.selector, + strict: true, + options: action.labels.map(label => ({ label })), + timeout, + }; + return { method: 'selectOption', title: 'Select Option', params }; + } + case 'expectValue': { + if (action.type === 'textbox' || action.type === 'combobox' || action.type === 'slider') { + const expectedText = serializeExpectedTextValues([action.value]); + const params: channels.FrameExpectParams = { + selector: action.selector, + expression: 'to.have.value', + expectedText, + isNot: !!action.isNot, + timeout: kDefaultTimeout, + }; + return { method: 'expect', title: 'Expect Value', params }; + } else if (action.type === 'checkbox' || action.type === 'radio') { + // TODO: provide serialized expected value + const params: channels.FrameExpectParams = { + selector: action.selector, + expression: 'to.be.checked', + isNot: !!action.isNot, + timeout: kDefaultTimeout, + }; + return { method: 'expect', title: 'Expect Checked', params }; + } else { + throw new Error(`Unsupported element type: ${action.type}`); + } + } + case 'expectVisible': { + const params: channels.FrameExpectParams = { + selector: action.selector, + expression: 'to.be.visible', + isNot: !!action.isNot, + timeout: kDefaultTimeout, + }; + return { method: 'expect', title: 'Expect Visible', params }; + } + case 'expectAria': { + // TODO: provide serialized expected value + const params: channels.FrameExpectParams = { + selector: 'body', + expression: 'to.match.snapshot', + expectedText: [], + isNot: !!action.isNot, + timeout: kDefaultTimeout, + }; + return { method: 'expect', title: 'Expect Aria Snapshot', params }; + } + } +} + +function callMetadataForAction(frame: Frame, action: actions.Action): CallMetadata { + const { method, title, params } = traceParamsForAction(action); + + const callMetadata: CallMetadata = { + id: `call@${createGuid()}`, + objectId: frame.guid, + pageId: frame._page.guid, + frameId: frame.guid, + startTime: monotonicTime(), + endTime: 0, + type: 'Frame', + method, + params, + title, + log: [], + }; + return callMetadata; +} + +const kDefaultTimeout = 5000; const strictTrue = { strict: true }; diff --git a/packages/playwright-core/src/server/agent/actions.ts b/packages/playwright-core/src/server/agent/actions.ts index 1eeabbfa1c907..dd058a5afd2b5 100644 --- a/packages/playwright-core/src/server/agent/actions.ts +++ b/packages/playwright-core/src/server/agent/actions.ts @@ -14,12 +14,17 @@ * limitations under the License. */ -import type * as channels from '@protocol/channels'; +export type NavigateAction = { + method: 'navigate'; + url: string; +}; export type ClickAction = { method: 'click'; selector: string; - options: Pick; + button?: 'left' | 'right' | 'middle'; + clickCount?: number; + modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[]; }; export type DragAction = { @@ -31,7 +36,7 @@ export type DragAction = { export type HoverAction = { method: 'hover'; selector: string; - options: Pick; + modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[]; }; export type SelectOptionAction = { @@ -42,6 +47,7 @@ export type SelectOptionAction = { export type PressAction = { method: 'pressKey'; + // Includes modifiers key: string; }; @@ -86,6 +92,7 @@ export type ExpectAria = { }; export type Action = + | NavigateAction | ClickAction | DragAction | HoverAction diff --git a/packages/playwright-core/src/server/agent/codegen.ts b/packages/playwright-core/src/server/agent/codegen.ts index 69df5932f0d8d..7446fde7349e1 100644 --- a/packages/playwright-core/src/server/agent/codegen.ts +++ b/packages/playwright-core/src/server/agent/codegen.ts @@ -22,9 +22,16 @@ import type { Language } from '../../utils/isomorphic/locatorGenerators'; export async function generateCode(sdkLanguage: Language, action: actions.Action) { switch (action.method) { + case 'navigate': { + return `await page.goto(${escapeWithQuotes(action.url)});`; + } case 'click': { const locator = asLocator(sdkLanguage, action.selector); - return `await page.${locator}.click(${formatObjectOrVoid(action.options)});`; + return `await page.${locator}.click(${formatObjectOrVoid({ + button: action.button, + clickCount: action.clickCount, + modifiers: action.modifiers, + })});`; } case 'drag': { const sourceLocator = asLocator(sdkLanguage, action.sourceSelector); @@ -33,7 +40,9 @@ export async function generateCode(sdkLanguage: Language, action: actions.Action } case 'hover': { const locator = asLocator(sdkLanguage, action.selector); - return `await page.${locator}.hover(${formatObjectOrVoid(action.options)});`; + return `await page.${locator}.hover(${formatObjectOrVoid({ + modifiers: action.modifiers, + })});`; } case 'pressKey': { return `await page.keyboard.press(${escapeWithQuotes(action.key, '\'')});`; diff --git a/packages/playwright-core/src/server/agent/context.ts b/packages/playwright-core/src/server/agent/context.ts index ead11d060dc10..2a9c93b606e26 100644 --- a/packages/playwright-core/src/server/agent/context.ts +++ b/packages/playwright-core/src/server/agent/context.ts @@ -24,7 +24,6 @@ import type * as actions from './actions'; import type { Page } from '../page'; import type { Progress } from '../progress'; import type { Language } from '../../utils/isomorphic/locatorGenerators.ts'; -import type { ToolDefinition } from './tool'; import type * as channels from '@protocol/channels'; @@ -47,15 +46,11 @@ export class Context { this.events = events; } - async callTool(progress: Progress, tool: ToolDefinition, params: any) { - return await tool.handle(progress, this, params); - } - async runActionAndWait(progress: Progress, action: actions.Action) { return await this.runActionsAndWait(progress, [action]); } - async runActionsAndWait(progress: Progress, action: actions.Action[]) { + async runActionsAndWait(progress: Progress, action: actions.Action[], options?: { noWait?: boolean }) { const error = await this.waitForCompletion(progress, async () => { for (const a of action) { await runAction(progress, 'generate', this.page, a, this.agentParams?.secrets ?? []); @@ -63,10 +58,14 @@ export class Context { this._actions.push({ ...a, code }); } return undefined; - }).catch((error: Error) => error); + }, options).catch((error: Error) => error); return await this.snapshotResult(progress, error); } + async runActionNoWait(progress: Progress, action: actions.Action) { + return await this.runActionsAndWait(progress, [action], { noWait: true }); + } + actions() { return this._actions.slice(); } @@ -80,7 +79,10 @@ export class Context { this._actions = []; } - async waitForCompletion(progress: Progress, callback: () => Promise): Promise { + async waitForCompletion(progress: Progress, callback: () => Promise, options?: { noWait?: boolean }): Promise { + if (options?.noWait) + return await callback(); + const requests: Request[] = []; const requestListener = (request: Request) => requests.push(request); const disposeListeners = () => { diff --git a/packages/playwright-core/src/server/agent/pageAgent.ts b/packages/playwright-core/src/server/agent/pageAgent.ts index 90532cb1564f2..4b8425c74f4bf 100644 --- a/packages/playwright-core/src/server/agent/pageAgent.ts +++ b/packages/playwright-core/src/server/agent/pageAgent.ts @@ -88,7 +88,7 @@ async function runLoop(progress: Progress, context: Context, toolDefinitions: To }> { const { page } = context; if (!context.agentParams.api || !context.agentParams.model) - throw new Error(`This action requires the API and API key to be set on the page agent. Did you mean to --run-agents=all?`); + throw new Error(`This action requires the API and API key to be set on the page agent. Did you mean to --run-agents=missing?`); if (!context.agentParams.apiKey) throw new Error(`This action requires API key to be set on the page agent.`); diff --git a/packages/playwright-core/src/server/agent/performTools.ts b/packages/playwright-core/src/server/agent/performTools.ts index c93fa5d3cae72..125af978d8a3a 100644 --- a/packages/playwright-core/src/server/agent/performTools.ts +++ b/packages/playwright-core/src/server/agent/performTools.ts @@ -20,6 +20,26 @@ import { defineTool } from './tool'; import type * as actions from './actions'; import type { ToolDefinition } from './tool'; +const navigateSchema = z.object({ + url: z.string().describe('URL to navigate to'), +}); + +const navigate = defineTool({ + schema: { + name: 'browser_navigate', + title: 'Navigate to URL', + description: 'Navigate to a URL', + inputSchema: navigateSchema, + }, + + handle: async (progress, context, params) => { + return await context.runActionNoWait(progress, { + method: 'navigate', + url: params.url, + }); + }, +}); + const snapshot = defineTool({ schema: { name: 'browser_snapshot', @@ -57,11 +77,9 @@ const click = defineTool({ return await context.runActionAndWait(progress, { method: 'click', selector, - options: { - button: params.button, - modifiers: params.modifiers, - clickCount: params.doubleClick ? 2 : undefined, - } + button: params.button, + modifiers: params.modifiers, + clickCount: params.doubleClick ? 2 : undefined, }); }, }); @@ -110,9 +128,7 @@ const hover = defineTool({ return await context.runActionAndWait(progress, { method: 'hover', selector, - options: { - modifiers: params.modifiers, - } + modifiers: params.modifiers, }); }, }); @@ -146,13 +162,14 @@ const pressKey = defineTool({ description: 'Press a key on the keyboard', inputSchema: z.object({ key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'), + modifiers: z.array(z.enum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'])).optional().describe('Modifier keys to press'), }), }, handle: async (progress, context, params) => { return await context.runActionAndWait(progress, { method: 'pressKey', - key: params.key + key: params.modifiers ? [...params.modifiers, params.key].join('+') : params.key, }); }, }); @@ -234,7 +251,30 @@ const fillForm = defineTool({ }, }); +const setCheckedSchema = elementSchema.extend({ + checked: z.boolean().describe('Whether to check the checkbox'), +}); + +const setChecked = defineTool({ + schema: { + name: 'browser_set_checked', + title: 'Set checked', + description: 'Set the checked state of a checkbox', + inputSchema: setCheckedSchema, + }, + + handle: async (progress, context, params) => { + const [selector] = await context.refSelectors(progress, [params]); + return await context.runActionAndWait(progress, { + method: 'setChecked', + selector, + checked: params.checked, + }); + }, +}); + export default [ + navigate, snapshot, click, drag, @@ -243,4 +283,5 @@ export default [ pressKey, type, fillForm, + setChecked, ] as ToolDefinition[]; diff --git a/packages/playwright-core/src/server/agent/tool.ts b/packages/playwright-core/src/server/agent/tool.ts index 0394edb4bf5a8..493826e3c636a 100644 --- a/packages/playwright-core/src/server/agent/tool.ts +++ b/packages/playwright-core/src/server/agent/tool.ts @@ -74,7 +74,7 @@ export function toolsForLoop(progress: Progress, context: Context, toolDefinitio } try { - return await context.callTool(progress, tool, params.arguments); + return await tool.handle(progress, context, params.arguments); } catch (error) { return { content: [{ type: 'text', text: error.message }], diff --git a/packages/playwright-core/src/server/instrumentation.ts b/packages/playwright-core/src/server/instrumentation.ts index 1b675a8746e1d..f468e312ed8f1 100644 --- a/packages/playwright-core/src/server/instrumentation.ts +++ b/packages/playwright-core/src/server/instrumentation.ts @@ -74,7 +74,7 @@ export function createRootSdkObject() { export interface Instrumentation { addListener(listener: InstrumentationListener, context: BrowserContext | APIRequestContext | null): void; removeListener(listener: InstrumentationListener): void; - onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise; + onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata, parentId?: string): Promise; onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise; onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void; onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise; @@ -87,7 +87,7 @@ export interface Instrumentation { } export interface InstrumentationListener { - onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise; + onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata, parentId?: string): Promise; onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata): Promise; onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void; onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise; diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 86a449b76bf9e..6888b688665cd 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -433,9 +433,9 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps return !!this._snapshotter?.started() && shouldCaptureSnapshot(metadata) && !!sdkObject.attribution.page; } - onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) { + onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata, parentId?: string) { // IMPORTANT: no awaits in this method, this._appendTraceEvent must be called synchronously. - const event = createBeforeActionTraceEvent(metadata, this._currentGroupId()); + const event = createBeforeActionTraceEvent(metadata, parentId ?? this._currentGroupId()); if (!event) return Promise.resolve(); sdkObject.attribution.page?.screencast.temporarilyDisableThrottling(); diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index b86c3fad0e4f7..616b5e838b092 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -313,8 +313,8 @@ export const methodMetainfo = new Map { }); test('page.perform history', async ({ page, agent }) => { - test.skip(true, 'Skipping because it needs LLM'); + let clicked = 0; + await page.exposeFunction('clicked', () => clicked++); await page.setContent(` - + `); await agent.perform('click the Fox button'); - const { result } = await agent.extract('return the name of the button you pressed', z.object({ - name: z.string(), - })); - expect(result.name).toBe('Fox'); + await agent.perform('click the Fox button again'); + expect(clicked).toBe(2); }); diff --git a/tests/library/perform-task.spec.ts-cache.json b/tests/library/perform-task.spec.ts-cache.json index 7b7bf4c53cdf9..2f884f4624567 100644 --- a/tests/library/perform-task.spec.ts-cache.json +++ b/tests/library/perform-task.spec.ts-cache.json @@ -58,5 +58,23 @@ "code": "await page.getByRole('textbox', { name: 'Email Address' }).fill('bogus');" } ] + }, + "click the Fox button": { + "actions": [ + { + "method": "click", + "selector": "internal:role=button[name=\"Fox\"i]", + "code": "await page.getByRole('button', { name: 'Fox' }).click();" + } + ] + }, + "click the Fox button again": { + "actions": [ + { + "method": "click", + "selector": "internal:role=button[name=\"Fox\"i]", + "code": "await page.getByRole('button', { name: 'Fox' }).click();" + } + ] } } \ No newline at end of file