tstyche 4.0.0-beta.5 → 4.0.0-beta.7

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/build/tstyche.js CHANGED
@@ -1672,14 +1672,12 @@ class ResultCount {
1672
1672
  class Result {
1673
1673
  expectCount = new ResultCount();
1674
1674
  fileCount = new ResultCount();
1675
- resolvedConfig;
1676
1675
  results = [];
1677
1676
  targetCount = new ResultCount();
1678
1677
  tasks;
1679
1678
  testCount = new ResultCount();
1680
1679
  timing = new ResultTiming();
1681
- constructor(resolvedConfig, tasks) {
1682
- this.resolvedConfig = resolvedConfig;
1680
+ constructor(tasks) {
1683
1681
  this.tasks = tasks;
1684
1682
  }
1685
1683
  }
@@ -1781,6 +1779,7 @@ class ResultHandler {
1781
1779
  this.#taskResult.timing.start = Date.now();
1782
1780
  break;
1783
1781
  case "task:error":
1782
+ case "collect:error":
1784
1783
  this.#targetResult.status = ResultStatus.Failed;
1785
1784
  this.#taskResult.status = ResultStatus.Failed;
1786
1785
  this.#taskResult.diagnostics.push(...payload.diagnostics);
@@ -1915,488 +1914,90 @@ class ResultHandler {
1915
1914
  }
1916
1915
  }
1917
1916
 
1918
- var TestTreeNodeBrand;
1919
- (function (TestTreeNodeBrand) {
1920
- TestTreeNodeBrand["Describe"] = "describe";
1921
- TestTreeNodeBrand["Test"] = "test";
1922
- TestTreeNodeBrand["Expect"] = "expect";
1923
- })(TestTreeNodeBrand || (TestTreeNodeBrand = {}));
1917
+ function jsx(type, props) {
1918
+ return { props, type };
1919
+ }
1924
1920
 
1925
- class TestTreeNode {
1926
- brand;
1927
- children = [];
1928
- #compiler;
1929
- diagnostics = new Set();
1930
- flags;
1931
- name = "";
1932
- node;
1933
- parent;
1934
- constructor(compiler, brand, node, parent, flags) {
1935
- this.brand = brand;
1936
- this.#compiler = compiler;
1937
- this.node = node;
1938
- this.parent = parent;
1939
- this.flags = flags;
1940
- if (node.arguments[0] != null && compiler.isStringLiteralLike(node.arguments[0])) {
1941
- this.name = node.arguments[0].text;
1942
- }
1943
- if (node.arguments[1] != null && compiler.isFunctionLike(node.arguments[1])) {
1944
- for (const diagnostic of parent.diagnostics) {
1945
- if (diagnosticBelongsToNode(diagnostic, node.arguments[1].body)) {
1946
- this.diagnostics.add(diagnostic);
1947
- parent.diagnostics.delete(diagnostic);
1948
- }
1949
- }
1950
- }
1951
- }
1952
- validate() {
1953
- const diagnostics = [];
1954
- const getText = (node) => `'${node.expression.getText()}()' cannot be nested within '${this.node.expression.getText()}()'.`;
1955
- const getParentCallExpression = (node) => {
1956
- while (!this.#compiler.isCallExpression(node.parent)) {
1957
- node = node.parent;
1958
- }
1959
- return node.parent;
1960
- };
1961
- switch (this.brand) {
1962
- case TestTreeNodeBrand.Describe:
1963
- for (const child of this.children) {
1964
- if (child.brand === TestTreeNodeBrand.Expect) {
1965
- diagnostics.push(Diagnostic.error(getText(child.node), DiagnosticOrigin.fromNode(getParentCallExpression(child.node))));
1966
- }
1967
- }
1968
- break;
1969
- case TestTreeNodeBrand.Test:
1970
- case TestTreeNodeBrand.Expect:
1971
- for (const child of this.children) {
1972
- if (child.brand !== TestTreeNodeBrand.Expect) {
1973
- diagnostics.push(Diagnostic.error(getText(child.node), DiagnosticOrigin.fromNode(child.node)));
1974
- }
1975
- }
1976
- break;
1977
- }
1978
- return diagnostics;
1921
+ var Color;
1922
+ (function (Color) {
1923
+ Color["Reset"] = "0";
1924
+ Color["Red"] = "31";
1925
+ Color["Green"] = "32";
1926
+ Color["Yellow"] = "33";
1927
+ Color["Blue"] = "34";
1928
+ Color["Magenta"] = "35";
1929
+ Color["Cyan"] = "36";
1930
+ Color["Gray"] = "90";
1931
+ })(Color || (Color = {}));
1932
+
1933
+ function Text({ children, color, indent }) {
1934
+ const ansiEscapes = [];
1935
+ if (color != null) {
1936
+ ansiEscapes.push(color);
1979
1937
  }
1938
+ return (jsx("text", { indent: indent ?? 0, children: [ansiEscapes.length > 0 ? jsx("ansi", { escapes: ansiEscapes }) : undefined, children, ansiEscapes.length > 0 ? jsx("ansi", { escapes: Color.Reset }) : undefined] }));
1980
1939
  }
1981
1940
 
1982
- class AssertionNode extends TestTreeNode {
1983
- abilityDiagnostics;
1984
- isNot;
1985
- matcherNode;
1986
- matcherNameNode;
1987
- modifierNode;
1988
- notNode;
1989
- source;
1990
- target;
1991
- constructor(compiler, brand, node, parent, flags, matcherNode, matcherNameNode, modifierNode, notNode) {
1992
- super(compiler, brand, node, parent, flags);
1993
- this.isNot = notNode != null;
1994
- this.matcherNode = matcherNode;
1995
- this.matcherNameNode = matcherNameNode;
1996
- this.modifierNode = modifierNode;
1997
- this.source = this.node.typeArguments ?? this.node.arguments;
1998
- if (compiler.isCallExpression(this.matcherNode)) {
1999
- this.target = this.matcherNode.typeArguments ?? this.matcherNode.arguments;
2000
- }
2001
- for (const diagnostic of parent.diagnostics) {
2002
- if (diagnosticBelongsToNode(diagnostic, this.source)) {
2003
- this.diagnostics.add(diagnostic);
2004
- parent.diagnostics.delete(diagnostic);
2005
- }
2006
- }
2007
- }
1941
+ function Line({ children, color, indent }) {
1942
+ return (jsx(Text, { color: color, indent: indent, children: [children, jsx("newLine", {})] }));
2008
1943
  }
2009
1944
 
2010
- class AbilityLayer {
2011
- #filePath = "";
2012
- #nodes = [];
2013
- #projectService;
2014
- #resolvedConfig;
2015
- #text = "";
2016
- constructor(projectService, resolvedConfig) {
2017
- this.#projectService = projectService;
2018
- this.#resolvedConfig = resolvedConfig;
1945
+ class Scribbler {
1946
+ #indentStep = " ";
1947
+ #newLine;
1948
+ #noColor;
1949
+ #notEmptyLineRegex = /^(?!$)/gm;
1950
+ constructor(options) {
1951
+ this.#newLine = options?.newLine ?? "\n";
1952
+ this.#noColor = options?.noColor ?? environmentOptions.noColor;
2019
1953
  }
2020
- #getErasedRangeText(range) {
2021
- if (this.#text.indexOf("\n", range.start) >= range.end) {
2022
- return " ".repeat(range.end - range.start);
2023
- }
2024
- const text = [];
2025
- for (let index = range.start; index < range.end; index++) {
2026
- const character = this.#text.charAt(index);
2027
- switch (character) {
2028
- case "\n":
2029
- case "\r":
2030
- text.push(character);
2031
- break;
2032
- default:
2033
- text.push(" ");
2034
- }
2035
- }
2036
- return text.join("");
1954
+ #escapeSequence(attributes) {
1955
+ return ["\u001B[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join("");
2037
1956
  }
2038
- #addRanges(node, ranges) {
2039
- this.#nodes.push(node);
2040
- for (const range of ranges) {
2041
- const rangeText = range.replacement != null
2042
- ? `${range.replacement}${this.#getErasedRangeText(range).slice(range.replacement.length)}`
2043
- : this.#getErasedRangeText(range);
2044
- this.#text = `${this.#text.slice(0, range.start)}${rangeText}${this.#text.slice(range.end)}`;
1957
+ #indentEachLine(lines, level) {
1958
+ if (level === 0) {
1959
+ return lines;
2045
1960
  }
1961
+ return lines.replace(this.#notEmptyLineRegex, this.#indentStep.repeat(level));
2046
1962
  }
2047
- close() {
2048
- if (this.#nodes.length > 0) {
2049
- this.#projectService.openFile(this.#filePath, this.#text, this.#resolvedConfig.rootPath);
2050
- const languageService = this.#projectService.getLanguageService(this.#filePath);
2051
- const diagnostics = new Set(languageService?.getSemanticDiagnostics(this.#filePath));
2052
- for (const node of this.#nodes.reverse()) {
2053
- for (const diagnostic of diagnostics) {
2054
- if (diagnosticBelongsToNode(diagnostic, node.matcherNode)) {
2055
- if (!node.abilityDiagnostics) {
2056
- node.abilityDiagnostics = new Set();
2057
- }
2058
- node.abilityDiagnostics.add(diagnostic);
2059
- diagnostics.delete(diagnostic);
2060
- }
2061
- }
2062
- }
1963
+ render(element) {
1964
+ if (typeof element.type === "function") {
1965
+ return this.render(element.type({ ...element.props }));
2063
1966
  }
2064
- this.#filePath = "";
2065
- this.#nodes = [];
2066
- this.#text = "";
2067
- }
2068
- handleNode(assertionNode) {
2069
- const expectStart = assertionNode.node.getStart();
2070
- const expectExpressionEnd = assertionNode.node.expression.getEnd();
2071
- const expectEnd = assertionNode.node.getEnd();
2072
- const matcherNameEnd = assertionNode.matcherNameNode.getEnd();
2073
- switch (assertionNode.matcherNameNode.name.text) {
2074
- case "toBeApplicable":
2075
- this.#addRanges(assertionNode, [
2076
- { end: expectExpressionEnd, start: expectStart },
2077
- { end: matcherNameEnd, start: expectEnd },
2078
- ]);
2079
- break;
2080
- case "toBeCallableWith":
2081
- this.#addRanges(assertionNode, [
2082
- { end: expectExpressionEnd, start: expectStart, replacement: ";" },
2083
- { end: matcherNameEnd, start: expectEnd },
2084
- ]);
2085
- break;
2086
- case "toBeConstructableWith":
2087
- this.#addRanges(assertionNode, [
2088
- { end: expectExpressionEnd, start: expectStart, replacement: "; new" },
2089
- { end: matcherNameEnd, start: expectEnd },
2090
- ]);
2091
- break;
1967
+ if (element.type === "ansi" && !this.#noColor) {
1968
+ return this.#escapeSequence(element.props.escapes);
2092
1969
  }
1970
+ if (element.type === "newLine") {
1971
+ return this.#newLine;
1972
+ }
1973
+ if (element.type === "text") {
1974
+ const text = this.#visitChildren(element.props.children);
1975
+ return this.#indentEachLine(text, element.props.indent);
1976
+ }
1977
+ return "";
2093
1978
  }
2094
- open(sourceFile) {
2095
- this.#filePath = sourceFile.fileName;
2096
- this.#text = sourceFile.text;
1979
+ #visitChildren(children) {
1980
+ const text = [];
1981
+ for (const child of children) {
1982
+ if (typeof child === "string" || typeof child === "number") {
1983
+ text.push(child);
1984
+ continue;
1985
+ }
1986
+ if (Array.isArray(child)) {
1987
+ text.push(this.#visitChildren(child));
1988
+ continue;
1989
+ }
1990
+ if (child != null && typeof child === "object") {
1991
+ text.push(this.render(child));
1992
+ }
1993
+ }
1994
+ return text.join("");
2097
1995
  }
2098
1996
  }
2099
1997
 
2100
- var TestTreeNodeFlags;
2101
- (function (TestTreeNodeFlags) {
2102
- TestTreeNodeFlags[TestTreeNodeFlags["None"] = 0] = "None";
2103
- TestTreeNodeFlags[TestTreeNodeFlags["Fail"] = 1] = "Fail";
2104
- TestTreeNodeFlags[TestTreeNodeFlags["Only"] = 2] = "Only";
2105
- TestTreeNodeFlags[TestTreeNodeFlags["Skip"] = 4] = "Skip";
2106
- TestTreeNodeFlags[TestTreeNodeFlags["Todo"] = 8] = "Todo";
2107
- })(TestTreeNodeFlags || (TestTreeNodeFlags = {}));
2108
-
2109
- class IdentifierLookup {
2110
- #compiler;
2111
- #identifiers;
2112
- #moduleSpecifiers = ['"tstyche"', "'tstyche'"];
2113
- constructor(compiler, identifiers) {
2114
- this.#compiler = compiler;
2115
- this.#identifiers = identifiers ?? {
2116
- namedImports: {
2117
- describe: undefined,
2118
- expect: undefined,
2119
- it: undefined,
2120
- namespace: undefined,
2121
- test: undefined,
2122
- },
2123
- namespace: undefined,
2124
- };
2125
- }
2126
- handleImportDeclaration(node) {
2127
- if (this.#moduleSpecifiers.includes(node.moduleSpecifier.getText()) &&
2128
- node.importClause?.isTypeOnly !== true &&
2129
- node.importClause?.namedBindings != null) {
2130
- if (this.#compiler.isNamedImports(node.importClause.namedBindings)) {
2131
- for (const element of node.importClause.namedBindings.elements) {
2132
- if (element.isTypeOnly) {
2133
- continue;
2134
- }
2135
- let identifierKey;
2136
- if (element.propertyName) {
2137
- identifierKey = element.propertyName.getText();
2138
- }
2139
- else {
2140
- identifierKey = element.name.getText();
2141
- }
2142
- if (identifierKey in this.#identifiers.namedImports) {
2143
- this.#identifiers.namedImports[identifierKey] = element.name.getText();
2144
- }
2145
- }
2146
- }
2147
- if (this.#compiler.isNamespaceImport(node.importClause.namedBindings)) {
2148
- this.#identifiers.namespace = node.importClause.namedBindings.name.getText();
2149
- }
2150
- }
2151
- }
2152
- resolveTestMemberMeta(node) {
2153
- let flags = TestTreeNodeFlags.None;
2154
- let expression = node.expression;
2155
- while (this.#compiler.isPropertyAccessExpression(expression)) {
2156
- if (expression.expression.getText() === this.#identifiers.namespace) {
2157
- break;
2158
- }
2159
- switch (expression.name.getText()) {
2160
- case "fail":
2161
- flags |= TestTreeNodeFlags.Fail;
2162
- break;
2163
- case "only":
2164
- flags |= TestTreeNodeFlags.Only;
2165
- break;
2166
- case "skip":
2167
- flags |= TestTreeNodeFlags.Skip;
2168
- break;
2169
- case "todo":
2170
- flags |= TestTreeNodeFlags.Todo;
2171
- break;
2172
- }
2173
- expression = expression.expression;
2174
- }
2175
- let identifierName;
2176
- if (this.#compiler.isPropertyAccessExpression(expression) &&
2177
- expression.expression.getText() === this.#identifiers.namespace) {
2178
- identifierName = expression.name.getText();
2179
- }
2180
- else {
2181
- identifierName = Object.keys(this.#identifiers.namedImports).find((key) => this.#identifiers.namedImports[key] === expression.getText());
2182
- }
2183
- if (!identifierName) {
2184
- return;
2185
- }
2186
- switch (identifierName) {
2187
- case "describe":
2188
- return { brand: TestTreeNodeBrand.Describe, flags };
2189
- case "it":
2190
- case "test":
2191
- return { brand: TestTreeNodeBrand.Test, flags };
2192
- case "expect":
2193
- return { brand: TestTreeNodeBrand.Expect, flags };
2194
- }
2195
- return;
2196
- }
2197
- }
2198
-
2199
- class TestTree {
2200
- children = [];
2201
- diagnostics;
2202
- hasOnly = false;
2203
- sourceFile;
2204
- constructor(diagnostics, sourceFile) {
2205
- this.diagnostics = diagnostics;
2206
- this.sourceFile = sourceFile;
2207
- }
2208
- }
2209
-
2210
- class CollectService {
2211
- #abilityLayer;
2212
- #compiler;
2213
- #projectService;
2214
- #resolvedConfig;
2215
- constructor(compiler, projectService, resolvedConfig) {
2216
- this.#compiler = compiler;
2217
- this.#projectService = projectService;
2218
- this.#resolvedConfig = resolvedConfig;
2219
- this.#abilityLayer = new AbilityLayer(this.#projectService, this.#resolvedConfig);
2220
- }
2221
- #collectTestTreeNodes(node, identifiers, parent) {
2222
- if (this.#compiler.isCallExpression(node)) {
2223
- const meta = identifiers.resolveTestMemberMeta(node);
2224
- if (meta != null && (meta.brand === TestTreeNodeBrand.Describe || meta.brand === TestTreeNodeBrand.Test)) {
2225
- const testTreeNode = new TestTreeNode(this.#compiler, meta.brand, node, parent, meta.flags);
2226
- parent.children.push(testTreeNode);
2227
- EventEmitter.dispatch(["collect:node", { testNode: testTreeNode }]);
2228
- this.#compiler.forEachChild(node, (node) => {
2229
- this.#collectTestTreeNodes(node, identifiers, testTreeNode);
2230
- });
2231
- return;
2232
- }
2233
- if (meta != null && meta.brand === TestTreeNodeBrand.Expect) {
2234
- const modifierNode = this.#getChainedNode(node, "type");
2235
- if (!modifierNode) {
2236
- return;
2237
- }
2238
- const notNode = this.#getChainedNode(modifierNode, "not");
2239
- const matcherNameNode = this.#getChainedNode(notNode ?? modifierNode);
2240
- if (!matcherNameNode) {
2241
- return;
2242
- }
2243
- const matcherNode = this.#getMatcherNode(matcherNameNode);
2244
- if (!matcherNode) {
2245
- return;
2246
- }
2247
- const assertionNode = new AssertionNode(this.#compiler, meta.brand, node, parent, meta.flags, matcherNode, matcherNameNode, modifierNode, notNode);
2248
- parent.children.push(assertionNode);
2249
- this.#abilityLayer.handleNode(assertionNode);
2250
- EventEmitter.dispatch(["collect:node", { testNode: assertionNode }]);
2251
- this.#compiler.forEachChild(node, (node) => {
2252
- this.#collectTestTreeNodes(node, identifiers, assertionNode);
2253
- });
2254
- return;
2255
- }
2256
- }
2257
- if (this.#compiler.isImportDeclaration(node)) {
2258
- identifiers.handleImportDeclaration(node);
2259
- return;
2260
- }
2261
- this.#compiler.forEachChild(node, (node) => {
2262
- this.#collectTestTreeNodes(node, identifiers, parent);
2263
- });
2264
- }
2265
- createTestTree(sourceFile, semanticDiagnostics = []) {
2266
- const testTree = new TestTree(new Set(semanticDiagnostics), sourceFile);
2267
- EventEmitter.dispatch(["collect:start", { testTree }]);
2268
- this.#abilityLayer.open(sourceFile);
2269
- this.#collectTestTreeNodes(sourceFile, new IdentifierLookup(this.#compiler), testTree);
2270
- this.#abilityLayer.close();
2271
- EventEmitter.dispatch(["collect:end", { testTree }]);
2272
- return testTree;
2273
- }
2274
- #getChainedNode({ parent }, name) {
2275
- if (!this.#compiler.isPropertyAccessExpression(parent)) {
2276
- return;
2277
- }
2278
- if (name != null && name !== parent.name.getText()) {
2279
- return;
2280
- }
2281
- return parent;
2282
- }
2283
- #getMatcherNode(node) {
2284
- if (this.#compiler.isCallExpression(node.parent)) {
2285
- return node.parent;
2286
- }
2287
- if (this.#compiler.isDecorator(node.parent)) {
2288
- return node.parent;
2289
- }
2290
- if (this.#compiler.isParenthesizedExpression(node.parent)) {
2291
- return this.#getMatcherNode(node.parent);
2292
- }
2293
- return;
2294
- }
2295
- }
2296
-
2297
- class TestTreeHandler {
2298
- testTree;
2299
- on([event, payload]) {
2300
- switch (event) {
2301
- case "collect:start":
2302
- this.testTree = payload.testTree;
2303
- break;
2304
- case "collect:end":
2305
- this.testTree = undefined;
2306
- break;
2307
- case "collect:node":
2308
- if (payload.testNode.flags & TestTreeNodeFlags.Only) {
2309
- this.testTree.hasOnly = true;
2310
- }
2311
- break;
2312
- }
2313
- }
2314
- }
2315
-
2316
- function jsx(type, props) {
2317
- return { props, type };
2318
- }
2319
-
2320
- var Color;
2321
- (function (Color) {
2322
- Color["Reset"] = "0";
2323
- Color["Red"] = "31";
2324
- Color["Green"] = "32";
2325
- Color["Yellow"] = "33";
2326
- Color["Blue"] = "34";
2327
- Color["Magenta"] = "35";
2328
- Color["Cyan"] = "36";
2329
- Color["Gray"] = "90";
2330
- })(Color || (Color = {}));
2331
-
2332
- function Text({ children, color, indent }) {
2333
- const ansiEscapes = [];
2334
- if (color != null) {
2335
- ansiEscapes.push(color);
2336
- }
2337
- return (jsx("text", { indent: indent ?? 0, children: [ansiEscapes.length > 0 ? jsx("ansi", { escapes: ansiEscapes }) : undefined, children, ansiEscapes.length > 0 ? jsx("ansi", { escapes: Color.Reset }) : undefined] }));
2338
- }
2339
-
2340
- function Line({ children, color, indent }) {
2341
- return (jsx(Text, { color: color, indent: indent, children: [children, jsx("newLine", {})] }));
2342
- }
2343
-
2344
- class Scribbler {
2345
- #indentStep = " ";
2346
- #newLine;
2347
- #noColor;
2348
- #notEmptyLineRegex = /^(?!$)/gm;
2349
- constructor(options) {
2350
- this.#newLine = options?.newLine ?? "\n";
2351
- this.#noColor = options?.noColor ?? environmentOptions.noColor;
2352
- }
2353
- #escapeSequence(attributes) {
2354
- return ["\u001B[", Array.isArray(attributes) ? attributes.join(";") : attributes, "m"].join("");
2355
- }
2356
- #indentEachLine(lines, level) {
2357
- if (level === 0) {
2358
- return lines;
2359
- }
2360
- return lines.replace(this.#notEmptyLineRegex, this.#indentStep.repeat(level));
2361
- }
2362
- render(element) {
2363
- if (typeof element.type === "function") {
2364
- return this.render(element.type({ ...element.props }));
2365
- }
2366
- if (element.type === "ansi" && !this.#noColor) {
2367
- return this.#escapeSequence(element.props.escapes);
2368
- }
2369
- if (element.type === "newLine") {
2370
- return this.#newLine;
2371
- }
2372
- if (element.type === "text") {
2373
- const text = this.#visitChildren(element.props.children);
2374
- return this.#indentEachLine(text, element.props.indent);
2375
- }
2376
- return "";
2377
- }
2378
- #visitChildren(children) {
2379
- const text = [];
2380
- for (const child of children) {
2381
- if (typeof child === "string" || typeof child === "number") {
2382
- text.push(child);
2383
- continue;
2384
- }
2385
- if (Array.isArray(child)) {
2386
- text.push(this.#visitChildren(child));
2387
- continue;
2388
- }
2389
- if (child != null && typeof child === "object") {
2390
- text.push(this.render(child));
2391
- }
2392
- }
2393
- return text.join("");
2394
- }
2395
- }
2396
-
2397
- function addsPackageText(packageVersion, packagePath) {
2398
- return (jsx(Line, { children: [jsx(Text, { color: Color.Gray, children: "adds" }), " TypeScript ", packageVersion, jsx(Text, { color: Color.Gray, children: [" to ", packagePath] })] }));
2399
- }
1998
+ function addsPackageText(packageVersion, packagePath) {
1999
+ return (jsx(Line, { children: [jsx(Text, { color: Color.Gray, children: "adds" }), " TypeScript ", packageVersion, jsx(Text, { color: Color.Gray, children: [" to ", packagePath] })] }));
2000
+ }
2400
2001
 
