Skip to content

Commit 0ab55b9

Browse files
michael-wengmatthewbastien
authored andcommitted
add tests for Documentation Live Preview
1 parent ef0f5de commit 0ab55b9

File tree

8 files changed

+280
-0
lines changed

8 files changed

+280
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// swift-tools-version: 6.1
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "documentation-live-preview",
8+
products: [
9+
// Products define the executables and libraries a package produces, making them visible to other packages.
10+
.library(
11+
name: "Library",
12+
targets: ["Library"]),
13+
],
14+
targets: [
15+
// Targets are the basic building blocks of a package, defining a module or a test suite.
16+
// Targets can depend on other targets in this package and products from dependencies.
17+
.target(
18+
name: "Library"),
19+
]
20+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Getting Started
2+
3+
This is the getting started page.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@Tutorial(time: 30) {
2+
@Intro(title: "Library") {
3+
Library Tutorial
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@Tutorials(name: "SlothCreator") {
2+
@Intro(title: "Meet Library") {
3+
Library Tutorial Overview
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// The Swift Programming Language
2+
// https://docs.swift.org/swift-book
3+
4+
/// The entry point for this arbitrary library.
5+
///
6+
/// Used for testing the Documentation Live Preview.
7+
public struct EntryPoint {
8+
/// The name of this EntryPoint
9+
public let name: String
10+
11+
/// Creates a new EntryPoint
12+
/// - Parameter name: the name of this entry point
13+
public init(name: String) {
14+
self.name = name
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Used to test Live Preview with an unsupported file.

src/documentation/DocumentationPreviewEditor.ts

+5
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class DocumentationPreviewEditor implements vscode.Disposable {
9898
private activeTextEditor?: vscode.TextEditor;
9999
private activeTextEditorSelection?: vscode.Selection;
100100
private subscriptions: vscode.Disposable[] = [];
101+
private isDisposed: boolean = false;
101102

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

135136
dispose() {
137+
this.isDisposed = true;
136138
this.subscriptions.forEach(subscription => subscription.dispose());
137139
this.subscriptions = [];
138140
this.webviewPanel.dispose();
139141
this.disposeEmitter.fire();
140142
}
141143

142144
private postMessage(message: WebviewMessage) {
145+
if (this.isDisposed) {
146+
return;
147+
}
143148
if (message.type === "update-content") {
144149
this.updateContentEmitter.fire(message.content);
145150
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2024 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import * as vscode from "vscode";
16+
import * as path from "path";
17+
import contextKeys from "../../../src/contextKeys";
18+
import { expect } from "chai";
19+
import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities";
20+
import { waitForNoRunningTasks } from "../../utilities/tasks";
21+
import { testAssetUri } from "../../fixtures";
22+
import { FolderContext } from "../../../src/FolderContext";
23+
import { WorkspaceContext } from "../../../src/WorkspaceContext";
24+
import { Commands } from "../../../src/commands";
25+
import { Workbench } from "../../../src/utilities/commands";
26+
import {
27+
RenderNodeContent,
28+
WebviewContent,
29+
} from "../../../src/documentation/webview/WebviewMessage";
30+
import { PreviewEditorConstant } from "../../../src/documentation/DocumentationPreviewEditor";
31+
32+
suite("Documentation Live Preview", function () {
33+
// Tests are short, but rely on SourceKit-LSP: give 30 seconds for each one
34+
this.timeout(30 * 1000);
35+
36+
let folderContext: FolderContext;
37+
let workspaceContext: WorkspaceContext;
38+
39+
activateExtensionForSuite({
40+
async setup(ctx) {
41+
workspaceContext = ctx;
42+
await waitForNoRunningTasks();
43+
folderContext = await folderInRootWorkspace("documentation-live-preview", ctx);
44+
await ctx.focusFolder(folderContext);
45+
},
46+
});
47+
48+
setup(function () {
49+
if (!contextKeys.supportsDocumentationLivePreview) {
50+
this.skip();
51+
}
52+
});
53+
54+
teardown(async function () {
55+
await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS);
56+
});
57+
58+
test("renders documentation for an opened Swift file", async function () {
59+
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
60+
filePath: "Sources/Library/Library.swift",
61+
position: new vscode.Position(0, 0),
62+
});
63+
expect(renderNodeString(webviewContent)).to.include(
64+
"The entry point for this arbitrary library."
65+
);
66+
});
67+
68+
test("renders documentation when moving the cursor within an opened Swift file", async function () {
69+
const { textEditor } = await launchLivePreviewEditor(workspaceContext, {
70+
filePath: "Sources/Library/Library.swift",
71+
position: new vscode.Position(0, 0),
72+
});
73+
// Move the cursor to the comment above EntryPoint.name
74+
let webviewContent = await moveCursor(workspaceContext, {
75+
textEditor,
76+
position: new vscode.Position(7, 12),
77+
});
78+
expect(renderNodeString(webviewContent)).to.include("The name of this EntryPoint");
79+
// Move the cursor to the comment above EntryPoint.init(name:)
80+
webviewContent = await moveCursor(workspaceContext, {
81+
textEditor,
82+
position: new vscode.Position(10, 18),
83+
});
84+
expect(renderNodeString(webviewContent)).to.include("Creates a new EntryPoint");
85+
});
86+
87+
test("renders documentation when editing an opened Swift file", async function () {
88+
const { textEditor } = await launchLivePreviewEditor(workspaceContext, {
89+
filePath: "Sources/Library/Library.swift",
90+
position: new vscode.Position(0, 0),
91+
});
92+
// Edit the comment above EntryPoint
93+
const webviewContent = await editDocument(workspaceContext, textEditor, editBuilder => {
94+
editBuilder.replace(new vscode.Selection(3, 29, 3, 38), "absolutely amazing");
95+
});
96+
expect(renderNodeString(webviewContent)).to.include(
97+
"The entry point for this absolutely amazing library."
98+
);
99+
});
100+
101+
test("renders documentation for an opened Markdown article", async function () {
102+
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
103+
filePath: "Sources/Library/Library.docc/GettingStarted.md",
104+
position: new vscode.Position(0, 0),
105+
});
106+
expect(renderNodeString(webviewContent)).to.include("This is the getting started page.");
107+
});
108+
109+
test("renders documentation for an opened tutorial overview", async function () {
110+
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
111+
filePath: "Sources/Library/Library.docc/TutorialOverview.tutorial",
112+
position: new vscode.Position(0, 0),
113+
});
114+
expect(renderNodeString(webviewContent)).to.include("Library Tutorial Overview");
115+
});
116+
117+
test("renders documentation for an opened tutorial", async function () {
118+
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
119+
filePath: "Sources/Library/Library.docc/Tutorial.tutorial",
120+
position: new vscode.Position(0, 0),
121+
});
122+
expect(renderNodeString(webviewContent)).to.include("Library Tutorial");
123+
});
124+
125+
test("displays an error for an unsupported active document", async function () {
126+
const { webviewContent } = await launchLivePreviewEditor(workspaceContext, {
127+
filePath: "UnsupportedFile.txt",
128+
position: new vscode.Position(0, 0),
129+
});
130+
expect(webviewContent).to.have.property("type").that.equals("error");
131+
expect(webviewContent)
132+
.to.have.property("errorMessage")
133+
.that.equals(PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE);
134+
});
135+
});
136+
137+
async function launchLivePreviewEditor(
138+
workspaceContext: WorkspaceContext,
139+
options: {
140+
filePath: string;
141+
position: vscode.Position;
142+
}
143+
): Promise<{ textEditor: vscode.TextEditor; webviewContent: WebviewContent }> {
144+
if (findTab(PreviewEditorConstant.VIEW_TYPE, PreviewEditorConstant.TITLE)) {
145+
throw new Error("The live preview editor cannot be launched twice in a single test");
146+
}
147+
const contentUpdatePromise = waitForNextContentUpdate(workspaceContext);
148+
const renderedPromise = waitForNextRender(workspaceContext);
149+
// Open up the test file before launching live preview
150+
const fileUri = testAssetUri(path.join("documentation-live-preview", options.filePath));
151+
const selection = new vscode.Selection(options.position, options.position);
152+
const textEditor = await vscode.window.showTextDocument(fileUri, { selection: selection });
153+
// Launch the documentation preview and wait for it to render
154+
expect(await vscode.commands.executeCommand(Commands.PREVIEW_DOCUMENTATION)).to.be.true;
155+
const [webviewContent] = await Promise.all([contentUpdatePromise, renderedPromise]);
156+
return { textEditor, webviewContent };
157+
}
158+
159+
async function editDocument(
160+
workspaceContext: WorkspaceContext,
161+
textEditor: vscode.TextEditor,
162+
callback: (editBuilder: vscode.TextEditorEdit) => void
163+
): Promise<WebviewContent> {
164+
const contentUpdatePromise = waitForNextContentUpdate(workspaceContext);
165+
const renderedPromise = waitForNextRender(workspaceContext);
166+
await expect(textEditor.edit(callback)).to.eventually.be.true;
167+
const [webviewContent] = await Promise.all([contentUpdatePromise, renderedPromise]);
168+
return webviewContent;
169+
}
170+
171+
async function moveCursor(
172+
workspaceContext: WorkspaceContext,
173+
options: {
174+
textEditor: vscode.TextEditor;
175+
position: vscode.Position;
176+
}
177+
): Promise<WebviewContent> {
178+
const contentUpdatePromise = waitForNextContentUpdate(workspaceContext);
179+
const renderedPromise = waitForNextRender(workspaceContext);
180+
options.textEditor.selection = new vscode.Selection(options.position, options.position);
181+
const [webviewContent] = await Promise.all([contentUpdatePromise, renderedPromise]);
182+
return webviewContent;
183+
}
184+
185+
function renderNodeString(webviewContent: WebviewContent): string {
186+
expect(webviewContent).to.have.property("type").that.equals("render-node");
187+
return JSON.stringify((webviewContent as RenderNodeContent).renderNode);
188+
}
189+
190+
function waitForNextContentUpdate(context: WorkspaceContext): Promise<WebviewContent> {
191+
return new Promise<WebviewContent>(resolve => {
192+
const disposable = context.documentation.onPreviewDidUpdateContent(
193+
(content: WebviewContent) => {
194+
resolve(content);
195+
disposable.dispose();
196+
}
197+
);
198+
});
199+
}
200+
201+
function waitForNextRender(context: WorkspaceContext): Promise<boolean> {
202+
return new Promise<boolean>(resolve => {
203+
const disposable = context.documentation.onPreviewDidRenderContent(() => {
204+
resolve(true);
205+
disposable.dispose();
206+
});
207+
});
208+
}
209+
210+
function findTab(viewType: string, title: string): vscode.Tab | undefined {
211+
for (const group of vscode.window.tabGroups.all) {
212+
for (const tab of group.tabs) {
213+
// Check if the tab is of type TabInputWebview and matches the viewType and title
214+
if (
215+
tab.input instanceof vscode.TabInputWebview &&
216+
tab.input.viewType.includes(viewType) &&
217+
tab.label === title
218+
) {
219+
// We are not checking if tab is active, so return true as long as the if clause is true
220+
return tab;
221+
}
222+
}
223+
}
224+
return undefined;
225+
}

0 commit comments

Comments
 (0)