Skip to content

Add tests and test support commands for visual editor #770

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

Merged
merged 6 commits into from
Jul 24, 2025
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
8 changes: 8 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ jobs:
sudo apt-get -y update
sudo apt-get -y install --fix-missing xvfb

- uses: quarto-dev/quarto-actions/setup@v2

- name: Build vscode extension
run: |
cd apps/vscode
yarn install
yarn run build

- name: Compile and run tests
run: |
yarn install --immutable --immutable-cache --check-cache
Expand Down
14 changes: 8 additions & 6 deletions apps/lsp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import {
WorkspaceSymbol
} from "vscode-languageserver";

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

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

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




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

return mdLs?.getCompletionItems(document, params.position, params.context, config, token) || [];
})
});

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


connection.onDocumentLinks(async (params, token): Promise<DocumentLink[]> => {
Expand Down Expand Up @@ -249,7 +251,7 @@ connection.onInitialized(async () => {
capabilities!,
config,
logger
)
);

// initialize parser
const parser = markdownitParser();
Expand Down Expand Up @@ -279,7 +281,7 @@ connection.onInitialized(async () => {
onRequest(method: string, handler: (params: unknown[]) => Promise<unknown>) {
return connection.onRequest(method, handler);
}
}
};

// register custom methods
registerCustomMethods(quarto, lspConnection, documents);
Expand Down
1 change: 1 addition & 0 deletions apps/vscode/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
out/
*.vsix
test-out/
examples-out/
3 changes: 3 additions & 0 deletions apps/vscode/.vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ export default defineConfig([
{
files: 'test-out/*.test.js',
workspaceFolder: 'src/test/examples',
mocha: {
timeout: 3000,
},
},
]);
18 changes: 15 additions & 3 deletions apps/vscode/src/providers/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ import {
import { ExtensionHost } from "../../host";
import { TabInputCustom } from "vscode";

const kVisualModeConfirmed = "visualModeConfirmed";

export interface QuartoVisualEditor extends QuartoEditor {
hasFocus(): Promise<boolean>;
getActiveBlockContext(): Promise<CodeViewActiveBlockContext | null>;
Expand All @@ -87,6 +89,18 @@ export function activateEditor(

// return commands
return [
{
id: 'quarto.test_setkVisualModeConfirmedTrue',
execute() {
context.globalState.update(kVisualModeConfirmed, true);
}
},
{
id: 'quarto.test_isInVisualEditor',
execute() {
return VisualEditorProvider.activeEditor() !== undefined;
}
},
editInVisualModeCommand(),
editInSourceModeCommand(),
toggleEditModeCommand(),
Expand All @@ -99,7 +113,7 @@ export class VisualEditorProvider implements CustomTextEditorProvider {

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

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


public async resolveCustomTextEditor(
document: TextDocument,
webviewPanel: WebviewPanel,
Expand All @@ -333,7 +346,6 @@ export class VisualEditorProvider implements CustomTextEditorProvider {
};

// prompt the user
const kVisualModeConfirmed = "visualModeConfirmed";

// Check for environment variables to force the state of the visual editor confirmation modal
// QUARTO_VISUAL_EDITOR_CONFIRMED > PW_TEST > CI
Expand Down
2 changes: 2 additions & 0 deletions apps/vscode/src/test/examples/hello.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ format: html
```{python}
1 + 1
```

*YO!*
13 changes: 13 additions & 0 deletions apps/vscode/src/test/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as vscode from "vscode";

export const QUARTO_EXTENSION_ID = 'quarto.quarto';

export function extension() {
const extension = vscode.extensions.getExtension(QUARTO_EXTENSION_ID);

if (extension === undefined) {
throw new Error(`Extension ${QUARTO_EXTENSION_ID} not found`);
}

return extension;
}
59 changes: 55 additions & 4 deletions apps/vscode/src/test/quartoDoc.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,64 @@
import * as vscode from "vscode";
import * as assert from "assert";
import { exampleWorkspacePath } from "./test-utils";
import { exampleWorkspacePath, exampleWorkspaceOutPath, copyFile, wait } from "./test-utils";
import { isQuartoDoc } from "../core/doc";
import { extension } from "./extension";

suite("Quarto basics", () => {
test("Can open a Quarto document", async () => {
const doc = await vscode.workspace.openTextDocument(exampleWorkspacePath("hello.qmd"));
const APPROX_TIME_TO_OPEN_VISUAL_EDITOR = 1600;

suite("Quarto basics", function () {
// Before we run any tests, we should copy any files that get edited in the tests to file under `exampleWorkspaceOutPath`
suiteSetup(async function () {
const didCopyFile = await copyFile(exampleWorkspacePath('hello.qmd'), exampleWorkspaceOutPath('hello.qmd'));
assert.ok(didCopyFile);
});

test("Can open a Quarto document", async function () {
const doc = await vscode.workspace.openTextDocument(exampleWorkspaceOutPath("hello.qmd"));
const editor = await vscode.window.showTextDocument(doc);

assert.strictEqual(editor?.document.languageId, "quarto");
assert.strictEqual(isQuartoDoc(editor?.document), true);
});

// Note: the following tests may be flaky. They rely on waiting estimated amounts of time for commands to complete.
test("Can edit in visual mode", async function () {
// don't run this in CI for now because we haven't figured out how to get the LSP to start
if (process.env['CI']) this.skip();

const doc = await vscode.workspace.openTextDocument(exampleWorkspaceOutPath("hello.qmd"));
const editor = await vscode.window.showTextDocument(doc);

// manually confirm visual mode so dialogue pop-up doesn't show because dialogues cause test errors
// and switch to visual editor
await vscode.commands.executeCommand("quarto.test_setkVisualModeConfirmedTrue");
await wait(300); // It seems necessary to wait around 300ms for this command to be done.
await vscode.commands.executeCommand("quarto.editInVisualMode");
await wait(APPROX_TIME_TO_OPEN_VISUAL_EDITOR);

assert.ok(await vscode.commands.executeCommand("quarto.test_isInVisualEditor"));
});
// Note: this test runs after the previous test, so `hello.qmd` has already been touched by the previous
// test. That's okay for this test, but could cause issues if you expect a qmd to look how it
// does in `/examples`.
test("Roundtrip doesn't change hello.qmd", async function () {
// don't run this in CI for now because we haven't figured out how to get the LSP to start
if (process.env['CI']) this.skip();

const doc = await vscode.workspace.openTextDocument(exampleWorkspaceOutPath("hello.qmd"));
const editor = await vscode.window.showTextDocument(doc);

const docTextBefore = doc.getText();

// switch to visual editor and back
await vscode.commands.executeCommand("quarto.test_setkVisualModeConfirmedTrue");
await wait(300);
await vscode.commands.executeCommand("quarto.editInVisualMode");
await wait(APPROX_TIME_TO_OPEN_VISUAL_EDITOR);
await vscode.commands.executeCommand("quarto.editInSourceMode");
await wait(300);

const docTextAfter = doc.getText();
assert.ok(docTextBefore === docTextAfter);
});
});
30 changes: 30 additions & 0 deletions apps/vscode/src/test/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as path from "path";
import * as vscode from "vscode";


/**
Expand All @@ -16,3 +17,32 @@ export const WORKSPACE_PATH = path.join(TEST_PATH, "examples");
export function exampleWorkspacePath(file: string): string {
return path.join(WORKSPACE_PATH, file);
}
export function exampleWorkspaceOutPath(file: string): string {
return path.join(WORKSPACE_PATH, 'examples-out', file);
}

export function wait(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

export async function copyFile(
sourcePath: string,
destPath: string,
): Promise<boolean> {
try {
const wsedit = new vscode.WorkspaceEdit();
const data = await vscode.workspace.fs.readFile(
vscode.Uri.file(sourcePath)
);
const destFileUri = vscode.Uri.file(destPath);
wsedit.createFile(destFileUri, { ignoreIfExists: true });

await vscode.workspace.fs.writeFile(destFileUri, data);

let isDone = await vscode.workspace.applyEdit(wsedit);
if (isDone) return true;
else return false;
} catch (err) {
return false;
}
}