참고소스 수정본

This commit is contained in:
LASTA_DEV01\lasta
2026-05-12 19:40:31 +09:00
parent 0f34a451fc
commit 2e9204243d
8708 changed files with 3259488 additions and 869 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,560 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './inspectorTest';
test.describe('cli codegen', () => {
test('should contain open page', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(``);
const sources = await recorder.waitForOutput('JavaScript', `page.goto`);
expect(sources.get('JavaScript')!.text).toContain(`
const page = await context.newPage();`);
expect(sources.get('Java')!.text).toContain(`
Page page = context.newPage();`);
expect(sources.get('Python')!.text).toContain(`
page = context.new_page()`);
expect(sources.get('Python Async')!.text).toContain(`
page = await context.new_page()`);
expect(sources.get('C#')!.text).toContain(`
var page = await context.NewPageAsync();`);
});
test('should contain second page', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(``);
await page.context().newPage();
const sources = await recorder.waitForOutput('JavaScript', 'page1');
expect(sources.get('JavaScript')!.text).toContain(`
const page1 = await context.newPage();`);
expect(sources.get('Java')!.text).toContain(`
Page page1 = context.newPage();`);
expect(sources.get('Python')!.text).toContain(`
page1 = context.new_page()`);
expect(sources.get('Python Async')!.text).toContain(`
page1 = await context.new_page()`);
expect(sources.get('C#')!.text).toContain(`
var page1 = await context.NewPageAsync();`);
});
test('should contain close page', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(``);
await page.context().newPage();
await recorder.page.close();
const sources = await recorder.waitForOutput('JavaScript', 'page.close();');
expect(sources.get('JavaScript')!.text).toContain(`
await page.close();`);
expect(sources.get('Java')!.text).toContain(`
page.close();`);
expect(sources.get('Python')!.text).toContain(`
page.close()`);
expect(sources.get('Python Async')!.text).toContain(`
await page.close()`);
expect(sources.get('C#')!.text).toContain(`
await page.CloseAsync();`);
});
test('should not lead to an error if html gets clicked', async ({ openRecorder, platform, macVersion }) => {
test.skip(platform === 'darwin' && macVersion < 15, 'recorder.page.evaluate hangs on CDP layer for some reason on macOS 14.');
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait('');
await page.context().newPage();
const errors: any[] = [];
recorder.page.on('pageerror', e => errors.push(e));
await recorder.page.evaluate(() => document.querySelector('body')!.remove());
await page.dispatchEvent('html', 'mousemove', { detail: 1 });
await recorder.page.close();
await recorder.waitForOutput('JavaScript', 'page.close();');
expect(errors.length).toBe(0);
});
test('should upload a single file', async ({ openRecorder, browserName, asset, isLinux }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`
<form>
<input type="file">
</form>
`);
await page.focus('input[type=file]');
await page.setInputFiles('input[type=file]', asset('file-to-upload.txt'));
await page.click('input[type=file]');
const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles');
expect(sources.get('JavaScript')!.text).toContain(`
await page.getByRole('button', { name: 'Choose File' }).setInputFiles('file-to-upload.txt');`);
expect(sources.get('Java')!.text).toContain(`
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Choose File")).setInputFiles(Paths.get("file-to-upload.txt"));`);
expect(sources.get('Python')!.text).toContain(`
page.get_by_role("button", name="Choose File").set_input_files(\"file-to-upload.txt\")`);
expect(sources.get('Python Async')!.text).toContain(`
await page.get_by_role("button", name="Choose File").set_input_files(\"file-to-upload.txt\")`);
expect(sources.get('C#')!.text).toContain(`
await page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).SetInputFilesAsync(new[] { \"file-to-upload.txt\" });`);
});
test('should upload multiple files', async ({ openRecorder, browserName, asset, isLinux }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`
<form>
<input type="file" multiple>
</form>
`);
await page.focus('input[type=file]');
await page.setInputFiles('input[type=file]', [asset('file-to-upload.txt'), asset('file-to-upload-2.txt')]);
await page.click('input[type=file]');
const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles');
expect(sources.get('JavaScript')!.text).toContain(`
await page.getByRole('button', { name: 'Choose File' }).setInputFiles(['file-to-upload.txt', 'file-to-upload-2.txt']);`);
expect(sources.get('Java')!.text).toContain(`
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Choose File")).setInputFiles(new Path[] {Paths.get("file-to-upload.txt"), Paths.get("file-to-upload-2.txt")});`);
expect(sources.get('Python')!.text).toContain(`
page.get_by_role("button", name="Choose File").set_input_files([\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
expect(sources.get('Python Async')!.text).toContain(`
await page.get_by_role("button", name="Choose File").set_input_files([\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
expect(sources.get('C#')!.text).toContain(`
await page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).SetInputFilesAsync(new[] { \"file-to-upload.txt\", \"file-to-upload-2.txt\" });`);
});
test('should clear files', async ({ openRecorder, browserName, asset, isLinux }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`
<form>
<input type="file" multiple>
</form>
`);
await page.focus('input[type=file]');
await page.setInputFiles('input[type=file]', asset('file-to-upload.txt'));
await page.setInputFiles('input[type=file]', []);
await page.click('input[type=file]');
const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles');
expect(sources.get('JavaScript')!.text).toContain(`
await page.getByRole('button', { name: 'Choose File' }).setInputFiles([]);`);
expect(sources.get('Java')!.text).toContain(`
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Choose File")).setInputFiles(new Path[0]);`);
expect(sources.get('Python')!.text).toContain(`
page.get_by_role("button", name="Choose File").set_input_files([])`);
expect(sources.get('Python Async')!.text).toContain(`
await page.get_by_role("button", name="Choose File").set_input_files([])`);
expect(sources.get('C#')!.text).toContain(`
await page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).SetInputFilesAsync(new[] { });`);
});
test('should download files', async ({ openRecorder, server }) => {
const { page, recorder } = await openRecorder();
server.setRoute('/download', (req, res) => {
const pathName = new URL(req.url, 'http://localhost').pathname;
if (pathName === '/download') {
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment; filename=file.txt');
res.end(`Hello world`);
} else {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end('');
}
});
await recorder.setContentAndWait(`
<a href="${server.PREFIX}/download" download>Download</a>
`, server.PREFIX);
await recorder.hoverOverElement('a');
await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const sources = await recorder.waitForOutput('JavaScript', 'await 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('Java')!.text).toContain(`
Download download = 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 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('C#')!.text).toContain(`
var download = await page.RunAndWaitForDownloadAsync(async () =>
{
await page.GetByRole(AriaRole.Link, new() { Name = "Download" }).ClickAsync();
});`);
});
test('should handle dialogs', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`
<button onclick="alert()">click me</button>
`);
await recorder.hoverOverElement('button');
page.once('dialog', async dialog => {
await dialog.dismiss();
});
await page.click('button');
const sources = await recorder.waitForOutput('JavaScript', 'once');
expect.soft(sources.get('JavaScript')!.text).toContain(`
page.once('dialog', dialog => {
console.log(\`Dialog message: \${dialog.message()}\`);
dialog.dismiss().catch(() => {});
});
await page.getByRole('button', { name: 'click me' }).click();`);
expect.soft(sources.get('Java')!.text).toContain(`
page.onceDialog(dialog -> {
System.out.println(String.format("Dialog message: %s", dialog.message()));
dialog.dismiss();
});
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("click me")).click();`);
expect.soft(sources.get('Python')!.text).toContain(`
page.once(\"dialog\", lambda dialog: dialog.dismiss())
page.get_by_role("button", name="click me").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(`
page.once(\"dialog\", lambda dialog: dialog.dismiss())
await page.get_by_role("button", name="click me").click()`);
expect.soft(sources.get('C#')!.text).toContain(`
void page_Dialog_EventHandler(object sender, IDialog dialog)
{
Console.WriteLine($\"Dialog message: {dialog.Message}\");
dialog.DismissAsync();
page.Dialog -= page_Dialog_EventHandler;
}
page.Dialog += page_Dialog_EventHandler;
await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync();`);
});
test('should handle history.postData', async ({ openRecorder, server }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`
<script>
let seqNum = 0;
function pushState() {
history.pushState({}, 'title', '${server.PREFIX}/#seqNum=' + (++seqNum));
}
</script>`, server.PREFIX);
for (let i = 1; i < 3; ++i) {
await page.evaluate('pushState()');
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/#seqNum=${i}');`);
}
});
test('should record open in a new tab with url', async ({ openRecorder, browserName }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
const locator = await recorder.hoverOverElement('a');
expect(locator).toBe(`getByRole('link', { name: 'link' })`);
await page.click('a', { modifiers: ['ControlOrMeta'] });
const sources = await recorder.waitForOutput('JavaScript', 'page1');
if (browserName !== 'firefox') {
expect(sources.get('JavaScript')!.text).toContain(`
const page1 = await context.newPage();
await page1.goto('about:blank?foo');`);
expect(sources.get('Python Async')!.text).toContain(`
page1 = await context.new_page()
await page1.goto("about:blank?foo")`);
expect(sources.get('C#')!.text).toContain(`
var page1 = await context.NewPageAsync();
await page1.GotoAsync("about:blank?foo");`);
} else {
expect(sources.get('JavaScript')!.text).toContain(`
const page1Promise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'link' }).click({
modifiers: ['ControlOrMeta']
});
const page1 = await page1Promise;`);
}
});
test('should not clash pages', async ({ openRecorder, browserName }) => {
const { page, recorder } = await openRecorder();
const [popup1] = await Promise.all([
page.context().waitForEvent('page'),
page.evaluate(`window.open('about:blank')`)
]);
await recorder.setPageContentAndWait(popup1, '<input id=name>');
const [popup2] = await Promise.all([
page.context().waitForEvent('page'),
page.evaluate(`window.open('about:blank')`)
]);
await recorder.setPageContentAndWait(popup2, '<input id=name>');
await popup1.type('input', 'TextA');
await recorder.waitForOutput('JavaScript', 'TextA');
await popup2.type('input', 'TextB');
await recorder.waitForOutput('JavaScript', 'TextB');
const sources = recorder.sources();
expect(sources.get('JavaScript')!.text).toContain(`await page1.locator('#name').fill('TextA');`);
expect(sources.get('JavaScript')!.text).toContain(`await page2.locator('#name').fill('TextB');`);
expect(sources.get('Java')!.text).toContain(`page1.locator("#name").fill("TextA");`);
expect(sources.get('Java')!.text).toContain(`page2.locator("#name").fill("TextB");`);
expect(sources.get('Python')!.text).toContain(`page1.locator("#name").fill("TextA")`);
expect(sources.get('Python')!.text).toContain(`page2.locator("#name").fill("TextB")`);
expect(sources.get('Python Async')!.text).toContain(`await page1.locator("#name").fill("TextA")`);
expect(sources.get('Python Async')!.text).toContain(`await page2.locator("#name").fill("TextB")`);
expect(sources.get('C#')!.text).toContain(`await page1.Locator("#name").FillAsync("TextA");`);
expect(sources.get('C#')!.text).toContain(`await page2.Locator("#name").FillAsync("TextB");`);
});
test('click should emit events in order', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`
<button id=button>
<script>
button.addEventListener('mousedown', e => console.log(e.type));
button.addEventListener('mouseup', e => console.log(e.type));
button.addEventListener('click', e => console.log(e.type));
</script>
`);
const messages: any[] = [];
page.on('console', message => {
if (message.type() !== 'error')
messages.push(message.text());
});
await Promise.all([
page.click('button'),
recorder.waitForOutput('JavaScript', '.click(')
]);
await expect.poll(() => messages).toEqual(['mousedown', 'mouseup', 'click']);
});
test('should reset hover model on action when element detaches', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" onclick="document.getElementById('checkbox').remove()">`);
const [models] = await Promise.all([
recorder.waitForActionPerformed(),
page.click('input')
]);
expect(models.hovered).toBe(null);
});
test('should update active model on action', async ({ openRecorder, browserName, headless }) => {
test.skip(browserName === 'webkit', 'webkit does not actually focus the input after click');
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
const [models] = await Promise.all([
recorder.waitForActionPerformed(),
page.click('input')
]);
expect(models.active).toBe('#checkbox');
});
test('should check input with chaining id', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name = 'updated'"></input>`);
await Promise.all([
recorder.waitForActionPerformed(),
page.click('input[id=checkbox]')
]);
});
test('should record navigations after identical pushState', async ({ openRecorder, server }) => {
const { page, recorder } = await openRecorder();
server.setRoute('/page2.html', (req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end('Hello world');
});
await recorder.setContentAndWait(`
<script>
function pushState() {
history.pushState({}, 'title', '${server.PREFIX}');
}
</script>`, server.PREFIX);
for (let i = 1; i < 3; ++i)
await page.evaluate('pushState()');
await page.goto(server.PREFIX + '/page2.html');
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
});
test('should fill tricky characters', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<textarea spellcheck=false id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`);
const locator = await recorder.focusElement('textarea');
expect(locator).toBe(`locator('#textarea')`);
const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('JavaScript', 'fill'),
page.fill('textarea', 'Hello\'\"\`\nWorld')
]);
expect(sources.get('JavaScript')!.text).toContain(`
await page.locator('#textarea').fill('Hello\\'"\`\\nWorld');`);
expect(sources.get('Java')!.text).toContain(`
page.locator("#textarea").fill("Hello'\\"\`\\nWorld");`);
expect(sources.get('Python')!.text).toContain(`
page.locator("#textarea").fill(\"Hello'\\"\`\\nWorld\")`);
expect(sources.get('Python Async')!.text).toContain(`
await page.locator("#textarea").fill(\"Hello'\\"\`\\nWorld\")`);
expect(sources.get('C#')!.text).toContain(`
await page.Locator("#textarea").FillAsync(\"Hello'\\"\`\\nWorld\");`);
expect(message.text()).toBe('Hello\'\"\`\nWorld');
});
test('should --test-id-attribute', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder({ testIdAttributeName: 'my-test-id' });
await recorder.setContentAndWait(`<div my-test-id="foo">Hello</div>`);
await page.click('[my-test-id=foo]');
const sources = await recorder.waitForOutput('JavaScript', `page.getByTestId`);
expect.soft(sources.get('JavaScript')!.text).toContain(`await page.getByTestId('foo').click()`);
expect.soft(sources.get('Java')!.text).toContain(`page.getByTestId("foo").click()`);
expect.soft(sources.get('Python')!.text).toContain(`page.get_by_test_id("foo").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(`await page.get_by_test_id("foo").click()`);
expect.soft(sources.get('C#')!.text).toContain(`await page.GetByTestId("foo").ClickAsync();`);
});
test('should auto-generate toBeVisible', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`
<button id=one>one</button>
<div id=insertion></div>
<button id=two>two</button>
<script>
const one = document.getElementById('one');
const insertion = document.getElementById('insertion');
one.addEventListener('click', () => {
insertion.innerHTML = '<h2>new header</h2>';
console.log('clicked one');
});
two.addEventListener('click', () => {
console.log('clicked two');
});
</script>
`);
await recorder.recorderPage.getByRole('button', { name: 'Settings' }).click();
await recorder.recorderPage.getByRole('checkbox', { name: 'Generate assertions' }).check();
const locatorOne = await recorder.hoverOverElement('#one');
expect(locatorOne).toBe(`getByRole('button', { name: 'one' })`);
await Promise.all([
page.waitForEvent('console', msg => msg.text() === 'clicked one'),
recorder.waitForOutput('JavaScript', 'one'),
recorder.trustedClick(),
]);
const locatorTwo = await recorder.hoverOverElement('#two');
expect(locatorTwo).toBe(`getByRole('button', { name: 'two' })`);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'two'),
page.waitForEvent('console', msg => msg.text() === 'clicked two'),
recorder.trustedClick(),
]);
expect.soft(sources.get('Playwright Test')!.text).toContain(`
await expect(page.getByRole('heading', { name: 'new header' })).toBeVisible();
await page.getByRole('button', { name: 'two' }).click();`);
expect.soft(sources.get('Python')!.text).toContain(`
expect(page.get_by_role("heading", name="new header")).to_be_visible()
page.get_by_role("button", name="two").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(`
await expect(page.get_by_role("heading", name="new header")).to_be_visible()
await page.get_by_role("button", name="two").click()`);
expect.soft(sources.get('Java')!.text).toContain(`
assertThat(page.getByRole(AriaRole.HEADING, new Page.GetByRoleOptions().setName("new header"))).isVisible();
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("two")).click();`);
expect.soft(sources.get('C#')!.text).toContain(`
await Expect(page.GetByRole(AriaRole.Heading, new() { Name = "new header" })).ToBeVisibleAsync();
await page.GetByRole(AriaRole.Button, new() { Name = "two" }).ClickAsync();`);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,185 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './inspectorTest';
import { roundBox } from '../../config/utils';
test.describe(() => {
test('should generate aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
await recorder.page.click('x-pw-tool-item.snapshot');
await recorder.page.hover('button');
await recorder.trustedClick();
await expect.poll(() =>
recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button "Submit"\`);`);
await expect.poll(() =>
recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button \\"Submit\\"")`);
await expect.poll(() =>
recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button \\"Submit\\"")`);
await expect.poll(() =>
recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button \\"Submit\\"");`);
await expect.poll(() =>
recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button \\"Submit\\"");`);
});
test('should generate regex in aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><button>Submit 123</button></main>`);
await recorder.page.click('x-pw-tool-item.snapshot');
await recorder.page.hover('button');
await recorder.trustedClick();
await expect.poll(() =>
recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button /Submit \\\\d+/\`);`);
await expect.poll(() =>
recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button /Submit \\\\d+/")`);
await expect.poll(() =>
recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button /Submit \\\\d+/")`);
await expect.poll(() =>
recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button /Submit \\\\d+/");`);
await expect.poll(() =>
recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button /Submit \\\\d+/");`);
});
test('should generate regex for uuid in aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><a href="/items/550e8400-e29b-41d4-a716-446655440000">Item 550e8400-e29b-41d4-a716-446655440000</a></main>`);
await recorder.page.click('x-pw-tool-item.snapshot');
await recorder.page.hover('a');
await recorder.trustedClick();
// url still contains full UUID, we can improve here.
await expect.poll(() =>
recorder.text('JavaScript')).toContain(`- link /Item [0-9a-fA-F-]+/:`);
});
test('should inspect aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
await recorder.page.click('x-pw-tool-item.pick-locator');
await recorder.page.hover('button');
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- textbox
- text: '- button "Submit"'
`);
});
test('should update aria snapshot highlight', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main>
<button>Submit</button>
<button>Cancel</button>
</main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover();
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"'
`);
await recorder.recorderPage.locator('.tab-aria .CodeMirror').click();
for (let i = 0; i < '"Submit"'.length; i++)
await recorder.recorderPage.keyboard.press('Backspace');
{
// No accessible name => two boxes.
const box11 = roundBox(await submitButton.boundingBox());
const box12 = roundBox(await recorder.page.locator('x-pw-highlight').first().boundingBox());
expect(box11).toEqual(box12);
const box21 = roundBox(await cancelButton.boundingBox());
const box22 = roundBox(await recorder.page.locator('x-pw-highlight').last().boundingBox());
expect(box21).toEqual(box22);
}
{
// Different button.
await recorder.recorderPage.locator('.tab-aria .CodeMirror').pressSequentially('"Cancel"');
await expect(recorder.page.locator('x-pw-highlight')).toBeVisible();
const box1 = roundBox(await cancelButton.boundingBox());
const box2 = roundBox(await recorder.page.locator('x-pw-highlight').boundingBox());
expect(box1).toEqual(box2);
}
});
test('should show aria snapshot error', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main>
<button>Submit</button>
<button>Cancel</button>
</main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover();
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"'
`);
await recorder.recorderPage.locator('.tab-aria .CodeMirror').click();
await recorder.recorderPage.keyboard.press('Backspace');
// 1 highlighted token
await expect(recorder.recorderPage.locator('.source-line-error-underline')).toHaveCount(1);
});
test('should generate valid javascript with multiline snapshot assertion', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
// set width and height to 100% to ensure click is outside of the list
await recorder.setContentAndWait(`<body style="width:100%;height:100%"><ul><li>item 1</li><li>item 2</li></ul></body>`);
await recorder.page.click('x-pw-tool-item.snapshot');
await recorder.page.hover('body');
await recorder.trustedClick();
// playwright tests assertions are uncommented
await expect.poll(() =>
recorder.text('Playwright Test')).toContain([
` await expect(page.locator('body')).toMatchAriaSnapshot(\``,
` - list:`,
` - listitem: item 1`,
` - listitem: item 2`,
` \`);`,
].join('\n'));
// non-test javascript has commented assertions
await expect.poll(() =>
recorder.text('JavaScript')).toContain([
` // await expect(page.locator('body')).toMatchAriaSnapshot(\``,
` // - list:`,
` // - listitem: item 1`,
` // - listitem: item 2`,
` // \`);`,
].join('\n'));
});
});

View File

@@ -0,0 +1,322 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './inspectorTest';
const launchOptions = (channel: string) => {
return channel ? `Channel = "${channel}",\n Headless = false,` : `Headless = false,`;
};
function capitalize(browserName: string): string {
return browserName[0].toUpperCase() + browserName.slice(1);
}
test('should print the correct imports and context options', async ({ browserName, channel, runCLI, server }) => {
const cli = runCLI(['--target=csharp', server.EMPTY_PAGE]);
const expectedResult = `using Microsoft.Playwright;
using System;
using System.Threading.Tasks;
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(new()
{
${launchOptions(channel)}
});
var context = await browser.NewContextAsync();`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options for custom settings', async ({ browserName, channel, runCLI, server, proxyServer }) => {
proxyServer.forwardTo(server.PORT);
const cli = runCLI([
'--color-scheme=dark',
'--geolocation=37.819722,-122.478611',
'--lang=es',
'--proxy-server=' + proxyServer.HOST,
'--timezone=Europe/Rome',
'--user-agent=hardkodemium',
'--viewport-size=1280,720',
'--target=csharp',
server.EMPTY_PAGE]);
const expectedResult = `
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(new()
{
${launchOptions(channel)}
Proxy = new()
{
Server = "${proxyServer.HOST}",
},
});
var context = await browser.NewContextAsync(new()
{
ColorScheme = ColorScheme.Dark,
Geolocation = new()
{
Latitude = 37.819722m,
Longitude = -122.478611m,
},
Locale = "es",
Permissions = new[] { "geolocation" },
TimezoneId = "Europe/Rome",
UserAgent = "hardkodemium",
ViewportSize = new()
{
Height = 720,
Width = 1280,
},
});`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, channel, runCLI, server }) => {
test.skip(browserName !== 'chromium');
const cli = runCLI(['--device=Pixel 2', '--target=csharp', server.EMPTY_PAGE]);
const expectedResult = `
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(new()
{
${launchOptions(channel)}
});
var context = await browser.NewContextAsync(playwright.Devices["Pixel 2"]);`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, channel, runCLI, server, proxyServer }) => {
test.skip(browserName !== 'webkit');
proxyServer.forwardTo(server.PORT);
const cli = runCLI([
'--device=iPhone 11',
'--color-scheme=dark',
'--geolocation=37.819722,-122.478611',
'--lang=es',
'--proxy-server=' + proxyServer.HOST,
'--timezone=Europe/Rome',
'--user-agent=hardkodemium',
'--viewport-size=1280,720',
'--target=csharp',
server.EMPTY_PAGE]);
const expectedResult = `
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(new()
{
${launchOptions(channel)}
Proxy = new()
{
Server = "${proxyServer.HOST}",
},
});
var context = await browser.NewContextAsync(new()
{
ColorScheme = ColorScheme.Dark,
Geolocation = new()
{
Latitude = 37.819722m,
Longitude = -122.478611m,
},${process.platform === 'linux' && browserName === 'webkit' ? '' : `
HasTouch = true,
IsMobile = true,`}
Locale = "es",
Permissions = new[] { "geolocation" },
Screen = new()
{
Height = 896,
Width = 414,
},
TimezoneId = "Europe/Rome",
UserAgent = "hardkodemium",
ViewportSize = new()
{
Height = 720,
Width = 1280,
},
});`;
await cli.waitFor(expectedResult);
});
test('should print load/save storageState', async ({ browserName, channel, runCLI, server }, testInfo) => {
const loadFileName = testInfo.outputPath('load.json');
const saveFileName = testInfo.outputPath('save.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=csharp', server.EMPTY_PAGE]);
const expectedResult1 = `
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(new()
{
${launchOptions(channel)}
});
var context = await browser.NewContextAsync(new()
{
StorageStatePath = "${loadFileName.replace(/\\/g, '\\\\')}",
});`;
await cli.waitFor(expectedResult1);
const expectedResult2 = `
await context.StorageStateAsync(new()
{
Path = "${saveFileName.replace(/\\/g, '\\\\')}"
});
`;
await cli.waitFor(expectedResult2);
});
test('should work with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `await context.RouteFromHARAsync(${JSON.stringify(harFileName)});`;
const cli = runCLI(['--target=csharp', `--save-har=${harFileName}`]);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
test('should work with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `await context.RouteFromHARAsync(${JSON.stringify(harFileName)}, new()
{
Url = "**/*.js",
});`;
const cli = runCLI(['--target=csharp', `--save-har=${harFileName}`, '--save-har-glob=**/*.js']);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
for (const testFramework of ['nunit', 'mstest', 'xunit'] as const) {
test(`should not print context options method override in ${testFramework} if no options were passed`, async ({ runCLI, server }) => {
const cli = runCLI([`--target=csharp-${testFramework}`, server.EMPTY_PAGE]);
await cli.waitFor(`Page.GotoAsync("${server.EMPTY_PAGE}")`);
expect(await cli.text()).not.toContain('public override BrowserNewContextOptions ContextOptions()');
});
test(`should print context options method override in ${testFramework} if options were passed`, async ({ runCLI, server }) => {
const cli = runCLI([`--target=csharp-${testFramework}`, '--color-scheme=dark', server.EMPTY_PAGE]);
await cli.waitFor(`Page.GotoAsync("${server.EMPTY_PAGE}")`);
expect(await cli.text()).toContain(` public override BrowserNewContextOptions ContextOptions()
{
return new()
{
ColorScheme = ColorScheme.Dark,
};
}
`);
});
test(`should work with --save-har in ${testFramework}`, async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `await Context.RouteFromHARAsync(${JSON.stringify(harFileName)});`;
const cli = runCLI([`--target=csharp-${testFramework}`, `--save-har=${harFileName}`]);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
test(`should work with --save-har and --save-har-glob in ${testFramework}`, async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `await Context.RouteFromHARAsync(${JSON.stringify(harFileName)}, new()
{
Url = "**/*.js",
});`;
const cli = runCLI([`--target=csharp-${testFramework}`, `--save-har=${harFileName}`, '--save-har-glob=**/*.js']);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
}
test(`should print a valid basic program in mstest`, async ({ runCLI, server }) => {
const cli = runCLI([`--target=csharp-mstest`, '--color-scheme=dark', server.EMPTY_PAGE]);
await cli.waitFor(`Page.GotoAsync("${server.EMPTY_PAGE}")`);
const expected = `using Microsoft.Playwright.MSTest;
using Microsoft.Playwright;
[TestClass]
public class Tests : PageTest
{
public override BrowserNewContextOptions ContextOptions()
{
return new()
{
ColorScheme = ColorScheme.Dark,
};
}
[TestMethod]
public async Task MyTest()
{
await Page.GotoAsync("${server.EMPTY_PAGE}");
}
}`;
expect(await cli.text()).toContain(expected);
});
test(`should print a valid basic program in nunit`, async ({ runCLI, server }) => {
const cli = runCLI([`--target=csharp-nunit`, '--color-scheme=dark', server.EMPTY_PAGE]);
await cli.waitFor(`Page.GotoAsync("${server.EMPTY_PAGE}")`);
const expected = `using Microsoft.Playwright.NUnit;
using Microsoft.Playwright;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class Tests : PageTest
{
public override BrowserNewContextOptions ContextOptions()
{
return new()
{
ColorScheme = ColorScheme.Dark,
};
}
[Test]
public async Task MyTest()
{
await Page.GotoAsync("${server.EMPTY_PAGE}");
}
}`;
expect(await cli.text()).toContain(expected);
});
test(`should print a valid basic program in xunit`, async ({ runCLI, server }) => {
const cli = runCLI([`--target=csharp-xunit`, '--color-scheme=dark', server.EMPTY_PAGE]);
await cli.waitFor(`Page.GotoAsync("${server.EMPTY_PAGE}")`);
const expected = `using Microsoft.Playwright.Xunit;
using Microsoft.Playwright;
using Xunit;
public class Tests : PageTest
{
public override BrowserNewContextOptions ContextOptions()
{
return new()
{
ColorScheme = ColorScheme.Dark,
};
}
[Fact]
public async Task MyTest()
{
await Page.GotoAsync("${server.EMPTY_PAGE}");
}
}`;
expect(await cli.text()).toContain(expected);
});

View File

@@ -0,0 +1,142 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './inspectorTest';
const launchOptions = (channel: string) => {
return channel ? `.setChannel("${channel}")\n .setHeadless(false)` : '.setHeadless(false)';
};
test('should print the correct imports and context options', async ({ runCLI, channel, browserName, server }) => {
const cli = runCLI(['--target=java', server.EMPTY_PAGE]);
const expectedResult = `import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import java.util.*;
public class Example {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.${browserName}().launch(new BrowserType.LaunchOptions()
${launchOptions(channel)});
BrowserContext context = browser.newContext();`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options for custom settings', async ({ runCLI, server }) => {
const cli = runCLI(['--color-scheme=light', '--target=java', server.EMPTY_PAGE]);
const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setColorScheme(ColorScheme.LIGHT));`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, runCLI, server }) => {
test.skip(browserName !== 'chromium');
const cli = runCLI(['--device=Pixel 2', '--target=java', server.EMPTY_PAGE]);
await cli.waitFor(`.setViewportSize(411, 731));`);
const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setDeviceScaleFactor(2.625)
.setHasTouch(true)
.setIsMobile(true)
.setUserAgent("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/XXXX Mobile Safari/537.36")
.setViewportSize(411, 731));`;
const text = await cli.text();
expect(text.replace(/(.*Chrome\/)(.*?)( .*)/m, '$1XXXX$3')).toContain(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, runCLI, server }) => {
test.skip(browserName !== 'webkit');
const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', '--target=java', server.EMPTY_PAGE]);
await cli.waitFor(`.setViewportSize(414, 715));`);
const expectedResult = `BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setColorScheme(ColorScheme.LIGHT)
.setDeviceScaleFactor(2)
.setHasTouch(true)
.setIsMobile(true)
.setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/XXXX Mobile/15E148 Safari/604.1")
.setViewportSize(414, 715));`;
const text = await cli.text();
expect(text.replace(/(.*Version\/)(.*?)( .*)/m, '$1XXXX$3')).toContain(expectedResult);
});
test('should print load/save storage_state', async ({ runCLI, server }, testInfo) => {
const loadFileName = testInfo.outputPath('load.json');
const saveFileName = testInfo.outputPath('save.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=java', server.EMPTY_PAGE]);
const expectedResult1 = `BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setStorageStatePath(Paths.get(${JSON.stringify(loadFileName)})));`;
await cli.waitFor(expectedResult1);
const expectedResult2 = `
context.storageState(new BrowserContext.StorageStateOptions().setPath("${saveFileName.replace(/\\/g, '\\\\')}"))`;
await cli.waitFor(expectedResult2);
});
test('should work with --save-har and --save-har-glob as java-library', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `context.routeFromHAR(Paths.get(${JSON.stringify(harFileName)}), new BrowserContext.RouteFromHAROptions()
.setUrl("**/*.js"));`;
const cli = runCLI(['--target=java', `--save-har=${harFileName}`, '--save-har-glob=**/*.js']);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
test('should work with --save-har and --save-har-glob as java-junit', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `page.routeFromHAR(Paths.get(${JSON.stringify(harFileName)}), new Page.RouteFromHAROptions()
.setUrl("**/*.js"));`;
const cli = runCLI(['--target=java-junit', `--save-har=${harFileName}`, '--save-har-glob=**/*.js']);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
test('should print the correct imports in junit', async ({ runCLI, server }) => {
const cli = runCLI(['--target=java-junit', server.EMPTY_PAGE]);
const expectedImportResult = `import com.microsoft.playwright.junit.UsePlaywright;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.*;
import org.junit.jupiter.api.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.*;`;
await cli.waitFor(expectedImportResult);
});
test('should print a valid basic program in junit', async ({ runCLI, server }) => {
const cli = runCLI(['--target=java-junit', server.EMPTY_PAGE]);
const expectedResult = `import com.microsoft.playwright.junit.UsePlaywright;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.*;
import org.junit.jupiter.api.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.*;
@UsePlaywright
public class TestExample {
@Test
void test(Page page) {
page.navigate("${server.EMPTY_PAGE}");
}
}`;
await cli.waitFor(expectedResult);
});

View File

@@ -0,0 +1,125 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test } from './inspectorTest';
const launchOptions = (channel: string) => {
return channel ? `channel: '${channel}',\n headless: false` : 'headless: false';
};
test('should print the correct imports and context options', async ({ browserName, channel, runCLI, server }) => {
const cli = runCLI(['--target=javascript', server.EMPTY_PAGE]);
const expectedResult = `const { ${browserName} } = require('playwright');
(async () => {
const browser = await ${browserName}.launch({
${launchOptions(channel)}
});
const context = await browser.newContext();`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options for custom settings', async ({ browserName, channel, runCLI, server }) => {
const cli = runCLI(['--color-scheme=light', '--target=javascript', server.EMPTY_PAGE]);
const expectedResult = `const { ${browserName} } = require('playwright');
(async () => {
const browser = await ${browserName}.launch({
${launchOptions(channel)}
});
const context = await browser.newContext({
colorScheme: 'light'
});`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, channel, runCLI, server }) => {
test.skip(browserName !== 'chromium');
const cli = runCLI(['--device=Pixel 2', '--target=javascript', server.EMPTY_PAGE]);
const expectedResult = `const { chromium, devices } = require('playwright');
(async () => {
const browser = await chromium.launch({
${launchOptions(channel)}
});
const context = await browser.newContext({
...devices['Pixel 2'],
});`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, channel, runCLI, server }) => {
test.skip(browserName !== 'webkit');
const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', '--target=javascript', server.EMPTY_PAGE]);
const expectedResult = `const { webkit, devices } = require('playwright');
(async () => {
const browser = await webkit.launch({
${launchOptions(channel)}
});
const context = await browser.newContext({
...devices['iPhone 11'],
colorScheme: 'light'
});`;
await cli.waitFor(expectedResult);
});
test('should save the codegen output to a file if specified', async ({ browserName, channel, runCLI, server }, testInfo) => {
const cli = runCLI(['--target=javascript', server.EMPTY_PAGE]);
await cli.waitFor(`const { ${browserName} } = require('playwright');
(async () => {
const browser = await ${browserName}.launch({
${launchOptions(channel)}
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('${server.EMPTY_PAGE}');
// ---------------------
await context.close();
await browser.close();
})();`);
});
test('should print load/save storageState', async ({ browserName, channel, runCLI, server }, testInfo) => {
const loadFileName = testInfo.outputPath('load.json');
const saveFileName = testInfo.outputPath('save.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=javascript', server.EMPTY_PAGE]);
const expectedResult1 = `const { ${browserName} } = require('playwright');
(async () => {
const browser = await ${browserName}.launch({
${launchOptions(channel)}
});
const context = await browser.newContext({
storageState: '${loadFileName.replace(/\\/g, '\\\\')}'
});`;
await cli.waitFor(expectedResult1);
const expectedResult2 = `
// ---------------------
await context.storageState({ path: '${saveFileName.replace(/\\/g, '\\\\')}' });
await context.close();
await browser.close();
})();`;
await cli.waitFor(expectedResult2);
});

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './inspectorTest';
import { roundBox } from '../../config/utils';
test.describe(() => {
test('should inspect locator', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
await recorder.page.click('x-pw-tool-item.pick-locator');
await recorder.page.hover('button');
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
- text: "getByRole('button', { name: 'Submit' })"
`);
});
test('should update locator highlight', async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main>
<button>Submit</button>
<button>Cancel</button>
</main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover();
await recorder.trustedClick();
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
- text: "getByRole('button', { name: 'Submit' })"
`);
await recorder.recorderPage.locator('.tab-locator .CodeMirror').click();
for (let i = 0; i < `Submit' })`.length; i++)
await recorder.recorderPage.keyboard.press('Backspace');
{
// Different button.
await recorder.recorderPage.locator('.tab-locator .CodeMirror').pressSequentially(`Cancel' })`);
await expect(recorder.page.locator('x-pw-highlight')).toBeVisible();
const box1 = roundBox(await cancelButton.boundingBox());
const box2 = roundBox(await recorder.page.locator('x-pw-highlight').boundingBox());
expect(box1).toEqual(box2);
}
});
});

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './inspectorTest';
test('should print the correct imports and context options', async ({ runCLI, server }) => {
const cli = runCLI(['--target=python-pytest', server.EMPTY_PAGE]);
const expectedResult = `import re
from playwright.sync_api import Page, expect
def test_example(page: Page) -> None:`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device and lang', async ({ browserName, runCLI, server }, testInfo) => {
test.skip(browserName !== 'webkit');
const cli = runCLI(['--target=python-pytest', '--device=iPhone 11', '--lang=en-US', server.EMPTY_PAGE]);
await cli.waitFor(`import pytest
import re
from playwright.sync_api import Page, expect
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args, playwright):
return {**playwright.devices["iPhone 11"], "locale": "en-US"}
def test_example(page: Page) -> None:
page.goto("${server.EMPTY_PAGE}")
`);
});
test('should save the codegen output to a file if specified', async ({ runCLI, server }, testInfo) => {
const cli = runCLI(['--target=python-pytest', server.EMPTY_PAGE]);
await cli.waitFor(`import re
from playwright.sync_api import Page, expect
def test_example(page: Page) -> None:
page.goto("${server.EMPTY_PAGE}")
`);
});
test('should work with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `page.route_from_har(${JSON.stringify(harFileName)})`;
const cli = runCLI(['--target=python-pytest', `--save-har=${harFileName}`]);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
test('should work with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `page.route_from_har(${JSON.stringify(harFileName)}, url="**/*.js")`;
const cli = runCLI(['--target=python-pytest', `--save-har=${harFileName}`, '--save-har-glob=**/*.js']);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});

View File

@@ -0,0 +1,157 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './inspectorTest';
const launchOptions = (channel: string) => {
return channel ? `channel="${channel}", headless=False` : 'headless=False';
};
test('should print the correct imports and context options', async ({ browserName, channel, runCLI, server }) => {
const cli = runCLI(['--target=python-async', server.EMPTY_PAGE]);
const expectedResult = `import asyncio
import re
from playwright.async_api import Playwright, async_playwright, expect
async def run(playwright: Playwright) -> None:
browser = await playwright.${browserName}.launch(${launchOptions(channel)})
context = await browser.new_context()`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options for custom settings', async ({ browserName, channel, runCLI, server }) => {
const cli = runCLI(['--color-scheme=light', '--target=python-async', server.EMPTY_PAGE]);
const expectedResult = `import asyncio
import re
from playwright.async_api import Playwright, async_playwright, expect
async def run(playwright: Playwright) -> None:
browser = await playwright.${browserName}.launch(${launchOptions(channel)})
context = await browser.new_context(color_scheme="light")`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, channel, runCLI, server }) => {
test.skip(browserName !== 'chromium');
const cli = runCLI(['--device=Pixel 2', '--target=python-async', server.EMPTY_PAGE]);
const expectedResult = `import asyncio
import re
from playwright.async_api import Playwright, async_playwright, expect
async def run(playwright: Playwright) -> None:
browser = await playwright.chromium.launch(${launchOptions(channel)})
context = await browser.new_context(**playwright.devices["Pixel 2"])`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, channel, runCLI, server }) => {
test.skip(browserName !== 'webkit');
const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', '--target=python-async', server.EMPTY_PAGE]);
const expectedResult = `import asyncio
import re
from playwright.async_api import Playwright, async_playwright, expect
async def run(playwright: Playwright) -> None:
browser = await playwright.webkit.launch(${launchOptions(channel)})
context = await browser.new_context(**playwright.devices["iPhone 11"], color_scheme="light")`;
await cli.waitFor(expectedResult);
});
test('should save the codegen output to a file if specified', async ({ browserName, channel, runCLI, server }, testInfo) => {
const cli = runCLI(['--target=python-async', server.EMPTY_PAGE]);
await cli.waitFor(`import asyncio
import re
from playwright.async_api import Playwright, async_playwright, expect
async def run(playwright: Playwright) -> None:
browser = await playwright.${browserName}.launch(${launchOptions(channel)})
context = await browser.new_context()
page = await context.new_page()
await page.goto("${server.EMPTY_PAGE}")
# ---------------------
await context.close()
await browser.close()
async def main() -> None:
async with async_playwright() as playwright:
await run(playwright)
asyncio.run(main())
`);
});
test('should print load/save storage_state', async ({ browserName, channel, runCLI, server }, testInfo) => {
const loadFileName = testInfo.outputPath('load.json');
const saveFileName = testInfo.outputPath('save.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=python-async', server.EMPTY_PAGE]);
const expectedResult1 = `import asyncio
import re
from playwright.async_api import Playwright, async_playwright, expect
async def run(playwright: Playwright) -> None:
browser = await playwright.${browserName}.launch(${launchOptions(channel)})
context = await browser.new_context(storage_state="${loadFileName.replace(/\\/g, '\\\\')}")`;
await cli.waitFor(expectedResult1);
const expectedResult2 = `
# ---------------------
await context.storage_state(path="${saveFileName.replace(/\\/g, '\\\\')}")
await context.close()
await browser.close()
async def main() -> None:
async with async_playwright() as playwright:
await run(playwright)
asyncio.run(main())
`;
await cli.waitFor(expectedResult2);
});
test('should work with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `await context.route_from_har(${JSON.stringify(harFileName)})`;
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`]);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
test('should work with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `await context.route_from_har(${JSON.stringify(harFileName)}, url="**/*.js")`;
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`, '--save-har-glob=**/*.js']);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});

