├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── .node-version ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── create-echoed ├── .eslintrc.js ├── .prettierignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── scripts │ └── check-otel-demo.js ├── src │ ├── ansi.ts │ ├── bin.ts │ └── creator.ts ├── template │ ├── base │ │ ├── .echoed.yml │ │ ├── .prettierignore │ │ ├── .prettierrc │ │ ├── example │ │ │ ├── Makefile │ │ │ ├── hack │ │ │ │ └── wait_demo_up.sh │ │ │ └── opentelemetry-demo-override │ │ │ │ ├── .env │ │ │ │ ├── .gitignore │ │ │ │ ├── docker-compose.yml │ │ │ │ └── src │ │ │ │ ├── checkout │ │ │ │ └── main.go │ │ │ │ ├── frontend │ │ │ │ ├── schema.yaml │ │ │ │ └── utils │ │ │ │ │ └── Cypress.ts │ │ │ │ └── otel-collector │ │ │ │ ├── otelcol-config-extras.yml │ │ │ │ └── otelcol-config.yml │ │ └── tsconfig.json │ ├── cypress │ │ ├── .instruction │ │ │ └── copy.json │ │ ├── README.md │ │ ├── _gitignore │ │ ├── cypress.config.ts │ │ ├── cypress │ │ │ ├── e2e │ │ │ │ └── example │ │ │ │ │ ├── advanced.cy.ts │ │ │ │ │ ├── checkout.cy.ts │ │ │ │ │ ├── home.cy.ts │ │ │ │ │ └── productDetail.cy.ts │ │ │ └── support │ │ │ │ ├── commands.js │ │ │ │ └── e2e.js │ │ ├── example │ │ │ ├── .echoed.yml │ │ │ ├── README.md │ │ │ └── util │ │ │ │ ├── const.ts │ │ │ │ └── session.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── jest-no-otel │ │ ├── .instruction │ │ │ └── copy.json │ │ ├── README.md │ │ ├── example │ │ │ ├── .echoed.yml │ │ │ ├── README.md │ │ │ ├── scenario │ │ │ │ ├── advancedExamples.yml │ │ │ │ ├── cart.yml │ │ │ │ ├── checkout.yml │ │ │ │ ├── product.yml │ │ │ │ ├── products.yml │ │ │ │ └── recommendation.yml │ │ │ └── test │ │ │ │ └── manual.test.ts │ │ └── jest.config.js │ ├── jest │ │ ├── .gitignore │ │ ├── .instruction │ │ │ └── copy.json │ │ ├── .prettierrc │ │ ├── README.md │ │ ├── _gitignore │ │ ├── example │ │ │ ├── .echoed.yml │ │ │ ├── README.md │ │ │ ├── scenario │ │ │ │ ├── advancedExamples.yml │ │ │ │ ├── cart.yml │ │ │ │ ├── checkout.yml │ │ │ │ ├── product.yml │ │ │ │ ├── products.yml │ │ │ │ └── recommendation.yml │ │ │ ├── test │ │ │ │ └── manual.test.ts │ │ │ └── util │ │ │ │ ├── assertItemLength.ts │ │ │ │ ├── cartRedis.ts │ │ │ │ └── session.ts │ │ ├── jest.config.js │ │ └── package.json │ ├── playwright-no-otel │ │ ├── .instruction │ │ │ └── copy.json │ │ ├── README.md │ │ ├── example │ │ │ ├── .echoed.yml │ │ │ ├── README.md │ │ │ └── test │ │ │ │ ├── advanced.test.ts │ │ │ │ └── scenario │ │ │ │ ├── checkout.yml │ │ │ │ ├── home.yml │ │ │ │ └── productDetail.yml │ │ └── playwright.config.ts │ └── playwright │ │ ├── .instruction │ │ └── copy.json │ │ ├── README.md │ │ ├── _gitignore │ │ ├── example │ │ ├── .echoed.yml │ │ ├── README.md │ │ ├── fixtures │ │ │ └── test.ts │ │ ├── test │ │ │ ├── advanced.test.ts │ │ │ └── scenario │ │ │ │ ├── checkout.yml │ │ │ │ ├── home.yml │ │ │ │ └── productDetail.yml │ │ └── util │ │ │ └── session.ts │ │ ├── package.json │ │ └── playwright.config.ts ├── tsconfig.json └── tsup.config.ts ├── cypressReporter.js ├── docs ├── configuration.md ├── howToUse.md ├── img │ ├── howEchoedWorks.png │ ├── logo.svg │ ├── readmeCoverage.png │ ├── readmeTraceDetailLog.png │ ├── readmeTraceDetailTrace.png │ ├── scenarioYamlCompile.jpg │ └── scenarioYamlToPlaywright.png ├── installation.md ├── scenario.md ├── yamlJestScenario.md └── yamlPlaywrightScenario.md ├── jest.config.js ├── package-lock.json ├── package.json ├── reporter ├── .gitignore ├── .prettierrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.svelte │ ├── app.css │ ├── components │ │ ├── breadcrumb │ │ │ └── Breadcrumb.svelte │ │ ├── data_table │ │ │ └── PagingDataTable.svelte │ │ ├── header │ │ │ └── Header.svelte │ │ ├── list │ │ │ └── ListItem.svelte │ │ ├── status_icons │ │ │ ├── BlockedIcon.svelte │ │ │ ├── FailedIcon.svelte │ │ │ └── SucceededIcon.svelte │ │ ├── trace │ │ │ ├── AttributeDataTable.svelte │ │ │ ├── LogAccordion.svelte │ │ │ ├── SpanDetail.svelte │ │ │ ├── TraceTree.svelte │ │ │ └── TraceWithLog.svelte │ │ └── trace_table │ │ │ └── TraceTable.svelte │ ├── consts │ │ ├── echoedParam.ts │ │ └── testNames.ts │ ├── dummy.ts │ ├── lib │ │ ├── EchoedParam.ts │ │ ├── MemorizedColorSelector.ts │ │ └── util │ │ │ └── http.ts │ ├── main.ts │ ├── pages │ │ ├── NotFound.svelte │ │ ├── coverage │ │ │ ├── Coverage.svelte │ │ │ ├── CoveragePage.svelte │ │ │ ├── CoverageTable.svelte │ │ │ ├── UnmeasuredServiceTable.svelte │ │ │ ├── service │ │ │ │ ├── CoverageService.svelte │ │ │ │ ├── CoverageServicePage.svelte │ │ │ │ ├── HttpCoverageTable.svelte │ │ │ │ ├── HttpUndocumentedOperationTable.svelte │ │ │ │ ├── RpcCoverageTable.svelte │ │ │ │ ├── RpcUndocumentedMethodTable.svelte │ │ │ │ └── undocumented │ │ │ │ │ ├── http │ │ │ │ │ └── operation │ │ │ │ │ │ ├── UndocumentedHttpOperation.svelte │ │ │ │ │ │ ├── UndocumentedHttpOperationPage.svelte │ │ │ │ │ │ └── trace │ │ │ │ │ │ ├── UndocumentedHttpTrace.svelte │ │ │ │ │ │ └── UndocumentedHttpTracePage.svelte │ │ │ │ │ └── rpc │ │ │ │ │ └── method │ │ │ │ │ ├── UndocumentedRpcMethod.svelte │ │ │ │ │ ├── UndocumentedRpcMethodPage.svelte │ │ │ │ │ └── trace │ │ │ │ │ ├── UndocumentedRpcMethodTrace.svelte │ │ │ │ │ └── UndocumentedRpcMethodTracePage.svelte │ │ │ └── unmeasured │ │ │ │ ├── CoverageUnmeasured.svelte │ │ │ │ ├── CoverageUnmeasuredPage.svelte │ │ │ │ └── trace │ │ │ │ ├── CoverageUnmeasuredTrace.svelte │ │ │ │ └── CoverageUnmeasuredTracePage.svelte │ │ ├── propagation_test │ │ │ ├── Description.svelte │ │ │ ├── PropagationTest.svelte │ │ │ ├── PropagationTestPage.svelte │ │ │ ├── TraceList.svelte │ │ │ └── unpropagated │ │ │ │ └── trace │ │ │ │ ├── Trace.svelte │ │ │ │ └── UnpropagatedTracePage.svelte │ │ ├── test │ │ │ ├── TestDescription.svelte │ │ │ ├── TestDetail.svelte │ │ │ ├── TestDetailPage.svelte │ │ │ ├── TraceList.svelte │ │ │ └── trace │ │ │ │ ├── TracePage.svelte │ │ │ │ └── TraceView.svelte │ │ └── test_list │ │ │ ├── OtherTestsList.svelte │ │ │ ├── TestListInSameFile.svelte │ │ │ └── TestListPage.svelte │ ├── routes.ts │ ├── types │ │ └── window.d.ts │ └── vite-env.d.ts ├── svelte.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── shared └── type │ ├── echoedParam.ts │ └── http.ts ├── src ├── cli │ └── bin.ts ├── command │ ├── bridge │ │ ├── compare.ts │ │ ├── span.test.ts │ │ └── span.ts │ ├── compare.ts │ ├── index.ts │ ├── span.test.ts │ ├── span.ts │ └── spanType.ts ├── comparision │ ├── comparable.ts │ ├── eq.test.ts │ ├── eq.ts │ ├── gt.test.ts │ ├── gt.ts │ ├── gte.test.ts │ ├── gte.ts │ ├── kind.ts │ ├── lt.test.ts │ ├── lt.ts │ ├── lte.test.ts │ ├── lte.ts │ ├── numComparable.ts │ ├── reg.test.ts │ ├── reg.ts │ ├── restore.test.ts │ └── restore.ts ├── config │ ├── config.ts │ ├── configLoader.test.ts │ ├── configLoader.ts │ ├── invalidConfigError.ts │ ├── propagationTestConfig.ts │ ├── scenarioCompileConfig.ts │ ├── scenarioCompileTargetConfig.ts │ └── util │ │ ├── mapper.test.ts │ │ └── mapper.ts ├── coverage │ ├── coverageCollector.test.ts │ ├── coverageCollector.ts │ ├── coverageResult.ts │ ├── openApi │ │ ├── __fixtures__ │ │ │ └── simple.yaml │ │ ├── openApiCoverageCollector.test.ts │ │ ├── openApiCoverageCollector.ts │ │ ├── operation.ts │ │ ├── operationNode.ts │ │ ├── operationTree.test.ts │ │ ├── operationTree.ts │ │ ├── spanCollector.test.ts │ │ └── spanCollector.ts │ ├── proto │ │ ├── __fixtures__ │ │ │ └── simple.proto │ │ ├── method.ts │ │ ├── protoCoverageCollector.test.ts │ │ ├── protoCoverageCollector.ts │ │ ├── service.test.ts │ │ ├── service.ts │ │ ├── spanCollector.test.ts │ │ └── spanCollector.ts │ ├── serviceCoverageCollector.ts │ └── unmeasuredTraceCollector.ts ├── echoedError.ts ├── echoedFatalError.ts ├── eventBus │ ├── infra │ │ ├── eventBus.ts │ │ ├── memoryBus.test.ts │ │ ├── memoryBus.ts │ │ └── timeoutError.ts │ ├── parameter.ts │ ├── spanBus.test.ts │ ├── spanBus.ts │ └── waitForSpanRequest.ts ├── fileLog │ ├── fileLogger.ts │ ├── iFileLogger.ts │ ├── testActionLogger.test.ts │ └── testActionLogger.ts ├── fileSpace │ ├── fileSpace.ts │ └── otelDirectory.ts ├── fs │ ├── IFile.ts │ ├── fsContainer.ts │ ├── iDirectory.ts │ ├── localDirectory.test.ts │ ├── localDirectory.ts │ ├── localFile.test.ts │ └── localFile.ts ├── generated │ ├── otelpbj.d.ts │ └── otelpbj.js ├── index.ts ├── integration │ ├── common │ │ ├── commonFetchRunner.test.ts │ │ ├── commonFetchRunner.ts │ │ ├── traceHistory.test.ts │ │ ├── traceHistory.ts │ │ └── util │ │ │ ├── config.ts │ │ │ ├── env.ts │ │ │ ├── error.ts │ │ │ ├── fetchResponse.ts │ │ │ ├── reporter.ts │ │ │ ├── response.test.ts │ │ │ └── response.ts │ ├── cypress │ │ ├── internal │ │ │ ├── command │ │ │ │ ├── requestCommander.test.ts │ │ │ │ ├── requestCommander.ts │ │ │ │ ├── waitForSpanCommander.test.ts │ │ │ │ └── waitForSpanCommander.ts │ │ │ ├── cypressHttpMessage.ts │ │ │ ├── cypressHttpRequest.ts │ │ │ ├── cypressHttpResponse.ts │ │ │ ├── infra │ │ │ │ ├── cypressFileLogger.ts │ │ │ │ ├── cypressObj.ts │ │ │ │ ├── cypressRequester.ts │ │ │ │ └── iCypressObj.ts │ │ │ ├── requestRunner.test.ts │ │ │ ├── requestRunner.ts │ │ │ ├── runInfo.test.ts │ │ │ ├── runInfo.ts │ │ │ ├── specHook.ts │ │ │ └── util │ │ │ │ ├── cypressRequest.test.ts │ │ │ │ ├── cypressRequest.ts │ │ │ │ ├── cypressResponse.test.ts │ │ │ │ ├── cypressResponse.ts │ │ │ │ ├── cypressSpec.ts │ │ │ │ ├── env.test.ts │ │ │ │ ├── env.ts │ │ │ │ ├── fileSpace.ts │ │ │ │ ├── headers.test.ts │ │ │ │ ├── headers.ts │ │ │ │ ├── promisify.ts │ │ │ │ ├── request.test.ts │ │ │ │ └── request.ts │ │ ├── nodeEvents │ │ │ ├── eventListener.test.ts │ │ │ ├── eventListener.ts │ │ │ ├── index.ts │ │ │ └── nodeEvents.ts │ │ ├── reporter │ │ │ ├── cypressReporter.ts │ │ │ ├── index.ts │ │ │ └── reporter.ts │ │ └── support │ │ │ ├── command.ts │ │ │ ├── command │ │ │ ├── request.ts │ │ │ └── waitForSpan.ts │ │ │ └── index.ts │ ├── jest │ │ ├── internal │ │ │ ├── fetchRunner.test.ts │ │ │ ├── fetchRunner.ts │ │ │ └── util │ │ │ │ ├── fetchPatch.test.ts │ │ │ │ └── fetchPatch.ts │ │ ├── nodeEnvironment │ │ │ ├── environment.test.ts │ │ │ ├── environment.ts │ │ │ ├── index.ts │ │ │ └── jestNodeEnvironment.ts │ │ └── reporter │ │ │ ├── index.ts │ │ │ ├── jestReporter.ts │ │ │ ├── reporter.test.ts │ │ │ ├── reporter.ts │ │ │ └── testCase.ts │ └── playwright │ │ ├── command │ │ ├── index.ts │ │ ├── span.test.ts │ │ └── span.ts │ │ ├── globalSetup │ │ ├── globalSetup.ts │ │ ├── index.ts │ │ ├── setupRunner.test.ts │ │ └── setupRunner.ts │ │ ├── index.ts │ │ ├── internal │ │ ├── hook │ │ │ ├── apiRequestProxyFetchRunner.test.ts │ │ │ ├── apiRequestProxyFetchRunner.ts │ │ │ ├── globalFetchRunner.test.ts │ │ │ ├── globalFetchRunner.ts │ │ │ ├── hook.test.ts │ │ │ ├── hook.ts │ │ │ ├── routeFetchRunner.test.ts │ │ │ └── routeFetchRunner.ts │ │ ├── type.ts │ │ └── util │ │ │ ├── apiResponse.test.ts │ │ │ ├── apiResponse.ts │ │ │ ├── browserContext.test.ts │ │ │ ├── browserContext.ts │ │ │ └── fileSpace.ts │ │ ├── reporter │ │ ├── index.ts │ │ ├── playwrightReporter.ts │ │ ├── reporter.test.ts │ │ └── reporter.ts │ │ └── test │ │ ├── fixture.ts │ │ ├── index.ts │ │ └── wrapper │ │ ├── fixture.ts │ │ ├── index.ts │ │ └── testExtender.ts ├── logger.ts ├── report │ ├── fetchInfo.ts │ ├── iReportFile.ts │ ├── otelAnyValueConverter.ts │ ├── otelLogRecordConverter.ts │ ├── otelSpanConverter.ts │ ├── reportFile.ts │ ├── testCaseResult.ts │ ├── testFailedError.ts │ └── testResult.ts ├── scenario │ ├── compile │ │ ├── common │ │ │ ├── act.test.ts │ │ │ ├── act.ts │ │ │ ├── actRunner.ts │ │ │ ├── arrange.test.ts │ │ │ ├── arrange.ts │ │ │ ├── arrangeRunner.test.ts │ │ │ ├── arrangeRunner.ts │ │ │ ├── assert.test.ts │ │ │ ├── assert.ts │ │ │ ├── assertConfig.test.ts │ │ │ ├── asserter.test.ts │ │ │ ├── asserter.ts │ │ │ ├── asserterConfig.ts │ │ │ ├── commonPluginConfig.test.ts │ │ │ ├── commonPluginConfig.ts │ │ │ ├── config.test.ts │ │ │ ├── config.ts │ │ │ ├── envConfig.test.ts │ │ │ ├── envConfig.ts │ │ │ ├── hookBase.test.ts │ │ │ ├── hookBase.ts │ │ │ ├── hookExecutorBase.test.ts │ │ │ ├── hookExecutorBase.ts │ │ │ ├── invalidScenarioError.ts │ │ │ ├── pluginConfig.test.ts │ │ │ ├── pluginConfig.ts │ │ │ ├── pluginLister.ts │ │ │ ├── rawString.test.ts │ │ │ ├── rawString.ts │ │ │ ├── runnerConfig.test.ts │ │ │ ├── runnerConfig.ts │ │ │ ├── runnerContainer.test.ts │ │ │ ├── runnerContainer.ts │ │ │ ├── runnerOption.test.ts │ │ │ ├── runnerOption.ts │ │ │ ├── scenarioBase.test.ts │ │ │ ├── scenarioBase.ts │ │ │ ├── scenarioBookBase.test.ts │ │ │ ├── scenarioBookBase.ts │ │ │ ├── scenarioRunnerConfig.ts │ │ │ ├── stepBase.test.ts │ │ │ ├── stepBase.ts │ │ │ ├── templateString.test.ts │ │ │ ├── templateString.ts │ │ │ ├── tsString.ts │ │ │ ├── tsVariable.test.ts │ │ │ ├── tsVariable.ts │ │ │ ├── tsVariableParser.test.ts │ │ │ ├── tsVariableParser.ts │ │ │ ├── typeUtil.ts │ │ │ ├── util.test.ts │ │ │ └── util.ts │ │ ├── compilerBuilder.ts │ │ ├── jest │ │ │ ├── hook.test.ts │ │ │ ├── hook.ts │ │ │ ├── hookExecutor.test.ts │ │ │ ├── hookExecutor.ts │ │ │ ├── jestScenarioCompiler.ts │ │ │ ├── scenario.test.ts │ │ │ ├── scenario.ts │ │ │ ├── scenarioBook.test.ts │ │ │ ├── scenarioBook.ts │ │ │ ├── scenarioBookParser.test.ts │ │ │ ├── scenarioBookParser.ts │ │ │ ├── step.test.ts │ │ │ ├── step.ts │ │ │ └── testdata │ │ │ │ ├── invalidScenario.yml │ │ │ │ ├── scenario.yml │ │ │ │ └── scenario │ │ │ │ ├── nest1 │ │ │ │ └── nest2 │ │ │ │ │ └── in_nest.yml │ │ │ │ └── simple.yml │ │ ├── playwright │ │ │ ├── arrange.test.ts │ │ │ ├── arrange.ts │ │ │ ├── assert.test.ts │ │ │ ├── assert.ts │ │ │ ├── assertionShortcut.test.ts │ │ │ ├── assertionShortcut.ts │ │ │ ├── fixtures.test.ts │ │ │ ├── fixtures.ts │ │ │ ├── hook.test.ts │ │ │ ├── hook.ts │ │ │ ├── hookExecutor.test.ts │ │ │ ├── hookExecutor.ts │ │ │ ├── locatorAssertionString.test.ts │ │ │ ├── locatorAssertionString.ts │ │ │ ├── pageAssertionString.test.ts │ │ │ ├── pageAssertionString.ts │ │ │ ├── playwrightScenarioCompiler.ts │ │ │ ├── scenario.test.ts │ │ │ ├── scenario.ts │ │ │ ├── scenarioBook.test.ts │ │ │ ├── scenarioBook.ts │ │ │ ├── scenarioBookParser.test.ts │ │ │ ├── scenarioBookParser.ts │ │ │ ├── step.test.ts │ │ │ ├── step.ts │ │ │ ├── testdata │ │ │ │ ├── invalidScenario.yml │ │ │ │ └── scenario.yml │ │ │ ├── useOption.test.ts │ │ │ └── useOption.ts │ │ ├── scenarioCompiler.ts │ │ ├── yamlScenarioCompiler.test.ts │ │ └── yamlScenarioCompiler.ts │ ├── gen │ │ ├── common │ │ │ ├── context.ts │ │ │ └── type.ts │ │ ├── internal │ │ │ ├── common │ │ │ │ ├── arrangeContext.test.ts │ │ │ │ ├── arrangeContext.ts │ │ │ │ ├── arrangeHistory.test.ts │ │ │ │ ├── arrangeHistory.ts │ │ │ │ ├── env.test.ts │ │ │ │ ├── env.ts │ │ │ │ ├── hookContext.test.ts │ │ │ │ ├── hookContext.ts │ │ │ │ ├── scenarioBookContext.test.ts │ │ │ │ ├── scenarioBookContext.ts │ │ │ │ ├── scenarioContext.test.ts │ │ │ │ ├── scenarioContext.ts │ │ │ │ ├── stepContext.test.ts │ │ │ │ ├── stepContext.ts │ │ │ │ ├── stepHistory.test.ts │ │ │ │ ├── stepHistory.ts │ │ │ │ └── type.ts │ │ │ ├── jest │ │ │ │ └── index.ts │ │ │ └── playwright │ │ │ │ └── index.ts │ │ ├── jest │ │ │ ├── asserter │ │ │ │ ├── assertStatus.test.ts │ │ │ │ ├── assertStatus.ts │ │ │ │ ├── asserter.ts │ │ │ │ └── index.ts │ │ │ └── runner │ │ │ │ ├── fetch.test.ts │ │ │ │ ├── fetch.ts │ │ │ │ ├── index.ts │ │ │ │ ├── runner.ts │ │ │ │ ├── waitForSpan.test.ts │ │ │ │ └── waitForSpan.ts │ │ └── playwright │ │ │ └── runner │ │ │ ├── index.ts │ │ │ ├── waitForSpanCreatedIn.test.ts │ │ │ ├── waitForSpanCreatedIn.ts │ │ │ ├── waitForSpanFromPlaywrightFetch.test.ts │ │ │ └── waitForSpanFromPlaywrightFetch.ts │ └── template │ │ ├── common │ │ ├── arrange.eta │ │ ├── plugin.eta │ │ └── step.eta │ │ ├── jest │ │ ├── hook.eta │ │ ├── root.eta │ │ └── scenario.eta │ │ └── playwright │ │ ├── hook.eta │ │ ├── root.eta │ │ └── scenario.eta ├── schema │ ├── configSchema.ts │ ├── configSchemaZod.ts │ ├── jestScenarioYamlSchema.ts │ └── playwrightScenarioYamlSchema.ts ├── server │ ├── constroller │ │ ├── eventController.test.ts │ │ ├── eventController.ts │ │ ├── otelLogController.test.ts │ │ ├── otelLogController.ts │ │ ├── otelTraceController.test.ts │ │ └── otelTraceController.ts │ ├── iServer.ts │ ├── parameter │ │ ├── commonParameter.ts │ │ ├── stateParameter.ts │ │ ├── testFinishedParameter.ts │ │ └── waitForSpanParameter.ts │ ├── request.test.ts │ ├── request.ts │ ├── requester │ │ ├── fetchRequester.ts │ │ ├── requester.ts │ │ └── resp.ts │ ├── server.ts │ ├── serverSpanBus.ts │ ├── service │ │ ├── otelService.test.ts │ │ ├── otelService.ts │ │ ├── stateService.test.ts │ │ ├── stateService.ts │ │ ├── testRecordService.test.ts │ │ ├── testRecordService.ts │ │ ├── waitForSpanService.test.ts │ │ └── waitForSpanService.ts │ └── store │ │ ├── otelLogStore.test.ts │ │ ├── otelLogStore.ts │ │ ├── otelSpanStore.test.ts │ │ ├── otelSpanStore.ts │ │ ├── stateStore.test.ts │ │ ├── stateStore.ts │ │ ├── testCaseStore.test.ts │ │ ├── testCaseStore.ts │ │ ├── waitForSpanRequestStore.test.ts │ │ └── waitForSpanRequestStore.ts ├── testUtil │ ├── async.ts │ ├── config │ │ ├── config.ts │ │ ├── openApiConfig.ts │ │ ├── protoConfig.ts │ │ └── scenarioCompileTargetConfig.ts │ ├── cypress │ │ ├── cypressSpec.ts │ │ ├── dummyCyObj.ts │ │ ├── httpResponse.ts │ │ ├── pluginConfigOptions.ts │ │ ├── request.ts │ │ └── response.ts │ ├── eventBus │ │ └── dummyBus.ts │ ├── fileLog │ │ └── mockFileLogger.ts │ ├── fs │ │ ├── fileSpace.ts │ │ ├── mockDirectory.ts │ │ ├── mockFile.ts │ │ ├── mockFileContents.ts │ │ └── mockFsContainer.ts │ ├── global │ │ └── dummyFetcher.ts │ ├── jest │ │ ├── aggregatedResult.ts │ │ ├── globalConfig.ts │ │ ├── nodeEnvironment.ts │ │ ├── projectConfig.ts │ │ ├── reporter.ts │ │ ├── testCaseResult.ts │ │ ├── testCaseStartInfo.ts │ │ └── test_.ts │ ├── openapi │ │ ├── apiV2.ts │ │ └── apiV3.ts │ ├── otel │ │ ├── id.ts │ │ ├── otelLogRecord.ts │ │ └── otelSpan.ts │ ├── playwright │ │ ├── apiResponse.ts │ │ ├── browserContext.ts │ │ ├── fullConfig.ts │ │ ├── fullProject.ts │ │ ├── request.ts │ │ ├── route.ts │ │ ├── testCase.ts │ │ ├── testInfo.ts │ │ └── testResult.ts │ ├── report │ │ ├── mockReportFile.ts │ │ └── testCase.ts │ ├── scenario │ │ ├── context.ts │ │ ├── dummyScenario.ts │ │ ├── dummyScenarioBook.ts │ │ ├── dummyStep.ts │ │ ├── gen │ │ │ └── internal │ │ │ │ └── jest │ │ │ │ ├── arrangeContext.ts │ │ │ │ ├── scenarioBookContext.ts │ │ │ │ ├── scenarioContext.ts │ │ │ │ └── stepContext.ts │ │ ├── plugin.ts │ │ ├── pluginLister.ts │ │ └── util.ts │ ├── server │ │ └── server.ts │ └── type │ │ └── jsonSpan.ts ├── type │ ├── common.ts │ ├── hexString.ts │ ├── http.ts │ ├── jsonSpan.ts │ ├── jsonZod.test.ts │ ├── jsonZod.ts │ ├── log.ts │ ├── otelLogRecord.ts │ ├── otelSpan.test.ts │ ├── otelSpan.ts │ ├── span.ts │ ├── spanFilterOption.ts │ └── testCase.ts └── util │ ├── ansi.ts │ ├── array.test.ts │ ├── array.ts │ ├── async.ts │ ├── byte.ts │ ├── eta.ts │ ├── file.ts │ ├── map.ts │ ├── never.ts │ ├── proxy.test.ts │ ├── proxy.ts │ ├── random.test.ts │ ├── random.ts │ ├── record.test.ts │ ├── record.ts │ ├── request.test.ts │ ├── request.ts │ ├── stream.ts │ ├── string.test.ts │ ├── string.ts │ ├── traceparent.ts │ ├── twoKeyValuesMap.ts │ ├── type.ts │ ├── ua.ts │ ├── url.test.ts │ ├── url.ts │ └── zod.ts ├── tsconfig.json └── tsup.config.ts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended-type-checked", 5 | ], 6 | parser: "@typescript-eslint/parser", 7 | plugins: ["@typescript-eslint"], 8 | root: true, 9 | ignorePatterns: ["/*", "!/src", "src/generated/", "src/opentelemetry-proto"], 10 | rules: { 11 | "@typescript-eslint/no-unused-vars": [ 12 | "error", 13 | { 14 | argsIgnorePattern: "^_", 15 | }, 16 | ], 17 | "@typescript-eslint/explicit-function-return-type": "error", 18 | }, 19 | overrides: [ 20 | { 21 | // Allow using "any" in gen/internal as they use "any" so often to accept any user input. 22 | files: ["./src/scenario/gen/internal/**/*.ts"], 23 | rules: { 24 | "@typescript-eslint/no-unsafe-assignment": "off", 25 | "@typescript-eslint/no-explicit-any": "off", 26 | "@typescript-eslint/no-unsafe-return": "off", 27 | }, 28 | }, 29 | ], 30 | parserOptions: { 31 | project: true, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/generated/* linguist-generated 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/opentelemetry-proto"] 2 | path = src/opentelemetry-proto 3 | url = git@github.com:open-telemetry/opentelemetry-proto.git 4 | [submodule "create-echoed/template/base/example/opentelemetry-demo"] 5 | path = create-echoed/template/base/example/opentelemetry-demo 6 | url = git@github.com:open-telemetry/opentelemetry-demo.git 7 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.11.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /reporter 3 | /src/generated 4 | /src/opentelemetry-proto 5 | /create-echoed 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 2 | ### Added 3 | - Add support for YAML for Playwright 4 | 5 | ## 0.3.1 6 | ### Changed 7 | - Refactoring 8 | - Update dependencies to silence Dependabot 9 | 10 | ## 0.3.0 11 | ### Added 12 | - Add support for Cypress 13 | 14 | ## 0.2.1 15 | ### Added 16 | - Add option to ignore spans from "undocumented" in coverage. 17 | ### Changed 18 | - Truncate the text of request's result to 1000 characters. 19 | 20 | ## 0.2.0 21 | ### Added 22 | - Add support for Playwright 23 | 24 | ## 0.1.2 25 | ### Added 26 | - Add "arrange" and "hook" (beforeAll, afterAll beforeEach and afterEach) section to YAML 27 | - Add "jest-no-otel" template into create-echoed and add its explanation in README. 28 | 29 | ## 0.1.1 30 | ### Added 31 | - Create "create-echoed" package to reduce size of "echoed" package 32 | 33 | ## 0.1.0 34 | ### Added 35 | - Feature to create tests from YAML 36 | -------------------------------------------------------------------------------- /create-echoed/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended-type-checked", 5 | ], 6 | parser: "@typescript-eslint/parser", 7 | plugins: ["@typescript-eslint"], 8 | root: true, 9 | ignorePatterns: ["/*", "!/src"], 10 | rules: { 11 | "@typescript-eslint/no-unused-vars": [ 12 | "error", 13 | { 14 | argsIgnorePattern: "^_", 15 | }, 16 | ], 17 | "@typescript-eslint/explicit-function-return-type": "error", 18 | }, 19 | parserOptions: { 20 | project: true, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /create-echoed/.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /template/base/example/opentelemetry-demo 3 | /template/base/example/opentelemetry-demo-override 4 | -------------------------------------------------------------------------------- /create-echoed/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports"] 3 | } 4 | -------------------------------------------------------------------------------- /create-echoed/README.md: -------------------------------------------------------------------------------- 1 | # create-echoed 2 | 3 | A CLI for creating new Echoed project 4 | 5 | ```sh 6 | npm create echoed@latest 7 | ``` 8 | -------------------------------------------------------------------------------- /create-echoed/src/ansi.ts: -------------------------------------------------------------------------------- 1 | export const AnsiReset = "\x1b[0m"; 2 | export const AnsiRed = "\x1b[31m"; 3 | export const AnsiGreen = "\x1b[32m"; 4 | -------------------------------------------------------------------------------- /create-echoed/template/base/.echoed.yml: -------------------------------------------------------------------------------- 1 | output: "report/result.html" 2 | 3 | # Remove the following line after removing `example` directory 4 | overrides: ["./example/.echoed.yml"] 5 | -------------------------------------------------------------------------------- /create-echoed/template/base/.prettierignore: -------------------------------------------------------------------------------- 1 | example/opentelemetry-demo 2 | -------------------------------------------------------------------------------- /create-echoed/template/base/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports"] 3 | } 4 | -------------------------------------------------------------------------------- /create-echoed/template/base/example/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: start 2 | start: 3 | cd echoed-opentelemetry-demo && docker compose up --force-recreate --remove-orphans --detach 4 | @echo "" 5 | @echo "OpenTelemetry Demo is starting..." 6 | @./hack/wait_demo_up.sh 7 | @echo "" 8 | @echo "OpenTelemetry Demo in minimal mode is running." 9 | @echo "Go to http://localhost:8080 for the demo UI." 10 | 11 | .PHONY: stop 12 | stop: 13 | cd echoed-opentelemetry-demo && docker compose down --remove-orphans --volumes 14 | @echo "" 15 | @echo "OpenTelemetry Demo is stopped." 16 | -------------------------------------------------------------------------------- /create-echoed/template/base/example/hack/wait_demo_up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | 4 | source ./echoed-opentelemetry-demo/.env 5 | 6 | if [ -z "$ENVOY_PORT" ]; then 7 | echo "ENVOY_PORT is not set" 8 | exit 1 9 | fi 10 | 11 | url="http://localhost:${ENVOY_PORT}" 12 | status_code=0 13 | 14 | max_attempts=20 15 | attempt=1 16 | while [ $attempt -le $max_attempts ]; do 17 | echo "Waiting for ${url} to become usable (attempt ${attempt} / ${max_attempts})" 18 | status_code=$(curl -s -o /dev/null -w "%{http_code}" "$url") 19 | 20 | if [ "$status_code" -eq 200 ]; then 21 | break 22 | fi 23 | 24 | ((attempt++)) 25 | 26 | sleep 1 27 | done 28 | 29 | if [ "$status_code" -ne 200 ]; then 30 | echo "Failed to request ${url} after ${max_attempts} attempts" 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /create-echoed/template/base/example/opentelemetry-demo-override/src/otel-collector/otelcol-config-extras.yml: -------------------------------------------------------------------------------- 1 | # Copyright The OpenTelemetry Authors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # extra settings to be merged into OpenTelemetry Collector configuration 5 | # do not delete this file 6 | 7 | ## Example configuration for sending data to your own OTLP HTTP backend 8 | ## Note: the spanmetrics exporter must be included in the exporters array 9 | ## if overriding the traces pipeline. 10 | ## 11 | # exporters: 12 | # otlphttp/example: 13 | # endpoint: 14 | # 15 | # service: 16 | # pipelines: 17 | # traces: 18 | # exporters: [spanmetrics, otlphttp/example] 19 | 20 | exporters: 21 | otlphttp/local: 22 | endpoint: http://host.docker.internal:3000 23 | retry_on_failure: 24 | enabled: false 25 | file/noop: 26 | path: /dev/null 27 | 28 | service: 29 | pipelines: 30 | traces: 31 | exporters: [otlphttp/local, debug, spanmetrics] 32 | metrics: 33 | exporters: [file/noop] 34 | logs: 35 | exporters: [otlphttp/local, debug] 36 | -------------------------------------------------------------------------------- /create-echoed/template/base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "nodenext", 5 | "outDir": "./dist", 6 | "rootDir": ".", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["./*"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/.instruction/copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "base" 3 | } 4 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/_gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos 3 | cypress/screenshots 4 | report/ 5 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | import { install } from "echoed/cypress/nodeEvents"; 3 | 4 | module.exports = defineConfig({ 5 | reporter: "echoed/cypressReporter.js", 6 | e2e: { 7 | setupNodeEvents: async ( 8 | on: Cypress.PluginEvents, 9 | options: Cypress.PluginConfigOptions, 10 | ) => { 11 | return install(on, options); 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/cypress/e2e/example/home.cy.ts: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from "@/example/util/const"; 2 | import { getSession } from "@/example/util/session"; 3 | 4 | describe("Home Page", { baseUrl: BASE_URL }, () => { 5 | beforeEach(() => { 6 | cy.visit("/"); 7 | }); 8 | 9 | it("should validate the home page", () => { 10 | cy.get("[data-cy=home-page]").should("be.visible"); 11 | cy.get("[data-cy=product-list] [data-cy=product-card]").should( 12 | "have.length", 13 | 10, 14 | ); 15 | 16 | getSession().then(({ userId, currencyCode }) => { 17 | cy.get("[data-cy=session-id]").should("contain.text", userId); 18 | }); 19 | }); 20 | 21 | it("should change currency", () => { 22 | cy.get("[data-cy=currency-switcher]").select("EUR"); 23 | cy.get("[data-cy=product-list] [data-cy=product-card]").should( 24 | "have.length", 25 | 10, 26 | ); 27 | cy.get("[data-cy=product-card]").first().should("contain.text", "€"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | import { install } from "echoed/cypress/support"; 17 | 18 | install(); 19 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/example/util/const.ts: -------------------------------------------------------------------------------- 1 | export const BASE_URL = "http://localhost:8080"; 2 | export const LOCALSTORAGE_ORIGIN = "http://localhost:8080"; 3 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/example/util/session.ts: -------------------------------------------------------------------------------- 1 | import { LOCALSTORAGE_ORIGIN } from "@/example/util/const"; 2 | 3 | export function getSession(): Cypress.Chainable<{ 4 | userId: string | undefined; 5 | currencyCode: string | undefined; 6 | }> { 7 | return cy.getAllLocalStorage().then((result) => { 8 | const sessionId = result[LOCALSTORAGE_ORIGIN].session; 9 | if (sessionId && typeof sessionId === "string") { 10 | const { userId, currencyCode } = JSON.parse(sessionId); 11 | return { userId: userId, currencyCode: currencyCode }; 12 | } 13 | 14 | return { userId: undefined, currencyCode: undefined }; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cy", 3 | "scripts": { 4 | "test": "cypress run" 5 | }, 6 | "devDependencies": { 7 | "@types/cypress": "^1.1.3", 8 | "cypress": "^13.7.1", 9 | "prettier": "3.1.0", 10 | "prettier-plugin-organize-imports": "^3.2.4", 11 | "typescript": "^5.4.3" 12 | }, 13 | "dependencies": { 14 | "echoed": "" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /create-echoed/template/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "nodenext", 5 | "rootDir": ".", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "types": ["cypress", "node", "echoed/cypress/support"], 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["./*"] 13 | } 14 | }, 15 | "include": ["**/*.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /create-echoed/template/jest-no-otel/.instruction/copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "jest" 3 | } 4 | -------------------------------------------------------------------------------- /create-echoed/template/jest-no-otel/README.md: -------------------------------------------------------------------------------- 1 | # Echoed with Jest 2 | 3 | This template provides a minimal setup to run Jest after compiling YAML to Jest tests by Echoed. 4 | The example demonstrates tests written in YAML and TypeScript. 5 | 6 | Feel free to remove the `example` directory once you've familiarized yourself and start crafting your own tests. 7 | 8 | # How to Run Example Tests 9 | 10 | Follow these steps to run the example tests in action: 11 | 12 | 1. **Setup:** Set up the necessary dependencies. 13 | ```sh 14 | npm install 15 | ``` 16 | 2. **Start Server:** Navigate to the `example` directory and start the Dockerized server. 17 | ```sh 18 | cd example 19 | make start 20 | ``` 21 | 3. **Open Website**: Check the server is running by opening http://localhost:8080 in your browser. 22 | 4. **Run Test:** In project's root directory, Run the tests after compiling YAML to Jest tests. 23 | ```sh 24 | cd ../ 25 | npm run compile && npm run test 26 | ``` 27 | 5. **Stop Server:** After testing, stop the server in the `example` directory. 28 | ```sh 29 | cd example 30 | make stop 31 | ``` 32 | -------------------------------------------------------------------------------- /create-echoed/template/jest-no-otel/example/.echoed.yml: -------------------------------------------------------------------------------- 1 | scenario: 2 | compile: 3 | yamlDir: example/scenario 4 | outDir: example/scenario_gen 5 | cleanOutDir: true 6 | env: 7 | BASE_ENDPOINT: http://localhost:8080 8 | plugin: 9 | runners: 10 | - name: fetch 11 | module: echoed/scenario/gen/jest/runner 12 | option: 13 | baseEndpoint: ${_env.BASE_ENDPOINT}/api 14 | headers: 15 | content-type: application/json 16 | asserters: 17 | - name: assertItemLength 18 | module: "@/example/util/assertItemLength" 19 | commons: 20 | - names: 21 | - createSession 22 | module: "@/example/util/session" 23 | - names: 24 | - CartRedis 25 | module: "@/example/util/cartRedis" 26 | -------------------------------------------------------------------------------- /create-echoed/template/jest-no-otel/example/README.md: -------------------------------------------------------------------------------- 1 | # What is this directory? 2 | 3 | This `example` directory serves as a reference implementation for running Echoed's tests. 4 | For instance, `test/manual.test.ts` makes a request to http://localhost:8080/api/cart, and it asserts that the response returns with a status code of 200. 5 | 6 | Feel free to remove this `example` directory once you are ready to create your own tests. 7 | 8 | ## Usage 9 | 10 | 1. Start server: Move to `example` directory and start DockerCompose by `make start`. 11 | 2. Run tests: Move to parent directory and execute `npm run compile` to create tests from YAMLs and `npm run test` to run Jest. 12 | 3. Stop server: Move to `example` directory and stop DockerCompose by `make stop`. 13 | 14 | Note: The server utilizes a modified version of the code from [opentelemetry-demo](https://github.com/open-telemetry/opentelemetry-demo) which sends OpenTelemetry's data to `host.docker.internal:3000`, and it can be launched from DockerCompose. 15 | -------------------------------------------------------------------------------- /create-echoed/template/jest-no-otel/example/scenario/product.yml: -------------------------------------------------------------------------------- 1 | variable: 2 | productId: OLJCESPC7Z 3 | scenarios: 4 | - name: /api/products/{id} should return a specified product 5 | steps: 6 | - description: Get cart 7 | act: 8 | runner: fetch 9 | argument: 10 | endpoint: products/${productId} 11 | assert: 12 | - expect(_.response.status).toBe(200) 13 | - expect(_.jsonBody.id).toBe(productId) 14 | -------------------------------------------------------------------------------- /create-echoed/template/jest-no-otel/example/scenario/products.yml: -------------------------------------------------------------------------------- 1 | scenarios: 2 | - name: /api/products should return ten product 3 | steps: 4 | - description: Get cart 5 | act: 6 | runner: fetch 7 | argument: 8 | endpoint: products 9 | assert: 10 | - expect(_.response.status).toBe(200) 11 | - expect(_.jsonBody.length).toBe(10); 12 | -------------------------------------------------------------------------------- /create-echoed/template/jest-no-otel/example/scenario/recommendation.yml: -------------------------------------------------------------------------------- 1 | variable: 2 | productId: OLJCESPC7Z 3 | scenarios: 4 | - name: /api/recommendations should returns four recommended products 5 | variable: 6 | session: ${createSession()} 7 | steps: 8 | - description: Get cart 9 | act: 10 | runner: fetch 11 | argument: 12 | endpoint: recommendations?productIds=${productId}&sessionId=${session.userId}¤cyCode=${session.currencyCode} 13 | assert: 14 | - expect(_.response.status).toBe(200) 15 | - expect(_.jsonBody.length).toBe(4) 16 | -------------------------------------------------------------------------------- /create-echoed/template/jest-no-otel/example/test/manual.test.ts: -------------------------------------------------------------------------------- 1 | import { createSession } from "../util/session"; 2 | 3 | describe("Manual test", () => { 4 | it("should pass", async () => { 5 | const session = createSession(); 6 | 7 | const response = await fetch( 8 | `http://localhost:8080/api/cart?sessionId=${session.userId}¤cyCode=${session.currencyCode}`, 9 | ); 10 | expect(response.status).toBe(200); 11 | 12 | const body = await response.json(); 13 | 14 | expect(body.items.length).toBe(0); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /create-echoed/template/jest-no-otel/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | verbose: true, 4 | testTimeout: 30000, 5 | // Exclude testEnvironment and reporters in order to run tests using plain Jest. 6 | // testEnvironment: "echoed/jest/nodeEnvironment", 7 | // reporters: ["default", "echoed/jest/reporter"], 8 | moduleNameMapper: { 9 | "^@/(.*)$": "/$1", 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /create-echoed/template/jest/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Ignore package-lock.json because it includes `npm link`ed packages which rererence local files 27 | package-lock.json 28 | 29 | # Reset ignore in example directory to use the example's .gitignore 30 | !example/** 31 | -------------------------------------------------------------------------------- /create-echoed/template/jest/.instruction/copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "base" 3 | } 4 | -------------------------------------------------------------------------------- /create-echoed/template/jest/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /create-echoed/template/jest/_gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | report 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | #### Echoed 28 | scenario_gen 29 | -------------------------------------------------------------------------------- /create-echoed/template/jest/example/README.md: -------------------------------------------------------------------------------- 1 | # What is this directory? 2 | 3 | This `example` directory serves as a reference implementation for running Echoed's tests. 4 | For instance, `test/manual.test.ts` makes a request to http://localhost:8080/api/cart, and it asserts that the response returns with a status code of 200. 5 | 6 | Feel free to remove this `example` directory once you are ready to create your own tests. 7 | 8 | ## Usage 9 | 10 | 1. Start server: Move to `example` directory and start DockerCompose by `make start`. 11 | 2. Run tests: Move to parent directory and execute `npm run compile` to create tests from YAMLs and `npm run test` to run Jest with Echoed. 12 | 3. Wait: Wait for the test to finish. 13 | 4. View results: Open `report/result.html` to see the test results. 14 | 5. Stop server: Move to `example` directory and stop DockerCompose by `make stop`. 15 | 16 | Note: The server utilizes a modified version of the code from [opentelemetry-demo](https://github.com/open-telemetry/opentelemetry-demo) which sends OpenTelemetry's data to `host.docker.internal:3000`, and it can be launched from DockerCompose. 17 | -------------------------------------------------------------------------------- /create-echoed/template/jest/example/scenario/product.yml: -------------------------------------------------------------------------------- 1 | variable: 2 | productId: OLJCESPC7Z 3 | scenarios: 4 | - name: /api/products/{id} should return a specified product 5 | steps: 6 | - description: Get cart 7 | act: 8 | runner: fetch 9 | argument: 10 | endpoint: products/${productId} 11 | assert: 12 | - expect(_.response.status).toBe(200) 13 | - expect(_.jsonBody.id).toBe(productId) 14 | -------------------------------------------------------------------------------- /create-echoed/template/jest/example/scenario/products.yml: -------------------------------------------------------------------------------- 1 | scenarios: 2 | - name: /api/products should return ten product 3 | steps: 4 | - description: Get cart 5 | act: 6 | runner: fetch 7 | argument: 8 | endpoint: products 9 | assert: 10 | - expect(_.response.status).toBe(200) 11 | - expect(_.jsonBody.length).toBe(10); 12 | -------------------------------------------------------------------------------- /create-echoed/template/jest/example/scenario/recommendation.yml: -------------------------------------------------------------------------------- 1 | variable: 2 | productId: OLJCESPC7Z 3 | scenarios: 4 | - name: /api/recommendations should returns four recommended products 5 | variable: 6 | session: ${createSession()} 7 | steps: 8 | - description: Get cart 9 | act: 10 | runner: fetch 11 | argument: 12 | endpoint: recommendations?productIds=${productId}&sessionId=${session.userId}¤cyCode=${session.currencyCode} 13 | assert: 14 | - expect(_.response.status).toBe(200) 15 | - expect(_.jsonBody.length).toBe(4) 16 | -------------------------------------------------------------------------------- /create-echoed/template/jest/example/util/assertItemLength.ts: -------------------------------------------------------------------------------- 1 | import { Asserter } from "echoed/scenario/gen/jest/asserter"; 2 | 3 | export const assertItemLength: Asserter = ( 4 | _ctx: unknown, 5 | items: unknown[], 6 | expectedLength: number, 7 | ): Promise => { 8 | expect(items.length).toEqual(expectedLength); 9 | 10 | return Promise.resolve(); 11 | }; 12 | -------------------------------------------------------------------------------- /create-echoed/template/jest/example/util/cartRedis.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | 3 | export class CartRedis { 4 | static async connect(): Promise { 5 | const redisPort = "6379" 6 | 7 | const redisClient = new Redis(redisPort); 8 | return new CartRedis(redisClient); 9 | } 10 | 11 | constructor(private readonly redisClient: Redis) {} 12 | 13 | async store(userId: string, productId: string, quantity: number) { 14 | const quantityHexBin = String.fromCharCode(quantity); 15 | 16 | // Because opentelemetry-demo stores cart's data as a protobuf format, convert arguments to the format. 17 | await this.redisClient.hset( 18 | userId, 19 | "cart", 20 | `\n$${userId}\x12\x0e\n\n${productId}\x10${quantityHexBin}`, 21 | ); 22 | } 23 | 24 | async quit() { 25 | await this.redisClient.quit(); 26 | } 27 | 28 | async resetUser(userId: string) { 29 | await this.redisClient.del(userId); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /create-echoed/template/jest/example/util/session.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | 3 | export type Session = { 4 | userId: string; 5 | currencyCode: string; 6 | }; 7 | 8 | export function createSession(): Session { 9 | return { 10 | userId: v4(), 11 | currencyCode: "USD", 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /create-echoed/template/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | verbose: true, 4 | testTimeout: 30000, 5 | testEnvironment: "echoed/jest/nodeEnvironment", 6 | reporters: ["default", "echoed/jest/reporter"], 7 | moduleNameMapper: { 8 | "^@/(.*)$": "/$1", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /create-echoed/template/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "jest", 4 | "compile": "echoed compile" 5 | }, 6 | "devDependencies": { 7 | "@types/dockerode": "^3.3.23", 8 | "@types/jest": "^29.5.10", 9 | "@types/uuid": "^9.0.7", 10 | "dockerode": "^4.0.2", 11 | "jest": "^29.7.0", 12 | "ioredis": "^5.3.2", 13 | "uuid": "^9.0.1", 14 | "prettier": "3.1.0", 15 | "prettier-plugin-organize-imports": "^3.2.4", 16 | "redis": "^4.6.13", 17 | "ts-jest": "^29.1.1", 18 | "ts-node": "^10.9.1", 19 | "typescript": "^5.3.2", 20 | "echoed": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /create-echoed/template/playwright-no-otel/.instruction/copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "playwright" 3 | } 4 | -------------------------------------------------------------------------------- /create-echoed/template/playwright-no-otel/example/.echoed.yml: -------------------------------------------------------------------------------- 1 | scenario: 2 | compile: 3 | targets: 4 | - yamlDir: example/test/scenario 5 | outDir: example/test/scenario_gen 6 | type: playwright 7 | useEchoedFeatures: false 8 | cleanOutDir: true 9 | env: 10 | BASE_ENDPOINT: http://localhost:8080 11 | plugin: 12 | commons: 13 | - names: 14 | - getSession 15 | module: "@/example/util/session" 16 | -------------------------------------------------------------------------------- /create-echoed/template/playwright-no-otel/example/README.md: -------------------------------------------------------------------------------- 1 | # What is this directory? 2 | 3 | This `example` directory serves as a reference implementation for running Echoed's tests. 4 | For instance, `test/scenario/home.yaml` visits http://localhost:8080, and it asserts that the page contains expected DOM elements. 5 | 6 | Feel free to remove this `example` directory once you are ready to create your own tests. 7 | 8 | ## Usage 9 | 10 | 1. Start server: Move to `example` directory and start DockerCompose by `make start`. 11 | 2. Run tests: Move to parent directory and execute `npm run compile` to create tests from YAMLs and `npm run test` to run Playwright. 12 | 3. Stop server: Move to `example` directory and stop DockerCompose by `make stop`. 13 | 14 | Note: The server utilizes a modified version of the code from [opentelemetry-demo](https://github.com/open-telemetry/opentelemetry-demo) which sends OpenTelemetry's data to `host.docker.internal:3000`, and it can be launched from DockerCompose. 15 | -------------------------------------------------------------------------------- /create-echoed/template/playwright/.instruction/copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "base" 3 | } 4 | -------------------------------------------------------------------------------- /create-echoed/template/playwright/_gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | 7 | /report/ 8 | -------------------------------------------------------------------------------- /create-echoed/template/playwright/example/README.md: -------------------------------------------------------------------------------- 1 | # What is this directory? 2 | 3 | This `example` directory serves as a reference implementation for running Echoed's tests. 4 | For instance, `test/scenario/home.yaml` visits http://localhost:8080, and it asserts that the page contains expected DOM elements. 5 | 6 | Feel free to remove this `example` directory once you are ready to create your own tests. 7 | 8 | ## Usage 9 | 10 | 1. Start server: Move to `example` directory and start DockerCompose by `make start`. 11 | 2. Run tests: Move to parent directory and execute `npm run compile` to create tests from YAMLs and `npm run test` to run Playwright with Echoed. 12 | 3. Wait: Wait for the test to finish. 13 | 4. View results: Open `report/result.html` to see the test results. 14 | 5. Stop server: Move to `example` directory and stop DockerCompose by `make stop`. 15 | 16 | Note: The server utilizes a modified version of the code from [opentelemetry-demo](https://github.com/open-telemetry/opentelemetry-demo) which sends OpenTelemetry's data to `host.docker.internal:3000`, and it can be launched from DockerCompose. 17 | -------------------------------------------------------------------------------- /create-echoed/template/playwright/example/fixtures/test.ts: -------------------------------------------------------------------------------- 1 | import { test as base } from "@playwright/test"; 2 | import { extendTest } from "echoed/playwright/test/wrapper"; 3 | 4 | /** 5 | * Use `extendTest` when not possible to use `test` of `echoed/playwright/test` directly. 6 | */ 7 | export const test = extendTest(base); 8 | -------------------------------------------------------------------------------- /create-echoed/template/playwright/example/util/session.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | import { v4 } from "uuid"; 3 | 4 | export type Session = { 5 | userId: string; 6 | currencyCode: string; 7 | }; 8 | 9 | export function createSession(): Session { 10 | return { 11 | userId: v4(), 12 | currencyCode: "USD", 13 | }; 14 | } 15 | 16 | export async function getSession(page: Page): Promise { 17 | const sessionId = await page.evaluate(() => localStorage.getItem("session")); 18 | const { userId, currencyCode } = JSON.parse(sessionId!); 19 | 20 | return { userId, currencyCode }; 21 | } 22 | -------------------------------------------------------------------------------- /create-echoed/template/playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_playwright", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "playwright test", 8 | "compile": "echoed compile" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@playwright/test": "^1.41.2", 15 | "@types/node": "^20.11.19", 16 | "echoed": "", 17 | "prettier": "3.1.0", 18 | "prettier-plugin-organize-imports": "^3.2.4", 19 | "typescript": "^5.3.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /create-echoed/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "nodenext", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "declaration": false, 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["./src/*"] 14 | }, 15 | "skipLibCheck": true, 16 | "noImplicitOverride": true, 17 | }, 18 | "exclude": ["dist", "tsup.config.ts", "template", "tmp"] 19 | } 20 | -------------------------------------------------------------------------------- /cypressReporter.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | require("./dist/integration/cypress/reporter/index.js").CypressReporter; 3 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Echoed can be configured at `.echoed.yml` in the root of your project. 4 | Explore available options [here](../src/schema/configSchema.ts). 5 | -------------------------------------------------------------------------------- /docs/img/howEchoedWorks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrasu/echoed/839f56aa9114f44655f19a26c2a5ae806acc6b7c/docs/img/howEchoedWorks.png -------------------------------------------------------------------------------- /docs/img/readmeCoverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrasu/echoed/839f56aa9114f44655f19a26c2a5ae806acc6b7c/docs/img/readmeCoverage.png -------------------------------------------------------------------------------- /docs/img/readmeTraceDetailLog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrasu/echoed/839f56aa9114f44655f19a26c2a5ae806acc6b7c/docs/img/readmeTraceDetailLog.png -------------------------------------------------------------------------------- /docs/img/readmeTraceDetailTrace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrasu/echoed/839f56aa9114f44655f19a26c2a5ae806acc6b7c/docs/img/readmeTraceDetailTrace.png -------------------------------------------------------------------------------- /docs/img/scenarioYamlCompile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrasu/echoed/839f56aa9114f44655f19a26c2a5ae806acc6b7c/docs/img/scenarioYamlCompile.jpg -------------------------------------------------------------------------------- /docs/img/scenarioYamlToPlaywright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrasu/echoed/839f56aa9114f44655f19a26c2a5ae806acc6b7c/docs/img/scenarioYamlToPlaywright.png -------------------------------------------------------------------------------- /reporter/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /reporter/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-svelte"], 3 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 4 | } 5 | -------------------------------------------------------------------------------- /reporter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | Echoed 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /reporter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reporter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json" 11 | }, 12 | "devDependencies": { 13 | "@smui-extra/accordion": "^7.0.0-beta.15", 14 | "@smui/list": "^7.0.0-beta.15", 15 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 16 | "@tsconfig/svelte": "^5.0.2", 17 | "prettier": "^3.1.0", 18 | "prettier-plugin-svelte": "^3.1.2", 19 | "svelte": "^4.2.3", 20 | "svelte-check": "^3.6.0", 21 | "tslib": "^2.6.2", 22 | "typescript": "^5.2.2", 23 | "vite": "^5.0.0", 24 | "vite-plugin-singlefile": "^0.13.5" 25 | }, 26 | "dependencies": { 27 | "buffer": "^6.0.3", 28 | "chart.js": "^4.4.0", 29 | "chartjs-plugin-hierarchical": "^4.3.5", 30 | "long": "^5.2.3", 31 | "svelte-material-ui": "^7.0.0-beta.15", 32 | "svelte-spa-router": "^3.3.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /reporter/src/App.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 | 14 |
15 | 16 | 21 | -------------------------------------------------------------------------------- /reporter/src/components/header/Header.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 26 | tabClicked(tab)}> 27 | 28 | 29 | 30 |
31 | 32 | 37 | -------------------------------------------------------------------------------- /reporter/src/components/list/ListItem.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | dispatch("click")}> 10 | 11 | 12 | -------------------------------------------------------------------------------- /reporter/src/components/status_icons/BlockedIcon.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | block 6 | -------------------------------------------------------------------------------- /reporter/src/components/status_icons/FailedIcon.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | close 6 | -------------------------------------------------------------------------------- /reporter/src/components/status_icons/SucceededIcon.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | done 6 | -------------------------------------------------------------------------------- /reporter/src/components/trace/AttributeDataTable.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | key 12 | value 13 | 14 | 15 | 16 | {#if attributes.length > 0} 17 | {#each attributes as attribute} 18 | 19 | {attribute.key} 20 | {#if attribute.value} 21 | {attribute.value.displayText} 22 | {:else} 23 | - 24 | {/if} 25 | 26 | {/each} 27 | {:else} 28 | 29 | No attribute 30 | 31 | {/if} 32 | 33 | 34 | -------------------------------------------------------------------------------- /reporter/src/consts/echoedParam.ts: -------------------------------------------------------------------------------- 1 | import { EchoedParam } from "@/lib/EchoedParam"; 2 | 3 | export const echoedParam = EchoedParam.convert(window.__echoed_param__); 4 | -------------------------------------------------------------------------------- /reporter/src/consts/testNames.ts: -------------------------------------------------------------------------------- 1 | export const TestNameForPropagation = "Propagation test"; 2 | -------------------------------------------------------------------------------- /reporter/src/lib/MemorizedColorSelector.ts: -------------------------------------------------------------------------------- 1 | // Pick colors from https://mui.com/material-ui/customization/color/#2014-material-design-color-palettes 2 | const colors = [ 3 | "#ef9a9a", 4 | "#ce93d8", 5 | "#9fa8da", 6 | "#81d4fa", 7 | "#80cbc4", 8 | "#c5e1a5", 9 | "#fff59d", 10 | "#ffcc80", 11 | "#ffab91", 12 | "#ffab91", 13 | ]; 14 | 15 | const gray = "#bdbdbd"; 16 | 17 | export class MemorizedColorSelector { 18 | private colorMap = new Map(); 19 | private colorIndex = 0; 20 | 21 | constructor() {} 22 | 23 | pickFor(name?: string): string { 24 | if (!name) { 25 | return gray; 26 | } 27 | 28 | const color = this.colorMap.get(name) 29 | if (color) { 30 | return color; 31 | } 32 | 33 | const newColor = colors[this.colorIndex] 34 | this.colorMap.set(name, newColor); 35 | this.colorIndex++; 36 | this.colorIndex = this.colorIndex % colors.length; 37 | return newColor; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /reporter/src/lib/util/http.ts: -------------------------------------------------------------------------------- 1 | export const HttpMethods = [ 2 | "get", 3 | "post", 4 | "put", 5 | "delete", 6 | "options", 7 | "head", 8 | "patch", 9 | ] as const; 10 | export type HttpMethod = (typeof HttpMethods)[number]; 11 | 12 | export const METHOD_ORDER_MAP = new Map( 13 | HttpMethods.map((method, i) => [method, i]), 14 | ); 15 | 16 | export function toMethod(rawMethod: string): HttpMethod | undefined { 17 | const method = rawMethod.toLowerCase() as HttpMethod; 18 | if (HttpMethods.includes(method)) { 19 | return method; 20 | } 21 | return undefined; 22 | } 23 | -------------------------------------------------------------------------------- /reporter/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./app.css"; 2 | import App from "./App.svelte"; 3 | 4 | const app = new App({ 5 | target: document.getElementById("app")!, 6 | }); 7 | 8 | export default app; 9 | -------------------------------------------------------------------------------- /reporter/src/pages/NotFound.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 | Page not found 5 | -------------------------------------------------------------------------------- /reporter/src/pages/coverage/Coverage.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | Coverage Per Service 20 | 21 | 22 | 23 | 24 | 25 | 26 | Not Configured Services 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /reporter/src/pages/coverage/CoveragePage.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /reporter/src/pages/coverage/service/CoverageServicePage.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {#if coverageInfo} 13 | 14 | {:else} 15 | Invalid service-name 16 | {/if} 17 | -------------------------------------------------------------------------------- /reporter/src/pages/coverage/service/undocumented/http/operation/UndocumentedHttpOperationPage.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | {#if httpOperationTraces && coverageInfo} 24 | 29 | {:else} 30 |

Invalid http operation

31 | {/if} 32 | -------------------------------------------------------------------------------- /reporter/src/pages/coverage/service/undocumented/rpc/method/UndocumentedRpcMethodPage.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | {#if rpcMethodTraces && coverageInfo} 23 | 28 | {:else} 29 |

Invalid rpc operation

30 | {/if} 31 | -------------------------------------------------------------------------------- /reporter/src/pages/coverage/service/undocumented/rpc/method/trace/UndocumentedRpcMethodTracePage.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | {#if trace && coverageInfo && rpcMethodTraces} 27 | 33 | {:else} 34 | Invalid trace-id 35 | {/if} 36 | -------------------------------------------------------------------------------- /reporter/src/pages/coverage/unmeasured/CoverageUnmeasuredPage.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if coverageInfo} 14 | 15 | {:else} 16 |

Not found

17 | {/if} 18 | -------------------------------------------------------------------------------- /reporter/src/pages/coverage/unmeasured/trace/CoverageUnmeasuredTrace.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 24 | 25 | TraceId: {trace.traceId} 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /reporter/src/pages/coverage/unmeasured/trace/CoverageUnmeasuredTracePage.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if coverageInfo && trace} 14 | 15 | {:else} 16 | Invalid trace-id 17 | {/if} 18 | -------------------------------------------------------------------------------- /reporter/src/pages/propagation_test/PropagationTest.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | {#if !passed} 19 | 20 | {/if} 21 | 22 | 27 | -------------------------------------------------------------------------------- /reporter/src/pages/propagation_test/PropagationTestPage.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /reporter/src/pages/propagation_test/unpropagated/trace/UnpropagatedTracePage.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#if trace} 11 | 12 | {:else} 13 | Invalid trace-id 14 | {/if} 15 | -------------------------------------------------------------------------------- /reporter/src/pages/test/TestDetail.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /reporter/src/pages/test/TestDetailPage.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#if testInfo} 11 | 12 | {:else} 13 | Invalid test-id 14 | {/if} 15 | -------------------------------------------------------------------------------- /reporter/src/pages/test/trace/TracePage.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#if testInfo} 15 | 16 | {:else} 17 | Invalid trace-id 18 | {/if} 19 | -------------------------------------------------------------------------------- /reporter/src/pages/test_list/OtherTestsList.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | {#if propagationTestEnabled} 19 | moveToPropagationTest()}> 20 |
21 | {#if propagationTestPassed} 22 | 23 | {:else} 24 | 25 | {/if} 26 |
27 | {TestNameForPropagation} 28 |
29 | {/if} 30 |
31 | -------------------------------------------------------------------------------- /reporter/src/types/window.d.ts: -------------------------------------------------------------------------------- 1 | import type { IEchoedParam } from "@shared/type/echoedParam"; 2 | 3 | declare global { 4 | interface Window { 5 | __echoed_param__: IEchoedParam; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /reporter/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /reporter/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | }; 8 | -------------------------------------------------------------------------------- /reporter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./src/*"], 20 | "@shared/*": ["../shared/*"], 21 | } 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "../shared/*"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /reporter/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /reporter/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | import { viteSingleFile } from "vite-plugin-singlefile"; 4 | import path from "path"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [svelte(), viteSingleFile()], 9 | resolve: { 10 | alias: { 11 | "@": path.resolve("./src"), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /shared/type/http.ts: -------------------------------------------------------------------------------- 1 | export const HttpMethods = [ 2 | "get", 3 | "post", 4 | "put", 5 | "delete", 6 | "options", 7 | "head", 8 | "patch", 9 | ] as const; 10 | export type HttpMethod = (typeof HttpMethods)[number]; 11 | -------------------------------------------------------------------------------- /src/command/compare.ts: -------------------------------------------------------------------------------- 1 | export function eq(value: Primitive): CompareEq { 2 | return { kind: "eq", value }; 3 | } 4 | 5 | export function gt(value: number): CompareNumber { 6 | return { kind: "gt", value }; 7 | } 8 | 9 | export function gte(value: number): CompareNumber { 10 | return { kind: "gte", value }; 11 | } 12 | 13 | export function lt(value: number): CompareNumber { 14 | return { kind: "lt", value }; 15 | } 16 | 17 | export function lte(value: number): CompareNumber { 18 | return { kind: "lte", value }; 19 | } 20 | 21 | type Primitive = string | number | boolean; 22 | 23 | export type Compare = CompareEq | CompareNumber; 24 | 25 | type CompareEq = { 26 | kind: "eq"; 27 | value: Primitive; 28 | }; 29 | 30 | type CompareNumber = { 31 | kind: ["gt", "gte", "lt", "lte"][number]; 32 | value: number; 33 | }; 34 | -------------------------------------------------------------------------------- /src/command/index.ts: -------------------------------------------------------------------------------- 1 | export { eq, gt, gte, lt, lte } from "@/command/compare"; 2 | export { waitForSpan } from "@/command/span"; 3 | -------------------------------------------------------------------------------- /src/comparision/comparable.ts: -------------------------------------------------------------------------------- 1 | import { Kind } from "@/comparision/kind"; 2 | import { opentelemetry } from "@/generated/otelpbj"; 3 | import { z } from "zod"; 4 | 5 | export const Primitive = z.union([z.string(), z.number(), z.boolean()]); 6 | export type Primitive = z.infer; 7 | 8 | export abstract class Comparable { 9 | abstract matchString(target: string | null | undefined): boolean; 10 | 11 | matchIAnyValue( 12 | target: opentelemetry.proto.common.v1.IAnyValue | undefined | null, 13 | ): boolean { 14 | if (!target) return false; 15 | 16 | return this.matchIAnyVal(target); 17 | } 18 | 19 | protected abstract matchIAnyVal( 20 | target: opentelemetry.proto.common.v1.IAnyValue, 21 | ): boolean; 22 | 23 | toJSON(): unknown { 24 | return { 25 | kind: this.kind, 26 | ...this.toJsonObj(), 27 | }; 28 | } 29 | 30 | protected abstract get kind(): Kind; 31 | protected abstract toJsonObj(): Record; 32 | } 33 | -------------------------------------------------------------------------------- /src/comparision/gt.ts: -------------------------------------------------------------------------------- 1 | import { Kind } from "@/comparision/kind"; 2 | import { JsonNumComparable, NumComparable } from "@/comparision/numComparable"; 3 | import Long from "long"; 4 | import { z } from "zod"; 5 | 6 | const KIND = "gt"; 7 | 8 | export const JsonGt = JsonNumComparable.extend({ 9 | kind: z.literal(KIND), 10 | }); 11 | export type JsonGt = z.infer; 12 | 13 | export class Gt extends NumComparable { 14 | constructor(value: number) { 15 | super(value); 16 | } 17 | 18 | protected matchNumber(target: number | Long): boolean { 19 | if (typeof target === "number") { 20 | return this.value < target; 21 | } else { 22 | return target.greaterThan(this.value); 23 | } 24 | } 25 | 26 | protected get kind(): Kind { 27 | return KIND; 28 | } 29 | 30 | static fromJsonObj(obj: JsonNumComparable): Gt { 31 | return new Gt(obj.value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/comparision/gte.ts: -------------------------------------------------------------------------------- 1 | import { Kind } from "@/comparision/kind"; 2 | import { JsonNumComparable, NumComparable } from "@/comparision/numComparable"; 3 | import Long from "long"; 4 | import { z } from "zod"; 5 | 6 | const KIND = "gte"; 7 | 8 | export const JsonGte = JsonNumComparable.extend({ 9 | kind: z.literal(KIND), 10 | }); 11 | export type JsonGte = z.infer; 12 | 13 | export class Gte extends NumComparable { 14 | constructor(value: number) { 15 | super(value); 16 | } 17 | 18 | protected matchNumber(target: number | Long): boolean { 19 | if (typeof target === "number") { 20 | return this.value <= target; 21 | } else { 22 | return target.greaterThanOrEqual(this.value); 23 | } 24 | } 25 | 26 | protected get kind(): Kind { 27 | return KIND; 28 | } 29 | 30 | static fromJsonObj(obj: JsonNumComparable): Gte { 31 | return new Gte(obj.value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/comparision/kind.ts: -------------------------------------------------------------------------------- 1 | const KINDS = ["eq", "gt", "gte", "lt", "lte", "reg"] as const; 2 | export type Kind = (typeof KINDS)[number]; 3 | -------------------------------------------------------------------------------- /src/comparision/lt.ts: -------------------------------------------------------------------------------- 1 | import { Kind } from "@/comparision/kind"; 2 | import { JsonNumComparable, NumComparable } from "@/comparision/numComparable"; 3 | import Long from "long"; 4 | import { z } from "zod"; 5 | 6 | const KIND = "lt"; 7 | 8 | export const JsonLt = JsonNumComparable.extend({ 9 | kind: z.literal(KIND), 10 | }); 11 | export type JsonLt = z.infer; 12 | 13 | export class Lt extends NumComparable { 14 | constructor(value: number) { 15 | super(value); 16 | } 17 | 18 | protected matchNumber(target: number | Long): boolean { 19 | if (typeof target === "number") { 20 | return target < this.value; 21 | } else { 22 | return target.lessThan(this.value); 23 | } 24 | } 25 | 26 | protected get kind(): Kind { 27 | return KIND; 28 | } 29 | 30 | static fromJsonObj(obj: JsonNumComparable): Lt { 31 | return new Lt(obj.value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/comparision/lte.ts: -------------------------------------------------------------------------------- 1 | import { Kind } from "@/comparision/kind"; 2 | import { JsonNumComparable, NumComparable } from "@/comparision/numComparable"; 3 | import Long from "long"; 4 | import { z } from "zod"; 5 | 6 | const KIND = "lte"; 7 | 8 | export const JsonLte = JsonNumComparable.extend({ 9 | kind: z.literal(KIND), 10 | }); 11 | export type JsonLte = z.infer; 12 | 13 | export class Lte extends NumComparable { 14 | constructor(value: number) { 15 | super(value); 16 | } 17 | 18 | protected matchNumber(target: number | Long): boolean { 19 | if (typeof target === "number") { 20 | return target <= this.value; 21 | } else { 22 | return target.lessThanOrEqual(this.value); 23 | } 24 | } 25 | 26 | protected get kind(): Kind { 27 | return KIND; 28 | } 29 | 30 | static fromJsonObj(obj: JsonNumComparable): Lte { 31 | return new Lte(obj.value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/comparision/numComparable.ts: -------------------------------------------------------------------------------- 1 | import { Comparable } from "@/comparision/comparable"; 2 | import { opentelemetry } from "@/generated/otelpbj"; 3 | import Long from "long"; 4 | import { z } from "zod"; 5 | 6 | export const JsonNumComparable = z.strictObject({ value: z.number() }); 7 | export type JsonNumComparable = z.infer; 8 | 9 | export abstract class NumComparable extends Comparable { 10 | protected constructor(protected value: number) { 11 | super(); 12 | } 13 | 14 | protected matchIAnyVal( 15 | target: opentelemetry.proto.common.v1.IAnyValue, 16 | ): boolean { 17 | const valueNum = target.intValue ?? target.doubleValue ?? undefined; 18 | if (!valueNum) return false; 19 | 20 | return this.matchNumber(valueNum); 21 | } 22 | 23 | protected abstract matchNumber(target: number | Long): boolean; 24 | 25 | matchString(_: string | null | undefined): boolean { 26 | return false; 27 | } 28 | 29 | protected toJsonObj(): JsonNumComparable { 30 | return { 31 | value: this.value, 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/config/invalidConfigError.ts: -------------------------------------------------------------------------------- 1 | import { EchoedError } from "@/echoedError"; 2 | 3 | export class InvalidConfigError extends EchoedError { 4 | constructor(message: string) { 5 | super(`Invalid configuration. ${message}`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/config/scenarioCompileTargetConfig.ts: -------------------------------------------------------------------------------- 1 | import { IDirectory } from "@/fs/iDirectory"; 2 | 3 | export class ScenarioCompileTargetConfig { 4 | constructor( 5 | public readonly yamlDir: IDirectory, 6 | public readonly outDir: IDirectory, 7 | public readonly type: "jest" | "playwright", 8 | public readonly useEchoedFeatures: boolean, 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/config/util/mapper.ts: -------------------------------------------------------------------------------- 1 | import { Comparable } from "@/comparision/comparable"; 2 | import { Eq } from "@/comparision/eq"; 3 | import { Reg } from "@/comparision/reg"; 4 | 5 | // ComparableValue is representation for `Comparable` in config files. 6 | type ComparableValue = string | boolean | number | { regexp: string }; 7 | 8 | export function convertToComparables( 9 | values: Record | undefined, 10 | ): Map { 11 | if (!values) return new Map(); 12 | 13 | const ret = new Map(); 14 | for (const [key, val] of Object.entries(values)) { 15 | const cmp = convertToComparable(val); 16 | ret.set(key, cmp); 17 | } 18 | return ret; 19 | } 20 | 21 | export function convertToComparable(val: ComparableValue): Comparable { 22 | if (typeof val === "object") { 23 | return Reg.fromString(val.regexp); 24 | } else { 25 | return new Eq(val); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/coverage/openApi/__fixtures__/simple.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: FrontendService API 4 | version: 0.0.1 5 | servers: 6 | - url: "http://localhost:8080/api" 7 | description: Local server 8 | paths: 9 | /products: 10 | get: 11 | responses: 12 | "200": 13 | description: A JSON array of products 14 | /products/{productId}: 15 | get: 16 | summary: Get a product by ID 17 | parameters: 18 | - name: productId 19 | in: path 20 | required: true 21 | schema: 22 | type: string 23 | responses: 24 | "200": 25 | description: A JSON object of a product 26 | -------------------------------------------------------------------------------- /src/coverage/openApi/operation.ts: -------------------------------------------------------------------------------- 1 | import { HttpMethod } from "@shared/type/http"; 2 | import { OpenAPI } from "openapi-types"; 3 | 4 | export class Operation { 5 | constructor( 6 | // specPath is path specified in OpenAPI specification 7 | public specPath: string, 8 | public method: HttpMethod, 9 | public openApiOperation: OpenAPI.Operation, 10 | public visited: boolean = false, 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /src/coverage/proto/method.ts: -------------------------------------------------------------------------------- 1 | import { Method as protobufMethod } from "protobufjs"; 2 | 3 | export class Method { 4 | constructor( 5 | public protobufMethod: protobufMethod, 6 | public visited: boolean = false, 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /src/coverage/proto/service.ts: -------------------------------------------------------------------------------- 1 | import { Method } from "@/coverage/proto/method"; 2 | import { Service as protobufService } from "protobufjs"; 3 | 4 | export class Service { 5 | service: protobufService; 6 | methods: Map; 7 | 8 | constructor(service: protobufService) { 9 | this.service = service; 10 | 11 | this.methods = new Map(); 12 | for (const method of service.methodsArray) { 13 | this.methods.set(method.name, new Method(method)); 14 | } 15 | } 16 | 17 | get rpcServiceName(): string { 18 | // remove leading dot from `fullName` 19 | // 20 | // c.f. definition of ReflectionObject.fullName 21 | // > /** Full name including leading dot. */ 22 | // > public readonly fullName: string; 23 | return this.service.fullName.replace(/^\./, ""); 24 | } 25 | 26 | isInTargettedService(targetServices: Set | undefined): boolean { 27 | if (!targetServices) return true; 28 | if (targetServices.has(this.service.name)) return true; 29 | if (targetServices.has(this.rpcServiceName)) return true; 30 | 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/coverage/serviceCoverageCollector.ts: -------------------------------------------------------------------------------- 1 | import { HttpCoverage, RpcCoverage } from "@/coverage/coverageResult"; 2 | import { OtelSpan } from "@/type/otelSpan"; 3 | 4 | export interface ServiceCoverageCollector { 5 | markVisited(spans: OtelSpan[]): void; 6 | getCoverage(): ServiceCoverageCollectorResult; 7 | } 8 | 9 | export type ServiceCoverageCollectorResult = { 10 | httpCoverage?: HttpCoverage; 11 | rpcCoverage?: RpcCoverage; 12 | }; 13 | -------------------------------------------------------------------------------- /src/coverage/unmeasuredTraceCollector.ts: -------------------------------------------------------------------------------- 1 | import { OtelSpan } from "@/type/otelSpan"; 2 | import { toHex } from "@/util/byte"; 3 | 4 | export class UnmeasuredTraceCollector { 5 | private readonly traceIds = new Set(); 6 | 7 | addSpans(spans: OtelSpan[]): void { 8 | for (const span of spans) { 9 | if (!span.traceId) continue; 10 | this.traceIds.add(toHex(span.traceId).hexString); 11 | } 12 | } 13 | 14 | get traceIdArray(): string[] { 15 | return Array.from(this.traceIds); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/echoedError.ts: -------------------------------------------------------------------------------- 1 | export class EchoedError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = new.target.name; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/echoedFatalError.ts: -------------------------------------------------------------------------------- 1 | export class EchoedFatalError extends Error { 2 | origMsg: string; 3 | 4 | constructor(msg: string) { 5 | super("Echoed: Fatal Error. " + msg); 6 | 7 | this.name = new.target.name; 8 | this.origMsg = msg; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/eventBus/infra/eventBus.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessage } from "@/type/common"; 2 | 3 | export type WatchCallback = (data: unknown) => Promise; 4 | 5 | export interface EventBus { 6 | on(eventName: string, callback: WatchCallback): void; 7 | onOnce( 8 | eventName: string, 9 | timeoutMs: number, 10 | fn: (data: unknown) => Promise, 11 | ): Promise; 12 | 13 | emit(eventName: string, data: unknown): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/eventBus/infra/timeoutError.ts: -------------------------------------------------------------------------------- 1 | import { EchoedError } from "@/echoedError"; 2 | 3 | export class TimeoutError extends EchoedError { 4 | constructor() { 5 | super("timeout"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/fileLog/fileLogger.ts: -------------------------------------------------------------------------------- 1 | import { IFileLogger } from "@/fileLog/iFileLogger"; 2 | import { IFile } from "@/fs/IFile"; 3 | 4 | export class FileLogger implements IFileLogger { 5 | constructor(private file: IFile) {} 6 | 7 | async appendFileLine(text: string): Promise { 8 | await this.file.appendLine(text); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/fileLog/iFileLogger.ts: -------------------------------------------------------------------------------- 1 | export interface IFileLogger { 2 | appendFileLine(text: string): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/fileSpace/fileSpace.ts: -------------------------------------------------------------------------------- 1 | import { OtelDirectory } from "@/fileSpace/otelDirectory"; 2 | import { IFile } from "@/fs/IFile"; 3 | import { IDirectory } from "@/fs/iDirectory"; 4 | import { buildRandomHexUUID } from "@/util/random"; 5 | 6 | export class FileSpace { 7 | readonly testLogDir: IDirectory; 8 | readonly otelDir: OtelDirectory; 9 | 10 | constructor(dir: IDirectory) { 11 | this.testLogDir = dir.newDir("test"); 12 | this.otelDir = new OtelDirectory(dir.newDir("otel")); 13 | } 14 | 15 | ensureDirectoryExistence(): void { 16 | this.testLogDir.mkdirSync(); 17 | this.otelDir.mkdirSync(); 18 | } 19 | 20 | createTestLogFile(): IFile { 21 | const logDir = this.testLogDir; 22 | const filename = buildRandomHexUUID() + ".json"; 23 | return logDir.newFile(filename); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/fileSpace/otelDirectory.ts: -------------------------------------------------------------------------------- 1 | import { IDirectory } from "@/fs/iDirectory"; 2 | import { IFile } from "@/fs/IFile"; 3 | 4 | export class OtelDirectory { 5 | constructor(private readonly dir: IDirectory) {} 6 | 7 | mkdirSync(): void { 8 | this.dir.mkdirSync(); 9 | } 10 | 11 | get spanFile(): IFile { 12 | return this.dir.newFile("span.jsonl"); 13 | } 14 | 15 | get logFile(): IFile { 16 | return this.dir.newFile("log.jsonl"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/fs/IFile.ts: -------------------------------------------------------------------------------- 1 | import { IDirectory } from "@/fs/iDirectory"; 2 | import fs from "fs"; 3 | 4 | export interface IFile { 5 | path: string; 6 | 7 | toDir(): IDirectory; 8 | 9 | statSync(): fs.Stats | undefined; 10 | existsSync(): boolean; 11 | 12 | read(): Promise; 13 | readSync(): string; 14 | 15 | ensureDir(): Promise; 16 | 17 | createEmptyWithDir(): Promise; 18 | 19 | write(text: string): Promise; 20 | 21 | append(text: string): Promise; 22 | appendLine(text: string): Promise; 23 | 24 | unlink(): Promise; 25 | } 26 | -------------------------------------------------------------------------------- /src/fs/fsContainer.ts: -------------------------------------------------------------------------------- 1 | import { IFile } from "@/fs/IFile"; 2 | import { IDirectory } from "@/fs/iDirectory"; 3 | import { LocalDirectory } from "@/fs/localDirectory"; 4 | import { LocalFile } from "@/fs/localFile"; 5 | import fs from "fs"; 6 | 7 | export type FsContainer = { 8 | mkdtempSync: (prefix: string) => IDirectory; 9 | newDirectory: (dir: string) => IDirectory; 10 | newFile: (filepath: string) => IFile; 11 | }; 12 | 13 | export const buildFsContainerForApp = (): FsContainer => { 14 | return { 15 | mkdtempSync: (prefix: string): IDirectory => { 16 | return new LocalDirectory(fs.mkdtempSync(prefix)); 17 | }, 18 | newDirectory: (dir: string): IDirectory => { 19 | return new LocalDirectory(dir); 20 | }, 21 | newFile(filepath: string): IFile { 22 | return new LocalFile(filepath); 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/fs/iDirectory.ts: -------------------------------------------------------------------------------- 1 | import { IFile } from "@/fs/IFile"; 2 | 3 | export interface IDirectory { 4 | path: string; 5 | 6 | newDir(dir: string): IDirectory; 7 | newFile(filename: string): IFile; 8 | 9 | resolve(): string; 10 | 11 | mkdirSync(): void; 12 | readdir(): Promise; 13 | 14 | rm(): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/fs/localDirectory.ts: -------------------------------------------------------------------------------- 1 | import { IFile } from "@/fs/IFile"; 2 | import { IDirectory } from "@/fs/iDirectory"; 3 | import { LocalFile } from "@/fs/localFile"; 4 | import fs from "fs"; 5 | import path from "path"; 6 | 7 | export class LocalDirectory implements IDirectory { 8 | constructor(public path: string) {} 9 | 10 | newDir(dir: string): IDirectory { 11 | return new LocalDirectory(path.join(this.path, dir)); 12 | } 13 | 14 | newFile(filename: string): IFile { 15 | const filepath = path.join(this.path, filename); 16 | return new LocalFile(filepath); 17 | } 18 | 19 | resolve(): string { 20 | return path.resolve(this.path); 21 | } 22 | 23 | mkdirSync(): void { 24 | fs.mkdirSync(this.path, { recursive: true }); 25 | } 26 | 27 | async readdir(): Promise { 28 | const files = await fs.promises.readdir(this.path); 29 | return files.map((file) => { 30 | return this.newFile(file); 31 | }); 32 | } 33 | 34 | async rm(): Promise { 35 | await fs.promises.rm(this.path, { recursive: true, force: true }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@/command/index"; 2 | -------------------------------------------------------------------------------- /src/integration/common/commonFetchRunner.test.ts: -------------------------------------------------------------------------------- 1 | import { CommonFetchRunner } from "@/integration/common/commonFetchRunner"; 2 | 3 | describe("CommonFetchRunner", () => { 4 | describe("run", () => { 5 | it("should call fetch with hook", async () => { 6 | const mockFetch = jest.fn().mockReturnValue(new Response()); 7 | const mockOnStart = jest.fn(); 8 | const mockOnFinished = jest.fn(); 9 | 10 | const runner = new CommonFetchRunner(mockFetch); 11 | await runner.run( 12 | "https://example.com", 13 | undefined, 14 | mockOnStart, 15 | mockOnFinished, 16 | ); 17 | 18 | expect(mockFetch).toHaveBeenCalled(); 19 | expect(mockFetch.mock.calls[0]).toEqual([ 20 | "https://example.com", 21 | { 22 | headers: { 23 | traceparent: expect.any(String) as string, 24 | }, 25 | }, 26 | ]); 27 | 28 | expect(mockOnStart).toHaveBeenCalled(); 29 | expect(mockOnFinished).toHaveBeenCalled(); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/integration/common/traceHistory.ts: -------------------------------------------------------------------------------- 1 | import { HexString } from "@/type/hexString"; 2 | 3 | export class TraceHistory { 4 | private traces: [string, HexString][] = []; 5 | 6 | push(key: string, traceId: HexString): void { 7 | this.traces.push([key, traceId]); 8 | } 9 | 10 | get copiedTraces(): [string, HexString][] { 11 | return this.traces.map((v) => [v[0], v[1]]); 12 | } 13 | 14 | getLastTraceId(pattern: string | RegExp): HexString | undefined { 15 | const trace = this.traces.reverse().find(([key]) => { 16 | if (pattern instanceof RegExp) { 17 | return pattern.test(key); 18 | } 19 | return key === pattern; 20 | }); 21 | 22 | return trace?.[1]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/integration/common/util/config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "@/config/config"; 2 | import { FsContainer } from "@/fs/fsContainer"; 3 | import { LocalFile } from "@/fs/localFile"; 4 | import { throwError } from "@/integration/common/util/error"; 5 | 6 | export function loadConfig( 7 | fsContainer: FsContainer, 8 | configFile: LocalFile, 9 | ): Config { 10 | try { 11 | return Config.load(fsContainer, configFile); 12 | } catch (e) { 13 | throwError(e); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/integration/common/util/env.ts: -------------------------------------------------------------------------------- 1 | export function setTmpDirToEnv(tmpDir: string): void { 2 | process.env.__ECHOED_TMPDIR__ = tmpDir; 3 | } 4 | 5 | export function getTmpDirFromEnv(): string | undefined { 6 | return process.env.__ECHOED_TMPDIR__; 7 | } 8 | 9 | export function deleteTmpDirFromEnv(): void { 10 | delete process.env.__ECHOED_TMPDIR__; 11 | } 12 | 13 | export function setServerPortToEnv(port: number): void { 14 | process.env.__ECHOED_SERVER_PORT__ = port.toString(); 15 | } 16 | 17 | export function getServerPortFromEnv(): number | undefined { 18 | const port = process.env.__ECHOED_SERVER_PORT__; 19 | if (!port) return undefined; 20 | 21 | return parseInt(port) || undefined; 22 | } 23 | 24 | export function deleteServerPortFromEnv(): void { 25 | delete process.env.__ECHOED_SERVER_PORT__; 26 | } 27 | -------------------------------------------------------------------------------- /src/integration/common/util/error.ts: -------------------------------------------------------------------------------- 1 | import { EchoedError } from "@/echoedError"; 2 | import { Logger } from "@/logger"; 3 | 4 | export function throwError(e: unknown): never { 5 | if (e instanceof EchoedError) { 6 | // Add new line to emphasize message. 7 | // Because long message including stacktrace will be printed after `throw e`, we need to emphasize message somehow. 8 | Logger.ln(2); 9 | Logger.error(e.message); 10 | Logger.ln(2); 11 | } 12 | 13 | throw e; 14 | } 15 | -------------------------------------------------------------------------------- /src/integration/common/util/fetchResponse.ts: -------------------------------------------------------------------------------- 1 | import { HexString } from "@/type/hexString"; 2 | 3 | const traceIdPropertyName = "__echoed_traceId"; 4 | 5 | export type WrappedResponse = { [traceIdPropertyName]: HexString }; 6 | 7 | export function setTraceIdToResponse( 8 | response: Response, 9 | traceId: HexString, 10 | ): void { 11 | (response as unknown as WrappedResponse)[traceIdPropertyName] = traceId; 12 | } 13 | 14 | export function getTraceIdFromResponse( 15 | response: Response, 16 | ): HexString | undefined { 17 | return (response as unknown as WrappedResponse)[traceIdPropertyName]; 18 | } 19 | -------------------------------------------------------------------------------- /src/integration/common/util/response.ts: -------------------------------------------------------------------------------- 1 | import { isReadableContentType } from "@/util/request"; 2 | 3 | export async function readResponseText(response: Response): Promise { 4 | const contentType = extractLowerCaseContentType(response); 5 | 6 | if (isReadableContentType(contentType)) { 7 | return await response.text(); 8 | } 9 | 10 | return buildNotDisplayableMessage(contentType); 11 | } 12 | 13 | function extractLowerCaseContentType(response: Response): string { 14 | for (const [key, value] of response.headers.entries()) { 15 | if (key.toLowerCase() === "content-type") { 16 | return value; 17 | } 18 | } 19 | 20 | return ""; 21 | } 22 | 23 | export function buildNotDisplayableMessage(origContentType?: string): string { 24 | if (origContentType === undefined) { 25 | return "[Not displayable]"; 26 | } 27 | 28 | const ct = origContentType || "unknown"; 29 | return `[Not displayable. content-type=${ct}]`; 30 | } 31 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/cypressHttpMessage.ts: -------------------------------------------------------------------------------- 1 | // CypressHttpMessage is a type for CyHttpMessages.BaseMessage in "cypress/types/net-stubbing" 2 | // Because CyHttpMessages.BaseMessage cannot be imported via "cypress/types/net-stubbing" directly, declare the type here. 3 | export interface CypressHttpMessage { 4 | body: unknown; 5 | headers: { [key: string]: string | string[] }; 6 | } 7 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/cypressHttpRequest.ts: -------------------------------------------------------------------------------- 1 | import { CypressHttpMessage } from "@/integration/cypress/internal/cypressHttpMessage"; 2 | import { CypressHttpResponse } from "@/integration/cypress/internal/cypressHttpResponse"; 3 | 4 | /** 5 | CypressHttpRequest is a minimal type for `CyHttpMessages.IncomingRequest` in "cypress/types/net-stubbing". 6 | 7 | Because CyHttpMessages.IncomingRequest cannot be imported via "cypress/types/net-stubbing" directly, declare the type here. 8 | */ 9 | export interface CypressHttpRequest extends CypressHttpMessage { 10 | url: string; 11 | method: string; 12 | 13 | on( 14 | eventName: "response", 15 | fn: (res: CypressHttpResponse) => Promise, 16 | ): this; 17 | } 18 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/cypressHttpResponse.ts: -------------------------------------------------------------------------------- 1 | import { CypressHttpMessage } from "@/integration/cypress/internal/cypressHttpMessage"; 2 | 3 | /** 4 | CypressHttpRequest is a minimal type for `CyHttpMessages.IncomingResponse` in "cypress/types/net-stubbing". 5 | 6 | Because CyHttpMessages.IncomingResponse cannot be imported via "cypress/types/net-stubbing" directly, declare the type here. 7 | */ 8 | export interface CypressHttpResponse extends CypressHttpMessage { 9 | statusCode: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/infra/cypressFileLogger.ts: -------------------------------------------------------------------------------- 1 | import { IFileLogger } from "@/fileLog/iFileLogger"; 2 | import { IFile } from "@/fs/IFile"; 3 | 4 | export interface ICypressFileLogger extends IFileLogger { 5 | writeToFile(): void; 6 | } 7 | 8 | export class CypressFileLogger implements ICypressFileLogger { 9 | private texts: string[] = []; 10 | constructor(private file: IFile) {} 11 | 12 | async appendFileLine(text: string): Promise { 13 | this.texts.push(text); 14 | 15 | return Promise.resolve(); 16 | } 17 | 18 | writeToFile(): void { 19 | let newText = ""; 20 | for (const text of this.texts) { 21 | newText += text + "\n"; 22 | } 23 | 24 | cy.writeFile(this.file.path, newText, { flag: "a" }); 25 | this.texts = []; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/infra/cypressObj.ts: -------------------------------------------------------------------------------- 1 | import { ICypressObj } from "@/integration/cypress/internal/infra/iCypressObj"; 2 | 3 | export class CypressObj implements ICypressObj { 4 | constructor(private c: Cypress.Cypress) {} 5 | 6 | env(key: string): unknown { 7 | return this.c.env(key); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/infra/cypressRequester.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from "@/integration/cypress/internal/util/promisify"; 2 | import { Requester } from "@/server/requester/requester"; 3 | import { Resp } from "@/server/requester/resp"; 4 | 5 | export class CypressRequester implements Requester { 6 | constructor(private timeoutMs: number) {} 7 | post( 8 | url: string, 9 | origHeaders: Record, 10 | body: string, 11 | ): Promise { 12 | const headers = { 13 | ...origHeaders, 14 | "Content-Type": "text/plain;charset=UTF-8", 15 | }; 16 | 17 | const chainable = cy 18 | .request({ 19 | url, 20 | headers, 21 | method: "POST", 22 | encoding: "utf8", 23 | body: body, 24 | timeout: this.timeoutMs, 25 | }) 26 | .then((response) => { 27 | return new Resp(response.status, response.body); 28 | }); 29 | 30 | return promisify(chainable); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/infra/iCypressObj.ts: -------------------------------------------------------------------------------- 1 | export interface ICypressObj { 2 | env(key: string): unknown; 3 | } 4 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/runInfo.test.ts: -------------------------------------------------------------------------------- 1 | import { RunInfo } from "@/integration/cypress/internal/runInfo"; 2 | import { buildCypressSpec } from "@/testUtil/cypress/cypressSpec"; 3 | import { buildDummyFileSpace } from "@/testUtil/fs/fileSpace"; 4 | 5 | describe("RunInfo", () => { 6 | describe("reset", () => { 7 | it("should set currentSpec undefined", () => { 8 | const fileSpace = buildDummyFileSpace(); 9 | const runInfo = new RunInfo(fileSpace); 10 | 11 | runInfo.currentSpec = buildCypressSpec(); 12 | expect(runInfo.currentSpec).not.toBe(undefined); 13 | 14 | runInfo.reset(); 15 | 16 | expect(runInfo.currentSpec).toBe(undefined); 17 | }); 18 | }); 19 | 20 | describe("setCurrentSpec", () => { 21 | it("should set currentSpec", () => { 22 | const fileSpace = buildDummyFileSpace(); 23 | const runInfo = new RunInfo(fileSpace); 24 | 25 | const spec = buildCypressSpec(); 26 | const specHook = runInfo.setCurrentSpec(spec); 27 | 28 | expect(runInfo.currentSpec).toBe(spec); 29 | expect(specHook).toBeDefined(); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/runInfo.ts: -------------------------------------------------------------------------------- 1 | import { FileSpace } from "@/fileSpace/fileSpace"; 2 | import { CypressFileLogger } from "@/integration/cypress/internal/infra/cypressFileLogger"; 3 | import { SpecHook } from "@/integration/cypress/internal/specHook"; 4 | 5 | export class RunInfo { 6 | fileLogger: CypressFileLogger; 7 | currentSpec: Cypress.Spec | undefined; 8 | 9 | constructor(fileSpace: FileSpace) { 10 | const file = fileSpace.createTestLogFile(); 11 | 12 | this.fileLogger = new CypressFileLogger(file); 13 | } 14 | 15 | reset(): void { 16 | this.currentSpec = undefined; 17 | } 18 | 19 | setCurrentSpec(spec: Cypress.Spec): SpecHook { 20 | this.currentSpec = spec; 21 | 22 | return new SpecHook(spec, this.fileLogger); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/specHook.ts: -------------------------------------------------------------------------------- 1 | import { CypressFileLogger } from "@/integration/cypress/internal/infra/cypressFileLogger"; 2 | import { RequestRunner } from "@/integration/cypress/internal/requestRunner"; 3 | import { initializeEchoedContext } from "@/integration/cypress/internal/util/cypressSpec"; 4 | 5 | export class SpecHook { 6 | constructor( 7 | private spec: Cypress.Spec, 8 | private fileLogger: CypressFileLogger, 9 | ) {} 10 | 11 | onBeforeEach(): void { 12 | initializeEchoedContext(this.spec); 13 | 14 | const requestRunner = new RequestRunner(this.spec, this.fileLogger); 15 | cy.intercept("*", { middleware: true }, async (req): Promise => { 16 | await requestRunner.run(req); 17 | }); 18 | } 19 | 20 | onAfterEach(): void { 21 | this.fileLogger.writeToFile(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/util/cypressRequest.ts: -------------------------------------------------------------------------------- 1 | import { CypressHttpRequest } from "@/integration/cypress/internal/cypressHttpRequest"; 2 | import { 3 | normalizeHeaders, 4 | normalizeRequestOptionHeaders, 5 | } from "@/integration/cypress/internal/util/headers"; 6 | import { ECHOED_USER_AGENT, USER_AGENT_HEADER_KEY } from "@/server/request"; 7 | 8 | export function isEchoedHttpRequest(request: CypressHttpRequest): boolean { 9 | const headers = normalizeHeaders(request.headers); 10 | return hasEchoedUserAgent(headers); 11 | } 12 | 13 | export function isEchoedRequestOption( 14 | opts: Partial, 15 | ): boolean { 16 | const headers = normalizeRequestOptionHeaders(opts.headers); 17 | return hasEchoedUserAgent(headers); 18 | } 19 | 20 | function hasEchoedUserAgent(headers: Map): boolean { 21 | for (const [key, values] of headers.entries()) { 22 | if (key !== USER_AGENT_HEADER_KEY) continue; 23 | 24 | return values.includes(ECHOED_USER_AGENT); 25 | } 26 | 27 | return false; 28 | } 29 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/util/env.ts: -------------------------------------------------------------------------------- 1 | import { ICypressObj } from "@/integration/cypress/internal/infra/iCypressObj"; 2 | 3 | const SERVER_PORT_KEY = "__ECHOED_SERVER_PORT__"; 4 | const TMP_DIR_KEY = "__ECHOED_TMP_DIR__"; 5 | 6 | export function setServerPortToCypressEnv( 7 | options: Cypress.PluginConfigOptions, 8 | port: number, 9 | ): void { 10 | options.env[SERVER_PORT_KEY] = port.toString(); 11 | } 12 | 13 | export function getServerPortFromCypressEnv( 14 | cyObj: ICypressObj, 15 | ): number | undefined { 16 | const port = cyObj.env(SERVER_PORT_KEY) as string | undefined; 17 | if (!port) return undefined; 18 | 19 | return parseInt(port) || undefined; 20 | } 21 | 22 | export function setTmpDirToCypressEnv( 23 | options: Cypress.PluginConfigOptions, 24 | tmpDir: string, 25 | ): void { 26 | options.env[TMP_DIR_KEY] = tmpDir; 27 | } 28 | 29 | export function getTmpDirFromCypressEnv( 30 | cyObj: ICypressObj, 31 | ): string | undefined { 32 | return cyObj.env(TMP_DIR_KEY) as string | undefined; 33 | } 34 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/util/fileSpace.ts: -------------------------------------------------------------------------------- 1 | import { EchoedFatalError } from "@/echoedFatalError"; 2 | import { FileSpace } from "@/fileSpace/fileSpace"; 3 | import { buildFsContainerForApp } from "@/fs/fsContainer"; 4 | import { ICypressObj } from "@/integration/cypress/internal/infra/iCypressObj"; 5 | import { getTmpDirFromCypressEnv } from "@/integration/cypress/internal/util/env"; 6 | 7 | export function buildFileSpaceFromCypressEnv(cyObj: ICypressObj): FileSpace { 8 | const tmpDirPath = getTmpDirFromCypressEnv(cyObj); 9 | if (!tmpDirPath) { 10 | throw new EchoedFatalError( 11 | "No directory for Echoed's log. not using reporter?", 12 | ); 13 | } 14 | 15 | const fsContainer = buildFsContainerForApp(); 16 | const tmpDir = fsContainer.newDirectory(tmpDirPath); 17 | 18 | const fileSpace = new FileSpace(tmpDir); 19 | 20 | return fileSpace; 21 | } 22 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/util/headers.ts: -------------------------------------------------------------------------------- 1 | export function normalizeHeaders( 2 | headers: Record, 3 | ): Map { 4 | const normalizedHeaders = new Map(); 5 | 6 | for (const [key, value] of Object.entries(headers)) { 7 | if (Array.isArray(value)) { 8 | normalizedHeaders.set(key, value); 9 | } else { 10 | normalizedHeaders.set(key, [value]); 11 | } 12 | } 13 | return normalizedHeaders; 14 | } 15 | 16 | export function normalizeRequestOptionHeaders( 17 | headers: Partial["headers"], 18 | ): Map { 19 | const normalized = new Map(); 20 | 21 | if (!headers) return normalized; 22 | for (const [key, value] of Object.entries(headers)) { 23 | if (Array.isArray(value)) { 24 | normalized.set( 25 | key, 26 | value.filter((v): v is string => typeof v === "string"), 27 | ); 28 | } else { 29 | if (typeof value === "string") { 30 | normalized.set(key, [value]); 31 | } 32 | } 33 | } 34 | 35 | return normalized; 36 | } 37 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/util/promisify.ts: -------------------------------------------------------------------------------- 1 | // Derived from https://github.com/NicholasBoll/cypress-promis 2 | export function promisify(chain: Cypress.Chainable): Promise { 3 | return new Cypress.Promise((resolve, reject) => { 4 | // We must subscribe to failures and bail. Without this, the Cypress runner would never stop 5 | Cypress.on("fail", rejectPromise); 6 | 7 | // unsubscribe from test failure on both success and failure. This cleanup is essential 8 | function resolvePromise(value: T): void { 9 | resolve(value); 10 | Cypress.off("fail", rejectPromise); 11 | } 12 | function rejectPromise(error: unknown): void { 13 | reject(error); 14 | Cypress.off("fail", rejectPromise); 15 | } 16 | 17 | chain.then(resolvePromise); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/integration/cypress/internal/util/request.ts: -------------------------------------------------------------------------------- 1 | import { buildNotDisplayableMessage } from "@/integration/common/util/response"; 2 | 3 | export function toDisplayableRequestBody(body: unknown): string | null { 4 | if (body === null || body === undefined) { 5 | return null; 6 | } 7 | 8 | if (typeof body === "string") { 9 | return body; 10 | } 11 | 12 | return buildNotDisplayableMessage(); 13 | } 14 | -------------------------------------------------------------------------------- /src/integration/cypress/nodeEvents/index.ts: -------------------------------------------------------------------------------- 1 | export { install } from "@/integration/cypress/nodeEvents/nodeEvents"; 2 | -------------------------------------------------------------------------------- /src/integration/cypress/reporter/index.ts: -------------------------------------------------------------------------------- 1 | import { CypressReporter } from "@/integration/cypress/reporter/cypressReporter"; 2 | 3 | export { CypressReporter }; 4 | -------------------------------------------------------------------------------- /src/integration/cypress/support/command.ts: -------------------------------------------------------------------------------- 1 | import { RunInfo } from "@/integration/cypress/internal/runInfo"; 2 | import { request } from "@/integration/cypress/support/command/request"; 3 | import { waitForSpan } from "@/integration/cypress/support/command/waitForSpan"; 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | type Cypress = typeof import("cypress"); 6 | 7 | export function installCommands(runInfo: RunInfo): void { 8 | Cypress.Commands.add("waitForSpan", waitForSpan); 9 | 10 | Cypress.Commands.overwrite("request", (originalFn, ...origArgs) => { 11 | return request(runInfo, originalFn, ...origArgs); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/integration/jest/internal/util/fetchPatch.ts: -------------------------------------------------------------------------------- 1 | import { IFileLogger } from "@/fileLog/iFileLogger"; 2 | import { TestActionLogger } from "@/fileLog/testActionLogger"; 3 | import { FetchRunner } from "@/integration/jest/internal/fetchRunner"; 4 | import type { Global } from "@jest/types"; 5 | 6 | let originalFetch: ( 7 | input: RequestInfo | URL, 8 | init?: RequestInit, 9 | ) => Promise; 10 | 11 | export function patchFetch( 12 | logger: IFileLogger, 13 | testPath: string, 14 | global: Global.Global, 15 | ): void { 16 | const testActionLogger = new TestActionLogger(logger); 17 | 18 | const fetchRunner = new FetchRunner(testActionLogger, global.fetch, testPath); 19 | const customFetch = async ( 20 | input: RequestInfo | URL, 21 | init?: RequestInit, 22 | ): Promise => { 23 | return fetchRunner.run(input, init); 24 | }; 25 | originalFetch = global.fetch; 26 | 27 | global.fetch = customFetch; 28 | } 29 | 30 | export function restoreFetch(global: Global.Global): void { 31 | global.fetch = originalFetch; 32 | } 33 | -------------------------------------------------------------------------------- /src/integration/jest/nodeEnvironment/index.ts: -------------------------------------------------------------------------------- 1 | import { JestNodeEnvironment } from "./jestNodeEnvironment"; 2 | 3 | export default JestNodeEnvironment; 4 | -------------------------------------------------------------------------------- /src/integration/jest/reporter/index.ts: -------------------------------------------------------------------------------- 1 | import { JestReporter } from "./jestReporter"; 2 | 3 | export default JestReporter; 4 | -------------------------------------------------------------------------------- /src/integration/jest/reporter/testCase.ts: -------------------------------------------------------------------------------- 1 | import { TestCase } from "@/type/testCase"; 2 | 3 | export class TestCaseStartInfo { 4 | constructor( 5 | public testId: string, 6 | public file: string, 7 | public name: string, 8 | public startTimeMillis: number, 9 | ) {} 10 | 11 | toTestCaseElement( 12 | status: string, 13 | duration: number, 14 | testEndTimeMillis: number, 15 | failureDetails: string[] | undefined, 16 | failureMessages: string[] | undefined, 17 | ): TestCase { 18 | return new TestCase( 19 | this.testId, 20 | this.file, 21 | this.name, 22 | this.startTimeMillis, 23 | status, 24 | duration, 25 | testEndTimeMillis, 26 | failureDetails, 27 | failureMessages, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/integration/playwright/command/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | waitForSpanCreatedIn, 3 | waitForSpanFromPlaywrightFetch, 4 | } from "@/integration/playwright/command/span"; 5 | -------------------------------------------------------------------------------- /src/integration/playwright/globalSetup/globalSetup.ts: -------------------------------------------------------------------------------- 1 | import { Config, ECHOED_CONFIG_FILE_NAME } from "@/config/config"; 2 | import { buildFsContainerForApp } from "@/fs/fsContainer"; 3 | import { LocalFile } from "@/fs/localFile"; 4 | import { throwError } from "@/integration/common/util/error"; 5 | import { SetupRunner } from "@/integration/playwright/globalSetup/setupRunner"; 6 | import { type FullConfig } from "@playwright/test"; 7 | import path from "path"; 8 | 9 | type TeardownFn = () => Promise; 10 | 11 | export default async function globalSetup( 12 | _playwrightConfig: FullConfig, 13 | ): Promise { 14 | const fsContainer = buildFsContainerForApp(); 15 | const configFilepath = path.join(process.cwd(), ECHOED_CONFIG_FILE_NAME); 16 | const configFile = new LocalFile(configFilepath); 17 | 18 | try { 19 | const echoedConfig = Config.load(fsContainer, configFile); 20 | 21 | const runner = new SetupRunner(fsContainer, echoedConfig); 22 | return await runner.run(); 23 | } catch (e) { 24 | throwError(e); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/integration/playwright/globalSetup/index.ts: -------------------------------------------------------------------------------- 1 | import globalSetup from "@/integration/playwright/globalSetup/globalSetup"; 2 | 3 | export default globalSetup; 4 | -------------------------------------------------------------------------------- /src/integration/playwright/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@/integration/playwright/command/index"; 2 | -------------------------------------------------------------------------------- /src/integration/playwright/internal/type.ts: -------------------------------------------------------------------------------- 1 | export type PlaywrightCore = typeof import("playwright-core"); 2 | -------------------------------------------------------------------------------- /src/integration/playwright/internal/util/fileSpace.ts: -------------------------------------------------------------------------------- 1 | import { EchoedFatalError } from "@/echoedFatalError"; 2 | import { FileSpace } from "@/fileSpace/fileSpace"; 3 | import { buildFsContainerForApp } from "@/fs/fsContainer"; 4 | import { getTmpDirFromEnv } from "@/integration/common/util/env"; 5 | 6 | export const getFileSpace = (): FileSpace => { 7 | const tmpDirPath = getTmpDirFromEnv(); 8 | if (!tmpDirPath) { 9 | throw new EchoedFatalError( 10 | "No directory for Echoed's log. not using Echoed reporter?", 11 | ); 12 | } 13 | const fsContainer = buildFsContainerForApp(); 14 | const tmpDir = fsContainer.newDirectory(tmpDirPath); 15 | return new FileSpace(tmpDir); 16 | }; 17 | -------------------------------------------------------------------------------- /src/integration/playwright/reporter/index.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightReporter } from "@/integration/playwright/reporter/playwrightReporter"; 2 | 3 | export = PlaywrightReporter; 4 | -------------------------------------------------------------------------------- /src/integration/playwright/test/fixture.ts: -------------------------------------------------------------------------------- 1 | import { extendTest } from "@/integration/playwright/test/wrapper/fixture"; 2 | import { test as playwrightTest } from "@playwright/test"; 3 | 4 | export const test = extendTest(playwrightTest); 5 | -------------------------------------------------------------------------------- /src/integration/playwright/test/index.ts: -------------------------------------------------------------------------------- 1 | export { test } from "@/integration/playwright/test/fixture"; 2 | -------------------------------------------------------------------------------- /src/integration/playwright/test/wrapper/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This directory is not to import "@playwright/test" except its type information. 3 | * 4 | * When node_modules resolves "@playwright/test" in user's code and Echoed, it returns different instance sometimes. 5 | * In that case, Playwright raises an error saying "Requiring @playwright/test second time". 6 | * To avoid the error, this directory doesn't import "@playwright/test" but takes arguments which reference user's "@playwright/test" instance. 7 | * 8 | * c.f. https://github.com/microsoft/playwright/issues/24300#issuecomment-1641927651 9 | */ 10 | 11 | export { extendTest } from "@/integration/playwright/test/wrapper/fixture"; 12 | -------------------------------------------------------------------------------- /src/report/fetchInfo.ts: -------------------------------------------------------------------------------- 1 | export type FetchInfo = { 2 | traceId: string; 3 | request: { 4 | url: string; 5 | method: string; 6 | body?: string; 7 | }; 8 | response: 9 | | { 10 | status: number; 11 | body?: string; 12 | } 13 | | { failed: true; reason: string }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/report/iReportFile.ts: -------------------------------------------------------------------------------- 1 | import { CoverageResult } from "@/coverage/coverageResult"; 2 | import { IFile } from "@/fs/IFile"; 3 | import { TestResult } from "@/report/testResult"; 4 | 5 | export interface IReportFile { 6 | generate( 7 | testResult: TestResult, 8 | coverageResult: CoverageResult, 9 | ): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /src/report/otelLogRecordConverter.ts: -------------------------------------------------------------------------------- 1 | import { convertAnyValue } from "@/report/otelAnyValueConverter"; 2 | import { OtelLogRecord } from "@/type/otelLogRecord"; 3 | import { toHexString } from "@/util/byte"; 4 | import { ILogRecord } from "@shared/type/echoedParam"; 5 | 6 | export class OtelLogRecordConverter { 7 | static convertAll(logRecords: OtelLogRecord[]): ILogRecord[] { 8 | return logRecords.map((logRecord) => 9 | new OtelLogRecordConverter().convert(logRecord), 10 | ); 11 | } 12 | 13 | convert(logRecord: OtelLogRecord): ILogRecord { 14 | return { 15 | timeUnixNano: logRecord.timeUnixNano?.toString(), 16 | traceId: toHexString(logRecord.traceId), 17 | spanId: toHexString(logRecord.spanId), 18 | body: logRecord.body ? convertAnyValue(logRecord.body) : undefined, 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/report/testCaseResult.ts: -------------------------------------------------------------------------------- 1 | import { FetchInfo } from "@/report/fetchInfo"; 2 | import { TestCase } from "@/type/testCase"; 3 | 4 | export class TestCaseResult { 5 | constructor( 6 | private testCase: TestCase, 7 | readonly orderedTraceIds: string[], 8 | readonly fetches: FetchInfo[], 9 | ) {} 10 | 11 | get testId(): string { 12 | return this.testCase.testId; 13 | } 14 | 15 | get file(): string { 16 | return this.testCase.file; 17 | } 18 | 19 | get name(): string { 20 | return this.testCase.name; 21 | } 22 | 23 | get startTimeMillis(): number { 24 | return this.testCase.startTimeMillis; 25 | } 26 | 27 | get status(): string { 28 | return this.testCase.status; 29 | } 30 | 31 | get duration(): number { 32 | return this.testCase.duration; 33 | } 34 | 35 | get failureDetails(): string[] | undefined { 36 | return this.testCase.failureDetails; 37 | } 38 | 39 | get failureMessages(): string[] | undefined { 40 | return this.testCase.failureMessages; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/report/testFailedError.ts: -------------------------------------------------------------------------------- 1 | import { EchoedError } from "@/echoedError"; 2 | 3 | export class TestFailedError extends EchoedError { 4 | constructor(message: string) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/scenario/compile/common/actRunner.ts: -------------------------------------------------------------------------------- 1 | import { RunnerContainer } from "@/scenario/compile/common/runnerContainer"; 2 | 3 | export class ActRunner extends RunnerContainer { 4 | constructor(container: RunnerContainer) { 5 | super(container.name, container.argument, container.option); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/scenario/compile/common/arrangeRunner.test.ts: -------------------------------------------------------------------------------- 1 | import { ArrangeRunner } from "@/scenario/compile/common/arrangeRunner"; 2 | import { RunnerContainer } from "@/scenario/compile/common/runnerContainer"; 3 | import { RunnerOption } from "@/scenario/compile/common/runnerOption"; 4 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 5 | 6 | describe("ArrangeRunner", () => { 7 | describe("boundVariables", () => { 8 | it("should return bound variables", () => { 9 | const runner = new ArrangeRunner( 10 | new RunnerContainer( 11 | "dummyRunner", 12 | undefined, 13 | new RunnerOption(new Map()), 14 | ), 15 | new Map([ 16 | ["foo", TsVariable.parse("fooVar")], 17 | ["bar", TsVariable.parse("barVar")], 18 | ]), 19 | ); 20 | 21 | expect(runner.boundVariables().sort()).toEqual(["bar", "foo"]); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/scenario/compile/common/arrangeRunner.ts: -------------------------------------------------------------------------------- 1 | import { RunnerContainer } from "@/scenario/compile/common/runnerContainer"; 2 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 3 | 4 | export class ArrangeRunner extends RunnerContainer { 5 | constructor( 6 | container: RunnerContainer, 7 | public readonly bind: Map, 8 | ) { 9 | super(container.name, container.argument, container.option); 10 | } 11 | 12 | boundVariables(): string[] { 13 | return [...this.bind.keys()]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/scenario/compile/common/assert.test.ts: -------------------------------------------------------------------------------- 1 | import { Assert } from "@/scenario/compile/common/assert"; 2 | import { Asserter } from "@/scenario/compile/common/asserter"; 3 | import { RawString } from "@/scenario/compile/common/rawString"; 4 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 5 | import { buildConfig } from "@/testUtil/scenario/util"; 6 | 7 | describe("Assert", () => { 8 | const config = buildConfig(); 9 | 10 | describe("parse", () => { 11 | it("should parse string", () => { 12 | const assert = Assert.parse(config, "foo"); 13 | 14 | expect(assert.tsString).toEqual(new RawString("foo")); 15 | expect(assert.asserter).toBeUndefined(); 16 | }); 17 | 18 | it("should parse asserter", () => { 19 | const assert = Assert.parse(config, { dummyAsserter: [1, 2] }); 20 | 21 | expect(assert.tsString).toBeUndefined(); 22 | expect(assert.asserter).toEqual( 23 | new Asserter("dummyAsserter", new TsVariable(1), new TsVariable(2)), 24 | ); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/scenario/compile/common/assert.ts: -------------------------------------------------------------------------------- 1 | import { Asserter, AsserterSchema } from "@/scenario/compile/common/asserter"; 2 | import { Config } from "@/scenario/compile/common/config"; 3 | import { RawString } from "@/scenario/compile/common/rawString"; 4 | import { TsString } from "@/scenario/compile/common/tsString"; 5 | import { z } from "zod"; 6 | 7 | export const AssertSchema = z.string().or(AsserterSchema); 8 | export type AssertSchema = z.infer; 9 | 10 | export class Assert { 11 | static parse(config: Config, schema: AssertSchema): Assert { 12 | if (typeof schema === "string") { 13 | return new Assert(new RawString(schema)); 14 | } else { 15 | return new Assert(undefined, Asserter.parse(config, schema)); 16 | } 17 | } 18 | 19 | constructor( 20 | public readonly tsString?: TsString, 21 | public readonly asserter?: Asserter, 22 | ) {} 23 | } 24 | -------------------------------------------------------------------------------- /src/scenario/compile/common/assertConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { AsserterConfig } from "@/scenario/compile/common/asserterConfig"; 2 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 3 | 4 | describe("AsserterConfig", () => { 5 | describe("parse", () => { 6 | it("should parse", () => { 7 | const config = AsserterConfig.parse({ 8 | module: "echoed/dummy/asserter", 9 | name: "dummyAsserter", 10 | option: { 11 | foo: "bar", 12 | complex: { foo: { bar: "buz" } }, 13 | }, 14 | }); 15 | 16 | expect(config.module).toBe("echoed/dummy/asserter"); 17 | expect(config.name).toBe("dummyAsserter"); 18 | expect(config.option).toEqual( 19 | new Map([ 20 | ["foo", TsVariable.parse("bar")], 21 | ["complex", TsVariable.parse({ foo: { bar: "buz" } })], 22 | ]), 23 | ); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/scenario/compile/common/asserterConfig.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioCompilePluginAsserterConfig } from "@/config/scenarioCompileConfig"; 2 | import { InvalidScenarioError } from "@/scenario/compile/common/invalidScenarioError"; 3 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 4 | import { transformToMap } from "@/util/record"; 5 | 6 | export class AsserterConfig { 7 | static parse(conf: ScenarioCompilePluginAsserterConfig): AsserterConfig { 8 | if (conf.name.startsWith("_")) { 9 | throw new InvalidScenarioError( 10 | `Asserter name must not start with "_". This is reserved for internal use: ${conf.name}`, 11 | ); 12 | } 13 | 14 | const option = transformToMap(conf.option, (v) => TsVariable.parse(v)); 15 | return new AsserterConfig(conf.name, conf.module, option); 16 | } 17 | 18 | constructor( 19 | public readonly name: string, 20 | public readonly module: string, 21 | public readonly option: Map, 22 | ) {} 23 | } 24 | -------------------------------------------------------------------------------- /src/scenario/compile/common/envConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { EnvConfig } from "@/scenario/compile/common/envConfig"; 2 | 3 | describe("EnvConfig", () => { 4 | describe("parse", () => { 5 | it("should parse", () => { 6 | const config = EnvConfig.parse({ 7 | foo: "bar", 8 | buz: null, 9 | }); 10 | 11 | expect(config).toEqual( 12 | new EnvConfig( 13 | new Map([ 14 | ["foo", "bar"], 15 | ["buz", null], 16 | ]), 17 | ), 18 | ); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/scenario/compile/common/envConfig.ts: -------------------------------------------------------------------------------- 1 | import { transformToMap } from "@/util/record"; 2 | 3 | export class EnvConfig { 4 | static parse(schema: Record): EnvConfig { 5 | const envMap = transformToMap(schema, (value) => value); 6 | 7 | return new EnvConfig(envMap); 8 | } 9 | 10 | constructor(public readonly map: Map) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/scenario/compile/common/hookExecutorBase.test.ts: -------------------------------------------------------------------------------- 1 | import { HookExecutorBase } from "@/scenario/compile/common/hookExecutorBase"; 2 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 3 | 4 | export class DummyHookExecutor extends HookExecutorBase { 5 | constructor(bind?: Map) { 6 | super(bind); 7 | } 8 | } 9 | 10 | describe("HookExecutorBase", () => { 11 | describe("boundVariables", () => { 12 | describe("when bind", () => { 13 | const variables = new Map([ 14 | ["foo", TsVariable.parse("bar")], 15 | ["buz", TsVariable.parse(123)], 16 | ]); 17 | const hookExecutor = new DummyHookExecutor(variables); 18 | 19 | it("should return bound variables", () => { 20 | expect(hookExecutor.boundVariables().sort()).toEqual(["buz", "foo"]); 21 | }); 22 | }); 23 | 24 | describe("when no bind", () => { 25 | const hookExecutor = new DummyHookExecutor(); 26 | 27 | it("should return empty", () => { 28 | expect(hookExecutor.boundVariables()).toEqual([]); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/scenario/compile/common/hookExecutorBase.ts: -------------------------------------------------------------------------------- 1 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 2 | 3 | export abstract class HookExecutorBase { 4 | protected constructor(public readonly bind?: Map) {} 5 | 6 | boundVariables(): string[] { 7 | return [...(this.bind?.keys() ?? [].values())]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/scenario/compile/common/invalidScenarioError.ts: -------------------------------------------------------------------------------- 1 | import { EchoedError } from "@/echoedError"; 2 | 3 | export class InvalidScenarioError extends EchoedError { 4 | constructor(message: string) { 5 | super(`Invalid scenario. ${message}`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/scenario/compile/common/pluginLister.ts: -------------------------------------------------------------------------------- 1 | export interface PluginLister { 2 | getUsedRunners(): Set; 3 | getUsedAsserters(): Set; 4 | } 5 | -------------------------------------------------------------------------------- /src/scenario/compile/common/rawString.test.ts: -------------------------------------------------------------------------------- 1 | import { RawString } from "@/scenario/compile/common/rawString"; 2 | 3 | describe("RawString", () => { 4 | describe("toTsLine", () => { 5 | it("should return value as-is", () => { 6 | const value = "foo-`bar`"; 7 | const rawString = new RawString(value); 8 | 9 | expect(rawString.toTsLine()).toBe(value); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/scenario/compile/common/rawString.ts: -------------------------------------------------------------------------------- 1 | import { TsString } from "@/scenario/compile/common/tsString"; 2 | 3 | export class RawString extends TsString { 4 | constructor(public readonly value: string) { 5 | super(); 6 | } 7 | 8 | override toTsLine(): string { 9 | return this.value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/scenario/compile/common/runnerConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { RunnerConfig } from "@/scenario/compile/common/runnerConfig"; 2 | import { RunnerOption } from "@/scenario/compile/common/runnerOption"; 3 | import { TemplateString } from "@/scenario/compile/common/templateString"; 4 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 5 | 6 | describe("RunnerConfig", () => { 7 | describe("parse", () => { 8 | it("should returns RunnerConfig", () => { 9 | const runnerConfig = RunnerConfig.parse({ 10 | module: "echoed/dummy2/runner", 11 | name: "runner2", 12 | option: { 13 | foo: "bar", 14 | baz: 1, 15 | }, 16 | }); 17 | 18 | expect(runnerConfig).toEqual( 19 | new RunnerConfig( 20 | "runner2", 21 | "echoed/dummy2/runner", 22 | new RunnerOption( 23 | new Map([ 24 | ["foo", new TsVariable(new TemplateString("bar"))], 25 | ["baz", new TsVariable(1)], 26 | ]), 27 | ), 28 | ), 29 | ); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/scenario/compile/common/runnerConfig.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioCompilePluginRunnerConfig } from "@/config/scenarioCompileConfig"; 2 | import { InvalidScenarioError } from "@/scenario/compile/common/invalidScenarioError"; 3 | import { RunnerOption } from "@/scenario/compile/common/runnerOption"; 4 | 5 | export class RunnerConfig { 6 | static parse(conf: ScenarioCompilePluginRunnerConfig): RunnerConfig { 7 | if (conf.name.startsWith("_")) { 8 | throw new InvalidScenarioError( 9 | `Runner name must not start with "_". This is reserved for internal use: ${conf.name}`, 10 | ); 11 | } 12 | 13 | return new RunnerConfig( 14 | conf.name, 15 | conf.module, 16 | RunnerOption.parse(conf.option), 17 | ); 18 | } 19 | 20 | constructor( 21 | public readonly name: string, 22 | public readonly module: string, 23 | public readonly option: RunnerOption, 24 | ) {} 25 | } 26 | -------------------------------------------------------------------------------- /src/scenario/compile/common/runnerOption.test.ts: -------------------------------------------------------------------------------- 1 | import { RawString } from "@/scenario/compile/common/rawString"; 2 | import { RunnerOption } from "@/scenario/compile/common/runnerOption"; 3 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 4 | 5 | describe("RunnerOption", () => { 6 | describe("parse", () => { 7 | it("should return RunnerOption", () => { 8 | const runnerOption = RunnerOption.parse({ 9 | foo: "bar", 10 | baz: 1, 11 | }); 12 | 13 | expect(runnerOption).toEqual( 14 | new RunnerOption( 15 | new Map([ 16 | ["foo", new TsVariable(new RawString("bar"))], 17 | ["baz", new TsVariable(1)], 18 | ]), 19 | ), 20 | ); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/scenario/compile/common/runnerOption.ts: -------------------------------------------------------------------------------- 1 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 2 | import { JsonSchema } from "@/type/jsonZod"; 3 | import { transformToMap } from "@/util/record"; 4 | 5 | export class RunnerOption { 6 | static parse(record: Record | undefined): RunnerOption { 7 | const option = transformToMap(record, (v) => TsVariable.parse(v)); 8 | 9 | return new RunnerOption(option); 10 | } 11 | 12 | constructor(private readonly option: Map) {} 13 | 14 | entries(): IterableIterator<[string, TsVariable]> { 15 | return this.option.entries(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/scenario/compile/common/scenarioBase.ts: -------------------------------------------------------------------------------- 1 | import { StepBase } from "@/scenario/compile/common/stepBase"; 2 | import { escapeTemplateString } from "@/scenario/compile/common/util"; 3 | 4 | export abstract class ScenarioBase { 5 | protected constructor( 6 | public readonly name: string, 7 | public readonly steps: T[], 8 | ) {} 9 | 10 | getBoundVariablesBefore(index: number): string[] { 11 | const variables = new Set(); 12 | 13 | for (let i = 0; i < index && index < this.steps.length; i++) { 14 | for (const variable of this.steps[i].boundVariables()) { 15 | variables.add(variable); 16 | } 17 | } 18 | return [...variables.values()]; 19 | } 20 | 21 | get escapedName(): string { 22 | return escapeTemplateString(this.name); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/scenario/compile/common/scenarioRunnerConfig.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOption } from "@/scenario/compile/common/runnerOption"; 2 | import { JsonSchema } from "@/type/jsonZod"; 3 | import { z } from "zod"; 4 | 5 | export const ScenarioRunnerConfigSchema = z.strictObject({ 6 | name: z.string(), 7 | option: z.record(JsonSchema).optional(), 8 | }); 9 | export type RunnerOptionSchema = z.infer; 10 | 11 | export class ScenarioRunnerConfig { 12 | static parse(schema: RunnerOptionSchema): ScenarioRunnerConfig { 13 | const name = schema.name; 14 | const option = RunnerOption.parse(schema.option); 15 | 16 | return new ScenarioRunnerConfig(name, option); 17 | } 18 | 19 | constructor( 20 | public readonly name: string, 21 | public readonly option: RunnerOption, 22 | ) {} 23 | } 24 | -------------------------------------------------------------------------------- /src/scenario/compile/common/templateString.test.ts: -------------------------------------------------------------------------------- 1 | import { TemplateString } from "@/scenario/compile/common/templateString"; 2 | 3 | describe("TemplateString", () => { 4 | describe("toTsLine", () => { 5 | it("should return template string", () => { 6 | const templateString = new TemplateString("foo`bar"); 7 | 8 | expect(templateString.toTsLine()).toEqual("`foo\\`bar`"); 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/scenario/compile/common/templateString.ts: -------------------------------------------------------------------------------- 1 | import { TsString } from "@/scenario/compile/common/tsString"; 2 | import { escapeTemplateString } from "@/scenario/compile/common/util"; 3 | 4 | export class TemplateString extends TsString { 5 | constructor(public readonly value: string) { 6 | super(); 7 | } 8 | 9 | override toTsLine(): string { 10 | return `\`${escapeTemplateString(this.value)}\``; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/scenario/compile/common/tsString.ts: -------------------------------------------------------------------------------- 1 | export abstract class TsString { 2 | abstract toTsLine(): string; 3 | } 4 | -------------------------------------------------------------------------------- /src/scenario/compile/common/typeUtil.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioBase } from "@/scenario/compile/common/scenarioBase"; 2 | import { ScenarioBookBase } from "@/scenario/compile/common/scenarioBookBase"; 3 | 4 | // TODO: create function to extract generics type from ScenarioBaseType or ScenarioBookBaseType 5 | export type ScenarioBaseType = Type extends ScenarioBase 6 | ? X 7 | : never; 8 | 9 | export type ScenarioBookBaseType = Type extends ScenarioBookBase 10 | ? X 11 | : never; 12 | -------------------------------------------------------------------------------- /src/scenario/compile/common/util.test.ts: -------------------------------------------------------------------------------- 1 | import { escapeTemplateString } from "@/scenario/compile/common/util"; 2 | 3 | describe("escapeTemplateString", () => { 4 | describe("when str contains back quote", () => { 5 | it("should return str with back slash", () => { 6 | expect(escapeTemplateString("foo`bar")).toEqual("foo\\`bar"); 7 | }); 8 | }); 9 | 10 | describe("when str doesn't contain back quote", () => { 11 | it("should return str as-is", () => { 12 | expect(escapeTemplateString("foo")).toEqual("foo"); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/scenario/compile/common/util.ts: -------------------------------------------------------------------------------- 1 | export function escapeTemplateString(str: string): string { 2 | return str.replace(/`/g, "\\`"); 3 | } 4 | -------------------------------------------------------------------------------- /src/scenario/compile/jest/scenarioBookParser.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "@/scenario/compile/common/config"; 2 | import { 3 | ScenarioBook, 4 | ScenarioBookSchema, 5 | } from "@/scenario/compile/jest/scenarioBook"; 6 | import { ZodError } from "zod"; 7 | 8 | export class ScenarioBookParser { 9 | constructor(private config: Config) {} 10 | 11 | parse( 12 | ymlObject: unknown, 13 | ): ScenarioBook | { success: false; error: ZodError } { 14 | const scenarioYamlSchema = ScenarioBookSchema.safeParse(ymlObject); 15 | 16 | if (!scenarioYamlSchema.success) { 17 | return scenarioYamlSchema; 18 | } 19 | 20 | return ScenarioBook.parse(this.config, scenarioYamlSchema.data); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/scenario/compile/jest/testdata/invalidScenario.yml: -------------------------------------------------------------------------------- 1 | scenarios: 2 | - variable: 3 | -------------------------------------------------------------------------------- /src/scenario/compile/jest/testdata/scenario.yml: -------------------------------------------------------------------------------- 1 | scenarios: 2 | - name: Simple test should pass 3 | variable: 4 | session: ${defaultSession()} 5 | steps: 6 | - description: Get cart 7 | arrange: 8 | - runner: fetch 9 | argument: 10 | endpoint: arrange 11 | act: 12 | runner: fetch 13 | argument: 14 | endpoint: /cart?sessionId=${session.userId}¤cyCode=${session.currencyCode} 15 | assert: 16 | - assertStatus: 17 | - ${_.response} 18 | - 200 19 | - expect(_.jsonBody.items.length).toBe(0) 20 | -------------------------------------------------------------------------------- /src/scenario/compile/jest/testdata/scenario/nest1/nest2/in_nest.yml: -------------------------------------------------------------------------------- 1 | scenarios: 2 | - name: Scenario in nest 3 | steps: 4 | - description: Execute fetch 5 | act: 6 | runner: fetch 7 | argument: 8 | endpoint: /in_nest 9 | assert: 10 | - expect(_).toBe("in nest") 11 | -------------------------------------------------------------------------------- /src/scenario/compile/jest/testdata/scenario/simple.yml: -------------------------------------------------------------------------------- 1 | scenarios: 2 | - name: Simple scenario 3 | steps: 4 | - act: 5 | runner: fetch 6 | argument: 7 | endpoint: /simple 8 | assert: 9 | - expect(_).toBe("in simple") 10 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/arrange.test.ts: -------------------------------------------------------------------------------- 1 | import { RawString } from "@/scenario/compile/common/rawString"; 2 | import { parseArrange } from "@/scenario/compile/playwright/arrange"; 3 | import { LocatorAssertionString } from "@/scenario/compile/playwright/locatorAssertionString"; 4 | import { buildConfig } from "@/testUtil/scenario/util"; 5 | 6 | describe("parseArrange", () => { 7 | const config = buildConfig(); 8 | 9 | describe("when schema is AssertionShortcutSchema", () => { 10 | it("should returnArrange including AssertionShortcut", () => { 11 | const schema = { expectToBeAttached: "q.q.q" }; 12 | 13 | const arrange = parseArrange(config, schema); 14 | expect(arrange.tsString).toStrictEqual( 15 | new LocatorAssertionString("toBeAttached", "q.q.q", [], false), 16 | ); 17 | }); 18 | }); 19 | 20 | describe("when schema is not AssertionShortcutSchema", () => { 21 | it("should return Arrange", () => { 22 | const arrange = parseArrange(config, "foo"); 23 | expect(arrange.tsString).toStrictEqual(new RawString("foo")); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/arrange.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Arrange, 3 | ArrangeSchema as CommonArrangeSchema, 4 | } from "@/scenario/compile/common/arrange"; 5 | import { Config } from "@/scenario/compile/common/config"; 6 | import { 7 | AssertionShortcutSchema, 8 | isAssertionShortcutSchema, 9 | parseAssertionShortcutSchema, 10 | } from "@/scenario/compile/playwright/assertionShortcut"; 11 | import { z } from "zod"; 12 | 13 | export const ArrangeSchema = z.union([ 14 | CommonArrangeSchema, 15 | AssertionShortcutSchema, 16 | ]); 17 | export type ArrangeSchema = z.infer; 18 | 19 | export function parseArrange(config: Config, arrange: ArrangeSchema): Arrange { 20 | if (isAssertionShortcutSchema(arrange)) { 21 | return new Arrange(parseAssertionShortcutSchema(arrange)); 22 | } 23 | 24 | return Arrange.parse(config, arrange); 25 | } 26 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/assert.test.ts: -------------------------------------------------------------------------------- 1 | import { RawString } from "@/scenario/compile/common/rawString"; 2 | import { parseAssert } from "@/scenario/compile/playwright/assert"; 3 | import { LocatorAssertionString } from "@/scenario/compile/playwright/locatorAssertionString"; 4 | import { buildConfig } from "@/testUtil/scenario/util"; 5 | 6 | describe("parseAssert", () => { 7 | const config = buildConfig(); 8 | 9 | describe("when schema is AssertionShortcutSchema", () => { 10 | it("should returnArrange including AssertionShortcut", () => { 11 | const schema = { expectToBeAttached: "q.q.q" }; 12 | 13 | const assert = parseAssert(config, schema); 14 | expect(assert.tsString).toStrictEqual( 15 | new LocatorAssertionString("toBeAttached", "q.q.q", [], false), 16 | ); 17 | }); 18 | }); 19 | 20 | describe("when schema is not AssertionShortcutSchema", () => { 21 | it("should return Arrange", () => { 22 | const assert = parseAssert(config, "foo"); 23 | expect(assert.tsString).toStrictEqual(new RawString("foo")); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/assert.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Assert, 3 | AssertSchema as CommonAssertSchema, 4 | } from "@/scenario/compile/common/assert"; 5 | import { Config } from "@/scenario/compile/common/config"; 6 | import { 7 | AssertionShortcutSchema, 8 | isAssertionShortcutSchema, 9 | parseAssertionShortcutSchema, 10 | } from "@/scenario/compile/playwright/assertionShortcut"; 11 | import { z } from "zod"; 12 | 13 | export const AssertSchema = z.union([ 14 | CommonAssertSchema, 15 | AssertionShortcutSchema, 16 | ]); 17 | export type AssertSchema = z.infer; 18 | 19 | export function parseAssert(config: Config, assert: AssertSchema): Assert { 20 | if (isAssertionShortcutSchema(assert)) { 21 | return new Assert(parseAssertionShortcutSchema(assert)); 22 | } 23 | 24 | return Assert.parse(config, assert); 25 | } 26 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/fixtures.test.ts: -------------------------------------------------------------------------------- 1 | import { Fixtures } from "@/scenario/compile/playwright/fixtures"; 2 | 3 | describe("Fixtures", () => { 4 | describe("toTs", () => { 5 | describe("when fixtures is empty", () => { 6 | it("should return empty string", () => { 7 | const fixtures = new Fixtures([]); 8 | expect(fixtures.toTs()).toBe(""); 9 | }); 10 | }); 11 | 12 | describe("when fixtures is not empty", () => { 13 | it("should return joined fixtures", () => { 14 | const fixtures = new Fixtures(["foo", "bar"]); 15 | expect(fixtures.toTs()).toBe("foo, bar"); 16 | }); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const FixturesSchema = z.array(z.string()); 4 | export type FixturesSchema = z.infer; 5 | 6 | export class Fixtures { 7 | static parse(schema: FixturesSchema): Fixtures { 8 | return new Fixtures(schema); 9 | } 10 | 11 | constructor(public readonly fixtures: string[]) {} 12 | 13 | toTs(): string { 14 | return this.fixtures.join(", "); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/locatorAssertionString.ts: -------------------------------------------------------------------------------- 1 | import { TsString } from "@/scenario/compile/common/tsString"; 2 | 3 | // TsString for https://playwright.dev/docs/api/class-locatorassertions 4 | export class LocatorAssertionString extends TsString { 5 | constructor( 6 | private readonly method: string, 7 | private readonly selector: string, 8 | private readonly args: TsString[], 9 | private readonly usesNot: boolean, 10 | ) { 11 | super(); 12 | } 13 | 14 | toTsLine(): string { 15 | const args = this.args.map((arg) => arg.toTsLine()).join(", "); 16 | const notText = this.usesNot ? "not." : ""; 17 | return `await expect(page.locator("${this.selector}")).${notText}${this.method}(${args});`; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/pageAssertionString.ts: -------------------------------------------------------------------------------- 1 | import { TsString } from "@/scenario/compile/common/tsString"; 2 | 3 | // TsString for https://playwright.dev/docs/api/class-pageassertions 4 | export class PageAssertionString extends TsString { 5 | constructor( 6 | private readonly method: string, 7 | private readonly args: TsString[], 8 | private readonly usesNot: boolean, 9 | ) { 10 | super(); 11 | } 12 | 13 | toTsLine(): string { 14 | const args = this.args.map((arg) => arg.toTsLine()).join(", "); 15 | const notText = this.usesNot ? "not." : ""; 16 | return `await expect(page).${notText}${this.method}(${args});`; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/scenarioBookParser.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "@/scenario/compile/common/config"; 2 | import { 3 | ScenarioBook, 4 | ScenarioBookSchema, 5 | } from "@/scenario/compile/playwright/scenarioBook"; 6 | import { ZodError } from "zod"; 7 | 8 | export class ScenarioBookParser { 9 | constructor(private config: Config) {} 10 | 11 | parse( 12 | ymlObject: unknown, 13 | ): ScenarioBook | { success: false; error: ZodError } { 14 | const scenarioYamlSchema = ScenarioBookSchema.safeParse(ymlObject); 15 | 16 | if (!scenarioYamlSchema.success) { 17 | return scenarioYamlSchema; 18 | } 19 | 20 | return ScenarioBook.parse(this.config, scenarioYamlSchema.data); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/testdata/invalidScenario.yml: -------------------------------------------------------------------------------- 1 | scenarios: 2 | - variable: 3 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/testdata/scenario.yml: -------------------------------------------------------------------------------- 1 | scenarios: 2 | - name: Simple test should pass 3 | fixtures: 4 | - page 5 | variable: 6 | session: ${defaultSession()} 7 | steps: 8 | - description: Get cart 9 | arrange: 10 | - expectToBeVisible: "[data-cy=home-page]" 11 | - await page.locator("[data-cy=cart-link]").click() 12 | act: 13 | raw: await page.locator("[data-cy=currency-switcher]").selectOption("EUR") 14 | assert: 15 | - expectToBeVisible: "[data-cy=product-detail]" 16 | - expect(_.jsonBody.items.length).toBe(0) 17 | -------------------------------------------------------------------------------- /src/scenario/compile/playwright/useOption.ts: -------------------------------------------------------------------------------- 1 | import { RawString } from "@/scenario/compile/common/rawString"; 2 | import { TsString } from "@/scenario/compile/common/tsString"; 3 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 4 | import { z } from "zod"; 5 | 6 | export const UseOptionSchema = z.record( 7 | z.string().or(z.strictObject({ raw: z.string() })), 8 | ); 9 | export type UseOptionSchema = z.infer; 10 | 11 | export class UseOption { 12 | static parse(schema: UseOptionSchema): UseOption { 13 | const options = new Map(); 14 | 15 | for (const [key, value] of Object.entries(schema)) { 16 | if (typeof value === "string") { 17 | options.set(key, TsVariable.parse(value)); 18 | } else { 19 | options.set(key, new RawString(value.raw)); 20 | } 21 | } 22 | 23 | return new UseOption(options); 24 | } 25 | 26 | constructor(public readonly options: Map) {} 27 | 28 | size(): number { 29 | return this.options.size; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/scenario/gen/common/context.ts: -------------------------------------------------------------------------------- 1 | import { RunnerResult } from "@/scenario/gen/common/type"; 2 | 3 | type EchoedContextBase = { 4 | scenarioName: string; 5 | 6 | /** 7 | * index starts from 0 8 | */ 9 | currentStepIndex: number; 10 | }; 11 | 12 | export type EchoedArrangeContext = EchoedContextBase & { 13 | kind: "arrange"; 14 | 15 | /** 16 | * index starts from 0 17 | */ 18 | currentArrangeIndex: number; 19 | }; 20 | 21 | export type EchoedActContext = EchoedContextBase & { 22 | kind: "act"; 23 | }; 24 | 25 | export type EchoedAssertContext = EchoedContextBase & { 26 | kind: "assert"; 27 | actResult: RunnerResult; 28 | }; 29 | 30 | export type EchoedContext = EchoedActContext | EchoedArrangeContext; 31 | -------------------------------------------------------------------------------- /src/scenario/gen/common/type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ArrangeResult represents the result of the runner 3 | */ 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export type RunnerResult = any; 6 | 7 | /** 8 | * ArrangeArgument represents the argument for the runner 9 | */ 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export type RunnerArgument = any; 12 | 13 | /** 14 | * AssertArgument represents the argument for the asserter 15 | */ 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | export type AssertArgument = any; 18 | 19 | /** 20 | * Option represents the option having key/value 21 | */ 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | export type Option = Record; 24 | -------------------------------------------------------------------------------- /src/scenario/gen/internal/common/env.ts: -------------------------------------------------------------------------------- 1 | export const buildEnv = ( 2 | envDefinition: Record, 3 | ): Record => { 4 | const env: Record = {}; 5 | for (const [key, defaultValue] of Object.entries(envDefinition)) { 6 | const envValue = process.env[key]; 7 | if (envValue && envValue.length > 0) { 8 | env[key] = envValue; 9 | } else { 10 | if (defaultValue === null) { 11 | throw new Error(`Missing environment variable: ${key}`); 12 | } 13 | env[key] = defaultValue; 14 | } 15 | } 16 | 17 | return env; 18 | }; 19 | -------------------------------------------------------------------------------- /src/scenario/gen/internal/common/hookContext.ts: -------------------------------------------------------------------------------- 1 | import { BoundVariables } from "@/scenario/gen/internal/common/type"; 2 | 3 | export type HookResult = { 4 | newBoundVariables: BoundVariables; 5 | }; 6 | 7 | export class HookContext { 8 | public newBoundVariables: BoundVariables = {}; 9 | 10 | constructor(public readonly boundVariables: BoundVariables) {} 11 | 12 | bindNewVariable(key: string, value: any): void { 13 | this.newBoundVariables[key] = value; 14 | } 15 | 16 | get result(): HookResult { 17 | return { 18 | newBoundVariables: this.newBoundVariables, 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/scenario/gen/internal/common/stepHistory.ts: -------------------------------------------------------------------------------- 1 | import { RunnerResult } from "@/scenario/gen/common/type"; 2 | import { buildRelativeIndexableArray } from "@/util/proxy"; 3 | 4 | type StepContent = { 5 | actResult: RunnerResult; 6 | }; 7 | 8 | export type ActResultHistory = RunnerResult[]; 9 | 10 | export class StepHistory { 11 | private readonly stepContents: StepContent[] = []; 12 | 13 | get currentStepIndex(): number { 14 | return this.stepContents.length - 1; 15 | } 16 | 17 | next(): void { 18 | this.stepContents.push({ 19 | actResult: undefined, 20 | }); 21 | } 22 | 23 | get actResultHistory(): ActResultHistory { 24 | return this.buildActResultHistoryProxy(); 25 | } 26 | 27 | setActResult(response: RunnerResult): void { 28 | this.stepContents[this.currentStepIndex].actResult = response; 29 | } 30 | 31 | private buildActResultHistoryProxy(): ActResultHistory { 32 | const copy = this.stepContents.map((c): RunnerResult => c.actResult); 33 | 34 | return buildRelativeIndexableArray(copy) as ActResultHistory; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/scenario/gen/internal/common/type.ts: -------------------------------------------------------------------------------- 1 | export type BoundVariables = Record; 2 | export type Option = Record; 3 | -------------------------------------------------------------------------------- /src/scenario/gen/internal/jest/index.ts: -------------------------------------------------------------------------------- 1 | export { buildEnv } from "@/scenario/gen/internal/common/env"; 2 | export { ScenarioBookContext } from "@/scenario/gen/internal/common/scenarioBookContext"; 3 | -------------------------------------------------------------------------------- /src/scenario/gen/internal/playwright/index.ts: -------------------------------------------------------------------------------- 1 | export { buildEnv } from "@/scenario/gen/internal/common/env"; 2 | export { ScenarioBookContext } from "@/scenario/gen/internal/common/scenarioBookContext"; 3 | -------------------------------------------------------------------------------- /src/scenario/gen/jest/asserter/assertStatus.ts: -------------------------------------------------------------------------------- 1 | import { EchoedAssertContext } from "@/scenario/gen/common/context"; 2 | import { Option } from "@/scenario/gen/common/type"; 3 | import { Asserter } from "@/scenario/gen/jest/asserter/asserter"; 4 | 5 | /** 6 | * Assert status code of Response of fetch is the same with expected 7 | */ 8 | export const assertStatus = ( 9 | _ctx: EchoedAssertContext, 10 | response: Response, 11 | expectedStatusCode: number, 12 | _option: Option, 13 | ): Promise => { 14 | expect(response.status).toBe(expectedStatusCode); 15 | 16 | return Promise.resolve(); 17 | }; 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 20 | const _check: Asserter = assertStatus; 21 | -------------------------------------------------------------------------------- /src/scenario/gen/jest/asserter/asserter.ts: -------------------------------------------------------------------------------- 1 | import { EchoedAssertContext } from "@/scenario/gen/common/context"; 2 | import { AssertArgument, Option } from "@/scenario/gen/common/type"; 3 | 4 | export interface Asserter { 5 | ( 6 | ctx: EchoedAssertContext, 7 | 8 | /** 9 | * The first argument for asserter. Typically, it's the actual value. 10 | */ 11 | x: AssertArgument, 12 | 13 | /** 14 | * The second argument for asserter. Typically, it's the expected value. 15 | */ 16 | y: AssertArgument, 17 | option: Option, 18 | ): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /src/scenario/gen/jest/asserter/index.ts: -------------------------------------------------------------------------------- 1 | export { assertStatus } from "@/scenario/gen/jest/asserter/assertStatus"; 2 | export { Asserter } from "@/scenario/gen/jest/asserter/asserter"; 3 | -------------------------------------------------------------------------------- /src/scenario/gen/jest/runner/index.ts: -------------------------------------------------------------------------------- 1 | export { fetch } from "@/scenario/gen/jest/runner/fetch"; 2 | export { Runner } from "@/scenario/gen/jest/runner/runner"; 3 | export { waitForSpan } from "@/scenario/gen/jest/runner/waitForSpan"; 4 | -------------------------------------------------------------------------------- /src/scenario/gen/jest/runner/runner.ts: -------------------------------------------------------------------------------- 1 | import { EchoedContext } from "@/scenario/gen/common/context"; 2 | import { 3 | Option, 4 | RunnerArgument, 5 | RunnerResult, 6 | } from "@/scenario/gen/common/type"; 7 | 8 | export interface Runner { 9 | ( 10 | ctx: EchoedContext, 11 | argument: RunnerArgument, 12 | option: Option, 13 | ): Promise>; 14 | } 15 | -------------------------------------------------------------------------------- /src/scenario/gen/playwright/runner/index.ts: -------------------------------------------------------------------------------- 1 | export { waitForSpanCreatedIn } from "@/scenario/gen/playwright/runner/waitForSpanCreatedIn"; 2 | export { waitForSpanFromPlaywrightFetch } from "@/scenario/gen/playwright/runner/waitForSpanFromPlaywrightFetch"; 3 | -------------------------------------------------------------------------------- /src/scenario/template/common/plugin.eta: -------------------------------------------------------------------------------- 1 | __scenarioBookCtx.setDefaultRunnerOption({ 2 | <% it.config.plugin.getUsedRunners(it.scenarioBook).forEach(function(runner) { %> 3 | <%= runner.name %>: { 4 | <% Array.from(runner.option.entries()).forEach(function([key, value]) {%> 5 | "<%= key %>": <%= value.toTsLine() %>, 6 | <% })%> 7 | }, 8 | <% }) %> 9 | }); 10 | 11 | <% it.scenarioBook.runnerOptions.forEach(function(runnerOption) { %> 12 | __scenarioBookCtx.overrideDefaultRunnerOption("<%= runnerOption.name %>", { 13 | <% Array.from(runnerOption.option.entries()).forEach(function([key, value]) {%> 14 | "<%= key %>": <%= value.toTsLine() %>, 15 | <% })%> 16 | }); 17 | <% }) %> 18 | 19 | __scenarioBookCtx.setDefaultAsserterOption({ 20 | <% it.config.plugin.getUsedAsserters(it.scenarioBook).forEach(function(asserter) { %> 21 | <%= asserter.name %>: { 22 | <% Array.from(asserter.option.entries()).forEach(function([key, value]) {%> 23 | "<%= key %>": <%= value.toTsLine() %>, 24 | <% })%> 25 | }, 26 | <% }) %> 27 | }); 28 | -------------------------------------------------------------------------------- /src/scenario/template/jest/hook.eta: -------------------------------------------------------------------------------- 1 | <%=it.hookType%>(async () => { 2 | __scenarioBookCtx.clearHookBoundVariablesFor("<%=it.hookType%>"); 3 | 4 | <% it.hook[it.hookType].forEach(function(hookExecutor, hookIndex) { %> 5 | await __scenarioBookCtx.runHook("<%=it.hookType%>", async (__hookContext) => { 6 | // <%=it.hookType%> (<%=hookIndex%>) 7 | 8 | const { 9 | <% Array.from(it.hook.getBoundVariablesBefore(it.hookType, hookIndex)).forEach(function(varName) { %> 10 | <%= varName %>, 11 | <% }) %> 12 | } = __hookContext.boundVariables; 13 | 14 | <% if (hookExecutor.rawString) {%> 15 | <%= hookExecutor.rawString.toTsLine() %>; 16 | <% } else { %> 17 | <% Array.from(hookExecutor.bind.entries()).forEach(function([key, value]) { %> 18 | __hookContext.bindNewVariable("<%= key %>", <%= value.toTsLine() %>) 19 | <% }) %> 20 | <% } %> 21 | }) 22 | 23 | <% }) %> 24 | }) 25 | -------------------------------------------------------------------------------- /src/scenario/template/jest/scenario.eta: -------------------------------------------------------------------------------- 1 | <% let scenario = it.scenario%> 2 | const { 3 | <% it.scenarioBook.hook.getBoundVariablesBefore("afterEach", 0).forEach(function(varName) { %> 4 | <%=varName %>, 5 | <% }) %> 6 | } = __scenarioBookCtx.getHookBoundVariablesForStep(); 7 | 8 | <% Array.from(scenario.variable.entries()).forEach(function([key, value]) { %> 9 | const <%=key%> = <%= value.toTsLine() %>; 10 | <% }) %> 11 | 12 | <% scenario.steps.forEach(function(step, stepIndex) { %> 13 | await __scenarioCtx.runStep(async (__stepCtx) => { 14 | // <%= step.description %> 15 | 16 | <%~ include("../common/step", {scenario: scenario, step: step, stepIndex: stepIndex})%> 17 | }) 18 | 19 | <% }) %> 20 | -------------------------------------------------------------------------------- /src/scenario/template/playwright/hook.eta: -------------------------------------------------------------------------------- 1 | test.<%=it.hookType%>(() => { 2 | __scenarioBookCtx.clearHookBoundVariablesFor("<%=it.hookType%>"); 3 | }) 4 | 5 | <% it.hook[it.hookType].forEach(function(hookExecutor, hookIndex) { %> 6 | test.<%=it.hookType%>(async ({<%=hookExecutor.fixtures.toTs()%>}, testInfo) => { 7 | await __scenarioBookCtx.runHook("<%=it.hookType%>", async (__hookContext) => { 8 | // <%=it.hookType%> (<%=hookIndex%>) 9 | 10 | const { 11 | <% Array.from(it.hook.getBoundVariablesBefore(it.hookType, hookIndex)).forEach(function(varName) { %> 12 | <%= varName %>, 13 | <% }) %> 14 | } = __hookContext.boundVariables; 15 | 16 | <% if (hookExecutor.rawString) {%> 17 | <%= hookExecutor.rawString.toTsLine() %>; 18 | <% } else { %> 19 | <% Array.from(hookExecutor.bind.entries()).forEach(function([key, value]) { %> 20 | __hookContext.bindNewVariable("<%= key %>", <%= value.toTsLine() %>) 21 | <% }) %> 22 | <% } %> 23 | }) 24 | }) 25 | 26 | <% }) %> 27 | -------------------------------------------------------------------------------- /src/scenario/template/playwright/scenario.eta: -------------------------------------------------------------------------------- 1 | <% let scenario = it.scenario%> 2 | const { 3 | <% it.scenarioBook.hook.getBoundVariablesBefore("afterEach", 0).forEach(function(varName) { %> 4 | <%=varName %>, 5 | <% }) %> 6 | } = __scenarioBookCtx.getHookBoundVariablesForStep(); 7 | 8 | <% Array.from(scenario.variable.entries()).forEach(function([key, value]) { %> 9 | const <%=key%> = <%= value.toTsLine() %>; 10 | <% }) %> 11 | 12 | <% scenario.steps.forEach(function(step, stepIndex) { %> 13 | await __scenarioCtx.runStep(async (__stepCtx) => { 14 | // <%= step.description %> 15 | 16 | <%~ include("../common/step", {scenario: scenario, step: step, stepIndex: stepIndex})%> 17 | }) 18 | 19 | <% }) %> 20 | -------------------------------------------------------------------------------- /src/server/constroller/otelLogController.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetry } from "@/generated/otelpbj"; 2 | import { NoResponse } from "@/server/parameter/commonParameter"; 3 | import { OtelService } from "@/server/service/otelService"; 4 | import LogsData = opentelemetry.proto.logs.v1.LogsData; 5 | 6 | export class OtelLogController { 7 | constructor(private otelService: OtelService) {} 8 | 9 | async post(body: Buffer): Promise { 10 | const logsData = LogsData.decode(body); 11 | await this.otelService.handleOtelLogs(logsData); 12 | 13 | return NoResponse; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/server/constroller/otelTraceController.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetry } from "@/generated/otelpbj"; 2 | import { NoResponse } from "@/server/parameter/commonParameter"; 3 | import { OtelService } from "@/server/service/otelService"; 4 | import TracesData = opentelemetry.proto.trace.v1.TracesData; 5 | 6 | export class OtelTraceController { 7 | constructor(private otelService: OtelService) {} 8 | 9 | async post(body: Buffer): Promise { 10 | const tracesData = TracesData.decode(body); 11 | await this.otelService.handleOtelTraces(tracesData); 12 | 13 | return NoResponse; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/server/iServer.ts: -------------------------------------------------------------------------------- 1 | import { OtelLogRecord } from "@/type/otelLogRecord"; 2 | import { OtelSpan } from "@/type/otelSpan"; 3 | import { TestCase } from "@/type/testCase"; 4 | 5 | export interface IServer { 6 | stopAfter(waitSec: number): Promise<{ 7 | capturedSpans: Map; 8 | capturedLogs: Map; 9 | capturedTestCases: Map; 10 | }>; 11 | } 12 | -------------------------------------------------------------------------------- /src/server/parameter/commonParameter.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const SuccessResponse = z.strictObject({ 4 | success: z.literal(true), 5 | }); 6 | 7 | export type SuccessResponse = z.infer; 8 | 9 | export const NoResponse = "{}"; 10 | export type NoResponse = typeof NoResponse; 11 | -------------------------------------------------------------------------------- /src/server/parameter/stateParameter.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const States = ["start", "end"] as const; 4 | export type State = (typeof States)[number]; 5 | 6 | export const StateEventRequestParam = z.strictObject({ 7 | name: z.string(), 8 | state: z.enum(States), 9 | }); 10 | 11 | export type StateEventRequestParam = z.infer; 12 | -------------------------------------------------------------------------------- /src/server/requester/fetchRequester.ts: -------------------------------------------------------------------------------- 1 | import { Requester } from "@/server/requester/requester"; 2 | import { Resp } from "@/server/requester/resp"; 3 | 4 | export class FetchRequester implements Requester { 5 | async post( 6 | url: string, 7 | headers: Record, 8 | body: string, 9 | ): Promise { 10 | const res = await fetch(url, { 11 | method: "POST", 12 | headers: headers, 13 | body: body, 14 | }); 15 | 16 | return new Resp(res.status, await res.text()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/server/requester/requester.ts: -------------------------------------------------------------------------------- 1 | import { Resp } from "@/server/requester/resp"; 2 | 3 | export interface Requester { 4 | post( 5 | url: string, 6 | headers: Record, 7 | body: string, 8 | ): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/server/requester/resp.ts: -------------------------------------------------------------------------------- 1 | export class Resp { 2 | constructor( 3 | public status: number, 4 | public body: string, 5 | ) {} 6 | } 7 | -------------------------------------------------------------------------------- /src/server/service/stateService.ts: -------------------------------------------------------------------------------- 1 | import { State } from "@/server/parameter/stateParameter"; 2 | import { StateStore } from "@/server/store/stateStore"; 3 | import { neverVisit } from "@/util/never"; 4 | 5 | export class StateService { 6 | constructor(private stateStore: StateStore) {} 7 | 8 | stateChanged(name: string, state: State): void { 9 | switch (state) { 10 | case "start": 11 | this.stateStore.set(name, "start"); 12 | break; 13 | case "end": 14 | this.stateStore.delete(name); 15 | break; 16 | default: 17 | neverVisit("unknown state", state); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/server/service/testRecordService.test.ts: -------------------------------------------------------------------------------- 1 | import { TestRecordService } from "@/server/service/testRecordService"; 2 | import { TestCaseStore } from "@/server/store/testCaseStore"; 3 | import { buildTestCase } from "@/testUtil/report/testCase"; 4 | import { TestCase } from "@/type/testCase"; 5 | 6 | describe("TestRecordService", () => { 7 | describe("recordFinished", () => { 8 | it("should add testCase to store", async () => { 9 | const testCaseStore = new TestCaseStore(); 10 | const service = new TestRecordService(testCaseStore); 11 | 12 | const tests = new Map([ 13 | ["test", [buildTestCase({ name: "dummy-test" })]], 14 | ]); 15 | await service.recordFinished(tests); 16 | 17 | expect(testCaseStore.capturedTestCases).toEqual(tests); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/server/service/testRecordService.ts: -------------------------------------------------------------------------------- 1 | import { TestCaseStore } from "@/server/store/testCaseStore"; 2 | import { TestCase } from "@/type/testCase"; 3 | 4 | export class TestRecordService { 5 | constructor(private testCaseStore: TestCaseStore) {} 6 | 7 | async recordFinished(tests: Map): Promise { 8 | await this.testCaseStore.add(tests); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/server/service/waitForSpanService.ts: -------------------------------------------------------------------------------- 1 | import { EventBus } from "@/eventBus/infra/eventBus"; 2 | import { SpanBus } from "@/eventBus/spanBus"; 3 | import { ErrorMessage } from "@/type/common"; 4 | import { HexString } from "@/type/hexString"; 5 | import { JsonSpan } from "@/type/jsonSpan"; 6 | import { SpanFilterOption } from "@/type/spanFilterOption"; 7 | 8 | export class WaitForSpanService { 9 | constructor(private bus: EventBus) {} 10 | 11 | async handleWaitForSpanEvent( 12 | traceId: HexString, 13 | filter: SpanFilterOption, 14 | waitTimeoutMs: number, 15 | ): Promise { 16 | const spanBus = new SpanBus(this.bus); 17 | const response = await spanBus.requestWaitForSpan( 18 | traceId, 19 | filter, 20 | waitTimeoutMs, 21 | ); 22 | 23 | return response; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/server/store/otelLogStore.ts: -------------------------------------------------------------------------------- 1 | import { OtelLogRecord } from "@/type/otelLogRecord"; 2 | import { toHex } from "@/util/byte"; 3 | import { Mutex } from "async-mutex"; 4 | 5 | export class OtelLogStore { 6 | capturedLogs: Map = new Map(); 7 | 8 | private mutex = new Mutex(); 9 | 10 | constructor() {} 11 | 12 | async capture(log: OtelLogRecord): Promise { 13 | const traceId = toHex(log.traceId).hexString; 14 | 15 | await this.mutex.runExclusive(() => { 16 | if (!this.capturedLogs.has(traceId)) { 17 | this.capturedLogs.set(traceId, []); 18 | } 19 | this.capturedLogs.get(traceId)?.push(log); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/server/store/otelSpanStore.ts: -------------------------------------------------------------------------------- 1 | import { OtelSpan } from "@/type/otelSpan"; 2 | import { toHex } from "@/util/byte"; 3 | import { Mutex } from "async-mutex"; 4 | 5 | export class OtelSpanStore { 6 | capturedSpans: Map = new Map(); 7 | 8 | constructor(private spanMutex: Mutex) {} 9 | 10 | async capture(span: OtelSpan): Promise { 11 | const traceId = toHex(span.traceId).hexString; 12 | await this.spanMutex.runExclusive(() => { 13 | if (!this.capturedSpans.has(traceId)) { 14 | this.capturedSpans.set(traceId, []); 15 | } 16 | this.capturedSpans.get(traceId)?.push(span); 17 | }); 18 | } 19 | 20 | getCaptured(traceId: string): OtelSpan[] | undefined { 21 | return this.capturedSpans.get(traceId); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/server/store/stateStore.ts: -------------------------------------------------------------------------------- 1 | import { State } from "@/server/parameter/stateParameter"; 2 | 3 | export class StateStore { 4 | states = new Map(); 5 | 6 | constructor() {} 7 | 8 | set(key: string, state: State): void { 9 | this.states.set(key, state); 10 | } 11 | 12 | delete(key: string): void { 13 | this.states.delete(key); 14 | } 15 | 16 | isEmpty(): boolean { 17 | return this.states.size === 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/server/store/testCaseStore.ts: -------------------------------------------------------------------------------- 1 | import { TestCase } from "@/type/testCase"; 2 | import { Mutex } from "async-mutex"; 3 | 4 | export class TestCaseStore { 5 | capturedTestCases: Map = new Map(); 6 | 7 | private mutex = new Mutex(); 8 | 9 | constructor() {} 10 | 11 | async add(tests: Map): Promise { 12 | for (const [file, testcases] of tests) { 13 | await this.addTest(file, testcases); 14 | } 15 | } 16 | 17 | private async addTest(file: string, testcases: TestCase[]): Promise { 18 | await this.mutex.runExclusive(() => { 19 | const existingTestCases = this.capturedTestCases.get(file) ?? []; 20 | existingTestCases.push(...testcases); 21 | this.capturedTestCases.set(file, existingTestCases); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/server/store/waitForSpanRequestStore.ts: -------------------------------------------------------------------------------- 1 | import { WaitForSpanRequest } from "@/eventBus/waitForSpanRequest"; 2 | import { Mutex } from "async-mutex"; 3 | 4 | export class WaitForSpanRequestStore { 5 | private waitForSpanRequests = new Map(); 6 | 7 | constructor(private spanMutex: Mutex) {} 8 | 9 | async update( 10 | traceId: string, 11 | fn: (existings: WaitForSpanRequest[]) => WaitForSpanRequest[], 12 | ): Promise { 13 | await this.spanMutex.runExclusive(() => { 14 | const requests = this.waitForSpanRequests.get(traceId) ?? []; 15 | const updatedRequests = fn(requests); 16 | this.waitForSpanRequests.set(traceId, updatedRequests); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/testUtil/async.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "@/util/async"; 2 | 3 | const WAIT_LOOP_TICK_MS = 10; 4 | const WAIT_LOOP_COUNT = 10; 5 | export const MAX_WAIT_MS = WAIT_LOOP_TICK_MS * WAIT_LOOP_COUNT; 6 | 7 | export async function waitUntilCalled( 8 | mock: jest.Mock, 9 | count: number = 1, 10 | ): Promise { 11 | for (let i = 0; i < WAIT_LOOP_COUNT; i++) { 12 | if (mock.mock.calls.length >= count) { 13 | return; 14 | } 15 | await sleep(WAIT_LOOP_TICK_MS); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/testUtil/config/openApiConfig.ts: -------------------------------------------------------------------------------- 1 | import { Eq } from "@/comparision/eq"; 2 | import { OpenApiConfig } from "@/config/config"; 3 | 4 | const DEFAULT_CONFIG: OpenApiConfig = { 5 | filePath: "dummy.yaml", 6 | coverage: { 7 | undocumentedOperation: { 8 | ignores: [ 9 | { 10 | path: new Eq("/ignored"), 11 | method: "post", 12 | }, 13 | ], 14 | }, 15 | }, 16 | }; 17 | 18 | export function buildOpenApiConfig( 19 | overrides: Partial = {}, 20 | ): OpenApiConfig { 21 | return { 22 | ...DEFAULT_CONFIG, 23 | ...overrides, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/testUtil/config/protoConfig.ts: -------------------------------------------------------------------------------- 1 | import { ProtoConfig } from "@/config/config"; 2 | 3 | const DEFAULT_CONFIG: ProtoConfig = { 4 | filePath: "dummy.proto", 5 | coverage: { 6 | undocumentedMethod: { 7 | ignores: [], 8 | }, 9 | }, 10 | }; 11 | 12 | export function buildProtoConfig( 13 | overrides: Partial = {}, 14 | ): ProtoConfig { 15 | return { 16 | ...DEFAULT_CONFIG, 17 | ...overrides, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/testUtil/config/scenarioCompileTargetConfig.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioCompileTargetConfig } from "@/config/scenarioCompileTargetConfig"; 2 | import { MockDirectory } from "@/testUtil/fs/mockDirectory"; 3 | 4 | const DEFAULT_TARGETS: { 5 | yamlDir: string; 6 | outDir: string; 7 | type: ScenarioCompileTargetConfig["type"]; 8 | useEchoedFeatures?: boolean; 9 | }[] = [ 10 | { 11 | yamlDir: "scenario", 12 | outDir: "scenario_gen", 13 | type: "jest", 14 | useEchoedFeatures: true, 15 | }, 16 | ]; 17 | 18 | export function buildScenarioCompileTargetConfigs( 19 | targets?: typeof DEFAULT_TARGETS, 20 | ): ScenarioCompileTargetConfig[] { 21 | return (targets ?? DEFAULT_TARGETS).map((target) => { 22 | return new ScenarioCompileTargetConfig( 23 | new MockDirectory(target.yamlDir), 24 | new MockDirectory(target.outDir), 25 | target.type, 26 | target.useEchoedFeatures ?? true, 27 | ); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/testUtil/cypress/cypressSpec.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_SPEC: Cypress.Spec = { 2 | absolute: "/path/to/spec.ts", 3 | name: "spec.ts", 4 | relative: "spec.ts", 5 | }; 6 | 7 | export function buildCypressSpec( 8 | overrides: Partial = {}, 9 | ): Cypress.Spec { 10 | return { 11 | ...DEFAULT_SPEC, 12 | ...overrides, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/testUtil/cypress/dummyCyObj.ts: -------------------------------------------------------------------------------- 1 | import { ICypressObj } from "@/integration/cypress/internal/infra/iCypressObj"; 2 | 3 | export class DummyCyObj implements ICypressObj { 4 | private readonly envs: Record; 5 | constructor({ envs }: { envs?: Record } = {}) { 6 | this.envs = envs ?? {}; 7 | } 8 | 9 | env(key: string): string | undefined { 10 | return this.envs[key]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/testUtil/cypress/httpResponse.ts: -------------------------------------------------------------------------------- 1 | import { CypressHttpResponse } from "@/integration/cypress/internal/cypressHttpResponse"; 2 | 3 | const DEFAULT_RESPONSE: CypressHttpResponse = { 4 | statusCode: 200, 5 | body: "test body", 6 | headers: {}, 7 | }; 8 | 9 | export function buildCypressHttpResponse( 10 | overrides: Partial = {}, 11 | ): CypressHttpResponse { 12 | return { 13 | ...DEFAULT_RESPONSE, 14 | ...overrides, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/testUtil/cypress/request.ts: -------------------------------------------------------------------------------- 1 | import { CypressHttpRequest } from "@/integration/cypress/internal/cypressHttpRequest"; 2 | import { mock } from "jest-mock-extended"; 3 | import { MockProxy } from "jest-mock-extended/lib/Mock"; 4 | 5 | type CypressHttpRequestOverrides = CypressHttpRequest & { 6 | body: Partial; 7 | }; 8 | 9 | const DEFAULT_REQUEST: Omit = { 10 | url: "https://example.com", 11 | method: "GET", 12 | body: "body", 13 | headers: { 14 | "User-Agent": 15 | "Mozilla/5.0 (..) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/x.x.x", 16 | }, 17 | }; 18 | 19 | export function buildCypressRequest( 20 | overrides: Partial = {}, 21 | ): MockProxy { 22 | const request = mock({ 23 | ...DEFAULT_REQUEST, 24 | ...overrides, 25 | }); 26 | 27 | return request; 28 | } 29 | -------------------------------------------------------------------------------- /src/testUtil/cypress/response.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_RESPONSE: Omit, "body"> = { 2 | allRequestResponses: [], 3 | duration: 100, 4 | headers: { "Content-Type": "application/json; charset=utf-8" }, 5 | isOkStatusCode: true, 6 | redirects: undefined, 7 | redirectedToUrl: undefined, 8 | requestHeaders: {}, 9 | status: 200, 10 | statusText: "OK", 11 | }; 12 | 13 | export function buildCypressResponse( 14 | override: Partial> & { body: T }, 15 | ): Cypress.Response { 16 | return { 17 | ...DEFAULT_RESPONSE, 18 | ...override, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/testUtil/fileLog/mockFileLogger.ts: -------------------------------------------------------------------------------- 1 | import { IFileLogger } from "@/fileLog/iFileLogger"; 2 | 3 | export class MockFileLogger implements IFileLogger { 4 | texts: string[] = []; 5 | 6 | async appendFileLine(text: string): Promise { 7 | this.texts.push(text); 8 | return Promise.resolve(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/testUtil/fs/fileSpace.ts: -------------------------------------------------------------------------------- 1 | import { FileSpace } from "@/fileSpace/fileSpace"; 2 | import { buildMockFsContainer } from "@/testUtil/fs/mockFsContainer"; 3 | 4 | export function buildDummyFileSpace(): FileSpace { 5 | const fsContainer = buildMockFsContainer(); 6 | const tmpDir = fsContainer.newDirectory("/tmp"); 7 | const fileSpace = new FileSpace(tmpDir); 8 | 9 | return fileSpace; 10 | } 11 | -------------------------------------------------------------------------------- /src/testUtil/fs/mockDirectory.ts: -------------------------------------------------------------------------------- 1 | import { IFile } from "@/fs/IFile"; 2 | import { IDirectory } from "@/fs/iDirectory"; 3 | import { MockFile } from "@/testUtil/fs/mockFile"; 4 | import { MockFileContents } from "@/testUtil/fs/mockFileContents"; 5 | import path from "path"; 6 | 7 | export class MockDirectory implements IDirectory { 8 | constructor( 9 | public path: string = "mockDir", 10 | public fileContents: MockFileContents = new MockFileContents(), 11 | ) {} 12 | 13 | newDir(dir: string): IDirectory { 14 | return new MockDirectory(path.join(this.path, dir), this.fileContents); 15 | } 16 | 17 | newFile(filename: string): MockFile { 18 | return new MockFile(path.join(this.path, filename), this.fileContents); 19 | } 20 | 21 | resolve(): string { 22 | return this.path; 23 | } 24 | 25 | mkdirSync(): void {} 26 | 27 | readdir(): Promise { 28 | return Promise.resolve([]); 29 | } 30 | 31 | rm(): Promise { 32 | return Promise.resolve(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/testUtil/fs/mockFileContents.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | export class MockFileContents { 4 | files = new Map(); 5 | 6 | constructor() {} 7 | 8 | replace(filename: string, content: string): void { 9 | this.set(filename, content); 10 | } 11 | 12 | append(filename: string, content: string): void { 13 | const fileContent = this.get(filename) || ""; 14 | const newContent = fileContent + content; 15 | this.set(filename, newContent); 16 | } 17 | 18 | get(filename: string): string | undefined { 19 | return this.files.get(this.resolve(filename)); 20 | } 21 | 22 | private set(filename: string, content: string): void { 23 | this.files.set(this.resolve(filename), content); 24 | } 25 | 26 | private resolve(filename: string): string { 27 | return path.resolve(filename); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/testUtil/fs/mockFsContainer.ts: -------------------------------------------------------------------------------- 1 | import { IFile } from "@/fs/IFile"; 2 | import { FsContainer } from "@/fs/fsContainer"; 3 | import { IDirectory } from "@/fs/iDirectory"; 4 | import { MockDirectory } from "@/testUtil/fs/mockDirectory"; 5 | import { MockFile } from "@/testUtil/fs/mockFile"; 6 | import { MockFileContents } from "@/testUtil/fs/mockFileContents"; 7 | 8 | export const buildMockFsContainer = (): FsContainer & { 9 | fileContents: MockFileContents; 10 | } => { 11 | const fileContents = new MockFileContents(); 12 | return { 13 | mkdtempSync: (prefix: string): IDirectory => { 14 | return new MockDirectory(prefix + "mock", fileContents); 15 | }, 16 | newDirectory: (dir: string): IDirectory => { 17 | return new MockDirectory(dir, fileContents); 18 | }, 19 | newFile: (filepath: string): IFile => { 20 | return new MockFile(filepath, fileContents); 21 | }, 22 | fileContents: fileContents, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/testUtil/global/dummyFetcher.ts: -------------------------------------------------------------------------------- 1 | export class DummyFetcher { 2 | fetchArguments: [RequestInfo | URL, RequestInit?][] = []; 3 | 4 | buildFetch(): ( 5 | input: RequestInfo | URL, 6 | init?: RequestInit, 7 | ) => Promise { 8 | return async (input: RequestInfo | URL, init?: RequestInit) => { 9 | this.fetchArguments.push([input, init]); 10 | const response = new Response(JSON.stringify({ status: 200 })); 11 | return Promise.resolve(response); 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/testUtil/jest/aggregatedResult.ts: -------------------------------------------------------------------------------- 1 | import { AggregatedResult } from "@jest/reporters"; 2 | 3 | const DEFAULT_AGGREGATED_RESULT: AggregatedResult = { 4 | numPendingTestSuites: 0, 5 | numRuntimeErrorTestSuites: 0, 6 | numTodoTests: 0, 7 | numTotalTestSuites: 0, 8 | numTotalTests: 0, 9 | openHandles: [], 10 | snapshot: { 11 | added: 0, 12 | didUpdate: false, 13 | failure: false, 14 | filesAdded: 0, 15 | filesRemoved: 0, 16 | filesRemovedList: [], 17 | filesUnmatched: 0, 18 | filesUpdated: 0, 19 | matched: 0, 20 | total: 0, 21 | unchecked: 0, 22 | uncheckedKeysByFile: [], 23 | unmatched: 0, 24 | updated: 0, 25 | }, 26 | startTime: 0, 27 | success: false, 28 | testResults: [], 29 | wasInterrupted: false, 30 | numFailedTests: 0, 31 | numFailedTestSuites: 0, 32 | numPassedTests: 0, 33 | numPassedTestSuites: 0, 34 | numPendingTests: 0, 35 | }; 36 | 37 | export function buildAggregatedResult( 38 | overrides: Partial = {}, 39 | ): AggregatedResult { 40 | return { ...DEFAULT_AGGREGATED_RESULT, ...overrides }; 41 | } 42 | -------------------------------------------------------------------------------- /src/testUtil/jest/nodeEnvironment.ts: -------------------------------------------------------------------------------- 1 | import { buildGlobalConfig } from "@/testUtil/jest/globalConfig"; 2 | import { buildProjectConfig } from "@/testUtil/jest/projectConfig"; 3 | import { EnvironmentContext } from "@jest/environment"; 4 | import NodeEnvironment from "jest-environment-node"; 5 | 6 | const context: EnvironmentContext = { 7 | console, 8 | docblockPragmas: {}, 9 | testPath: __filename, 10 | }; 11 | 12 | export function buildNodeEnvironment(): NodeEnvironment { 13 | const testEnvConfig = { 14 | globalConfig: buildGlobalConfig(), 15 | projectConfig: buildProjectConfig(), 16 | }; 17 | 18 | const nodeEnvironment = new NodeEnvironment(testEnvConfig, context); 19 | 20 | return nodeEnvironment; 21 | } 22 | -------------------------------------------------------------------------------- /src/testUtil/jest/reporter.ts: -------------------------------------------------------------------------------- 1 | import { ReporterOnStartOptions } from "@jest/reporters"; 2 | 3 | export function buildReporterOnStartOptions(): ReporterOnStartOptions { 4 | return { 5 | estimatedTime: 0, 6 | showStatus: false, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/testUtil/jest/testCaseResult.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_TEST_FULL_NAME } from "@/testUtil/jest/testCaseStartInfo"; 2 | import { DEFAULT_TEST_PATH } from "@/testUtil/jest/test_"; 3 | import { TestCaseResult } from "@jest/test-result"; 4 | 5 | const DEFAULT_TEST_CASE_RESULT: TestCaseResult = { 6 | failureDetails: [], 7 | failureMessages: [], 8 | numPassingAsserts: 0, 9 | status: "passed", 10 | ancestorTitles: [], 11 | fullName: DEFAULT_TEST_PATH, 12 | duration: 123, 13 | title: DEFAULT_TEST_FULL_NAME, 14 | }; 15 | 16 | export function buildTestCaseResult( 17 | test: Partial = {}, 18 | ): TestCaseResult { 19 | return { 20 | ...DEFAULT_TEST_CASE_RESULT, 21 | ...test, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/testUtil/jest/testCaseStartInfo.ts: -------------------------------------------------------------------------------- 1 | import { Circus } from "@jest/types"; 2 | 3 | export const DEFAULT_TEST_FULL_NAME = "some awesome test"; 4 | 5 | const DEFAULT_TEST_CASE_START_INFO: Circus.TestCaseStartInfo = { 6 | ancestorTitles: [], 7 | fullName: DEFAULT_TEST_FULL_NAME, 8 | mode: undefined, 9 | title: "test", 10 | startedAt: 1234, 11 | }; 12 | 13 | export function buildTestCaseStartInfo( 14 | test: Partial = {}, 15 | ): Circus.TestCaseStartInfo { 16 | return { 17 | ...DEFAULT_TEST_CASE_START_INFO, 18 | ...test, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/testUtil/jest/test_.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestContext } from "@jest/test-result"; 2 | 3 | export const DEFAULT_TEST_PATH = "/path/to/dummy.test.js"; 4 | 5 | const testContext = {} as unknown as TestContext; 6 | const DEFAULT_TEST: Test = { 7 | context: testContext, 8 | duration: undefined, 9 | path: DEFAULT_TEST_PATH, 10 | }; 11 | 12 | export function buildTest(test: Partial = {}): Test { 13 | return { 14 | ...DEFAULT_TEST, 15 | ...test, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/testUtil/openapi/apiV2.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV2 } from "openapi-types"; 2 | 3 | const DEFAULT_V2_DOC: OpenAPIV2.Document = { 4 | swagger: "2.0", 5 | info: { 6 | title: "dummy", 7 | version: "1.0.0", 8 | }, 9 | basePath: "/api", 10 | paths: { 11 | "/users": { 12 | get: { 13 | responses: {}, 14 | }, 15 | post: { 16 | responses: {}, 17 | }, 18 | }, 19 | }, 20 | }; 21 | 22 | export function buildV2Document( 23 | overrides: Partial = {}, 24 | ): OpenAPIV2.Document { 25 | return { 26 | ...DEFAULT_V2_DOC, 27 | ...overrides, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/testUtil/openapi/apiV3.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from "openapi-types"; 2 | 3 | const DEFAULT_V3_DOC: OpenAPIV3.Document = { 4 | openapi: "3.0.0", 5 | info: { 6 | title: "dummy", 7 | version: "1.0.0", 8 | }, 9 | servers: [ 10 | { 11 | url: "http://localhost:8080/api", 12 | }, 13 | ], 14 | paths: { 15 | "/users": { 16 | get: { 17 | responses: {}, 18 | }, 19 | post: { 20 | responses: {}, 21 | }, 22 | }, 23 | }, 24 | }; 25 | 26 | export function buildV3Document( 27 | overrides: Partial = {}, 28 | ): OpenAPIV3.Document { 29 | return { 30 | ...DEFAULT_V3_DOC, 31 | ...overrides, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/testUtil/otel/id.ts: -------------------------------------------------------------------------------- 1 | import { HexString } from "@/type/hexString"; 2 | import { buildRandomHexBytes, buildRandomHexUUID } from "@/util/random"; 3 | 4 | export function buildTraceId(): HexString { 5 | return new HexString(buildRandomHexUUID()); 6 | } 7 | 8 | export function buildSpanId(): HexString { 9 | return new HexString(buildRandomHexBytes(8)); 10 | } 11 | -------------------------------------------------------------------------------- /src/testUtil/otel/otelLogRecord.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetry } from "@/generated/otelpbj"; 2 | import { OtelLogRecord } from "@/type/otelLogRecord"; 3 | import ILogRecord = opentelemetry.proto.logs.v1.ILogRecord; 4 | 5 | const DEFAULT_TRACE_ID = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]); 6 | const DEFAULT_LOG: ILogRecord = { 7 | traceId: DEFAULT_TRACE_ID, 8 | body: { 9 | stringValue: "test log", 10 | }, 11 | }; 12 | 13 | export function buildOtelLogRecord( 14 | log: Partial = {}, 15 | ): OtelLogRecord { 16 | return new OtelLogRecord({ 17 | ...DEFAULT_LOG, 18 | ...log, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/testUtil/playwright/browserContext.ts: -------------------------------------------------------------------------------- 1 | import { initializeEchoedContext } from "@/integration/playwright/internal/util/browserContext"; 2 | import { BrowserContext } from "@playwright/test"; 3 | import { mock } from "jest-mock-extended"; 4 | import { MockProxy } from "jest-mock-extended/lib/Mock"; 5 | 6 | export function buildBrowserContext(): MockProxy { 7 | const ctx = mock(); 8 | 9 | return ctx; 10 | } 11 | 12 | export function buildInitializedBrowserContext(): MockProxy { 13 | const ctx = buildBrowserContext(); 14 | initializeEchoedContext(ctx); 15 | 16 | return ctx; 17 | } 18 | -------------------------------------------------------------------------------- /src/testUtil/playwright/fullConfig.ts: -------------------------------------------------------------------------------- 1 | import { FullConfig } from "@playwright/test/reporter"; 2 | import { undefined } from "zod"; 3 | 4 | const DEFAULT_FULL_CONFIG: FullConfig = { 5 | forbidOnly: false, 6 | fullyParallel: false, 7 | globalSetup: null, 8 | globalTeardown: null, 9 | globalTimeout: 0, 10 | grep: [], 11 | grepInvert: null, 12 | maxFailures: 0, 13 | metadata: undefined, 14 | preserveOutput: "always", 15 | projects: [], 16 | quiet: false, 17 | reportSlowTests: null, 18 | reporter: [], 19 | rootDir: "", 20 | shard: null, 21 | updateSnapshots: "all", 22 | version: "", 23 | webServer: null, 24 | workers: 0, 25 | }; 26 | 27 | export function buildFullConfig( 28 | overrides: Partial = {}, 29 | ): FullConfig { 30 | const fullConfig: FullConfig = { 31 | ...DEFAULT_FULL_CONFIG, 32 | ...overrides, 33 | }; 34 | return fullConfig; 35 | } 36 | -------------------------------------------------------------------------------- /src/testUtil/playwright/fullProject.ts: -------------------------------------------------------------------------------- 1 | import { FullProject } from "@playwright/test"; 2 | import { undefined } from "zod"; 3 | 4 | const DEFAULT_FULL_PROJECT: FullProject = { 5 | dependencies: [], 6 | grep: [], 7 | grepInvert: [], 8 | metadata: undefined, 9 | name: "", 10 | outputDir: "", 11 | repeatEach: 0, 12 | retries: 0, 13 | snapshotDir: "", 14 | testDir: "", 15 | testIgnore: [], 16 | testMatch: [], 17 | timeout: 0, 18 | use: {}, 19 | }; 20 | 21 | export function buildFullProject( 22 | overrides: Partial = {}, 23 | ): FullProject { 24 | const fullProject: FullProject = { 25 | ...DEFAULT_FULL_PROJECT, 26 | ...overrides, 27 | }; 28 | return fullProject; 29 | } 30 | -------------------------------------------------------------------------------- /src/testUtil/playwright/request.ts: -------------------------------------------------------------------------------- 1 | import { mock } from "jest-mock-extended"; 2 | import { MockProxy } from "jest-mock-extended/lib/Mock"; 3 | import { Request } from "playwright-core"; 4 | 5 | const DEFAULT_REQUEST = { 6 | headers: {}, 7 | url: "https://example.com/dummy", 8 | method: "GET", 9 | }; 10 | 11 | export function buildPlaywrightRequest( 12 | overrides: Partial = {}, 13 | ): MockProxy { 14 | const returnValues = { 15 | ...DEFAULT_REQUEST, 16 | ...overrides, 17 | }; 18 | 19 | const request = mock(); 20 | request.headers.calledWith().mockReturnValue(returnValues.headers); 21 | request.url.calledWith().mockReturnValue(returnValues.url); 22 | request.method.calledWith().mockReturnValue(returnValues.method); 23 | 24 | return request; 25 | } 26 | -------------------------------------------------------------------------------- /src/testUtil/playwright/route.ts: -------------------------------------------------------------------------------- 1 | import { buildPlaywrightApiResponse } from "@/testUtil/playwright/apiResponse"; 2 | import { buildPlaywrightRequest } from "@/testUtil/playwright/request"; 3 | import { APIResponse, Route } from "@playwright/test"; 4 | import { mock } from "jest-mock-extended"; 5 | import { MockProxy } from "jest-mock-extended/lib/Mock"; 6 | 7 | const DEFAULT_ROUTE = { 8 | fetch: (): Promise> => { 9 | return Promise.resolve(buildPlaywrightApiResponse()); 10 | }, 11 | request: buildPlaywrightRequest(), 12 | }; 13 | 14 | export function buildRoute( 15 | overrides: Partial = {}, 16 | ): MockProxy { 17 | const returnValues = { 18 | ...DEFAULT_ROUTE, 19 | ...overrides, 20 | }; 21 | 22 | const route = mock(); 23 | route.fulfill.calledWith(); 24 | route.fetch.calledWith().mockImplementation(returnValues.fetch); 25 | route.request.calledWith().mockReturnValue(returnValues.request); 26 | 27 | return route; 28 | } 29 | -------------------------------------------------------------------------------- /src/testUtil/playwright/testResult.ts: -------------------------------------------------------------------------------- 1 | import { TestResult } from "playwright/types/testReporter"; 2 | 3 | const DEFAULT_TEST_RESULT: TestResult = { 4 | attachments: [], 5 | duration: 123, 6 | error: undefined, 7 | errors: [], 8 | parallelIndex: 0, 9 | retry: 0, 10 | startTime: new Date(), 11 | status: "passed", 12 | stderr: [], 13 | stdout: [], 14 | steps: [], 15 | workerIndex: 0, 16 | }; 17 | 18 | export function buildPlaywrightTestResult( 19 | overrides: Partial = {}, 20 | ): TestResult { 21 | const result: TestResult = { 22 | ...DEFAULT_TEST_RESULT, 23 | ...overrides, 24 | }; 25 | 26 | return result; 27 | } 28 | 29 | export function makeTestResultFailed(result: TestResult): TestResult { 30 | return { 31 | ...result, 32 | status: "failed", 33 | errors: [ 34 | { 35 | location: { 36 | file: "dummyFile", 37 | line: 1, 38 | column: 1, 39 | }, 40 | message: "test failed", 41 | snippet: "snippet", 42 | stack: "stack", 43 | value: "value", 44 | }, 45 | ], 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/testUtil/report/mockReportFile.ts: -------------------------------------------------------------------------------- 1 | import { CoverageResult } from "@/coverage/coverageResult"; 2 | import { IFile } from "@/fs/IFile"; 3 | import { IReportFile } from "@/report/iReportFile"; 4 | import { TestResult } from "@/report/testResult"; 5 | import { MockFile } from "@/testUtil/fs/mockFile"; 6 | 7 | export class MockReportFile implements IReportFile { 8 | testResult: TestResult | undefined; 9 | generate( 10 | testResult: TestResult, 11 | _coverageResult: CoverageResult, 12 | ): Promise { 13 | this.testResult = testResult; 14 | 15 | return Promise.resolve(new MockFile()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/testUtil/report/testCase.ts: -------------------------------------------------------------------------------- 1 | import { TestCase } from "@/type/testCase"; 2 | 3 | const DEFAULT_TEST_CASE = { 4 | testId: "testId", 5 | file: "file", 6 | name: "name", 7 | startTimeMillis: 1234, 8 | status: "status", 9 | duration: 1234, 10 | testEndTimeMillis: 1234, 11 | }; 12 | 13 | export function buildTestCase(overrides: Partial = {}): TestCase { 14 | const vals = { 15 | ...DEFAULT_TEST_CASE, 16 | ...overrides, 17 | }; 18 | return new TestCase( 19 | vals.testId, 20 | vals.file, 21 | vals.name, 22 | vals.startTimeMillis, 23 | vals.status, 24 | vals.duration, 25 | vals.testEndTimeMillis, 26 | vals.failureDetails, 27 | vals.failureMessages, 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/testUtil/scenario/context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EchoedActContext, 3 | EchoedAssertContext, 4 | } from "@/scenario/gen/common/context"; 5 | 6 | export const buildEchoedActContext = (): EchoedActContext => { 7 | return { kind: "act", scenarioName: "scenario", currentStepIndex: 0 }; 8 | }; 9 | 10 | export const buildEchoedAssertContext = (): EchoedAssertContext => { 11 | return { 12 | kind: "assert", 13 | scenarioName: "scenario", 14 | currentStepIndex: 0, 15 | actResult: {}, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/testUtil/scenario/dummyScenario.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioBase } from "@/scenario/compile/common/scenarioBase"; 2 | import { DummyStep } from "@/testUtil/scenario/dummyStep"; 3 | 4 | const defaultScenarioVals = { 5 | name: "dummy", 6 | steps: [] as DummyStep[], 7 | }; 8 | 9 | export class DummyScenario extends ScenarioBase { 10 | constructor(values: Partial = {}) { 11 | const vals = { ...defaultScenarioVals, ...values }; 12 | super(vals.name, vals.steps); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/testUtil/scenario/dummyScenarioBook.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioBase } from "@/scenario/compile/common/scenarioBase"; 2 | import { ScenarioBookBase } from "@/scenario/compile/common/scenarioBookBase"; 3 | import { ScenarioRunnerConfig } from "@/scenario/compile/common/scenarioRunnerConfig"; 4 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 5 | import { DummyStep } from "@/testUtil/scenario/dummyStep"; 6 | 7 | const defaultScenarioBookVals = { 8 | scenarios: [] as ScenarioBase[], 9 | runnerOptions: [] as ScenarioRunnerConfig[], 10 | variables: new Map(), 11 | retry: undefined as number | undefined, 12 | }; 13 | 14 | export class DummyScenarioBookBase extends ScenarioBookBase< 15 | ScenarioBase 16 | > { 17 | constructor(values: Partial = {}) { 18 | const vals = { ...defaultScenarioBookVals, ...values }; 19 | super(vals.scenarios, vals.runnerOptions, vals.variables, vals.retry); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/testUtil/scenario/dummyStep.ts: -------------------------------------------------------------------------------- 1 | import { Act } from "@/scenario/compile/common/act"; 2 | import { Arrange } from "@/scenario/compile/common/arrange"; 3 | import { Assert } from "@/scenario/compile/common/assert"; 4 | import { StepBase } from "@/scenario/compile/common/stepBase"; 5 | import { TsVariable } from "@/scenario/compile/common/tsVariable"; 6 | 7 | const defaultStepVals = { 8 | arranges: [] as Arrange[], 9 | act: undefined as Act | undefined, 10 | asserts: [] as Assert[], 11 | bind: new Map(), 12 | }; 13 | 14 | export class DummyStep extends StepBase { 15 | constructor(values: Partial = {}) { 16 | const vals = { ...defaultStepVals, ...values }; 17 | 18 | super(vals.arranges, vals.act, vals.asserts, vals.bind); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/testUtil/scenario/gen/internal/jest/arrangeContext.ts: -------------------------------------------------------------------------------- 1 | import { ArrangeContext } from "@/scenario/gen/internal/common/arrangeContext"; 2 | import { ScenarioContext } from "@/scenario/gen/internal/common/scenarioContext"; 3 | import { StepContext } from "@/scenario/gen/internal/common/stepContext"; 4 | import { buildStepContext } from "@/testUtil/scenario/gen/internal/jest/stepContext"; 5 | 6 | export function buildArrangeContext( 7 | overrides?: Partial, 8 | stepOverrides?: Partial, 9 | scenarioOverrides?: Partial, 10 | ): ArrangeContext { 11 | const ctx = new ArrangeContext( 12 | buildStepContext(stepOverrides, scenarioOverrides), 13 | overrides?.index ?? 0, 14 | overrides?.boundVariables ?? {}, 15 | overrides?.arrangeResultHistory ?? [undefined], 16 | ); 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 19 | ctx.runnerResult = overrides?.runnerResult; 20 | ctx.newBoundVariables = overrides?.newBoundVariables ?? {}; 21 | 22 | return ctx; 23 | } 24 | -------------------------------------------------------------------------------- /src/testUtil/scenario/gen/internal/jest/scenarioContext.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioContext } from "@/scenario/gen/internal/common/scenarioContext"; 2 | 3 | export function buildScenarioContext( 4 | overrides?: Partial, 5 | ): ScenarioContext { 6 | return new ScenarioContext(overrides?.scenarioName ?? "test scenario name"); 7 | } 8 | -------------------------------------------------------------------------------- /src/testUtil/scenario/gen/internal/jest/stepContext.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioContext } from "@/scenario/gen/internal/common/scenarioContext"; 2 | import { StepContext } from "@/scenario/gen/internal/common/stepContext"; 3 | import { buildScenarioContext } from "@/testUtil/scenario/gen/internal/jest/scenarioContext"; 4 | 5 | export function buildStepContext( 6 | overrides?: Partial, 7 | scenarioOverrides?: Partial, 8 | ): StepContext { 9 | return new StepContext( 10 | buildScenarioContext(scenarioOverrides), 11 | overrides?.index ?? 0, 12 | overrides?.boundVariables ?? {}, 13 | overrides?.actResultHistory ?? [undefined], 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/testUtil/scenario/plugin.ts: -------------------------------------------------------------------------------- 1 | import { AsserterConfig } from "@/scenario/compile/common/asserterConfig"; 2 | import { CommonPluginConfig } from "@/scenario/compile/common/commonPluginConfig"; 3 | import { RunnerConfig } from "@/scenario/compile/common/runnerConfig"; 4 | import { RunnerOption } from "@/scenario/compile/common/runnerOption"; 5 | 6 | const DEFAULT_RUNNER_NAMES = ["fetch", "waitForSpan"]; 7 | export function buildRunners(): RunnerConfig[] { 8 | return DEFAULT_RUNNER_NAMES.map((name) => { 9 | return new RunnerConfig( 10 | name, 11 | "echoed/scenario/gen/dummy/runner", 12 | new RunnerOption(new Map()), 13 | ); 14 | }); 15 | } 16 | 17 | const DEFAULT_ASSERTER_NAMES = ["assertStatus"]; 18 | export function buildAsserters(): AsserterConfig[] { 19 | return DEFAULT_ASSERTER_NAMES.map((name) => { 20 | return new AsserterConfig( 21 | name, 22 | "echoed/scenario/gen/dummy/asserter", 23 | new Map(), 24 | ); 25 | }); 26 | } 27 | 28 | export function buildCommonPlugins(): CommonPluginConfig[] { 29 | return []; 30 | } 31 | -------------------------------------------------------------------------------- /src/testUtil/type/jsonSpan.ts: -------------------------------------------------------------------------------- 1 | import { JsonSpan } from "@/type/jsonSpan"; 2 | 3 | const DEFAULT_JSON_SPAN = { 4 | name: "testSpan", 5 | attributes: [], 6 | parentSpanId: "", 7 | spanId: "", 8 | traceId: "", 9 | }; 10 | 11 | export function buildJsonSpan(overrides: Partial = {}): JsonSpan { 12 | return { 13 | ...DEFAULT_JSON_SPAN, 14 | ...overrides, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/type/common.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const ErrorMessage = z.strictObject({ 4 | error: z.literal(true), 5 | reason: z.string(), 6 | }); 7 | export type ErrorMessage = z.infer; 8 | -------------------------------------------------------------------------------- /src/type/hexString.ts: -------------------------------------------------------------------------------- 1 | export class HexString { 2 | constructor(public hexString: string) {} 3 | 4 | equals(other: HexString): boolean { 5 | return this.hexString === other.hexString; 6 | } 7 | 8 | get uint8Array(): Uint8Array { 9 | return new Uint8Array(Buffer.from(this.hexString, "hex")); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/type/http.ts: -------------------------------------------------------------------------------- 1 | import { HttpMethod, HttpMethods } from "@shared/type/http"; 2 | 3 | const MethodsSet = new Set(HttpMethods); 4 | export function toMethod(method: string): HttpMethod | undefined { 5 | const normalizedMethod = method.toLowerCase(); 6 | if (!MethodsSet.has(normalizedMethod as HttpMethod)) { 7 | return undefined; 8 | } 9 | return normalizedMethod as HttpMethod; 10 | } 11 | -------------------------------------------------------------------------------- /src/type/jsonZod.test.ts: -------------------------------------------------------------------------------- 1 | import { JsonSchema } from "@/type/jsonZod"; 2 | 3 | describe("JsonSchema", () => { 4 | describe("when value is literal", () => { 5 | it("should return value", () => { 6 | const actual = JsonSchema.parse(1); 7 | expect(actual).toEqual(1); 8 | }); 9 | }); 10 | 11 | describe("when value is array", () => { 12 | it("should return value", () => { 13 | const actual = JsonSchema.parse([1, 2, 3]); 14 | expect(actual).toEqual([1, 2, 3]); 15 | }); 16 | }); 17 | 18 | describe("when value is object", () => { 19 | it("should return value", () => { 20 | const actual = JsonSchema.parse({ a: 1, b: 2, c: 3 }); 21 | expect(actual).toEqual({ a: 1, b: 2, c: 3 }); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/type/jsonZod.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const Literal = z.union([z.string(), z.boolean(), z.number(), z.null()]); 4 | type Literal = z.infer; 5 | 6 | type Json = Literal | { [key: string]: Json } | Json[]; 7 | 8 | export const JsonSchema: z.ZodSchema = z.lazy(() => 9 | z.union([Literal, z.array(JsonSchema), z.record(JsonSchema)]), 10 | ); 11 | export type JsonSchema = z.infer; 12 | -------------------------------------------------------------------------------- /src/type/otelLogRecord.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetry } from "@/generated/otelpbj"; 2 | import LogRecord = opentelemetry.proto.logs.v1.LogRecord; 3 | 4 | export class OtelLogRecord extends LogRecord { 5 | static fromJsonLogs( 6 | logRecords: opentelemetry.proto.logs.v1.ILogRecord[], 7 | ): OtelLogRecord[] { 8 | return logRecords.map((log) => LogRecord.fromObject(log)); 9 | } 10 | 11 | constructor(logRecord: opentelemetry.proto.logs.v1.ILogRecord) { 12 | super(logRecord); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/type/testCase.ts: -------------------------------------------------------------------------------- 1 | export class TestCase { 2 | constructor( 3 | public testId: string, 4 | public file: string, 5 | public name: string, 6 | public startTimeMillis: number, 7 | public status: string, 8 | public duration: number, 9 | /** 10 | testEndTimeMillis is time Echoed recognize test finished at. 11 | 12 | Because frameworks don't provide test end time, Echoed estimate it. 13 | This time cannot be displayed to user or used to calculate duration due to ambiguity. 14 | But it guarantees the test was running while startTimeMillis to testEndTimeMillis, so it can be used to identify what event happened while testing. 15 | */ 16 | public testEndTimeMillis: number, 17 | public failureDetails?: string[], 18 | public failureMessages?: string[], 19 | ) {} 20 | } 21 | -------------------------------------------------------------------------------- /src/util/ansi.ts: -------------------------------------------------------------------------------- 1 | export const AnsiReset = "\x1b[0m"; 2 | export const AnsiGray = "\x1b[2m"; 3 | export const AnsiRed = "\x1b[31m"; 4 | export const AnsiGreen = "\x1b[32m"; 5 | export const AnsiYellow = "\x1b[33m"; 6 | -------------------------------------------------------------------------------- /src/util/array.ts: -------------------------------------------------------------------------------- 1 | export const addOrOverride = ( 2 | origin: T[], 3 | merge: T[], 4 | keyFn: (_: T) => string, 5 | ): T[] => { 6 | const mergeTargetMap = new Map(merge.map((v) => [keyFn(v), v])); 7 | 8 | const originKeys = new Set(); 9 | const ret: T[] = []; 10 | for (const elm of origin) { 11 | const key = keyFn(elm); 12 | originKeys.add(key); 13 | 14 | const value = mergeTargetMap.get(key); 15 | 16 | if (value) { 17 | ret.push(value); 18 | } else { 19 | ret.push(elm); 20 | } 21 | } 22 | 23 | for (const elm of merge) { 24 | const key = keyFn(elm); 25 | if (!originKeys.has(key)) { 26 | ret.push(elm); 27 | } 28 | } 29 | 30 | return ret; 31 | }; 32 | -------------------------------------------------------------------------------- /src/util/async.ts: -------------------------------------------------------------------------------- 1 | const sleepingCallbackIntervalMs = 1000; 2 | type sleepingCallbackFn = () => void; 3 | 4 | export async function sleep( 5 | ms: number, 6 | sleeping: sleepingCallbackFn = (): void => {}, 7 | ): Promise { 8 | let waitMs = 0; 9 | while (waitMs < ms) { 10 | if (waitMs > 0) { 11 | sleeping(); 12 | } 13 | 14 | const nextMs = Math.min(ms - waitMs, sleepingCallbackIntervalMs); 15 | await new Promise((res) => setTimeout(res, nextMs)); 16 | waitMs += nextMs; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/util/byte.ts: -------------------------------------------------------------------------------- 1 | import { HexString } from "@/type/hexString"; 2 | 3 | export function toBase64String(bytes: Uint8Array | null | undefined): string { 4 | if (!bytes || bytes.length === 0) { 5 | return ""; 6 | } 7 | return Buffer.from(bytes).toString("base64"); 8 | } 9 | 10 | export function decodeBase64(base64: string): Uint8Array { 11 | return new Uint8Array(Buffer.from(base64, "base64")); 12 | } 13 | 14 | export function toHexString(bytes: Uint8Array | null | undefined): string { 15 | if (!bytes || bytes.length === 0) { 16 | return ""; 17 | } 18 | return Buffer.from(bytes).toString("hex"); 19 | } 20 | 21 | export function toHex(bytes: Uint8Array | null | undefined): HexString { 22 | return new HexString(toHexString(bytes)); 23 | } 24 | -------------------------------------------------------------------------------- /src/util/eta.ts: -------------------------------------------------------------------------------- 1 | import { IDirectory } from "@/fs/iDirectory"; 2 | import { Eta } from "eta"; 3 | 4 | export const buildNoEscapeEta = (viewsDirectory: IDirectory): Eta => { 5 | return new Eta({ 6 | views: viewsDirectory.path, 7 | autoEscape: false, 8 | cache: true, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /src/util/file.ts: -------------------------------------------------------------------------------- 1 | export function omitDirPath(file: string, dir: string): string { 2 | return file.replace(dir, ""); 3 | } 4 | -------------------------------------------------------------------------------- /src/util/map.ts: -------------------------------------------------------------------------------- 1 | export function arrayToMap( 2 | vals: T[], 3 | fn: (v: T) => [K, V], 4 | ): Map { 5 | const res = new Map(); 6 | for (const val of vals) { 7 | const [k, v] = fn(val); 8 | res.set(k, v); 9 | } 10 | 11 | return res; 12 | } 13 | -------------------------------------------------------------------------------- /src/util/never.ts: -------------------------------------------------------------------------------- 1 | export function neverVisit(message: string, x: never): never { 2 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 3 | throw new Error(`${message}: ${x}`); 4 | } 5 | -------------------------------------------------------------------------------- /src/util/proxy.test.ts: -------------------------------------------------------------------------------- 1 | import { buildRelativeIndexableArray } from "@/util/proxy"; 2 | 3 | describe("buildRelativeIndexableArray", () => { 4 | it("should creates array accessible with negative index", () => { 5 | const orig = [1, 2, 3]; 6 | const actual = buildRelativeIndexableArray(orig); 7 | 8 | expect(actual[0]).toBe(1); 9 | expect(actual[1]).toBe(2); 10 | expect(actual[2]).toBe(3); 11 | 12 | expect(actual[-1]).toBe(2); 13 | expect(actual[-2]).toBe(1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/util/random.test.ts: -------------------------------------------------------------------------------- 1 | import { buildRandomHexBytes, buildRandomHexUUID } from "@/util/random"; 2 | 3 | describe("buildRandomHexUUID", () => { 4 | it("should return hex string of length 32", () => { 5 | const res = buildRandomHexUUID(); 6 | expect(res.length).toBe(32); 7 | }); 8 | }); 9 | 10 | describe("buildRandomHexBytes", () => { 11 | it("should return hex string of specified byte size", () => { 12 | const length = 3; 13 | const res = buildRandomHexBytes(length); 14 | expect(res.length).toBe(length * 2); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/util/random.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | export function buildRandomHexUUID(): string { 4 | // Use `crypto.randomBytes()` instead of `crypto.randomUUID()` to create UUID because some Browser does not support `crypto.randomUUID()`. 5 | return buildRandomHexBytes(16); 6 | } 7 | 8 | export function buildRandomHexBytes(byteSize: number): string { 9 | return crypto.randomBytes(byteSize).toString("hex"); 10 | } 11 | -------------------------------------------------------------------------------- /src/util/request.test.ts: -------------------------------------------------------------------------------- 1 | import { isReadableContentType } from "@/util/request"; 2 | 3 | describe("isReadableContentType", () => { 4 | describe("when content type is application/json", () => { 5 | it("should return true", () => { 6 | expect(isReadableContentType("application/json")).toBe(true); 7 | }); 8 | }); 9 | 10 | describe("when content type is text/html", () => { 11 | it("should return true", () => { 12 | expect(isReadableContentType("text/html")).toBe(true); 13 | }); 14 | }); 15 | 16 | describe("when content type is application/octet-stream", () => { 17 | it("should return false", () => { 18 | expect(isReadableContentType("application/octet-stream")).toBe(false); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/util/request.ts: -------------------------------------------------------------------------------- 1 | const BODY_READABLE_CONTENT_TYPES = ["application/json", "text/"]; 2 | 3 | export function isReadableContentType(contentType: string): boolean { 4 | const lowerCaseContentType = contentType.toLowerCase(); 5 | 6 | for (const type of BODY_READABLE_CONTENT_TYPES) { 7 | if (lowerCaseContentType.includes(type)) { 8 | return true; 9 | } 10 | } 11 | 12 | return false; 13 | } 14 | -------------------------------------------------------------------------------- /src/util/string.test.ts: -------------------------------------------------------------------------------- 1 | import { toOnlyCharacters, truncateString } from "@/util/string"; 2 | 3 | describe("truncateString", () => { 4 | describe("when the string is shorter than the max length", () => { 5 | it("should not truncate the string", () => { 6 | expect(truncateString("hello", 10)).toBe("hello"); 7 | }); 8 | }); 9 | 10 | describe("when the string is equal to the max length", () => { 11 | it("should not truncate the string", () => { 12 | expect(truncateString("hello", 5)).toBe("hello"); 13 | }); 14 | }); 15 | 16 | describe("when the string is longer than the max length", () => { 17 | it("should truncate the string", () => { 18 | expect(truncateString("hello world", 5)).toBe("hello..."); 19 | }); 20 | }); 21 | }); 22 | 23 | describe("toOnlyCharacters", () => { 24 | it("should remove non-alphanumeric characters", () => { 25 | expect(toOnlyCharacters("hello, world!")).toBe("helloworld"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/util/string.ts: -------------------------------------------------------------------------------- 1 | export function truncateString(str: string, maxLength: number): string { 2 | if (str.length <= maxLength) { 3 | return str; 4 | } 5 | return str.substring(0, maxLength) + "..."; 6 | } 7 | 8 | export function toOnlyCharacters(str: string): string { 9 | return str.replace(/[^a-zA-Z1-9]/g, ""); 10 | } 11 | -------------------------------------------------------------------------------- /src/util/traceparent.ts: -------------------------------------------------------------------------------- 1 | import { HexString } from "@/type/hexString"; 2 | import { buildRandomHexBytes, buildRandomHexUUID } from "@/util/random"; 3 | 4 | export function generateTraceparent(): { 5 | traceparent: string; 6 | traceId: HexString; 7 | } { 8 | const traceId = buildRandomHexUUID(); 9 | const spanId = buildRandomHexBytes(8); 10 | 11 | const traceparent = `00-${traceId}-${spanId}-01`; 12 | 13 | return { traceparent, traceId: new HexString(traceId) }; 14 | } 15 | -------------------------------------------------------------------------------- /src/util/type.ts: -------------------------------------------------------------------------------- 1 | export function hasValue(val: T | undefined | null): val is T { 2 | return val !== undefined && val !== null; 3 | } 4 | -------------------------------------------------------------------------------- /src/util/zod.ts: -------------------------------------------------------------------------------- 1 | import { ZodError } from "zod"; 2 | 3 | export const formatZodError = (error: ZodError): string => { 4 | const v = JSON.stringify( 5 | error.format(), 6 | (k, v: unknown) => { 7 | if (Array.isArray(v)) { 8 | if (v.length === 0) return undefined; 9 | } 10 | return v; 11 | }, 12 | 2, 13 | ); 14 | 15 | return v; 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "nodenext", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "declaration": false, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["./src/*"], 13 | "@shared/*": ["./shared/*"], 14 | }, 15 | "skipLibCheck": true, 16 | "noImplicitOverride": true 17 | }, 18 | "include": ["src/**/*", "../shared/**/*"], 19 | } 20 | --------------------------------------------------------------------------------