Skip to content
Merged
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
9 changes: 7 additions & 2 deletions packages/common/src/ide/fake/FakeIDE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default class FakeIDE implements IDE {
workspaceFolders: readonly WorkspaceFolder[] | undefined = undefined;
private disposables: Disposable[] = [];
private assetsRoot_: string | undefined;
private quickPickReturnValue: string | undefined = undefined;

async flashRanges(_flashDescriptors: FlashDescriptor[]): Promise<void> {
// empty
Expand Down Expand Up @@ -93,11 +94,15 @@ export default class FakeIDE implements IDE {
throw Error("Not implemented");
}

public showQuickPick(
public setQuickPickReturnValue(value: string | undefined) {
this.quickPickReturnValue = value;
}

public async showQuickPick(
_items: readonly string[],
_options?: QuickPickOptions,
): Promise<string | undefined> {
throw new Error("Method not implemented.");
return this.quickPickReturnValue;
}

public showInputBox(_options?: any): Promise<string | undefined> {
Expand Down
12 changes: 11 additions & 1 deletion packages/common/src/ide/normalized/NormalizedIDE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import FakeIDE from "../fake/FakeIDE";
import PassthroughIDEBase from "../PassthroughIDEBase";
import { FlashDescriptor } from "../types/FlashDescriptor";
import type { IDE } from "../types/ide.types";
import { QuickPickOptions } from "../types/QuickPickOptions";

export class NormalizedIDE extends PassthroughIDEBase {
configuration: FakeConfiguration;
Expand All @@ -15,7 +16,7 @@ export class NormalizedIDE extends PassthroughIDEBase {

constructor(
original: IDE,
private fakeIde: FakeIDE,
public fakeIde: FakeIDE,
private isSilent: boolean,
) {
super(original);
Expand Down Expand Up @@ -61,4 +62,13 @@ export class NormalizedIDE extends PassthroughIDEBase {
? this.fakeIde.setHighlightRanges(highlightId, editor, ranges)
: super.setHighlightRanges(highlightId, editor, ranges);
}

public async showQuickPick(
_items: readonly string[],
_options?: QuickPickOptions,
): Promise<string | undefined> {
return this.isSilent
? this.fakeIde.showQuickPick(_items, _options)
: super.showQuickPick(_items, _options);
}
}
6 changes: 5 additions & 1 deletion packages/common/src/testUtil/getFixturePaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ export function getFixturePath(fixturePath: string) {
return path.join(getFixturesPath(), fixturePath);
}

export function getRecordedTestsDirPath() {
return path.join(getFixturesPath(), "recorded");
}

export function getRecordedTestPaths() {
const directory = path.join(getFixturesPath(), "recorded");
const directory = getRecordedTestsDirPath();

return walkFilesSync(directory).filter(
(path) => path.endsWith(".yml") || path.endsWith(".yaml"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import {
toLineRange,
walkDirsSync,
TestCaseCommand,
showError,
} from "@cursorless/common";
import * as fs from "fs";
import { readFile } from "fs/promises";
import { access, readFile } from "fs/promises";
import { invariant } from "immutability-helper";
import { merge } from "lodash";
import * as path from "path";
Expand Down Expand Up @@ -79,8 +80,6 @@ const TIMING_CALIBRATION_HIGHLIGHT_ID = "timingCalibration";

export class TestCaseRecorder {
private active: boolean = false;
private workspacePath: string | null;
private workspaceName: string | null;
private fixtureRoot: string | null;
private targetDirectory: string | null = null;
private testCase: TestCase | null = null;
Expand All @@ -100,18 +99,14 @@ export class TestCaseRecorder {
constructor(private graph: Graph) {
const { runMode, assetsRoot, workspaceFolders } = ide();

this.workspacePath =
runMode === "development"
? assetsRoot
const workspacePath =
runMode === "development" || runMode === "test"
? path.join(assetsRoot, "../../..")
: workspaceFolders?.[0].uri.path ?? null;

this.workspaceName = this.workspacePath
? path.basename(this.workspacePath)
: null;

this.fixtureRoot = this.workspacePath
this.fixtureRoot = workspacePath
? path.join(
this.workspacePath,
workspacePath,
"packages/cursorless-vscode-e2e/src/suite/fixtures/recorded",
)
: null;
Expand Down Expand Up @@ -377,14 +372,16 @@ export class TestCaseRecorder {
}

private async promptSubdirectory(): Promise<string | undefined> {
if (
this.workspaceName == null ||
this.fixtureRoot == null ||
!["cursorless-vscode", "cursorless"].includes(this.workspaceName)
) {
throw new Error(
'"Cursorless record" must be run from within cursorless directory',
);
try {
if (this.fixtureRoot == null) {
throw Error();
}
await access(this.fixtureRoot);
} catch (err) {
const errorMessage =
'"Cursorless record" must be run from within cursorless directory';
showError(ide().messages, "promptSubdirectoryError", errorMessage);
throw new Error(errorMessage);
}

const subdirectorySelection = await ide().showQuickPick(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
languageId: plaintext
command:
version: 4
spokenForm: take harp
action: {name: setSelection}
targets:
- type: primitive
mark: {type: decoratedSymbol, symbolColor: default, character: h}
usePrePhraseSnapshot: false
initialState:
documentContents: hello world
selections:
- anchor: {line: 0, character: 11}
active: {line: 0, character: 11}
marks:
default.h:
start: {line: 0, character: 0}
end: {line: 0, character: 5}
finalState:
documentContents: hello world
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 5}
115 changes: 115 additions & 0 deletions packages/cursorless-vscode-e2e/src/suite/testCaseRecorder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
getFixturePath,
getRecordedTestsDirPath,
HatTokenMap,
} from "@cursorless/common";
import {
getCursorlessApi,
openNewEditor,
runCursorlessCommand,
} from "@cursorless/vscode-common";
import { assert } from "chai";
import * as crypto from "crypto";
import { mkdir, readdir, readFile, rm } from "fs/promises";
import * as path from "path";
import * as os from "os";
import { basename } from "path";
import * as vscode from "vscode";
import { endToEndTestSetup } from "../endToEndTestSetup";

// Ensure that the test case recorder works
suite("testCaseRecorder", async function () {
endToEndTestSetup(this);

test("no args", testCaseRecorderNoArgs);
test("path arg", testCaseRecorderPathArg);
});

async function testCaseRecorderNoArgs() {
const {
hatTokenMap,
ide: { fakeIde },
} = (await getCursorlessApi()).testHelpers!;
const dirName = crypto.randomBytes(16).toString("hex");
fakeIde.setQuickPickReturnValue(dirName);
const tmpdir = path.join(getRecordedTestsDirPath(), dirName);

try {
await runAndCheckTestCaseRecorder(hatTokenMap, tmpdir);
} finally {
fakeIde.setQuickPickReturnValue(undefined);
await rm(tmpdir, { recursive: true, force: true });
}
}

async function testCaseRecorderPathArg() {
const { hatTokenMap } = (await getCursorlessApi()).testHelpers!;
const tmpdir = path.join(os.tmpdir(), crypto.randomBytes(16).toString("hex"));
await mkdir(tmpdir, { recursive: true });

try {
await runAndCheckTestCaseRecorder(hatTokenMap, tmpdir, {
directory: tmpdir,
});
} finally {
await rm(tmpdir, { recursive: true, force: true });
}
}

async function runAndCheckTestCaseRecorder(
hatTokenMap: HatTokenMap,
tmpdir: string,
...extraArgs: unknown[]
) {
const editor = await openNewEditor("hello world");

editor.selections = [new vscode.Selection(0, 11, 0, 11)];

await hatTokenMap.allocateHats();

await vscode.commands.executeCommand(
"cursorless.recordTestCase",
...extraArgs,
);

await runCursorlessCommand({
version: 4,
action: { name: "setSelection" },
targets: [
{
type: "primitive",
mark: {
type: "decoratedSymbol",
symbolColor: "default",
character: "h",
},
},
],
usePrePhraseSnapshot: false,
spokenForm: "take harp",
});

await vscode.commands.executeCommand("cursorless.recordTestCase");

const paths = await readdir(tmpdir);
assert.lengthOf(paths, 1);

const actualRecordedTestPath = paths[0];
assert.equal(basename(actualRecordedTestPath), "takeHarp.yml");

const expected = (
await readFile(
getFixturePath("recorded/testCaseRecorder/takeHarp.yml"),
"utf8",
)
)
// We use this to ensure that the test works on Windows. Depending on user
// / CI git config, the file might be checked out with CRLF line endings
.replaceAll("\r\n", "\n");
const actualRecordedTest = await readFile(
path.join(tmpdir, actualRecordedTestPath),
"utf8",
);

assert.equal(actualRecordedTest, expected);
}
2 changes: 1 addition & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"moduleResolution": "nodenext",
"moduleDetection": "force",
"target": "es6",
"lib": ["es2020"],
"lib": ["es2022"],
"sourceMap": true,
"declarationMap": true,
"resolveJsonModule": true,
Expand Down