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

Issue:
Some options passed to preprocessor cause a TypeError
Taking for example the TypeScript example from Cypress docs:

```js
// cypress/plugins/index.js
const browserify = require('@cypress/browserify-preprocessor')

module.exports = on => {
  const options = {
    browserifyOptions: {
      extensions: ['.js', '.ts'],
      plugin: [
        ['tsify']
      ]
    }
  }

  on('file:preprocessor', browserify(options))
}
```

If we utilize this example when setting up cypress-cucumber-preprocessor:

```js
// cypress/plugins/index.js
const cucumber = require("cypress-cucumber-preprocessor").default

module.exports = on => {
  const options = {
    browserifyOptions: {
      extensions: ['.js', '.ts'],
      plugin: [
        ['tsify']
      ]
    }
  }

  on('file:preprocessor', cucumber(options))
}
```

Will result in `TypeError: Cannot read property 'indexOf' of undefined`, since the preprocessor code expects an array at `options.browserifyOptions.transform`.

Pull request description:
Address cases where options passed to preprocessor would result in a TypeError
Fixes #345

Patch:
diff --git a/cypress-tags.js b/cypress-tags.js
index 60445f0..dab6a59 100755
--- a/cypress-tags.js
+++ b/cypress-tags.js
@@ -38,6 +38,11 @@ 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(
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);
+  });
+});