Skip to content
Open
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
40 changes: 28 additions & 12 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import { LaunchConfigManager, providedHeadlessDebugConfig, providedLaunchDevToolsConfig } from './launchConfigManager';
import { ErrorReporter } from './errorReporter';
import { ErrorCodes } from './common/errorCodes';
import { shouldSuppressWebhintDiagnostic } from './webhintDiagnostics';
import type {
LanguageClientOptions,
ServerOptions,
Expand Down Expand Up @@ -249,13 +250,13 @@ export function activate(context: vscode.ExtensionContext): void {
void setCSSMirrorContentEnabled(context, !cssMirrorContent);
}));

context.subscriptions.push(vscode.commands.registerCommand(`${SETTINGS_VIEW_NAME}.launchHtml`, async (fileUri: vscode.Uri): Promise<void> => {
context.subscriptions.push(vscode.commands.registerCommand(`${SETTINGS_VIEW_NAME}.launchHtml`, async (fileUri?: vscode.Uri): Promise<void> => {
telemetryReporter.sendTelemetryEvent('contextMenu/launchHtml');
await launchHtml(fileUri);
}));


context.subscriptions.push(vscode.commands.registerCommand(`${SETTINGS_VIEW_NAME}.launchScreencast`, async (fileUri: vscode.Uri): Promise<void> => {
context.subscriptions.push(vscode.commands.registerCommand(`${SETTINGS_VIEW_NAME}.launchScreencast`, async (fileUri?: vscode.Uri): Promise<void> => {
telemetryReporter.sendTelemetryEvent('contextMenu/launchScreencast');
await launchScreencast(context, fileUri);
}));
Expand All @@ -280,36 +281,47 @@ export function activate(context: vscode.ExtensionContext): void {
});
}

export async function launchHtml(fileUri: vscode.Uri): Promise<void> {
export async function launchHtml(fileUri?: vscode.Uri): Promise<void> {
const edgeDebugConfig = providedHeadlessDebugConfig;
const devToolsAttachConfig = providedLaunchDevToolsConfig;
const { port, userDataDir, defaultUrl } = getRemoteEndpointSettings();
if (!vscode.env.remoteName) {
edgeDebugConfig.url = `file://${fileUri.fsPath}`;
devToolsAttachConfig.url = `file://${fileUri.fsPath}`;
const url = fileUri ? fileUri.toString(true) : defaultUrl;
edgeDebugConfig.url = url;
devToolsAttachConfig.url = url;
void vscode.debug.startDebugging(undefined, edgeDebugConfig).then(() => vscode.debug.startDebugging(undefined, devToolsAttachConfig));
} else {
} else if (fileUri) {
// Parse the filename from the remoteName, file authority and path e.g. file://wsl.localhost/ubuntu-20.04/test/index.html
const url = `file://${vscode.env.remoteName}.localhost/${fileUri.authority.split('+')[1]}/${fileUri.fsPath.replace(/\\/g, '/')}`;
edgeDebugConfig.url = url;
devToolsAttachConfig.url = url;
const { port, userDataDir } = getRemoteEndpointSettings();
const browserPath = await getBrowserPath();
await launchBrowser(browserPath, port, url, userDataDir, /** headless */ true).then(() => vscode.debug.startDebugging(undefined, devToolsAttachConfig));
} else {
edgeDebugConfig.url = defaultUrl;
devToolsAttachConfig.url = defaultUrl;
const browserPath = await getBrowserPath();
await launchBrowser(browserPath, port, defaultUrl, userDataDir, /** headless */ true).then(() => vscode.debug.startDebugging(undefined, devToolsAttachConfig));
}
}

export async function launchScreencast(context: vscode.ExtensionContext, fileUri: vscode.Uri): Promise<void> {
export async function launchScreencast(context: vscode.ExtensionContext, fileUri?: vscode.Uri): Promise<void> {
const edgeDebugConfig = providedHeadlessDebugConfig;
const { port, userDataDir, defaultUrl } = getRemoteEndpointSettings();
if (!vscode.env.remoteName) {
edgeDebugConfig.url = `file://${fileUri.fsPath}`;
void vscode.debug.startDebugging(undefined, edgeDebugConfig).then(() => attach(context, fileUri.fsPath, undefined, true, true));
} else {
const url = fileUri ? fileUri.toString(true) : defaultUrl;
edgeDebugConfig.url = url;
void vscode.debug.startDebugging(undefined, edgeDebugConfig).then(() => attach(context, url, undefined, true, true));
} else if (fileUri) {
// Parse the filename from the remoteName, file authority and path e.g. file://wsl.localhost/ubuntu-20.04/test/index.html
const url = `file://${vscode.env.remoteName}.localhost/${fileUri.authority.split('+')[1]}/${fileUri.fsPath.replace(/\\/g, '/')}`;
edgeDebugConfig.url = url;
const { port, userDataDir } = getRemoteEndpointSettings();
const browserPath = await getBrowserPath();
await launchBrowser(browserPath, port, url, userDataDir, /** headless */ true).then(() => attach(context, url, undefined, true, true));
} else {
edgeDebugConfig.url = defaultUrl;
const browserPath = await getBrowserPath();
await launchBrowser(browserPath, port, defaultUrl, userDataDir, /** headless */ true).then(() => attach(context, defaultUrl, undefined, true, true));
}
}

Expand Down Expand Up @@ -338,6 +350,10 @@ async function startWebhint(context: vscode.ExtensionContext): Promise<void> {
fileEvents: vscode.workspace.createFileSystemWatcher('**/.hintrc'),
},
middleware: {
handleDiagnostics: (uri, diagnostics, next) => {
const filteredDiagnostics = diagnostics.filter(diagnostic => !shouldSuppressWebhintDiagnostic(uri, diagnostic));
next(uri, filteredDiagnostics);
},
executeCommand: (command, args, next) => {
const hintName = args[0] as string;
const featureName = args[1] as string;
Expand Down
30 changes: 30 additions & 0 deletions src/webhintDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as path from 'path';
import * as vscode from 'vscode';

const WEBHINT_TS_CONFIG_HINT = 'typescript-config/is-valid';
const TS_CONFIG_FILE_PATTERN = /^tsconfig(\..+)?\.json$/i;

type WebhintDiagnosticCode = {
value?: string;
};

function isTsConfigFile(uri: vscode.Uri): boolean {
return TS_CONFIG_FILE_PATTERN.test(path.basename(uri.fsPath));
}

function isWebhintTsConfigDiagnostic(diagnostic: vscode.Diagnostic): boolean {
const diagnosticCode = diagnostic.code as WebhintDiagnosticCode | string | undefined;
const diagnosticCodeValue = typeof diagnosticCode === 'string' ? diagnosticCode : diagnosticCode?.value;
return diagnosticCodeValue === WEBHINT_TS_CONFIG_HINT;
}

function isUnsupportedEs2023Diagnostic(diagnostic: vscode.Diagnostic): boolean {
return diagnostic.message.includes('ES2023');
}

export function shouldSuppressWebhintDiagnostic(uri: vscode.Uri, diagnostic: vscode.Diagnostic): boolean {
return isTsConfigFile(uri) && isWebhintTsConfigDiagnostic(diagnostic) && isUnsupportedEs2023Diagnostic(diagnostic);
}
31 changes: 23 additions & 8 deletions test/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,14 +479,8 @@ describe("extension", () => {

it("can launch html files in non-remote contexts", async () => {
mockVSCode.env.remoteName = undefined;
const testFileUri = {
scheme: 'file',
authority: '',
fsPath: 'test/path.html',
query: '',
fragment: ''
} as Uri;
const expectedUrl = `file://test/path.html`;
const testFileUri = mockVSCode.Uri.file('test/path.html') as Uri;
const expectedUrl = `file:///test/path.html`;

const newExtension = await import("../src/extension");
await newExtension.launchHtml(testFileUri);
Expand Down Expand Up @@ -653,6 +647,27 @@ describe("extension", () => {
);
}
});
it("can launch the browser with the default url when no file uri is provided", async () => {
mockVSCode.env.remoteName = undefined;
mockUtils.getRemoteEndpointSettings!.mockReturnValue({
defaultUrl: "http://localhost:3000",
hostname: "localhost",
port: 9222,
timeout: 10000,
useHttps: false,
userDataDir: "profile",
});

const newExtension = await import("../src/extension");
await newExtension.launchHtml(undefined);

expect(mockVSCode.debug.startDebugging).toHaveBeenNthCalledWith(1, undefined, expect.objectContaining({
url: "http://localhost:3000",
}));
expect(mockVSCode.debug.startDebugging).toHaveBeenNthCalledWith(2, undefined, expect.objectContaining({
url: "http://localhost:3000",
}));
});
});
describe("attachToCurrentDebugTarget", () => {
let mocks: {
Expand Down
118 changes: 55 additions & 63 deletions test/launchConfigManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,94 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { afterAll, describe, expect, it, jest } from '@jest/globals';
import {createFakeVSCode} from "./helpers/helpers";
import { extensionCompoundConfigs, extensionConfigs, LaunchConfigManager, providedDebugConfig } from "../src/launchConfigManager";

jest.mock("vscode", () => createFakeVSCode(), { virtual: true });
jest.mock("fs-extra");

const getVscodeMock = () => jest.requireMock("vscode") as ReturnType<typeof createFakeVSCode>;
const getFsExtraMock = () => jest.requireMock("fs-extra") as any;

describe("launchConfigManager", () => {

describe('getLaunchJson', () => {
it('updates launchJsonStatus with "None" when launch.json does not exist', async () => {
const vscodeMock = jest.requireMock("vscode");
const fse = jest.requireMock("fs-extra");
vscodeMock.workspace.getConfiguration.mockImplementation(() => {
return {
get: (name: string) => [{type: 'vscode-edge-devtools.debug'}]
}
});
const vscodeMock = getVscodeMock();
const fse = getFsExtraMock();
vscodeMock.workspace.getConfiguration.mockImplementation(() => ({
get: (name: string) => [{type: 'vscode-edge-devtools.debug'}],
} as any));
fse.pathExistsSync.mockImplementation(() => false);
const launchConfigManager = LaunchConfigManager.instance;
expect(launchConfigManager.getLaunchConfig()).toEqual('None');
expect(vscodeMock.commands.executeCommand).toHaveBeenCalledWith('setContext', 'launchJsonStatus', 'None');
});

it('updates launchJsonStatus with "Unsupported" when there is no supported debug config', async () => {
const vscodeMock = jest.requireMock("vscode");
const fse = jest.requireMock("fs-extra");
const vscodeMock = getVscodeMock();
const fse = getFsExtraMock();
fse.pathExistsSync.mockImplementation(() => true);
vscodeMock.workspace.getConfiguration.mockImplementation(() => {
return {
get: (name: string) => [{type: ''}]
}
});
vscodeMock.workspace.getConfiguration.mockImplementation(() => ({
get: (name: string) => [{type: ''}],
} as any));
const launchConfigManager = LaunchConfigManager.instance;
expect(launchConfigManager.getLaunchConfig()).toEqual('Unsupported');
expect(vscodeMock.commands.executeCommand).toHaveBeenCalledWith('setContext', 'launchJsonStatus', 'Unsupported');
});

it('returns a supported debug config when one exists', async () => {
const vscodeMock = jest.requireMock("vscode");
const fse = jest.requireMock("fs-extra");
const vscodeMock = getVscodeMock();
const fse = getFsExtraMock();
fse.pathExistsSync.mockImplementation(() => true);
vscodeMock.workspace.getConfiguration.mockImplementation(() => {
return {
get: (name: string) => {
if (name === 'configurations') {
return extensionConfigs;
} else {
return extensionCompoundConfigs;
}
vscodeMock.workspace.getConfiguration.mockImplementation(() => ({
get: (name: string) => {
if (name === 'configurations') {
return extensionConfigs;
} else {
return extensionCompoundConfigs;
}
}
});
},
} as any));
const launchConfigManager = LaunchConfigManager.instance;
expect(launchConfigManager.getLaunchConfig()).toEqual('Launch Edge Headless and attach DevTools');
expect(vscodeMock.commands.executeCommand).toHaveBeenCalledWith('setContext', 'launchJsonStatus', 'Supported');
});

it('updates launchJsonStatus with "None" when there is no folder open', async () => {
const vscodeMock = jest.requireMock("vscode");
const vscodeMock = getVscodeMock();
const launchConfigManager = LaunchConfigManager.instance;
vscodeMock.workspace.workspaceFolders = null;
(vscodeMock.workspace as any).workspaceFolders = null;
expect(launchConfigManager.getLaunchConfig()).toEqual('None');
expect(vscodeMock.commands.executeCommand).toHaveBeenCalledWith('setContext', 'launchJsonStatus', 'None');
});
});

describe('configureLaunchJson', () => {
it('adds extension configs/compounds to launch.json', async () => {
const vscodeMock = jest.requireMock("vscode");
const fse = jest.requireMock("fs-extra");
const vscodeMock = getVscodeMock();
const fse = getFsExtraMock();
fse.readFileSync.mockImplementation((() => ''));
vscodeMock.workspace.workspaceFolders = [{
(vscodeMock.workspace as any).workspaceFolders = [{
uri: 'file:///g%3A/GIT/testPage'
}];
vscodeMock.WorkspaceConfiguration = {
(vscodeMock as any).WorkspaceConfiguration = {
update: jest.fn((name: string, value: any) => {}),
};
vscodeMock.workspace.getConfiguration.mockImplementation(() => {
return {
get: jest.fn((name: string) => []),
update: vscodeMock.WorkspaceConfiguration.update,
}
});
vscodeMock.workspace.getConfiguration.mockImplementation(() => ({
get: jest.fn((name: string) => []),
update: (vscodeMock as any).WorkspaceConfiguration.update,
} as any));
vscodeMock.Uri.joinPath = jest.fn();
const launchConfigManager = LaunchConfigManager.instance;
await launchConfigManager.configureLaunchJson();
expect(vscodeMock.WorkspaceConfiguration.update).toHaveBeenCalledWith('configurations', expect.arrayContaining([...extensionConfigs]));
expect(vscodeMock.WorkspaceConfiguration.update).toHaveBeenCalledWith('compounds', expect.arrayContaining([...extensionCompoundConfigs]));
expect((vscodeMock as any).WorkspaceConfiguration.update).toHaveBeenCalledWith('configurations', expect.arrayContaining([...extensionConfigs]));
expect((vscodeMock as any).WorkspaceConfiguration.update).toHaveBeenCalledWith('compounds', expect.arrayContaining([...extensionCompoundConfigs]));
});

it('inserts a comment after the url property', async () => {
const fse = jest.requireMock("fs-extra");
const fse = getFsExtraMock();
const expectedText = '// Provide your project\'s url to finish configuring';
fse.readFileSync.mockImplementation(() => JSON.stringify(providedDebugConfig));
const launchConfigManager = LaunchConfigManager.instance;
Expand All @@ -101,47 +97,43 @@ describe("launchConfigManager", () => {
});

it('replaces config with duplicate name with extension config', async () => {
const vscodeMock = jest.requireMock("vscode");
const fse = jest.requireMock("fs-extra");
const vscodeMock = getVscodeMock();
const fse = getFsExtraMock();
fse.readFileSync.mockImplementation((() => ''));
vscodeMock.workspace.workspaceFolders = [{
(vscodeMock.workspace as any).workspaceFolders = [{
uri: 'file:///g%3A/GIT/testPage'
}];
vscodeMock.WorkspaceConfiguration = {
(vscodeMock as any).WorkspaceConfiguration = {
update: jest.fn((name: string, value: any) => {}),
};
vscodeMock.workspace.getConfiguration.mockImplementation(() => {
return {
get: jest.fn((name: string) => [{name: 'Launch Microsoft Edge in headless mode'}]),
update: vscodeMock.WorkspaceConfiguration.update,
}
});
vscodeMock.workspace.getConfiguration.mockImplementation(() => ({
get: jest.fn((name: string) => [{name: 'Launch Microsoft Edge in headless mode'}]),
update: (vscodeMock as any).WorkspaceConfiguration.update,
} as any));
vscodeMock.Uri.joinPath = jest.fn();
const launchConfigManager = LaunchConfigManager.instance;
launchConfigManager.configureLaunchJson();
expect(vscodeMock.WorkspaceConfiguration.update).toHaveBeenCalledWith('configurations', Array(3).fill(expect.anything()));
expect((vscodeMock as any).WorkspaceConfiguration.update).toHaveBeenCalledWith('configurations', Array(3).fill(expect.anything()));
});

it('retains user config', async () => {
const vscodeMock = jest.requireMock("vscode");
const fse = jest.requireMock("fs-extra");
const vscodeMock = getVscodeMock();
const fse = getFsExtraMock();
fse.readFileSync.mockImplementation((() => ''));
vscodeMock.workspace.workspaceFolders = [{
(vscodeMock.workspace as any).workspaceFolders = [{
uri: 'file:///g%3A/GIT/testPage'
}];
vscodeMock.WorkspaceConfiguration = {
(vscodeMock as any).WorkspaceConfiguration = {
update: jest.fn((name: string, value: any) => {}),
};
vscodeMock.workspace.getConfiguration.mockImplementation(() => {
return {
get: jest.fn((name: string) => [{name: 'Personal config'}]),
update: vscodeMock.WorkspaceConfiguration.update,
}
});
vscodeMock.workspace.getConfiguration.mockImplementation(() => ({
get: jest.fn((name: string) => [{name: 'Personal config'}]),
update: (vscodeMock as any).WorkspaceConfiguration.update,
} as any));
vscodeMock.Uri.joinPath = jest.fn();
const launchConfigManager = LaunchConfigManager.instance;
launchConfigManager.configureLaunchJson();
expect(vscodeMock.WorkspaceConfiguration.update).toHaveBeenCalledWith('configurations', Array(4).fill(expect.anything()));
expect((vscodeMock as any).WorkspaceConfiguration.update).toHaveBeenCalledWith('configurations', Array(4).fill(expect.anything()));
});
});

Expand Down
Loading