Skip to content

Commit

Permalink
[test-recorder] Support test context from vitest (Azure#28350)
Browse files Browse the repository at this point in the history
Currently we just need suite title and test title to generate recording
file names. `vitest` provides the info via `context` of the callback
function.

This PR adds vitest support to recorder. Call site would look like

```ts
   //...
  beforeEach(async (context) => {
    recorder = new Recorder(context);
```
  • Loading branch information
jeremymeng committed Feb 22, 2024
1 parent aaba9b5 commit 693683c
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 21 deletions.
1 change: 1 addition & 0 deletions sdk/test-utils/recorder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export {
export { env } from "./utils/env";
export { delay } from "./utils/delay";
export { CustomMatcherOptions } from "./matcher";
export { TestInfo, MochaTest, VitestTestContext } from "./testInfo";
62 changes: 52 additions & 10 deletions sdk/test-utils/recorder/src/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import {
RecorderStartOptions,
RecordingStateManager,
} from "./utils/utils";
import { Test } from "mocha";
import { assetsJsonPath, sessionFilePath } from "./utils/sessionFilePath";
import { assetsJsonPath, sessionFilePath, TestContext } from "./utils/sessionFilePath";
import { SanitizerOptions } from "./utils/utils";
import { paths } from "./utils/paths";
import { addSanitizers, transformsInfo } from "./sanitizer";
Expand All @@ -35,6 +34,45 @@ import { isBrowser, isNode } from "@azure/core-util";
import { env } from "./utils/env";
import { decodeBase64 } from "./utils/encoding";
import { AdditionalPolicyConfig } from "@azure/core-client";
import { isMochaTest, isVitestTestContext, TestInfo, VitestSuite } from "./testInfo";

/**
* Caculates session file path and JSON assets path from test context
*
* @internal
*/
export function calculatePaths(testContext: TestInfo): TestContext {
if (isMochaTest(testContext)) {
if (!testContext.parent) {
throw new RecorderError(
`The parent of test '${testContext.title}' is undefined, so a file path for its recording could not be generated. Please place the test inside a describe block.`,
);
}
return {
suiteTitle: testContext.parent.fullTitle(),
testTitle: testContext.title,
};
} else if (isVitestTestContext(testContext)) {
if (!testContext.task.name || !testContext.task.suite.name) {
throw new RecorderError(
`Unable to determine the recording file path. Unexpected empty Vitest context`,
);
}
const suites: string[] = [];
let p: VitestSuite | undefined = testContext.task.suite;
while (p?.name) {
suites.push(p.name);
p = p.suite;
}

return {
suiteTitle: suites.reverse().join("_"),
testTitle: testContext.task.name,
};
} else {
throw new RecorderError(`Unrecognized test info: ${testContext}`);
}
}

/**
* This client manages the recorder life cycle and interacts with the proxy-tool to do the recording,
Expand All @@ -54,19 +92,23 @@ export class Recorder {
private variables: Record<string, string>;
private matcherSet = false;

constructor(private testContext?: Test | undefined) {
constructor(private testContext?: TestInfo) {
if (!this.testContext) {
throw new Error(
"Unable to determine the recording file path, testContext provided is not defined.",
);
}

logger.info(`[Recorder#constructor] Creating a recorder instance in ${getTestMode()} mode`);
if (isRecordMode() || isPlaybackMode()) {
if (this.testContext) {
this.sessionFile = sessionFilePath(this.testContext);
this.assetsJson = assetsJsonPath();
const context = calculatePaths(this.testContext);

this.sessionFile = sessionFilePath(context);
this.assetsJson = assetsJsonPath();

if (this.testContext) {
logger.info(`[Recorder#constructor] Using a session file located at ${this.sessionFile}`);
this.httpClient = createDefaultHttpClient();
} else {
throw new Error(
"Unable to determine the recording file path, testContext provided is not defined.",
);
}
}
this.variables = {};
Expand Down
91 changes: 91 additions & 0 deletions sdk/test-utils/recorder/src/testInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
* Represents a Test.
*/
export type TestInfo = MochaTest | VitestTestContext;

/**
* Represents a Mocha Test.
*/
export interface MochaTest {
/**
* The title of the test.
*/
title: string;
/**
* The parent of the Mocha Test Suite.
*/
parent?: MochaTestSuite;
}

/**
* Represents a Mocha Test Suite.
*/
export interface MochaTestSuite {
fullTitle(): string;
}

/**
* Represents a Vitest Test Context
*/
export interface VitestTestContext {
/**
* The Vitest Context Task.
*/
task: VitestTask;
}

export interface VitestTaskBase {
name: string;
suite?: VitestSuite;
}

/**
* Represents a Vitest Test Context Task
*/
export interface VitestTask extends VitestTaskBase {
/**
* The Vitest Context Task Name.
*/
name: string;
/**
* The Vitest Context Task Suite.
*/
suite: VitestSuite;
}

/**
* Represents a Vitest Test Suite.
*/
export interface VitestSuite extends VitestTaskBase {
/**
* The Vitest Context Task Suite Name.
*/
name: string;
}

/**
* Determines whether the given test is a Mocha Test.
* @param test - The test to check.
* @returns true if the given test is a Mocha Test.
*/
export function isMochaTest(test: unknown): test is MochaTest {
return typeof test === "object" && test != null && "title" in test;
}

/**
* Determines whether the given test is a Vitest Test.
* @param test - The test to check.
* @returns true if the given test is a Vitest Test.
*/
export function isVitestTestContext(test: unknown): test is VitestTestContext {
return (
typeof test == "function" &&
"task" in test &&
typeof test.task === "object" &&
test.task != null &&
"name" in test.task
);
}
20 changes: 9 additions & 11 deletions sdk/test-utils/recorder/src/utils/sessionFilePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
import { isNode } from "@azure/core-util";
import { generateTestRecordingFilePath } from "./filePathGenerator";
import { relativeRecordingsPath } from "./relativePathCalculator";
import { RecorderError } from "./utils";

export function sessionFilePath(testContext: Mocha.Test): string {
export interface TestContext {
suiteTitle: string; // describe(suiteTitle, () => {})
testTitle: string; // it(testTitle, () => {})
}

export function sessionFilePath(testContext: TestContext): string {
// sdk/service/project/recordings/{node|browsers}/<describe-block-title>/recording_<test-title>.json
return `${relativeRecordingsPath()}/${recordingFilePath(testContext)}`;
}
Expand All @@ -16,17 +20,11 @@ export function sessionFilePath(testContext: Mocha.Test): string {
*
* `{node|browsers}/<describe-block-title>/recording_<test-title>.json`
*/
export function recordingFilePath(testContext: Mocha.Test): string {
if (!testContext.parent) {
throw new RecorderError(
`Test ${testContext.title} is not inside a describe block, so a file path for its recording could not be generated. Please place the test inside a describe block.`,
);
}

export function recordingFilePath(testContext: TestContext): string {
return generateTestRecordingFilePath(
isNode ? "node" : "browsers",
testContext.parent.fullTitle(),
testContext.title,
testContext.suiteTitle,
testContext.testTitle,
);
}

Expand Down
62 changes: 62 additions & 0 deletions sdk/test-utils/recorder/test/recorder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { expect } from "chai";

import { calculatePaths } from "../src/recorder";

describe("Recorder file paths", () => {
it("calculates paths for a Mocha test", () => {
const mochaTest = {
title: "mocha test title",
parent: {
fullTitle: () => "mocha suite title",
},
};
const context = calculatePaths(mochaTest);

expect(context).to.eql({
suiteTitle: "mocha suite title",
testTitle: "mocha test title",
});
});

it("calculates paths for a vitest test", () => {
const vitestTest = () => {
/* no-op */
};
(vitestTest as any).task = {
name: "vitest test title",
suite: {
name: "vitest suite title",
},
};

const context = calculatePaths(vitestTest as any);
expect(context).to.eql({
suiteTitle: "vitest suite title",
testTitle: "vitest test title",
});
});

it("calculates paths for a vitest test with nested suites", () => {
const vitestTest = () => {
/* no-op */
};
(vitestTest as any).task = {
name: "vitest test title",
suite: {
name: "vitest suite title",
suite: {
name: "toplevel suite",
},
},
};

const context = calculatePaths(vitestTest as any);
expect(context).to.eql({
suiteTitle: "toplevel suite_vitest suite title",
testTitle: "vitest test title",
});
});
});

0 comments on commit 693683c

Please sign in to comment.