Skip to content

Commit 8fc9934

Browse files
feat!: add async function config support
1 parent 1604cb6 commit 8fc9934

19 files changed

Lines changed: 939 additions & 845 deletions

File tree

src/base-testplane.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,31 @@ import { ConfigInput } from "./config/types";
1616

1717
export abstract class BaseTestplane extends AsyncEmitter {
1818
protected _interceptors: Interceptor[] = [];
19-
protected _config: Config;
19+
protected _config!: Config;
20+
private _pendingConfig?: string | ConfigInput;
2021

21-
static create<T extends BaseTestplane>(
22+
static async create<T extends BaseTestplane>(
2223
this: new (config?: string | ConfigInput) => T,
2324
config?: string | ConfigInput,
24-
): T {
25-
return new this(config);
25+
): Promise<T> {
26+
const instance = new this(config);
27+
28+
await instance._setup();
29+
30+
return instance;
2631
}
2732

2833
protected constructor(config?: string | ConfigInput) {
2934
super();
3035

3136
this._interceptors = [];
37+
this._pendingConfig = config;
38+
}
3239

40+
protected async _setup(): Promise<void> {
3341
registerTransformHook(this.isWorker());
34-
this._config = Config.create(config);
42+
this._config = await Config.create(this._pendingConfig);
43+
this._pendingConfig = undefined;
3544
updateTransformHook(this._config);
3645

3746
this._setLogLevel();

src/cli/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const run = async (opts: TestplaneRunOpts = {}): Promise<void> => {
5858
}
5959

6060
const configPath = preparseOption(program, "config") as string;
61-
testplane = Testplane.create(configPath);
61+
testplane = await Testplane.create(configPath);
6262

6363
withCommonCliOptions({ cmd: program, actionName: "run" })
6464
.on("--help", () => console.log(configOverriding(opts)))

src/config/index.ts

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,103 @@
1-
import * as path from "path";
2-
import * as _ from "lodash";
1+
import path from "path";
2+
import _ from "lodash";
33
import defaults from "./defaults";
44
import { BrowserConfig } from "./browser-config";
55
import parseOptions from "./options";
66
import * as logger from "../utils/logger";
7-
import { ConfigInput, ConfigParsed } from "./types";
7+
import { ConfigInput, ConfigInputData, ConfigParsed } from "./types";
88
import { addUserAgentToArgs } from "./utils";
99

1010
export { TimeTravelMode } from "./types";
1111

1212
export class Config {
13-
configPath!: string;
13+
configPath?: string;
1414

15-
static create(config?: string | ConfigInput): Config {
16-
return new Config(config);
15+
static async create(config?: string | ConfigInput): Promise<Config> {
16+
try {
17+
const { configPath, options } = await Config._resolve(config);
18+
19+
await Config._prepareEnvironment(options);
20+
21+
return new Config(options, configPath);
22+
} catch (e: unknown) {
23+
const error = new Error(`Got an error while trying to read config: ${(e as Error).message}`);
24+
error.stack = (e as Error).stack;
25+
error.cause = (e as Error).cause;
26+
27+
throw error;
28+
}
1729
}
1830

19-
static read(configPath: string): unknown {
31+
static async read(configPath: string): Promise<ConfigInputData> {
2032
try {
2133
// eslint-disable-next-line @typescript-eslint/no-var-requires
2234
const configModule = require(path.resolve(process.cwd(), configPath));
35+
const exported = (configModule.__esModule ? configModule.default : configModule) as ConfigInput;
2336

24-
return configModule.__esModule ? configModule.default : configModule;
37+
return await Config._resolveExportedConfig(exported);
2538
} catch (e) {
2639
logger.error(`Unable to read config from path ${configPath}`);
2740
throw e;
2841
}
2942
}
3043

31-
constructor(config?: string | ConfigInput) {
32-
let options: ConfigInput;
44+
private static async _resolve(
45+
config?: string | ConfigInput,
46+
): Promise<{ configPath?: string; options: ConfigInputData }> {
47+
if (typeof config === "function") {
48+
return { options: await Config._resolveExportedConfig(config) };
49+
}
50+
3351
if (_.isObjectLike(config)) {
34-
options = config as ConfigInput;
35-
} else if (typeof config === "string") {
36-
this.configPath = config;
37-
options = Config.read(config) as ConfigInput;
38-
} else {
39-
for (const configPath of defaults.configPaths) {
40-
try {
41-
const resolvedConfigPath = path.resolve(configPath);
42-
require(resolvedConfigPath);
43-
this.configPath = resolvedConfigPath;
44-
45-
break;
46-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
47-
} catch (err: any) {
48-
if (err.code !== "MODULE_NOT_FOUND") {
49-
throw err;
50-
}
52+
return { options: config as ConfigInputData };
53+
}
54+
55+
if (typeof config === "string") {
56+
return { configPath: config, options: await Config.read(config) };
57+
}
58+
59+
const located = Config._locateConfigPath();
60+
61+
if (!located) {
62+
throw new Error(`Unable to read config from paths: ${defaults.configPaths.join(", ")}`);
63+
}
64+
65+
return { configPath: located, options: await Config.read(located) };
66+
}
67+
68+
private static _locateConfigPath(): string | null {
69+
for (const configPath of defaults.configPaths) {
70+
try {
71+
const resolvedConfigPath = path.resolve(configPath);
72+
// eslint-disable-next-line @typescript-eslint/no-var-requires
73+
require(resolvedConfigPath);
74+
75+
return resolvedConfigPath;
76+
} catch (err: unknown) {
77+
if ((err as { code?: string }).code !== "MODULE_NOT_FOUND") {
78+
throw err;
5179
}
5280
}
81+
}
5382

54-
if (!this.configPath) {
55-
throw new Error(`Unable to read config from paths: ${defaults.configPaths.join(", ")}`);
56-
}
83+
return null;
84+
}
5785

58-
options = Config.read(this.configPath) as ConfigInput;
59-
}
86+
private static async _resolveExportedConfig(exported: ConfigInput): Promise<ConfigInputData> {
87+
const resolved = typeof exported === "function" ? await (exported as () => unknown)() : exported;
88+
89+
return resolved as ConfigInputData;
90+
}
6091

92+
private static async _prepareEnvironment(options: ConfigInputData): Promise<void> {
6193
if (_.isFunction(options.prepareEnvironment)) {
62-
options.prepareEnvironment();
94+
await options.prepareEnvironment();
95+
}
96+
}
97+
98+
constructor(options: ConfigInputData, configPath?: string) {
99+
if (configPath) {
100+
this.configPath = configPath;
63101
}
64102

65103
const parsedOptions = parseOptions({

src/config/types.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -460,20 +460,22 @@ type PartialCommonConfig = Partial<
460460
export type HookType = (params: { config: Config }) => Promise<void> | undefined;
461461

462462
// Only browsers desiredCapabilities are required in input config
463-
export type ConfigInput = Partial<PartialCommonConfig> & {
463+
export type ConfigInputData = Partial<PartialCommonConfig> & {
464464
browsers: Record<string, PartialCommonConfig & { desiredCapabilities: WebdriverIO.Capabilities }>;
465465
plugins?: Record<string, unknown>;
466466
sets?: Record<string, SetsConfig>;
467-
prepareEnvironment?: () => void | null;
467+
prepareEnvironment?: () => void | Promise<void> | null;
468468
beforeAll?: HookType;
469469
afterAll?: HookType;
470470
};
471471

472+
export type ConfigInput = ConfigInputData | (() => ConfigInputData) | (() => Promise<ConfigInputData>);
473+
472474
export interface ConfigParsed extends CommonConfig {
473475
browsers: Record<string, BrowserConfig>;
474476
plugins: Record<string, Record<string, unknown>>;
475477
sets: Record<string, SetsConfigParsed>;
476-
prepareEnvironment?: () => void | null;
478+
prepareEnvironment?: () => void | Promise<void> | null;
477479
beforeAll?: HookType;
478480
afterAll?: HookType;
479481
}

src/dev-server/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { findCwd, pipeLogsWithPrefix, probeServer, waitDevServerReady } from "./
66
import * as logger from "../utils/logger";
77
import type { Testplane } from "../testplane";
88

9-
export type DevServerOpts = { testplane: Testplane; devServerConfig: Config["devServer"]; configPath: string };
9+
export type DevServerOpts = {
10+
testplane: Testplane;
11+
devServerConfig: Config["devServer"];
12+
configPath?: string;
13+
};
1014

1115
export type InitDevServer = (opts: DevServerOpts) => Promise<void>;
1216

@@ -45,7 +49,7 @@ export const initDevServer: InitDevServer = async ({ testplane, devServerConfig,
4549

4650
const devServer = spawn(devServerConfig.command, devServerConfig.args, {
4751
env: { ...process.env, ...devServerConfig.env },
48-
cwd: devServerConfig.cwd || findCwd(configPath),
52+
cwd: devServerConfig.cwd || (configPath ? findCwd(configPath) : process.cwd()),
4953
shell: true,
5054
windowsHide: true,
5155
});

src/worker/testplane-facade.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,15 @@ module.exports = class TestplaneFacade {
6969
});
7070
}
7171

72-
promise = promise.then(() => {
73-
RuntimeConfig.getInstance().extend(runtimeConfig);
74-
const testplane = Testplane.create(configPath);
75-
76-
debug("worker initialized");
77-
resolve(testplane);
78-
});
72+
promise = promise
73+
.then(async () => {
74+
RuntimeConfig.getInstance().extend(runtimeConfig);
75+
const testplane = await Testplane.create(configPath);
76+
77+
debug("worker initialized");
78+
resolve(testplane);
79+
})
80+
.catch(reject);
7981
} catch (e) {
8082
debug("worker initialization failed");
8183
reject(e);

src/worker/testplane.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,14 @@ export interface Testplane {
4141
}
4242

4343
export class Testplane extends BaseTestplane {
44-
protected runner: Runner;
44+
protected runner!: Runner;
4545

4646
constructor(config?: string | ConfigInput) {
4747
super(config);
48+
}
49+
50+
protected async _setup(): Promise<void> {
51+
await super._setup();
4852

4953
this.runner = Runner.create(this._config);
5054

test/src/cli/commands/config/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe("cli/commands/config", () => {
2121
beforeEach(() => {
2222
testplaneStub = Object.create(Testplane.prototype);
2323

24-
sandbox.stub(Testplane, "create").returns(testplaneStub);
24+
sandbox.stub(Testplane, "create").resolves(testplaneStub);
2525

2626
consoleInfoStub = sandbox.stub(console, "info");
2727
jsonStringifyStub = sandbox.spy(JSON, "stringify");

test/src/cli/commands/install-deps/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import type { Config } from "../../../../../src/config";
99
describe("cli/commands/install-deps", () => {
1010
const sandbox = sinon.createSandbox();
1111

12-
let cli: { run: () => void };
12+
let cli: { run: () => Promise<void> };
1313
let loggerStub: { log: SinonStub; warn: SinonStub; error: SinonStub };
1414
let testplaneStub: Writable<Testplane>;
1515
let installBrowsersWithDriversStub: SinonStub;
1616

1717
const installBrowsers_ = async (argv: string = ""): Promise<void> => {
1818
process.argv = ["foo/bar/node", "foo/bar/script", "install-deps", ...argv.split(" ")].filter(Boolean);
19-
cli.run();
19+
await cli.run();
2020

2121
await new Promise(resolve => setImmediate(resolve));
2222
};
@@ -36,7 +36,7 @@ describe("cli/commands/install-deps", () => {
3636
configurable: true,
3737
});
3838

39-
sandbox.stub(Testplane, "create").returns(testplaneStub as Testplane);
39+
sandbox.stub(Testplane, "create").resolves(testplaneStub as Testplane);
4040

4141
sandbox.stub(process, "exit");
4242

test/src/cli/commands/list-browsers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ describe("cli/commands/list-browsers", () => {
5959
configurable: true,
6060
});
6161

62-
sandbox.stub(Testplane, "create").returns(testplaneStub);
62+
sandbox.stub(Testplane, "create").resolves(testplaneStub);
6363

6464
consoleInfoStub = sandbox.stub(console, "info");
6565
sandbox.stub(process, "exit");

0 commit comments

Comments
 (0)