Skip to content

Add tests for Documentation Live Preview #1285

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
20 changes: 20 additions & 0 deletions assets/test/documentation-live-preview/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "documentation-live-preview",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "Library",
targets: ["Library"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "Library"),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Getting Started

This is the getting started page.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@Tutorial(time: 30) {
@Intro(title: "Library") {
Library Tutorial
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@Tutorials(name: "SlothCreator") {
@Intro(title: "Meet Library") {
Library Tutorial Overview
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

/// The entry point for this arbitrary library.
///
/// Used for testing the Documentation Live Preview.
public struct EntryPoint {
/// The name of this EntryPoint
public let name: String

/// Creates a new EntryPoint
/// - Parameter name: the name of this entry point
public init(name: String) {
self.name = name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Used to test Live Preview with an unsupported file.
5 changes: 5 additions & 0 deletions src/documentation/DocumentationPreviewEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class DocumentationPreviewEditor implements vscode.Disposable {
private activeTextEditor?: vscode.TextEditor;
private activeTextEditorSelection?: vscode.Selection;
private subscriptions: vscode.Disposable[] = [];
private isDisposed: boolean = false;

private disposeEmitter = new vscode.EventEmitter<void>();
private renderEmitter = new vscode.EventEmitter<void>();
Expand Down Expand Up @@ -133,13 +134,17 @@ export class DocumentationPreviewEditor implements vscode.Disposable {
}

dispose() {
this.isDisposed = true;
this.subscriptions.forEach(subscription => subscription.dispose());
this.subscriptions = [];
this.webviewPanel.dispose();
this.disposeEmitter.fire();
}

private postMessage(message: WebviewMessage) {
if (this.isDisposed) {
return;
}
if (message.type === "update-content") {
this.updateContentEmitter.fire(message.content);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2024 the VS Code Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import * as vscode from "vscode";
import * as path from "path";
import contextKeys from "../../../src/contextKeys";
import { expect } from "chai";
import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities";
import { waitForNoRunningTasks } from "../../utilities/tasks";
import { testAssetUri } from "../../fixtures";
import { FolderContext } from "../../../src/FolderContext";
import { WorkspaceContext } from "../../../src/WorkspaceContext";
import { Commands } from "../../../src/commands";
import { Workbench } from "../../../src/utilities/commands";
import {
RenderNodeContent,
WebviewContent,
} from "../../../src/documentation/webview/WebviewMessage";
import { PreviewEditorConstant } from "../../../src/documentation/DocumentationPreviewEditor";

suite("Documentation Live Preview", function () {
// Tests are short, but rely on SourceKit-LSP: give 30 seconds for each one
this.timeout(30 * 1000);

let folderContext: FolderContext;
let workspaceContext: WorkspaceContext;

activateExtensionForSuite({
async setup(ctx) {
workspaceContext = ctx;
await waitForNoRunningTasks();
folderContext = await folderInRootWorkspace("documentation-live-preview", ctx);
await ctx.focusFolder(folderContext);
},
});

setup(function () {
if (!contextKeys.supportsDocumentationLivePreview) {
this.skip();
}
});

teardown(async function () {
await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS);
});

test("renders documentation for an opened Swift file", async function () {
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
filePath: "Sources/Library/Library.swift",
position: new vscode.Position(0, 0),
});
expect(renderNodeString(webviewContent)).to.include(
"The entry point for this arbitrary library."
);
});

test("renders documentation when moving the cursor within an opened Swift file", async function () {
const { textEditor } = await launchLivePreviewEditor(workspaceContext, {
filePath: "Sources/Library/Library.swift",
position: new vscode.Position(0, 0),
});
// Move the cursor to the comment above EntryPoint.name
let webviewContent = await moveCursor(workspaceContext, {
textEditor,
position: new vscode.Position(7, 12),
});
expect(renderNodeString(webviewContent)).to.include("The name of this EntryPoint");
// Move the cursor to the comment above EntryPoint.init(name:)
webviewContent = await moveCursor(workspaceContext, {
textEditor,
position: new vscode.Position(10, 18),
});
expect(renderNodeString(webviewContent)).to.include("Creates a new EntryPoint");
});

test("renders documentation when editing an opened Swift file", async function () {
const { textEditor } = await launchLivePreviewEditor(workspaceContext, {
filePath: "Sources/Library/Library.swift",
position: new vscode.Position(0, 0),
});
// Edit the comment above EntryPoint
const webviewContent = await editDocument(workspaceContext, textEditor, editBuilder => {
editBuilder.replace(new vscode.Selection(3, 29, 3, 38), "absolutely amazing");
});
expect(renderNodeString(webviewContent)).to.include(
"The entry point for this absolutely amazing library."
);
});

test("renders documentation for an opened Markdown article", async function () {
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
filePath: "Sources/Library/Library.docc/GettingStarted.md",
position: new vscode.Position(0, 0),
});
expect(renderNodeString(webviewContent)).to.include("This is the getting started page.");
});

test("renders documentation for an opened tutorial overview", async function () {
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
filePath: "Sources/Library/Library.docc/TutorialOverview.tutorial",
position: new vscode.Position(0, 0),
});
expect(renderNodeString(webviewContent)).to.include("Library Tutorial Overview");
});

test("renders documentation for an opened tutorial", async function () {
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
filePath: "Sources/Library/Library.docc/Tutorial.tutorial",
position: new vscode.Position(0, 0),
});
expect(renderNodeString(webviewContent)).to.include("Library Tutorial");
});

test("displays an error for an unsupported active document", async function () {
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
filePath: "UnsupportedFile.txt",
position: new vscode.Position(0, 0),
});
expect(webviewContent).to.have.property("type").that.equals("error");
expect(webviewContent)
.to.have.property("errorMessage")
.that.equals(PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE);
});
});

