Skip to content

Commit 8871478

Browse files
authored
Add tests and test support commands for visual editor (#770)
* Add tests and test support commands for visual editor * Attempt to await extension being activated * Attempt to build in test workflow * Attempt to install quarto-cli in test workflow * Try to skip extension tests in CI, and remove debug logs * Properly skip tests
1 parent 0424deb commit 8871478

File tree

9 files changed

+135
-13
lines changed

9 files changed

+135
-13
lines changed

.github/workflows/test.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ jobs:
2525
sudo apt-get -y update
2626
sudo apt-get -y install --fix-missing xvfb
2727
28+
- uses: quarto-dev/quarto-actions/setup@v2
29+
30+
- name: Build vscode extension
31+
run: |
32+
cd apps/vscode
33+
yarn install
34+
yarn run build
35+
2836
- name: Compile and run tests
2937
run: |
3038
yarn install --immutable --immutable-cache --check-cache

apps/lsp/src/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ import {
2929
WorkspaceSymbol
3030
} from "vscode-languageserver";
3131

32-
import { CompletionItem, Hover, Location } from "vscode-languageserver-types"
32+
import { CompletionItem, Hover, Location } from "vscode-languageserver-types";
3333

34-
import { createConnection } from "vscode-languageserver/node"
34+
import { createConnection } from "vscode-languageserver/node";
3535

3636
import { URI } from "vscode-uri";
3737
import { TextDocument } from "vscode-languageserver-textdocument";
@@ -48,6 +48,8 @@ import { initializeQuarto } from "./quarto";
4848
import { registerDiagnostics } from "./diagnostics";
4949

5050

51+
52+
5153
// Create a connection for the server. The connection uses Node's IPC as a transport.
5254
// Also include all preview / proposed LSP features.
5355
const connection = createConnection(ProposedFeatures.all);
@@ -98,7 +100,7 @@ connection.onInitialize((params: InitializeParams) => {
98100
}
99101

100102
return mdLs?.getCompletionItems(document, params.position, params.context, config, token) || [];
101-
})
103+
});
102104

103105
connection.onHover(async (params, token): Promise<Hover | null | undefined> => {
104106
logger.logRequest('hover');
@@ -108,7 +110,7 @@ connection.onInitialize((params: InitializeParams) => {
108110
return null;
109111
}
110112
return mdLs?.getHover(document, params.position, config, token);
111-
})
113+
});
112114

113115

