UNPKG

testplane

Version:

Tests framework based on mocha and wdio

307 lines 13.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.instrumentWithTypeScript = void 0; const node_path_1 = __importDefault(require("node:path")); const repl_1 = require("../../constants/repl"); const instrumentWithTypeScript = (ts, code, sourceFileName, options) => { const programContext = createProgramContext(ts, code, sourceFileName); if (!programContext) { return code; } let changed = false; const transformer = transformContext => { const visit = node => { if (ts.isCallExpression(node)) { const testCall = options.beforeTest ? instrumentTestCall(ts, programContext, transformContext, visit, node) : null; if (testCall) { changed = true; return testCall; } const switchToReplCall = instrumentSwitchToReplCall(ts, programContext, transformContext, visit, node); if (switchToReplCall) { changed = true; return switchToReplCall; } } return ts.visitEachChild(node, visit, transformContext); }; return sourceFile => ts.visitNode(sourceFile, visit); }; const result = ts.transform(programContext.sourceFile, [transformer]); try { if (!changed) { return code; } const printer = ts.createPrinter(); return printer.printFile(result.transformed[0]); } finally { result.dispose(); } }; exports.instrumentWithTypeScript = instrumentWithTypeScript; function createProgramContext(ts, code, sourceFileName) { const normalizedSourceFileName = node_path_1.default.resolve(sourceFileName); const compilerOptions = { allowJs: true, allowNonTsExtensions: true, checkJs: false, module: ts.ModuleKind.CommonJS, noResolve: true, skipLibCheck: true, target: ts.ScriptTarget.ESNext, }; const host = ts.createCompilerHost(compilerOptions, true); const scriptKind = getScriptKind(ts, normalizedSourceFileName); const sourceFile = ts.createSourceFile(normalizedSourceFileName, code, ts.ScriptTarget.ESNext, true, scriptKind); const isTargetFile = (fileName) => node_path_1.default.resolve(fileName) === normalizedSourceFileName; host.getSourceFile = (fileName) => { return isTargetFile(fileName) ? sourceFile : undefined; }; host.fileExists = isTargetFile; host.readFile = (fileName) => (isTargetFile(fileName) ? code : undefined); const program = ts.createProgram([normalizedSourceFileName], compilerOptions, host); const programSourceFile = program.getSourceFile(normalizedSourceFileName); if (!programSourceFile) { return null; } return { checker: program.getTypeChecker(), sourceFile: programSourceFile, }; } function getScriptKind(ts, sourceFileName) { switch (node_path_1.default.extname(sourceFileName)) { case ".ts": case ".mts": case ".cts": return ts.ScriptKind.TS; case ".tsx": return ts.ScriptKind.TSX; case ".jsx": return ts.ScriptKind.JSX; default: return ts.ScriptKind.JS; } } function instrumentSwitchToReplCall(ts, programContext, transformContext, visit, node) { if (!isSwitchToReplCall(ts, node)) { return null; } const visitedNode = ts.visitEachChild(node, visit, transformContext); const replContext = createReplContextObject(ts, programContext, node, node.getStart(programContext.sourceFile)); // browser.switchToRepl(userCtx) -> browser.switchToRepl(userCtx, generatedLexicalCtx) if (!replContext) { return null; } return ts.factory.updateCallExpression(visitedNode, visitedNode.expression, visitedNode.typeArguments, [ ...visitedNode.arguments, replContext, ]); } function instrumentTestCall(ts, programContext, transformContext, visit, node) { const callbackIndex = getTestCallbackIndex(ts, node); if (callbackIndex === null) { return null; } const callback = node.arguments[callbackIndex]; const browserExpression = getBrowserExpression(ts, callback); if (!browserExpression) { return null; } const visitedCallback = ts.visitEachChild(callback, visit, transformContext); const replContext = createReplContextObject(ts, programContext, callback.body, callback.body.getStart(programContext.sourceFile)); const injectedCallback = injectBeforeTestReplCall(ts, visitedCallback, browserExpression, replContext); const markedCallback = markCallbackAsInstrumented(ts, injectedCallback); const args = [...node.arguments]; args[callbackIndex] = markedCallback; return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, args); } function isSwitchToReplCall(ts, node) { return ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "switchToRepl"; } function getTestCallbackIndex(ts, node) { if (!isTestCall(ts, node.expression)) { return null; } for (let i = node.arguments.length - 1; i >= 0; i--) { const arg = node.arguments[i]; if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) { return i; } } return null; } function isTestCall(ts, expression) { if (ts.isIdentifier(expression)) { return expression.text === "it"; } return ts.isPropertyAccessExpression(expression) && isTestCall(ts, expression.expression); } function getBrowserExpression(ts, callback) { for (const param of callback.parameters) { if (ts.isIdentifier(param.name)) { return ts.factory.createPropertyAccessExpression(param.name, "browser"); } if (ts.isObjectBindingPattern(param.name)) { const browserName = getBrowserBindingName(ts, param.name); if (browserName) { return browserName; } } } return ts.isFunctionExpression(callback) ? ts.factory.createPropertyAccessExpression(ts.factory.createThis(), "browser") : null; } function getBrowserBindingName(ts, bindingPattern) { for (const element of bindingPattern.elements) { const propertyName = element.propertyName; const isBrowserProperty = (propertyName && ts.isIdentifier(propertyName) && propertyName.text === "browser") || (!propertyName && ts.isIdentifier(element.name) && element.name.text === "browser"); if (isBrowserProperty && ts.isIdentifier(element.name)) { return element.name; } } return null; } function injectBeforeTestReplCall(ts, callback, browserExpression, replContext) { // it("x", async ({ browser }) => { body }) -> it("x", async ({ browser }) => { await browser.switchToRepl(replContext); body }) const switchToReplArgs = replContext ? [replContext] : []; const switchToRepl = ts.factory.createAwaitExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(browserExpression, "switchToRepl"), undefined, switchToReplArgs)); const injectedStatement = ts.factory.createExpressionStatement(switchToRepl); const body = ts.isBlock(callback.body) ? ts.factory.updateBlock(callback.body, [injectedStatement, ...callback.body.statements]) : ts.factory.createBlock([injectedStatement, ts.factory.createReturnStatement(callback.body)], true); const modifiers = ensureAsyncModifier(ts, callback.modifiers); return ts.isArrowFunction(callback) ? ts.factory.updateArrowFunction(callback, modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, body) : ts.factory.updateFunctionExpression(callback, modifiers, callback.asteriskToken, callback.name, callback.typeParameters, callback.parameters, callback.type, body); } function ensureAsyncModifier(ts, modifiers) { const existingModifiers = (modifiers || []).filter(modifier => modifier.kind !== ts.SyntaxKind.Decorator); if (existingModifiers.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) { return ts.factory.createNodeArray(existingModifiers); } return ts.factory.createNodeArray([ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword), ...existingModifiers]); } function markCallbackAsInstrumented(ts, callback) { // The runner checks this symbol to avoid opening a second before-test REPL. // Roughly, converts it("x", async ({ browser }) => { body }) to it("x", Object.assign(async ({ browser }) => { body }, { [Symbol.for("...")]: true })); return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("Object"), "assign"), undefined, [ callback, ts.factory.createObjectLiteralExpression([ ts.factory.createPropertyAssignment(ts.factory.createComputedPropertyName(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("Symbol"), "for"), undefined, [ts.factory.createStringLiteral(repl_1.REPL_INSTRUMENTED_FN_FLAG_KEY)])), ts.factory.createTrue()), ]), ]); } function createReplContextObject(ts, { checker, sourceFile }, node, position) { const symbols = checker.getSymbolsInScope(node, ts.SymbolFlags.Value); const accessors = []; const seenNames = new Set(); const addAccessor = (name, canWrite) => { if (seenNames.has(name) || name.startsWith("__testplane")) { return; } seenNames.add(name); accessors.push(createGetter(ts, name)); if (canWrite) { accessors.push(createSetter(ts, name)); } }; // Import bindings are alias symbols, so getSymbolsInScope(...Value) does not cover them for (const name of collectImportBindingNames(ts, sourceFile)) { addAccessor(name, false); } for (const symbol of symbols) { const name = symbol.getName(); if (!shouldExposeSymbol(sourceFile, symbol, name, position)) { continue; } addAccessor(name, canWriteSymbol(ts, sourceFile, symbol)); } return accessors.length > 0 ? ts.factory.createObjectLiteralExpression(accessors, true) : null; } function collectImportBindingNames(ts, sourceFile) { const names = []; for (const statement of sourceFile.statements) { if (ts.isImportDeclaration(statement)) { names.push(...collectImportDeclarationBindingNames(ts, statement)); continue; } if (ts.isImportEqualsDeclaration(statement) && !statement.isTypeOnly) { names.push(statement.name.text); } } return names; } function collectImportDeclarationBindingNames(ts, declaration) { const importClause = declaration.importClause; const names = []; if (!importClause || importClause.isTypeOnly) { return names; } if (importClause.name) { names.push(importClause.name.text); } if (!importClause.namedBindings) { return names; } if (ts.isNamespaceImport(importClause.namedBindings)) { names.push(importClause.namedBindings.name.text); return names; } for (const element of importClause.namedBindings.elements) { if (!element.isTypeOnly) { names.push(element.name.text); } } return names; } function shouldExposeSymbol(sourceFile, symbol, name, position) { if (name === "arguments") { return false; } return (symbol.getDeclarations() || []).some(declaration => { return declaration.getSourceFile() === sourceFile && declaration.getStart(sourceFile) <= position; }); } function canWriteSymbol(ts, sourceFile, symbol) { return (symbol.getDeclarations() || []).some(declaration => { const variableDeclaration = findVariableDeclaration(ts, declaration); return (variableDeclaration?.getSourceFile() === sourceFile && isWritableVariableDeclaration(ts, variableDeclaration)); }); } function isWritableVariableDeclaration(ts, declaration) { const declarationList = declaration.parent; // eslint-disable-next-line no-bitwise return ts.isVariableDeclarationList(declarationList) && (declarationList.flags & ts.NodeFlags.Const) === 0; } function findVariableDeclaration(ts, node) { let current = node; while (current) { if (ts.isVariableDeclaration(current)) { return current; } current = current.parent; } return null; } function createGetter(ts, name) { // get localValue() { return localValue; } keeps REPL reads linked to the lexical binding. return ts.factory.createGetAccessorDeclaration(undefined, name, [], undefined, ts.factory.createBlock([ts.factory.createReturnStatement(ts.factory.createIdentifier(name))], true)); } function createSetter(ts, name) { const value = ts.factory.createIdentifier("__testplaneReplValue"); // set localValue(v) { localValue = v; } lets REPL writes update writable lexical bindings. return ts.factory.createSetAccessorDeclaration(undefined, name, [ts.factory.createParameterDeclaration(undefined, undefined, value)], ts.factory.createBlock([ ts.factory.createExpressionStatement(ts.factory.createAssignment(ts.factory.createIdentifier(name), value)), ], true)); } //# sourceMappingURL=typescript.js.map