harmonyc 0.16.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Harmony Code
2
2
 
3
- A test design & BDD tool that helps you separate the _what_ to test from the _how_ to automate it. You write test cases in a simple easy-to-read format, and then automate them with Vitest (and soon with other frameworks and languages).
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 specify which folder to watch for `.harmony` files:
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({ watchDir: 'src' })],
19
+ plugins: [harmony()],
20
+ include: ['src/**/*.harmony'],
20
21
  }
21
22
  ```
22
23
 
23
- You can run it manually for all `.harmony` files in your `src` folder:
24
+ This will run .harmony files in vitest.
24
25
 
25
- ```bash
26
- harmonyc src/**/*.harmony
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
- This will generate `.test.mjs` files next to the `.harmony` files, and generate empty definition files for you.
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_("John");
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
 
@@ -95,6 +96,14 @@ They are not included in the test case, but the test case name is generated from
95
96
 
96
97
  Lines starting with `#` or `//` are comments and are ignored.
97
98
 
99
+ ### Switches
100
+
101
+ You can generate multiple test cases by adding a `{ A / B / C }` syntax into action(s) and possibly response(s).
102
+
103
+ ```harmony
104
+ + password is { "A" / "asdf" / "password123" } => !! "password is too weak"
105
+ ```
106
+
98
107
  ### Error matching
99
108
 
100
109
  You can use `!!` to denote an error response. This will verify that the action throws an error. You can specify the error message after the `!!`.
@@ -128,8 +137,6 @@ test('T2 - store result in variable', (context) => {
128
137
  })
129
138
  ```
130
139
 
131
- ## Running the tests
132
-
133
140
  ## License
134
141
 
135
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>;
@@ -2,14 +2,15 @@ import { basename } from 'path';
2
2
  import { Arg, Response, Word, } from "../model/model.js";
3
3
  export class VitestGenerator {
4
4
  static error(message, stack) {
5
- return `const e = new SyntaxError(${str(message)});
6
- e.stack = undefined;
7
- throw e;
5
+ return `const e = new SyntaxError(${str(message)});
6
+ e.stack = undefined;
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.name)}, () => {`);
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) {
@@ -72,10 +77,10 @@ export class VitestGenerator {
72
77
  }
73
78
  errorStep(action, errorResponse) {
74
79
  this.declareFeatureVariables([action]);
75
- this.tf.print(`context.task.meta.phrases.push(${str(errorResponse.toSingleLineString())});`);
76
80
  this.tf.print(`await expect(async () => {`);
77
81
  this.tf.indent(() => {
78
82
  action.toCode(this);
83
+ this.tf.print(`context.task.meta.phrases.push(${str(errorResponse.toSingleLineString())});`);
79
84
  });
80
85
  this.tf.print(`}).rejects.toThrow(${(errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.message) !== undefined
81
86
  ? str(errorResponse.message.text)
@@ -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
- this.tf.print(`(context.task.meta.phrases.push(${str(p.toString())}),`);
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: SourceMapGenerator;
8
+ sm: SourceNode;
8
9
  indentSpaces: number;
9
- private currentLoc;
10
- constructor(name: string);
10
+ constructor(name: string, sourceFile: string);
11
11
  indent(fn: () => void): void;
12
- print(...lines: string[]): this;
13
- loc({ location }: {
14
- location?: Location;
15
- }): this;
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 { SourceMapGenerator } from 'source-map-js';
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
- print(...lines) {
20
- const l = this.lines.length;
21
- this.lines.push(...lines.map((line) => ' '.repeat(this.level * this.indentSpaces) + line));
22
- if (this.currentLoc)
23
- for (const i of lines.keys()) {
24
- this.sm.addMapping({
25
- source: this.currentLoc.fileName,
26
- original: {
27
- line: this.currentLoc.line,
28
- column: this.currentLoc.column,
29
- },
30
- generated: {
31
- line: l + i,
32
- column: this.level * this.indentSpaces,
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({ location }) {
39
- this.currentLoc = location;
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
- let res = this.lines.join('\n');
44
- if (this.currentLoc) {
45
- res +=
46
- `\n\n//# sour` + // not for this file ;)
47
- `ceMappingURL=data:application/json,${encodeURIComponent(this.sm.toString())}`;
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
  }
@@ -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}\n${e.errorMessage}`;
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}\n${e.stack}`;
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
  }
@@ -7,3 +7,4 @@ export declare function compileFile(fn: string): Promise<{
7
7
  outFile: import("../code_generator/outFile.ts").OutFile;
8
8
  phrasesFile: import("../code_generator/outFile.ts").OutFile;
9
9
  } | undefined>;
10
+ export declare function preprocess(src: string): string;
@@ -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 Branch {
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: string[];
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
- name: string;
212
+ label: Label;
201
213
  items: (Test | TestGroup)[];
202
- constructor(name: string, items: (Test | TestGroup)[]);
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 Branch {
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.text);
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[this.steps.length - 1];
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 name = tests[0].labels[0];
449
- let count = tests.findIndex((t) => t.labels[0] !== name);
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(name, makeGroups(tests.slice(0, count))),
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(name, items) {
462
- this.name = name;
507
+ constructor(label, items) {
508
+ this.label = label;
463
509
  this.items = items;
464
510
  }
465
511
  toString() {
466
- return `+ ${this.name}:` + indent(this.items.join('\n'));
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(s: Branch): void;
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(s) {
3
- const forks = s.children.filter((c, i) => c.isFork || i === 0);
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((c) => c instanceof Step)
7
- .forEach((c) => {
8
- const label = c.action.toSingleLineString();
9
- const autoSection = new Section(new Label(label), [], c.isFork);
10
- c.replaceWith(autoSection);
11
- autoSection.addChild(c);
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
- s.children.forEach((c) => autoLabel(c));
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.16.3",
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",
@@ -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, kright, opt_sc, rep_sc, seq, tok, list_sc, kleft, kmid, fail, nil, unableToConsumeToken, } from 'typescript-parsec';
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({ watchDir, }: HarmonyPluginOptions): Plugin;
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 { compileFiles } from "../compiler/compiler.js";
4
- export default function harmonyPlugin({ watchDir, }) {
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
- async configureServer(server) {
18
- const isWatchMode = server.config.server.watch !== null;
19
- const patterns = [`${watchDir}/**/*.harmony`];
20
- if (isWatchMode) {
21
- await watchFiles(patterns);
22
- }
23
- else {
24
- await compileFiles(patterns);
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 {