diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 757b413704..defe73192f 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -205,7 +205,7 @@ class InspectTool implements RecorderTool { class RecordActionTool implements RecorderTool { private _recorder: Recorder; - private _performingAction: actions.PerformOnRecordAction | null = null; + private _performingActions = new Set(); private _hoveredModel: HighlightModel | null = null; private _hoveredElement: HTMLElement | null = null; private _activeModel: HighlightModel | null = null; @@ -333,21 +333,21 @@ class RecordActionTool implements RecorderTool { onPointerDown(event: PointerEvent) { if (this._shouldIgnoreMouseEvent(event)) return; - if (!this._performingAction) + if (!this._performingActions.size) consumeEvent(event); } onPointerUp(event: PointerEvent) { if (this._shouldIgnoreMouseEvent(event)) return; - if (!this._performingAction) + if (!this._performingActions.size) consumeEvent(event); } onMouseDown(event: MouseEvent) { if (this._shouldIgnoreMouseEvent(event)) return; - if (!this._performingAction) + if (!this._performingActions.size) consumeEvent(event); this._activeModel = this._hoveredModel; } @@ -355,7 +355,7 @@ class RecordActionTool implements RecorderTool { onMouseUp(event: MouseEvent) { if (this._shouldIgnoreMouseEvent(event)) return; - if (!this._performingAction) + if (!this._performingActions.size) consumeEvent(event); } @@ -509,12 +509,13 @@ class RecordActionTool implements RecorderTool { private _actionInProgress(event: Event): boolean { // If Playwright is performing action for us, bail. const isKeyEvent = event instanceof KeyboardEvent; - if (this._performingAction?.name === 'press' && isKeyEvent && event.key === this._performingAction.key) - return true; - const isMouseOrPointerEvent = event instanceof MouseEvent || event instanceof PointerEvent; - if (isMouseOrPointerEvent && (this._performingAction?.name === 'click' || this._performingAction?.name === 'check' || this._performingAction?.name === 'uncheck')) - return true; + for (const action of this._performingActions) { + if (isKeyEvent && action.name === 'press' && event.key === action.key) + return true; + if (isMouseOrPointerEvent && (action.name === 'click' || action.name === 'check' || action.name === 'uncheck')) + return true; + } // Consume event if action is not being executed. consumeEvent(event); @@ -540,9 +541,9 @@ class RecordActionTool implements RecorderTool { this._hoveredModel = null; this._activeModel = null; this._recorder.updateHighlight(null, false); - this._performingAction = action; + this._performingActions.add(action); void this._recorder.performAction(action).then(() => { - this._performingAction = null; + this._performingActions.delete(action); // If that was a keyboard action, it similarly requires new selectors for active model. this._onFocus(false); diff --git a/tests/library/inspector/cli-codegen-1.spec.ts b/tests/library/inspector/cli-codegen-1.spec.ts index d825ed6e93..a876a25e47 100644 --- a/tests/library/inspector/cli-codegen-1.spec.ts +++ b/tests/library/inspector/cli-codegen-1.spec.ts @@ -421,18 +421,32 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`); `); - await page.click('input[name="one"]'); - await recorder.waitForOutput('JavaScript', 'click'); - await page.keyboard.type('foobar123'); - await recorder.waitForOutput('JavaScript', 'foobar123'); + const input1 = page.locator('input[name="one"]'); + const input2 = page.locator('input[name="two"]'); - await page.keyboard.press('Tab'); - await recorder.waitForOutput('JavaScript', 'Tab'); - await page.keyboard.type('barfoo321'); - // I can't explain it atm, first character is being consumed for no apparent reason. - if (browserName === 'webkit' && codegenMode === 'trace-events') - await page.waitForTimeout(1000); - await recorder.waitForOutput('JavaScript', 'barfoo321'); + { + await input1.click(); + await recorder.waitForOutput('JavaScript', 'click'); + await expect(input1).toBeFocused(); + } + + { + await page.keyboard.type('foobar123'); + await recorder.waitForOutput('JavaScript', 'foobar123'); + await expect(input1).toHaveValue('foobar123'); + } + + { + await page.keyboard.press('Tab'); + await recorder.waitForOutput('JavaScript', 'Tab'); + await expect(input2).toBeFocused(); + } + + { + await page.keyboard.type('barfoo321'); + await recorder.waitForOutput('JavaScript', 'barfoo321'); + await expect(input2).toHaveValue('barfoo321'); + } const text = recorder.sources().get('JavaScript')!.text; expect(text).toContain(` diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index 0a130e0936..efde7d52f5 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -220,55 +220,30 @@ await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { });`); page.waitForEvent('download'), page.click('a') ]); - await Promise.all([ - page.waitForEvent('download'), - page.click('a') - ]); - const sources = await recorder.waitForOutput('JavaScript', 'download1Promise'); + const sources = await recorder.waitForOutput('JavaScript', 'downloadPromise'); expect.soft(sources.get('JavaScript')!.text).toContain(` const downloadPromise = page.waitForEvent('download'); await page.getByRole('link', { name: 'Download' }).click(); const download = await downloadPromise;`); - expect.soft(sources.get('JavaScript')!.text).toContain(` - const download1Promise = page.waitForEvent('download'); - await page.getByRole('link', { name: 'Download' }).click(); - const download1 = await download1Promise;`); expect.soft(sources.get('Java')!.text).toContain(` Download download = page.waitForDownload(() -> { page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Download")).click(); });`); - expect.soft(sources.get('Java')!.text).toContain(` - Download download1 = page.waitForDownload(() -> { - page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Download")).click(); - });`); expect.soft(sources.get('Python')!.text).toContain(` with page.expect_download() as download_info: page.get_by_role("link", name="Download").click() download = download_info.value`); - expect.soft(sources.get('Python')!.text).toContain(` - with page.expect_download() as download1_info: - page.get_by_role("link", name="Download").click() - download1 = download1_info.value`); expect.soft(sources.get('Python Async')!.text).toContain(` async with page.expect_download() as download_info: await page.get_by_role("link", name="Download").click() download = await download_info.value`); - expect.soft(sources.get('Python Async')!.text).toContain(` - async with page.expect_download() as download1_info: - await page.get_by_role("link", name="Download").click() - download1 = await download1_info.value`); expect.soft(sources.get('C#')!.text).toContain(` var download = await page.RunAndWaitForDownloadAsync(async () => -{ - await page.GetByRole(AriaRole.Link, new() { Name = "Download" }).ClickAsync(); -});`); - expect.soft(sources.get('C#')!.text).toContain(` -var download1 = await page.RunAndWaitForDownloadAsync(async () => { await page.GetByRole(AriaRole.Link, new() { Name = "Download" }).ClickAsync(); });`); diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 47495a9909..ddc44914b9 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -14,7 +14,10 @@ * limitations under the License. */ +import type { TestServer } from 'tests/config/testserver'; +import type { Recorder } from './inspectorTest'; import { test, expect } from './inspectorTest'; +import type { Page } from '@playwright/test'; test.describe('cli codegen', () => { test.skip(({ mode }) => mode !== 'default'); @@ -90,7 +93,82 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).Nth(1).ClickAsy expect(message.text()).toBe('click2'); }); - test('should generate frame locators', async ({ openRecorder, server }) => { + test('should generate frame locators (1)', async ({ openRecorder, server }) => { + const { page, recorder } = await openRecorder(); + const { frameHello1 } = await createFrameHierarchy(page, recorder, server); + + const [sources] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'Hello1'), + frameHello1.click('text=Hello1'), + ]); + + expect.soft(sources.get('JavaScript')!.text).toContain(` + await page.locator('#frame1').contentFrame().getByText('Hello1').click();`); + + expect.soft(sources.get('Java')!.text).toContain(` + page.locator("#frame1").contentFrame().getByText("Hello1").click();`); + + expect.soft(sources.get('Python')!.text).toContain(` + page.locator("#frame1").content_frame.get_by_text("Hello1").click()`); + + expect.soft(sources.get('Python Async')!.text).toContain(` + await page.locator("#frame1").content_frame.get_by_text("Hello1").click()`); + + expect.soft(sources.get('C#')!.text).toContain(` +await page.Locator("#frame1").ContentFrame.GetByText("Hello1").ClickAsync();`); + }); + + test('should generate frame locators (2)', async ({ openRecorder, server }) => { + const { page, recorder } = await openRecorder(); + const { frameHello2 } = await createFrameHierarchy(page, recorder, server); + + const [sources] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'Hello2'), + frameHello2.click('text=Hello2'), + ]); + + expect.soft(sources.get('JavaScript')!.text).toContain(` + await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().getByText('Hello2').click();`); + + expect.soft(sources.get('Java')!.text).toContain(` + page.locator("#frame1").contentFrame().locator("iframe").contentFrame().getByText("Hello2").click();`); + + expect.soft(sources.get('Python')!.text).toContain(` + page.locator("#frame1").content_frame.locator("iframe").content_frame.get_by_text("Hello2").click()`); + + expect.soft(sources.get('Python Async')!.text).toContain(` + await page.locator("#frame1").content_frame.locator("iframe").content_frame.get_by_text("Hello2").click()`); + + expect.soft(sources.get('C#')!.text).toContain(` +await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.GetByText("Hello2").ClickAsync();`); + }); + + test('should generate frame locators (3)', async ({ openRecorder, server }) => { + const { page, recorder } = await openRecorder(); + const { frameAnonymous } = await createFrameHierarchy(page, recorder, server); + + const [sources] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'HelloNameAnonymous'), + frameAnonymous.click('text=HelloNameAnonymous'), + ]); + + expect.soft(sources.get('JavaScript')!.text).toContain(` + await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().locator('iframe').nth(2).contentFrame().getByText('HelloNameAnonymous').click();`); + + expect.soft(sources.get('Java')!.text).toContain(` + page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe").nth(2).contentFrame().getByText("HelloNameAnonymous").click();`); + + expect.soft(sources.get('Python')!.text).toContain(` + page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe").nth(2).content_frame.get_by_text("HelloNameAnonymous").click()`); + + expect.soft(sources.get('Python Async')!.text).toContain(` + await page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe").nth(2).content_frame.get_by_text("HelloNameAnonymous").click()`); + + expect.soft(sources.get('C#')!.text).toContain(` +await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.Locator("iframe").Nth(2).ContentFrame.GetByText("HelloNameAnonymous").ClickAsync();`); + }); + + test('should generate frame locators (4)', async ({ openRecorder, server }) => { const { page, recorder } = await openRecorder(); /* iframe @@ -99,8 +177,6 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).Nth(1).ClickAsy div Hello2 iframe[name=one] div HelloNameOne - iframe[name=two] - dev HelloNameTwo iframe dev HelloAnonymous */ @@ -109,8 +185,6 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).Nth(1).ClickAsy `, server.EMPTY_PAGE, 6); const frameHello1 = page.mainFrame().childFrames()[0]; const frameHello2 = frameHello1.childFrames()[0]; - const frameOne = page.frame({ name: 'one' })!; - await frameOne.setContent(`
HelloNameOne
`); const frameTwo = page.frame({ name: 'two' })!; await frameTwo.setContent(`
HelloNameTwo
`); const frameAnonymous = frameHello2.childFrames().find(f => !f.name())!; @@ -157,27 +231,6 @@ await page.Locator("#frame1").ContentFrame.GetByText("Hello1").ClickAsync();`); expect.soft(sources.get('C#')!.text).toContain(` await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.GetByText("Hello2").ClickAsync();`); - - [sources] = await Promise.all([ - recorder.waitForOutput('JavaScript', 'one'), - frameOne.click('text=HelloNameOne'), - ]); - - expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().locator('iframe[name="one"]').contentFrame().getByText('HelloNameOne').click();`); - - expect.soft(sources.get('Java')!.text).toContain(` - page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe[name=\\"one\\"]").contentFrame().getByText("HelloNameOne").click();`); - - expect.soft(sources.get('Python')!.text).toContain(` - page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe[name=\\"one\\"]").content_frame.get_by_text("HelloNameOne").click()`); - - expect.soft(sources.get('Python Async')!.text).toContain(` - await page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe[name=\\"one\\"]").content_frame.get_by_text("HelloNameOne").click()`); - - expect.soft(sources.get('C#')!.text).toContain(` -await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.Locator("iframe[name=\\"one\\"]").ContentFrame.GetByText("HelloNameOne").ClickAsync();`); - [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'HelloNameAnonymous'), frameAnonymous.click('text=HelloNameAnonymous'), @@ -758,3 +811,31 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`); await expect(recorder.page.locator('x-pw-glass')).toBeVisible(); }); }); + +async function createFrameHierarchy(page: Page, recorder: Recorder, server: TestServer) { + /* + iframe + div Hello1 + iframe + div Hello2 + iframe[name=one] + div HelloNameOne + iframe + dev HelloAnonymous + */ + await recorder.setContentAndWait(` +