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

Single acceptance test for ts plugin mode #789

Merged
merged 14 commits into from
Jan 13, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "template-imports-app-ts-plugin",
"private": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Component from '@glimmer/component';

export interface GreetingSignature {
Args: { target: string };
}

export default class Greeting extends Component<GreetingSignature> {
private message = 'Hello';

<template>
{{this.message}}, {{@target}}!
</template>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import '@glint/environment-ember-loose';
import '@glint/environment-ember-template-imports';

// TS-PLUGIN: I had to add the .gts extension in order for this to work, otherwise
// Cannot find module './Greeting' or its corresponding type declarations. [ts-plugin(2307)]

// import Greeting from './Greeting';
import Greeting from './Greeting.gts';

<template>
<Greeting @target="World" />
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../../../tsconfig.compileroptions.json",
"glint": {
"environment": ["ember-loose", "ember-template-imports"],
"enableTsPlugin": true
},
"compilerOptions": {
"baseUrl": ".",

// TODO: work out the interplay between this and typescriptServerPlugins in extension's package.json
"plugins": [{ "name": "@glint/typescript-plugin" }]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import {
import * as path from 'path';
import { describe, afterEach, test } from 'mocha';
import { expect } from 'expect';
import { waitUntil } from './helpers/async';
import { waitUntil } from '../helpers/async';

describe.skip('Smoke test: Ember', () => {
const rootDir = path.resolve(__dirname, '../../__fixtures__/ember-app');
const rootDir = path.resolve(__dirname, '../../../__fixtures__/ember-app');

afterEach(async () => {
while (window.activeTextEditor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
import * as path from 'path';
import { describe, afterEach, test } from 'mocha';
import { expect } from 'expect';
import { waitUntil } from './helpers/async';
import { waitUntil } from '../helpers/async';

describe('Smoke test: ETI Environment', () => {
const rootDir = path.resolve(__dirname, '../../__fixtures__/template-imports-app');
const rootDir = path.resolve(__dirname, '../../../__fixtures__/template-imports-app');

afterEach(async () => {
while (window.activeTextEditor) {
Expand Down
52 changes: 44 additions & 8 deletions packages/vscode/__tests__/support/launch-from-cli.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,66 @@ import * as path from 'node:path';
import * as os from 'node:os';
import { fileURLToPath } from 'node:url';
import { runTests } from '@vscode/test-electron';
import * as fs from 'node:fs';

const dirname = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(dirname, '../../..');
const emptyTempDir = path.join(os.tmpdir(), `user-data-${Math.random()}`);
const emptyExtensionsDir = path.join(os.tmpdir(), `extensions-${Math.random()}`);
const emptyUserDataDir = path.join(os.tmpdir(), `user-data-${Math.random()}`);

const settingsDir = path.join(emptyUserDataDir, 'User');
fs.mkdirSync(settingsDir, { recursive: true });

const userPreferences = {
// When testing TS Plugin, it can be useful to look at tsserver logs within
// the test runner VSCode instance. To do this, uncomment the following line,
// and then check the logs for TypeScript
// "typescript.tsserver.log": "verbose",
};

fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify(userPreferences, null, 2));

const testType = process.argv[2];

let disableExtensionArgs: string[] = [];

let testRunner: string;
switch (testType) {
case 'language-server':
testRunner = 'vscode-runner-language-server.js';

// Disable vanilla TS for full "takeover" mode.
disableExtensionArgs = ['--disable-extension', 'vscode.typescript-language-features'];
break;
case 'ts-plugin':
testRunner = 'vscode-runner-ts-plugin.js';

// Note: here, we WANT vanilla TS to be enabled since we're testing the TS Plugin.
break;
default:
console.error('Test type must be either "language-server" or "ts-plugin"');
process.exit(1);
}

try {
await runTests({
extensionDevelopmentPath: packageRoot,
extensionTestsPath: path.resolve(dirname, 'vscode-runner.js'),
extensionTestsPath: path.resolve(dirname, testRunner),
launchArgs: [
// Don't show the "hey do you trust this folder?" prompt
'--disable-workspace-trust',
// Explicitly turn off the built-in TS extension
'--disable-extension',
'vscode.typescript-language-features',
...disableExtensionArgs,
// Point at an empty directory so no third-party extensions load
'--extensions-dir',
emptyTempDir,
emptyExtensionsDir,
// Point at an empty directory so we don't have to contend with any local user preferences
'--user-data-dir',
emptyTempDir,
// Load the app fixtures
emptyUserDataDir,
// Load the app fixtures. Note that it's ok to load fixtures that aren't used for the
// particular test type.
`${packageRoot}/__fixtures__/ember-app`,
`${packageRoot}/__fixtures__/template-imports-app`,
`${packageRoot}/__fixtures__/template-imports-app-ts-plugin`,
],
});
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This file is invoked by VSCode itself when configured to run extension
// tests via the `--extensionTestsPath` flag.

import { run as runShared } from './vscode-runner';

export function run(runner: unknown, callback: (error: unknown, failures?: number) => void): void {
runShared(runner, callback, 'language-server-tests');
}
8 changes: 8 additions & 0 deletions packages/vscode/__tests__/support/vscode-runner-ts-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This file is invoked by VSCode itself when configured to run extension
// tests via the `--extensionTestsPath` flag.

import { run as runShared } from './vscode-runner';

export function run(runner: unknown, callback: (error: unknown, failures?: number) => void): void {
runShared(runner, callback, 'ts-plugin-tests');
}
8 changes: 6 additions & 2 deletions packages/vscode/__tests__/support/vscode-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import * as path from 'node:path';
import * as glob from 'glob';
import Mocha = require('mocha');

export function run(runner: unknown, callback: (error: unknown, failures?: number) => void): void {
export function run(
runner: unknown,
callback: (error: unknown, failures?: number) => void,
testSubfolder: 'language-server-tests' | 'ts-plugin-tests',
): void {
try {
let mocha = new Mocha({ color: true, slow: 3_000, timeout: 30_000 });
let tests = path.resolve(__dirname, '..').replace(/\\/g, '/');

for (let testFile of glob.sync(`${tests}/**/*.test.js`)) {
for (let testFile of glob.sync(`${tests}/${testSubfolder}/**/*.test.js`)) {
if (process.platform === 'win32') {
// Mocha is weird about drive letter casing under Windows
testFile = testFile[0].toLowerCase() + testFile.slice(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {
commands,
languages,
ViewColumn,
window,
Uri,
Range,
Position,
CodeAction,
workspace,
} from 'vscode';
import * as path from 'path';
import { describe, afterEach, test } from 'mocha';
import { expect } from 'expect';
import { waitUntil } from '../helpers/async';

describe('Smoke test: ETI Environment (TS Plugin Mode)', () => {
const rootDir = path.resolve(__dirname, '../../../__fixtures__/template-imports-app-ts-plugin');

afterEach(async () => {
while (window.activeTextEditor) {
await commands.executeCommand('workbench.action.files.revert');
await commands.executeCommand('workbench.action.closeActiveEditor');
}
});

describe('diagnostics for errors', () => {
// TODO: fix remaining tests and remove this `.only`
test.only('with a custom extension', async () => {
let scriptURI = Uri.file(`${rootDir}/src/index.gts`);
let scriptEditor = await window.showTextDocument(scriptURI, { viewColumn: ViewColumn.One });

// Ensure we have a clean bill of health
expect(languages.getDiagnostics(scriptURI)).toEqual([]);

await hackishlyWaitForTypescriptPluginToActivate();

// Replace a string with a number
await scriptEditor.edit((edit) => {
edit.replace(new Range(10, 20, 10, 27), '{{123}}');

// Original range, in case we revert some of the TS-Plugin-specific
// edit.replace(new Range(6, 20, 6, 27), '{{123}}');
});

// Wait for the diagnostic to show up
await waitUntil(() => languages.getDiagnostics(scriptURI).length);

// Verify it's what we expect
expect(languages.getDiagnostics(scriptURI)).toMatchObject([
{
message: "Type 'number' is not assignable to type 'string'.",
source: 'ts-plugin',
code: 2322,
// range: new Range(6, 13, 6, 19),
range: new Range(10, 13, 10, 19),
},
]);
});

describe('codeactions args', () => {
test('adds missing args from template into Args type', async () => {
let scriptURI = Uri.file(`${rootDir}/src/Greeting.gts`);

// Open the script and the template
let scriptEditor = await window.showTextDocument(scriptURI, { viewColumn: ViewColumn.One });

// Ensure neither has any diagnostic messages
expect(languages.getDiagnostics(scriptURI)).toEqual([]);

// Comment out a property in the script that's referenced in the template
await scriptEditor.edit((edit) => {
edit.insert(new Position(10, 4), '{{@undocumentedProperty}} ');
});

// Wait for a diagnostic to appear in the template
await waitUntil(() => languages.getDiagnostics(scriptURI).length);

const fixes = await commands.executeCommand<CodeAction[]>(
'vscode.executeCodeActionProvider',
scriptURI,
new Range(new Position(10, 9), new Position(10, 9)),
);

expect(fixes.length).toBe(4);

const fix = fixes.find((fix) => fix.title === "Declare property 'undocumentedProperty'");

expect(fix).toBeDefined();

// apply the missing arg fix
await workspace.applyEdit(fix!.edit!);

await waitUntil(
() =>
scriptEditor.document.getText().includes('undocumentedProperty: any') &&
languages.getDiagnostics(scriptURI).length === 0,
);
});
});

describe('codeactions locals', () => {
test('add local props to a class', async () => {
let scriptURI = Uri.file(`${rootDir}/src/Greeting.gts`);

// Open the script and the template
let scriptEditor = await window.showTextDocument(scriptURI, { viewColumn: ViewColumn.One });

// Ensure neither has any diagnostic messages
expect(languages.getDiagnostics(scriptURI)).toEqual([]);

await scriptEditor.edit((edit) => {
edit.insert(new Position(10, 4), '{{this.localProp}} ');
});

// Wait for a diagnostic to appear in the template
await waitUntil(() => languages.getDiagnostics(scriptURI).length);

const fixes = await commands.executeCommand<CodeAction[]>(
'vscode.executeCodeActionProvider',
scriptURI,
new Range(new Position(10, 12), new Position(10, 12)),
);

expect(fixes.length).toBe(4);

const fix = fixes.find((fix) => fix.title === "Declare property 'localProp'");

expect(fix).toBeDefined();

// select ignore
await workspace.applyEdit(fix!.edit!);

await waitUntil(
() =>
scriptEditor.document.getText().includes('localProp: any') &&
languages.getDiagnostics(scriptURI).length,
);
});
});
});
});

/**
* We shouldn't have to use this function for many reasons:
*
* 1. Using timers to avoid a race condition is brittle
* 2. More importantly: this only solves the problem of "make sure the TS Plugin is activated
* before we edit the file" when what we REALLY want is diagnostics to kick in without
* editing.
*/
function hackishlyWaitForTypescriptPluginToActivate(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 5000));
}
4 changes: 3 additions & 1 deletion packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
],
"scripts": {
"pretest": "yarn build",
"test": "node lib/__tests__/support/launch-from-cli.mjs",
"test": "yarn test-language-server && yarn test-ts-plugin",
"test-language-server": "node lib/__tests__/support/launch-from-cli.mjs language-server",
"test-ts-plugin": "node lib/__tests__/support/launch-from-cli.mjs ts-plugin",
"test:typecheck": "echo 'no standalone typecheck within this project'",
"test:tsc": "echo 'no standalone tsc within this project'",
"build": "yarn compile && yarn bundle",
Expand Down
Loading