diff --git a/augmentations.d.ts b/augmentations.d.ts index 617e64b5..51724cc5 100644 --- a/augmentations.d.ts +++ b/augmentations.d.ts @@ -17,8 +17,6 @@ declare module "@cucumber/cucumber" { declare global { namespace globalThis { - var __cypress_cucumber_preprocessor_dont_use_this: true | undefined; - var __cypress_cucumber_preprocessor_registry_dont_use_this: | Registry | undefined; diff --git a/docs/faq.md b/docs/faq.md index aa4ae298..092b7140 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -30,7 +30,7 @@ You may have stumbled upon a configuration caveat (see [docs/configuration.md: C ## JSON reports aren't generated in open / interactive mode -JSON reports aren't typically generated in open / interactive mode. They rely on some events that aren't available in open-mode, at least not without `experimentalInteractiveRunEvents: true`. However, this experimental flag broke some time ago, ref. [cypress-io/cypress#18955](https://github.com/cypress-io/cypress/issues/18955). +JSON reports aren't generated in open / interactive mode. They rely on some events that aren't available in open-mode, at least not without `experimentalInteractiveRunEvents: true`. However, this experimental flag broke some time ago, ref. [cypress-io/cypress#18955](https://github.com/cypress-io/cypress/issues/18955), [cypress-io/cypress#26634](https://github.com/cypress-io/cypress/issues/26634). There's unfortunately little indication that these issues will be fixed and meanwhile reports will not be available in open / interactive mode. ## I get `cypress_esbuild_preprocessor_1.createBundler is not a function` diff --git a/features/fixtures/attachments/screenshot.ndjson b/features/fixtures/attachments/screenshot.ndjson index 77cf9061..d2e77843 100644 --- a/features/fixtures/attachments/screenshot.ndjson +++ b/features/fixtures/attachments/screenshot.ndjson @@ -2,11 +2,11 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} -{"attachment":{"testCaseStartedId": "id", "testStepId":"id","body":"iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAM0lEQVR4Aa3BAQEAAAiDMKR/51uC7QYjJDGJSUxiEpOYxCQmMYlJTGISk5jEJCYxiUnsARwEAibDACoRAAAAAElFTkSuQmCC","mediaType":"image/png","contentEncoding":"BASE64"}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} +{"attachment":{"testCaseStartedId":"id","testStepId":"id","body":"iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAM0lEQVR4Aa3BAQEAAAiDMKR/51uC7QYjJDGJSUxiEpOYxCQmMYlJTGISk5jEJCYxiUnsARwEAibDACoRAAAAAElFTkSuQmCC","mediaType":"image/png","contentEncoding":"BASE64"}} {"testStepFinished":{"testStepId":"id","testCaseStartedId":"id","testStepResult":{"status":"PASSED","duration":0},"timestamp":{"seconds":0,"nanos":0}}} {"testCaseFinished":{"testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0},"willBeRetried":false}} {"testRunFinished":{"timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/failing-step.ndjson b/features/fixtures/failing-step.ndjson index 9aece8f1..d92be2fc 100644 --- a/features/fixtures/failing-step.ndjson +++ b/features/fixtures/failing-step.ndjson @@ -2,8 +2,8 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a failing step\n And another step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a failing step"},{"id":"id","location":{"line":4,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"another step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a failing step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"another step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a failing step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"another step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/passed-outline.ndjson b/features/fixtures/passed-outline.ndjson index f5091720..4a294d7b 100644 --- a/features/fixtures/passed-outline.ndjson +++ b/features/fixtures/passed-outline.ndjson @@ -3,9 +3,8 @@ {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario Outline","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[{"id":"id","tags":[],"location":{"line":4,"column":5},"keyword":"Examples","name":"","description":"","tableHeader":{"id":"id","location":{"line":5,"column":7},"cells":[{"location":{"line":5,"column":9},"value":"value"}]},"tableBody":[{"id":"id","location":{"line":6,"column":7},"cells":[{"location":{"line":6,"column":9},"value":"foo"}]},{"id":"id","location":{"line":7,"column":7},"cells":[{"location":{"line":7,"column":9},"value":"bar"}]}]}]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id","id"],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id","id"]}],"tags":[]}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id","id"],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a step","type":"Context","astNodeIds":["id","id"]}],"tags":[]}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/pending-steps.ndjson b/features/fixtures/pending-steps.ndjson index 94b072f9..1bc9d8e2 100644 --- a/features/fixtures/pending-steps.ndjson +++ b/features/fixtures/pending-steps.ndjson @@ -2,9 +2,9 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a pending step\n And another pending step\n And an implemented step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a pending step"},{"id":"id","location":{"line":4,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"another pending step"},{"id":"id","location":{"line":5,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an implemented step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a pending step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"another pending step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"an implemented step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a pending step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"another pending step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an implemented step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} {"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/rescued-error.json b/features/fixtures/rescued-error.json index a49c1d07..d784750d 100644 --- a/features/fixtures/rescued-error.json +++ b/features/fixtures/rescued-error.json @@ -30,9 +30,6 @@ "result": { "status": "unknown", "duration": 0 - }, - "match": { - "location": "not available:0" } } ], diff --git a/features/fixtures/rescued-error.ndjson b/features/fixtures/rescued-error.ndjson index 0042c4e6..3b4f9d91 100644 --- a/features/fixtures/rescued-error.ndjson +++ b/features/fixtures/rescued-error.ndjson @@ -2,9 +2,8 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given a failing step\n And an unimplemented step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a failing step"},{"id":"id","location":{"line":4,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an unimplemented step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"a failing step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"an unimplemented step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a failing step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":[]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/fixtures/undefined-steps.json b/features/fixtures/undefined-steps.json index 8969fb3b..f4f2447a 100644 --- a/features/fixtures/undefined-steps.json +++ b/features/fixtures/undefined-steps.json @@ -17,9 +17,6 @@ "result": { "status": "undefined", "duration": 0 - }, - "match": { - "location": "not available:0" } }, { @@ -30,9 +27,6 @@ "result": { "status": "skipped", "duration": 0 - }, - "match": { - "location": "not available:0" } } ], diff --git a/features/fixtures/undefined-steps.ndjson b/features/fixtures/undefined-steps.ndjson index af8e8dfe..765f9b7e 100644 --- a/features/fixtures/undefined-steps.ndjson +++ b/features/fixtures/undefined-steps.ndjson @@ -2,9 +2,8 @@ {"source":{"data":"Feature: a feature\n Scenario: a scenario\n Given an undefined step\n And another step","uri":"cypress/e2e/a.feature","mediaType":"text/x.cucumber.gherkin+plain"}} {"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"a feature","description":"","children":[{"scenario":{"id":"id","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"id","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"an undefined step"},{"id":"id","location":{"line":4,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"another step"}],"examples":[]}}]},"comments":[],"uri":"cypress/e2e/a.feature"}} {"pickle":{"id":"id","uri":"cypress/e2e/a.feature","astNodeIds":["id"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"id","text":"an undefined step","type":"Context","astNodeIds":["id"]},{"id":"id","text":"another step","type":"Context","astNodeIds":["id"]}]}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"stepDefinition":{"id":"id","pattern":{"source":"a step","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"uri":"not available","location":{"line":0}}}} -{"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]},{"id":"id","pickleStepId":"id","stepDefinitionIds":["id"]}]}} +{"stepDefinition":{"id":"id","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a defined step"},"sourceReference":{"uri":"not available","location":{"line":0}}}} +{"testCase":{"id":"id","pickleId":"id","testSteps":[{"id":"id","pickleStepId":"id","stepDefinitionIds":[]},{"id":"id","pickleStepId":"id","stepDefinitionIds":[]}]}} {"testCaseStarted":{"id":"id","testCaseId":"id","attempt":0,"timestamp":{"seconds":0,"nanos":0}}} {"testStepStarted":{"testStepId":"id","testCaseStartedId":"id","timestamp":{"seconds":0,"nanos":0}}} {"testStepFinished":{"testStepId":"id","testCaseStartedId":"id","testStepResult":{"status":"UNDEFINED","duration":0},"timestamp":{"seconds":0,"nanos":0}}} diff --git a/features/reporters/messages.feature b/features/reporters/messages.feature index 7383ee15..79b5cab4 100644 --- a/features/reporters/messages.feature +++ b/features/reporters/messages.feature @@ -121,7 +121,8 @@ Feature: messages report Given additional Cypress configuration """ { - "screenshotOnRunFailure": false + "screenshotOnRunFailure": false, + "experimentalInteractiveRunEvents": true } """ And a file named "cypress/e2e/a.feature" with: diff --git a/lib/add-cucumber-preprocessor-plugin.ts b/lib/add-cucumber-preprocessor-plugin.ts index 651c279b..60c0ba1a 100644 --- a/lib/add-cucumber-preprocessor-plugin.ts +++ b/lib/add-cucumber-preprocessor-plugin.ts @@ -14,22 +14,26 @@ import { import { INTERNAL_PROPERTY_NAME, INTERNAL_SUITE_PROPERTIES } from "./constants"; import { - TASK_APPEND_MESSAGES, + TASK_SPEC_ENVELOPES, TASK_CREATE_STRING_ATTACHMENT, TASK_TEST_CASE_STARTED, TASK_TEST_STEP_STARTED, + TASK_TEST_STEP_FINISHED, + TASK_TEST_CASE_FINISHED, } from "./cypress-task-definitions"; import { afterRunHandler, afterScreenshotHandler, afterSpecHandler, - appendMessagesHandler, + specEnvelopesHandler, beforeRunHandler, beforeSpecHandler, createStringAttachmentHandler, testCaseStartedHandler, testStepStartedHandler, + testStepFinishedHandler, + testCaseFinishedHandler, } from "./plugin-event-handlers"; import { resolve as origResolve } from "./preprocessor-configuration"; @@ -107,10 +111,15 @@ export default async function addCucumberPreprocessorPlugin( } on("task", { - [TASK_APPEND_MESSAGES]: appendMessagesHandler, - [TASK_TEST_CASE_STARTED]: testCaseStartedHandler, - [TASK_TEST_STEP_STARTED]: testStepStartedHandler, - [TASK_CREATE_STRING_ATTACHMENT]: createStringAttachmentHandler, + [TASK_SPEC_ENVELOPES]: specEnvelopesHandler.bind(null, config), + [TASK_TEST_CASE_STARTED]: testCaseStartedHandler.bind(null, config), + [TASK_TEST_STEP_STARTED]: testStepStartedHandler.bind(null, config), + [TASK_TEST_STEP_FINISHED]: testStepFinishedHandler.bind(null, config), + [TASK_TEST_CASE_FINISHED]: testCaseFinishedHandler.bind(null, config), + [TASK_CREATE_STRING_ATTACHMENT]: createStringAttachmentHandler.bind( + null, + config + ), }); const tags = getTags(config.env); diff --git a/lib/browser-runtime.ts b/lib/browser-runtime.ts index 8e4ee13e..e46d04ab 100644 --- a/lib/browser-runtime.ts +++ b/lib/browser-runtime.ts @@ -1,10 +1,11 @@ -import messages, { TestStepResultStatus } from "@cucumber/messages"; +import * as messages from "@cucumber/messages"; import parse from "@cucumber/tag-expressions"; import { CucumberExpressionGenerator, ParameterTypeRegistry, + RegularExpression, } from "@cucumber/cucumber-expressions"; import { v4 as uuid } from "uuid"; @@ -29,22 +30,23 @@ import { import { HOOK_FAILURE_EXPR, - INTERNAL_PROPERTY_NAME, INTERNAL_SPEC_PROPERTIES, INTERNAL_SUITE_PROPERTIES, } from "./constants"; import { - ITaskAppendMessages, + ITaskSpecEnvelopes, ITaskTestCaseStarted, + ITaskTestCaseFinished, ITaskTestStepStarted, - TASK_APPEND_MESSAGES, + ITaskTestStepFinished, + TASK_SPEC_ENVELOPES, TASK_TEST_CASE_STARTED, + TASK_TEST_CASE_FINISHED, TASK_TEST_STEP_STARTED, + TASK_TEST_STEP_FINISHED, } from "./cypress-task-definitions"; -import { getTags } from "./helpers/environment"; - import { notNull } from "./helpers/type-guards"; import { looksLikeOptions, tagToCypressOptions } from "./helpers/tag-parser"; @@ -57,6 +59,8 @@ import { generateSnippet } from "./helpers/snippets"; import { runStepWithLogGroup } from "./helpers/cypress"; +import { getTags } from "./helpers/environment"; + type Node = ReturnType; interface CompositionContext { @@ -64,12 +68,10 @@ interface CompositionContext { gherkinDocument: messages.GherkinDocument; astIdsMap: ReturnType; pickles: messages.Pickle[]; + specEnvelopes: messages.Envelope[]; testFilter: Node; omitFiltered: boolean; - messages: { - enabled: boolean; - stack: messages.Envelope[]; - }; + messagesEnabled: boolean; stepDefinitionHints: { stepDefinitions: string | string[]; stepDefinitionPatterns: string[]; @@ -131,14 +133,34 @@ function retrieveInternalSuiteProperties(): return Cypress.env(INTERNAL_SUITE_PROPERTIES); } -function flushMessages(messages: CompositionContext["messages"]) { - if (messages.enabled) { - const data: ITaskAppendMessages = { - messages: messages.stack.splice(0, messages.stack.length), - }; +function taskSpecEnvelopes(messages: messages.Envelope[]) { + cy.task(TASK_SPEC_ENVELOPES, { messages } as ITaskSpecEnvelopes, { + log: false, + }); +} - cy.task(TASK_APPEND_MESSAGES, data, { log: false }); - } +function taskTestCaseStarted(testCaseStarted: messages.TestCaseStarted) { + cy.task(TASK_TEST_CASE_STARTED, testCaseStarted as ITaskTestCaseStarted, { + log: false, + }); +} + +function taskTestCaseFinished(testCasefinished: messages.TestCaseFinished) { + cy.task(TASK_TEST_CASE_FINISHED, testCasefinished as ITaskTestCaseFinished, { + log: false, + }); +} + +function taskTestStepStarted(testStepStarted: messages.TestStepStarted) { + cy.task(TASK_TEST_STEP_STARTED, testStepStarted as ITaskTestStepStarted, { + log: false, + }); +} + +function taskTestStepFinished(testStepfinished: messages.TestStepFinished) { + cy.task(TASK_TEST_STEP_FINISHED, testStepfinished as ITaskTestStepFinished, { + log: false, + }); } function findPickleById(context: CompositionContext, astId: string) { @@ -165,6 +187,18 @@ function collectExampleIds(examples: readonly messages.Examples[]) { function createFeature(context: CompositionContext, feature: messages.Feature) { describe(feature.name || "", () => { + before(function () { + beforeHandler.call(this, context); + }); + + beforeEach(function () { + beforeEachHandler.call(this, context); + }); + + afterEach(function () { + afterEachHandler.call(this, context); + }); + if (feature.children) { for (const child of feature.children) { if (child.scenario) { @@ -227,14 +261,11 @@ function createScenario( for (let i = 0; i < exampleIds.length; i++) { const exampleId = exampleIds[i]; - const pickle = findPickleById(context, exampleId); - const baseName = pickle.name || ""; - const exampleName = `${baseName} (example #${i + 1})`; - createPickle(context, { ...scenario, name: exampleName }, pickle); + createPickle(context, { ...pickle, name: exampleName }); } } else { const scenarioId = assertAndReturn( @@ -244,23 +275,18 @@ function createScenario( const pickle = findPickleById(context, scenarioId); - createPickle(context, scenario, pickle); + createPickle(context, pickle); } } -function createPickle( - context: CompositionContext, - scenario: messages.Scenario, - pickle: messages.Pickle -) { - const { registry, gherkinDocument, pickles, testFilter, messages } = context; - const testCaseId = uuid(); +function createPickle(context: CompositionContext, pickle: messages.Pickle) { + const { registry, gherkinDocument, pickles, testFilter } = context; + const testCaseId = pickle.id; const pickleSteps = pickle.steps ?? []; - const scenarioName = scenario.name || ""; + const scenarioName = pickle.name || ""; const tags = collectTagNames(pickle.tags); const beforeHooks = registry.resolveBeforeHooks(tags); const afterHooks = registry.resolveAfterHooks(tags); - const definitionIds = pickleSteps.map(() => uuid()); const steps: IStep[] = [ ...beforeHooks.map((hook) => ({ hook })), @@ -268,54 +294,6 @@ function createPickle( ...afterHooks.map((hook) => ({ hook })), ]; - // TODO: Why am I doing this? For example, an undefined step should not resolve to a step definition with location "not available:0". - for (const id of definitionIds) { - messages.stack.push({ - stepDefinition: { - id, - pattern: { - source: "a step", - type: "CUCUMBER_EXPRESSION" as unknown as messages.StepDefinitionPatternType.CUCUMBER_EXPRESSION, - }, - sourceReference, - }, - }); - } - - const testSteps: messages.TestStep[] = []; - - for (const beforeHook of beforeHooks) { - testSteps.push({ - id: beforeHook.id, - hookId: beforeHook.id, - }); - } - - for (let i = 0; i < pickleSteps.length; i++) { - const step = pickleSteps[i]; - - testSteps.push({ - id: step.id, - pickleStepId: step.id, - stepDefinitionIds: [definitionIds[i]], - }); - } - - for (const afterHook of afterHooks) { - testSteps.push({ - id: afterHook.id, - hookId: afterHook.id, - }); - } - - messages.stack.push({ - testCase: { - id: testCaseId, - pickleId: pickle.id, - testSteps, - }, - }); - if (!testFilter.evaluate(tags) || tags.includes("@skip")) { if (!context.omitFiltered) { it.skip(scenarioName); @@ -353,25 +331,15 @@ function createPickle( const { remainingSteps, testCaseStartedId } = retrieveInternalSpecProperties(); - assignRegistry(registry); - - messages.stack.push({ - testCaseStarted: { + if (context.messagesEnabled) { + taskTestCaseStarted({ id: testCaseStartedId, testCaseId, attempt: attempt++, timestamp: createTimestamp(), - }, - }); - - if (messages.enabled) { - const data: ITaskTestCaseStarted = { testCaseStartedId }; - - cy.task(TASK_TEST_CASE_STARTED, data, { log: false }); + }); } - flushMessages(context.messages); - window.testState = { gherkinDocument, pickles, @@ -387,18 +355,12 @@ function createPickle( const start = createTimestamp(); - messages.stack.push({ - testStepStarted: { + if (context.messagesEnabled) { + taskTestStepStarted({ testStepId: hook.id, testCaseStartedId, timestamp: start, - }, - }); - - if (messages.enabled) { - const data: ITaskTestStepStarted = { testStepId: hook.id }; - - cy.task(TASK_TEST_STEP_STARTED, data, { log: false }); + }); } return cy.wrap(start, { log: false }); @@ -415,17 +377,17 @@ function createPickle( .then((start) => { const end = createTimestamp(); - messages.stack.push({ - testStepFinished: { + if (context.messagesEnabled) { + taskTestStepFinished({ testStepId: hook.id, testCaseStartedId, testStepResult: { - status: TestStepResultStatus.PASSED, + status: messages.TestStepResultStatus.PASSED, duration: duration(start, end), }, timestamp: end, - }, - }); + }); + } remainingSteps.shift(); }); @@ -461,18 +423,12 @@ function createPickle( const start = createTimestamp(); - messages.stack.push({ - testStepStarted: { + if (context.messagesEnabled) { + taskTestStepStarted({ testStepId: pickleStep.id, testCaseStartedId, timestamp: start, - }, - }); - - if (messages.enabled) { - const data: ITaskTestStepStarted = { testStepId: pickleStep.id }; - - cy.task(TASK_TEST_STEP_STARTED, data, { log: false }); + }); } return cy.wrap(start, { log: false }); @@ -510,67 +466,63 @@ function createPickle( const end = createTimestamp(); if (result === "pending") { - messages.stack.push({ - testStepFinished: { + if (context.messagesEnabled) { + taskTestStepFinished({ testStepId: pickleStep.id, testCaseStartedId, testStepResult: { - status: TestStepResultStatus.PENDING, + status: messages.TestStepResultStatus.PENDING, duration: duration(start, end), }, timestamp: end, - }, - }); + }); - remainingSteps.shift(); + remainingSteps.shift(); - for (const skippedStep of remainingSteps) { - const testStepId = assertAndReturn( - skippedStep.hook?.id ?? skippedStep.pickleStep?.id, - "Expected a step to either be a hook or a pickleStep" - ); + for (const skippedStep of remainingSteps) { + const testStepId = assertAndReturn( + skippedStep.hook?.id ?? skippedStep.pickleStep?.id, + "Expected a step to either be a hook or a pickleStep" + ); - messages.stack.push({ - testStepStarted: { + taskTestStepStarted({ testStepId, testCaseStartedId, timestamp: createTimestamp(), - }, - }); + }); - messages.stack.push({ - testStepFinished: { + taskTestStepFinished({ testStepId, testCaseStartedId, testStepResult: { - status: TestStepResultStatus.SKIPPED, + status: messages.TestStepResultStatus.SKIPPED, duration: { seconds: 0, nanos: 0, }, }, timestamp: createTimestamp(), - }, - }); + }); + } } for (let i = 0, count = remainingSteps.length; i < count; i++) { remainingSteps.pop(); } - this.skip(); + cy.then(() => this.skip()); } else { - messages.stack.push({ - testStepFinished: { + if (context.messagesEnabled) { + taskTestStepFinished({ testStepId: pickleStep.id, testCaseStartedId, testStepResult: { - status: TestStepResultStatus.PASSED, + status: messages.TestStepResultStatus.PASSED, duration: duration(start, end), }, timestamp: end, - }, - }); + }); + } remainingSteps.shift(); } @@ -594,106 +546,45 @@ function collectTagNamesFromGherkinDocument( return tagNames; } -export default function createTests( - registry: Registry, - source: string, +function createTestFilter( gherkinDocument: messages.GherkinDocument, - pickles: messages.Pickle[], - messagesEnabled: boolean, - omitFiltered: boolean, - stepDefinitionHints: { - stepDefinitions: string | string[]; - stepDefinitionPatterns: string[]; - stepDefinitionPaths: string[]; - } -) { - const noopNode = { evaluate: () => true }; - const environmentTags = getTags(Cypress.env()); - const messages: messages.Envelope[] = []; - - messages.push({ - source: { - data: source, - uri: assertAndReturn( - gherkinDocument.uri, - "Expected gherkin document to have URI" - ), - mediaType: - "text/x.cucumber.gherkin+plain" as messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN, - }, - }); - - messages.push({ - gherkinDocument: { - ...gherkinDocument, - }, - }); - - for (const pickle of pickles) { - messages.push({ - pickle, - }); - } - - for (const hook of [...registry.beforeHooks, ...registry.afterHooks]) { - messages.push({ - hook: { - id: hook.id, - sourceReference, - }, - }); - } - + environment: Cypress.ObjectLike +): Node { const tagsInDocument = collectTagNamesFromGherkinDocument(gherkinDocument); - const testFilter = - tagsInDocument.includes("@only") || tagsInDocument.includes("@focus") - ? parse("@only or @focus") - : environmentTags - ? parse(environmentTags) - : noopNode; - - const context: CompositionContext = { - registry, - gherkinDocument, - astIdsMap: createAstIdMap(gherkinDocument), - pickles, - testFilter, - omitFiltered, - messages: { - enabled: messagesEnabled, - stack: messages, - }, - stepDefinitionHints, - }; + if (tagsInDocument.includes("@only") || tagsInDocument.includes("@focus")) { + return parse("@only or @focus"); + } else { + const tags = getTags(environment); - if (gherkinDocument.feature) { - createFeature(context, gherkinDocument.feature); + return tags ? parse(tags) : { evaluate: () => true }; } +} - const isHooksAttached = globalThis[INTERNAL_PROPERTY_NAME]; +function beforeHandler(context: CompositionContext) { + if (!retrieveInternalSuiteProperties()?.isEventHandlersAttached) { + fail( + "Missing preprocessor event handlers (this usally means you've not invoked `addCucumberPreprocessorPlugin()` or not returned the config object in `setupNodeEvents()`)" + ); + } - if (isHooksAttached) { - return; - } else { - globalThis[INTERNAL_PROPERTY_NAME] = true; + if (context.messagesEnabled) { + taskSpecEnvelopes(context.specEnvelopes); } +} - before(function () { - if (!retrieveInternalSuiteProperties()?.isEventHandlersAttached) { - fail( - "Missing preprocessor event handlers (this usally means you've not invoked `addCucumberPreprocessorPlugin()` or not returned the config object in `setupNodeEvents()`)" - ); - } - }); +function beforeEachHandler(context: CompositionContext) { + assignRegistry(context.registry); +} - afterEach(function () { - freeRegistry(); +function afterEachHandler(this: Mocha.Context, context: CompositionContext) { + freeRegistry(); - const properties = retrieveInternalSpecProperties(); + const properties = retrieveInternalSpecProperties(); - const { testCaseStartedId, remainingSteps } = properties; + const { testCaseStartedId, remainingSteps } = properties; + if (context.messagesEnabled) { const endTimestamp = createTimestamp(); if (remainingSteps.length > 0) { @@ -717,29 +608,25 @@ export default function createTests( "Expected a step to either be a hook or a pickleStep" ); - const failedTestStepFinished: messages.Envelope = error.includes( - "Step implementation missing" - ) - ? { - testStepFinished: { + const failedTestStepFinished: messages.TestStepFinished = + error.includes("Step implementation missing") + ? { testStepId, testCaseStartedId, testStepResult: { - status: TestStepResultStatus.UNDEFINED, + status: messages.TestStepResultStatus.UNDEFINED, duration: { seconds: 0, nanos: 0, }, }, timestamp: endTimestamp, - }, - } - : { - testStepFinished: { + } + : { testStepId, testCaseStartedId, testStepResult: { - status: TestStepResultStatus.FAILED, + status: messages.TestStepResultStatus.FAILED, message: this.currentTest?.err?.message, // TODO: Create a proper duration from when the step started. duration: { @@ -748,10 +635,9 @@ export default function createTests( }, }, timestamp: endTimestamp, - }, - }; + }; - messages.push(failedTestStepFinished); + taskTestStepFinished(failedTestStepFinished); for (const skippedStep of remainingSteps) { const testStepId = assertAndReturn( @@ -759,27 +645,23 @@ export default function createTests( "Expected a step to either be a hook or a pickleStep" ); - messages.push({ - testStepStarted: { - testStepId, - testCaseStartedId, - timestamp: endTimestamp, - }, + taskTestStepStarted({ + testStepId, + testCaseStartedId, + timestamp: endTimestamp, }); - messages.push({ - testStepFinished: { - testStepId, - testCaseStartedId, - testStepResult: { - status: TestStepResultStatus.SKIPPED, - duration: { - seconds: 0, - nanos: 0, - }, + taskTestStepFinished({ + testStepId, + testCaseStartedId, + testStepResult: { + status: messages.TestStepResultStatus.SKIPPED, + duration: { + seconds: 0, + nanos: 0, }, - timestamp: endTimestamp, }, + timestamp: endTimestamp, }); } } else { @@ -789,27 +671,23 @@ export default function createTests( "Expected a step to either be a hook or a pickleStep" ); - messages.push({ - testStepStarted: { - testStepId, - testCaseStartedId, - timestamp: endTimestamp, - }, + taskTestStepStarted({ + testStepId, + testCaseStartedId, + timestamp: endTimestamp, }); - messages.push({ - testStepFinished: { - testStepId, - testCaseStartedId, - testStepResult: { - status: TestStepResultStatus.UNKNOWN, - duration: { - seconds: 0, - nanos: 0, - }, + taskTestStepFinished({ + testStepId, + testCaseStartedId, + testStepResult: { + status: messages.TestStepResultStatus.UNKNOWN, + duration: { + seconds: 0, + nanos: 0, }, - timestamp: endTimestamp, }, + timestamp: endTimestamp, }); } } @@ -828,26 +706,151 @@ export default function createTests( const willBeRetried = this.currentTest?.state === "failed" ? currentRetry < retries : false; - messages.push({ - testCaseFinished: { - testCaseStartedId, - timestamp: endTimestamp, - willBeRetried, - }, + taskTestCaseFinished({ + testCaseStartedId, + timestamp: endTimestamp, + willBeRetried, }); + } + + /** + * Repopulate internal properties in case previous test is retried. + */ + updateInternalSpecProperties({ + testCaseStartedId: uuid(), + remainingSteps: [...properties.allSteps], + }); +} - /** - * Repopulate internal properties in case previous test is retried. - */ - updateInternalSpecProperties({ - testCaseStartedId: uuid(), - remainingSteps: [...properties.allSteps], +export default function createTests( + registry: Registry, + source: string, + gherkinDocument: messages.GherkinDocument, + pickles: messages.Pickle[], + messagesEnabled: boolean, + omitFiltered: boolean, + stepDefinitionHints: { + stepDefinitions: string | string[]; + stepDefinitionPatterns: string[]; + stepDefinitionPaths: string[]; + } +) { + const stepDefinitions: messages.StepDefinition[] = + registry.stepDefinitions.map((stepDefinition) => { + const type: messages.StepDefinitionPatternType = + stepDefinition.expression instanceof RegularExpression + ? messages.StepDefinitionPatternType.REGULAR_EXPRESSION + : messages.StepDefinitionPatternType.CUCUMBER_EXPRESSION; + + return { + id: stepDefinition.id, + pattern: { + type, + source: stepDefinition.expression.source, + }, + sourceReference, + }; }); + + const testCases: messages.TestCase[] = pickles.map((pickle) => { + const tags = collectTagNames(pickle.tags); + const beforeHooks = registry.resolveBeforeHooks(tags); + const afterHooks = registry.resolveAfterHooks(tags); + + const hooksToStep = (hook: IHook): messages.TestStep => { + return { + id: hook.id, + hookId: hook.id, + }; + }; + + const pickleStepToTestStep = ( + pickleStep: messages.PickleStep + ): messages.TestStep => { + const stepDefinitionIds = registry + .getMatchingStepDefinitions(pickleStep.text) + .map((stepDefinition) => stepDefinition.id); + + return { + id: pickleStep.id, + pickleStepId: pickleStep.id, + stepDefinitionIds, + }; + }; + + return { + id: pickle.id, + pickleId: pickle.id, + testSteps: [ + ...beforeHooks.map(hooksToStep), + ...pickle.steps.map(pickleStepToTestStep), + ...afterHooks.map(hooksToStep), + ], + }; + }); + + const specEnvelopes: messages.Envelope[] = []; + + specEnvelopes.push({ + source: { + data: source, + uri: assertAndReturn( + gherkinDocument.uri, + "Expected gherkin document to have URI" + ), + mediaType: + "text/x.cucumber.gherkin+plain" as messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN, + }, }); - after(function () { - flushMessages(context.messages); + specEnvelopes.push({ + gherkinDocument, }); + + for (const pickle of pickles) { + specEnvelopes.push({ + pickle, + }); + } + + for (const hook of [...registry.beforeHooks, ...registry.afterHooks]) { + specEnvelopes.push({ + hook: { + id: hook.id, + sourceReference, + }, + }); + } + + for (const stepDefinition of stepDefinitions) { + specEnvelopes.push({ + stepDefinition, + }); + } + + for (const testCase of testCases) { + specEnvelopes.push({ + testCase, + }); + } + + const testFilter = createTestFilter(gherkinDocument, Cypress.env()); + + const context: CompositionContext = { + registry, + gherkinDocument, + astIdsMap: createAstIdMap(gherkinDocument), + pickles, + specEnvelopes, + testFilter, + omitFiltered, + messagesEnabled, + stepDefinitionHints, + }; + + if (gherkinDocument.feature) { + createFeature(context, gherkinDocument.feature); + } } type Tail = T extends [infer _A, ...infer R] ? R : never; diff --git a/lib/cypress-task-definitions.ts b/lib/cypress-task-definitions.ts index 6c0a17d8..d52fb9c9 100644 --- a/lib/cypress-task-definitions.ts +++ b/lib/cypress-task-definitions.ts @@ -1,25 +1,31 @@ import messages from "@cucumber/messages"; -export const TASK_APPEND_MESSAGES = - "cypress-cucumber-preprocessor:append-messages"; +export const TASK_SPEC_ENVELOPES = + "cypress-cucumber-preprocessor:spec-envelopes"; -export interface ITaskAppendMessages { +export interface ITaskSpecEnvelopes { messages: messages.Envelope[]; } export const TASK_TEST_CASE_STARTED = "cypress-cucumber-preprocessor:test-case-started"; -export interface ITaskTestCaseStarted { - testCaseStartedId: string; -} +export type ITaskTestCaseStarted = messages.TestCaseStarted; + +export const TASK_TEST_CASE_FINISHED = + "cypress-cucumber-preprocessor:test-case-finished"; + +export type ITaskTestCaseFinished = messages.TestCaseFinished; export const TASK_TEST_STEP_STARTED = "cypress-cucumber-preprocessor:test-step-started"; -export interface ITaskTestStepStarted { - testStepId: string; -} +export type ITaskTestStepStarted = messages.TestStepStarted; + +export const TASK_TEST_STEP_FINISHED = + "cypress-cucumber-preprocessor:test-step-finished"; + +export type ITaskTestStepFinished = messages.TestStepFinished; export const TASK_CREATE_STRING_ATTACHMENT = "cypress-cucumber-preprocessor:create-string-attachment"; diff --git a/lib/entrypoint-browser.ts b/lib/entrypoint-browser.ts index 033d50b3..ec06ec1a 100644 --- a/lib/entrypoint-browser.ts +++ b/lib/entrypoint-browser.ts @@ -4,7 +4,7 @@ import parse from "@cucumber/tag-expressions"; import { fromByteArray } from "base64-js"; -import { createError } from "./helpers/assertions"; +import { createError } from "./helpers/error"; import { collectTagNames } from "./helpers/ast"; diff --git a/lib/helpers/assertions.ts b/lib/helpers/assertions.ts index 17af3a19..a16d0a97 100644 --- a/lib/helpers/assertions.ts +++ b/lib/helpers/assertions.ts @@ -1,12 +1,6 @@ -import { isString } from "./type-guards"; - -const homepage = "https://github.com/badeball/cypress-cucumber-preprocessor"; +import { createError } from "./error"; -export function createError(message: string) { - return new Error( - `${message} (this might be a bug, please report at ${homepage})` - ); -} +import { isString } from "./type-guards"; export function fail(message: string) { throw createError(message); diff --git a/lib/helpers/error.ts b/lib/helpers/error.ts index 358ea69e..e9c635cd 100644 --- a/lib/helpers/error.ts +++ b/lib/helpers/error.ts @@ -1 +1,9 @@ +const homepage = "https://github.com/badeball/cypress-cucumber-preprocessor"; + export class CypressCucumberError extends Error {} + +export function createError(message: string) { + return new CypressCucumberError( + `${message} (this might be a bug, please report at ${homepage})` + ); +} diff --git a/lib/plugin-event-handlers.ts b/lib/plugin-event-handlers.ts index 3553bacf..c6aa7296 100644 --- a/lib/plugin-event-handlers.ts +++ b/lib/plugin-event-handlers.ts @@ -19,10 +19,12 @@ import messages from "@cucumber/messages"; import { HOOK_FAILURE_EXPR } from "./constants"; import { - ITaskAppendMessages, + ITaskSpecEnvelopes, ITaskCreateStringAttachment, ITaskTestCaseStarted, ITaskTestStepStarted, + ITaskTestStepFinished, + ITaskTestCaseFinished, } from "./cypress-task-definitions"; import { resolve as origResolve } from "./preprocessor-configuration"; @@ -35,13 +37,74 @@ import { notNull } from "./helpers/type-guards"; import { memoize } from "./helpers/memoize"; +import debug from "./helpers/debug"; + +import { createError } from "./helpers/error"; + const resolve = memoize(origResolve); -let currentTestCaseStartedId: string; -let currentTestStepStartedId: string; -let currentSpecMessages: messages.Envelope[]; +interface StateInitial { + state: "initial"; +} + +interface StateBeforeSpec { + state: "before-spec"; +} + +interface StateReceivedSpecEnvelopes { + state: "received-envelopes"; + messages: messages.Envelope[]; +} + +interface StateTestStarted { + state: "test-started"; + messages: messages.Envelope[]; + testCaseStartedId: string; +} + +interface StateStepStarted { + state: "step-started"; + messages: messages.Envelope[]; + testCaseStartedId: string; + testStepStartedId: string; +} + +interface StateStepFinished { + state: "step-finished"; + messages: messages.Envelope[]; + testCaseStartedId: string; +} + +interface StateTestFinished { + state: "test-finished"; + messages: messages.Envelope[]; +} + +interface StateAfterSpec { + state: "after-spec"; +} + +type State = + | StateInitial + | StateBeforeSpec + | StateReceivedSpecEnvelopes + | StateTestStarted + | StateStepStarted + | StateStepFinished + | StateTestFinished + | StateAfterSpec; + +let state: State = { + state: "initial", +}; export async function beforeRunHandler(config: Cypress.PluginConfigOptions) { + debug("beforeRunHandler()"); + + if (!config.isTextTerminal) { + return; + } + const preprocessor = await resolve(config, config.env, "/"); if (!preprocessor.messages.enabled) { @@ -67,6 +130,12 @@ export async function beforeRunHandler(config: Cypress.PluginConfigOptions) { } export async function afterRunHandler(config: Cypress.PluginConfigOptions) { + debug("afterRunHandler()"); + + if (!config.isTextTerminal) { + return; + } + const preprocessor = await resolve(config, config.env, "/"); if ( @@ -122,7 +191,7 @@ export async function afterRunHandler(config: Cypress.PluginConfigOptions) { const log = (output: string | Uint8Array) => { if (typeof output !== "string") { - throw new Error( + throw createError( "Expected a JSON output of string, but got " + typeof output ); } else { @@ -167,7 +236,7 @@ export async function afterRunHandler(config: Cypress.PluginConfigOptions) { } if (typeof jsonOutput !== "string") { - throw new Error( + throw createError( "Expected JSON formatter to have finished, but it never returned" ); } @@ -212,8 +281,31 @@ export async function afterRunHandler(config: Cypress.PluginConfigOptions) { } } -export async function beforeSpecHandler(_config: Cypress.PluginConfigOptions) { - currentSpecMessages = []; +export async function beforeSpecHandler(config: Cypress.PluginConfigOptions) { + debug("beforeSpecHandler()"); + + if (!config.isTextTerminal) { + return; + } + + const preprocessor = await resolve(config, config.env, "/"); + + if (!preprocessor.messages.enabled) { + return; + } + + switch (state.state) { + case "initial": + case "after-spec": + state = { + state: "before-spec", + }; + break; + default: + throw createError( + "Unexpected state in beforeSpecHandler: " + state.state + ); + } } export async function afterSpecHandler( @@ -221,6 +313,12 @@ export async function afterSpecHandler( spec: Cypress.Spec, results: CypressCommandLine.RunResult ) { + debug("afterSpecHandler()"); + + if (!config.isTextTerminal) { + return; + } + const preprocessor = await resolve(config, config.env, "/"); const messagesPath = ensureIsAbsolute( @@ -229,42 +327,57 @@ export async function afterSpecHandler( ); // `results` is undefined when running via `cypress open`. - if (!preprocessor.messages.enabled || !currentSpecMessages || !results) { - return; - } - - const wasRemainingSkipped = results.tests.some((test) => - test.displayError?.match(HOOK_FAILURE_EXPR) - ); - - if (wasRemainingSkipped) { - console.log( - chalk.yellow( - ` Hook failures can't be represented in messages / JSON reports, thus none is created for ${spec.relative}.` - ) - ); - } else { - await fs.writeFile( - messagesPath, - currentSpecMessages.map((message) => JSON.stringify(message)).join("\n") + - "\n", - { - flag: "a", - } + if (preprocessor.messages.enabled && results) { + const wasRemainingSkipped = results.tests.some((test) => + test.displayError?.match(HOOK_FAILURE_EXPR) ); + + if (wasRemainingSkipped) { + console.log( + chalk.yellow( + ` Hook failures can't be represented in messages / JSON reports, thus none is created for ${spec.relative}.` + ) + ); + } else if ("messages" in state) { + await fs.writeFile( + messagesPath, + state.messages.map((message) => JSON.stringify(message)).join("\n") + + "\n", + { + flag: "a", + } + ); + } } + + state = { + state: "after-spec", + }; } export async function afterScreenshotHandler( config: Cypress.PluginConfigOptions, details: Cypress.ScreenshotDetails ) { + debug("afterScreenshotHandler()"); + + if (!config.isTextTerminal) { + return details; + } + const preprocessor = await resolve(config, config.env, "/"); - if (!preprocessor.messages.enabled || !currentSpecMessages) { + if (!preprocessor.messages.enabled) { return details; } + switch (state.state) { + case "step-started": + break; + default: + return details; + } + let buffer; try { @@ -275,8 +388,8 @@ export async function afterScreenshotHandler( const message: messages.Envelope = { attachment: { - testCaseStartedId: currentTestCaseStartedId, - testStepId: currentTestStepStartedId, + testCaseStartedId: state.testCaseStartedId, + testStepId: state.testStepStartedId, body: buffer.toString("base64"), mediaType: "image/png", contentEncoding: @@ -284,61 +397,192 @@ export async function afterScreenshotHandler( }, }; - currentSpecMessages.push(message); + state.messages.push(message); return details; } -export function appendMessagesHandler(data: ITaskAppendMessages) { - if (!currentSpecMessages) { +export function specEnvelopesHandler( + config: Cypress.PluginConfigOptions, + data: ITaskSpecEnvelopes +) { + debug("specEnvelopesHandler()"); + + if (!config.isTextTerminal) { + return true; + } + + switch (state.state) { + case "before-spec": + break; + default: + throw createError( + "Unexpected state in specEnvelopesHandler: " + state.state + ); + } + + state = { + state: "received-envelopes", + messages: data.messages, + }; + + return true; +} + +export function testCaseStartedHandler( + config: Cypress.PluginConfigOptions, + data: ITaskTestCaseStarted +) { + debug("testCaseStartedHandler()"); + + if (!config.isTextTerminal) { + return true; + } + + switch (state.state) { + case "received-envelopes": + case "test-finished": + break; + default: + throw createError( + "Unexpected state in testCaseStartedHandler: " + state.state + ); + } + + state = { + state: "test-started", + messages: state.messages.concat({ testCaseStarted: data }), + testCaseStartedId: data.id, + }; + + return true; +} + +export function testStepStartedHandler( + config: Cypress.PluginConfigOptions, + data: ITaskTestStepStarted +) { + debug("testStepStartedHandler()"); + + if (!config.isTextTerminal) { return true; } - currentSpecMessages.push(...data.messages); + switch (state.state) { + case "test-started": + case "step-finished": + break; + // This state can happen in cases where an error is "rescued". + case "step-started": + break; + default: + throw createError( + "Unexpected state in testStepStartedHandler: " + state.state + ); + } + + state = { + state: "step-started", + messages: state.messages.concat({ testStepStarted: data }), + testCaseStartedId: state.testCaseStartedId, + testStepStartedId: data.testStepId, + }; return true; } -export function testCaseStartedHandler(data: ITaskTestCaseStarted) { - if (!currentSpecMessages) { +export function testStepFinishedHandler( + config: Cypress.PluginConfigOptions, + data: ITaskTestStepFinished +) { + debug("testStepFinishedHandler()"); + + if (!config.isTextTerminal) { return true; } - currentTestCaseStartedId = data.testCaseStartedId; + switch (state.state) { + case "step-started": + break; + default: + throw createError( + "Unexpected state in testStepFinishedHandler: " + state.state + ); + } + + state = { + state: "step-finished", + messages: state.messages.concat({ testStepFinished: data }), + testCaseStartedId: state.testCaseStartedId, + }; return true; } -export function testStepStartedHandler(data: ITaskTestStepStarted) { - if (!currentSpecMessages) { +export function testCaseFinishedHandler( + config: Cypress.PluginConfigOptions, + data: ITaskTestCaseFinished +) { + debug("testCaseFinishedHandler()"); + + if (!config.isTextTerminal) { return true; } - currentTestStepStartedId = data.testStepId; + switch (state.state) { + case "test-started": + case "step-finished": + break; + default: + throw createError( + "Unexpected state in testCaseFinishedHandler: " + state.state + ); + } + + state = { + state: "test-finished", + messages: state.messages.concat({ testCaseFinished: data }), + }; return true; } -export function createStringAttachmentHandler({ - data, - mediaType, - encoding, -}: ITaskCreateStringAttachment) { - if (!currentSpecMessages) { +export async function createStringAttachmentHandler( + config: Cypress.PluginConfigOptions, + { data, mediaType, encoding }: ITaskCreateStringAttachment +) { + debug("createStringAttachmentHandler()"); + + if (!config.isTextTerminal) { return true; } + const preprocessor = await resolve(config, config.env, "/"); + + if (!preprocessor.messages.enabled) { + return true; + } + + switch (state.state) { + case "step-started": + break; + default: + throw createError( + "Unexpected state in createStringAttachmentHandler: " + state.state + ); + } + const message: messages.Envelope = { attachment: { - testCaseStartedId: currentTestCaseStartedId, - testStepId: currentTestStepStartedId, + testCaseStartedId: state.testCaseStartedId, + testStepId: state.testStepStartedId, body: data, mediaType: mediaType, contentEncoding: encoding, }, }; - currentSpecMessages.push(message); + state.messages.push(message); return true; } diff --git a/lib/registry.ts b/lib/registry.ts index 246dbaad..0fc4e6d7 100644 --- a/lib/registry.ts +++ b/lib/registry.ts @@ -28,6 +28,7 @@ import { } from "./helpers/source-map"; export interface IStepDefinition { + id: string; expression: Expression; implementation: IStepDefinitionBody; position?: Position; @@ -96,6 +97,7 @@ export class Registry { .preliminaryStepDefinitions) { if (typeof description === "string") { this.stepDefinitions.push({ + id: uuid(), expression: new CucumberExpression( description, this.parameterTypeRegistry @@ -105,6 +107,7 @@ export class Registry { }); } else { this.stepDefinitions.push({ + id: uuid(), expression: new RegularExpression( description, this.parameterTypeRegistry diff --git a/package.json b/package.json index 8de6a982..00000ae9 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/parser": "^5.59.1", "ast-types": "^0.15.2", - "cypress": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "cypress": "^10.0.0 || ^11.0.0 || ^12.0.0", "eslint": "^8.39.0", "jsdom": "^21.1.1", "mocha": "^10.2.0", @@ -103,7 +103,7 @@ }, "peerDependencies": { "@cypress/browserify-preprocessor": "^3.0.1", - "cypress": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0" + "cypress": "^10.0.0 || ^11.0.0 || ^12.0.0" }, "peerDependenciesMeta": { "@cypress/browserify-preprocessor": {