Quick Take

import { strict as assert } from "assert";
import { mixer } from "test-mixer";

// check all possible combinations of all boolean opts:
const defaultOpts = {
  scrapeWindshield: true,
  checkOil: true,
  inflateTires: false,
  extinguishersCount: 1, // as non-boolean will be ignored
};

// generates 2^3 = 8 combinations all possible bools
assert.deepEqual(
  mixer(
    {
      // empty first arg object means you want all combinations
    },
    defaultOpts
  ),
  [
    {
      scrapeWindshield: false,
      checkOil: false,
      inflateTires: false,
      extinguishersCount: 1,
    },
    {
      scrapeWindshield: true,
      checkOil: false,
      inflateTires: false,
      extinguishersCount: 1,
    },
    {
      scrapeWindshield: false,
      checkOil: true,
      inflateTires: false,
      extinguishersCount: 1,
    },
    {
      scrapeWindshield: true,
      checkOil: true,
      inflateTires: false,
      extinguishersCount: 1,
    },
    {
      scrapeWindshield: false,
      checkOil: false,
      inflateTires: true,
      extinguishersCount: 1,
    },
    {
      scrapeWindshield: true,
      checkOil: false,
      inflateTires: true,
      extinguishersCount: 1,
    },
    {
      scrapeWindshield: false,
      checkOil: true,
      inflateTires: true,
      extinguishersCount: 1,
    },
    {
      scrapeWindshield: true,
      checkOil: true,
      inflateTires: true,
      extinguishersCount: 1,
    },
  ]
);

// let's "pin" a value, prepare two sets of options objects,
// one where scrapeWindshield === true and another with "false"

// you'll get 2 ^ (3-1) = 4 variations:
const variationsWithScrapeWindshieldOn = mixer(
  {
    scrapeWindshield: true,
  },
  defaultOpts
);
assert.deepEqual(variationsWithScrapeWindshieldOn, [
  {
    scrapeWindshield: true, // <--- pinned
    checkOil: false,
    inflateTires: false,
    extinguishersCount: 1,
  },
  {
    scrapeWindshield: true, // <--- pinned
    checkOil: true,
    inflateTires: false,
    extinguishersCount: 1,
  },
  {
    scrapeWindshield: true, // <--- pinned
    checkOil: false,
    inflateTires: true,
    extinguishersCount: 1,
  },
  {
    scrapeWindshield: true, // <--- pinned
    checkOil: true,
    inflateTires: true,
    extinguishersCount: 1,
  },
]);

// also 4 variations, similar but with scrapeWindshield === false pinned:
const variationsWithScrapeWindshieldOff = mixer(
  {
    scrapeWindshield: false,
  },
  defaultOpts
);
assert.equal(
  variationsWithScrapeWindshieldOff.length,
  4
);

Purpose

It's used to generate an array of all possible combinations of options object boolean settings.

detergent has 12 boolean toggles — that's 2^12 = 4096 variations to test for each input. This program generates those options variations.

string-collapse-white-space has 6 boolean toggles — that's 2^6 = 64 variations to test for each input.

And so on.

API - Input

mixer(
  objWithFixedKeys,
  [defaultsObj]
)

In other words, it's a function which takes two input arguments, second one being optional (marked by square brackets).

Input argument Type Description
objWithFixedKeys something falsy or a plain object Plain object with "pinned" values (each will reduce the generated combinations count by 1)
defaultsObj Plain object Put the default options object here.

For example,

mixer(
{
trim: true // this is pinned value
},
{
trim: false, // will be static in generated variations
encode: false // variations of "encode", 2^1=2 will be generated
}
)

The order of the arguments seems backward because typically you want to export a wrapper around the mixer(), with defaults set, so that you can save time and omit the second argument, defaults. See an example below, with import { mixer as testMixer } from "text-mixer";, then const mixer = (ref) => testMixer(ref, opts);, then prepped mixer() is one-argument function, ready for use.

API - Output

Program returns an array of zero or more plain objects.

In practice

Let's say, we test detergent. It has the following default options:

{
fixBrokenEntities: true,
removeWidows: true,
convertEntities: true,
convertDashes: true,
convertApostrophes: true,
replaceLineBreaks: true,
removeLineBreaks: false,
useXHTML: true,
dontEncodeNonLatin: true,
addMissingSpaces: true,
convertDotsToEllipsis: true,
stripHtml: true,
eol: "lf",
stripHtmlButIgnoreTags: ["b", "strong", "i", "em", "br", "sup"],
stripHtmlAddNewLine: ["li", "/ul"],
cb: null,
}

Imagine, we're testing how detergent strips HTML, setting, opts.stripHtml.

// 😱 eleven other boolean opts settings were left out, 2^12 - 1 = 4095 other combinations!
import tap from "tap";
const { det } = require("detergent");
tap.test(`01`, (t) => {
t.equal(
det(t, n, `text <a>text</a> text`, {
stripHtml: true,
}).res,
"text text text"
);
t.end();
});

Here's the proper way:

// ✅
import tap from "tap";
const { det, opts } = require("detergent");
import { mixer as testMixer } from "text-mixer";

// we create a wrapper to skip writing the second input argument again and again
const mixer = (ref) => testMixer(ref, opts);

tap.test(`01`, (t) => {
// 2^11 = 2048 variations:
mixer({
stripHtml: false, // <--- will be the same across all generated objects
}).forEach((opt, n) => {
t.equal(
det(t, n, `text <a>text</a> text`, opt).res,
`text <a>text</a> text`,
JSON.stringify(opt, null, 4)
);
});
// another 2^11 = 2048 variations:
mixer({
stripHtml: true,
}).forEach((opt, n) => {
t.equal(
det(t, n, `text <a>text</a> text`, opt).res,
`text text text`,
JSON.stringify(opt, null, 4)
);
});
t.end();
});

Changelog

See it in the monorepo opens in a new tab, on GitHub.

Contributing

To report bugs or request features or assistance, raise an issue on GitHub opens in a new tab.

Any code contributions welcome! All Pull Requests will be dealt promptly.

Licence

MIT opens in a new tab

Copyright © 2010–2021 Roy Revelt and other contributors

Related packages:

📦 object-boolean-combinations 4.1.0
Consumes a defaults object with booleans, generates all possible variations of it