114116
connection.onDocumentLinks(async (params, token): Promise<DocumentLink[]> => {
@@ -249,7 +251,7 @@ connection.onInitialized(async () => {
249251
capabilities!,
250252
config,
251253
logger
252-
)
254+
);
253255

254256
// initialize parser
255257
const parser = markdownitParser();
@@ -279,7 +281,7 @@ connection.onInitialized(async () => {
279281
onRequest(method: string, handler: (params: unknown[]) => Promise<unknown>) {
280282
return connection.onRequest(method, handler);
281283
}
282-
}
284+
};
283285

284286
// register custom methods
285287
registerCustomMethods(quarto, lspConnection, documents);

apps/vscode/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
out/
22
*.vsix
33
test-out/
4+
examples-out/

apps/vscode/.vscode-test.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ export default defineConfig([
44
{
55
files: 'test-out/*.test.js',
66
workspaceFolder: 'src/test/examples',
7+
mocha: {
8+
timeout: 3000,
9+
},
710
},
811
]);

apps/vscode/src/providers/editor/editor.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ import {
6969
import { ExtensionHost } from "../../host";
7070
import { TabInputCustom } from "vscode";
7171

72+
const kVisualModeConfirmed = "visualModeConfirmed";
73+
7274
export interface QuartoVisualEditor extends QuartoEditor {
7375
hasFocus(): Promise<boolean>;
7476
getActiveBlockContext(): Promise<CodeViewActiveBlockContext | null>;
@@ -87,6 +89,18 @@ export function activateEditor(
8789

8890
// return commands
8991
return [
92+
{
93+
id: 'quarto.test_setkVisualModeConfirmedTrue',
94+
execute() {
95+
context.globalState.update(kVisualModeConfirmed, true);
96+
}
97+
},
98+
{
99+
id: 'quarto.test_isInVisualEditor',
100+
execute() {
101+
return VisualEditorProvider.activeEditor() !== undefined;
102+
}
103+
},
90104
editInVisualModeCommand(),
91105
editInSourceModeCommand(),
92106
toggleEditModeCommand(),
@@ -99,7 +113,7 @@ export class VisualEditorProvider implements CustomTextEditorProvider {
99113

100114
// track the last contents of any active untitled docs (used
101115
// for recovering from attempt to edit )
102-
private static activeUntitled?: { uri: Uri, content: string };
116+
private static activeUntitled?: { uri: Uri, content: string; };
103117

104118
// track the last edited line of code in text editors (used for syncing position)
105119
private static editorLastSourcePos = new Map<string, number>();
@@ -312,7 +326,6 @@ export class VisualEditorProvider implements CustomTextEditorProvider {
312326
private readonly lspRequest: JsonRpcRequestTransport,
313327
private readonly engine: MarkdownEngine) { }
314328

315-
316329
public async resolveCustomTextEditor(
317330
document: TextDocument,
318331
webviewPanel: WebviewPanel,
@@ -333,7 +346,6 @@ export class VisualEditorProvider implements CustomTextEditorProvider {
333346
};
334347

335348
// prompt the user
336-
const kVisualModeConfirmed = "visualModeConfirmed";
337349

338350
// Check for environment variables to force the state of the visual editor confirmation modal
339351
// QUARTO_VISUAL_EDITOR_CONFIRMED > PW_TEST > CI

apps/vscode/src/test/examples/hello.qmd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ format: html
88
```{python}
99
1 + 1
1010
```
11+
12+
*YO!*

apps/vscode/src/test/extension.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as vscode from "vscode";
2+
3+
export const QUARTO_EXTENSION_ID = 'quarto.quarto';
4+
5+
export function extension() {
6+
const extension = vscode.extensions.getExtension(QUARTO_EXTENSION_ID);
7+
8+
if (extension === undefined) {
9+
throw new Error(`Extension ${QUARTO_EXTENSION_ID} not found`);
10+
}
11+
12+
return extension;
13+
}
Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,64 @@
11
import * as vscode from "vscode";
22
import * as assert from "assert";
3-
import { exampleWorkspacePath } from "./test-utils";
3+
import { exampleWorkspacePath, exampleWorkspaceOutPath, copyFile, wait } from "./test-utils";
44
import { isQuartoDoc } from "../core/doc";
5+
import { extension } from "./extension";
56

6-
suite("Quarto basics", () => {
7-
test("Can open a Quarto document", async () => {
8-
const doc = await vscode.workspace.openTextDocument(exampleWorkspacePath("hello.qmd"));
7+
const APPROX_TIME_TO_OPEN_VISUAL_EDITOR = 1600;
8+
9+
suite("Quarto basics", function () {
10+
// Before we run any tests, we should copy any files that get edited in the tests to file under `exampleWorkspaceOutPath`
11+
suiteSetup(async function () {
12+
const didCopyFile = await copyFile(exampleWorkspacePath('hello.qmd'), exampleWorkspaceOutPath('hello.qmd'));
13+
assert.ok(didCopyFile);
14+
});
15+
16+
test("Can open a Quarto document", async function () {
17+
const doc = await vscode.workspace.openTextDocument(exampleWorkspaceOutPath("hello.qmd"));
918
const editor = await vscode.window.showTextDocument(doc);
19+
1020
assert.strictEqual(editor?.document.languageId, "quarto");
1121
assert.strictEqual(isQuartoDoc(editor?.document), true);
1222
});
23+
24+
// Note: the following tests may be flaky. They rely on waiting estimated amounts of time for commands to complete.
25+
test("Can edit in visual mode", async function () {
26+
// don't run this in CI for now because we haven't figured out how to get the LSP to start
27+
if (process.env['CI']) this.skip();
28+
29+
const doc = await vscode.workspace.openTextDocument(exampleWorkspaceOutPath("hello.qmd"));
30+
const editor = await vscode.window.showTextDocument(doc);
31+
32+
// manually confirm visual mode so dialogue pop-up doesn't show because dialogues cause test errors
33+
// and switch to visual editor
34+
await vscode.commands.executeCommand("quarto.test_setkVisualModeConfirmedTrue");
35+
await wait(300); // It seems necessary to wait around 300ms for this command to be done.
36+
await vscode.commands.executeCommand("quarto.editInVisualMode");
37+
await wait(APPROX_TIME_TO_OPEN_VISUAL_EDITOR);
38+
39+
assert.ok(await vscode.commands.executeCommand("quarto.test_isInVisualEditor"));
40+
});
41+
// Note: this test runs after the previous test, so `hello.qmd` has already been touched by the previous
42+
// test. That's okay for this test, but could cause issues if you expect a qmd to look how it
43+
// does in `/examples`.
44+
test("Roundtrip doesn't change hello.qmd", async function () {
45+
// don't run this in CI for now because we haven't figured out how to get the LSP to start
46+
if (process.env['CI']) this.skip();
47+
48+
const doc = await vscode.workspace.openTextDocument(exampleWorkspaceOutPath("hello.qmd"));
49+
const editor = await vscode.window.showTextDocument(doc);
50+
51+
const docTextBefore = doc.getText();
52+
53+
// switch to visual editor and back
54+
await vscode.commands.executeCommand("quarto.test_setkVisualModeConfirmedTrue");
55+
await wait(300);
56+
await vscode.commands.executeCommand("quarto.editInVisualMode");
57+
await wait(APPROX_TIME_TO_OPEN_VISUAL_EDITOR);
58+
await vscode.commands.executeCommand("quarto.editInSourceMode");
59+
await wait(300);
60+
61+
const docTextAfter = doc.getText();
62+
assert.ok(docTextBefore === docTextAfter);
63+
});
1364
});

apps/vscode/src/test/test-utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as path from "path";
2+
import * as vscode from "vscode";
23

34

45
/**
@@ -16,3 +17,32 @@ export const WORKSPACE_PATH = path.join(TEST_PATH, "examples");
1617
export function exampleWorkspacePath(file: string): string {
1718
return path.join(WORKSPACE_PATH, file);
1819
}
20+
export function exampleWorkspaceOutPath(file: string): string {
21+
return path.join(WORKSPACE_PATH, 'examples-out', file);
22+
}
23+
24+
export function wait(ms: number) {
25+
return new Promise(resolve => setTimeout(resolve, ms));
26+
}
27+
28+
export async function copyFile(
29+
sourcePath: string,
30+
destPath: string,
31+
): Promise<boolean> {
32+
try {
33+
const wsedit = new vscode.WorkspaceEdit();
34+
const data = await vscode.workspace.fs.readFile(
35+
vscode.Uri.file(sourcePath)
36+
);
37+
const destFileUri = vscode.Uri.file(destPath);
38+
wsedit.createFile(destFileUri, { ignoreIfExists: true });
39+
40+
await vscode.workspace.fs.writeFile(destFileUri, data);
41+
42+
let isDone = await vscode.workspace.applyEdit(wsedit);
43+
if (isDone) return true;
44+
else return false;
45+
} catch (err) {
46+
return false;
47+
}
48+
}

0 commit comments

Comments
 (0)