harmonyc 0.16.4 → 0.17.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 +13 -13
- package/code_generator/VitestGenerator.d.ts +2 -1
- package/code_generator/VitestGenerator.js +11 -5
- package/code_generator/outFile.d.ts +18 -8
- package/code_generator/outFile.js +52 -28
- package/compiler/compile.js +7 -7
- package/compiler/compiler.d.ts +1 -0
- package/compiler/compiler.js +11 -6
- package/model/model.d.ts +21 -9
- package/model/model.js +58 -12
- package/optimizations/autoLabel/autoLabel.d.ts +1 -1
- package/optimizations/autoLabel/autoLabel.js +13 -9
- package/package.json +4 -2
- package/parser/parser.d.ts +1 -1
- package/parser/parser.js +3 -3
- package/vitest/index.d.ts +1 -2
- package/vitest/index.js +28 -13
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Harmony Code
|
|
2
2
|
|
|
3
|
-
A test design & BDD tool that helps you separate
|
|
3
|
+
A test design & BDD tool that helps you separate _what_ you test and _how_ you automate it. You write test cases in a simple easy-to-read format, and then automate them with Vitest.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -10,23 +10,24 @@ You need to have Node.js installed. Then you can install Harmony Code in your pr
|
|
|
10
10
|
npm install harmonyc
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Then add it to your `vitest.config.js` or `vite.config.js` file, and
|
|
13
|
+
Then add it as a plugin to your `vitest.config.js` or `vite.config.js` file, and make sure to include your `.harmony` files:
|
|
14
14
|
|
|
15
15
|
```js
|
|
16
16
|
import harmony from 'harmonyc/vitest'
|
|
17
17
|
|
|
18
18
|
export default {
|
|
19
|
-
plugins: [harmony(
|
|
19
|
+
plugins: [harmony()],
|
|
20
|
+
include: ['src/**/*.harmony'],
|
|
20
21
|
}
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
This will run .harmony files in vitest.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
## VSCode plugin
|
|
27
|
+
|
|
28
|
+
Harmony Code has a [VSCode plugin](https://marketplace.visualstudio.com/items?itemName=harmony-ac.harmony-code) that supports syntax highlighting.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
Harmony Code is compatible with Vitest's VSCode plugin, so you can run and debug tests from the editor.
|
|
30
31
|
|
|
31
32
|
## Syntax
|
|
32
33
|
|
|
@@ -77,12 +78,12 @@ becomes
|
|
|
77
78
|
|
|
78
79
|
```javascript
|
|
79
80
|
test('T1 - strings', async () => {
|
|
80
|
-
const P = new Phrases()
|
|
81
|
-
await P.When_hello_(
|
|
81
|
+
const P = new Phrases()
|
|
82
|
+
await P.When_hello_('John')
|
|
82
83
|
})
|
|
83
84
|
test('T2 - code fragment', async () => {
|
|
84
|
-
const P = new Phrases()
|
|
85
|
-
await P.When_greet__times(3)
|
|
85
|
+
const P = new Phrases()
|
|
86
|
+
await P.When_greet__times(3)
|
|
86
87
|
})
|
|
87
88
|
```
|
|
88
89
|
|
|
@@ -136,7 +137,6 @@ test('T2 - store result in variable', (context) => {
|
|
|
136
137
|
})
|
|
137
138
|
```
|
|
138
139
|
|
|
139
|
-
|
|
140
140
|
## License
|
|
141
141
|
|
|
142
142
|
MIT
|
|
@@ -3,11 +3,12 @@ import { OutFile } from './outFile.ts';
|
|
|
3
3
|
export declare class VitestGenerator implements CodeGenerator {
|
|
4
4
|
private tf;
|
|
5
5
|
private sf;
|
|
6
|
+
private _sourceFileName;
|
|
6
7
|
static error(message: string, stack: string): string;
|
|
7
8
|
framework: string;
|
|
8
9
|
phraseFns: Map<string, Phrase>;
|
|
9
10
|
currentFeatureName: string;
|
|
10
|
-
constructor(tf: OutFile, sf: OutFile);
|
|
11
|
+
constructor(tf: OutFile, sf: OutFile, _sourceFileName: string);
|
|
11
12
|
feature(feature: Feature): void;
|
|
12
13
|
testGroup(g: TestGroup): void;
|
|
13
14
|
featureVars: Map<string, string>;
|
|
@@ -7,9 +7,10 @@ export class VitestGenerator {
|
|
|
7
7
|
throw e;
|
|
8
8
|
${stack ? `/* ${stack} */` : ''}`;
|
|
9
9
|
}
|
|
10
|
-
constructor(tf, sf) {
|
|
10
|
+
constructor(tf, sf, _sourceFileName) {
|
|
11
11
|
this.tf = tf;
|
|
12
12
|
this.sf = sf;
|
|
13
|
+
this._sourceFileName = _sourceFileName;
|
|
13
14
|
this.framework = 'vitest';
|
|
14
15
|
this.phraseFns = new Map();
|
|
15
16
|
this.currentFeatureName = '';
|
|
@@ -20,6 +21,7 @@ export class VitestGenerator {
|
|
|
20
21
|
const phrasesModule = './' + basename(this.sf.name.replace(/.(js|ts)$/, ''));
|
|
21
22
|
const fn = (this.currentFeatureName = pascalCase(feature.name));
|
|
22
23
|
this.phraseFns = new Map();
|
|
24
|
+
// test file
|
|
23
25
|
if (this.framework === 'vitest') {
|
|
24
26
|
this.tf.print(`import { describe, test, expect } from "vitest";`);
|
|
25
27
|
}
|
|
@@ -33,6 +35,8 @@ export class VitestGenerator {
|
|
|
33
35
|
for (const item of feature.testGroups) {
|
|
34
36
|
item.toCode(this);
|
|
35
37
|
}
|
|
38
|
+
this.tf.print(``);
|
|
39
|
+
// phrases file
|
|
36
40
|
this.sf.print(`export default class ${pascalCase(feature.name)}Phrases {`);
|
|
37
41
|
this.sf.indent(() => {
|
|
38
42
|
for (const ph of this.phraseFns.keys()) {
|
|
@@ -48,7 +52,7 @@ export class VitestGenerator {
|
|
|
48
52
|
this.sf.print(`};`);
|
|
49
53
|
}
|
|
50
54
|
testGroup(g) {
|
|
51
|
-
this.tf.print(`describe(${str(g.
|
|
55
|
+
this.tf.print(`describe(${str(g.label.text)}, () => {`, g.label.start, g.label.text);
|
|
52
56
|
this.tf.indent(() => {
|
|
53
57
|
for (const item of g.items) {
|
|
54
58
|
item.toCode(this);
|
|
@@ -57,11 +61,12 @@ export class VitestGenerator {
|
|
|
57
61
|
this.tf.print('});');
|
|
58
62
|
}
|
|
59
63
|
test(t) {
|
|
64
|
+
var _a;
|
|
60
65
|
this.resultCount = 0;
|
|
61
66
|
this.featureVars = new Map();
|
|
62
67
|
// avoid shadowing this import name
|
|
63
68
|
this.featureVars.set(new Object(), this.currentFeatureName);
|
|
64
|
-
this.tf.print(`test(${str(t.name)}, async (context) => {
|
|
69
|
+
this.tf.print(`test(${str(t.name)}, async (context) => {`, (_a = t.lastStrain) === null || _a === void 0 ? void 0 : _a.start, t.testNumber);
|
|
65
70
|
this.tf.indent(() => {
|
|
66
71
|
this.tf.print(`context.task.meta.phrases ??= [];`);
|
|
67
72
|
for (const step of t.steps) {
|
|
@@ -128,11 +133,12 @@ export class VitestGenerator {
|
|
|
128
133
|
if (p instanceof Response && p.parts.length === 1 && p.saveToVariable) {
|
|
129
134
|
return this.saveToVariable(p.saveToVariable);
|
|
130
135
|
}
|
|
131
|
-
|
|
136
|
+
const name = p.toSingleLineString();
|
|
137
|
+
this.tf.print(`(context.task.meta.phrases.push(${str(p.toString())}),`, p.start, name);
|
|
132
138
|
if (p instanceof Response && p.saveToVariable) {
|
|
133
139
|
this.saveToVariable(p.saveToVariable, '');
|
|
134
140
|
}
|
|
135
|
-
this.tf.print(`await ${f}.${functionName(p)}(${args.join(', ')}))
|
|
141
|
+
this.tf.print(`await ${f}.${functionName(p)}(${args.join(', ')}));`, p.start, name);
|
|
136
142
|
}
|
|
137
143
|
setVariable(action) {
|
|
138
144
|
this.tf.print(`(context.task.meta.variables ??= {})[${str(action.variableName)}] = ${action.value.toCode(this)};`);
|
|
@@ -1,17 +1,27 @@
|
|
|
1
|
+
import { SourceNode } from 'source-map-js';
|
|
1
2
|
import { Location } from '../model/model.ts';
|
|
2
|
-
import { SourceMapGenerator } from 'source-map-js';
|
|
3
3
|
export declare class OutFile {
|
|
4
4
|
name: string;
|
|
5
|
+
sourceFile: string;
|
|
5
6
|
lines: string[];
|
|
6
7
|
level: number;
|
|
7
|
-
sm:
|
|
8
|
+
sm: SourceNode;
|
|
8
9
|
indentSpaces: number;
|
|
9
|
-
|
|
10
|
-
constructor(name: string);
|
|
10
|
+
constructor(name: string, sourceFile: string);
|
|
11
11
|
indent(fn: () => void): void;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
clear(): void;
|
|
13
|
+
append(s: string): void;
|
|
14
|
+
print(line: string, start?: Location, name?: string, end?: Location): this;
|
|
15
|
+
loc(location: Location | undefined, name?: string): this;
|
|
16
|
+
get sourceMap(): import("source-map-js").RawSourceMap;
|
|
17
|
+
get valueWithoutSourceMap(): string;
|
|
16
18
|
get value(): string;
|
|
19
|
+
get currentLineEnd(): {
|
|
20
|
+
line: number;
|
|
21
|
+
column: number;
|
|
22
|
+
};
|
|
23
|
+
get currentLineStart(): {
|
|
24
|
+
line: number;
|
|
25
|
+
column: number;
|
|
26
|
+
};
|
|
17
27
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SourceNode } from 'source-map-js';
|
|
2
2
|
export class OutFile {
|
|
3
|
-
constructor(name) {
|
|
3
|
+
constructor(name, sourceFile) {
|
|
4
4
|
this.name = name;
|
|
5
|
+
this.sourceFile = sourceFile;
|
|
5
6
|
this.lines = [];
|
|
6
7
|
this.level = 0;
|
|
7
|
-
this.sm = new SourceMapGenerator();
|
|
8
8
|
this.indentSpaces = 2;
|
|
9
|
+
this.sm = new SourceNode(0, 0, sourceFile);
|
|
9
10
|
}
|
|
10
11
|
indent(fn) {
|
|
11
12
|
this.level++;
|
|
@@ -16,36 +17,59 @@ export class OutFile {
|
|
|
16
17
|
this.level--;
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
clear() {
|
|
21
|
+
this.sm = new SourceNode(0, 0, this.sourceFile);
|
|
22
|
+
}
|
|
23
|
+
append(s) {
|
|
24
|
+
if (this.lines.length === 0)
|
|
25
|
+
this.lines.push('');
|
|
26
|
+
this.lines[this.lines.length - 1] += s;
|
|
27
|
+
}
|
|
28
|
+
print(line, start, name, end) {
|
|
29
|
+
const chunk = ' '.repeat(this.level * this.indentSpaces) + line + '\n';
|
|
30
|
+
this.lines.push(chunk);
|
|
31
|
+
if (start) {
|
|
32
|
+
this.sm.add(new SourceNode(start.line, start.column, this.sourceFile, chunk, name));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.sm.add(new SourceNode(null, null, null, chunk));
|
|
36
|
+
}
|
|
37
|
+
if (end) {
|
|
38
|
+
this.sm.add(new SourceNode(end.line, end.column, this.sourceFile));
|
|
39
|
+
}
|
|
36
40
|
return this;
|
|
37
41
|
}
|
|
38
|
-
loc(
|
|
39
|
-
|
|
42
|
+
loc(location, name) {
|
|
43
|
+
if (!location)
|
|
44
|
+
return this;
|
|
40
45
|
return this;
|
|
41
46
|
}
|
|
47
|
+
get sourceMap() {
|
|
48
|
+
return this.sm.toStringWithSourceMap({ file: this.name }).map.toJSON();
|
|
49
|
+
}
|
|
50
|
+
get valueWithoutSourceMap() {
|
|
51
|
+
return this.sm.toString();
|
|
52
|
+
}
|
|
42
53
|
get value() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
54
|
+
const { code, map } = this.sm.toStringWithSourceMap({ file: this.name });
|
|
55
|
+
let res = code;
|
|
56
|
+
res +=
|
|
57
|
+
`\n//# sour` + // not for this file ;)
|
|
58
|
+
`ceMappingURL=data:application/json,${encodeURIComponent(map.toString())}`;
|
|
49
59
|
return res;
|
|
50
60
|
}
|
|
61
|
+
get currentLineEnd() {
|
|
62
|
+
var _a, _b;
|
|
63
|
+
return {
|
|
64
|
+
line: this.lines.length,
|
|
65
|
+
column: (_b = (_a = this.lines.at(-1)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
get currentLineStart() {
|
|
69
|
+
var _a, _b, _c;
|
|
70
|
+
return {
|
|
71
|
+
line: this.lines.length + 1,
|
|
72
|
+
column: (_c = (_b = (_a = this.lines.at(-1)) === null || _a === void 0 ? void 0 : _a.match(/^\s*/)) === null || _b === void 0 ? void 0 : _b[0].length) !== null && _c !== void 0 ? _c : 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
51
75
|
}
|
package/compiler/compile.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
1
2
|
import { VitestGenerator } from "../code_generator/VitestGenerator.js";
|
|
2
3
|
import { OutFile } from "../code_generator/outFile.js";
|
|
3
|
-
import { parse } from "../parser/parser.js";
|
|
4
4
|
import { base, phrasesFileName, testFileName } from "../filenames/filenames.js";
|
|
5
5
|
import { Feature } from "../model/model.js";
|
|
6
|
-
import { basename } from 'node:path';
|
|
7
6
|
import { autoLabel } from "../optimizations/autoLabel/autoLabel.js";
|
|
7
|
+
import { parse } from "../parser/parser.js";
|
|
8
8
|
export function compileFeature(fileName, src) {
|
|
9
9
|
const feature = new Feature(basename(base(fileName)));
|
|
10
10
|
try {
|
|
@@ -13,20 +13,20 @@ export function compileFeature(fileName, src) {
|
|
|
13
13
|
catch (e) {
|
|
14
14
|
if (e.pos && e.errorMessage) {
|
|
15
15
|
e.message =
|
|
16
|
-
e.stack = `Error in ${fileName}:${e.pos.rowBegin}:${e.pos.columnBegin}
|
|
16
|
+
e.stack = `Error in ${fileName}:${e.pos.rowBegin}:${e.pos.columnBegin}: ${e.errorMessage}`;
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
|
-
e.stack = `Error in ${fileName}
|
|
19
|
+
e.stack = `Error in ${fileName}: ${e.stack}`;
|
|
20
20
|
}
|
|
21
21
|
throw e;
|
|
22
22
|
}
|
|
23
23
|
feature.root.setFeature(feature);
|
|
24
24
|
autoLabel(feature.root);
|
|
25
25
|
const testFn = testFileName(fileName);
|
|
26
|
-
const testFile = new OutFile(testFn);
|
|
26
|
+
const testFile = new OutFile(testFn, fileName);
|
|
27
27
|
const phrasesFn = phrasesFileName(fileName);
|
|
28
|
-
const phrasesFile = new OutFile(phrasesFn);
|
|
29
|
-
const cg = new VitestGenerator(testFile, phrasesFile);
|
|
28
|
+
const phrasesFile = new OutFile(phrasesFn, fileName);
|
|
29
|
+
const cg = new VitestGenerator(testFile, phrasesFile, fileName);
|
|
30
30
|
feature.toCode(cg);
|
|
31
31
|
return { outFile: testFile, phrasesFile };
|
|
32
32
|
}
|
package/compiler/compiler.d.ts
CHANGED
package/compiler/compiler.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import glob from 'fast-glob';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
|
-
import { compileFeature } from "./compile.js";
|
|
5
|
-
import { testFileName } from "../filenames/filenames.js";
|
|
6
4
|
import { VitestGenerator } from "../code_generator/VitestGenerator.js";
|
|
5
|
+
import { testFileName } from "../filenames/filenames.js";
|
|
6
|
+
import { compileFeature } from "./compile.js";
|
|
7
7
|
export async function compileFiles(pattern) {
|
|
8
8
|
var _a;
|
|
9
9
|
const fns = await glob(pattern);
|
|
@@ -26,10 +26,7 @@ export async function compileFiles(pattern) {
|
|
|
26
26
|
export async function compileFile(fn) {
|
|
27
27
|
var _a;
|
|
28
28
|
fn = resolve(fn);
|
|
29
|
-
const src = readFileSync(fn, 'utf8')
|
|
30
|
-
.toString()
|
|
31
|
-
.replace(/\r\n/g, '\n')
|
|
32
|
-
.replace(/\r/g, '\n');
|
|
29
|
+
const src = preprocess(readFileSync(fn, 'utf8').toString());
|
|
33
30
|
try {
|
|
34
31
|
const { outFile, phrasesFile } = compileFeature(fn, src);
|
|
35
32
|
writeFileSync(outFile.name, outFile.value);
|
|
@@ -46,3 +43,11 @@ export async function compileFile(fn) {
|
|
|
46
43
|
return undefined;
|
|
47
44
|
}
|
|
48
45
|
}
|
|
46
|
+
export function preprocess(src) {
|
|
47
|
+
// strip BOM
|
|
48
|
+
if (src.charCodeAt(0) === 0xfeff) {
|
|
49
|
+
src = src.slice(1);
|
|
50
|
+
}
|
|
51
|
+
src = src.replace(/\r\n?/g, '\n');
|
|
52
|
+
return src;
|
|
53
|
+
}
|
package/model/model.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Token } from 'typescript-parsec';
|
|
1
2
|
export interface CodeGenerator {
|
|
2
3
|
feature(feature: Feature): void;
|
|
3
4
|
testGroup(group: TestGroup): void;
|
|
@@ -17,7 +18,6 @@ export interface CodeGenerator {
|
|
|
17
18
|
export interface Location {
|
|
18
19
|
line: number;
|
|
19
20
|
column: number;
|
|
20
|
-
fileName: string;
|
|
21
21
|
}
|
|
22
22
|
export declare class Feature {
|
|
23
23
|
name: string;
|
|
@@ -28,7 +28,19 @@ export declare class Feature {
|
|
|
28
28
|
get testGroups(): (TestGroup | Test)[];
|
|
29
29
|
toCode(cg: CodeGenerator): void;
|
|
30
30
|
}
|
|
31
|
-
export declare abstract class
|
|
31
|
+
export declare abstract class Node {
|
|
32
|
+
start?: {
|
|
33
|
+
line: number;
|
|
34
|
+
column: number;
|
|
35
|
+
};
|
|
36
|
+
end?: {
|
|
37
|
+
line: number;
|
|
38
|
+
column: number;
|
|
39
|
+
};
|
|
40
|
+
at([startToken, endToken]: [Token<any> | undefined, Token<any> | undefined]): this;
|
|
41
|
+
atSameAs(other: Node): this;
|
|
42
|
+
}
|
|
43
|
+
export declare abstract class Branch extends Node {
|
|
32
44
|
parent?: Branch;
|
|
33
45
|
children: Branch[];
|
|
34
46
|
isFork: boolean;
|
|
@@ -65,7 +77,7 @@ export declare class State {
|
|
|
65
77
|
text: string;
|
|
66
78
|
constructor(text?: string);
|
|
67
79
|
}
|
|
68
|
-
export declare class Label {
|
|
80
|
+
export declare class Label extends Node {
|
|
69
81
|
text: string;
|
|
70
82
|
constructor(text?: string);
|
|
71
83
|
get isEmpty(): boolean;
|
|
@@ -131,10 +143,9 @@ export declare class CodeLiteral extends Arg {
|
|
|
131
143
|
toCode(cg: CodeGenerator): string;
|
|
132
144
|
toDeclaration(cg: CodeGenerator, index: number): string;
|
|
133
145
|
}
|
|
134
|
-
export declare abstract class Phrase {
|
|
146
|
+
export declare abstract class Phrase extends Node {
|
|
135
147
|
parts: Part[];
|
|
136
148
|
feature: Feature;
|
|
137
|
-
location?: Location;
|
|
138
149
|
abstract get kind(): string;
|
|
139
150
|
constructor(parts: Part[]);
|
|
140
151
|
setFeature(feature: Feature): this;
|
|
@@ -186,10 +197,11 @@ export declare function makeTests(root: Branch): Test[];
|
|
|
186
197
|
export declare class Test {
|
|
187
198
|
branches: Branch[];
|
|
188
199
|
testNumber?: string;
|
|
189
|
-
labels:
|
|
200
|
+
labels: Label[];
|
|
190
201
|
constructor(branches: Branch[]);
|
|
191
202
|
get steps(): Step[];
|
|
192
|
-
get last(): Step;
|
|
203
|
+
get last(): Step | undefined;
|
|
204
|
+
get lastStrain(): Branch | undefined;
|
|
193
205
|
get name(): string;
|
|
194
206
|
toCode(cg: CodeGenerator): void;
|
|
195
207
|
toString(): string;
|
|
@@ -197,9 +209,9 @@ export declare class Test {
|
|
|
197
209
|
}
|
|
198
210
|
export declare function makeGroups(tests: Test[]): (Test | TestGroup)[];
|
|
199
211
|
export declare class TestGroup {
|
|
200
|
-
|
|
212
|
+
label: Label;
|
|
201
213
|
items: (Test | TestGroup)[];
|
|
202
|
-
constructor(
|
|
214
|
+
constructor(label: Label, items: (Test | TestGroup)[]);
|
|
203
215
|
toString(): string;
|
|
204
216
|
toCode(cg: CodeGenerator): void;
|
|
205
217
|
}
|
package/model/model.js
CHANGED
|
@@ -15,8 +15,38 @@ export class Feature {
|
|
|
15
15
|
cg.feature(this);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
export class
|
|
18
|
+
export class Node {
|
|
19
|
+
at([startToken, endToken]) {
|
|
20
|
+
while (startToken && startToken.kind === 'newline') {
|
|
21
|
+
startToken = startToken.next;
|
|
22
|
+
}
|
|
23
|
+
if (startToken) {
|
|
24
|
+
this.start = {
|
|
25
|
+
line: startToken.pos.rowBegin,
|
|
26
|
+
column: startToken.pos.columnBegin - 1,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (startToken && endToken) {
|
|
30
|
+
let t = startToken;
|
|
31
|
+
while (t.next && t.next !== endToken) {
|
|
32
|
+
t = t.next;
|
|
33
|
+
}
|
|
34
|
+
this.end = {
|
|
35
|
+
line: t.pos.rowEnd,
|
|
36
|
+
column: t.pos.columnEnd - 1,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
atSameAs(other) {
|
|
42
|
+
this.start = other.start;
|
|
43
|
+
this.end = other.end;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class Branch extends Node {
|
|
19
48
|
constructor(children = []) {
|
|
49
|
+
super();
|
|
20
50
|
this.isFork = false;
|
|
21
51
|
this.isEnd = false;
|
|
22
52
|
this.children = children;
|
|
@@ -127,8 +157,9 @@ export class State {
|
|
|
127
157
|
this.text = text;
|
|
128
158
|
}
|
|
129
159
|
}
|
|
130
|
-
export class Label {
|
|
160
|
+
export class Label extends Node {
|
|
131
161
|
constructor(text = '') {
|
|
162
|
+
super();
|
|
132
163
|
this.text = text;
|
|
133
164
|
}
|
|
134
165
|
get isEmpty() {
|
|
@@ -260,8 +291,9 @@ export class CodeLiteral extends Arg {
|
|
|
260
291
|
return cg.variantParamDeclaration(index);
|
|
261
292
|
}
|
|
262
293
|
}
|
|
263
|
-
export class Phrase {
|
|
294
|
+
export class Phrase extends Node {
|
|
264
295
|
constructor(parts) {
|
|
296
|
+
super();
|
|
265
297
|
this.parts = parts;
|
|
266
298
|
}
|
|
267
299
|
setFeature(feature) {
|
|
@@ -417,16 +449,28 @@ export class Test {
|
|
|
417
449
|
this.labels = this.branches
|
|
418
450
|
.filter((b) => b instanceof Section)
|
|
419
451
|
.filter((s) => !s.isEmpty)
|
|
420
|
-
.map((s) => s.label
|
|
452
|
+
.map((s) => s.label);
|
|
421
453
|
}
|
|
422
454
|
get steps() {
|
|
423
455
|
return this.branches.filter((b) => b instanceof Step);
|
|
424
456
|
}
|
|
425
457
|
get last() {
|
|
426
|
-
return this.steps
|
|
458
|
+
return this.steps.at(-1);
|
|
459
|
+
}
|
|
460
|
+
get lastStrain() {
|
|
461
|
+
// Find the last branch that has no forks after it
|
|
462
|
+
const lastForking = this.branches.length -
|
|
463
|
+
1 -
|
|
464
|
+
this.branches
|
|
465
|
+
.slice()
|
|
466
|
+
.reverse()
|
|
467
|
+
.findIndex((b) => b.successors.length > 1);
|
|
468
|
+
if (lastForking === this.branches.length)
|
|
469
|
+
return this.branches.at(0);
|
|
470
|
+
return this.branches.at(lastForking + 1);
|
|
427
471
|
}
|
|
428
472
|
get name() {
|
|
429
|
-
return `${[this.testNumber, ...this.labels].join(' - ')}`;
|
|
473
|
+
return `${[this.testNumber, ...this.labels.map((x) => x.text)].join(' - ')}`;
|
|
430
474
|
}
|
|
431
475
|
toCode(cg) {
|
|
432
476
|
cg.test(this);
|
|
@@ -445,25 +489,27 @@ export function makeGroups(tests) {
|
|
|
445
489
|
return [];
|
|
446
490
|
if (tests[0].labels.length === 0)
|
|
447
491
|
return [tests[0], ...makeGroups(tests.slice(1))];
|
|
448
|
-
const
|
|
449
|
-
let count = tests.findIndex((t) =>
|
|
492
|
+
const label = tests[0].labels[0];
|
|
493
|
+
let count = tests.findIndex((t) =>
|
|
494
|
+
// using identity instead of text equality, which means identically named labels will not be grouped together
|
|
495
|
+
t.labels[0] !== label);
|
|
450
496
|
if (count === -1)
|
|
451
497
|
count = tests.length;
|
|
452
498
|
if (count === 1)
|
|
453
499
|
return [tests[0], ...makeGroups(tests.slice(1))];
|
|
454
500
|
tests.slice(0, count).forEach((test) => test.labels.shift());
|
|
455
501
|
return [
|
|
456
|
-
new TestGroup(
|
|
502
|
+
new TestGroup(label, makeGroups(tests.slice(0, count))),
|
|
457
503
|
...makeGroups(tests.slice(count)),
|
|
458
504
|
];
|
|
459
505
|
}
|
|
460
506
|
export class TestGroup {
|
|
461
|
-
constructor(
|
|
462
|
-
this.
|
|
507
|
+
constructor(label, items) {
|
|
508
|
+
this.label = label;
|
|
463
509
|
this.items = items;
|
|
464
510
|
}
|
|
465
511
|
toString() {
|
|
466
|
-
return `+ ${this.
|
|
512
|
+
return `+ ${this.label.text}:` + indent(this.items.join('\n'));
|
|
467
513
|
}
|
|
468
514
|
toCode(cg) {
|
|
469
515
|
cg.testGroup(this);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { Branch } from '../../model/model.ts';
|
|
2
|
-
export declare function autoLabel(
|
|
2
|
+
export declare function autoLabel(b: Branch): void;
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { Label, Section, Step } from "../../model/model.js";
|
|
2
|
-
export function autoLabel(
|
|
3
|
-
const forks =
|
|
2
|
+
export function autoLabel(b) {
|
|
3
|
+
const forks = b.children.filter((c, i) => c.isFork || i === 0);
|
|
4
4
|
if (forks.length > 1) {
|
|
5
5
|
forks
|
|
6
|
-
.filter((
|
|
7
|
-
.forEach((
|
|
8
|
-
const label =
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
autoSection.
|
|
6
|
+
.filter((child) => child instanceof Step)
|
|
7
|
+
.forEach((step) => {
|
|
8
|
+
const label = step.action.toSingleLineString();
|
|
9
|
+
const autoLabel = new Label(label);
|
|
10
|
+
autoLabel.atSameAs(step.action);
|
|
11
|
+
const autoSection = new Section(autoLabel, [], step.isFork);
|
|
12
|
+
// todo this is some redundancy with both section and label storing the position
|
|
13
|
+
autoSection.atSameAs(step);
|
|
14
|
+
step.replaceWith(autoSection);
|
|
15
|
+
autoSection.addChild(step);
|
|
12
16
|
});
|
|
13
17
|
}
|
|
14
|
-
|
|
18
|
+
b.children.forEach((c) => autoLabel(c));
|
|
15
19
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harmonyc",
|
|
3
3
|
"description": "Harmony Code - model-driven BDD for Vitest",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.0",
|
|
5
5
|
"author": "Bernát Kalló",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"fast-glob": "^3.3.2",
|
|
26
26
|
"tinyrainbow": "1",
|
|
27
|
-
"typescript-parsec": "0.3.4"
|
|
27
|
+
"typescript-parsec": "0.3.4"
|
|
28
|
+
},
|
|
29
|
+
"optionalDependencies": {
|
|
28
30
|
"watcher": "^2.3.1"
|
|
29
31
|
},
|
|
30
32
|
"license": "MIT",
|
package/parser/parser.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Parser, ParserOutput, Token } from 'typescript-parsec';
|
|
2
|
+
import { Action, CodeLiteral, Docstring, ErrorResponse, Label, Response, Section, SetVariable, Step, StringLiteral, Switch, Word } from '../model/model.ts';
|
|
2
3
|
import { T } from './lexer.ts';
|
|
3
|
-
import { Action, Response, CodeLiteral, StringLiteral, Section, Step, Docstring, Word, Label, ErrorResponse, SetVariable, Switch } from '../model/model.ts';
|
|
4
4
|
export declare function parse(input: string): Section;
|
|
5
5
|
export declare function parse<T>(input: string, production: Parser<any, T>): T;
|
|
6
6
|
export declare const NEWLINES: Parser<T, Token<T>[]>, WORDS: Parser<T, Word>, DOUBLE_QUOTE_STRING: Parser<T, StringLiteral>, BACKTICK_STRING: Parser<T, CodeLiteral>, DOCSTRING: Parser<T, Docstring>, ERROR_MARK: Parser<T, Token<T>>, VARIABLE: Parser<T, string>, SIMPLE_PART: Parser<T, Word | StringLiteral | Docstring | CodeLiteral>, SWITCH: Parser<T, Switch>, BRACES: Parser<T, Switch>, PART: Parser<T, Word | Switch | StringLiteral | Docstring | CodeLiteral>, PHRASE: Parser<T, (Word | Switch | StringLiteral | Docstring | CodeLiteral)[]>, ARG: Parser<T, StringLiteral | Docstring | CodeLiteral>, SET_VARIABLE: Parser<T, SetVariable>, ACTION: Parser<T, Action | SetVariable>, RESPONSE: Parser<T, Response>, ERROR_RESPONSE: Parser<T, ErrorResponse>, SAVE_TO_VARIABLE: Parser<T, Response>, ARROW: Parser<T, Token<T>>, RESPONSE_ITEM: Parser<T, Response | ErrorResponse>, STEP: Parser<T, Step>, LABEL: Parser<T, Label>, SECTION: Parser<T, Section>, BRANCH: Parser<T, Section | Step>, // section first, to make sure there is no colon after step
|
package/parser/parser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { alt_sc, apply, expectEOF, expectSingleResult,
|
|
1
|
+
import { alt_sc, apply, expectEOF, expectSingleResult, fail, kleft, kmid, kright, list_sc, nil, opt_sc, rep_sc, seq, tok, unableToConsumeToken, } from 'typescript-parsec';
|
|
2
|
+
import { Action, CodeLiteral, Docstring, ErrorResponse, Label, Response, SaveToVariable, Section, SetVariable, Step, StringLiteral, Switch, Word, } from "../model/model.js";
|
|
2
3
|
import { T, lexer } from "./lexer.js";
|
|
3
|
-
import { Action, Response, CodeLiteral, StringLiteral, Section, Step, Docstring, Word, Label, ErrorResponse, SaveToVariable, SetVariable, Switch, } from "../model/model.js";
|
|
4
4
|
export function parse(input, production = TEST_DESIGN) {
|
|
5
5
|
const tokens = lexer.parse(input);
|
|
6
6
|
return expectSingleResult(expectEOF(production.parse(tokens)));
|
|
@@ -33,7 +33,7 @@ export const NEWLINES = list_sc(tok(T.Newline), nil()), WORDS = apply(tok(T.Word
|
|
|
33
33
|
//),
|
|
34
34
|
SWITCH = apply(list_sc(SIMPLE_PART, tok(T.Slash)), (cs) => new Switch(cs)),
|
|
35
35
|
//ROUTER = apply(list_sc(REPEATER, tok(T.Semicolon)), (cs) => new Router(cs)),
|
|
36
|
-
BRACES = kmid(tok(T.OpeningBrace), SWITCH, tok(T.ClosingBrace)), PART = alt_sc(SIMPLE_PART, BRACES), PHRASE = rep_sc(PART), ARG = alt_sc(DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), SET_VARIABLE = apply(seq(VARIABLE, ARG), ([variable, value]) => new SetVariable(variable, value)), ACTION = alt_sc(SET_VARIABLE, apply(PHRASE, (parts) => new Action(parts))), RESPONSE = apply(seq(PHRASE, opt_sc(VARIABLE)), ([parts, variable]) => new Response(parts, variable !== undefined ? new SaveToVariable(variable) : undefined)), ERROR_RESPONSE = apply(seq(ERROR_MARK, opt_sc(alt_sc(DOUBLE_QUOTE_STRING, DOCSTRING))), ([, parts]) => new ErrorResponse(parts)), SAVE_TO_VARIABLE = apply(VARIABLE, (variable) => new Response([], new SaveToVariable(variable))), ARROW = kright(opt_sc(NEWLINES), tok(T.ResponseArrow)), RESPONSE_ITEM = kright(ARROW, alt_sc(SAVE_TO_VARIABLE, ERROR_RESPONSE, RESPONSE)), STEP = apply(seq(ACTION, rep_sc(RESPONSE_ITEM)), ([action, responses]) => new Step(action, responses).setFork(true)), LABEL = apply(kleft(list_sc(PART, nil()), tok(T.Colon)), (words) => new Label(words.map((w) => w.toString()).join(' '))), SECTION = apply(LABEL, (text) => new Section(text)), BRANCH = alt_sc(SECTION, STEP), // section first, to make sure there is no colon after step
|
|
36
|
+
BRACES = kmid(tok(T.OpeningBrace), SWITCH, tok(T.ClosingBrace)), PART = alt_sc(SIMPLE_PART, BRACES), PHRASE = rep_sc(PART), ARG = alt_sc(DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), SET_VARIABLE = apply(seq(VARIABLE, ARG), ([variable, value]) => new SetVariable(variable, value)), ACTION = alt_sc(SET_VARIABLE, apply(PHRASE, (parts, range) => new Action(parts).at(range))), RESPONSE = apply(seq(PHRASE, opt_sc(VARIABLE)), ([parts, variable], range) => new Response(parts, variable !== undefined ? new SaveToVariable(variable) : undefined).at(range)), ERROR_RESPONSE = apply(seq(ERROR_MARK, opt_sc(alt_sc(DOUBLE_QUOTE_STRING, DOCSTRING))), ([, parts]) => new ErrorResponse(parts)), SAVE_TO_VARIABLE = apply(VARIABLE, (variable) => new Response([], new SaveToVariable(variable))), ARROW = kright(opt_sc(NEWLINES), tok(T.ResponseArrow)), RESPONSE_ITEM = kright(ARROW, alt_sc(SAVE_TO_VARIABLE, ERROR_RESPONSE, RESPONSE)), STEP = apply(seq(ACTION, rep_sc(RESPONSE_ITEM)), ([action, responses]) => new Step(action, responses).setFork(true)), LABEL = apply(kleft(list_sc(PART, nil()), tok(T.Colon)), (words, range) => new Label(words.map((w) => w.toString()).join(' ')).at(range)), SECTION = apply(LABEL, (text) => new Section(text)), BRANCH = apply(alt_sc(SECTION, STEP), (branch, range) => branch.at(range)), // section first, to make sure there is no colon after step
|
|
37
37
|
DENTS = apply(alt_sc(tok(T.Plus), tok(T.Minus)), (seqOrFork) => {
|
|
38
38
|
return {
|
|
39
39
|
dent: (seqOrFork.text.length - 2) / 2,
|
package/vitest/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
export interface HarmonyPluginOptions {
|
|
3
|
-
watchDir: string;
|
|
4
3
|
}
|
|
5
|
-
export default function harmonyPlugin({
|
|
4
|
+
export default function harmonyPlugin({}?: HarmonyPluginOptions): Plugin;
|
|
6
5
|
declare module 'vitest' {
|
|
7
6
|
interface TaskMeta {
|
|
8
7
|
phrases?: string[];
|
package/vitest/index.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import { watchFiles } from "../cli/watch.js";
|
|
2
1
|
import c from 'tinyrainbow';
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { compileFeature } from "../compiler/compile.js";
|
|
3
|
+
import { preprocess } from "../compiler/compiler.js";
|
|
4
|
+
export default function harmonyPlugin({} = {}) {
|
|
5
5
|
return {
|
|
6
6
|
name: 'harmony',
|
|
7
|
+
resolveId(id) {
|
|
8
|
+
if (id.endsWith('.harmony')) {
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
transform(code, id, options) {
|
|
13
|
+
if (!id.endsWith('.harmony'))
|
|
14
|
+
return null;
|
|
15
|
+
code = preprocess(code);
|
|
16
|
+
const { outFile } = compileFeature(id, code);
|
|
17
|
+
return {
|
|
18
|
+
code: outFile.valueWithoutSourceMap,
|
|
19
|
+
map: outFile.sourceMap,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
7
22
|
config(config) {
|
|
8
23
|
var _a, _b;
|
|
9
24
|
var _c;
|
|
@@ -14,16 +29,16 @@ export default function harmonyPlugin({ watchDir, }) {
|
|
|
14
29
|
}
|
|
15
30
|
config.test.reporters.splice(0, 0, new HarmonyReporter());
|
|
16
31
|
},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
32
|
+
// This has been removed in favor of using transform, so no need to generate an actual file
|
|
33
|
+
// async configureServer(server) {
|
|
34
|
+
// const isWatchMode = server.config.server.watch !== null
|
|
35
|
+
// const patterns = [`${watchDir}/**/*.harmony`]
|
|
36
|
+
// if (isWatchMode) {
|
|
37
|
+
// await watchFiles(patterns)
|
|
38
|
+
// } else {
|
|
39
|
+
// await compileFiles(patterns)
|
|
40
|
+
// }
|
|
41
|
+
// },
|
|
27
42
|
};
|
|
28
43
|
}
|
|
29
44
|
class HarmonyReporter {
|