Repository: TheBrainFamily/cypress-cucumber-preprocessor (js)

Issue:
Use cypress-tags as main binary
Hi!

I want to use `cypress-tags` as main binary of `cypress` because it nicely wraps the execution. I do the following in `package.json`:

```json
{
    "scripts": {
        "cypress": "cypress-tags"
    }
}
```

With that step above I can always run `yarn cypress` and it should respect `TAGS` environment variable, too. But, if I do `yarn cypress run` I get the following error:

```
const envTags = found.replace(/.*=/, "");
                      ^

TypeError: Cannot read property 'replace' of undefined
    at Object.<anonymous> (/d/Programme/VSCode Workspace/wp-reactjs-starter/node_modules/cypress-cucumber-preprocessor/cypress-tags.js:34:23)
```

The implementation does not allow optional `TAGS`:

https://github.com/TheBrainFamily/cypress-cucumber-preprocessor/blob/152ccdd8952abbf0bcc33c108d12305f83e9fd99/cypress-tags.js#L32

Would it be possible to make it optional, too, like `GLOB`?

https://github.com/TheBrainFamily/cypress-cucumber-preprocessor/blob/152ccdd8952abbf0bcc33c108d12305f83e9fd99/cypress-tags.js#L20-L26

Regards,
Matthew 🙂

Pull request description:
feat(tags): make the TAGS env. variable optional
Fixes #301

