Skip to content
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

feat: ability to read tests in ".jsx" and ".tsx" files #851

Merged
merged 1 commit into from
Mar 21, 2024
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
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,7 @@ The maximum number of tests to be run in one worker before it will be restarted.
By default, `hermione` will run all browsers simultaneously. Sometimes (i.e. when using cloud services, such as SauceLabs) you have to limit the amount of browsers that can be run at the same time. This option effectively limits how many browsers `hermione` will try to run in parallel. Default value is `Infinity`.

#### fileExtensions
Ability to set file extensions, which hermione will search on the file system. Default value is `[.js]`.
Ability to set file extensions, which hermione will search on the file system. Default value is `[".js", ".mjs", ".ts", ".mts", ".jsx", ".tsx"]`.

### plugins
`Hermione` plugins are commonly used to extend built-in functionality. For example, [html-reporter](https://github.com/gemini-testing/html-reporter) and [hermione-safari-commands](https://github.com/gemini-testing/hermione-safari-commands).
Expand Down Expand Up @@ -1621,8 +1621,6 @@ Using `-r` or `--require` option you can load external modules, which exists in
- compilers such as TypeScript via [ts-node](https://www.npmjs.com/package/ts-node) (using `--require ts-node/register`) or Babel via [@babel/register](https://www.npmjs.com/package/@babel/register) (using `--require @babel/register`);
- loaders such as ECMAScript modules via [esm](https://www.npmjs.com/package/esm).

Be sure to update [fileExtensions](#fileExtensions) apropriately, if you are planning to import anything other than `.js`.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted because it doesn't make sense anymore


### Overriding settings

All options can also be overridden via command-line flags or environment variables. Priorities are the following:
Expand Down
2,837 changes: 2,470 additions & 367 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"typings"
],
"scripts": {
"build": "tsc && npm run copy-static",
"build": "tsc && npm run copy-static && npm run build-bundle -- --minify",
"build-bundle": "esbuild ./src/bundle/index.ts --outdir=./build/src/bundle --bundle --format=cjs --platform=node --target=ES2021",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I build a bundle in order to not install all heavy babel deps in dependencies

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add it to the watch script as well.

In watch script, we can either build the bundle once on startup, or make 2 separate scripts like watch:bundle watch:src and unite them in watch script via concurrently (we use the same approach in html-reporter).

I'm not sure how often we'll want to change (watch) bundle contents, so it's up to you to decide.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think about it, but it seemed to me that this file would rarely change. Anyway I add it to watch script.

"copy-static": "copyfiles 'src/browser/client-scripts/*' build",
"check-types": "tsc --project tsconfig.spec.json",
"clean": "rimraf build/ *.tsbuildinfo",
Expand Down Expand Up @@ -79,10 +80,16 @@
"yallist": "3.1.1"
},
"devDependencies": {
"@babel/core": "7.24.1",
"@babel/plugin-transform-modules-commonjs": "7.24.1",
"@babel/plugin-transform-react-jsx": "7.23.4",
"@babel/preset-react": "7.24.1",
"@babel/preset-typescript": "7.24.1",
"@commitlint/cli": "^19.0.3",
"@commitlint/config-conventional": "^19.0.3",
"@sinonjs/fake-timers": "10.3.0",
"@swc/core": "1.3.40",
"@types/babel__core": "7.20.5",
"@types/bluebird": "3.5.38",
"@types/chai": "4.3.4",
"@types/chai-as-promised": "7.1.5",
Expand All @@ -99,6 +106,7 @@
"chai-as-promised": "7.1.1",
"copyfiles": "2.4.1",
"doctoc": "2.2.0",
"esbuild": "0.20.2",
"escape-string-regexp": "1.0.5",
"eslint": "8.25.0",
"eslint-config-gemini-testing": "2.8.0",
Expand All @@ -107,6 +115,7 @@
"jsdom": "^24.0.0",
"jsdom-global": "3.0.2",
"onchange": "7.1.0",
"pirates": "4.0.6",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all deps installed in dev deps and used only through bundle

"prettier": "2.8.4",
"proxyquire": "1.8.0",
"rimraf": "4.1.2",
Expand Down
2 changes: 2 additions & 0 deletions src/bundle/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const TRANSFORM_EXTENSIONS = [".jsx", ".cjsx", ".mjsx", ".tsx", ".ctsx", ".mtsx"];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extensions which should be transformed. I took various types of dependencies in which components can be described from this site - https://www.totaltypescript.com/concepts/mjs-cjs-mts-and-cts-extensions.

export const JS_EXTENSION_RE = /^\.([cm]?[tj]sx?|json)$/;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used in order to transform only javascript files and json. Omit all other extensions like .png, .woff, .css and etc.

2 changes: 2 additions & 0 deletions src/bundle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./test-transformer";
export * from "./constants";
49 changes: 49 additions & 0 deletions src/bundle/test-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as nodePath from "node:path";
import * as babel from "@babel/core";
import { addHook } from "pirates";
import { TRANSFORM_EXTENSIONS, JS_EXTENSION_RE } from "./constants";

import type { NodePath, PluginObj, TransformOptions } from "@babel/core";
import type { ImportDeclaration } from "@babel/types";

export const setupTransformHook = (): VoidFunction => {
const transformOptions: TransformOptions = {
browserslistConfigFile: false,
babelrc: false,
configFile: false,
compact: false,
plugins: [
[
require("@babel/plugin-transform-react-jsx"),
{
throwIfNamespace: false,
runtime: "automatic",
},
],
require("@babel/plugin-transform-modules-commonjs"),
[
(): PluginObj => ({
name: "ignore-imports",
visitor: {
ImportDeclaration(path: NodePath<ImportDeclaration>): void {
const extname = nodePath.extname(path.node.source.value);

if (extname && !extname.match(JS_EXTENSION_RE)) {
path.remove();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here omit imports like .css and others which can be transformed

}
},
},
}),
],
],
};

const revertTransformHook = addHook(
(originalCode, filename) => {
return babel.transform(originalCode, { filename, ...transformOptions })!.code as string;
},
{ exts: TRANSFORM_EXTENSIONS },
);

return revertTransformHook;
};
2 changes: 1 addition & 1 deletion src/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ module.exports = {
resetCursor: true,
strictTestsOrder: false,
saveHistoryMode: SAVE_HISTORY_MODE.ALL,
fileExtensions: [".js", ".mjs", ".ts", ".mts"],
fileExtensions: [".js", ".mjs", ".ts", ".mts", ".jsx", ".tsx"],
outputDir: null,
agent: null,
headers: null,
Expand Down
5 changes: 5 additions & 0 deletions src/test-reader/test-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { TreeBuilder } = require("./tree-builder");
const { readFiles } = require("./mocha-reader");
const { TestReaderEvents } = require("../events");
const { TestParserAPI } = require("./test-parser-api");
const { setupTransformHook } = require("./test-transformer");
const { MasterEvents } = require("../events");
const _ = require("lodash");
const clearRequire = require("clear-require");
Expand Down Expand Up @@ -47,9 +48,13 @@ class TestParser extends EventEmitter {

this.#clearRequireCache(files);

const revertTransformHook = setupTransformHook();

const rand = Math.random();
const esmDecorator = f => f + `?rand=${rand}`;
await readFiles(files, { esmDecorator, config: mochaOpts, eventBus });

revertTransformHook();
}

#applyInstructionsEvents(eventBus) {
Expand Down
3 changes: 3 additions & 0 deletions src/test-reader/test-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
export const setupTransformHook: () => VoidFunction = require("../bundle").setupTransformHook;
export const TRANSFORM_EXTENSIONS: string[] = require("../bundle").TRANSFORM_EXTENSIONS;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I use require in order to ts not understand that folder bundle should be builded. If I use import tsc build it even if exclude it in tsconfig

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding this comment to the code as well.

To me it seems like typescript's project references would allow the use of imports here, but don't include those files in build. But probably not worth spending time on this right now, maybe just leave a todo in the comment.

20 changes: 20 additions & 0 deletions test/src/test-reader/test-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,21 @@ describe("test-reader/test-parser", () => {
let TestParser;
let clearRequire;
let readFiles;
let setupTransformHook;
let browserVersionController = {
mkProvider: sinon.stub().returns(() => {}),
};

beforeEach(() => {
clearRequire = sandbox.stub().named("clear-require");
readFiles = sandbox.stub().named("readFiles").resolves();
setupTransformHook = sandbox.stub().named("setupTransformHook").returns(sinon.stub());

TestParser = proxyquire("src/test-reader/test-parser", {
"clear-require": clearRequire,
"./mocha-reader": { readFiles },
"./controllers/browser-version-controller": browserVersionController,
"./test-transformer": { setupTransformHook },
}).TestParser;

sandbox.stub(InstructionsList.prototype, "push").returnsThis();
Expand Down Expand Up @@ -369,6 +372,23 @@ describe("test-reader/test-parser", () => {
assert.notEqual(firstCallModuleName, lastCallModuleName);
});
});

describe("transform hook", () => {
it("should setup hook before read files", async () => {
await loadFiles_({ files: ["foo/bar", "baz/qux"] });

assert.callOrder(setupTransformHook, readFiles);
});

it("should call revert transformation after read files", async () => {
const revertFn = sinon.stub();
setupTransformHook.returns(revertFn);

await loadFiles_({ files: ["foo/bar", "baz/qux"] });

assert.callOrder(readFiles, revertFn);
});
});
});

describe("root suite decorators", () => {
Expand Down
74 changes: 74 additions & 0 deletions test/src/test-reader/test-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as pirates from "pirates";
import sinon, { SinonStub } from "sinon";
import { setupTransformHook, TRANSFORM_EXTENSIONS } from "../../../src/test-reader/test-transformer";

describe("test-transformer", () => {
const sandbox = sinon.createSandbox();

beforeEach(() => {
sandbox.stub(pirates, "addHook").returns(sandbox.stub());
});

afterEach(() => sandbox.restore());

describe("setupTransformHook", () => {
it("should return function to revert transformation", () => {
const revertFn = sandbox.stub();
(pirates.addHook as SinonStub).returns(revertFn);

assert.equal(setupTransformHook(), revertFn);
});

describe("should transform", () => {
TRANSFORM_EXTENSIONS.forEach(extName => {
it(`component with extension: "${extName}"`, () => {
let transformedCode;
(pirates.addHook as SinonStub).callsFake(cb => {
transformedCode = cb(`import "some${extName}"`);
});

setupTransformHook();

assert.exists(transformedCode, `require("some${extName}")`);
});
});

it("modules without extension", () => {
let transformedCode;
(pirates.addHook as SinonStub).callsFake(cb => {
transformedCode = cb('import "some-module"');
});

setupTransformHook();

assert.exists(transformedCode, 'require("some-module")');
});

it(".json", () => {
let transformedCode;
(pirates.addHook as SinonStub).callsFake(cb => {
transformedCode = cb('import "some.json"');
});

setupTransformHook();

assert.exists(transformedCode, 'require("some.json")');
});
});

describe("should not transform", () => {
[".css", ".less", ".scss", ".jpg", ".png", ".woff"].forEach(extName => {
it(`asset with extension: "${extName}"`, () => {
let transformedCode;
(pirates.addHook as SinonStub).callsFake(cb => {
transformedCode = cb(`import "some${extName}"`);
});

setupTransformHook();

assert.equal(transformedCode, '"use strict";');
});
});
});
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extends": "./tsconfig.common.json",
"include": ["src"],
"exclude": ["src/**/__*", "src/**/*.test.ts", "src/browser/client-scripts"],
"exclude": ["src/**/__*", "src/**/*.test.ts", "src/browser/client-scripts", "src/bundle"],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/bundle should not be build by typescript. I build it using esbuild

"compilerOptions": {
"outDir": "build"
}
Expand Down
Loading