2401
2002
  function describeNameText(name, indent = 0) {
2402
2003
  return jsx(Line, { indent: indent + 1, children: name });
@@ -2627,39 +2228,12 @@ function CountText({ failed, passed, skipped, todo, total }) {
2627
2228
  function DurationText({ seconds }) {
2628
2229
  return jsx(Text, { children: `${Math.round(seconds * 10) / 10}s` });
2629
2230
  }
2630
- function MatchText({ text }) {
2631
- if (typeof text === "string") {
2632
- return jsx(Text, { children: ["'", text, "'"] });
2633
- }
2634
- if (text.length === 1) {
2635
- return jsx(Text, { children: ["'", ...text, "'"] });
2636
- }
2637
- const lastItem = text.pop();
2638
- return (jsx(Text, { children: [text.map((match, index, list) => (jsx(Text, { children: ["'", match, "'", index === list.length - 1 ? jsx(Text, { children: " " }) : jsx(Text, { color: Color.Gray, children: ", " })] }))), jsx(Text, { color: Color.Gray, children: "or" }), " '", lastItem, "'"] }));
2639
- }
2640
- function RanFilesText({ onlyMatch, pathMatch, skipMatch }) {
2641
- const testNameMatchText = [];
2642
- if (onlyMatch != null) {
2643
- testNameMatchText.push(jsx(Text, { children: [jsx(Text, { color: Color.Gray, children: "matching " }), jsx(MatchText, { text: onlyMatch })] }));
2644
- }
2645
- if (skipMatch != null) {
2646
- testNameMatchText.push(jsx(Text, { children: [onlyMatch && jsx(Text, { color: Color.Gray, children: " and " }), jsx(Text, { color: Color.Gray, children: "not matching " }), jsx(MatchText, { text: skipMatch })] }));
2647
- }
2648
- let pathMatchText;
2649
- if (pathMatch.length > 0) {
2650
- pathMatchText = (jsx(Text, { children: [jsx(Text, { color: Color.Gray, children: "test files matching " }), jsx(MatchText, { text: pathMatch }), jsx(Text, { color: Color.Gray, children: "." })] }));
2651
- }
2652
- else {
2653
- pathMatchText = jsx(Text, { color: Color.Gray, children: "all test files." });
2654
- }
2655
- return (jsx(Line, { children: [jsx(Text, { color: Color.Gray, children: "Ran " }), testNameMatchText.length > 0 ? jsx(Text, { color: Color.Gray, children: "tests " }) : undefined, testNameMatchText, testNameMatchText.length > 0 ? jsx(Text, { color: Color.Gray, children: " in " }) : undefined, pathMatchText] }));
2656
- }
2657
- function summaryText({ duration, expectCount, fileCount, onlyMatch, pathMatch, skipMatch, targetCount, testCount, }) {
2231
+ function summaryText({ duration, expectCount, fileCount, targetCount, testCount, }) {
2658
2232
  const targetCountText = (jsx(RowText, { label: "Targets", text: jsx(CountText, { failed: targetCount.failed, passed: targetCount.passed, skipped: targetCount.skipped, todo: targetCount.todo, total: targetCount.total }) }));
2659
2233
  const fileCountText = (jsx(RowText, { label: "Test files", text: jsx(CountText, { failed: fileCount.failed, passed: fileCount.passed, skipped: fileCount.skipped, todo: fileCount.todo, total: fileCount.total }) }));
2660
2234
  const testCountText = (jsx(RowText, { label: "Tests", text: jsx(CountText, { failed: testCount.failed, passed: testCount.passed, skipped: testCount.skipped, todo: testCount.todo, total: testCount.total }) }));
2661
2235
  const assertionCountText = (jsx(RowText, { label: "Assertions", text: jsx(CountText, { failed: expectCount.failed, passed: expectCount.passed, skipped: expectCount.skipped, todo: expectCount.todo, total: expectCount.total }) }));
2662
- return (jsx(Text, { children: [targetCountText, fileCountText, testCount.total > 0 ? testCountText : undefined, expectCount.total > 0 ? assertionCountText : undefined, jsx(RowText, { label: "Duration", text: jsx(DurationText, { seconds: duration / 1000 }) }), jsx(Line, {}), jsx(RanFilesText, { onlyMatch: onlyMatch, pathMatch: pathMatch, skipMatch: skipMatch })] }));
2236
+ return (jsx(Text, { children: [targetCountText, fileCountText, testCount.total > 0 ? testCountText : undefined, expectCount.total > 0 ? assertionCountText : undefined, jsx(RowText, { label: "Duration", text: jsx(DurationText, { seconds: duration / 1000 }) })] }));
2663
2237
  }
2664
2238
 
2665
2239
  function StatusText({ status }) {
@@ -2808,6 +2382,7 @@ class ListReporter extends BaseReporter {
2808
2382
  this.#hasReportedError = false;
2809
2383
  break;
2810
2384
  case "task:error":
2385
+ case "collect:error":
2811
2386
  for (const diagnostic of payload.diagnostics) {
2812
2387
  this.#fileView.addMessage(diagnosticText(diagnostic));
2813
2388
  }
@@ -2903,9 +2478,6 @@ class SummaryReporter extends BaseReporter {
2903
2478
  duration: payload.result.timing.duration,
2904
2479
  expectCount: payload.result.expectCount,
2905
2480
  fileCount: payload.result.fileCount,
2906
- onlyMatch: payload.result.resolvedConfig.only,
2907
- pathMatch: payload.result.resolvedConfig.pathMatch,
2908
- skipMatch: payload.result.resolvedConfig.skip,
2909
2481
  targetCount: payload.result.targetCount,
2910
2482
  testCount: payload.result.testCount,
2911
2483
  }));
@@ -2963,340 +2535,825 @@ class CancellationToken {
2963
2535
  this.#reason = reason;
2964
2536
  }
2965
2537
  }
2966
- reset() {
2967
- if (this.#isCancelled) {
2968
- this.#isCancelled = false;
2969
- this.#reason = undefined;
2970
- }
2538
+ reset() {
2539
+ if (this.#isCancelled) {
2540
+ this.#isCancelled = false;
2541
+ this.#reason = undefined;
2542
+ }
2543
+ }
2544
+ }
2545
+
2546
+ var CancellationReason;
2547
+ (function (CancellationReason) {
2548
+ CancellationReason["ConfigChange"] = "configChange";
2549
+ CancellationReason["ConfigError"] = "configError";
2550
+ CancellationReason["CollectError"] = "collectError";
2551
+ CancellationReason["FailFast"] = "failFast";
2552
+ CancellationReason["WatchClose"] = "watchClose";
2553
+ })(CancellationReason || (CancellationReason = {}));
2554
+
2555
+ class Watcher {
2556
+ #onChanged;
2557
+ #onRemoved;
2558
+ #recursive;
2559
+ #targetPath;
2560
+ #watcher;
2561
+ constructor(targetPath, onChanged, onRemoved, options) {
2562
+ this.#targetPath = targetPath;
2563
+ this.#onChanged = onChanged;
2564
+ this.#onRemoved = onRemoved ?? onChanged;
2565
+ this.#recursive = options?.recursive;
2566
+ }
2567
+ close() {
2568
+ this.#watcher?.close();
2569
+ }
2570
+ watch() {
2571
+ this.#watcher = watch(this.#targetPath, { recursive: this.#recursive }, (_eventType, fileName) => {
2572
+ if (fileName != null) {
2573
+ const filePath = Path.resolve(this.#targetPath, fileName);
2574
+ if (existsSync(filePath)) {
2575
+ this.#onChanged(filePath);
2576
+ }
2577
+ else {
2578
+ this.#onRemoved(filePath);
2579
+ }
2580
+ }
2581
+ });
2582
+ }
2583
+ }
2584
+
2585
+ class FileWatcher extends Watcher {
2586
+ constructor(targetPath, onChanged) {
2587
+ const onChangedFile = (filePath) => {
2588
+ if (filePath === targetPath) {
2589
+ onChanged();
2590
+ }
2591
+ };
2592
+ super(Path.dirname(targetPath), onChangedFile);
2593
+ }
2594
+ }
2595
+
2596
+ class InputService {
2597
+ #onInput;
2598
+ #stdin = process.stdin;
2599
+ constructor(onInput) {
2600
+ this.#onInput = onInput;
2601
+ this.#stdin.setRawMode?.(true);
2602
+ this.#stdin.setEncoding("utf8");
2603
+ this.#stdin.unref();
2604
+ this.#stdin.addListener("data", this.#onInput);
2605
+ }
2606
+ close() {
2607
+ this.#stdin.removeListener("data", this.#onInput);
2608
+ this.#stdin.setRawMode?.(false);
2609
+ }
2610
+ }
2611
+
2612
+ class GlobPattern {
2613
+ static #reservedCharacterRegex = /[^\w\s/]/g;
2614
+ static #parse(pattern, usageTarget) {
2615
+ const segments = pattern.split("/");
2616
+ let resultPattern = "\\.";
2617
+ let optionalSegmentCount = 0;
2618
+ for (const segment of segments) {
2619
+ if (segment === ".") {
2620
+ continue;
2621
+ }
2622
+ if (segment === "**") {
2623
+ resultPattern += "(\\/(?!(node_modules)(\\/|$))[^./][^/]*)*?";
2624
+ continue;
2625
+ }
2626
+ if (usageTarget === "directories") {
2627
+ resultPattern += "(";
2628
+ optionalSegmentCount++;
2629
+ }
2630
+ resultPattern += "\\/";
2631
+ const segmentPattern = segment.replace(GlobPattern.#reservedCharacterRegex, GlobPattern.#replaceReservedCharacter);
2632
+ if (segmentPattern !== segment) {
2633
+ resultPattern += "(?!(node_modules)(\\/|$))";
2634
+ }
2635
+ resultPattern += segmentPattern;
2636
+ }
2637
+ resultPattern += ")?".repeat(optionalSegmentCount);
2638
+ return resultPattern;
2639
+ }
2640
+ static #replaceReservedCharacter(match, offset) {
2641
+ switch (match) {
2642
+ case "*":
2643
+ return offset === 0 ? "([^./][^/]*)?" : "([^/]*)?";
2644
+ case "?":
2645
+ return offset === 0 ? "[^./]" : "[^/]";
2646
+ default:
2647
+ return `\\${match}`;
2648
+ }
2649
+ }
2650
+ static toRegex(patterns, target) {
2651
+ const patternText = patterns.map((pattern) => `(${GlobPattern.#parse(pattern, target)})`).join("|");
2652
+ return new RegExp(`^(${patternText})$`);
2653
+ }
2654
+ }
2655
+
2656
+ class SelectDiagnosticText {
2657
+ static #pathSelectOptions(resolvedConfig) {
2658
+ const text = [
2659
+ `Root path: ${resolvedConfig.rootPath}`,
2660
+ `Test file match: ${resolvedConfig.testFileMatch.join(", ")}`,
2661
+ ];
2662
+ if (resolvedConfig.pathMatch.length > 0) {
2663
+ text.push(`Path match: ${resolvedConfig.pathMatch.join(", ")}`);
2664
+ }
2665
+ return text;
2666
+ }
2667
+ static noTestFilesWereLeft(resolvedConfig) {
2668
+ return [
2669
+ "No test files were left to run using current configuration.",
2670
+ ...SelectDiagnosticText.#pathSelectOptions(resolvedConfig),
2671
+ ];
2672
+ }
2673
+ static noTestFilesWereSelected(resolvedConfig) {
2674
+ return [
2675
+ "No test files were selected using current configuration.",
2676
+ ...SelectDiagnosticText.#pathSelectOptions(resolvedConfig),
2677
+ ];
2678
+ }
2679
+ }
2680
+
2681
+ class Select {
2682
+ static #patternsCache = new WeakMap();
2683
+ static async #getAccessibleFileSystemEntries(targetPath) {
2684
+ const directories = [];
2685
+ const files = [];
2686
+ try {
2687
+ const entries = await fs.readdir(targetPath, { withFileTypes: true });
2688
+ for (const entry of entries) {
2689
+ let entryMeta = entry;
2690
+ if (entry.isSymbolicLink()) {
2691
+ entryMeta = await fs.stat([targetPath, entry.name].join("/"));
2692
+ }
2693
+ if (entryMeta.isDirectory()) {
2694
+ directories.push(entry.name);
2695
+ }
2696
+ else if (entryMeta.isFile()) {
2697
+ files.push(entry.name);
2698
+ }
2699
+ }
2700
+ }
2701
+ catch {
2702
+ }
2703
+ return { directories, files };
2704
+ }
2705
+ static #getMatchPatterns(globPatterns) {
2706
+ let matchPatterns = Select.#patternsCache.get(globPatterns);
2707
+ if (!matchPatterns) {
2708
+ matchPatterns = {
2709
+ includedDirectory: GlobPattern.toRegex(globPatterns, "directories"),
2710
+ includedFile: GlobPattern.toRegex(globPatterns, "files"),
2711
+ };
2712
+ Select.#patternsCache.set(globPatterns, matchPatterns);
2713
+ }
2714
+ return matchPatterns;
2715
+ }
2716
+ static #isDirectoryIncluded(directoryPath, matchPatterns) {
2717
+ return matchPatterns.includedDirectory.test(directoryPath);
2718
+ }
2719
+ static #isFileIncluded(filePath, matchPatterns, resolvedConfig) {
2720
+ if (resolvedConfig.pathMatch.length > 0 &&
2721
+ !resolvedConfig.pathMatch.some((match) => filePath.toLowerCase().includes(match.toLowerCase()))) {
2722
+ return false;
2723
+ }
2724
+ return matchPatterns.includedFile.test(filePath);
2725
+ }
2726
+ static isTestFile(filePath, resolvedConfig) {
2727
+ const matchPatterns = Select.#getMatchPatterns(resolvedConfig.testFileMatch);
2728
+ return Select.#isFileIncluded(Path.relative(resolvedConfig.rootPath, filePath), matchPatterns, resolvedConfig);
2729
+ }
2730
+ static #onDiagnostics(diagnostic) {
2731
+ EventEmitter.dispatch(["select:error", { diagnostics: [diagnostic] }]);
2732
+ }
2733
+ static async selectFiles(resolvedConfig) {
2734
+ const matchPatterns = Select.#getMatchPatterns(resolvedConfig.testFileMatch);
2735
+ const testFilePaths = [];
2736
+ await Select.#visitDirectory(".", testFilePaths, matchPatterns, resolvedConfig);
2737
+ if (testFilePaths.length === 0) {
2738
+ Select.#onDiagnostics(Diagnostic.error(SelectDiagnosticText.noTestFilesWereSelected(resolvedConfig)));
2739
+ }
2740
+ return testFilePaths.sort();
2741
+ }
2742
+ static async #visitDirectory(currentPath, testFilePaths, matchPatterns, resolvedConfig) {
2743
+ const targetPath = Path.join(resolvedConfig.rootPath, currentPath);
2744
+ const entries = await Select.#getAccessibleFileSystemEntries(targetPath);
2745
+ for (const directoryName of entries.directories) {
2746
+ const directoryPath = [currentPath, directoryName].join("/");
2747
+ if (Select.#isDirectoryIncluded(directoryPath, matchPatterns)) {
2748
+ await Select.#visitDirectory(directoryPath, testFilePaths, matchPatterns, resolvedConfig);
2749
+ }
2750
+ }
2751
+ for (const fileName of entries.files) {
2752
+ const filePath = [currentPath, fileName].join("/");
2753
+ if (Select.#isFileIncluded(filePath, matchPatterns, resolvedConfig)) {
2754
+ testFilePaths.push([targetPath, fileName].join("/"));
2755
+ }
2756
+ }
2757
+ }
2758
+ }
2759
+
2760
+ class Debounce {
2761
+ #delay;
2762
+ #onResolve;
2763
+ #resolve;
2764
+ #timeout;
2765
+ constructor(delay, onResolve) {
2766
+ this.#delay = delay;
2767
+ this.#onResolve = onResolve;
2768
+ }
2769
+ cancel() {
2770
+ clearTimeout(this.#timeout);
2971
2771
  }
2972
- }
2973
-
2974
- var CancellationReason;
2975
- (function (CancellationReason) {
2976
- CancellationReason["ConfigChange"] = "configChange";
2977
- CancellationReason["ConfigError"] = "configError";
2978
- CancellationReason["FailFast"] = "failFast";
2979
- CancellationReason["WatchClose"] = "watchClose";
2980
- })(CancellationReason || (CancellationReason = {}));
2981
-
2982
- class Watcher {
2983
- #onChanged;
2984
- #onRemoved;
2985
- #recursive;
2986
- #targetPath;
2987
- #watcher;
2988
- constructor(targetPath, onChanged, onRemoved, options) {
2989
- this.#targetPath = targetPath;
2990
- this.#onChanged = onChanged;
2991
- this.#onRemoved = onRemoved ?? onChanged;
2992
- this.#recursive = options?.recursive;
2772
+ refresh() {
2773
+ this.cancel();
2774
+ this.#timeout = setTimeout(() => {
2775
+ this.#resolve?.(this.#onResolve());
2776
+ }, this.#delay);
2993
2777
  }
2994
- close() {
2995
- this.#watcher?.close();
2778
+ resolve(value) {
2779
+ this.#resolve?.(value);
2996
2780
  }
2997
- watch() {
2998
- this.#watcher = watch(this.#targetPath, { recursive: this.#recursive }, (_eventType, fileName) => {
2999
- if (fileName != null) {
3000
- const filePath = Path.resolve(this.#targetPath, fileName);
3001
- if (existsSync(filePath)) {
3002
- this.#onChanged(filePath);
3003
- }
3004
- else {
3005
- this.#onRemoved(filePath);
3006
- }
3007
- }
2781
+ schedule() {
2782
+ return new Promise((resolve) => {
2783
+ this.#resolve = resolve;
3008
2784
  });
3009
2785
  }
3010
2786
  }
3011
2787
 
3012
- class FileWatcher extends Watcher {
3013
- constructor(targetPath, onChanged) {
2788
+ class WatchService {
2789
+ #changedTestFiles = new Map();
2790
+ #inputService;
2791
+ #resolvedConfig;
2792
+ #watchedTestFiles;
2793
+ #watchers = [];
2794
+ constructor(resolvedConfig, tasks) {
2795
+ this.#resolvedConfig = resolvedConfig;
2796
+ this.#watchedTestFiles = new Map(tasks.map((task) => [task.filePath, task]));
2797
+ }
2798
+ #onDiagnostics(diagnostic) {
2799
+ EventEmitter.dispatch(["watch:error", { diagnostics: [diagnostic] }]);
2800
+ }
2801
+ async *watch(cancellationToken) {
2802
+ const onResolve = () => {
2803
+ const testFiles = [...this.#changedTestFiles.values()];
2804
+ this.#changedTestFiles.clear();
2805
+ return testFiles;
2806
+ };
2807
+ const debounce = new Debounce(100, onResolve);
2808
+ const onClose = (reason) => {
2809
+ debounce.cancel();
2810
+ this.#inputService?.close();
2811
+ for (const watcher of this.#watchers) {
2812
+ watcher.close();
2813
+ }
2814
+ cancellationToken.cancel(reason);
2815
+ debounce.resolve([]);
2816
+ };
2817
+ if (!environmentOptions.noInteractive) {
2818
+ const onInput = (chunk) => {
2819
+ switch (chunk.toLowerCase()) {
2820
+ case "\u0003":
2821
+ case "\u0004":
2822
+ case "\u001B":
2823
+ case "q":
2824
+ case "x":
2825
+ onClose(CancellationReason.WatchClose);
2826
+ break;
2827
+ case "\u000D":
2828
+ case "\u0020":
2829
+ case "a":
2830
+ debounce.cancel();
2831
+ if (this.#watchedTestFiles.size > 0) {
2832
+ debounce.resolve([...this.#watchedTestFiles.values()]);
2833
+ }
2834
+ break;
2835
+ }
2836
+ };
2837
+ this.#inputService = new InputService(onInput);
2838
+ }
3014
2839
  const onChangedFile = (filePath) => {
3015
- if (filePath === targetPath) {
3016
- onChanged();
2840
+ debounce.refresh();
2841
+ let task = this.#watchedTestFiles.get(filePath);
2842
+ if (task != null) {
2843
+ this.#changedTestFiles.set(filePath, task);
2844
+ }
2845
+ else if (Select.isTestFile(filePath, this.#resolvedConfig)) {
2846
+ task = new Task(filePath);
2847
+ this.#changedTestFiles.set(filePath, task);
2848
+ this.#watchedTestFiles.set(filePath, task);
3017
2849
  }
3018
2850
  };
3019
- super(Path.dirname(targetPath), onChangedFile);
2851
+ const onRemovedFile = (filePath) => {
2852
+ this.#changedTestFiles.delete(filePath);
2853
+ this.#watchedTestFiles.delete(filePath);
2854
+ if (this.#watchedTestFiles.size === 0) {
2855
+ debounce.cancel();
2856
+ this.#onDiagnostics(Diagnostic.error(SelectDiagnosticText.noTestFilesWereLeft(this.#resolvedConfig)));
2857
+ }
2858
+ };
2859
+ this.#watchers.push(new Watcher(this.#resolvedConfig.rootPath, onChangedFile, onRemovedFile, { recursive: true }));
2860
+ const onChangedConfigFile = () => {
2861
+ onClose(CancellationReason.ConfigChange);
2862
+ };
2863
+ this.#watchers.push(new FileWatcher(this.#resolvedConfig.configFilePath, onChangedConfigFile));
2864
+ for (const watcher of this.#watchers) {
2865
+ watcher.watch();
2866
+ }
2867
+ while (!cancellationToken.isCancellationRequested) {
2868
+ const testFiles = await debounce.schedule();
2869
+ if (testFiles.length > 0) {
2870
+ yield testFiles;
2871
+ }
2872
+ }
3020
2873
  }
3021
2874
  }
3022
2875
 
3023
- class InputService {
3024
- #onInput;
3025
- #stdin = process.stdin;
3026
- constructor(onInput) {
3027
- this.#onInput = onInput;
3028
- this.#stdin.setRawMode?.(true);
3029
- this.#stdin.setEncoding("utf8");
3030
- this.#stdin.unref();
3031
- this.#stdin.addListener("data", this.#onInput);
3032
- }
3033
- close() {
3034
- this.#stdin.removeListener("data", this.#onInput);
3035
- this.#stdin.setRawMode?.(false);
3036
- }
3037
- }
2876
+ var TestTreeNodeBrand;
2877
+ (function (TestTreeNodeBrand) {
2878
+ TestTreeNodeBrand["Describe"] = "describe";
2879
+ TestTreeNodeBrand["Test"] = "test";
2880
+ TestTreeNodeBrand["Expect"] = "expect";
2881
+ TestTreeNodeBrand["When"] = "when";
2882
+ })(TestTreeNodeBrand || (TestTreeNodeBrand = {}));
3038
2883
 
3039
- class GlobPattern {
3040
- static #reservedCharacterRegex = /[^\w\s/]/g;
3041
- static #parse(pattern, usageTarget) {
3042
- const segments = pattern.split("/");
3043
- let resultPattern = "\\.";
3044
- let optionalSegmentCount = 0;
3045
- for (const segment of segments) {
3046
- if (segment === ".") {
3047
- continue;
3048
- }
3049
- if (segment === "**") {
3050
- resultPattern += "(\\/(?!(node_modules)(\\/|$))[^./][^/]*)*?";
3051
- continue;
3052
- }
3053
- if (usageTarget === "directories") {
3054
- resultPattern += "(";
3055
- optionalSegmentCount++;
2884
+ class TestTreeNode {
2885
+ brand;
2886
+ children = [];
2887
+ #compiler;
2888
+ diagnostics = new Set();
2889
+ flags;
2890
+ name = "";
2891
+ node;
2892
+ parent;
2893
+ constructor(compiler, brand, node, parent, flags) {
2894
+ this.brand = brand;
2895
+ this.#compiler = compiler;
2896
+ this.node = node;
2897
+ this.parent = parent;
2898
+ this.flags = flags;
2899
+ if (node.arguments[0] != null && compiler.isStringLiteralLike(node.arguments[0])) {
2900
+ this.name = node.arguments[0].text;
2901
+ }
2902
+ if (node.arguments[1] != null && compiler.isFunctionLike(node.arguments[1])) {
2903
+ for (const diagnostic of parent.diagnostics) {
2904
+ if (diagnosticBelongsToNode(diagnostic, node.arguments[1].body)) {
2905
+ this.diagnostics.add(diagnostic);
2906
+ parent.diagnostics.delete(diagnostic);
2907
+ }
3056
2908
  }
3057
- resultPattern += "\\/";
3058
- const segmentPattern = segment.replace(GlobPattern.#reservedCharacterRegex, GlobPattern.#replaceReservedCharacter);
3059
- if (segmentPattern !== segment) {
3060
- resultPattern += "(?!(node_modules)(\\/|$))";
2909
+ }
2910
+ }
2911
+ validate() {
2912
+ const diagnostics = [];
2913
+ const getText = (node) => `'${node.expression.getText()}()' cannot be nested within '${this.node.expression.getText()}()'.`;
2914
+ const getParentCallExpression = (node) => {
2915
+ while (!this.#compiler.isCallExpression(node.parent)) {
2916
+ node = node.parent;
3061
2917
  }
3062
- resultPattern += segmentPattern;
2918
+ return node.parent;
2919
+ };
2920
+ switch (this.brand) {
2921
+ case TestTreeNodeBrand.Describe:
2922
+ for (const child of this.children) {
2923
+ if (child.brand === TestTreeNodeBrand.Expect || child.brand === TestTreeNodeBrand.When) {
2924
+ diagnostics.push(Diagnostic.error(getText(child.node), DiagnosticOrigin.fromNode(getParentCallExpression(child.node))));
2925
+ }
2926
+ }
2927
+ break;
2928
+ case TestTreeNodeBrand.Test:
2929
+ case TestTreeNodeBrand.Expect:
2930
+ for (const child of this.children) {
2931
+ if (child.brand === TestTreeNodeBrand.Describe || child.brand === TestTreeNodeBrand.Test) {
2932
+ diagnostics.push(Diagnostic.error(getText(child.node), DiagnosticOrigin.fromNode(child.node)));
2933
+ }
2934
+ }
2935
+ break;
3063
2936
  }
3064
- resultPattern += ")?".repeat(optionalSegmentCount);
3065
- return resultPattern;
2937
+ return diagnostics;
3066
2938
  }
3067
- static #replaceReservedCharacter(match, offset) {
3068
- switch (match) {
3069
- case "*":
3070
- return offset === 0 ? "([^./][^/]*)?" : "([^/]*)?";
3071
- case "?":
3072
- return offset === 0 ? "[^./]" : "[^/]";
3073
- default:
3074
- return `\\${match}`;
2939
+ }
2940
+
2941
+ class AssertionNode extends TestTreeNode {
2942
+ abilityDiagnostics;
2943
+ isNot;
2944
+ matcherNode;
2945
+ matcherNameNode;
2946
+ modifierNode;
2947
+ notNode;
2948
+ source;
2949
+ target;
2950
+ constructor(compiler, brand, node, parent, flags, matcherNode, matcherNameNode, modifierNode, notNode) {
2951
+ super(compiler, brand, node, parent, flags);
2952
+ this.isNot = notNode != null;
2953
+ this.matcherNode = matcherNode;
2954
+ this.matcherNameNode = matcherNameNode;
2955
+ this.modifierNode = modifierNode;
2956
+ this.source = this.node.typeArguments ?? this.node.arguments;
2957
+ if (compiler.isCallExpression(this.matcherNode)) {
2958
+ this.target = this.matcherNode.typeArguments ?? this.matcherNode.arguments;
2959
+ }
2960
+ for (const diagnostic of parent.diagnostics) {
2961
+ if (diagnosticBelongsToNode(diagnostic, this.source)) {
2962
+ this.diagnostics.add(diagnostic);
2963
+ parent.diagnostics.delete(diagnostic);
2964
+ }
3075
2965
  }
3076
- }
3077
- static toRegex(patterns, target) {
3078
- const patternText = patterns.map((pattern) => `(${GlobPattern.#parse(pattern, target)})`).join("|");
3079
- return new RegExp(`^(${patternText})$`);
3080
2966
  }
3081
2967
  }
3082
2968
 
3083
- class SelectDiagnosticText {
3084
- static #pathSelectOptions(resolvedConfig) {
3085
- const text = [
3086
- `Root path: ${resolvedConfig.rootPath}`,
3087
- `Test file match: ${resolvedConfig.testFileMatch.join(", ")}`,
3088
- ];
3089
- if (resolvedConfig.pathMatch.length > 0) {
3090
- text.push(`Path match: ${resolvedConfig.pathMatch.join(", ")}`);
3091
- }
3092
- return text;
3093
- }
3094
- static noTestFilesWereLeft(resolvedConfig) {
3095
- return [
3096
- "No test files were left to run using current configuration.",
3097
- ...SelectDiagnosticText.#pathSelectOptions(resolvedConfig),
3098
- ];
3099
- }
3100
- static noTestFilesWereSelected(resolvedConfig) {
3101
- return [
3102
- "No test files were selected using current configuration.",
3103
- ...SelectDiagnosticText.#pathSelectOptions(resolvedConfig),
3104
- ];
2969
+ function nodeBelongsToArgumentList(compiler, node) {
2970
+ if (compiler.isCallExpression(node.parent)) {
2971
+ return node.parent.arguments.some((argument) => argument === node);
3105
2972
  }
2973
+ return false;
3106
2974
  }
3107
2975
 
3108
- class Select {
3109
- static #patternsCache = new WeakMap();
3110
- static async #getAccessibleFileSystemEntries(targetPath) {
3111
- const directories = [];
3112
- const files = [];
3113
- try {
3114
- const entries = await fs.readdir(targetPath, { withFileTypes: true });
3115
- for (const entry of entries) {
3116
- let entryMeta = entry;
3117
- if (entry.isSymbolicLink()) {
3118
- entryMeta = await fs.stat([targetPath, entry.name].join("/"));
3119
- }
3120
- if (entryMeta.isDirectory()) {
3121
- directories.push(entry.name);
3122
- }
3123
- else if (entryMeta.isFile()) {
3124
- files.push(entry.name);
3125
- }
2976
+ class AbilityLayer {
2977
+ #compiler;
2978
+ #filePath = "";
2979
+ #nodes = [];
2980
+ #projectService;
2981
+ #resolvedConfig;
2982
+ #text = "";
2983
+ constructor(compiler, projectService, resolvedConfig) {
2984
+ this.#compiler = compiler;
2985
+ this.#projectService = projectService;
2986
+ this.#resolvedConfig = resolvedConfig;
2987
+ }
2988
+ #getErasedRangeText(range) {
2989
+ if (this.#text.indexOf("\n", range.start) >= range.end) {
2990
+ return " ".repeat(range.end - range.start);
2991
+ }
2992
+ const text = [];
2993
+ for (let index = range.start; index < range.end; index++) {
2994
+ const character = this.#text.charAt(index);
2995
+ switch (character) {
2996
+ case "\n":
2997
+ case "\r":
2998
+ text.push(character);
2999
+ break;
3000
+ default:
3001
+ text.push(" ");
3126
3002
  }
3127
3003
  }
3128
- catch {
3004
+ return text.join("");
3005
+ }
3006
+ #addRanges(node, ranges) {
3007
+ this.#nodes.push(node);
3008
+ for (const range of ranges) {
3009
+ const rangeText = range.replacement != null
3010
+ ? `${range.replacement}${this.#getErasedRangeText(range).slice(range.replacement.length)}`
3011
+ : this.#getErasedRangeText(range);
3012
+ this.#text = `${this.#text.slice(0, range.start)}${rangeText}${this.#text.slice(range.end)}`;
3129
3013
  }
3130
- return { directories, files };
3131
3014
  }
3132
- static #getMatchPatterns(globPatterns) {
3133
- let matchPatterns = Select.#patternsCache.get(globPatterns);
3134
- if (!matchPatterns) {
3135
- matchPatterns = {
3136
- includedDirectory: GlobPattern.toRegex(globPatterns, "directories"),
3137
- includedFile: GlobPattern.toRegex(globPatterns, "files"),
3138
- };
3139
- Select.#patternsCache.set(globPatterns, matchPatterns);
3015
+ close() {
3016
+ if (this.#nodes.length > 0) {
3017
+ this.#projectService.openFile(this.#filePath, this.#text, this.#resolvedConfig.rootPath);
3018
+ const languageService = this.#projectService.getLanguageService(this.#filePath);
3019
+ const diagnostics = new Set(languageService?.getSemanticDiagnostics(this.#filePath));
3020
+ for (const node of this.#nodes.reverse()) {
3021
+ for (const diagnostic of diagnostics) {
3022
+ if (diagnosticBelongsToNode(diagnostic, "matcherNode" in node ? node.matcherNode : node.actionNode)) {
3023
+ if (!node.abilityDiagnostics) {
3024
+ node.abilityDiagnostics = new Set();
3025
+ }
3026
+ node.abilityDiagnostics.add(diagnostic);
3027
+ diagnostics.delete(diagnostic);
3028
+ }
3029
+ }
3030
+ }
3140
3031
  }
3141
- return matchPatterns;
3032
+ this.#filePath = "";
3033
+ this.#nodes = [];
3034
+ this.#text = "";
3142
3035
  }
3143
- static #isDirectoryIncluded(directoryPath, matchPatterns) {
3144
- return matchPatterns.includedDirectory.test(directoryPath);
3036
+ handleWhen(whenNode) {
3037
+ const whenStart = whenNode.node.getStart();
3038
+ const whenExpressionEnd = whenNode.node.expression.getEnd();
3039
+ const whenEnd = whenNode.node.getEnd();
3040
+ const actionNameEnd = whenNode.actionNameNode.getEnd();
3041
+ switch (whenNode.actionNameNode.name.text) {
3042
+ case "isCalledWith":
3043
+ this.#addRanges(whenNode, [
3044
+ {
3045
+ end: whenExpressionEnd,
3046
+ start: whenStart,
3047
+ replacement: nodeBelongsToArgumentList(this.#compiler, whenNode.actionNode) ? "" : ";",
3048
+ },
3049
+ { end: actionNameEnd, start: whenEnd },
3050
+ ]);
3051
+ break;
3052
+ }
3145
3053
  }
3146
- static #isFileIncluded(filePath, matchPatterns, resolvedConfig) {
3147
- if (resolvedConfig.pathMatch.length > 0 &&
3148
- !resolvedConfig.pathMatch.some((match) => filePath.toLowerCase().includes(match.toLowerCase()))) {
3149
- return false;
3054
+ handleAssertion(assertionNode) {
3055
+ const expectStart = assertionNode.node.getStart();
3056
+ const expectExpressionEnd = assertionNode.node.expression.getEnd();
3057
+ const expectEnd = assertionNode.node.getEnd();
3058
+ const matcherNameEnd = assertionNode.matcherNameNode.getEnd();
3059
+ switch (assertionNode.matcherNameNode.name.text) {
3060
+ case "toBeApplicable":
3061
+ this.#addRanges(assertionNode, [
3062
+ { end: expectExpressionEnd, start: expectStart },
3063
+ { end: matcherNameEnd, start: expectEnd },
3064
+ ]);
3065
+ break;
3066
+ case "toBeCallableWith":
3067
+ this.#addRanges(assertionNode, [
3068
+ {
3069
+ end: expectExpressionEnd,
3070
+ start: expectStart,
3071
+ replacement: nodeBelongsToArgumentList(this.#compiler, assertionNode.matcherNode) ? "" : ";",
3072
+ },
3073
+ { end: matcherNameEnd, start: expectEnd },
3074
+ ]);
3075
+ break;
3076
+ case "toBeConstructableWith":
3077
+ this.#addRanges(assertionNode, [
3078
+ {
3079
+ end: expectExpressionEnd,
3080
+ start: expectStart,
3081
+ replacement: nodeBelongsToArgumentList(this.#compiler, assertionNode.matcherNode) ? "new" : "; new",
3082
+ },
3083
+ { end: matcherNameEnd, start: expectEnd },
3084
+ ]);
3085
+ break;
3150
3086
  }
3151
- return matchPatterns.includedFile.test(filePath);
3152
3087
  }
3153
- static isTestFile(filePath, resolvedConfig) {
3154
- const matchPatterns = Select.#getMatchPatterns(resolvedConfig.testFileMatch);
3155
- return Select.#isFileIncluded(Path.relative(resolvedConfig.rootPath, filePath), matchPatterns, resolvedConfig);
3088
+ open(sourceFile) {
3089
+ this.#filePath = sourceFile.fileName;
3090
+ this.#text = sourceFile.text;
3156
3091
  }
3157
- static #onDiagnostics(diagnostic) {
3158
- EventEmitter.dispatch(["select:error", { diagnostics: [diagnostic] }]);
3092
+ }
3093
+
3094
+ class CollectDiagnosticText {
3095
+ static cannotBeNestedWithin(source, target) {
3096
+ return `'${source}()' cannot be nested within '${target}()'.`;
3159
3097
  }
3160
- static async selectFiles(resolvedConfig) {
3161
- const matchPatterns = Select.#getMatchPatterns(resolvedConfig.testFileMatch);
3162
- const testFilePaths = [];
3163
- await Select.#visitDirectory(".", testFilePaths, matchPatterns, resolvedConfig);
3164
- if (testFilePaths.length === 0) {
3165
- Select.#onDiagnostics(Diagnostic.error(SelectDiagnosticText.noTestFilesWereSelected(resolvedConfig)));
3098
+ }
3099
+
3100
+ var TestTreeNodeFlags;
3101
+ (function (TestTreeNodeFlags) {
3102
+ TestTreeNodeFlags[TestTreeNodeFlags["None"] = 0] = "None";
3103
+ TestTreeNodeFlags[TestTreeNodeFlags["Fail"] = 1] = "Fail";
3104
+ TestTreeNodeFlags[TestTreeNodeFlags["Only"] = 2] = "Only";
3105
+ TestTreeNodeFlags[TestTreeNodeFlags["Skip"] = 4] = "Skip";
3106
+ TestTreeNodeFlags[TestTreeNodeFlags["Todo"] = 8] = "Todo";
3107
+ })(TestTreeNodeFlags || (TestTreeNodeFlags = {}));
3108
+
3109
+ class IdentifierLookup {
3110
+ #compiler;
3111
+ #identifiers;
3112
+ #moduleSpecifiers = ['"tstyche"', "'tstyche'"];
3113
+ constructor(compiler, identifiers) {
3114
+ this.#compiler = compiler;
3115
+ this.#identifiers = identifiers ?? {
3116
+ namedImports: {
3117
+ describe: undefined,
3118
+ expect: undefined,
3119
+ it: undefined,
3120
+ namespace: undefined,
3121
+ test: undefined,
3122
+ when: undefined,
3123
+ },
3124
+ namespace: undefined,
3125
+ };
3126
+ }
3127
+ handleImportDeclaration(node) {
3128
+ if (this.#moduleSpecifiers.includes(node.moduleSpecifier.getText()) &&
3129
+ node.importClause?.isTypeOnly !== true &&
3130
+ node.importClause?.namedBindings != null) {
3131
+ if (this.#compiler.isNamedImports(node.importClause.namedBindings)) {
3132
+ for (const element of node.importClause.namedBindings.elements) {
3133
+ if (element.isTypeOnly) {
3134
+ continue;
3135
+ }
3136
+ let identifierKey;
3137
+ if (element.propertyName) {
3138
+ identifierKey = element.propertyName.getText();
3139
+ }
3140
+ else {
3141
+ identifierKey = element.name.getText();
3142
+ }
3143
+ if (identifierKey in this.#identifiers.namedImports) {
3144
+ this.#identifiers.namedImports[identifierKey] = element.name.getText();
3145
+ }
3146
+ }
3147
+ }
3148
+ if (this.#compiler.isNamespaceImport(node.importClause.namedBindings)) {
3149
+ this.#identifiers.namespace = node.importClause.namedBindings.name.getText();
3150
+ }
3166
3151
  }
3167
- return testFilePaths.sort();
3168
3152
  }
3169
- static async #visitDirectory(currentPath, testFilePaths, matchPatterns, resolvedConfig) {
3170
- const targetPath = Path.join(resolvedConfig.rootPath, currentPath);
3171
- const entries = await Select.#getAccessibleFileSystemEntries(targetPath);
3172
- for (const directoryName of entries.directories) {
3173
- const directoryPath = [currentPath, directoryName].join("/");
3174
- if (Select.#isDirectoryIncluded(directoryPath, matchPatterns)) {
3175
- await Select.#visitDirectory(directoryPath, testFilePaths, matchPatterns, resolvedConfig);
3153
+ resolveTestMemberMeta(node) {
3154
+ let flags = TestTreeNodeFlags.None;
3155
+ let expression = node.expression;
3156
+ while (this.#compiler.isPropertyAccessExpression(expression)) {
3157
+ if (expression.expression.getText() === this.#identifiers.namespace) {
3158
+ break;
3159
+ }
3160
+ switch (expression.name.getText()) {
3161
+ case "fail":
3162
+ flags |= TestTreeNodeFlags.Fail;
3163
+ break;
3164
+ case "only":
3165
+ flags |= TestTreeNodeFlags.Only;
3166
+ break;
3167
+ case "skip":
3168
+ flags |= TestTreeNodeFlags.Skip;
3169
+ break;
3170
+ case "todo":
3171
+ flags |= TestTreeNodeFlags.Todo;
3172
+ break;
3176
3173
  }
3174
+ expression = expression.expression;
3175
+ }
3176
+ let identifierName;
3177
+ if (this.#compiler.isPropertyAccessExpression(expression) &&
3178
+ expression.expression.getText() === this.#identifiers.namespace) {
3179
+ identifierName = expression.name.getText();
3180
+ }
3181
+ else {
3182
+ identifierName = Object.keys(this.#identifiers.namedImports).find((key) => this.#identifiers.namedImports[key] === expression.getText());
3183
+ }
3184
+ if (!identifierName) {
3185
+ return;
3177
3186
  }
3178
- for (const fileName of entries.files) {
3179
- const filePath = [currentPath, fileName].join("/");
3180
- if (Select.#isFileIncluded(filePath, matchPatterns, resolvedConfig)) {
3181
- testFilePaths.push([targetPath, fileName].join("/"));
3182
- }
3187
+ switch (identifierName) {
3188
+ case "describe":
3189
+ return { brand: TestTreeNodeBrand.Describe, flags };
3190
+ case "it":
3191
+ case "test":
3192
+ return { brand: TestTreeNodeBrand.Test, flags };
3193
+ case "expect":
3194
+ return { brand: TestTreeNodeBrand.Expect, flags };
3195
+ case "when":
3196
+ return { brand: TestTreeNodeBrand.When, flags };
3183
3197
  }
3198
+ return;
3184
3199
  }
3185
3200
  }
3186
3201
 
3187
- class Debounce {
3188
- #delay;
3189
- #onResolve;
3190
- #resolve;
3191
- #timeout;
3192
- constructor(delay, onResolve) {
3193
- this.#delay = delay;
3194
- this.#onResolve = onResolve;
3195
- }
3196
- cancel() {
3197
- clearTimeout(this.#timeout);
3198
- }
3199
- refresh() {
3200
- this.cancel();
3201
- this.#timeout = setTimeout(() => {
3202
- this.#resolve?.(this.#onResolve());
3203
- }, this.#delay);
3204
- }
3205
- resolve(value) {
3206
- this.#resolve?.(value);
3202
+ class TestTree {
3203
+ children = [];
3204
+ diagnostics;
3205
+ hasOnly = false;
3206
+ sourceFile;
3207
+ constructor(diagnostics, sourceFile) {
3208
+ this.diagnostics = diagnostics;
3209
+ this.sourceFile = sourceFile;
3207
3210
  }
3208
- schedule() {
3209
- return new Promise((resolve) => {
3210
- this.#resolve = resolve;
3211
- });
3211
+ }
3212
+
3213
+ class WhenNode extends TestTreeNode {
3214
+ actionNode;
3215
+ actionNameNode;
3216
+ abilityDiagnostics;
3217
+ target;
3218
+ constructor(compiler, brand, node, parent, flags, actionNode, actionNameNode) {
3219
+ super(compiler, brand, node, parent, flags);
3220
+ this.actionNode = actionNode;
3221
+ this.actionNameNode = actionNameNode;
3222
+ this.target = this.node.typeArguments ?? this.node.arguments;
3223
+ for (const diagnostic of parent.diagnostics) {
3224
+ if (diagnosticBelongsToNode(diagnostic, node)) {
3225
+ this.diagnostics.add(diagnostic);
3226
+ parent.diagnostics.delete(diagnostic);
3227
+ }
3228
+ }
3212
3229
  }
3213
3230
  }
3214
3231
 
3215
- class WatchService {
3216
- #changedTestFiles = new Map();
3217
- #inputService;
3232
+ class CollectService {
3233
+ #abilityLayer;
3234
+ #compiler;
3235
+ #projectService;
3218
3236
  #resolvedConfig;
3219
- #watchedTestFiles;
3220
- #watchers = [];
3221
- constructor(resolvedConfig, tasks) {
3237
+ #testTree;
3238
+ constructor(compiler, projectService, resolvedConfig) {
3239
+ this.#compiler = compiler;
3240
+ this.#projectService = projectService;
3222
3241
  this.#resolvedConfig = resolvedConfig;
3223
- this.#watchedTestFiles = new Map(tasks.map((task) => [task.filePath, task]));
3224
- }
3225
- #onDiagnostics(diagnostic) {
3226
- EventEmitter.dispatch(["watch:error", { diagnostics: [diagnostic] }]);
3242
+ this.#abilityLayer = new AbilityLayer(compiler, this.#projectService, this.#resolvedConfig);
3227
3243
  }
3228
- async *watch(cancellationToken) {
3229
- const onResolve = () => {
3230
- const testFiles = [...this.#changedTestFiles.values()];
3231
- this.#changedTestFiles.clear();
3232
- return testFiles;
3233
- };
3234
- const debounce = new Debounce(100, onResolve);
3235
- const onClose = (reason) => {
3236
- debounce.cancel();
3237
- this.#inputService?.close();
3238
- for (const watcher of this.#watchers) {
3239
- watcher.close();
3244
+ #collectTestTreeNodes(node, identifiers, parent) {
3245
+ if (this.#compiler.isCallExpression(node)) {
3246
+ const meta = identifiers.resolveTestMemberMeta(node);
3247
+ if (meta != null && (meta.brand === TestTreeNodeBrand.Describe || meta.brand === TestTreeNodeBrand.Test)) {
3248
+ const testTreeNode = new TestTreeNode(this.#compiler, meta.brand, node, parent, meta.flags);
3249
+ this.#compiler.forEachChild(node, (node) => {
3250
+ this.#collectTestTreeNodes(node, identifiers, testTreeNode);
3251
+ });
3252
+ this.#onNode(testTreeNode, parent);
3253
+ return;
3240
3254
  }
3241
- cancellationToken.cancel(reason);
3242
- debounce.resolve([]);
3243
- };
3244
- if (!environmentOptions.noInteractive) {
3245
- const onInput = (chunk) => {
3246
- switch (chunk.toLowerCase()) {
3247
- case "\u0003":
3248
- case "\u0004":
3249
- case "\u001B":
3250
- case "q":
3251
- case "x":
3252
- onClose(CancellationReason.WatchClose);
3253
- break;
3254
- case "\u000D":
3255
- case "\u0020":
3256
- case "a":
3257
- debounce.cancel();
3258
- if (this.#watchedTestFiles.size > 0) {
3259
- debounce.resolve([...this.#watchedTestFiles.values()]);
3260
- }
3261
- break;
3255
+ if (meta != null && meta.brand === TestTreeNodeBrand.Expect) {
3256
+ const modifierNode = this.#getChainedNode(node, "type");
3257
+ if (!modifierNode) {
3258
+ return;
3262
3259
  }
3263
- };
3264
- this.#inputService = new InputService(onInput);
3265
- }
3266
- const onChangedFile = (filePath) => {
3267
- debounce.refresh();
3268
- let task = this.#watchedTestFiles.get(filePath);
3269
- if (task != null) {
3270
- this.#changedTestFiles.set(filePath, task);
3271
- }
3272
- else if (Select.isTestFile(filePath, this.#resolvedConfig)) {
3273
- task = new Task(filePath);
3274
- this.#changedTestFiles.set(filePath, task);
3275
- this.#watchedTestFiles.set(filePath, task);
3260
+ const notNode = this.#getChainedNode(modifierNode, "not");
3261
+ const matcherNameNode = this.#getChainedNode(notNode ?? modifierNode);
3262
+ if (!matcherNameNode) {
3263
+ return;
3264
+ }
3265
+ const matcherNode = this.#getMatcherNode(matcherNameNode);
3266
+ if (!matcherNode) {
3267
+ return;
3268
+ }
3269
+ const assertionNode = new AssertionNode(this.#compiler, meta.brand, node, parent, meta.flags, matcherNode, matcherNameNode, modifierNode, notNode);
3270
+ this.#abilityLayer.handleAssertion(assertionNode);
3271
+ this.#compiler.forEachChild(node, (node) => {
3272
+ this.#collectTestTreeNodes(node, identifiers, assertionNode);
3273
+ });
3274
+ this.#onNode(assertionNode, parent);
3275
+ return;
3276
3276
  }
3277
- };
3278
- const onRemovedFile = (filePath) => {
3279
- this.#changedTestFiles.delete(filePath);
3280
- this.#watchedTestFiles.delete(filePath);
3281
- if (this.#watchedTestFiles.size === 0) {
3282
- debounce.cancel();
3283
- this.#onDiagnostics(Diagnostic.error(SelectDiagnosticText.noTestFilesWereLeft(this.#resolvedConfig)));
3277
+ if (meta != null && meta.brand === TestTreeNodeBrand.When) {
3278
+ const actionNameNode = this.#getChainedNode(node);
3279
+ if (!actionNameNode) {
3280
+ return;
3281
+ }
3282
+ const actionNode = this.#getActionNode(actionNameNode);
3283
+ if (!actionNode) {
3284
+ return;
3285
+ }
3286
+ this.#compiler.forEachChild(actionNode, (node) => {
3287
+ if (this.#compiler.isCallExpression(node)) {
3288
+ const meta = identifiers.resolveTestMemberMeta(node);
3289
+ if (meta != null && (meta.brand === TestTreeNodeBrand.Describe || meta.brand === TestTreeNodeBrand.Test)) {
3290
+ const text = CollectDiagnosticText.cannotBeNestedWithin(node.expression.getText(), "when");
3291
+ const origin = DiagnosticOrigin.fromNode(node);
3292
+ this.#onDiagnostics(Diagnostic.error(text, origin));
3293
+ }
3294
+ }
3295
+ });
3296
+ const whenNode = new WhenNode(this.#compiler, meta.brand, node, parent, meta.flags, actionNode, actionNameNode);
3297
+ this.#abilityLayer.handleWhen(whenNode);
3298
+ this.#onNode(whenNode, parent);
3299
+ return;
3284
3300
  }
3285
- };
3286
- this.#watchers.push(new Watcher(this.#resolvedConfig.rootPath, onChangedFile, onRemovedFile, { recursive: true }));
3287
- const onChangedConfigFile = () => {
3288
- onClose(CancellationReason.ConfigChange);
3289
- };
3290
- this.#watchers.push(new FileWatcher(this.#resolvedConfig.configFilePath, onChangedConfigFile));
3291
- for (const watcher of this.#watchers) {
3292
- watcher.watch();
3293
3301
  }
3294
- while (!cancellationToken.isCancellationRequested) {
3295
- const testFiles = await debounce.schedule();
3296
- if (testFiles.length > 0) {
3297
- yield testFiles;
3298
- }
3302
+ if (this.#compiler.isImportDeclaration(node)) {
3303
+ identifiers.handleImportDeclaration(node);
3304
+ return;
3305
+ }
3306
+ this.#compiler.forEachChild(node, (node) => {
3307
+ this.#collectTestTreeNodes(node, identifiers, parent);
3308
+ });
3309
+ }
3310
+ createTestTree(sourceFile, semanticDiagnostics = []) {
3311
+ const testTree = new TestTree(new Set(semanticDiagnostics), sourceFile);
3312
+ EventEmitter.dispatch(["collect:start", { tree: testTree }]);
3313
+ this.#testTree = testTree;
3314
+ this.#abilityLayer.open(sourceFile);
3315
+ this.#collectTestTreeNodes(sourceFile, new IdentifierLookup(this.#compiler), testTree);
3316
+ this.#abilityLayer.close();
3317
+ this.#testTree = undefined;
3318
+ EventEmitter.dispatch(["collect:end", { tree: testTree }]);
3319
+ return testTree;
3320
+ }
3321
+ #getChainedNode({ parent }, name) {
3322
+ if (!this.#compiler.isPropertyAccessExpression(parent)) {
3323
+ return;
3324
+ }
3325
+ if (name != null && name !== parent.name.getText()) {
3326
+ return;
3327
+ }
3328
+ return parent;
3329
+ }
3330
+ #getMatcherNode(node) {
3331
+ if (this.#compiler.isCallExpression(node.parent)) {
3332
+ return node.parent;
3333
+ }
3334
+ if (this.#compiler.isDecorator(node.parent)) {
3335
+ return node.parent;
3336
+ }
3337
+ if (this.#compiler.isParenthesizedExpression(node.parent)) {
3338
+ return this.#getMatcherNode(node.parent);
3299
3339
  }
3340
+ return;
3341
+ }
3342
+ #getActionNode(node) {
3343
+ if (this.#compiler.isCallExpression(node.parent)) {
3344
+ return node.parent;
3345
+ }
3346
+ return;
3347
+ }
3348
+ #onDiagnostics(diagnostic) {
3349
+ EventEmitter.dispatch(["collect:error", { diagnostics: [diagnostic] }]);
3350
+ }
3351
+ #onNode(node, parent) {
3352
+ parent.children.push(node);
3353
+ if (node.flags & TestTreeNodeFlags.Only) {
3354
+ this.#testTree.hasOnly = true;
3355
+ }
3356
+ EventEmitter.dispatch(["collect:node", { node }]);
3300
3357
  }
3301
3358
  }
3302
3359
 
@@ -3400,7 +3457,7 @@ class ProjectService {
3400
3457
  { diagnostics: Diagnostic.fromDiagnostics(configFileErrors) },
3401
3458
  ]);
3402
3459
  }
3403
- if (this.#resolvedConfig.checkSourceFiles) {
3460
+ if (this.#resolvedConfig.checkSourceFiles && !sourceText) {
3404
3461
  const languageService = this.getLanguageService(filePath);
3405
3462
  const program = languageService?.getProgram();
3406
3463
  if (!program || this.#seenPrograms.has(program)) {
@@ -3437,10 +3494,8 @@ var RunMode;
3437
3494
  RunMode[RunMode["Todo"] = 8] = "Todo";
3438
3495
  })(RunMode || (RunMode = {}));
3439
3496
 
3440
- class Format {
3441
- static capitalize(text) {
3442
- return text.replace(/^./, text.charAt(0).toUpperCase());
3443
- }
3497
+ function capitalize(text) {
3498
+ return text.replace(/^./, text.charAt(0).toUpperCase());
3444
3499
  }
3445
3500
 
3446
3501
  class ExpectDiagnosticText {
@@ -3456,23 +3511,23 @@ class ExpectDiagnosticText {
3456
3511
  static argumentMustBeProvided(argumentNameText) {
3457
3512
  return `An argument for '${argumentNameText}' must be provided.`;
3458
3513
  }
3459
- static isCallable(isTypeNode, targetText) {
3460
- return `${isTypeNode ? "Type" : "Expression"} is callable ${targetText}.`;
3514
+ static isCallable(isExpression, targetText) {
3515
+ return `${isExpression ? "Expression" : "Type"} is callable ${targetText}.`;
3461
3516
  }
3462
- static isNotCallable(isTypeNode, targetText) {
3463
- return `${isTypeNode ? "Type" : "Expression"} is not callable ${targetText}.`;
3517
+ static isNotCallable(isExpression, targetText) {
3518
+ return `${isExpression ? "Expression" : "Type"} is not callable ${targetText}.`;
3464
3519
  }
3465
- static isConstructable(isTypeNode, targetText) {
3466
- return `${isTypeNode ? "Type" : "Expression"} is constructable ${targetText}.`;
3520
+ static isConstructable(isExpression, targetText) {
3521
+ return `${isExpression ? "Expression" : "Type"} is constructable ${targetText}.`;
3467
3522
  }
3468
- static isNotConstructable(isTypeNode, targetText) {
3469
- return `${isTypeNode ? "Type" : "Expression"} is not constructable ${targetText}.`;
3523
+ static isNotConstructable(isExpression, targetText) {
3524
+ return `${isExpression ? "Expression" : "Type"} is not constructable ${targetText}.`;
3470
3525
  }
3471
- static acceptsProps(isTypeNode) {
3472
- return `${isTypeNode ? "Component type" : "Component"} accepts props of the given type.`;
3526
+ static acceptsProps(isExpression) {
3527
+ return `${isExpression ? "Component" : "Component type"} accepts props of the given type.`;
3473
3528
  }
3474
- static doesNotAcceptProps(isTypeNode) {
3475
- return `${isTypeNode ? "Component type" : "Component"} does not accept props of the given type.`;
3529
+ static doesNotAcceptProps(isExpression) {
3530
+ return `${isExpression ? "Component" : "Component type"} does not accept props of the given type.`;
3476
3531
  }
3477
3532
  static canBeApplied(targetText) {
3478
3533
  return `The decorator function can be applied${targetText}.`;
@@ -3504,21 +3559,21 @@ class ExpectDiagnosticText {
3504
3559
  static typeArgumentMustBe(argumentNameText, expectedText) {
3505
3560
  return `A type argument for '${argumentNameText}' must be ${expectedText}.`;
3506
3561
  }
3507
- static raisedError(isTypeNode, count, targetCount) {
3562
+ static raisedError(isExpression, count, targetCount) {
3508
3563
  let countText = "a";
3509
3564
  if (count > 1 || targetCount > 1) {
3510
3565
  countText = count > targetCount ? `${count}` : `only ${count}`;
3511
3566
  }
3512
- return `${isTypeNode ? "Type" : "Expression type"} raised ${countText} type error${count === 1 ? "" : "s"}.`;
3567
+ return `${isExpression ? "Expression" : "Type"} raised ${countText} type error${count === 1 ? "" : "s"}.`;
3513
3568
  }
3514
- static didNotRaiseError(isTypeNode) {
3515
- return `${isTypeNode ? "Type" : "Expression type"} did not raise a type error.`;
3569
+ static didNotRaiseError(isExpression) {
3570
+ return `${isExpression ? "Expression" : "Type"} did not raise a type error.`;
3516
3571
  }
3517
- static raisedMatchingError(isTypeNode) {
3518
- return `${isTypeNode ? "Type" : "Expression type"} raised a matching type error.`;
3572
+ static raisedMatchingError(isExpression) {
3573
+ return `${isExpression ? "Expression" : "Type"} raised a matching type error.`;
3519
3574
  }
3520
- static didNotRaiseMatchingError(isTypeNode) {
3521
- return `${isTypeNode ? "Type" : "Expression type"} did not raise a matching type error.`;
3575
+ static didNotRaiseMatchingError(isExpression) {
3576
+ return `${isExpression ? "Expression" : "Type"} did not raise a matching type error.`;
3522
3577
  }
3523
3578
  static isAssignableTo(sourceTypeText, targetTypeText) {
3524
3579
  return `Type '${sourceTypeText}' is assignable to type '${targetTypeText}'.`;
@@ -3548,7 +3603,7 @@ class ExpectDiagnosticText {
3548
3603
  return `Types of property '${propertyNameText}' are not compatible.`;
3549
3604
  }
3550
3605
  static typeWasRejected(typeText) {
3551
- const optionNameText = `reject${Format.capitalize(typeText)}Type`;
3606
+ const optionNameText = `reject${capitalize(typeText)}Type`;
3552
3607
  return [
3553
3608
  `The '${typeText}' type was rejected because the '${optionNameText}' option is enabled.`,
3554
3609
  `If this check is necessary, pass '${typeText}' as the type argument explicitly.`,
@@ -3670,12 +3725,13 @@ class ToAcceptProps {
3670
3725
  this.#typeChecker = typeChecker;
3671
3726
  }
3672
3727
  #explain(matchWorker, sourceNode, targetNode) {
3728
+ const isExpression = nodeBelongsToArgumentList(this.#compiler, sourceNode);
3673
3729
  const signatures = matchWorker.getSignatures(sourceNode);
3674
3730
  return signatures.reduce((accumulator, signature, index) => {
3675
3731
  let diagnostic;
3676
3732
  const introText = matchWorker.assertion.isNot
3677
- ? ExpectDiagnosticText.acceptsProps(this.#compiler.isTypeNode(sourceNode))
3678
- : ExpectDiagnosticText.doesNotAcceptProps(this.#compiler.isTypeNode(sourceNode));
3733
+ ? ExpectDiagnosticText.acceptsProps(isExpression)
3734
+ : ExpectDiagnosticText.doesNotAcceptProps(isExpression);
3679
3735
  const origin = DiagnosticOrigin.fromNode(targetNode, matchWorker.assertion);
3680
3736
  if (signatures.length > 1) {
3681
3737
  const signatureText = this.#typeChecker.signatureToString(signature, sourceNode);
@@ -3815,18 +3871,18 @@ class ToAcceptProps {
3815
3871
  const signatures = matchWorker.getSignatures(sourceNode);
3816
3872
  if (signatures.length === 0) {
3817
3873
  const expectedText = "of a function or class type";
3818
- const text = this.#compiler.isTypeNode(sourceNode)
3819
- ? ExpectDiagnosticText.typeArgumentMustBe("Source", expectedText)
3820
- : ExpectDiagnosticText.argumentMustBe("source", expectedText);
3874
+ const text = nodeBelongsToArgumentList(this.#compiler, sourceNode)
3875
+ ? ExpectDiagnosticText.argumentMustBe("source", expectedText)
3876
+ : ExpectDiagnosticText.typeArgumentMustBe("Source", expectedText);
3821
3877
  const origin = DiagnosticOrigin.fromNode(sourceNode);
3822
3878
  diagnostics.push(Diagnostic.error(text, origin));
3823
3879
  }
3824
3880
  const targetType = matchWorker.getType(targetNode);
3825
3881
  if (!(targetType.flags & this.#compiler.TypeFlags.Object)) {
3826
3882
  const expectedText = "of an object type";
3827
- const text = this.#compiler.isTypeNode(targetNode)
3828
- ? ExpectDiagnosticText.typeArgumentMustBe("Target", expectedText)
3829
- : ExpectDiagnosticText.argumentMustBe("target", expectedText);
3883
+ const text = nodeBelongsToArgumentList(this.#compiler, targetNode)
3884
+ ? ExpectDiagnosticText.argumentMustBe("target", expectedText)
3885
+ : ExpectDiagnosticText.typeArgumentMustBe("Target", expectedText);
3830
3886
  const origin = DiagnosticOrigin.fromNode(targetNode);
3831
3887
  diagnostics.push(Diagnostic.error(text, origin));
3832
3888
  }
@@ -3919,9 +3975,9 @@ class ToBeApplicable {
3919
3975
  const type = matchWorker.getType(sourceNode);
3920
3976
  if (type.getCallSignatures().length === 0) {
3921
3977
  const expectedText = "of a function type";
3922
- const text = this.#compiler.isTypeNode(sourceNode)
3923
- ? ExpectDiagnosticText.typeArgumentMustBe("Source", expectedText)
3924
- : ExpectDiagnosticText.argumentMustBe("source", expectedText);
3978
+ const text = nodeBelongsToArgumentList(this.#compiler, sourceNode)
3979
+ ? ExpectDiagnosticText.argumentMustBe("source", expectedText)
3980
+ : ExpectDiagnosticText.typeArgumentMustBe("Source", expectedText);
3925
3981
  const origin = DiagnosticOrigin.fromNode(sourceNode);
3926
3982
  onDiagnostics([Diagnostic.error(text, origin)]);
3927
3983
  return;
@@ -3970,12 +4026,15 @@ class ToBeCallableWith {
3970
4026
  return `with the given argument${nodes.length === 1 ? "" : "s"}`;
3971
4027
  }
3972
4028
  #explain(matchWorker, sourceNode, targetNodes) {
3973
- const isTypeNode = this.#compiler.isTypeNode(sourceNode);
4029
+ const isExpression = nodeBelongsToArgumentList(this.#compiler, sourceNode);
3974
4030
  const targetText = this.#resolveTargetText(targetNodes);
3975
4031
  const diagnostics = [];
3976
4032
  if (matchWorker.assertion.abilityDiagnostics) {
3977
4033
  for (const diagnostic of matchWorker.assertion.abilityDiagnostics) {
3978
- const text = [ExpectDiagnosticText.isNotCallable(isTypeNode, targetText), getDiagnosticMessageText(diagnostic)];
4034
+ const text = [
4035
+ ExpectDiagnosticText.isNotCallable(isExpression, targetText),
4036
+ getDiagnosticMessageText(diagnostic),
4037
+ ];
3979
4038
  let origin;
3980
4039
  if (isDiagnosticWithLocation(diagnostic) && diagnosticBelongsToNode(diagnostic, targetNodes)) {
3981
4040
  origin = new DiagnosticOrigin(diagnostic.start, getTextSpanEnd(diagnostic), sourceNode.getSourceFile());
@@ -3995,7 +4054,7 @@ class ToBeCallableWith {
3995
4054
  }
3996
4055
  else {
3997
4056
  const origin = DiagnosticOrigin.fromAssertion(matchWorker.assertion);
3998
- diagnostics.push(Diagnostic.error(ExpectDiagnosticText.isCallable(isTypeNode, targetText), origin));
4057
+ diagnostics.push(Diagnostic.error(ExpectDiagnosticText.isCallable(isExpression, targetText), origin));
3999
4058
  }
4000
4059
  return diagnostics;
4001
4060
  }
@@ -4008,16 +4067,17 @@ class ToBeCallableWith {
4008
4067
  this.#compiler.isFunctionDeclaration(sourceNode) ||
4009
4068
  this.#compiler.isFunctionExpression(sourceNode) ||
4010
4069
  this.#compiler.isExpressionWithTypeArguments(sourceNode) ||
4011
- this.#compiler.isIdentifier(sourceNode)) {
4070
+ this.#compiler.isIdentifier(sourceNode) ||
4071
+ this.#compiler.isPropertyAccessExpression(sourceNode)) {
4012
4072
  type = matchWorker.getType(sourceNode);
4013
4073
  }
4014
4074
  if (!type || type.getCallSignatures().length === 0) {
4015
4075
  const text = [];
4016
- if (this.#compiler.isTypeNode(sourceNode)) {
4017
- text.push(ExpectDiagnosticText.typeArgumentMustBe("Source", "a callable type"));
4076
+ if (nodeBelongsToArgumentList(this.#compiler, sourceNode)) {
4077
+ text.push(ExpectDiagnosticText.argumentMustBe("source", "a callable expression"));
4018
4078
  }
4019
4079
  else {
4020
- text.push(ExpectDiagnosticText.argumentMustBe("source", "a callable expression"));
4080
+ text.push(ExpectDiagnosticText.typeArgumentMustBe("Source", "a callable type"));
4021
4081
  }
4022
4082
  if (type != null && type.getConstructSignatures().length > 0) {
4023
4083
  text.push(ExpectDiagnosticText.didYouMeanToUse("the '.toBeConstructableWith()' matcher"));
@@ -4048,13 +4108,13 @@ class ToBeConstructableWith {
4048
4108
  return `with the given argument${nodes.length === 1 ? "" : "s"}`;
4049
4109
  }
4050
4110
  #explain(matchWorker, sourceNode, targetNodes) {
4051
- const isTypeNode = this.#compiler.isTypeNode(sourceNode);
4111
+ const isExpression = nodeBelongsToArgumentList(this.#compiler, sourceNode);
4052
4112
  const targetText = this.#resolveTargetText(targetNodes);
4053
4113
  const diagnostics = [];
4054
4114
  if (matchWorker.assertion.abilityDiagnostics) {
4055
4115
  for (const diagnostic of matchWorker.assertion.abilityDiagnostics) {
4056
4116
  const text = [
4057
- ExpectDiagnosticText.isNotConstructable(isTypeNode, targetText),
4117
+ ExpectDiagnosticText.isNotConstructable(isExpression, targetText),
4058
4118
  getDiagnosticMessageText(diagnostic),
4059
4119
  ];
4060
4120
  let origin;
@@ -4076,7 +4136,7 @@ class ToBeConstructableWith {
4076
4136
  }
4077
4137
  else {
4078
4138
  const origin = DiagnosticOrigin.fromAssertion(matchWorker.assertion);
4079
- diagnostics.push(Diagnostic.error(ExpectDiagnosticText.isConstructable(isTypeNode, targetText), origin));
4139
+ diagnostics.push(Diagnostic.error(ExpectDiagnosticText.isConstructable(isExpression, targetText), origin));
4080
4140
  }
4081
4141
  return diagnostics;
4082
4142
  }
@@ -4086,16 +4146,17 @@ class ToBeConstructableWith {
4086
4146
  type = matchWorker.typeChecker.getResolvedSignature(sourceNode)?.getReturnType();
4087
4147
  }
4088
4148
  if (this.#compiler.isExpressionWithTypeArguments(sourceNode) ||
4089
- this.#compiler.isIdentifier(sourceNode)) {
4149
+ this.#compiler.isIdentifier(sourceNode) ||
4150
+ this.#compiler.isPropertyAccessExpression(sourceNode)) {
4090
4151
  type = matchWorker.getType(sourceNode);
4091
4152
  }
4092
4153
  if (!type || type.getConstructSignatures().length === 0) {
4093
4154
  const text = [];
4094
- if (this.#compiler.isTypeNode(sourceNode)) {
4095
- text.push(ExpectDiagnosticText.typeArgumentMustBe("Source", "a constructable type"));
4155
+ if (nodeBelongsToArgumentList(this.#compiler, sourceNode)) {
4156
+ text.push(ExpectDiagnosticText.argumentMustBe("source", "a constructable expression"));
4096
4157
  }
4097
4158
  else {
4098
- text.push(ExpectDiagnosticText.argumentMustBe("source", "a constructable expression"));
4159
+ text.push(ExpectDiagnosticText.typeArgumentMustBe("Source", "a constructable type"));
4099
4160
  }
4100
4161
  if (type != null && type.getCallSignatures().length > 0) {
4101
4162
  text.push(ExpectDiagnosticText.didYouMeanToUse("the '.toBeCallableWith()' matcher"));
@@ -4137,9 +4198,9 @@ class ToHaveProperty {
4137
4198
  if (sourceType.flags & (this.#compiler.TypeFlags.Any | this.#compiler.TypeFlags.Never) ||
4138
4199
  !matchWorker.extendsObjectType(sourceType)) {
4139
4200
  const expectedText = "of an object type";
4140
- const text = this.#compiler.isTypeNode(sourceNode)
4141
- ? ExpectDiagnosticText.typeArgumentMustBe("Source", expectedText)
4142
- : ExpectDiagnosticText.argumentMustBe("source", expectedText);
4201
+ const text = nodeBelongsToArgumentList(this.#compiler, sourceNode)
4202
+ ? ExpectDiagnosticText.argumentMustBe("source", expectedText)
4203
+ : ExpectDiagnosticText.typeArgumentMustBe("Source", expectedText);
4143
4204
  const origin = DiagnosticOrigin.fromNode(sourceNode);
4144
4205
  diagnostics.push(Diagnostic.error(text, origin));
4145
4206
  }
@@ -4176,15 +4237,15 @@ class ToRaiseError {
4176
4237
  this.#compiler = compiler;
4177
4238
  }
4178
4239
  #explain(matchWorker, sourceNode, targetNodes) {
4179
- const isTypeNode = this.#compiler.isTypeNode(sourceNode);
4240
+ const isExpression = nodeBelongsToArgumentList(this.#compiler, sourceNode);
4180
4241
  const origin = DiagnosticOrigin.fromAssertion(matchWorker.assertion);
4181
4242
  if (matchWorker.assertion.diagnostics.size === 0) {
4182
- const text = ExpectDiagnosticText.didNotRaiseError(isTypeNode);
4243
+ const text = ExpectDiagnosticText.didNotRaiseError(isExpression);
4183
4244
  return [Diagnostic.error(text, origin)];
4184
4245
  }
4185
4246
  if (matchWorker.assertion.diagnostics.size !== targetNodes.length) {
4186
4247
  const count = matchWorker.assertion.diagnostics.size;
4187
- const text = ExpectDiagnosticText.raisedError(isTypeNode, count, targetNodes.length);
4248
+ const text = ExpectDiagnosticText.raisedError(isExpression, count, targetNodes.length);
4188
4249
  const related = [
4189
4250
  Diagnostic.error(ExpectDiagnosticText.raisedTypeError(count)),
4190
4251
  ...Diagnostic.fromDiagnostics([...matchWorker.assertion.diagnostics]),
@@ -4196,8 +4257,8 @@ class ToRaiseError {
4196
4257
  const isMatch = this.#matchExpectedError(diagnostic, targetNode);
4197
4258
  if (matchWorker.assertion.isNot ? isMatch : !isMatch) {
4198
4259
  const text = matchWorker.assertion.isNot
4199
- ? ExpectDiagnosticText.raisedMatchingError(isTypeNode)
4200
- : ExpectDiagnosticText.didNotRaiseMatchingError(isTypeNode);
4260
+ ? ExpectDiagnosticText.raisedMatchingError(isExpression)
4261
+ : ExpectDiagnosticText.didNotRaiseMatchingError(isExpression);
4201
4262
  const origin = DiagnosticOrigin.fromNode(targetNode, matchWorker.assertion);
4202
4263
  const related = [
4203
4264
  Diagnostic.error(ExpectDiagnosticText.raisedTypeError()),
@@ -4346,7 +4407,7 @@ class ExpectService {
4346
4407
  }
4347
4408
  #rejectsTypeArguments(matchWorker, onDiagnostics) {
4348
4409
  for (const rejectedType of this.#rejectTypes) {
4349
- const allowedKeyword = this.#compiler.SyntaxKind[`${Format.capitalize(rejectedType)}Keyword`];
4410
+ const allowedKeyword = this.#compiler.SyntaxKind[`${capitalize(rejectedType)}Keyword`];
4350
4411
  if (matchWorker.assertion.source[0]?.kind === allowedKeyword ||
4351
4412
  matchWorker.assertion.target?.[0]?.kind === allowedKeyword) {
4352
4413
  continue;
@@ -4356,11 +4417,11 @@ class ExpectService {
4356
4417
  if (!argumentNode) {
4357
4418
  continue;
4358
4419
  }
4359
- if (matchWorker.getType(argumentNode).flags & this.#compiler.TypeFlags[Format.capitalize(rejectedType)]) {
4420
+ if (matchWorker.getType(argumentNode).flags & this.#compiler.TypeFlags[capitalize(rejectedType)]) {
4360
4421
  const text = [
4361
- this.#compiler.isTypeNode(argumentNode)
4362
- ? ExpectDiagnosticText.typeArgumentCannotBeOfType(Format.capitalize(argumentName), rejectedType)
4363
- : ExpectDiagnosticText.argumentCannotBeOfType(argumentName, rejectedType),
4422
+ nodeBelongsToArgumentList(this.#compiler, argumentNode)
4423
+ ? ExpectDiagnosticText.argumentCannotBeOfType(argumentName, rejectedType)
4424
+ : ExpectDiagnosticText.typeArgumentCannotBeOfType(capitalize(argumentName), rejectedType),
4364
4425
  ...ExpectDiagnosticText.typeWasRejected(rejectedType),
4365
4426
  ];
4366
4427
  const origin = DiagnosticOrigin.fromNode(argumentNode);
@@ -4431,6 +4492,9 @@ class TestTreeWalker {
4431
4492
  case TestTreeNodeBrand.Expect:
4432
4493
  this.#visitAssertion(testNode, runMode, parentResult);
4433
4494
  break;
4495
+ case TestTreeNodeBrand.When:
4496
+ this.#visitWhen(testNode, runMode, parentResult);
4497
+ break;
4434
4498
  }
4435
4499
  }
4436
4500
  }
@@ -4523,11 +4587,36 @@ class TestTreeWalker {
4523
4587
  EventEmitter.dispatch(["test:pass", { result: testResult }]);
4524
4588
  }
4525
4589
  }
4590
+ #visitWhen(when, _runMode, _parentResult) {
4591
+ if (when.abilityDiagnostics != null && when.abilityDiagnostics.size > 0) {
4592
+ const diagnostics = [];
4593
+ for (const diagnostic of when.abilityDiagnostics) {
4594
+ if (isDiagnosticWithLocation(diagnostic)) {
4595
+ const text = getDiagnosticMessageText(diagnostic);
4596
+ let origin;
4597
+ if (isDiagnosticWithLocation(diagnostic) && diagnosticBelongsToNode(diagnostic, when.node)) {
4598
+ origin = DiagnosticOrigin.fromNodes(when.target);
4599
+ }
4600
+ else {
4601
+ origin = new DiagnosticOrigin(diagnostic.start, getTextSpanEnd(diagnostic), when.node.getSourceFile());
4602
+ }
4603
+ let related;
4604
+ if (diagnostic.relatedInformation != null) {
4605
+ related = Diagnostic.fromDiagnostics(diagnostic.relatedInformation);
4606
+ }
4607
+ diagnostics.push(Diagnostic.error(text, origin).add({ related }));
4608
+ }
4609
+ }
4610
+ EventEmitter.dispatch(["task:error", { diagnostics, result: this.#taskResult }]);
4611
+ return;
4612
+ }
4613
+ }
4526
4614
  }
4527
4615
 
4528
4616
  class TaskRunner {
4529
4617
  #collectService;
4530
4618
  #compiler;
4619
+ #eventEmitter = new EventEmitter();
4531
4620
  #resolvedConfig;
4532
4621
  #projectService;
4533
4622
  constructor(compiler, resolvedConfig) {
@@ -4536,18 +4625,18 @@ class TaskRunner {
4536
4625
  this.#projectService = new ProjectService(compiler, this.#resolvedConfig);
4537
4626
  this.#collectService = new CollectService(compiler, this.#projectService, this.#resolvedConfig);
4538
4627
  }
4539
- run(task, cancellationToken) {
4540
- if (cancellationToken?.isCancellationRequested) {
4628
+ async run(task, cancellationToken) {
4629
+ if (cancellationToken.isCancellationRequested) {
4541
4630
  return;
4542
4631
  }
4543
4632
  this.#projectService.openFile(task.filePath, undefined, this.#resolvedConfig.rootPath);
4544
4633
  const taskResult = new TaskResult(task);
4545
4634
  EventEmitter.dispatch(["task:start", { result: taskResult }]);
4546
- this.#run(task, taskResult, cancellationToken);
4635
+ await this.#run(task, taskResult, cancellationToken);
4547
4636
  EventEmitter.dispatch(["task:end", { result: taskResult }]);
4548
4637
  this.#projectService.closeFile(task.filePath);
4549
4638
  }
4550
- #run(task, taskResult, cancellationToken) {
4639
+ async #run(task, taskResult, cancellationToken) {
4551
4640
  if (!existsSync(task.filePath)) {
4552
4641
  EventEmitter.dispatch([
4553
4642
  "task:error",
@@ -4555,28 +4644,51 @@ class TaskRunner {
4555
4644
  ]);
4556
4645
  return;
4557
4646
  }
4558
- const languageService = this.#projectService.getLanguageService(task.filePath);
4559
- if (!languageService) {
4560
- return;
4561
- }
4562
- const syntacticDiagnostics = languageService.getSyntacticDiagnostics(task.filePath);
4563
- if (syntacticDiagnostics.length > 0) {
4647
+ let languageService = this.#projectService.getLanguageService(task.filePath);
4648
+ const syntacticDiagnostics = languageService?.getSyntacticDiagnostics(task.filePath);
4649
+ if (syntacticDiagnostics != null && syntacticDiagnostics.length > 0) {
4564
4650
  EventEmitter.dispatch([
4565
4651
  "task:error",
4566
4652
  { diagnostics: Diagnostic.fromDiagnostics(syntacticDiagnostics), result: taskResult },
4567
4653
  ]);
4568
4654
  return;
4569
4655
  }
4570
- const semanticDiagnostics = languageService.getSemanticDiagnostics(task.filePath);
4571
- const program = languageService.getProgram();
4572
- if (!program) {
4573
- return;
4656
+ let semanticDiagnostics = languageService?.getSemanticDiagnostics(task.filePath);
4657
+ let program = languageService?.getProgram();
4658
+ let sourceFile = program?.getSourceFile(task.filePath);
4659
+ if (sourceFile?.text.startsWith("// @tstyche-template")) {
4660
+ if (semanticDiagnostics != null && semanticDiagnostics.length > 0) {
4661
+ EventEmitter.dispatch([
4662
+ "task:error",
4663
+ { diagnostics: Diagnostic.fromDiagnostics(semanticDiagnostics), result: taskResult },
4664
+ ]);
4665
+ return;
4666
+ }
4667
+ const text = (await import(task.filePath)).default;
4668
+ this.#projectService.openFile(task.filePath, text, this.#resolvedConfig.rootPath);
4669
+ languageService = this.#projectService.getLanguageService(task.filePath);
4670
+ const syntacticDiagnostics = languageService?.getSyntacticDiagnostics(task.filePath);
4671
+ if (syntacticDiagnostics != null && syntacticDiagnostics.length > 0) {
4672
+ EventEmitter.dispatch([
4673
+ "task:error",
4674
+ { diagnostics: Diagnostic.fromDiagnostics(syntacticDiagnostics), result: taskResult },
4675
+ ]);
4676
+ return;
4677
+ }
4678
+ semanticDiagnostics = languageService?.getSemanticDiagnostics(task.filePath);
4679
+ program = languageService?.getProgram();
4680
+ sourceFile = program?.getSourceFile(task.filePath);
4574
4681
  }
4575
- const sourceFile = program.getSourceFile(task.filePath);
4576
4682
  if (!sourceFile) {
4577
4683
  return;
4578
4684
  }
4685
+ const cancellationHandler = new CancellationHandler(cancellationToken, CancellationReason.CollectError);
4686
+ this.#eventEmitter.addHandler(cancellationHandler);
4579
4687
  const testTree = this.#collectService.createTestTree(sourceFile, semanticDiagnostics);
4688
+ this.#eventEmitter.removeHandler(cancellationHandler);
4689
+ if (cancellationToken.isCancellationRequested) {
4690
+ return;
4691
+ }
4580
4692
  if (testTree.diagnostics.size > 0) {
4581
4693
  EventEmitter.dispatch([
4582
4694
  "task:error",
@@ -4584,7 +4696,7 @@ class TaskRunner {
4584
4696
  ]);
4585
4697
  return;
4586
4698
  }
4587
- const typeChecker = program.getTypeChecker();
4699
+ const typeChecker = program?.getTypeChecker();
4588
4700
  const testTreeWalker = new TestTreeWalker(this.#compiler, typeChecker, this.#resolvedConfig, {
4589
4701
  cancellationToken,
4590
4702
  taskResult,
@@ -4598,15 +4710,13 @@ class TaskRunner {
4598
4710
  class Runner {
4599
4711
  #eventEmitter = new EventEmitter();
4600
4712
  #resolvedConfig;
4601
- static version = "4.0.0-beta.5";
4713
+ static version = "4.0.0-beta.7";
4602
4714
  constructor(resolvedConfig) {
4603
4715
  this.#resolvedConfig = resolvedConfig;
4604
4716
  }
4605
4717
  #addHandlers(cancellationToken) {
4606
4718
  const resultHandler = new ResultHandler();
4607
4719
  this.#eventEmitter.addHandler(resultHandler);
4608
- const testTreeHandler = new TestTreeHandler();
4609
- this.#eventEmitter.addHandler(testTreeHandler);
4610
4720
  if (this.#resolvedConfig.failFast) {
4611
4721
  const cancellationHandler = new CancellationHandler(cancellationToken, CancellationReason.FailFast);
4612
4722
  this.#eventEmitter.addHandler(cancellationHandler);
@@ -4649,7 +4759,7 @@ class Runner {
4649
4759
  this.#eventEmitter.removeHandlers();
4650
4760
  }
4651
4761
  async #run(tasks, cancellationToken) {
4652
- const result = new Result(this.#resolvedConfig, tasks);
4762
+ const result = new Result(tasks);
4653
4763
  EventEmitter.dispatch(["run:start", { result }]);
4654
4764
  for (const target of this.#resolvedConfig.target) {
4655
4765
  const targetResult = new TargetResult(target, tasks);
@@ -4658,7 +4768,7 @@ class Runner {
4658
4768
  if (compiler) {
4659
4769
  const taskRunner = new TaskRunner(compiler, this.#resolvedConfig);
4660
4770
  for (const task of tasks) {
4661
- taskRunner.run(task, cancellationToken);
4771
+ await taskRunner.run(task, cancellationToken);
4662
4772
  }
4663
4773
  }
4664
4774
  EventEmitter.dispatch(["target:end", { result: targetResult }]);
@@ -4800,4 +4910,4 @@ class Cli {
4800
4910
  }
4801
4911
  }
4802
4912
 
4803
- export { AssertionNode, BaseReporter, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, Config, ConfigDiagnosticText, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, EventEmitter, ExitCodeHandler, ExpectResult, ExpectService, FileWatcher, InputService, Line, ListReporter, OptionBrand, OptionGroup, Options, OutputService, Path, PluginService, ProjectResult, ProjectService, Result, ResultCount, ResultHandler, ResultStatus, ResultTiming, Runner, Scribbler, Select, SelectDiagnosticText, SetupReporter, SourceFile, Store, SummaryReporter, TargetResult, Task, TaskResult, TestResult, TestTree, TestTreeHandler, TestTreeNode, TestTreeNodeBrand, TestTreeNodeFlags, Text, Version, WatchReporter, WatchService, Watcher, addsPackageText, defaultOptions, describeNameText, diagnosticBelongsToNode, diagnosticText, environmentOptions, fileViewText, formattedText, getDiagnosticMessageText, getTextSpanEnd, helpText, isDiagnosticWithLocation, summaryText, taskStatusText, testNameText, usesCompilerText, waitingForFileChangesText, watchUsageText };
4913
+ export { AssertionNode, BaseReporter, CancellationHandler, CancellationReason, CancellationToken, Cli, CollectService, Color, Config, ConfigDiagnosticText, DescribeResult, Diagnostic, DiagnosticCategory, DiagnosticOrigin, EventEmitter, ExitCodeHandler, ExpectResult, ExpectService, FileWatcher, InputService, Line, ListReporter, OptionBrand, OptionGroup, Options, OutputService, Path, PluginService, ProjectResult, ProjectService, Result, ResultCount, ResultHandler, ResultStatus, ResultTiming, Runner, Scribbler, Select, SelectDiagnosticText, SetupReporter, SourceFile, Store, SummaryReporter, TargetResult, Task, TaskResult, TestResult, TestTree, TestTreeNode, TestTreeNodeBrand, TestTreeNodeFlags, Text, Version, WatchReporter, WatchService, Watcher, WhenNode, addsPackageText, defaultOptions, describeNameText, diagnosticBelongsToNode, diagnosticText, environmentOptions, fileViewText, formattedText, getDiagnosticMessageText, getTextSpanEnd, helpText, isDiagnosticWithLocation, nodeBelongsToArgumentList, summaryText, taskStatusText, testNameText, usesCompilerText, waitingForFileChangesText, watchUsageText };