harmonyc 0.4.3 → 0.6.0-0
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/README.md +26 -9
- package/cli.js +17 -6
- package/compile.js +3 -2
- package/compiler.js +5 -6
- package/js_api/js_api.js +59 -0
- package/js_api/js_api.steps.js +11 -0
- package/languages/JavaScript.js +22 -12
- package/outFile.js +3 -2
- package/package.json +1 -1
- package/run.js +24 -0
- package/watch.js +2 -1
package/README.md
CHANGED
|
@@ -6,18 +6,35 @@ A test design & BDD tool that helps you separate the _what_ to test from the _ho
|
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
2. You can compile your `*.md` files in the `src` folder by running
|
|
9
|
+
- ### watch and run mode
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
npx harmonyc src/**/*.md
|
|
14
|
-
```
|
|
11
|
+
- You can compile and run your `*.md` files in the `src` folder, and watch it, by running
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
```bash script
|
|
14
|
+
npx harmonyc --run --watch src/**/*.md
|
|
15
|
+
```
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
- ### compiling and running
|
|
18
|
+
|
|
19
|
+
- You can compile and run your `*.md` files in the `src` folder by running
|
|
20
|
+
|
|
21
|
+
```bash script
|
|
22
|
+
npx harmonyc --run src/**/*.md
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- ### compiling and running separately
|
|
26
|
+
|
|
27
|
+
1. You can compile your `*.md` files in the `src` folder by running
|
|
28
|
+
|
|
29
|
+
```bash script
|
|
30
|
+
npx harmonyc src/**/*.md
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
2. Then you can run the generated tests with Vitest by running
|
|
34
|
+
|
|
35
|
+
```bash script
|
|
36
|
+
npx vitest
|
|
37
|
+
```
|
|
21
38
|
|
|
22
39
|
## Syntax
|
|
23
40
|
|
package/cli.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import { compileFiles } from './compiler.js';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { watchFiles } from './watch.js';
|
|
5
|
+
import { run, runWatch } from './run.js';
|
|
5
6
|
const args = parseArgs({
|
|
6
7
|
options: {
|
|
7
8
|
help: { type: 'boolean' },
|
|
8
9
|
watch: { type: 'boolean' },
|
|
10
|
+
run: { type: 'boolean' },
|
|
9
11
|
},
|
|
10
12
|
allowPositionals: true,
|
|
11
13
|
});
|
|
@@ -13,9 +15,18 @@ if (args.positionals.length === 0 || args.values.help) {
|
|
|
13
15
|
console.error('Usage: harmonyc <input files glob pattern>');
|
|
14
16
|
process.exit(1);
|
|
15
17
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
;
|
|
19
|
+
(async () => {
|
|
20
|
+
if (args.values.watch) {
|
|
21
|
+
const outFns = await watchFiles(args.positionals);
|
|
22
|
+
if (args.values.run) {
|
|
23
|
+
runWatch(outFns);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const { outFns } = await compileFiles(args.positionals);
|
|
28
|
+
if (args.values.run) {
|
|
29
|
+
run(outFns);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})();
|
package/compile.js
CHANGED
|
@@ -3,8 +3,9 @@ import { OutFile } from './outFile.js';
|
|
|
3
3
|
import { parse } from './syntax.js';
|
|
4
4
|
export function compileFeature(fileName, src) {
|
|
5
5
|
const feature = parse({ fileName, src });
|
|
6
|
-
const
|
|
6
|
+
const outFn = `${fileName.replace(/\.[a-z]+$/i, '')}.mjs`;
|
|
7
|
+
const of = new OutFile(outFn);
|
|
7
8
|
const cg = new NodeTest(of);
|
|
8
9
|
feature.toCode(cg);
|
|
9
|
-
return of
|
|
10
|
+
return of;
|
|
10
11
|
}
|
package/compiler.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import glob from 'fast-glob';
|
|
2
2
|
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
-
import { basename } from 'path';
|
|
4
3
|
import { compileFeature } from './compile.js';
|
|
5
4
|
export async function compileFiles(pattern) {
|
|
6
5
|
const fns = await glob(pattern);
|
|
7
6
|
if (!fns.length)
|
|
8
7
|
throw new Error(`No files found for pattern: ${String(pattern)}`);
|
|
9
|
-
await Promise.all(fns.map((fn) => compileFile(fn)));
|
|
8
|
+
const outFns = await Promise.all(fns.map((fn) => compileFile(fn)));
|
|
10
9
|
console.log(`Compiled ${fns.length} file${fns.length === 1 ? '' : 's'}.`);
|
|
11
|
-
return fns;
|
|
10
|
+
return { fns, outFns };
|
|
12
11
|
}
|
|
13
12
|
export async function compileFile(fn) {
|
|
14
13
|
const src = readFileSync(fn, 'utf8').toString();
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const outFile = compileFeature(fn, src);
|
|
15
|
+
writeFileSync(outFile.name, outFile.value);
|
|
16
|
+
return outFile.name;
|
|
18
17
|
}
|
package/js_api/js_api.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var _FeatureContext_defs, _FeatureContext_parameterTypeRegistry;
|
|
7
|
+
import { CucumberExpression, ParameterTypeRegistry, } from '@cucumber/cucumber-expressions';
|
|
8
|
+
class Definition {
|
|
9
|
+
constructor(expr, fn) {
|
|
10
|
+
this.expr = expr;
|
|
11
|
+
this.fn = fn;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const features = new Map();
|
|
15
|
+
class FeatureContext {
|
|
16
|
+
constructor() {
|
|
17
|
+
_FeatureContext_defs.set(this, []);
|
|
18
|
+
_FeatureContext_parameterTypeRegistry.set(this, new ParameterTypeRegistry());
|
|
19
|
+
this.Action = ((s, fn) => {
|
|
20
|
+
if (fn) {
|
|
21
|
+
const expr = new CucumberExpression(s, __classPrivateFieldGet(this, _FeatureContext_parameterTypeRegistry, "f"));
|
|
22
|
+
const def = new Definition(expr, fn);
|
|
23
|
+
__classPrivateFieldGet(this, _FeatureContext_defs, "f").push(def);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// call the action
|
|
27
|
+
const matches = __classPrivateFieldGet(this, _FeatureContext_defs, "f").map((def) => def.expr.match(s));
|
|
28
|
+
const matching = [...matches.keys()].filter((i) => matches[i]);
|
|
29
|
+
if (matching.length === 0) {
|
|
30
|
+
throw new Error(`Not defined: ${s}`);
|
|
31
|
+
}
|
|
32
|
+
if (matching.length > 1) {
|
|
33
|
+
throw new Error(`Ambiguous: ${s}\n${matching
|
|
34
|
+
.map((i) => __classPrivateFieldGet(this, _FeatureContext_defs, "f")[i].expr.source)
|
|
35
|
+
.join('\n')}`);
|
|
36
|
+
}
|
|
37
|
+
const match = matches[matching[0]];
|
|
38
|
+
const def = __classPrivateFieldGet(this, _FeatureContext_defs, "f")[matching[0]];
|
|
39
|
+
return Promise.resolve(def.fn(...match.map((m) => m.getValue(undefined))));
|
|
40
|
+
});
|
|
41
|
+
this.Response = this.Action;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
_FeatureContext_defs = new WeakMap(), _FeatureContext_parameterTypeRegistry = new WeakMap();
|
|
45
|
+
export function Feature(s, fn) {
|
|
46
|
+
let ctx;
|
|
47
|
+
if (!fn) {
|
|
48
|
+
ctx = features.get(s);
|
|
49
|
+
if (!ctx)
|
|
50
|
+
throw new Error(`Feature not found: ${s}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// redefine the feature
|
|
54
|
+
ctx = new FeatureContext();
|
|
55
|
+
features.set(s, ctx);
|
|
56
|
+
}
|
|
57
|
+
fn === null || fn === void 0 ? void 0 : fn(ctx);
|
|
58
|
+
return ctx;
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
import { Feature } from 'harmonyc/test';
|
|
3
|
+
Feature('js api', ({ Action, Response }) => {
|
|
4
|
+
let n = 0;
|
|
5
|
+
Action('add {int}', async function (k) {
|
|
6
|
+
n += k;
|
|
7
|
+
});
|
|
8
|
+
Response('result is {float}', async function (k) {
|
|
9
|
+
expect(n).toBe(k);
|
|
10
|
+
});
|
|
11
|
+
});
|
package/languages/JavaScript.js
CHANGED
|
@@ -1,31 +1,41 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
1
2
|
export class NodeTest {
|
|
2
|
-
constructor(
|
|
3
|
-
this.
|
|
3
|
+
constructor(of) {
|
|
4
|
+
this.of = of;
|
|
4
5
|
this.framework = 'vitest';
|
|
6
|
+
this.phrases = [];
|
|
5
7
|
}
|
|
6
8
|
feature(feature) {
|
|
9
|
+
const stepsModule = './' +
|
|
10
|
+
basename(this.of.name.replace(/(\.(spec|test)s?)?\.[a-z]+$/i, '.steps'));
|
|
11
|
+
this.phrases = [];
|
|
7
12
|
if (this.framework === 'vitest') {
|
|
8
|
-
this.
|
|
13
|
+
this.of.print(`import { test, expect } from 'vitest';`);
|
|
14
|
+
this.of.print(`import { Feature } from 'harmonyc/test';`);
|
|
15
|
+
this.of.print(`import ${JSON.stringify(stepsModule)};`);
|
|
9
16
|
}
|
|
10
|
-
this.
|
|
17
|
+
this.of.print(feature.prelude);
|
|
11
18
|
for (const test of feature.tests) {
|
|
12
19
|
test.toCode(this);
|
|
13
20
|
}
|
|
14
21
|
}
|
|
15
22
|
test(t) {
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
23
|
+
this.of.print(`test('${t.name}', async (context) => {`);
|
|
24
|
+
this.of.indent(() => {
|
|
18
25
|
for (const step of t.steps) {
|
|
19
26
|
step.toCode(this);
|
|
20
27
|
}
|
|
21
28
|
});
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
29
|
+
this.of.print('})');
|
|
30
|
+
this.of.print('');
|
|
24
31
|
}
|
|
25
32
|
phrase(p) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
this.
|
|
33
|
+
if (!this.phrases.some((x) => x.text === p.text))
|
|
34
|
+
this.phrases.push(p);
|
|
35
|
+
const feature = p.feature.name;
|
|
36
|
+
this.of.print(`await Feature(${JSON.stringify(feature)}).${capitalize(p.kind)}(${JSON.stringify(p.text)})`);
|
|
30
37
|
}
|
|
31
38
|
}
|
|
39
|
+
function capitalize(s) {
|
|
40
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
41
|
+
}
|
package/outFile.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { SourceMapGenerator } from 'source-map-js';
|
|
2
2
|
export class OutFile {
|
|
3
|
-
constructor(
|
|
4
|
-
this.
|
|
3
|
+
constructor(name) {
|
|
4
|
+
this.name = name;
|
|
5
5
|
this.lines = [];
|
|
6
6
|
this.level = 0;
|
|
7
7
|
this.sm = new SourceMapGenerator();
|
|
8
|
+
this.indentSpaces = 2;
|
|
8
9
|
}
|
|
9
10
|
indent(fn) {
|
|
10
11
|
this.level++;
|
package/package.json
CHANGED
package/run.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
function runCommand(patterns) {
|
|
3
|
+
return `npx vitest run ${args(patterns)}`;
|
|
4
|
+
}
|
|
5
|
+
function runWatchCommand(patterns) {
|
|
6
|
+
return `npx vitest ${args(patterns)}`;
|
|
7
|
+
}
|
|
8
|
+
function args(patterns) {
|
|
9
|
+
return patterns.map((s) => JSON.stringify(s)).join(' ');
|
|
10
|
+
}
|
|
11
|
+
export function run(patterns) {
|
|
12
|
+
var _a, _b;
|
|
13
|
+
const cmd = runCommand(patterns);
|
|
14
|
+
const p = exec(cmd, { cwd: process.cwd() });
|
|
15
|
+
(_a = p.stdout) === null || _a === void 0 ? void 0 : _a.pipe(process.stdout);
|
|
16
|
+
(_b = p.stderr) === null || _b === void 0 ? void 0 : _b.pipe(process.stderr);
|
|
17
|
+
}
|
|
18
|
+
export function runWatch(patterns) {
|
|
19
|
+
var _a, _b;
|
|
20
|
+
const cmd = runWatchCommand(patterns);
|
|
21
|
+
const p = exec(cmd, { cwd: process.cwd() });
|
|
22
|
+
(_a = p.stdout) === null || _a === void 0 ? void 0 : _a.pipe(process.stdout);
|
|
23
|
+
(_b = p.stderr) === null || _b === void 0 ? void 0 : _b.pipe(process.stderr);
|
|
24
|
+
}
|
package/watch.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { watch } from 'node:fs';
|
|
2
2
|
import { compileFile, compileFiles } from './compiler.js';
|
|
3
3
|
export async function watchFiles(patterns) {
|
|
4
|
-
const fns = await compileFiles(patterns);
|
|
4
|
+
const { fns, outFns } = await compileFiles(patterns);
|
|
5
5
|
for (const file of fns) {
|
|
6
6
|
watch(file, () => {
|
|
7
7
|
compileFile(file);
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
|
+
return outFns;
|
|
10
11
|
}
|