-
-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
1,206 additions
and
272 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import * as vscode from "vscode"; | ||
|
||
/** | ||
* A mockable layer over the vscode clipboard | ||
* | ||
* For unknown reasons it's not possible to mock the clipboard directly. | ||
* Use this instead of vscode.env.clipboard so it can be mocked in testing. | ||
**/ | ||
export class Clipboard { | ||
static readText = vscode.env.clipboard.readText; | ||
static writeText = vscode.env.clipboard.writeText; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import * as path from "path"; | ||
import * as fs from "fs"; | ||
import * as vscode from "vscode"; | ||
import NavigationMap from "./NavigationMap"; | ||
import { ThatMark } from "./ThatMark"; | ||
import { ActionType, PartialTarget, Target } from "./Types"; | ||
import { extractTargetedMarks } from "./extractTargetedMarks"; | ||
import { marksToPlainObject, SerializedMarks } from "./toPlainObject"; | ||
import { takeSnapshot, TestCaseSnapshot } from "./takeSnapshot"; | ||
import serialize from "./serialize"; | ||
|
||
type TestCaseCommand = { | ||
actionName: ActionType; | ||
partialTargets: PartialTarget[]; | ||
extraArgs: any[]; | ||
}; | ||
|
||
type TestCaseContext = { | ||
spokenForm: string; | ||
thatMark: ThatMark; | ||
targets: Target[]; | ||
navigationMap: NavigationMap; | ||
}; | ||
|
||
export type TestCaseFixture = { | ||
spokenForm: string; | ||
command: TestCaseCommand; | ||
languageId: string; | ||
marks: SerializedMarks; | ||
initialState: TestCaseSnapshot; | ||
finalState: TestCaseSnapshot; | ||
returnValue: unknown; | ||
/** Inferred full targets added for context; not currently used in testing */ | ||
fullTargets: Target[]; | ||
}; | ||
|
||
export class TestCase { | ||
spokenForm: string; | ||
command: TestCaseCommand; | ||
languageId: string; | ||
fullTargets: Target[]; | ||
marks: SerializedMarks; | ||
context: TestCaseContext; | ||
initialState: TestCaseSnapshot | null = null; | ||
finalState: TestCaseSnapshot | null = null; | ||
returnValue: unknown = null; | ||
|
||
constructor(command: TestCaseCommand, context: TestCaseContext) { | ||
const activeEditor = vscode.window.activeTextEditor!; | ||
const { navigationMap, targets, spokenForm } = context; | ||
const targetedMarks = extractTargetedMarks(targets, navigationMap); | ||
|
||
this.spokenForm = spokenForm; | ||
this.command = command; | ||
this.languageId = activeEditor.document.languageId; | ||
this.marks = marksToPlainObject(targetedMarks); | ||
this.fullTargets = targets; | ||
this.context = context; | ||
} | ||
|
||
private includesThatMark(target: Target) { | ||
if (target.type === "primitive" && target.mark.type === "that") { | ||
return true; | ||
} else if (target.type === "list") { | ||
return target.elements.some(this.includesThatMark, this); | ||
} else if (target.type === "range") { | ||
return [target.start, target.end].some(this.includesThatMark, this); | ||
} | ||
return false; | ||
} | ||
|
||
private getExcludedFields(context?: { initialSnapshot?: boolean }) { | ||
const excludableFields = { | ||
clipboard: !["copy", "paste"].includes(this.command.actionName), | ||
thatMark: | ||
context?.initialSnapshot && | ||
!this.fullTargets.some(this.includesThatMark, this), | ||
visibleRanges: ![ | ||
"fold", | ||
"unfold", | ||
"scrollToBottom", | ||
"scrollToCenter", | ||
"scrollToTop", | ||
].includes(this.command.actionName), | ||
}; | ||
|
||
return Object.keys(excludableFields).filter( | ||
(field) => excludableFields[field] | ||
); | ||
} | ||
|
||
toYaml() { | ||
if (this.initialState == null || this.finalState == null) { | ||
throw Error("Two snapshots must be taken before serializing"); | ||
} | ||
const fixture: TestCaseFixture = { | ||
spokenForm: this.spokenForm, | ||
languageId: this.languageId, | ||
command: this.command, | ||
marks: this.marks, | ||
initialState: this.initialState, | ||
finalState: this.finalState, | ||
returnValue: this.returnValue, | ||
fullTargets: this.fullTargets, | ||
}; | ||
return serialize(fixture); | ||
} | ||
|
||
async recordInitialState() { | ||
const excludeFields = this.getExcludedFields({ initialSnapshot: true }); | ||
this.initialState = await takeSnapshot( | ||
this.context.thatMark, | ||
excludeFields | ||
); | ||
} | ||
|
||
async recordFinalState(returnValue: unknown) { | ||
const excludeFields = this.getExcludedFields(); | ||
this.returnValue = returnValue; | ||
this.finalState = await takeSnapshot(this.context.thatMark, excludeFields); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import * as vscode from "vscode"; | ||
import * as path from "path"; | ||
import * as fs from "fs"; | ||
import { TestCase } from "./TestCase"; | ||
import { walkDirsSync } from "./test/suite/walkSync"; | ||
|
||
export class TestCaseRecorder { | ||
active: boolean = false; | ||
outPath: string | null = null; | ||
spokenForm: string | null = null; | ||
workspacePath: string | null; | ||
workSpaceFolder: string | null; | ||
fixtureRoot: string | null; | ||
fixtureSubdirectory: string | null = null; | ||
|
||
constructor(extensionContext: vscode.ExtensionContext) { | ||
this.workspacePath = | ||
extensionContext.extensionMode === vscode.ExtensionMode.Development | ||
? extensionContext.extensionPath | ||
: vscode.workspace.workspaceFolders?.[0].uri.path ?? null; | ||
|
||
this.workSpaceFolder = this.workspacePath | ||
? path.basename(this.workspacePath) | ||
: null; | ||
|
||
this.fixtureRoot = this.workspacePath | ||
? path.join(this.workspacePath, "src/test/suite/fixtures/recorded") | ||
: null; | ||
} | ||
|
||
start(): Promise<void> { | ||
this.active = true; | ||
return this.promptSpokenForm(); | ||
} | ||
|
||
async finish(testCase: TestCase): Promise<string | null> { | ||
this.active = false; | ||
const outPath = await this.promptSubdirectory(); | ||
const fixture = testCase.toYaml(); | ||
|
||
if (outPath) { | ||
this.writeToFile(outPath, fixture); | ||
} else { | ||
this.showFixture(fixture); | ||
} | ||
|
||
return outPath; | ||
} | ||
|
||
private async writeToFile(outPath: string, fixture: string) { | ||
fs.writeFileSync(outPath, fixture); | ||
vscode.window | ||
.showInformationMessage("Cursorless test case saved.", "View") | ||
.then(async (action) => { | ||
if (action === "View") { | ||
const document = await vscode.workspace.openTextDocument(outPath); | ||
await vscode.window.showTextDocument(document); | ||
} | ||
}); | ||
} | ||
|
||
private async showFixture(fixture: string) { | ||
const document = await vscode.workspace.openTextDocument({ | ||
language: "yaml", | ||
content: fixture, | ||
}); | ||
await vscode.window.showTextDocument(document, { | ||
viewColumn: vscode.ViewColumn.Beside, | ||
}); | ||
} | ||
|
||
private async promptSpokenForm(): Promise<void> { | ||
const result = await vscode.window.showInputBox({ | ||
prompt: "Talon Command", | ||
ignoreFocusOut: true, | ||
validateInput: (input) => (input.trim().length > 0 ? null : "Required"), | ||
}); | ||
|
||
// Inputs return undefined when a user cancels by hitting 'escape' | ||
if (result === undefined) { | ||
this.active = false; | ||
return; | ||
} | ||
|
||
this.spokenForm = result; | ||
} | ||
|
||
private async promptSubdirectory(): Promise<string | null> { | ||
if ( | ||
this.workspacePath == null || | ||
this.fixtureRoot == null || | ||
this.workSpaceFolder !== "cursorless-vscode" | ||
) { | ||
return null; | ||
} | ||
|
||
const subdirectories = walkDirsSync(this.fixtureRoot).concat("/"); | ||
|
||
const createNewSubdirectory = "Create new folder →"; | ||
const subdirectorySelection = await vscode.window.showQuickPick([ | ||
...subdirectories, | ||
createNewSubdirectory, | ||
]); | ||
|
||
if (subdirectorySelection === undefined) { | ||
return null; | ||
} else if (subdirectorySelection === createNewSubdirectory) { | ||
return this.promptNewSubdirectory(); | ||
} else { | ||
this.fixtureSubdirectory = subdirectorySelection; | ||
return this.promptFileName(); | ||
} | ||
} | ||
|
||
private async promptNewSubdirectory(): Promise<string | null> { | ||
if (this.fixtureRoot == null) { | ||
throw new Error("Missing fixture root. Not in cursorless workspace?"); | ||
} | ||
|
||
const subdirectory = await vscode.window.showInputBox({ | ||
prompt: "New Folder Name", | ||
ignoreFocusOut: true, | ||
validateInput: (input) => (input.trim().length > 0 ? null : "Required"), | ||
}); | ||
|
||
if (subdirectory === undefined) { | ||
return this.promptSubdirectory(); // go back a prompt | ||
} | ||
|
||
this.fixtureSubdirectory = subdirectory; | ||
return this.promptFileName(); | ||
} | ||
|
||
private async promptFileName(): Promise<string | null> { | ||
if (this.fixtureRoot == null) { | ||
throw new Error("Missing fixture root. Not in cursorless workspace?"); | ||
} | ||
|
||
const filename = await vscode.window.showInputBox({ | ||
prompt: "Fixture Filename", | ||
}); | ||
|
||
if (filename === undefined || this.fixtureSubdirectory == null) { | ||
return this.promptSubdirectory(); // go back a prompt | ||
} | ||
|
||
const targetDirectory = path.join( | ||
this.fixtureRoot, | ||
this.fixtureSubdirectory | ||
); | ||
|
||
if (!fs.existsSync(targetDirectory)) { | ||
fs.mkdirSync(targetDirectory); | ||
} | ||
|
||
this.outPath = path.join(targetDirectory, `${filename}.yml`); | ||
return this.outPath; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { SelectionWithEditor } from "./Types"; | ||
|
||
export class ThatMark { | ||
private mark: SelectionWithEditor[] = []; | ||
|
||
set(value: SelectionWithEditor[]) { | ||
this.mark = value; | ||
} | ||
|
||
get() { | ||
return this.mark; | ||
} | ||
} |
Oops, something went wrong.