-
-
Notifications
You must be signed in to change notification settings - Fork 77
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
Test Case Recorder #87
Changes from 23 commits
f7587e3
815e829
ed4ee0d
3d0e8b0
e9b60e4
efb696c
e95a993
b06f27f
c51def0
cc0e4bf
9d65fde
fa52794
35a8ce5
0471c7f
09a743f
43a4473
829882d
087c7fd
50ff1e4
f6db9e4
be29b40
84326b4
c6cd5ec
ca334b2
01b8553
c5c3d9b
060d91d
08ebc6b
076da28
52f4408
ff6a1e1
f585de0
b8df923
decc22f
16f67bb
0b8df41
3f188a3
a255e0b
8ce8f86
39d62f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,241 @@ | ||||||||||||||||||||||
import * as path from "path"; | ||||||||||||||||||||||
import * as fs from "fs"; | ||||||||||||||||||||||
import * as yaml from "js-yaml"; | ||||||||||||||||||||||
import * as vscode from "vscode"; | ||||||||||||||||||||||
import { Position, Range, Selection } from "vscode"; | ||||||||||||||||||||||
import { Clipboard } from "./Clipboard"; | ||||||||||||||||||||||
import NavigationMap from "./NavigationMap"; | ||||||||||||||||||||||
import { ThatMark } from "./ThatMark"; | ||||||||||||||||||||||
import { | ||||||||||||||||||||||
ActionType, | ||||||||||||||||||||||
PartialTarget, | ||||||||||||||||||||||
PrimitiveTarget, | ||||||||||||||||||||||
SelectionWithEditor, | ||||||||||||||||||||||
Target, | ||||||||||||||||||||||
} from "./Types"; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export type SerializedPosition = { | ||||||||||||||||||||||
line: number; | ||||||||||||||||||||||
character: number; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export type SerializedRange = { | ||||||||||||||||||||||
start: SerializedPosition; | ||||||||||||||||||||||
end: SerializedPosition; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export type SerializedSelection = { | ||||||||||||||||||||||
anchor: SerializedPosition; | ||||||||||||||||||||||
active: SerializedPosition; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export function serializeRange(range: Range): SerializedRange { | ||||||||||||||||||||||
return { | ||||||||||||||||||||||
start: serializePosition(range.start), | ||||||||||||||||||||||
end: serializePosition(range.end), | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
export function serializeSelection(selection: Selection): SerializedSelection { | ||||||||||||||||||||||
return { | ||||||||||||||||||||||
active: serializePosition(selection.active), | ||||||||||||||||||||||
anchor: serializePosition(selection.anchor), | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
export function serializePosition(position: Position): SerializedPosition { | ||||||||||||||||||||||
return { line: position.line, character: position.character }; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
type TestCaseCommand = { | ||||||||||||||||||||||
actionName: ActionType; | ||||||||||||||||||||||
partialTargets: PartialTarget[]; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For completeness I'd be tempted to also capture the full target, even if we don't end up using it for anything |
||||||||||||||||||||||
extraArgs: any[]; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
type TestCaseContext = { | ||||||||||||||||||||||
talonCommand: string; | ||||||||||||||||||||||
thatMark: ThatMark; | ||||||||||||||||||||||
targets: Target[]; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a funny smell to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I follow. Do you mean you would rather construct test case with a single argument/type ex. |
||||||||||||||||||||||
navigationMap: NavigationMap; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
type TestCaseSnapshot = { | ||||||||||||||||||||||
document: string; | ||||||||||||||||||||||
clipboard: string; | ||||||||||||||||||||||
visibleRanges: SerializedRange[]; | ||||||||||||||||||||||
selections: SerializedSelection[]; | ||||||||||||||||||||||
thatMark: SerializedSelection[] | null; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would make |
||||||||||||||||||||||
|
||||||||||||||||||||||
type DecorationRanges = { [coloredSymbol: string]: SerializedRange }; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export type TestCaseFixture = { | ||||||||||||||||||||||
talonCommand: string; | ||||||||||||||||||||||
command: TestCaseCommand; | ||||||||||||||||||||||
targets: Target[]; | ||||||||||||||||||||||
languageId: string; | ||||||||||||||||||||||
marks: DecorationRanges; | ||||||||||||||||||||||
initialState: TestCaseSnapshot; | ||||||||||||||||||||||
finalState: TestCaseSnapshot; | ||||||||||||||||||||||
returnValue: any; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
export default class TestCase { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something doesn't quite feel right here but it's tough to see everything with all the functions in this file. Maybe let's split things up a bit to get a better feel? |
||||||||||||||||||||||
talonCommand: string; | ||||||||||||||||||||||
command: TestCaseCommand; | ||||||||||||||||||||||
languageId: string; | ||||||||||||||||||||||
targets: Target[]; | ||||||||||||||||||||||
marks: DecorationRanges; | ||||||||||||||||||||||
context: TestCaseContext; | ||||||||||||||||||||||
initialState: TestCaseSnapshot | null = null; | ||||||||||||||||||||||
finalState: TestCaseSnapshot | null = null; | ||||||||||||||||||||||
returnValue: any = null; | ||||||||||||||||||||||
|
||||||||||||||||||||||
constructor(command: TestCaseCommand, context: TestCaseContext) { | ||||||||||||||||||||||
const activeEditor = vscode.window.activeTextEditor!; | ||||||||||||||||||||||
const { navigationMap, targets, talonCommand } = context; | ||||||||||||||||||||||
|
||||||||||||||||||||||
this.talonCommand = talonCommand; | ||||||||||||||||||||||
this.command = command; | ||||||||||||||||||||||
this.languageId = activeEditor.document.languageId; | ||||||||||||||||||||||
this.marks = this.extractTargetedDecorations(targets, navigationMap); | ||||||||||||||||||||||
this.targets = targets; | ||||||||||||||||||||||
this.context = context; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
extractPrimitiveTargetKeys(...targets: PrimitiveTarget[]) { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd get all these primitive target extraction functions into another file |
||||||||||||||||||||||
const keys: string[] = []; | ||||||||||||||||||||||
targets.forEach((target) => { | ||||||||||||||||||||||
if (target.mark.type === "decoratedSymbol") { | ||||||||||||||||||||||
const { character, symbolColor } = target.mark; | ||||||||||||||||||||||
keys.push(NavigationMap.getKey(symbolColor, character)); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
return keys; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
extractTargetKeys(target: Target): string[] { | ||||||||||||||||||||||
switch (target.type) { | ||||||||||||||||||||||
case "primitive": | ||||||||||||||||||||||
return this.extractPrimitiveTargetKeys(target); | ||||||||||||||||||||||
|
||||||||||||||||||||||
case "list": | ||||||||||||||||||||||
return target.elements.map(this.extractTargetKeys, this).flat(); | ||||||||||||||||||||||
|
||||||||||||||||||||||
case "range": | ||||||||||||||||||||||
return this.extractPrimitiveTargetKeys(target.start, target.end); | ||||||||||||||||||||||
|
||||||||||||||||||||||
default: | ||||||||||||||||||||||
return []; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
extractTargetedDecorations(targets: Target[], navigationMap: NavigationMap) { | ||||||||||||||||||||||
if (!navigationMap) { | ||||||||||||||||||||||
return {}; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
const decorationRanges = navigationMap.serializeRanges(); | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems wasteful / not quite the right separation of concerns to have navigation map dump everything. Might be better to do |
||||||||||||||||||||||
const targetedDecorations: DecorationRanges = {}; | ||||||||||||||||||||||
const targetKeys = targets.map(this.extractTargetKeys, this).flat(); | ||||||||||||||||||||||
targetKeys.forEach((key) => { | ||||||||||||||||||||||
targetedDecorations[key] = decorationRanges[key]; | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
return targetedDecorations; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
isThatMarkTargeted() { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won't work if a "that" mark is in a compound target, right? I think you need to descend like you do for finding decorated marks |
||||||||||||||||||||||
return this.targets.some( | ||||||||||||||||||||||
(target) => target.type === "primitive" && target.mark.type === "that" | ||||||||||||||||||||||
); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
static async getSnapshot( | ||||||||||||||||||||||
thatMark: SelectionWithEditor[] | ||||||||||||||||||||||
): Promise<TestCaseSnapshot> { | ||||||||||||||||||||||
const activeEditor = vscode.window.activeTextEditor!; | ||||||||||||||||||||||
return { | ||||||||||||||||||||||
document: activeEditor.document.getText(), | ||||||||||||||||||||||
selections: activeEditor.selections.map(serializeSelection), | ||||||||||||||||||||||
visibleRanges: activeEditor.visibleRanges.map(serializeRange), | ||||||||||||||||||||||
clipboard: await Clipboard.readText(), | ||||||||||||||||||||||
thatMark: thatMark.map((mark) => serializeSelection(mark.selection)), | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
async getSnapshot(): Promise<TestCaseSnapshot> { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's a bit confusing to have both this function and the static one |
||||||||||||||||||||||
return await TestCase.getSnapshot(this.context.thatMark.get()); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
async saveSnapshot() { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The difference between |
||||||||||||||||||||||
const snapshot = await this.getSnapshot(); | ||||||||||||||||||||||
|
||||||||||||||||||||||
if (!["copy", "paste"].includes(this.command.actionName)) { | ||||||||||||||||||||||
snapshot.clipboard = ""; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not in love with setting things then deleting them later, tho not a showstopper |
||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
if (!["fold", "unfold"].includes(this.command.actionName)) { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
snapshot.visibleRanges = []; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
if (this.initialState == null && !this.isThatMarkTargeted()) { | ||||||||||||||||||||||
snapshot.thatMark = []; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
if (this.initialState == null) { | ||||||||||||||||||||||
this.initialState = snapshot; | ||||||||||||||||||||||
} else if (this.finalState == null) { | ||||||||||||||||||||||
this.finalState = snapshot; | ||||||||||||||||||||||
} else { | ||||||||||||||||||||||
throw Error("Both snapshots already taken"); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
return snapshot; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
toYaml() { | ||||||||||||||||||||||
if (this.initialState == null || this.finalState == null) { | ||||||||||||||||||||||
throw Error("Two snapshots must be taken before serializing"); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
const fixture: TestCaseFixture = { | ||||||||||||||||||||||
talonCommand: this.talonCommand, | ||||||||||||||||||||||
command: this.command, | ||||||||||||||||||||||
languageId: this.languageId, | ||||||||||||||||||||||
targets: this.targets, | ||||||||||||||||||||||
marks: this.marks, | ||||||||||||||||||||||
initialState: this.initialState, | ||||||||||||||||||||||
finalState: this.finalState, | ||||||||||||||||||||||
returnValue: this.returnValue, | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
return yaml.dump(fixture, { noRefs: true, quotingType: '"' }); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
async writeFixture(filename: string) { | ||||||||||||||||||||||
if (filename === "") { | ||||||||||||||||||||||
throw new Error("Filename required"); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
const fixture = this.toYaml(); | ||||||||||||||||||||||
const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.path; | ||||||||||||||||||||||
let document; | ||||||||||||||||||||||
|
||||||||||||||||||||||
if (workspacePath) { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when will this one be null? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we are in a fresh, untitled window, ex. |
||||||||||||||||||||||
const fixturePath = path.join( | ||||||||||||||||||||||
workspacePath, | ||||||||||||||||||||||
"testFixtures", | ||||||||||||||||||||||
`${filename}.yml` | ||||||||||||||||||||||
); | ||||||||||||||||||||||
fs.writeFileSync(fixturePath, fixture); | ||||||||||||||||||||||
document = await vscode.workspace.openTextDocument(fixturePath); | ||||||||||||||||||||||
} else { | ||||||||||||||||||||||
document = await vscode.workspace.openTextDocument({ | ||||||||||||||||||||||
language: "yaml", | ||||||||||||||||||||||
content: fixture, | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
await vscode.window.showTextDocument(document, { | ||||||||||||||||||||||
viewColumn: vscode.ViewColumn.Beside, | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} |
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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this for? Don't you just serialize the targets that actually get referred to?