Patch:
diff --git a/README.md b/README.md
index 7ac0c15..e8e538f 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ You can follow the documentation below, or if you prefer to hack on a working ex
   * [Smart tagging](#smart-tagging)
 * [How to run the tests](#excluding-tests)
   * [Running tagged tests](#running-tagged-tests)
+  * [Ignoring specific scenarios using tags when executing test runner](#ignoring-specific-scenarios-using-tags-when-executing-test-runner)
   * [Output](#output)
 * [IDE support](#ide-support)
   * [Webstorm](#webstorm)
@@ -329,6 +330,25 @@ Example:
 Please note - we use our own cypress-tags wrapper to speed things up.
 For more details and examples please take a look to the [example repo](https://github.com/TheBrainFamily/cypress-cucumber-example).
 
+### Ignoring specific scenarios using tags when executing test runner
+You can also use tags to skip or ignore specific tests/scenarios when running cypress test runner (where you don't have the abilitiy to pass parameters like in the examples above for the execution)
+
+The trick consists in adding the "env" property with the "TAGS" subproperty in the cypress.json configuration file. It would look something like this:
+
+```javascript
+{
+    "env": {
+        "TAGS": "not @ignore"
+    },
+    //rest of configuration options
+    "baseUrl": "yourBaseUrl",       
+    "ignoreTestFiles": "*.js",
+    //etc
+}
+```
+
+Then, any scenarios tagged with @ignore will be skipped when running the tests using the cypress test runner
+
 ### Limiting to a subset of feature files
 You can use a glob expression to select which feature files should be included.
 
diff --git a/cypress-tags.js b/cypress-tags.js
index 60445f0..b882171 100755
--- a/cypress-tags.js
+++ b/cypress-tags.js
@@ -31,13 +31,18 @@ const featuresToRun = [];
 
 const found = process.argv.slice(2).find(arg => arg.indexOf("TAGS=") === 0);
 
-const envTags = found.replace(/.*=/, "");
+const envTags = found ? found.replace(/.*=/, "") : "";
 debug("Found tag expression", envTags);
 
 paths.forEach(featurePath => {
   const spec = `${fs.readFileSync(featurePath)}`;
   const parsedFeature = new Parser().parse(spec);
 
+  if (!parsedFeature.feature) {
+    debug(`Feature: ${featurePath} is empty`);
+    return;
+  }
+
   const featureTags = parsedFeature.feature.tags;
   const featureShouldRun = shouldProceedCurrentStep(featureTags, envTags);
   const taggedScenarioShouldRun = parsedFeature.feature.children.some(
@@ -58,7 +63,7 @@ try {
   if (featuresToRun.length || envTags === "") {
     execFileSync(
       process.platform === "win32"
-        ? `${__dirname}/../.bin/cypress.cmd`
+        ? `cypress.cmd`
         : `${__dirname}/../.bin/cypress`,
       [...process.argv.slice(2), "--spec", featuresToRun.join(",")],
       {
diff --git a/lib/index.js b/lib/index.js
index 76ffb4e..0e55ada 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -37,24 +37,51 @@ const touch = filename => {
 
 let watcher;
 
-const preprocessor = (options = browserify.defaultOptions) => async file => {
-  if (options.browserifyOptions.transform.indexOf(transform) === -1) {
-    options.browserifyOptions.transform.unshift(transform);
+// Include our transform in Browserify options
+const wrapOptions = options => {
+  let wrappedTransform;
+  if (
+    !options.browserifyOptions ||
+    !Array.isArray(options.browserifyOptions.transform)
+  ) {
+    wrappedTransform = [transform];
+  } else if (!options.browserifyOptions.transform.includes(transform)) {
+    wrappedTransform = [transform, ...options.browserifyOptions.transform];
+  } else {
+    wrappedTransform = options.browserifyOptions.transform;
   }
 
-  if (file.shouldWatch) {
-    if (watcher) {
-      watcher.close();
+  return {
+    ...options,
+    browserifyOptions: {
+      ...(options.browserifyOptions || {}),
+      transform: wrappedTransform
     }
-    watcher = chokidar
-      .watch([`${stepDefinitionPath()}*.js`, `${stepDefinitionPath()}*.ts`], {
-        ignoreInitial: true
-      })
-      .on("all", () => {
-        touch(file.filePath);
-      });
+  };
+};
+
+const preprocessor = (options = browserify.defaultOptions) => {
+  if (typeof options !== "object") {
+    throw new Error("Preprocessor options must be an object");
   }
-  return browserify(options)(file);
+
+  const wrappedOptions = wrapOptions(options);
+
+  return async file => {
+    if (file.shouldWatch) {
+      if (watcher) {
+        watcher.close();
+      }
+      watcher = chokidar
+        .watch([`${stepDefinitionPath()}*.js`, `${stepDefinitionPath()}*.ts`], {
+          ignoreInitial: true
+        })
+        .on("all", () => {
+          touch(file.filePath);
+        });
+    }
+    return browserify(wrappedOptions)(file);
+  };
 };
 
 module.exports = {
diff --git a/package-lock.json b/package-lock.json
index 24892de..b05e9eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13524,9 +13524,9 @@
       }
     },
     "minimist": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
-      "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.2.tgz",
+      "integrity": "sha512-rIqbOrKb8GJmx/5bc2M0QchhUouMXSpd1RTclXsB41JdL+VtnojfaJR+h7F9k18/4kHUsBFgk80Uk+q569vjPA=="
     },
     "minimist-options": {
       "version": "3.0.2",


Test patch:
diff --git a/lib/index.test.js b/lib/index.test.js
new file mode 100644
index 0000000..e5d8013
--- /dev/null
+++ b/lib/index.test.js
@@ -0,0 +1,98 @@
+const browserify = require("@cypress/browserify-preprocessor");
+const { default: preprocessor, transform } = require(".");
+
+jest.mock("@cypress/browserify-preprocessor");
+const { defaultOptions } = jest.requireActual(
+  "@cypress/browserify-preprocessor"
+);
+
+describe("Preprocessor", () => {
+  beforeEach(() => {
+    browserify.defaultOptions = defaultOptions;
+    browserify.mockReturnValue(() => {});
+  });
+
+  afterEach(() => {
+    browserify.mockClear();
+  });
+
+  it("should use Cypress browserify options by default", async () => {
+    await preprocessor()({ shouldWatch: false });
+
+    global.jestExpect(browserify).toHaveBeenCalledWith({
+      ...defaultOptions,
+      browserifyOptions: {
+        ...defaultOptions.browserifyOptions,
+        transform: [transform, ...defaultOptions.browserifyOptions.transform]
+      }
+    });
+  });
+
+  it("should add transform when other transforms are defined in options", async () => {
+    await preprocessor({
+      browserifyOptions: {
+        transform: ["babelify"]
+      }
+    })({ shouldWatch: false });
+
+    global.jestExpect(browserify).toHaveBeenCalledWith({
+      browserifyOptions: {
+        transform: [transform, "babelify"]
+      }
+    });
+  });
+
+  it("should preserve transforms in options when our transform is already included", async () => {
+    const options = {
+      browserifyOptions: {
+        extensions: ["js", "ts"],
+        plugins: [["tsify"]],
+        transform: ["aliasify", transform, "babelify"]
+      }
+    };
+
+    await preprocessor(options)({ shouldWatch: false });
+
+    global.jestExpect(browserify).toHaveBeenCalledWith(options);
+  });
+
+  it("should add our transform when no other transforms are defined in options", async () => {
+    const options = {
+      browserifyOptions: {
+        extensions: ["js", "ts"],
+        plugins: [["tsify"]]
+      }
+    };
+
+    await preprocessor(options)({ shouldWatch: false });
+
+    global.jestExpect(browserify).toHaveBeenCalledWith({
+      ...options,
+      browserifyOptions: {
+        ...options.browserifyOptions,
+        transform: [transform]
+      }
+    });
+  });
+
+  it("should add our transform when browserifyOptions property is not passed to options", async () => {
+    const options = { unsupported: true };
+
+    await preprocessor(options)({ shouldWatch: false });
+
+    global.jestExpect(browserify).toHaveBeenCalledWith({
+      ...options,
+      browserifyOptions: {
+        transform: [transform]
+      }
+    });
+  });
+
+  it("should fail gracefully when options is not an object", () => {
+    const err = new Error("Preprocessor options must be an object");
+
+    global.jestExpect(() => preprocessor("options")).toThrow(err);
+    global.jestExpect(() => preprocessor(1)).toThrow(err);
+    global.jestExpect(() => preprocessor(false)).toThrow(err);
+  });
+});