fix(inspector): highlight xpath/css locators without engine prefix (#27742)

Motivation: As of today when a user inspects a Locator which is a xpath,
it won't work if the user has not prefixed it with `xpath=` because we
internally compare the given with the generated locator.

Works: `locator('xpath=//div[contains(@class, "foo")]')`
Does not work: `locator('//div[contains(@class, "foo")]')`

Relates
https://github.com/microsoft/playwright/issues/27707#issue-1952360264
Fixes
https://github.com/microsoft/playwright-dotnet/issues/2718#issuecomment-1771073816

---------

Signed-off-by: Max Schmitt <max@schmitt.mx>
This commit is contained in:
Max Schmitt 2023-10-23 18:23:28 +02:00 committed by GitHub
parent 9af667be26
commit f48861ddee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 43 additions and 8 deletions

View File

@ -204,7 +204,14 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
}
}
tokens.push([locatorPart]);
// Selectors can be prefixed with engine name, e.g. xpath=//foo
let locatorPartWithEngine: string | undefined;
if (['xpath', 'css'].includes(part.name)) {
const selectorPart = stringifySelector({ parts: [part] }, /* forceEngineName */ true);
locatorPartWithEngine = factory.generateLocator(base, locatorType, selectorPart);
}
tokens.push([locatorPart, locatorPartWithEngine].filter(Boolean) as string[]);
}
return combineTokens(factory, tokens, maxOutputSize);

View File

@ -123,11 +123,18 @@ function selectorPartsEqual(list1: ParsedSelectorPart[], list2: ParsedSelectorPa
return stringifySelector({ parts: list1 }) === stringifySelector({ parts: list2 });
}
export function stringifySelector(selector: string | ParsedSelector): string {
export function stringifySelector(selector: string | ParsedSelector, forceEngineName?: boolean): string {
if (typeof selector === 'string')
return selector;
return selector.parts.map((p, i) => {
const prefix = p.name === 'css' ? '' : p.name + '=';
let includeEngine = true;
if (!forceEngineName && i !== selector.capture) {
if (p.name === 'css')
includeEngine = false;
else if (p.name === 'xpath' && p.source.startsWith('//') || p.source.startsWith('..'))
includeEngine = false;
}
const prefix = includeEngine ? p.name + '=' : '';
return `${i === selector.capture ? '*' : ''}${prefix}${p.source}`;
}).join(' >> ');
}

View File

@ -503,10 +503,20 @@ it('asLocator internal:chain', async () => {
it('asLocator xpath', async () => {
const selector = `//*[contains(normalizer-text(), 'foo']`;
expect.soft(asLocator('javascript', selector, false)).toBe(`locator('xpath=//*[contains(normalizer-text(), \\'foo\\']')`);
expect.soft(asLocator('python', selector, false)).toBe(`locator(\"xpath=//*[contains(normalizer-text(), 'foo']\")`);
expect.soft(asLocator('java', selector, false)).toBe(`locator(\"xpath=//*[contains(normalizer-text(), 'foo']\")`);
expect.soft(asLocator('csharp', selector, false)).toBe(`Locator(\"xpath=//*[contains(normalizer-text(), 'foo']\")`);
expect.soft(asLocator('javascript', selector, false)).toBe(`locator('//*[contains(normalizer-text(), \\'foo\\']')`);
expect.soft(asLocator('python', selector, false)).toBe(`locator(\"//*[contains(normalizer-text(), 'foo']\")`);
expect.soft(asLocator('java', selector, false)).toBe(`locator(\"//*[contains(normalizer-text(), 'foo']\")`);
expect.soft(asLocator('csharp', selector, false)).toBe(`Locator(\"//*[contains(normalizer-text(), 'foo']\")`);
expect.soft(parseLocator('javascript', `locator('//*[contains(normalizer-text(), \\'foo\\']')`, 'data-testid')).toBe("//*[contains(normalizer-text(), 'foo']");
expect.soft(parseLocator('javascript', `locator("//*[contains(normalizer-text(), 'foo']")`, 'data-testid')).toBe("//*[contains(normalizer-text(), 'foo']");
expect.soft(parseLocator('javascript', `locator('xpath=//*[contains(normalizer-text(), \\'foo\\']')`, 'data-testid')).toBe("xpath=//*[contains(normalizer-text(), 'foo']");
expect.soft(parseLocator('javascript', `locator("xpath=//*[contains(normalizer-text(), 'foo']")`, 'data-testid')).toBe("xpath=//*[contains(normalizer-text(), 'foo']");
expect.soft(parseLocator('python', `locator("//*[contains(normalizer-text(), 'foo']")`, 'data-testid')).toBe("//*[contains(normalizer-text(), 'foo']");
expect.soft(parseLocator('python', `locator("xpath=//*[contains(normalizer-text(), 'foo']")`, 'data-testid')).toBe("xpath=//*[contains(normalizer-text(), 'foo']");
expect.soft(parseLocator('java', `locator("//*[contains(normalizer-text(), 'foo']")`, 'data-testid')).toBe("//*[contains(normalizer-text(), 'foo']");
expect.soft(parseLocator('java', `locator("xpath=//*[contains(normalizer-text(), 'foo']")`, 'data-testid')).toBe("xpath=//*[contains(normalizer-text(), 'foo']");
expect.soft(parseLocator('csharp', `Locator("//*[contains(normalizer-text(), 'foo']")`, 'data-testid')).toBe("//*[contains(normalizer-text(), 'foo']");
expect.soft(parseLocator('csharp', `Locator("xpath=//*[contains(normalizer-text(), 'foo']")`, 'data-testid')).toBe("xpath=//*[contains(normalizer-text(), 'foo']");
});
it('parseLocator quotes', async () => {
@ -521,6 +531,17 @@ it('parseLocator quotes', async () => {
expect.soft(parseLocator('csharp', `Locator('text="bar"')`, '')).toBe(``);
});
it('parseLocator css', async () => {
expect.soft(parseLocator('javascript', `locator('.foo')`, '')).toBe(`.foo`);
expect.soft(parseLocator('javascript', `locator('css=.foo')`, '')).toBe(`css=.foo`);
expect.soft(parseLocator('python', `locator(".foo")`, '')).toBe(`.foo`);
expect.soft(parseLocator('python', `locator("css=.foo")`, '')).toBe(`css=.foo`);
expect.soft(parseLocator('java', `locator(".foo")`, '')).toBe(`.foo`);
expect.soft(parseLocator('java', `locator("css=.foo")`, '')).toBe(`css=.foo`);
expect.soft(parseLocator('csharp', `Locator(".foo")`, '')).toBe(`.foo`);
expect.soft(parseLocator('csharp', `Locator("css=.foo")`, '')).toBe(`css=.foo`);
});
it('parse locators strictly', () => {
const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span';

View File

@ -233,7 +233,7 @@ it('should respect timeout xpath', async ({ page, playwright }) => {
await page.waitForSelector('//div', { state: 'attached', timeout: 3000 }).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('page.waitForSelector: Timeout 3000ms exceeded');
expect(error.message).toContain('waiting for locator(\'xpath=//div\')');
expect(error.message).toContain('waiting for locator(\'//div\')');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});