async function launchLivePreviewEditor(
workspaceContext: WorkspaceContext,
options: {
filePath: string;
position: vscode.Position;
}
): Promise<{ textEditor: vscode.TextEditor; webviewContent: WebviewContent }> {
if (findTab(PreviewEditorConstant.VIEW_TYPE, PreviewEditorConstant.TITLE)) {
throw new Error("The live preview editor cannot be launched twice in a single test");
}
const contentUpdatePromise = waitForNextContentUpdate(workspaceContext);
const renderedPromise = waitForNextRender(workspaceContext);
// Open up the test file before launching live preview
const fileUri = testAssetUri(path.join("documentation-live-preview", options.filePath));
const selection = new vscode.Selection(options.position, options.position);
const textEditor = await vscode.window.showTextDocument(fileUri, { selection: selection });
// Launch the documentation preview and wait for it to render
expect(await vscode.commands.executeCommand(Commands.PREVIEW_DOCUMENTATION)).to.be.true;
const [webviewContent] = await Promise.all([contentUpdatePromise, renderedPromise]);
return { textEditor, webviewContent };
}

async function editDocument(
workspaceContext: WorkspaceContext,
textEditor: vscode.TextEditor,
callback: (editBuilder: vscode.TextEditorEdit) => void
): Promise<WebviewContent> {
const contentUpdatePromise = waitForNextContentUpdate(workspaceContext);
const renderedPromise = waitForNextRender(workspaceContext);
await expect(textEditor.edit(callback)).to.eventually.be.true;
const [webviewContent] = await Promise.all([contentUpdatePromise, renderedPromise]);
return webviewContent;
}

async function moveCursor(
workspaceContext: WorkspaceContext,
options: {
textEditor: vscode.TextEditor;
position: vscode.Position;
}
): Promise<WebviewContent> {
const contentUpdatePromise = waitForNextContentUpdate(workspaceContext);
const renderedPromise = waitForNextRender(workspaceContext);
options.textEditor.selection = new vscode.Selection(options.position, options.position);
const [webviewContent] = await Promise.all([contentUpdatePromise, renderedPromise]);
return webviewContent;
}

function renderNodeString(webviewContent: WebviewContent): string {
expect(webviewContent).to.have.property("type").that.equals("render-node");
return JSON.stringify((webviewContent as RenderNodeContent).renderNode);
}

function waitForNextContentUpdate(context: WorkspaceContext): Promise<WebviewContent> {
return new Promise<WebviewContent>(resolve => {
const disposable = context.documentation.onPreviewDidUpdateContent(
(content: WebviewContent) => {
resolve(content);
disposable.dispose();
}
);
});
}

function waitForNextRender(context: WorkspaceContext): Promise<boolean> {
return new Promise<boolean>(resolve => {
const disposable = context.documentation.onPreviewDidRenderContent(() => {
resolve(true);
disposable.dispose();
});
});
}

function findTab(viewType: string, title: string): vscode.Tab | undefined {
for (const group of vscode.window.tabGroups.all) {
for (const tab of group.tabs) {
// Check if the tab is of type TabInputWebview and matches the viewType and title
if (
tab.input instanceof vscode.TabInputWebview &&
tab.input.viewType.includes(viewType) &&
tab.label === title
) {
// We are not checking if tab is active, so return true as long as the if clause is true
return tab;
}
}
}
return undefined;
}
Loading