View File

@@ -0,0 +1,143 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './inspectorTest';
const launchOptions = (channel: string) => {
return channel ? `channel="${channel}", headless=False` : 'headless=False';
};
test('should print the correct imports and context options', async ({ runCLI, channel, browserName, server }) => {
const cli = runCLI(['--target=python', server.EMPTY_PAGE]);
const expectedResult = `import re
from playwright.sync_api import Playwright, sync_playwright, expect
def run(playwright: Playwright) -> None:
browser = playwright.${browserName}.launch(${launchOptions(channel)})
context = browser.new_context()`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options for custom settings', async ({ runCLI, channel, browserName, server }) => {
const cli = runCLI(['--color-scheme=light', '--target=python', server.EMPTY_PAGE]);
const expectedResult = `import re
from playwright.sync_api import Playwright, sync_playwright, expect
def run(playwright: Playwright) -> None:
browser = playwright.${browserName}.launch(${launchOptions(channel)})
context = browser.new_context(color_scheme="light")`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, channel, runCLI, server }) => {
test.skip(browserName !== 'chromium');
const cli = runCLI(['--device=Pixel 2', '--target=python', server.EMPTY_PAGE]);
const expectedResult = `import re
from playwright.sync_api import Playwright, sync_playwright, expect
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(${launchOptions(channel)})
context = browser.new_context(**playwright.devices["Pixel 2"])`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, channel, runCLI, server }) => {
test.skip(browserName !== 'webkit');
const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', '--target=python', server.EMPTY_PAGE]);
const expectedResult = `import re
from playwright.sync_api import Playwright, sync_playwright, expect
def run(playwright: Playwright) -> None:
browser = playwright.webkit.launch(${launchOptions(channel)})
context = browser.new_context(**playwright.devices["iPhone 11"], color_scheme="light")`;
await cli.waitFor(expectedResult);
});
test('should save the codegen output to a file if specified', async ({ runCLI, channel, browserName, server }, testInfo) => {
const cli = runCLI(['--target=python', server.EMPTY_PAGE]);
await cli.waitFor(`import re
from playwright.sync_api import Playwright, sync_playwright, expect
def run(playwright: Playwright) -> None:
browser = playwright.${browserName}.launch(${launchOptions(channel)})
context = browser.new_context()
page = context.new_page()
page.goto("${server.EMPTY_PAGE}")
# ---------------------
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
`);
});
test('should print load/save storage_state', async ({ runCLI, channel, browserName, server }, testInfo) => {
const loadFileName = testInfo.outputPath('load.json');
const saveFileName = testInfo.outputPath('save.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, '--target=python', server.EMPTY_PAGE]);
const expectedResult1 = `import re
from playwright.sync_api import Playwright, sync_playwright, expect
def run(playwright: Playwright) -> None:
browser = playwright.${browserName}.launch(${launchOptions(channel)})
context = browser.new_context(storage_state="${loadFileName.replace(/\\/g, '\\\\')}")`;
await cli.waitFor(expectedResult1);
const expectedResult2 = `
# ---------------------
context.storage_state(path="${saveFileName.replace(/\\/g, '\\\\')}")
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
`;
await cli.waitFor(expectedResult2);
});
test('should work with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `context.route_from_har(${JSON.stringify(harFileName)})`;
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`]);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
test('should work with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `context.route_from_har(${JSON.stringify(harFileName)}, url="**/*.js")`;
const cli = runCLI(['--target=python-async', `--save-har=${harFileName}`, '--save-har-glob=**/*.js']);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});

View File

@@ -0,0 +1,120 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './inspectorTest';
test('should print the correct imports and context options', async ({ runCLI, server }) => {
const cli = runCLI([server.EMPTY_PAGE]);
const expectedResult = `import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('${server.EMPTY_PAGE}');
});`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options for custom settings', async ({ runCLI, server }) => {
const cli = runCLI(['--color-scheme=light', server.EMPTY_PAGE]);
const expectedResult = `import { test, expect } from '@playwright/test';
test.use({
colorScheme: 'light'
});
test('test', async ({ page }) => {`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device', async ({ browserName, runCLI, server }) => {
test.skip(browserName !== 'chromium');
const cli = runCLI(['--device=Pixel 2', server.EMPTY_PAGE]);
const expectedResult = `import { test, expect, devices } from '@playwright/test';
test.use({
...devices['Pixel 2'],
});
test('test', async ({ page }) => {`;
await cli.waitFor(expectedResult);
});
test('should print the correct context options when using a device and additional options', async ({ browserName, server, runCLI }) => {
test.skip(browserName !== 'webkit');
const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', server.EMPTY_PAGE]);
const expectedResult = `import { test, expect, devices } from '@playwright/test';
test.use({
...devices['iPhone 11'],
colorScheme: 'light'
});
test('test', async ({ page }) => {`;
await cli.waitFor(expectedResult);
});
test('should print load storageState', async ({ runCLI, server }, testInfo) => {
const loadFileName = testInfo.outputPath('load.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, server.EMPTY_PAGE]);
const expectedResult = `import { test, expect } from '@playwright/test';
test.use({
storageState: '${loadFileName.replace(/\\/g, '\\\\')}'
});
test('test', async ({ page }) => {`;
await cli.waitFor(expectedResult);
});
test('should not generate recordHAR with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = ` await page.routeFromHAR('${harFileName.replace(/\\/g, '\\\\')}');`;
const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`]);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
test('should generate routeFromHAR with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `test('test', async ({ page }) => {
await page.routeFromHAR('${harFileName.replace(/\\/g, '\\\\')}');
});`;
const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`]);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
test('should generate routeFromHAR with --save-har and --save-har-glob', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har');
const expectedResult = `test('test', async ({ page }) => {
await page.routeFromHAR('${harFileName.replace(/\\/g, '\\\\')}', {
url: '**/*.js'
});
});`;
const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`, '--save-har-glob=**/*.js']);
await cli.waitFor(expectedResult);
await cli.exit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});

View File

@@ -0,0 +1,121 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test as it, expect } from './inspectorTest';
let scriptPromise: Promise<void>;
it.beforeEach(async ({ page, recorderPageGetter }) => {
scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
})();
await recorderPageGetter();
});
it.afterEach(async ({ recorderPageGetter }) => {
const recorderPage = await recorderPageGetter();
recorderPage.click('[title="Resume (F8)"]').catch(() => {});
await scriptPromise;
recorderPage.click('[title="Resume (F8)"]').catch(() => {});
});
it('should support playwright.$, playwright.$$', async ({ page }) => {
const body = await page.evaluateHandle('playwright.$("body")');
expect(await body.evaluate<string, HTMLBodyElement>((node: HTMLBodyElement) => node.nodeName)).toBe('BODY');
const length = await page.evaluate('playwright.$$("body").length');
expect(length).toBe(1);
});
it('should support playwright.selector', async ({ page }) => {
const length = await page.evaluate('playwright.selector(document.body)');
expect(length).toBe('body');
});
it('should support playwright.locator.value', async ({ page }) => {
await page.setContent('<div>Hello<div>');
const handle = await page.evaluateHandle(`playwright.locator('div', { hasText: 'Hello' }).element`);
expect(await handle.evaluate<string, HTMLDivElement>((node: HTMLDivElement) => node.nodeName)).toBe('DIV');
});
it('should support playwright.locator.values', async ({ page }) => {
await page.setContent('<div>Hello<div>Bar</div></div>');
expect(await page.evaluate(`playwright.locator('div', { hasText: 'Hello' }).elements.length`)).toBe(1);
expect(await page.evaluate(`playwright.locator('div', { hasText: 'HElLo' }).elements.length`)).toBe(1);
expect(await page.evaluate(`playwright.locator('div', { hasText: /ELL/ }).elements.length`)).toBe(0);
expect(await page.evaluate(`playwright.locator('div', { hasText: /ELL/i }).elements.length`)).toBe(1);
expect(await page.evaluate(`playwright.locator('div', { hasText: /Hello/ }).elements.length`)).toBe(1);
expect(await page.evaluate(`playwright.locator('div', { hasNotText: /Bar/ }).elements.length`)).toBe(0);
expect(await page.evaluate(`playwright.locator('div', { hasNotText: /Hello/ }).elements.length`)).toBe(1);
});
it('should support playwright.locator({ has })', async ({ page }) => {
await page.setContent(`
<div>Hi</div>
<div><span>Hello</span></div>
<div><span>dont match</span></div>
`);
expect(await page.evaluate(`playwright.locator('div', { has: playwright.locator('span') }).element.innerHTML`)).toContain('Hello');
expect(await page.evaluate(`playwright.locator('div', { has: playwright.locator('text=Hello') }).element.innerHTML`)).toContain('span');
expect(await page.evaluate(`playwright.locator('div', { has: playwright.locator('span', { hasText: 'Hello' }) }).elements.length`)).toBe(1);
});
it('should support playwright.locator({ hasNot })', async ({ page }) => {
await page.setContent('<div>Hi</div><div><span>Hello</span></div>');
expect(await page.evaluate(`playwright.locator('div', { hasNot: playwright.locator('span') }).element.innerHTML`)).toContain('Hi');
expect(await page.evaluate(`playwright.locator('div', { hasNot: playwright.locator('text=Hello') }).element.innerHTML`)).toContain('Hi');
});
it('should support locator.and()', async ({ page }) => {
await page.setContent('<div data-testid=Hey>Hi</div>');
expect(await page.evaluate(`playwright.locator('div').and(playwright.getByTestId('Hey')).elements.map(e => e.innerHTML)`)).toEqual(['Hi']);
});
it('should support locator.or()', async ({ page }) => {
await page.setContent('<div>Hi</div><span>Hello</span>');
expect(await page.evaluate(`playwright.locator('div').or(playwright.locator('span')).elements.map(e => e.innerHTML)`)).toEqual(['Hi', 'Hello']);
});
it('should support playwright.getBy*', async ({ page }) => {
await page.setContent('<span>Hello</span><span title="world">World</span><div>one</div><div style="display:none">two</div>');
expect(await page.evaluate(`playwright.getByText('hello').element.innerHTML`)).toContain('Hello');
expect(await page.evaluate(`playwright.getByTitle('world').element.innerHTML`)).toContain('World');
expect(await page.evaluate(`playwright.locator('span').filter({ hasText: 'hello' }).element.innerHTML`)).toContain('Hello');
expect(await page.evaluate(`playwright.locator('span').first().element.innerHTML`)).toContain('Hello');
expect(await page.evaluate(`playwright.locator('span').last().element.innerHTML`)).toContain('World');
expect(await page.evaluate(`playwright.locator('span').nth(1).element.innerHTML`)).toContain('World');
expect(await page.evaluate(`playwright.locator('div').filter({ visible: false }).element.innerHTML`)).toContain('two');
});
it('expected properties on playwright object', async ({ page }) => {
expect(await page.evaluate(`Object.keys(playwright)`)).toEqual([
'$',
'$$',
'inspect',
'selector',
'generateLocator',
'ariaSnapshot',
'resume',
'locator',
'getByTestId',
'getByAltText',
'getByLabel',
'getByPlaceholder',
'getByText',
'getByTitle',
'getByRole',
]);
});

View File

@@ -0,0 +1,306 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { contextTest } from '../../config/browserTest';
import type { Locator, Page } from 'playwright-core';
import { step } from '../../config/baseTest';
import * as path from 'path';
import fs from 'fs';
import type { Source } from '../../../packages/recorder/src/recorderTypes';
import type { CommonFixtures, TestChildProcess } from '../../config/commonFixtures';
import { expect } from '@playwright/test';
export { expect } from '@playwright/test';
type CLITestArgs = {
recorderPageGetter: () => Promise<Page>;
closeRecorder: () => Promise<void>;
openRecorder: (options?: { testIdAttributeName?: string, language?: string }) => Promise<{ recorder: Recorder, page: Page }>;
runCLI: (args: string[]) => CLIMock;
};
const codegenLang2Id: Map<string, string> = new Map([
['JSON', 'jsonl'],
['JavaScript', 'javascript'],
['Java', 'java'],
['Java JUnit', 'java-junit'],
['Python', 'python'],
['Python Async', 'python-async'],
['Pytest', 'python-pytest'],
['C#', 'csharp'],
['C# NUnit', 'csharp-nunit'],
['C# MSTest', 'csharp-mstest'],
['C# xUnit', 'csharp-xunit'],
['Playwright Test', 'playwright-test'],
]);
const codegenLangId2lang = new Map([...codegenLang2Id.entries()].map(([lang, langId]) => [langId, lang]));
import { inprocess } from '../../../packages/playwright-core/lib/coreBundle';
// Use a separate Playwright instance for automating the inspector so that
// contexts created here do not get tracked by the test runner's tracing.
const playwrightToAutomateInspector = inprocess.createInProcessPlaywright();
export const test = contextTest.extend<CLITestArgs>({
recorderPageGetter: async ({ context, toImpl, mode, headless }, run, testInfo) => {
testInfo.skip(mode !== 'default');
testInfo.skip(!headless, 'real mouse moves mess up with recording');
await run(async () => {
while (!toImpl(context).recorderAppForTest)
await new Promise(f => setTimeout(f, 100));
const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpointForTest;
const browser = await playwrightToAutomateInspector.chromium.connectOverCDP({ wsEndpoint });
const c = browser.contexts()[0];
return c.pages()[0] || await c.waitForEvent('page');
});
},
closeRecorder: async ({ context, toImpl }, run) => {
await run(async () => {
await toImpl(context).recorderAppForTest.close();
});
},
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => {
testInfo.slow();
testInfo.skip(mode.startsWith('service'));
let cli: CLIMock | undefined;
await run(cliArgs => {
const outputFile = testInfo.outputPath('codegen.output');
cli = new CLIMock(childProcess, {
outputFile,
browserName,
channel,
headless,
args: cliArgs,
executablePath: launchOptions.executablePath,
});
return cli;
});
await cli?.exit();
},
openRecorder: async ({ context, recorderPageGetter }, use) => {
await use(async options => {
await (context as any)._enableRecorder({
mode: 'recording',
omitCallTracking: true,
...options
});
const page = await context.newPage();
return { page, recorder: new Recorder(page, await recorderPageGetter()) };
});
},
});
export class Recorder {
page: Page;
_highlightCallback: Function;
_highlightInstalled: boolean;
_actionReporterInstalled: boolean;
_actionPerformedCallback: Function;
recorderPage: Page;
private _sources = new Map<string, Source>();
constructor(page: Page, recorderPage: Page) {
this.page = page;
this.recorderPage = recorderPage;
this._highlightCallback = () => { };
this._highlightInstalled = false;
this._actionReporterInstalled = false;
this._actionPerformedCallback = () => { };
}
async setContentAndWait(content: string, url: string = 'about:blank', frameCount: number = 1) {
await this.setPageContentAndWait(this.page, content, url, frameCount);
}
async setPageContentAndWait(page: Page, content: string, url: string = 'about:blank', frameCount: number = 1) {
let callback;
const result = new Promise(f => callback = f);
let msgCount = 0;
const listener = msg => {
if (msg.text() === 'Recorder script ready for test') {
++msgCount;
if (msgCount === frameCount) {
page.off('console', listener);
callback();
}
}
};
page.on('console', listener);
await page.goto(url);
await Promise.all([
result,
page.setContent(content)
]);
}
async waitForOutput(file: string, text: string): Promise<Map<string, Source>> {
return await test.step('waitForOutput', async () => {
if (!codegenLang2Id.has(file))
throw new Error(`Unknown language: ${file}`);
await expect.poll(() => this.recorderPage.evaluate(languageId => {
const sources = ((window as any).playwrightSourcesEchoForTest || []) as Source[];
return sources.find(s => s.id === languageId)?.text || '';
}, codegenLang2Id.get(file)), { timeout: 0 }).toContain(text);
const sources: Source[] = await this.recorderPage.evaluate(() => (window as any).playwrightSourcesEchoForTest || []);
for (const source of sources) {
if (!codegenLangId2lang.has(source.id))
throw new Error(`Unknown language: ${source.id}`);
this._sources.set(codegenLangId2lang.get(source.id), source);
}
return this._sources;
}, { box: true });
}
sources(): Map<string, Source> {
return this._sources;
}
async text(file: string): Promise<string> {
const sources: Source[] = await this.recorderPage.evaluate(() => (window as any).playwrightSourcesEchoForTest || []);
for (const source of sources) {
if (codegenLangId2lang.get(source.id) === file)
return source.text;
}
return '';
}
async waitForHighlight(action: () => Promise<void>): Promise<string> {
return await test.step('waitForHighlight', async () => {
await this.page.$$eval('x-pw-highlight', els => els.forEach(e => e.remove()));
await this.page.$$eval('x-pw-tooltip', els => els.forEach(e => e.remove()));
await action();
await this.page.locator('x-pw-highlight').waitFor();
await this.page.locator('x-pw-tooltip').waitFor();
await expect(this.page.locator('x-pw-tooltip')).not.toHaveText('');
await expect(this.page.locator('x-pw-tooltip')).not.toHaveText(`locator('body')`);
return this.page.locator('x-pw-tooltip').textContent();
}, { box: true });
}
async waitForHighlightNoTooltip(action: () => Promise<void>): Promise<string> {
await this.page.$$eval('x-pw-highlight', els => els.forEach(e => e.remove()));
await action();
await this.page.locator('x-pw-highlight').waitFor();
return '';
}
async waitForActionPerformed(): Promise<{ hovered: string | null, active: string | null }> {
let callback;
const listener = async msg => {
const prefix = 'Action performed for test: ';
if (msg.text().startsWith(prefix)) {
this.page.off('console', listener);
const arg = JSON.parse(msg.text().substr(prefix.length));
callback(arg);
}
};
this.page.on('console', listener);
return new Promise(f => callback = f);
}
async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }, omitTooltip?: boolean }): Promise<string> {
return (options?.omitTooltip ? this.waitForHighlightNoTooltip : this.waitForHighlight).call(this, async () => {
const box = await this.page.locator(selector).first().boundingBox();
const offset = options?.position || { x: box.width / 2, y: box.height / 2 };
await this.page.mouse.move(box.x + offset.x, box.y + offset.y);
});
}
async trustedMove(selector: string | Locator) {
const locator = typeof selector === 'string' ? this.page.locator(selector).first() : selector;
const box = await locator.boundingBox();
await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
}
async trustedClick(options?: { button?: 'left' | 'right' | 'middle' }) {
await this.page.mouse.down(options);
await this.page.mouse.up(options);
}
async trustedPress(text: string) {
await this.page.keyboard.press(text);
}
async trustedDblclick() {
await this.page.mouse.down();
await this.page.mouse.up();
await this.page.mouse.down({ clickCount: 2 });
await this.page.mouse.up();
}
async focusElement(selector: string): Promise<string> {
return this.waitForHighlight(() => this.page.focus(selector));
}
}
class CLIMock {
private _process: TestChildProcess;
private _outputFile: string;
private _exitPromise: Promise<void> | undefined;
constructor(childProcess: CommonFixtures['childProcess'], options: { outputFile: string, browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined }) {
this._outputFile = options.outputFile;
const nodeArgs = [
'node',
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
'codegen',
...options.args,
`--browser=${options.browserName}`,
`--output=${this._outputFile}`,
];
if (options.channel)
nodeArgs.push(`--channel=${options.channel}`);
this._process = childProcess({
command: nodeArgs,
env: {
PWTEST_CLI_IS_UNDER_TEST: '1',
PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined,
PWTEST_CLI_EXECUTABLE_PATH: options.executablePath,
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
},
});
}
@step
async waitFor(text: string): Promise<void> {
await expect.poll(() => this.text(), { timeout: 30000 }).toContain(text);
}
@step
async exit() {
if (!this._exitPromise) {
this._process.write('exit\n');
this._exitPromise = this._process.cleanExit();
}
await this._exitPromise;
}
sigint() {
const result = this._process.kill('SIGINT');
this._exitPromise = this._process.exited.then(() => undefined); // Avoid double closing.
return result;
}
async text() {
try {
return await fs.promises.readFile(this._outputFile, 'utf-8');
} catch {
return '';
}
}
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Page } from 'playwright-core';
export async function pauseHelper(page: Page) {
await page.setContent('<div>here we go</div>');
}

View File

@@ -0,0 +1,589 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Page } from 'playwright-core';
import { test as it, expect, Recorder } from './inspectorTest';
import { roundBox, waitForTestLog } from '../../config/utils';
import type { BoundingBox } from '../../config/utils';
import { pauseHelper } from './pause-helper';
it('should resume when closing inspector', async ({ page, recorderPageGetter, closeRecorder, mode }) => {
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
})();
await recorderPageGetter();
await closeRecorder();
await scriptPromise;
});
it('should not reset timeouts', async ({ page, recorderPageGetter, closeRecorder, server }) => {
page.context().setDefaultNavigationTimeout(1000);
page.context().setDefaultTimeout(1000);
// @ts-ignore
const pausePromise = page.pause({ __testHookKeepTestTimeout: true });
await recorderPageGetter();
await closeRecorder();
await pausePromise;
server.setRoute('/empty.html', () => {});
const error = await page.goto(server.EMPTY_PAGE).catch(e => e);
expect(error.message).toContain('page.goto: Timeout 1000ms exceeded.');
});
it('should collapse log entries to a single line', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.keyboard.type(`Hello
world`);
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume (F8)"]');
await expect(recorderPage.locator('.call-log-call').nth(1)).toContainText('Type "Hello\\nworld"');
await scriptPromise;
});
it.describe('pause', () => {
it.afterEach(async ({ recorderPageGetter }, testInfo) => {
if (testInfo.status === 'skipped')
return;
try {
const recorderPage = await recorderPageGetter();
recorderPage.click('[title="Resume (F8)"]').catch(() => {});
} catch (e) {
// Some tests close context.
}
});
it('should pause and resume the script', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should pause and resume the script with keyboard shortcut', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
})();
const recorderPage = await recorderPageGetter();
await expect(recorderPage.getByRole('button', { name: 'Resume' })).toBeEnabled();
await recorderPage.keyboard.press('F8');
await scriptPromise;
});
it('should resume from console', async ({ page, mode }) => {
it.skip(mode !== 'default');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
})();
await page.waitForFunction(() => (window as any).playwright && (window as any).playwright.resume() !== false);
await scriptPromise;
});
it('should pause after a navigation', async ({ page, server, recorderPageGetter }) => {
const scriptPromise = (async () => {
await page.goto(server.EMPTY_PAGE);
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should show source', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await pauseHelper(page);
})();
const recorderPage = await recorderPageGetter();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause\.spec\.ts/);
await expect(recorderPage.locator('.source-line-paused')).toContainText('page.pause({ __testHookKeepTestTimeout: true })');
await recorderPage.click('[title="Step over (F10)"]');
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause-helper\.ts/);
await expect(recorderPage.locator('.source-line-paused')).toContainText('page.setContent(\'<div>here we go</div>\')');
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should pause on next pause', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true }); // 1
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true }); // 2
})();
const recorderPage = await recorderPageGetter();
const source = await recorderPage.textContent('.source-line-paused');
expect(source).toContain('page.pause({ __testHookKeepTestTimeout: true }); // 1');
await recorderPage.click('[title="Resume (F8)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")');
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should step', async ({ page, recorderPageGetter }) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.click('button');
})();
const recorderPage = await recorderPageGetter();
const source = await recorderPage.textContent('.source-line-paused');
expect(source).toContain('page.pause({ __testHookKeepTestTimeout: true });');
await recorderPage.click('[title="Step over (F10)"]');
await recorderPage.waitForSelector('.source-line-paused :has-text("page.click")');
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should disable timeout on paused actions', async ({ page, recorderPageGetter }) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.click('button', { timeout: 1000 });
})();
const recorderPage = await recorderPageGetter();
const source = await recorderPage.textContent('.source-line-paused');
expect(source).toContain('page.pause({ __testHookKeepTestTimeout: true });');
await recorderPage.click('[title="Step over (F10)"]');
await recorderPage.waitForSelector('.source-line-paused :has-text("page.click")');
await page.waitForTimeout(5000);
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should step with keyboard shortcut', async ({ page, recorderPageGetter }) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.click('button');
})();
const recorderPage = await recorderPageGetter();
const source = await recorderPage.textContent('.source-line-paused');
expect(source).toContain('page.pause({ __testHookKeepTestTimeout: true });');
await recorderPage.keyboard.press('F10');
await recorderPage.waitForSelector('.source-line-paused :has-text("page.click")');
await recorderPage.isEnabled('[title="Resume (F8)"]');
await recorderPage.keyboard.press('F8');
await scriptPromise;
});
it('should highlight pointer, only in main frame', async ({ page, recorderPageGetter }) => {
await page.setContent(`
<iframe
style="margin: 100px;"
srcdoc="<button style='margin: 80px;'>Submit</button>">
</iframe>
`);
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.frameLocator('iframe').locator('button').click();
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Step over (F10)"]');
const { box1, box2 } = await (page as any)._wrapApiCall(async () => {
const iframe = page.frames()[1];
const button = await iframe.waitForSelector('button');
const box1 = await button.boundingBox();
const actionPoint = await page.waitForSelector('x-pw-action-point');
const box2 = await actionPoint.boundingBox();
const iframeActionPoint = await iframe.$('x-pw-action-point');
expect(await iframeActionPoint?.isVisible()).toBeFalsy();
return { box1, box2 };
}, { internal: true });
await recorderPage.click('[title="Resume (F8)"]');
const x1 = box1!.x + box1!.width / 2;
const y1 = box1!.y + box1!.height / 2;
const x2 = box2!.x + box2!.width / 2;
const y2 = box2!.y + box2!.height / 2;
expect(Math.abs(x1 - x2) < 2).toBeTruthy();
expect(Math.abs(y1 - y2) < 2).toBeTruthy();
await scriptPromise;
});
it('should skip input when resuming', async ({ page, recorderPageGetter }) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.click('button');
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true }); // 2
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume (F8)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")');
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should populate log', async ({ page, recorderPageGetter }) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.click('button');
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true }); // 2
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume (F8)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
'Click(page.locator(\'button\'))- XXms',
'Pause',
]);
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should hide internal calls', async ({ page, recorderPageGetter, trace }) => {
it.skip(trace === 'on');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.context().tracing.start();
page.setDefaultTimeout(0);
page.context().setDefaultNavigationTimeout(0);
await page.context().tracing.stop();
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true }); // 2
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume (F8)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
'Start tracing- XXms',
'Stop tracing- XXms',
'Pause',
]);
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should show expect.toHaveText', async ({ page, recorderPageGetter }) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await expect(page.locator('button')).toHaveText('Submit');
await expect(page.locator('button')).not.toHaveText('Submit2');
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true }); // 2
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume (F8)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
'Expect "toHaveText"(page.locator(\'button\'))- XXms',
'Expect "not toHaveText"(page.locator(\'button\'))- XXms',
'Pause',
]);
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should highlight waitForEvent', async ({ page, recorderPageGetter }) => {
await page.setContent('<button onclick="console.log(1)">Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await Promise.all([
page.waitForEvent('console', msg => msg.type() === 'log' && msg.text() === '1'),
page.click('button'),
]);
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Step over (F10)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.click")');
await recorderPage.waitForSelector('.source-line-running:has-text("page.waitForEvent")');
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should populate log with waitForEvent', async ({ page, recorderPageGetter }) => {
await page.setContent('<button onclick="console.log(1)">Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await Promise.all([
page.waitForEvent('console'),
page.getByRole('button', { name: 'Submit' }).click(),
]);
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true }); // 2
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume (F8)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
'Wait for event "console"- XXms',
'Click(page.getByRole(\'button\', { name: \'Submit\' }))- XXms',
'Pause',
]);
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should populate log with error', async ({ page, recorderPageGetter }) => {
await page.setContent('<button onclick="console.log(1)">Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.getByRole('button').isChecked();
})().catch(e => e);
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume (F8)"]');
await recorderPage.waitForSelector('.source-line-error-underline');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
'Is checked(page.getByRole(\'button\'))- XXms',
'waiting for getByRole(\'button\')',
'error: Error: Not a checkbox or radio button',
]);
const error = await scriptPromise;
expect(error.message).toContain('Not a checkbox or radio button');
});
it('should populate log with error in waitForEvent', async ({ page, recorderPageGetter }) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await Promise.all([
page.waitForEvent('console', { timeout: 1 }).catch(() => {}),
// @ts-ignore
page.pause({ __testHookKeepTestTimeout: true }),
]);
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Step over (F10)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause")');
await recorderPage.waitForSelector('.source-line-error:has-text("page.waitForEvent")');
expect(await sanitizeLog(recorderPage)).toEqual([
'Pause- XXms',
'Wait for event "console"- XXms',
'waiting for event "console"',
'error: Timeout 1ms exceeded while waiting for event "console"',
'Pause',
]);
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should pause on page close', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.close();
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Step over (F10)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.close();")');
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should pause on context close', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.context().close();
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Step over (F10)"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.context().close();")');
// Next line can throw because closing context also closes the inspector page.
await recorderPage.click('[title="Resume (F8)"]').catch(e => {});
await scriptPromise;
});
it('should highlight on explore', async ({ page, recorderPageGetter }) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
})();
const recorderPage = await recorderPageGetter();
const box1Promise = waitForTestLog<BoundingBox>(page, 'Highlight box for test: ');
await recorderPage.getByText('Locator', { exact: true }).click();
await recorderPage.locator('.tabbed-pane .CodeMirror').click();
await recorderPage.keyboard.press('ControlOrMeta+A');
await recorderPage.keyboard.press('Backspace');
await recorderPage.keyboard.type('getByText(\'Submit\')');
const box1 = await box1Promise;
const button = await page.$('text=Submit');
const box2 = await button!.boundingBox();
expect(roundBox(box1)).toEqual(roundBox(box2!));
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should highlight on explore (csharp)', async ({ openRecorder }) => {
process.env.TEST_INSPECTOR_LANGUAGE = 'csharp';
try {
const { page, recorder } = await openRecorder();
await page.setContent('<button>Submit</button>');
const box1Promise = waitForTestLog<BoundingBox>(page, 'Highlight box for test: ');
await recorder.recorderPage.getByText('Locator', { exact: true }).click();
await recorder.recorderPage.locator('.tabbed-pane .CodeMirror').click();
await recorder.recorderPage.keyboard.press('ControlOrMeta+A');
await recorder.recorderPage.keyboard.press('Backspace');
await recorder.recorderPage.keyboard.type('GetByText("Submit")');
const box1 = await box1Promise;
const button = await page.$('text=Submit');
const box2 = await button.boundingBox();
expect(roundBox(box1)).toEqual(roundBox(box2));
} finally {
delete process.env.TEST_INSPECTOR_LANGUAGE;
}
});
it('should not prevent key events', async ({ page, recorderPageGetter }) => {
await page.setContent('<div>Hello</div>');
await page.evaluate(() => {
(window as any).log = [];
for (const event of ['keydown', 'keyup', 'keypress'])
window.addEventListener(event, e => (window as any).log.push(e.type));
});
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
await page.keyboard.press('Enter');
await page.keyboard.press('A');
await page.keyboard.press('Shift+A');
})();
const recorderPage = await recorderPageGetter();
await recorderPage.waitForSelector(`.source-line-paused:has-text("page.pause")`);
await recorderPage.click('[title="Step over (F10)"]');
await recorderPage.waitForSelector(`.source-line-paused:has-text("press('Enter')")`);
await recorderPage.click('[title="Step over (F10)"]');
await recorderPage.waitForSelector(`.source-line-paused:has-text("press('A')")`);
await recorderPage.click('[title="Step over (F10)"]');
await recorderPage.waitForSelector(`.source-line-paused:has-text("press('Shift+A')")`);
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
const log = await page.evaluate(() => (window as any).log);
expect(log).toEqual([
'keydown',
'keypress',
'keyup',
'keydown',
'keypress',
'keyup',
'keydown',
'keydown',
'keypress',
'keyup',
'keyup',
]);
});
it('should highlight locators with custom testId', async ({ page, playwright, recorderPageGetter }) => {
await page.setContent('<div data-custom-id=foo id=target>and me</div>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
playwright.selectors.setTestIdAttribute('data-custom-id');
await page.getByTestId('foo').click();
})();
const recorderPage = await recorderPageGetter();
const box1Promise = waitForTestLog<BoundingBox>(page, 'Highlight box for test: ');
await recorderPage.click('[title="Step over (F10)"]');
const box2 = roundBox((await page.locator('#target').boundingBox())!);
const box1 = roundBox(await box1Promise);
expect(box1).toEqual(box2);
await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise;
});
it('should record from debugger', async ({ page, recorderPageGetter }) => {
await page.setContent('<body style="width: 100%; height: 100%"></body>');
const scriptPromise = (async () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
})();
const recorderPage = await recorderPageGetter();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause\.spec\.ts/);
await expect(recorderPage.locator('.source-line-paused')).toHaveText(/await page\.pause\(.*\)/);
await recorderPage.getByRole('button', { name: 'Record' }).click();
const recorder = new Recorder(page, recorderPage);
await recorder.hoverOverElement('body', { omitTooltip: true });
await recorder.trustedClick();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue('playwright-test');
await expect(recorderPage.locator('.cm-wrapper')).toContainText(`await page.locator('body').click();`);
await recorderPage.getByRole('button', { name: 'Resume' }).click();
await scriptPromise;
});
});
async function sanitizeLog(recorderPage: Page): Promise<string[]> {
const results = [];
for (const entry of await recorderPage.$$('.call-log-call')) {
const header = (await (await entry.$('.call-log-call-header'))!.textContent())!.replace(/— [\d.]+(ms|s)/, '- XXms');
results.push(header.replace(/page\.waitForEvent\(console\).*/, 'page.waitForEvent(console)'));
results.push(...await entry.$$eval('.call-log-message', ee => ee.map(e => {
return (e.classList.contains('error') ? 'error: ' : '') + e.textContent;
})));
}
return results;
}

View File

@@ -0,0 +1,192 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './inspectorTest';
import type { Page } from '@playwright/test';
import type * as actions from '@recorder/actions';
class RecorderLog {
actions: (actions.ActionInContext & { code: string })[] = [];
actionAdded(page: Page, actionInContext: actions.ActionInContext, code: string): void {
this.actions.push({ ...actionInContext, code });
}
actionUpdated(page: Page, actionInContext: actions.ActionInContext, code: string): void {
this.actions[this.actions.length - 1] = { ...actionInContext, code };
}
}
async function startRecording(context) {
const log = new RecorderLog();
await (context as any)._enableRecorder({
mode: 'recording',
recorderMode: 'api',
}, log);
return {
action: (name: string) => log.actions.filter(a => a.action.name === name),
};
}
function normalizeCode(code: string): string {
return code.replace(/\s+/g, ' ').trim();
}
test('should click', async ({ context, browserName, platform, channel }) => {
const log = await startRecording(context);
const page = await context.newPage();
await page.setContent(`<button onclick="console.log('click')">Submit</button>`);
await page.getByRole('button', { name: 'Submit' }).click();
const clickActions = log.action('click');
expect(clickActions).toEqual([
expect.objectContaining({
action: expect.objectContaining({
name: 'click',
selector: 'internal:role=button[name="Submit"i]',
ref: 'e2',
// Safari does not focus after a click: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#clicking_and_focus
ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || (platform === 'win32' && channel !== 'webkit-wsl'))) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]',
}),
startTime: expect.any(Number),
})
]);
expect(normalizeCode(clickActions[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).click();`);
});
test('should double click', async ({ context, browserName, platform, channel }) => {
const log = await startRecording(context);
const page = await context.newPage();
await page.setContent(`<button onclick="console.log('click')" ondblclick="console.log('dblclick')">Submit</button>`);
await page.getByRole('button', { name: 'Submit' }).dblclick();
const clickActions = log.action('click');
expect(clickActions).toEqual([
expect.objectContaining({
action: expect.objectContaining({
name: 'click',
clickCount: 2,
selector: 'internal:role=button[name="Submit"i]',
ref: 'e2',
// Safari does not focus after a click: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#clicking_and_focus
ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || (platform === 'win32' && channel !== 'webkit-wsl'))) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]',
}),
startTime: expect.any(Number),
})
]);
expect(normalizeCode(clickActions[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).dblclick();`);
});
test('should right click', async ({ context, browserName, platform, channel }) => {
const log = await startRecording(context);
const page = await context.newPage();
await page.setContent(`<button oncontextmenu="console.log('contextmenu')">Submit</button>`);
await page.getByRole('button', { name: 'Submit' }).click({ button: 'right' });
const clickActions = log.action('click');
expect(clickActions).toEqual([
expect.objectContaining({
action: expect.objectContaining({
name: 'click',
button: 'right',
selector: 'internal:role=button[name="Submit"i]',
ref: 'e2',
// Safari does not focus after a click: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#clicking_and_focus
ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || (platform === 'win32' && channel !== 'webkit-wsl'))) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]',
}),
startTime: expect.any(Number),
})
]);
expect(normalizeCode(clickActions[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).click({ button: 'right' });`);
});
test('should type', async ({ context }) => {
const log = await startRecording(context);
const page = await context.newPage();
await page.setContent(`<input type="text" />`);
await page.getByRole('textbox').pressSequentially('Hello');
const fillActions = log.action('fill');
expect(fillActions).toEqual([
expect.objectContaining({
action: expect.objectContaining({
name: 'fill',
selector: 'internal:role=textbox',
ref: 'e2',
ariaSnapshot: '- textbox [active] [ref=e2]: Hello',
}),
startTime: expect.any(Number),
})
]);
expect(normalizeCode(fillActions[0].code)).toEqual(`await page.getByRole('textbox').fill('Hello');`);
});
test('should disable recorder', async ({ context }) => {
const log = await startRecording(context);
const page = await context.newPage();
await page.setContent(`<button onclick="console.log('click')">Submit</button>`);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('button', { name: 'Submit' }).click();
expect(log.action('click')).toHaveLength(2);
await (context as any)._disableRecorder();
await page.getByRole('button', { name: 'Submit' }).click();
expect(log.action('click')).toHaveLength(2);
});
test('page.pickLocator should return locator for picked element', async ({ page }) => {
await page.setContent(`<button>Submit</button>`);
const scriptReady = page.waitForEvent('console', msg => msg.text() === 'Recorder script ready for test');
const pickPromise = page.pickLocator();
await scriptReady;
const box = await page.getByRole('button', { name: 'Submit' }).boundingBox();
await page.mouse.click(box!.x + box!.width / 2, box!.y + box!.height / 2);
const locator = await pickPromise;
await expect(locator).toHaveText('Submit');
});
test('page.cancelPickLocator should cancel ongoing pickLocator', async ({ page }) => {
const pickPromise = page.pickLocator();
await Promise.all([
page.cancelPickLocator(),
expect(pickPromise).rejects.toThrow('Locator picking was cancelled')
]);
});
test('closing page should cancel ongoing pickLocator', async ({ page }) => {
await page.setContent(`<button>Click me</button>`);
const pickPromise = page.pickLocator().catch(e => e.message);
await page.close();
expect(await pickPromise).toContain('Target page, context or browser has been closed');
});
test('page2.pickLocator() should cancel page1.pickLocator()', async ({ page, context, browserName, headless, isMac, macVersion }) => {
test.fixme(browserName === 'chromium' && !headless && isMac && macVersion === 14, 'times out on chromium headed on macOS 14');
const pick1Promise = page.pickLocator().catch(e => e.message);
const page2 = await context.newPage();
page2.pickLocator().catch(() => {});
expect(await pick1Promise).toContain('Locator picking was cancelled');
});

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './inspectorTest';
test('should reflect formatted URL of the page', async ({
openRecorder,
server,
}) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait('');
await expect(recorder.recorderPage).toHaveTitle(
'Playwright Inspector - about:blank',
);
await recorder.setContentAndWait('', server.EMPTY_PAGE);
await expect(recorder.recorderPage).toHaveTitle(
`Playwright Inspector - ${server.EMPTY_PAGE}`,
);
});
test('should update primary page URL when original primary closes', async ({
context,
openRecorder,
server,
}) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(
'',
`${server.PREFIX}/background-color.html`,
);
await expect(recorder.recorderPage).toHaveTitle(
`Playwright Inspector - ${server.PREFIX}/background-color.html`,
);
const page2 = await context.newPage();
await page2.goto(`${server.PREFIX}/dom.html`);
await expect(recorder.recorderPage).toHaveTitle(
`Playwright Inspector - ${server.PREFIX}/background-color.html`,
);
await context.pages()[0].close();
await expect(recorder.recorderPage).toHaveTitle(
`Playwright Inspector - ${server.PREFIX}/dom.html`,
);
});
test('should render primary language', async ({ openRecorder }) => {
const { recorder } = await openRecorder({ language: 'python' });
await recorder.setContentAndWait('');
await expect(recorder.recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue('python');
});