diff --git a/src/test/runTest.ts b/src/test/runTest.ts index 6711685..cf86e4f 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -1,24 +1,64 @@ -import * as path from 'path'; +import * as path from "path"; +import * as fs from "fs"; +import * as os from "os"; +import * as cp from "child_process"; -import { runTests } from 'vscode-test'; +const packageJson = require("../../package.json"); + +import { + downloadAndUnzipVSCode, + resolveCliArgsFromVSCodeExecutablePath, + runTests, +} from "@vscode/test-electron"; +import { rimraf } from "rimraf"; async function main() { try { // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` - const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); + const extensionDevelopmentPath = path.resolve(__dirname, "../../"); // The path to the extension test runner script // Passed to --extensionTestsPath - const extensionTestsPath = path.resolve(__dirname, './suite/index'); + const extensionTestsPath = path.resolve(__dirname, "./suite/index"); + + const vscodeExecutablePath = await downloadAndUnzipVSCode(); + + const [cliPath, ...args] = + resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); + + // for (const extensionId of packageJson.extensionDependencies) { + // cp.spawnSync(cliPath, [...args, "--install-extension", extensionId], { + // encoding: "utf-8", + // stdio: "inherit", + // }); + // } + + await rimraf(".vscode-test/user-data"); + + let cwd = fs.mkdtempSync(path.join(os.tmpdir(), "coded_project")); + fs.writeFileSync(path.join(cwd, "dub.sdl"), 'name "codedproject"\n'); + fs.mkdirSync(path.join(cwd, "source")); + fs.writeFileSync( + path.join(cwd, "source", "app.d"), + 'import std.stdio;\n\nvoid main() {\n\twriteln("hello world");\n}\n' + ); // Download VS Code, unzip it and run the integration test - await runTests({ extensionDevelopmentPath, extensionTestsPath }); + await runTests({ + vscodeExecutablePath, + extensionDevelopmentPath, + launchArgs: [cwd], + extensionTestsPath, + extensionTestsEnv: { + PROJECT_DIR: cwd, + }, + }); } catch (err) { console.error(err); - console.error('Failed to run tests'); + console.error("Failed to run tests"); process.exit(1); } } -main(); \ No newline at end of file +main(); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index db6f321..6119594 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -1,38 +1,33 @@ +import { glob } from "glob"; +import * as Mocha from "mocha"; +import * as path from "path"; -import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; +export async function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: "tdd", + timeout: 120000, + }); -export function run(): Promise { - // Create the mocha test - const mocha = new Mocha({ - ui: 'tdd' - }); + const testsRoot = path.resolve(__dirname, ".."); - const testsRoot = path.resolve(__dirname, '..'); + let files = await glob("**/**.test.js", { cwd: testsRoot }); - return new Promise((c, e) => { - glob('suite/**/**.test.js', { cwd: testsRoot }, (err, files) => { - if (err) { - return e(err); - } + // Add files to the test suite + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); - - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - console.error(err); - e(err); - } - }); - }); + return new Promise((resolve, reject) => { + // Run the mocha test + try { + mocha.run((failures) => { + if (failures > 0) { + reject(new Error(`${failures} tests failed.`)); + } else { + resolve(); + } + }); + } catch (e) { + reject(e); + } + }); } diff --git a/src/test/suite/it.test.ts b/src/test/suite/it.test.ts new file mode 100644 index 0000000..f6fe557 --- /dev/null +++ b/src/test/suite/it.test.ts @@ -0,0 +1,54 @@ +import * as assert from "assert"; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from "vscode"; +import { sleep, testCompletion } from "../utils"; +// import * as myExtension from '../../extension'; + +suite("Integration Tests", () => { + vscode.window.showInformationMessage("Start all tests."); + + // sanity test that we have the correct window open + let workspaces = vscode.workspace.workspaceFolders; + assert.strictEqual(workspaces?.length, 1); + assert.strictEqual(workspaces[0].uri.fsPath, process.env["PROJECT_DIR"]); + let workspace = workspaces[0]; + + test("check code-d installed", async () => { + let coded = vscode.extensions.getExtension("webfreak.code-d")!; + assert.notStrictEqual(coded, undefined, "code-d not installed?!"); + }); + + function file(relative: string): vscode.Uri { + return vscode.Uri.joinPath(workspace.uri, relative); + } + + test("Wait for python and code-d extensions", async () => { + let coded = vscode.extensions.getExtension("webfreak.code-d")!; + await coded.activate(); + await sleep(5000); // give sufficient startup time + }); + + test("Recipe file", async () => { + let recipe = await vscode.window.showTextDocument( + await vscode.workspace.openTextDocument(file("dub.sdl")), + vscode.ViewColumn.One + ); + + await recipe.edit((edit) => { + edit.insert(new vscode.Position(2, 0), "dep"); + }); + + await testCompletion( + recipe, + new vscode.Position(2, 3), + new vscode.CompletionList([ + new vscode.CompletionItem("dependency", vscode.CompletionItemKind.Field), + ]), + "contains" + ); + }); + + // test('interactive', () => new Promise((resolve, reject) => {})); +}); diff --git a/src/test/utils.ts b/src/test/utils.ts new file mode 100644 index 0000000..c8fa73e --- /dev/null +++ b/src/test/utils.ts @@ -0,0 +1,64 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function testCompletion( + editor: vscode.TextEditor, + position: vscode.Position, + expectedCompletionList: vscode.CompletionList, + type: "exact" | "contains", + testKeys: (keyof vscode.CompletionItem)[] = ["label", "kind"] +) { + editor = await vscode.window.showTextDocument(editor.document, editor.viewColumn); + await sleep(500); + editor.selection = new vscode.Selection(position, position); + await sleep(500); + + // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion + const actualCompletionList = (await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + editor.document.uri, + position + )) as vscode.CompletionList; + + if (type === "exact") { + assert.strictEqual(actualCompletionList.items.length, expectedCompletionList.items.length); + expectedCompletionList.items.forEach((expectedItem, i) => { + const actualItem = actualCompletionList.items[i]; + testKeys.forEach(key => { + assert.strictEqual(actualItem[key], expectedItem[key], + "completion " + + JSON.stringify(expectedItem.label) + + " mismatch on key " + JSON.stringify(key) + ":\n" + + "expected = " + JSON.stringify(expectedItem[key]) + "\n" + + " actual = " + JSON.stringify(actualItem[key])); + }); + }); + } else if (type === "contains") { + assert.ok(actualCompletionList.items.length >= expectedCompletionList.items.length, + "Expected at least " + expectedCompletionList.items.length + + " completions, but only got " + actualCompletionList.items.length); + expectedCompletionList.items.forEach((expectedItem, i) => { + const actualItem = actualCompletionList.items.find(i => i.label == expectedItem.label); + if (!actualItem) + assert.fail("can't find completion item " + + JSON.stringify(expectedItem.label) + + " in " + + JSON.stringify(actualCompletionList.items.map(c => c.label))); + + testKeys.forEach(key => { + assert.strictEqual(actualItem[key], expectedItem[key], + "completion " + + JSON.stringify(expectedItem.label) + + " mismatch on key " + JSON.stringify(key) + ":\n" + + "expected = " + JSON.stringify(expectedItem[key]) + "\n" + + " actual = " + JSON.stringify(actualItem[key])); + }); + }); + } else { + throw new Error("invalid type"); + } +}