@akanjs/test 0.0.54 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.mjs +1 -0
- package/package.json +7 -1
- package/src/index.mjs +6 -0
- package/src/jest.config.base.mjs +33 -0
- package/src/jest.globalSetup.mjs +19 -0
- package/src/jest.globalTeardown.mjs +11 -0
- package/src/jest.setupFilesAfterEnv.mjs +19 -0
- package/src/jest.testServer.mjs +106 -0
- package/src/playwright.config.base.mjs +57 -0
- package/src/playwright.pageAgent.mjs +42 -0
- package/src/sample.mjs +12 -0
- package/src/sampleOf.mjs +57 -0
package/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/test",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.55",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -24,5 +24,11 @@
|
|
|
24
24
|
"mongoose": "^8.9.3",
|
|
25
25
|
"redis-memory-server": "^0.11.0",
|
|
26
26
|
"tsconfig-paths": "^4.2.0"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"require": "./index.js",
|
|
31
|
+
"import": "./index.mjs"
|
|
32
|
+
}
|
|
27
33
|
}
|
|
28
34
|
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { config } from "dotenv";
|
|
2
|
+
const withBase = (name) => {
|
|
3
|
+
config();
|
|
4
|
+
process.env.NEXT_PUBLIC_ENV = "testing";
|
|
5
|
+
process.env.NEXT_PUBLIC_OPERATION_MODE = "local";
|
|
6
|
+
process.env.NEXT_PUBLIC_APP_NAME = name;
|
|
7
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
8
|
+
const akanjsPrefix = process.env.USE_AKANJS_PKGS === "true" ? "../../pkgs/" : "";
|
|
9
|
+
return {
|
|
10
|
+
displayName: name,
|
|
11
|
+
resolver: "@nx/jest/plugins/resolver",
|
|
12
|
+
globalSetup: `${akanjsPrefix}@akanjs/test/src/jest.globalSetup.ts`,
|
|
13
|
+
setupFilesAfterEnv: [`${akanjsPrefix}@akanjs/test/src/jest.setupFilesAfterEnv.ts`],
|
|
14
|
+
globalTeardown: `${akanjsPrefix}@akanjs/test/src/jest.globalTeardown.ts`,
|
|
15
|
+
testMatch: ["**/?(*.)+(test).ts?(x)"],
|
|
16
|
+
testPathIgnorePatterns: ["/node_modules/", "/app/"],
|
|
17
|
+
maxWorkers: 1,
|
|
18
|
+
transform: {
|
|
19
|
+
"signal\\.(test)\\.ts$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
|
20
|
+
"^.+\\.(ts|js|html)$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }]
|
|
21
|
+
},
|
|
22
|
+
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
|
|
23
|
+
coverageDirectory: `../../coverage/libs/${name}`,
|
|
24
|
+
coverageReporters: ["html"],
|
|
25
|
+
testEnvironment: "node",
|
|
26
|
+
testEnvironmentOptions: {
|
|
27
|
+
customExportConditions: ["node", "require", "default"]
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export {
|
|
32
|
+
withBase
|
|
33
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "tsconfig-paths/register";
|
|
2
|
+
import { TestServer } from "./jest.testServer";
|
|
3
|
+
const setup = async (globalConfig, projectConfig) => {
|
|
4
|
+
const { env } = require(`${globalConfig.rootDir}/env/env.server.testing`);
|
|
5
|
+
const { fetch, registerModules } = require(`${globalConfig.rootDir}/server`);
|
|
6
|
+
const maxWorkers = globalConfig.maxWorkers;
|
|
7
|
+
if (!maxWorkers)
|
|
8
|
+
throw new Error("maxWorkers is not defined");
|
|
9
|
+
const testServers = new Array(maxWorkers).fill(0).map((_, idx) => new TestServer(registerModules, env, idx + 1));
|
|
10
|
+
await Promise.all(testServers.map((server) => server.init()));
|
|
11
|
+
global.__TEST_SERVERS__ = testServers;
|
|
12
|
+
global.fetch = fetch;
|
|
13
|
+
global.env = env;
|
|
14
|
+
global.registerModules = registerModules;
|
|
15
|
+
};
|
|
16
|
+
var jest_globalSetup_default = setup;
|
|
17
|
+
export {
|
|
18
|
+
jest_globalSetup_default as default
|
|
19
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "tsconfig-paths/register";
|
|
2
|
+
const teardown = async (globalConfig, projectConfig) => {
|
|
3
|
+
const testServers = global.__TEST_SERVERS__;
|
|
4
|
+
if (!testServers)
|
|
5
|
+
throw new Error("Test servers are not defined");
|
|
6
|
+
await Promise.all(testServers.map((server) => server.terminate()));
|
|
7
|
+
};
|
|
8
|
+
var jest_globalTeardown_default = teardown;
|
|
9
|
+
export {
|
|
10
|
+
jest_globalTeardown_default as default
|
|
11
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
3
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
4
|
+
};
|
|
5
|
+
import { TestServer } from "./jest.testServer";
|
|
6
|
+
var require_jest_setupFilesAfterEnv = __commonJS({
|
|
7
|
+
"pkgs/@akanjs/test/src/jest.setupFilesAfterEnv.ts"() {
|
|
8
|
+
const { env, fetch } = global;
|
|
9
|
+
jest.setTimeout(3e4);
|
|
10
|
+
global.beforeAll(async () => {
|
|
11
|
+
TestServer.initClient(env);
|
|
12
|
+
await fetch.cleanup();
|
|
13
|
+
});
|
|
14
|
+
global.afterAll(async () => {
|
|
15
|
+
await fetch.client.terminate();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
export default require_jest_setupFilesAfterEnv();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Logger, sleep } from "@akanjs/common";
|
|
2
|
+
import { createNestApp } from "@akanjs/server";
|
|
3
|
+
import { client } from "@akanjs/signal";
|
|
4
|
+
import { MongoMemoryServer } from "mongodb-memory-server";
|
|
5
|
+
import mongoose from "mongoose";
|
|
6
|
+
import { RedisMemoryServer } from "redis-memory-server";
|
|
7
|
+
const MAX_RETRY = 5;
|
|
8
|
+
const TEST_LISTEN_PORT_BASE = 38080;
|
|
9
|
+
const TEST_MONGODB_PORT_BASE = 38081;
|
|
10
|
+
const TEST_REDIS_PORT_BASE = 38082;
|
|
11
|
+
const MIN_ACTIVATION_TIME = 0;
|
|
12
|
+
const MAX_ACTIVATION_TIME = 3e4;
|
|
13
|
+
class TestServer {
|
|
14
|
+
#logger = new Logger("TestServer");
|
|
15
|
+
#mongod;
|
|
16
|
+
#redis;
|
|
17
|
+
#registerModules;
|
|
18
|
+
#env;
|
|
19
|
+
workerId;
|
|
20
|
+
#startAt = Date.now();
|
|
21
|
+
#app;
|
|
22
|
+
#portOffset = 0;
|
|
23
|
+
static initClient(env, workerId = parseInt(process.env.JEST_WORKER_ID ?? "0")) {
|
|
24
|
+
if (workerId === 0)
|
|
25
|
+
throw new Error("TestClient should not be used in main thread");
|
|
26
|
+
const portOffset = workerId * 1e3 + env.appCode * 10;
|
|
27
|
+
const port = TEST_LISTEN_PORT_BASE + portOffset;
|
|
28
|
+
const endpoint = `http://localhost:${port}/backend/graphql`;
|
|
29
|
+
client.init({ uri: endpoint });
|
|
30
|
+
}
|
|
31
|
+
constructor(registerModules, env, workerId) {
|
|
32
|
+
this.workerId = workerId ?? parseInt(process.env.JEST_WORKER_ID ?? "0");
|
|
33
|
+
if (this.workerId === 0)
|
|
34
|
+
throw new Error("TestServer should not be used in main thread");
|
|
35
|
+
this.#portOffset = this.workerId * 1e3 + env.appCode * 10;
|
|
36
|
+
this.#mongod = new MongoMemoryServer({ instance: { port: TEST_MONGODB_PORT_BASE + this.#portOffset } });
|
|
37
|
+
this.#redis = new RedisMemoryServer({ instance: { port: TEST_REDIS_PORT_BASE + this.#portOffset } });
|
|
38
|
+
this.#env = { ...env };
|
|
39
|
+
this.#registerModules = registerModules;
|
|
40
|
+
}
|
|
41
|
+
async init() {
|
|
42
|
+
for (let i = 0; i < MAX_RETRY; i++) {
|
|
43
|
+
try {
|
|
44
|
+
const watchdog = setTimeout(() => {
|
|
45
|
+
throw new Error("TestServer Init Timeout");
|
|
46
|
+
}, MAX_ACTIVATION_TIME);
|
|
47
|
+
await this.#init();
|
|
48
|
+
clearTimeout(watchdog);
|
|
49
|
+
return;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
this.#logger.error(e);
|
|
52
|
+
await this.terminate();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async #init() {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
this.#logger.log(`Test System #${this.workerId} Initializing...`);
|
|
59
|
+
const port = TEST_LISTEN_PORT_BASE + this.#portOffset;
|
|
60
|
+
const [mongoUri, redisHost, redisPort] = await Promise.all([
|
|
61
|
+
this.startMongo(),
|
|
62
|
+
this.#redis.getHost(),
|
|
63
|
+
this.#redis.getPort()
|
|
64
|
+
]);
|
|
65
|
+
this.#env.port = port;
|
|
66
|
+
this.#env.mongoUri = mongoUri;
|
|
67
|
+
this.#env.redisUri = `redis://${redisHost}:${redisPort}`;
|
|
68
|
+
this.#env.meiliUri = "http://localhost:7700/search";
|
|
69
|
+
this.#env.onCleanup = async () => {
|
|
70
|
+
await this.cleanup();
|
|
71
|
+
};
|
|
72
|
+
this.#app = await createNestApp({ registerModules: this.#registerModules, env: this.#env });
|
|
73
|
+
this.#logger.log(`Test System #${this.workerId} Initialized, Mongo: ${mongoUri}, Redis: ${redisHost}:${redisPort}`);
|
|
74
|
+
this.#startAt = Date.now();
|
|
75
|
+
this.#logger.log(`Test System #${this.workerId} Activation Time: ${this.#startAt - now}ms`);
|
|
76
|
+
}
|
|
77
|
+
async startMongo() {
|
|
78
|
+
await this.#mongod.start();
|
|
79
|
+
return this.#mongod.getUri();
|
|
80
|
+
}
|
|
81
|
+
async cleanup() {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
this.#logger.log("Mongo Memory Database Cleaning up...");
|
|
84
|
+
if (this.#mongod.state === "running") {
|
|
85
|
+
await this.#mongod.stop();
|
|
86
|
+
await this.#mongod.start(true);
|
|
87
|
+
}
|
|
88
|
+
this.#logger.log(`Mongo Memory Database Cleaned up in ${Date.now() - now}ms`);
|
|
89
|
+
}
|
|
90
|
+
async terminate() {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const elapsed = now - this.#startAt;
|
|
93
|
+
await sleep(50);
|
|
94
|
+
client.io?.socket.close();
|
|
95
|
+
await this.#app.close();
|
|
96
|
+
if (elapsed < MIN_ACTIVATION_TIME) {
|
|
97
|
+
this.#logger.log(`waiting for ${MIN_ACTIVATION_TIME - elapsed}`);
|
|
98
|
+
await sleep(MIN_ACTIVATION_TIME - elapsed);
|
|
99
|
+
}
|
|
100
|
+
await Promise.all([mongoose.disconnect(), this.#mongod.stop(), this.#redis.stop()]);
|
|
101
|
+
this.#logger.log(`System Terminated in ${Date.now() - now}ms`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
TestServer
|
|
106
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { workspaceRoot } from "@nx/devkit";
|
|
2
|
+
import { nxE2EPreset } from "@nx/playwright/preset";
|
|
3
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
4
|
+
const baseURL = process.env.BASE_URL ?? "http://127.0.0.1:4200";
|
|
5
|
+
const projectName = process.env.NEXT_PUBLIC_APP_NAME ?? "unknown";
|
|
6
|
+
const withBase = (filename, config = {}) => defineConfig({
|
|
7
|
+
...nxE2EPreset(filename, { testDir: "./app" }),
|
|
8
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
9
|
+
use: {
|
|
10
|
+
baseURL,
|
|
11
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
12
|
+
trace: "on-first-retry"
|
|
13
|
+
},
|
|
14
|
+
/* Run your local dev server before starting the tests */
|
|
15
|
+
webServer: {
|
|
16
|
+
command: `nx serve ${projectName}`,
|
|
17
|
+
url: "http://127.0.0.1:4200",
|
|
18
|
+
reuseExistingServer: !process.env.CI,
|
|
19
|
+
cwd: workspaceRoot
|
|
20
|
+
},
|
|
21
|
+
projects: [
|
|
22
|
+
{
|
|
23
|
+
name: "chromium",
|
|
24
|
+
use: { ...devices["Desktop Chrome"] }
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "firefox",
|
|
28
|
+
use: { ...devices["Desktop Firefox"] }
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "webkit",
|
|
32
|
+
use: { ...devices["Desktop Safari"] }
|
|
33
|
+
}
|
|
34
|
+
// Uncomment for mobile browsers support
|
|
35
|
+
/* {
|
|
36
|
+
name: 'Mobile Chrome',
|
|
37
|
+
use: { ...devices['Pixel 5'] },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Mobile Safari',
|
|
41
|
+
use: { ...devices['iPhone 12'] },
|
|
42
|
+
}, */
|
|
43
|
+
// Uncomment for branded browsers
|
|
44
|
+
/* {
|
|
45
|
+
name: 'Microsoft Edge',
|
|
46
|
+
use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'Google Chrome',
|
|
50
|
+
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
51
|
+
} */
|
|
52
|
+
],
|
|
53
|
+
...config
|
|
54
|
+
});
|
|
55
|
+
export {
|
|
56
|
+
withBase
|
|
57
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { test } from "@playwright/test";
|
|
2
|
+
import { expect } from "@playwright/test";
|
|
3
|
+
class PageAgent {
|
|
4
|
+
page;
|
|
5
|
+
#defaultWaitMs = 500;
|
|
6
|
+
#isInitialized = false;
|
|
7
|
+
constructor(page) {
|
|
8
|
+
this.page = page;
|
|
9
|
+
}
|
|
10
|
+
async goto(path) {
|
|
11
|
+
await Promise.all([this.page.goto(path), this.waitForPathChange(path)]);
|
|
12
|
+
if (!this.#isInitialized) {
|
|
13
|
+
this.#isInitialized = true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async waitForPathChange(path) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const timeout = setTimeout(() => {
|
|
19
|
+
reject(new Error("Timeout waiting for pathChange message"));
|
|
20
|
+
}, 3e4);
|
|
21
|
+
this.page.on("console", (msg) => {
|
|
22
|
+
if (msg.type() === "log" && msg.text().startsWith(`%cpathChange-finished:${path ?? ""}`)) {
|
|
23
|
+
clearInterval(timeout);
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
resolve(true);
|
|
26
|
+
}, this.#defaultWaitMs);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async wait(ms = this.#defaultWaitMs) {
|
|
32
|
+
await this.page.waitForTimeout(ms);
|
|
33
|
+
}
|
|
34
|
+
url() {
|
|
35
|
+
return "/" + this.page.url().split("/").slice(4).join("/");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
PageAgent,
|
|
40
|
+
expect,
|
|
41
|
+
test
|
|
42
|
+
};
|
package/src/sample.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { dayjs } from "@akanjs/base";
|
|
2
|
+
import { randomPick, randomPicks } from "@akanjs/common";
|
|
3
|
+
import Chance from "chance";
|
|
4
|
+
const chance = new Chance();
|
|
5
|
+
const sample = Object.assign(chance, {
|
|
6
|
+
dayjs: (opt) => dayjs(chance.date({ ...opt, min: opt?.min?.toDate(), max: opt?.max?.toDate() })),
|
|
7
|
+
pick: randomPick,
|
|
8
|
+
picks: randomPicks
|
|
9
|
+
});
|
|
10
|
+
export {
|
|
11
|
+
sample
|
|
12
|
+
};
|
package/src/sampleOf.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Float, ID, Int, isGqlScalar, JSON, Upload } from "@akanjs/base";
|
|
2
|
+
import { randomPick } from "@akanjs/common";
|
|
3
|
+
import { getFieldMetas } from "@akanjs/constant";
|
|
4
|
+
import { sample } from "./sample";
|
|
5
|
+
const getFieldTypeExample = {
|
|
6
|
+
email: () => sample.email(),
|
|
7
|
+
password: () => sample.string({ length: 8 }),
|
|
8
|
+
url: () => sample.url()
|
|
9
|
+
};
|
|
10
|
+
const scalarSampleMap = /* @__PURE__ */ new Map([
|
|
11
|
+
[ID, () => sample.hash({ length: 24 })],
|
|
12
|
+
[Int, () => sample.integer({ min: -1e4, max: 1e4 })],
|
|
13
|
+
[Float, () => sample.floating({ min: -1e4, max: 1e4 })],
|
|
14
|
+
[String, () => sample.string({ length: 100 })],
|
|
15
|
+
[Boolean, () => sample.bool()],
|
|
16
|
+
[Date, () => sample.dayjs()],
|
|
17
|
+
[Upload, () => "FileUpload"],
|
|
18
|
+
[JSON, () => ({})]
|
|
19
|
+
]);
|
|
20
|
+
const getScalarSample = (ref, fieldMeta) => {
|
|
21
|
+
if (fieldMeta.type) {
|
|
22
|
+
return getFieldTypeExample[fieldMeta.type]();
|
|
23
|
+
} else if (typeof fieldMeta.min === "number") {
|
|
24
|
+
return fieldMeta.min;
|
|
25
|
+
} else if (typeof fieldMeta.max === "number") {
|
|
26
|
+
return fieldMeta.max;
|
|
27
|
+
} else {
|
|
28
|
+
return scalarSampleMap.get(ref)?.() ?? null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const makeSample = (fieldMeta) => {
|
|
32
|
+
if (fieldMeta.default)
|
|
33
|
+
return typeof fieldMeta.default === "function" ? fieldMeta.default() : fieldMeta.default;
|
|
34
|
+
else if (fieldMeta.enum)
|
|
35
|
+
return randomPick([...fieldMeta.enum.values]);
|
|
36
|
+
if (isGqlScalar(fieldMeta.modelRef))
|
|
37
|
+
return getScalarSample(fieldMeta.modelRef, fieldMeta);
|
|
38
|
+
return Object.fromEntries(
|
|
39
|
+
getFieldMetas(fieldMeta.modelRef).map(
|
|
40
|
+
(fieldMeta2) => [
|
|
41
|
+
fieldMeta2.key,
|
|
42
|
+
fieldMeta2.arrDepth ? [] : fieldMeta2.isClass && !fieldMeta2.isScalar ? null : makeSample(fieldMeta2)
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
const sampleOf = (modelRef) => {
|
|
48
|
+
return Object.fromEntries(
|
|
49
|
+
getFieldMetas(modelRef).map((fieldMeta) => [
|
|
50
|
+
fieldMeta.key,
|
|
51
|
+
fieldMeta.arrDepth ? [] : fieldMeta.isClass && !fieldMeta.isScalar ? null : makeSample(fieldMeta)
|
|
52
|
+
])
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
export {
|
|
56
|
+
sampleOf
|
|
57
|
+
};
|