Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: best-effort support for POMs when highlighting #378

Merged
merged 3 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/debugHighlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ import * as vscodeTypes from './vscodeTypes';

export type DebuggerError = { error: string, location: Location };

const debugSessions = new Map<string, vscodeTypes.DebugSession>();

export class DebugHighlight {
private _debugSessions = new Map<string, vscodeTypes.DebugSession>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: reason for moving it out was that the tests were not isolated before, the _debugSession struct leaked.

private _errorInDebugger: vscodeTypes.EventEmitter<DebuggerError>;
readonly onErrorInDebugger: vscodeTypes.Event<DebuggerError>;
private _disposables: vscodeTypes.Disposable[] = [];
Expand All @@ -40,10 +39,10 @@ export class DebugHighlight {
this._disposables = [
vscode.debug.onDidStartDebugSession(session => {
if (isPlaywrightSession(session))
debugSessions.set(session.id, session);
this._debugSessions.set(session.id, session);
}),
vscode.debug.onDidTerminateDebugSession(session => {
debugSessions.delete(session.id);
this._debugSessions.delete(session.id);
self._hideHighlight();
}),
vscode.languages.registerHoverProvider('typescript', {
Expand Down Expand Up @@ -105,7 +104,7 @@ export class DebugHighlight {
private async _highlightLocator(document: vscodeTypes.TextDocument, position: vscodeTypes.Position, token?: vscodeTypes.CancellationToken) {
if (!this._reusedBrowser.pageCount())
return;
const result = await locatorToHighlight(document, position, token);
const result = await locatorToHighlight(this._debugSessions, document, position, token);
if (result)
this._reusedBrowser.highlight(result);
else
Expand All @@ -132,7 +131,7 @@ export type StackFrame = {

const sessionsWithHighlight = new Set<vscodeTypes.DebugSession>();

async function locatorToHighlight(document: vscodeTypes.TextDocument, position: vscodeTypes.Position, token?: vscodeTypes.CancellationToken): Promise<string | undefined> {
async function locatorToHighlight(debugSessions: Map<string, vscodeTypes.DebugSession>, document: vscodeTypes.TextDocument, position: vscodeTypes.Position, token?: vscodeTypes.CancellationToken): Promise<string | undefined> {
const fsPath = document.uri.fsPath;

if (!debugSessions.size) {
Expand All @@ -147,6 +146,8 @@ async function locatorToHighlight(document: vscodeTypes.TextDocument, position:
});
// Translate locator expressions starting with "component." to be starting with "page.".
locatorExpression = locatorExpression?.replace(/^component\s*\./, `page.locator('#root').locator('internal:control=component').`);
// Translate 'this.page', or 'this._page' to 'page' to have best-effort support for POMs.
locatorExpression = locatorExpression?.replace(/this\._?page\s*\./, 'page.');
// Only consider locator expressions starting with "page." because we know the base for them (root).
// Other locators can be relative.
const match = locatorExpression?.match(/^page\s*\.([\s\S]*)/m);
Expand Down
93 changes: 93 additions & 0 deletions tests/highlight-locators.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* 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 { chromium } from '@playwright/test';
import { expect, test } from './utils';

test.beforeEach(({ mode }) => {
// Locator highlighting is only relevant when the browser stays open.
test.skip(mode !== 'reuse');
// the x-pw-highlight element has otherwise a closed shadow root.
process.env.PWTEST_UNDER_TEST = '1';
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
});

test('should work', async ({ activate }) => {
const cdpPort = 9234 + test.info().workerIndex * 2;
const { vscode, testController } = await activate({
'playwright.config.js': `module.exports = {
use: {
launchOptions: {
args: ['--remote-debugging-port=${cdpPort}']
}
}
}`,
'test.spec.ts': `
import { test } from '@playwright/test';
test('one', async ({ page }) => {
await page.goto('https://example.com');
await page.setContent(\`
<button>one</button>
<button>two</button>
\`);
await page.getByRole('button', { name: 'one' }).click(); // line 8
await page.getByRole('button', { name: 'two' }).click(); // line 9
page.getByRole('button', { name: 'not there!' }); // line 10
});

class MyPom {
constructor(page) {
this.myElementOne1 = page.getByRole('button', { name: 'one' }); // line 15
this.myElementTwo1 = this._page.getByRole('button', { name: 'two' }); // line 16
this.myElementOne2 = this.page.getByRole('button', { name: 'one' }); // line 17
}
}
`,
});

const testItems = testController.findTestItems(/test.spec.ts/);
expect(testItems.length).toBe(1);
await vscode.openEditors('test.spec.ts');
await testController.run(testItems);
const browser = await chromium.connectOverCDP(`http://localhost:${cdpPort}`);
{
expect(browser.contexts()).toHaveLength(1);
expect(browser.contexts()[0].pages()).toHaveLength(1);
}
const page = browser.contexts()[0].pages()[0];

for (const language of ['javascript', 'typescript']) {
for (const [[line, column], expectedLocator] of [
[[9, 26], page.getByRole('button', { name: 'two' })],
[[8, 26], page.getByRole('button', { name: 'one' })],
[[10, 26], null],
[[15, 30], page.getByRole('button', { name: 'one' })],
[[16, 30], page.getByRole('button', { name: 'two' })],
[[17, 30], page.getByRole('button', { name: 'one' })],
] as const) {
await test.step(`should highlight ${language} ${line}:${column}`, async () => {
vscode.languages.emitHoverEvent(language, vscode.window.activeTextEditor.document, new vscode.Position(line, column));
await expect(async () => {
if (!expectedLocator)
await expect(page.locator('x-pw-highlight')).toBeHidden();
else
expect(await page.locator('x-pw-highlight').boundingBox()).toEqual(await expectedLocator.boundingBox());
}).toPass();
});
}
}
await browser.close();
});
16 changes: 15 additions & 1 deletion tests/mock/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,10 @@ enum UIKind {
Web = 2
}

type HoverProvider = {
provideHover?(document: Document, position: Position, token: CancellationToken): void
};

export class VSCode {
isUnderTest = true;
CancellationTokenSource = CancellationTokenSource;
Expand Down Expand Up @@ -747,6 +751,7 @@ export class VSCode {
readonly commandLog: string[] = [];
readonly l10n = new L10n();
lastWithProgressData = undefined;
private _hoverProviders: Map<string, HoverProvider> = new Map();

constructor(baseDir: string, browser: Browser) {
this.context = { subscriptions: [], extensionUri: Uri.file(baseDir) };
Expand All @@ -764,7 +769,16 @@ export class VSCode {
this.debug = new Debug();

const diagnosticsCollections: DiagnosticsCollection[] = [];
this.languages.registerHoverProvider = () => disposable;
this.languages.registerHoverProvider = (language: string, provider: HoverProvider) => {
this._hoverProviders.set(language, provider);
return disposable;
};
this.languages.emitHoverEvent = (language: string, document: Document, position: Position, token: CancellationToken) => {
const provider = this._hoverProviders.get(language);
if (!provider)
return;
provider.provideHover?.(document, position, token);
};
this.languages.getDiagnostics = () => {
const result: Diagnostic[] = [];
for (const collection of diagnosticsCollections) {
Expand Down