diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e69f3360d..bb31c71c3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -49,7 +49,7 @@ jobs: with: needs_token: true # Linux - linux_exclude_swift_versions: '[{"swift_version": "nightly-6.1"},{"swift_version": "nightly-6.2"},{"swift_version": "nightly-main"}]' + linux_exclude_swift_versions: "${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '[{\"swift_version\": \"nightly-6.1\"}]' || '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly-6.2\"},{\"swift_version\": \"nightly-main\"}]' }}" linux_env_vars: | NODE_VERSION=v20.19.0 NODE_PATH=/usr/local/nvm/versions/node/v20.19.0/bin @@ -61,7 +61,7 @@ jobs: linux_pre_build_command: . .github/workflows/scripts/setup-linux.sh linux_build_command: ./scripts/test.sh # Windows - windows_exclude_swift_versions: '[{"swift_version": "nightly-6.1"},{"swift_version": "nightly-6.2"},{"swift_version": "nightly"}]' + windows_exclude_swift_versions: "${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly\"}]' || '[{\"swift_version\": \"nightly-6.1\"},{\"swift_version\": \"nightly-6.2\"},{\"swift_version\": \"nightly\"}]' }}" windows_env_vars: | CI=1 FAST_TEST_RUN=${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '0' || '1'}} diff --git a/assets/test/documentation-live-preview/Package.swift b/assets/test/documentation-live-preview/Package.swift new file mode 100644 index 000000000..82f5c8a15 --- /dev/null +++ b/assets/test/documentation-live-preview/Package.swift @@ -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"), + ] +) diff --git a/assets/test/documentation-live-preview/Sources/Library/Library.docc/GettingStarted.md b/assets/test/documentation-live-preview/Sources/Library/Library.docc/GettingStarted.md new file mode 100644 index 000000000..0de13a7cf --- /dev/null +++ b/assets/test/documentation-live-preview/Sources/Library/Library.docc/GettingStarted.md @@ -0,0 +1,3 @@ +# Getting Started + +This is the getting started page. \ No newline at end of file diff --git a/assets/test/documentation-live-preview/Sources/Library/Library.docc/Tutorial.tutorial b/assets/test/documentation-live-preview/Sources/Library/Library.docc/Tutorial.tutorial new file mode 100644 index 000000000..00f6ffb13 --- /dev/null +++ b/assets/test/documentation-live-preview/Sources/Library/Library.docc/Tutorial.tutorial @@ -0,0 +1,5 @@ +@Tutorial(time: 30) { + @Intro(title: "Library") { + Library Tutorial + } +} diff --git a/assets/test/documentation-live-preview/Sources/Library/Library.docc/TutorialOverview.tutorial b/assets/test/documentation-live-preview/Sources/Library/Library.docc/TutorialOverview.tutorial new file mode 100644 index 000000000..3cef7bbde --- /dev/null +++ b/assets/test/documentation-live-preview/Sources/Library/Library.docc/TutorialOverview.tutorial @@ -0,0 +1,5 @@ +@Tutorials(name: "SlothCreator") { + @Intro(title: "Meet Library") { + Library Tutorial Overview + } +} diff --git a/assets/test/documentation-live-preview/Sources/Library/Library.swift b/assets/test/documentation-live-preview/Sources/Library/Library.swift new file mode 100644 index 000000000..2f9e68f77 --- /dev/null +++ b/assets/test/documentation-live-preview/Sources/Library/Library.swift @@ -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 + } +} \ No newline at end of file diff --git a/assets/test/documentation-live-preview/UnsupportedFile.txt b/assets/test/documentation-live-preview/UnsupportedFile.txt new file mode 100644 index 000000000..c18a55cae --- /dev/null +++ b/assets/test/documentation-live-preview/UnsupportedFile.txt @@ -0,0 +1 @@ +Used to test Live Preview with an unsupported file. \ No newline at end of file diff --git a/src/SwiftSnippets.ts b/src/SwiftSnippets.ts index d6aff6e13..1e133b17a 100644 --- a/src/SwiftSnippets.ts +++ b/src/SwiftSnippets.ts @@ -14,7 +14,6 @@ import * as vscode from "vscode"; import * as path from "path"; -import contextKeys from "./contextKeys"; import { createSwiftTask } from "./tasks/SwiftTaskProvider"; import { WorkspaceContext } from "./WorkspaceContext"; import { createSnippetConfiguration, debugLaunchConfig } from "./debugger/launch"; @@ -30,16 +29,16 @@ export function setSnippetContextKey(ctx: WorkspaceContext) { !ctx.currentDocument || ctx.currentFolder.swiftVersion.isLessThan({ major: 5, minor: 7, patch: 0 }) ) { - contextKeys.fileIsSnippet = false; + ctx.contextKeys.fileIsSnippet = false; return; } const filename = ctx.currentDocument.fsPath; const snippetsFolder = path.join(ctx.currentFolder.folder.fsPath, "Snippets"); if (filename.startsWith(snippetsFolder)) { - contextKeys.fileIsSnippet = true; + ctx.contextKeys.fileIsSnippet = true; } else { - contextKeys.fileIsSnippet = false; + ctx.contextKeys.fileIsSnippet = false; } return; } diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index 252f67f65..6c22057f6 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -22,7 +22,7 @@ import { LanguageClientToolchainCoordinator } from "./sourcekit-lsp/LanguageClie import { TaskManager } from "./tasks/TaskManager"; import { makeDebugConfigurations } from "./debugger/launch"; import configuration from "./configuration"; -import contextKeys from "./contextKeys"; +import { ContextKeys } from "./contextKeys"; import { setSnippetContextKey } from "./SwiftSnippets"; import { CommentCompletionProviders } from "./editor/CommentCompletion"; import { SwiftBuildStatus } from "./ui/SwiftBuildStatus"; @@ -77,6 +77,7 @@ export class WorkspaceContext implements vscode.Disposable { constructor( extensionContext: vscode.ExtensionContext, + public contextKeys: ContextKeys, public logger: SwiftLogger, public globalToolchain: SwiftToolchain ) { @@ -233,9 +234,9 @@ export class WorkspaceContext implements vscode.Disposable { */ updateContextKeys(folderContext: FolderContext | null) { if (!folderContext) { - contextKeys.hasPackage = false; - contextKeys.hasExecutableProduct = false; - contextKeys.packageHasDependencies = false; + this.contextKeys.hasPackage = false; + this.contextKeys.hasExecutableProduct = false; + this.contextKeys.packageHasDependencies = false; return; } @@ -244,9 +245,9 @@ export class WorkspaceContext implements vscode.Disposable { folderContext.swiftPackage.executableProducts, folderContext.swiftPackage.dependencies, ]).then(([foundPackage, executableProducts, dependencies]) => { - contextKeys.hasPackage = foundPackage; - contextKeys.hasExecutableProduct = executableProducts.length > 0; - contextKeys.packageHasDependencies = dependencies.length > 0; + this.contextKeys.hasPackage = foundPackage; + this.contextKeys.hasExecutableProduct = executableProducts.length > 0; + this.contextKeys.packageHasDependencies = dependencies.length > 0; }); } @@ -258,9 +259,9 @@ export class WorkspaceContext implements vscode.Disposable { const target = await this.currentFolder?.swiftPackage.getTarget( this.currentDocument?.fsPath ); - contextKeys.currentTargetType = target?.type; + this.contextKeys.currentTargetType = target?.type; } else { - contextKeys.currentTargetType = undefined; + this.contextKeys.currentTargetType = undefined; } if (this.currentFolder) { @@ -268,13 +269,13 @@ export class WorkspaceContext implements vscode.Disposable { await languageClient.useLanguageClient(async client => { const experimentalCaps = client.initializeResult?.capabilities.experimental; if (!experimentalCaps) { - contextKeys.supportsReindexing = false; - contextKeys.supportsDocumentationLivePreview = false; + this.contextKeys.supportsReindexing = false; + this.contextKeys.supportsDocumentationLivePreview = false; return; } - contextKeys.supportsReindexing = + this.contextKeys.supportsReindexing = experimentalCaps[ReIndexProjectRequest.method] !== undefined; - contextKeys.supportsDocumentationLivePreview = + this.contextKeys.supportsDocumentationLivePreview = experimentalCaps[DocCDocumentationRequest.method] !== undefined; }); } @@ -293,7 +294,7 @@ export class WorkspaceContext implements vscode.Disposable { break; } } - contextKeys.packageHasPlugins = hasPlugins; + this.contextKeys.packageHasPlugins = hasPlugins; } /** Setup the vscode event listeners to catch folder changes and active window changes */ diff --git a/src/commands/dependencies/updateDepViewList.ts b/src/commands/dependencies/updateDepViewList.ts index 42293619d..0cb0d566c 100644 --- a/src/commands/dependencies/updateDepViewList.ts +++ b/src/commands/dependencies/updateDepViewList.ts @@ -12,12 +12,11 @@ // //===----------------------------------------------------------------------===// -import contextKeys from "../../contextKeys"; import { FolderOperation, WorkspaceContext } from "../../WorkspaceContext"; export function updateDependenciesViewList(ctx: WorkspaceContext, flatList: boolean) { if (ctx.currentFolder) { - contextKeys.flatDependenciesList = flatList; + ctx.contextKeys.flatDependenciesList = flatList; void ctx.fireEvent(ctx.currentFolder, FolderOperation.packageViewUpdated); } } diff --git a/src/contextKeys.ts b/src/contextKeys.ts index 348c73c3c..8d7160a5c 100644 --- a/src/contextKeys.ts +++ b/src/contextKeys.ts @@ -23,7 +23,7 @@ import { Version } from "./utilities/version"; */ /** Interface for getting and setting the VS Code Swift extension's context keys */ -interface ContextKeys { +export interface ContextKeys { /** * Whether or not the swift extension is activated. */ @@ -96,7 +96,7 @@ interface ContextKeys { } /** Creates the getters and setters for the VS Code Swift extension's context keys. */ -function createContextKeys(): ContextKeys { +export function createContextKeys(): ContextKeys { let isActivated: boolean = false; let hasPackage: boolean = false; let hasExecutableProduct: boolean = false; @@ -292,10 +292,3 @@ function createContextKeys(): ContextKeys { }, }; } - -/** - * Type-safe wrapper around context keys used in `when` clauses. - */ -const contextKeys: ContextKeys = createContextKeys(); - -export default contextKeys; diff --git a/src/documentation/DocumentationManager.ts b/src/documentation/DocumentationManager.ts index 2725e417c..703beac69 100644 --- a/src/documentation/DocumentationManager.ts +++ b/src/documentation/DocumentationManager.ts @@ -16,7 +16,6 @@ import * as vscode from "vscode"; import { DocumentationPreviewEditor } from "./DocumentationPreviewEditor"; import { WorkspaceContext } from "../WorkspaceContext"; import { WebviewContent } from "./webview/WebviewMessage"; -import contextKeys from "../contextKeys"; export class DocumentationManager implements vscode.Disposable { private previewEditor?: DocumentationPreviewEditor; @@ -25,26 +24,26 @@ export class DocumentationManager implements vscode.Disposable { constructor( private readonly extension: vscode.ExtensionContext, - private readonly context: WorkspaceContext + private readonly workspaceContext: WorkspaceContext ) {} onPreviewDidUpdateContent = this.editorUpdatedContentEmitter.event; onPreviewDidRenderContent = this.editorRenderedEmitter.event; async launchDocumentationPreview(): Promise { - if (!contextKeys.supportsDocumentationLivePreview) { + if (!this.workspaceContext.contextKeys.supportsDocumentationLivePreview) { return false; } if (!this.previewEditor) { - const folderContext = this.context.currentFolder; + const folderContext = this.workspaceContext.currentFolder; if (!folderContext) { return false; } this.previewEditor = await DocumentationPreviewEditor.create( this.extension, - this.context + this.workspaceContext ); const subscriptions: vscode.Disposable[] = [ this.previewEditor.onDidUpdateContent(content => { diff --git a/src/documentation/DocumentationPreviewEditor.ts b/src/documentation/DocumentationPreviewEditor.ts index dffaa4213..3f971d048 100644 --- a/src/documentation/DocumentationPreviewEditor.ts +++ b/src/documentation/DocumentationPreviewEditor.ts @@ -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(); private renderEmitter = new vscode.EventEmitter(); @@ -133,6 +134,7 @@ export class DocumentationPreviewEditor implements vscode.Disposable { } dispose() { + this.isDisposed = true; this.subscriptions.forEach(subscription => subscription.dispose()); this.subscriptions = []; this.webviewPanel.dispose(); @@ -140,6 +142,9 @@ export class DocumentationPreviewEditor implements vscode.Disposable { } private postMessage(message: WebviewMessage) { + if (this.isDisposed) { + return; + } if (message.type === "update-content") { this.updateContentEmitter.fire(message.content); } diff --git a/src/extension.ts b/src/extension.ts index 6c1f35a2f..d0defd677 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -34,10 +34,10 @@ import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from " import { resolveFolderDependencies } from "./commands/dependencies/resolve"; import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher"; import configuration, { handleConfigurationChangeEvent } from "./configuration"; -import contextKeys from "./contextKeys"; import { registerSourceKitSchemaWatcher } from "./commands/generateSourcekitConfiguration"; import { SwiftLogger } from "./logging/SwiftLogger"; import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory"; +import { ContextKeys, createContextKeys } from "./contextKeys"; /** * External API as exposed by the extension. Can be queried by other extensions @@ -65,7 +65,8 @@ export async function activate(context: vscode.ExtensionContext): Promise { checkAndWarnAboutWindowsSymlinks(logger); - const toolchain = await createActiveToolchain(logger); + const contextKeys = createContextKeys(); + const toolchain = await createActiveToolchain(contextKeys, logger); // If we don't have a toolchain, show an error and stop initializing the extension. // This can happen if the user has not installed Swift or if the toolchain is not @@ -82,7 +83,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { }; } - const workspaceContext = new WorkspaceContext(context, logger, toolchain); + const workspaceContext = new WorkspaceContext(context, contextKeys, logger, toolchain); context.subscriptions.push(workspaceContext); context.subscriptions.push(new SwiftEnvironmentVariablesManager(context)); @@ -239,7 +240,10 @@ function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise }; } -async function createActiveToolchain(logger: SwiftLogger): Promise { +async function createActiveToolchain( + contextKeys: ContextKeys, + logger: SwiftLogger +): Promise { try { const toolchain = await SwiftToolchain.create(undefined, logger); toolchain.logDiagnostics(logger); @@ -252,7 +256,10 @@ async function createActiveToolchain(logger: SwiftLogger): Promise { - contextKeys.isActivated = false; + const workspaceContext = (context.extension.exports as Api).workspaceContext; + if (workspaceContext) { + workspaceContext.contextKeys.isActivated = false; + } context.subscriptions.forEach(subscription => subscription.dispose()); context.subscriptions.length = 0; } diff --git a/src/ui/ProjectPanelProvider.ts b/src/ui/ProjectPanelProvider.ts index bc087eca8..aa74daa7b 100644 --- a/src/ui/ProjectPanelProvider.ts +++ b/src/ui/ProjectPanelProvider.ts @@ -18,7 +18,6 @@ import * as path from "path"; import configuration from "../configuration"; import { WorkspaceContext } from "../WorkspaceContext"; import { FolderOperation } from "../WorkspaceContext"; -import contextKeys from "../contextKeys"; import { Dependency, ResolvedDependency, Target } from "../SwiftPackage"; import { FolderContext } from "../FolderContext"; import { getPlatformConfig, resolveTaskCwd } from "../utilities/tasks"; @@ -485,9 +484,9 @@ export class ProjectPanelProvider implements vscode.TreeDataProvider { constructor(private workspaceContext: WorkspaceContext) { // default context key to false. These will be updated as folders are given focus - contextKeys.hasPackage = false; - contextKeys.hasExecutableProduct = false; - contextKeys.packageHasDependencies = false; + workspaceContext.contextKeys.hasPackage = false; + workspaceContext.contextKeys.hasExecutableProduct = false; + workspaceContext.contextKeys.packageHasDependencies = false; this.observeTasks(workspaceContext); } @@ -688,7 +687,7 @@ export class ProjectPanelProvider implements vscode.TreeDataProvider { } const pkg = folderContext.swiftPackage; const rootDeps = await pkg.rootDependencies; - if (contextKeys.flatDependenciesList) { + if (this.workspaceContext.contextKeys.flatDependenciesList) { const existenceMap = new Map(); const gatherChildren = (dependencies: ResolvedDependency[]): ResolvedDependency[] => { const result: ResolvedDependency[] = []; diff --git a/test/integration-tests/documentation/DocumentationLivePreview.test.ts b/test/integration-tests/documentation/DocumentationLivePreview.test.ts new file mode 100644 index 000000000..95385d89a --- /dev/null +++ b/test/integration-tests/documentation/DocumentationLivePreview.test.ts @@ -0,0 +1,224 @@ +//===----------------------------------------------------------------------===// +// +// 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 { 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 (!workspaceContext.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 { + 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 { + 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 { + return new Promise(resolve => { + const disposable = context.documentation.onPreviewDidUpdateContent( + (content: WebviewContent) => { + resolve(content); + disposable.dispose(); + } + ); + }); +} + +function waitForNextRender(context: WorkspaceContext): Promise { + return new Promise(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; +} diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index e7016cd66..2ede4266a 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -30,7 +30,6 @@ import { folderInRootWorkspace, updateSettings, } from "../utilities/testutilities"; -import contextKeys from "../../../src/contextKeys"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; import { Version } from "../../../src/utilities/version"; import { wait } from "../../../src/utilities/utilities"; @@ -58,7 +57,7 @@ suite("ProjectPanelProvider Test Suite", function () { await executeTaskAndWaitForResult(buildAllTask); }, async teardown() { - contextKeys.flatDependenciesList = false; + workspaceContext.contextKeys.flatDependenciesList = false; treeProvider.dispose(); }, testAssets: ["targets"], @@ -301,7 +300,7 @@ suite("ProjectPanelProvider Test Suite", function () { suite("Dependencies", () => { test("Includes remote dependency", async () => { - contextKeys.flatDependenciesList = false; + workspaceContext.contextKeys.flatDependenciesList = false; const items = await getHeaderChildren("Dependencies"); const dep = items.find(n => n.name === "swift-markdown") as PackageNode; expect(dep, `${JSON.stringify(items, null, 2)}`).to.not.be.undefined; @@ -324,7 +323,7 @@ suite("ProjectPanelProvider Test Suite", function () { }); test("Lists local dependency file structure", async () => { - contextKeys.flatDependenciesList = false; + workspaceContext.contextKeys.flatDependenciesList = false; const children = await getHeaderChildren("Dependencies"); const dep = children.find(n => n.name === "defaultpackage") as PackageNode; expect( @@ -358,7 +357,7 @@ suite("ProjectPanelProvider Test Suite", function () { }); test("Lists remote dependency file structure", async () => { - contextKeys.flatDependenciesList = false; + workspaceContext.contextKeys.flatDependenciesList = false; const children = await getHeaderChildren("Dependencies"); const dep = children.find(n => n.name === "swift-markdown") as PackageNode; expect(dep, `${JSON.stringify(children, null, 2)}`).to.not.be.undefined; @@ -384,7 +383,7 @@ suite("ProjectPanelProvider Test Suite", function () { }); test("Shows a flat dependency list", async () => { - contextKeys.flatDependenciesList = true; + workspaceContext.contextKeys.flatDependenciesList = true; const items = await getHeaderChildren("Dependencies"); expect(items.length).to.equal(3); expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; @@ -393,7 +392,7 @@ suite("ProjectPanelProvider Test Suite", function () { }); test("Shows a nested dependency list", async () => { - contextKeys.flatDependenciesList = false; + workspaceContext.contextKeys.flatDependenciesList = false; const items = await getHeaderChildren("Dependencies"); expect(items.length).to.equal(2); expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; @@ -419,7 +418,7 @@ suite("ProjectPanelProvider Test Suite", function () { }); test("Excludes files based on settings", async () => { - contextKeys.flatDependenciesList = false; + workspaceContext.contextKeys.flatDependenciesList = false; const children = await getHeaderChildren("Dependencies"); const dep = children.find(n => n.name === "swift-markdown") as PackageNode; expect(dep, `${JSON.stringify(children, null, 2)}`).to.not.be.undefined; diff --git a/test/unit-tests/MockUtils.test.ts b/test/unit-tests/MockUtils.test.ts index f922a5bdc..d580fc4aa 100644 --- a/test/unit-tests/MockUtils.test.ts +++ b/test/unit-tests/MockUtils.test.ts @@ -27,7 +27,6 @@ import { waitForReturnedPromises, } from "../MockUtils"; import { Version } from "../../src/utilities/version"; -import contextKeys from "../../src/contextKeys"; import configuration from "../../src/configuration"; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -207,7 +206,6 @@ suite("MockUtils Test Suite", () => { suite("mockGlobalModule()", () => { const mockedFS = mockGlobalModule(fs); - const mockedContextKeys = mockGlobalModule(contextKeys); const mockedConfiguration = mockGlobalModule(configuration); test("can mock the fs/promises module", async () => { @@ -217,17 +215,6 @@ suite("MockUtils Test Suite", () => { expect(mockedFS.readFile).to.have.been.calledOnceWithExactly("some_file"); }); - test("can mock the contextKeys module", () => { - // Initial value should match default value in context keys - expect(contextKeys.isActivated).to.be.false; - // Make sure that you can set the value of contextKeys using the mock - mockedContextKeys.isActivated = true; - expect(contextKeys.isActivated).to.be.true; - // Make sure that setting isActivated via contextKeys is also possible - contextKeys.isActivated = false; - expect(contextKeys.isActivated).to.be.false; - }); - test("can mock the configuration module", () => { expect(configuration.sdk).to.equal(""); // Make sure you can set a value using the mock