Skip to content

Commit

Permalink
feat(component-testing): implement mocks
Browse files Browse the repository at this point in the history
  • Loading branch information
DudaGod committed Oct 25, 2024
1 parent c2af168 commit 2c471d4
Show file tree
Hide file tree
Showing 17 changed files with 976 additions and 54 deletions.
105 changes: 105 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"name": "testplane",
"version": "8.20.5",
"description": "Tests framework based on mocha and wdio",
"main": "build/src/index.js",
"files": [
"build",
"typings"
Expand Down Expand Up @@ -34,6 +33,10 @@
"type": "git",
"url": "git://github.com/gemini-testing/testplane.git"
},
"exports": {
".": "./src/index.js",
"./mock": "./src/mock.js"
},
"homepage": "https://testplane.io/",
"engines": {
"node": ">= 18.0.0"
Expand All @@ -56,6 +59,7 @@
"@jspm/core": "2.0.1",
"@types/debug": "4.1.12",
"@types/yallist": "4.0.4",
"@vitest/spy": "2.1.2",
"@wdio/globals": "8.39.0",
"@wdio/protocols": "8.38.0",
"@wdio/types": "8.39.0",
Expand All @@ -81,6 +85,7 @@
"mocha": "10.2.0",
"plugins-loader": "1.3.4",
"png-validator": "1.1.0",
"recast": "0.23.6",
"resolve.exports": "2.0.2",
"sharp": "0.32.6",
"sizzle": "2.3.6",
Expand Down
13 changes: 13 additions & 0 deletions src/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// TODO: use from browser code when migrate to esm
type MockFactory = (originalImport?: unknown) => Promise<unknown>;

/**
* Re-export mock types
*/
export * from "@vitest/spy";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function mock(_moduleName: string, _factory?: MockFactory): void {}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function unmock(_moduleName: string): void {}
41 changes: 41 additions & 0 deletions src/runner/browser-env/vite/browser-modules/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export * from "@vitest/spy";
import type { MockFactory } from "./types.js";

// solution found here - https://stackoverflow.com/questions/48674303/resolve-relative-path-to-absolute-url-from-es6-module-in-browser
const a = document.createElement("a");
function resolveUrl(path: string): string {
a.href = path;
return a.href;
}

export async function mock(moduleName: string, factory?: MockFactory, originalImport?: unknown): Promise<void> {
// Mock call without factory parameter is handled by manual-mock module and removed from the source code by mock vite plugin
if (!factory || typeof factory !== "function") {
return;
}

const { file, mockCache } = window.__testplane__;
const isModuleLocal = moduleName.startsWith("/") || moduleName.startsWith("./") || moduleName.startsWith("../");

let mockPath: string;

if (isModuleLocal) {
const absModuleUrl = resolveUrl(file.split("/").slice(0, -1).join("/") + `/${moduleName}`);
mockPath = new URL(absModuleUrl).pathname;
} else {
mockPath = moduleName;
}

try {
const resolvedMock = await factory(originalImport);
mockCache.set(mockPath, resolvedMock);
} catch (err: unknown) {
const error = err as Error;
throw new Error(`There was an error in mock factory of module "${moduleName}"\n${error.stack}`);
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function unmock(_moduleName: string): void {
// implement in manual-mock module and removed from the source code by mock vite plugin
}
3 changes: 3 additions & 0 deletions src/runner/browser-env/vite/browser-modules/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,13 @@ declare global {
errors: BrowserError[];
socket: BrowserViteSocket;
browser: WebdriverIO.Browser;
mockCache: Map<string, unknown>;
} & WorkerInitializePayload;
testplane: typeof Proxy;
hermione: typeof Proxy;
browser: WebdriverIO.Browser;
expect: Expect;
}
}

export type MockFactory = (originalImport?: unknown) => Promise<unknown>;
5 changes: 5 additions & 0 deletions src/runner/browser-env/vite/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ export const SOCKET_MAX_TIMEOUT = 2147483647;
export const SOCKET_TIMED_OUT_ERROR = "operation has timed out";

export const WORKER_ENV_BY_RUN_UUID = new Map<string, WorkerInitializePayload>();

export const MOCK_MODULE_NAME = "testplane/mock";

export const DEFAULT_AUTOMOCK = false;
export const DEFAULT_AUTOMOCK_DIRECTORY = "__mocks__";
100 changes: 100 additions & 0 deletions src/runner/browser-env/vite/manual-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import path from "node:path";
import fs from "node:fs/promises";
import _ from "lodash";
import { DEFAULT_AUTOMOCK, DEFAULT_AUTOMOCK_DIRECTORY } from "./constants";

import type { InlineConfig } from "vite";
import type { BrowserTestRunEnvOptions } from "./types";

type MockOnFs = {
fullPath: string;
moduleName: string;
};

type ManualMockOptions = {
automock: boolean;
mocksOnFs: MockOnFs[];
};

export class ManualMock {
private _automock: boolean;
private _mocksOnFs: MockOnFs[];
private _mocks: string[];
private _unmocks: string[];

static async create<T extends ManualMock>(
this: new (opts: ManualMockOptions) => T,
config: Partial<InlineConfig>,
options?: BrowserTestRunEnvOptions,
): Promise<T> {
const automock = typeof options?.automock === "boolean" ? options?.automock : DEFAULT_AUTOMOCK;
const automockDir = path.resolve(config?.root || "", options?.automockDir || DEFAULT_AUTOMOCK_DIRECTORY);
const mocksOnFs = await getMocksOnFs(automockDir);

return new this({ automock, mocksOnFs });
}

constructor(options: ManualMockOptions) {
this._automock = options.automock;
this._mocksOnFs = options.mocksOnFs;
this._mocks = [];
this._unmocks = [];
}

async resolveId(id: string): Promise<string | void> {
const foundMockOnFs = this._mocksOnFs.find(mock => id === mock.moduleName);

if ((this._mocks.includes(id) || this._automock) && foundMockOnFs && !this._unmocks.includes(id)) {
return foundMockOnFs.fullPath;
}
}

mock(moduleName: string): void {
this._mocks.push(moduleName);
}

unmock(moduleName: string): void {
this._unmocks.push(moduleName);
}

resetMocks(): void {
this._mocks = [];
this._unmocks = [];
}
}

async function getMocksOnFs(automockDir: string): Promise<{ fullPath: string; moduleName: string }[]> {
const mockedModules = await getFilesFromDirectory(automockDir);

return mockedModules.map(filePath => {
const extName = path.extname(filePath);

return {
fullPath: filePath,
moduleName: filePath.slice(automockDir.length + 1, -extName.length),
};
});
}

async function getFilesFromDirectory(dir: string): Promise<string[]> {
const isDirExists = await fs.access(dir).then(
() => true,
() => false,
);

if (!isDirExists) {
return [];
}

const files = await fs.readdir(dir);
const allFiles = await Promise.all(
files.map(async (file: string): Promise<string | string[]> => {
const filePath = path.join(dir, file);
const stats = await fs.stat(filePath);

return stats.isDirectory() ? getFilesFromDirectory(filePath) : filePath;
}),
);

return _.flatten(allFiles).filter(Boolean) as string[];
}
Loading

0 comments on commit 2c471d4

Please sign in to comment.