{"id":15285378,"url":"https://github.com/t2ym/scenarist","last_synced_at":"2026-01-18T23:02:44.016Z","repository":{"id":58235194,"uuid":"77215127","full_name":"t2ym/scenarist","owner":"t2ym","description":"Class-based branching test scenario runner for mocha","archived":false,"fork":false,"pushed_at":"2019-01-15T05:23:23.000Z","size":4176,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-11-27T10:25:39.187Z","etag":null,"topics":["es6","mocha","test-runner","test-scenarios"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/t2ym.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-12-23T09:36:07.000Z","updated_at":"2019-06-26T17:49:29.000Z","dependencies_parsed_at":"2022-08-31T09:21:56.702Z","dependency_job_id":null,"html_url":"https://github.com/t2ym/scenarist","commit_stats":null,"previous_names":[],"tags_count":44,"template":false,"template_full_name":null,"purl":"pkg:github/t2ym/scenarist","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t2ym%2Fscenarist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t2ym%2Fscenarist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t2ym%2Fscenarist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t2ym%2Fscenarist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/t2ym","download_url":"https://codeload.github.com/t2ym/scenarist/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/t2ym%2Fscenarist/sbom","scorecard":{"id":864747,"data":{"date":"2025-08-11","repo":{"name":"github.com/t2ym/scenarist","commit":"08472af46d92745a04ff8504083049574f054ae2"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":1.7,"checks":[{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":9,"reason":"license file detected","details":["Info: project has a license file: LICENSE.md:0","Warn: project license file does not contain an FSF or OSI license."],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"121 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-6chw-6frg-f759","Warn: Project is vulnerable to: GHSA-v88g-cgmw-v5xw","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-fwr7-v2mv-hh25","Warn: Project is vulnerable to: GHSA-pp7h-53gx-mx7r","Warn: Project is vulnerable to: GHSA-qwcr-r2fm-qrc7","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-cwfw-4gq5-mrqx","Warn: Project is vulnerable to: GHSA-g95f-p29q-9xw4","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-257v-vj4p-3w2h","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-9vvw-cc9w-f27h","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq","Warn: Project is vulnerable to: GHSA-wm7h-9275-46v2","Warn: Project is vulnerable to: GHSA-h6ch-v84p-w6p9","Warn: Project is vulnerable to: GHSA-ff7x-qrg7-qggm","Warn: Project is vulnerable to: GHSA-j4f2-536g-r55m","Warn: Project is vulnerable to: GHSA-r7qp-cfhv-p84w","Warn: Project is vulnerable to: GHSA-rv95-896h-c2vc","Warn: Project is vulnerable to: GHSA-qw6h-vgh9-j6wx","Warn: Project is vulnerable to: GHSA-74fj-2j2h-c42q","Warn: Project is vulnerable to: GHSA-pw2r-vq6v-hr8c","Warn: Project is vulnerable to: GHSA-jchw-25xp-jwwc","Warn: Project is vulnerable to: GHSA-cxjh-pqwp-8mfp","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-pfrx-2q88-qq97","Warn: Project is vulnerable to: GHSA-qh2h-chj9-jffq","Warn: Project is vulnerable to: GHSA-q42p-pg8m-cqh6","Warn: Project is vulnerable to: GHSA-w457-6q6x-cgp9","Warn: Project is vulnerable to: GHSA-62gr-4qp9-h98f","Warn: Project is vulnerable to: GHSA-f52g-6jhx-586p","Warn: Project is vulnerable to: GHSA-2cf5-4w76-r9qv","Warn: Project is vulnerable to: GHSA-3cqr-58rm-57f8","Warn: Project is vulnerable to: GHSA-g9r4-xpmj-mj65","Warn: Project is vulnerable to: GHSA-q2c6-c6pm-g3gh","Warn: Project is vulnerable to: GHSA-765h-qjxv-5f44","Warn: Project is vulnerable to: GHSA-f2jv-r9rf-7988","Warn: Project is vulnerable to: GHSA-44pw-h2cw-w3vq","Warn: Project is vulnerable to: GHSA-jp4x-w63m-7wgm","Warn: Project is vulnerable to: GHSA-c429-5p7v-vgjp","Warn: Project is vulnerable to: GHSA-43f8-2h32-f4cj","Warn: Project is vulnerable to: GHSA-pfq8-rq6v-vf5m","Warn: Project is vulnerable to: GHSA-6x33-pw7p-hmpq","Warn: Project is vulnerable to: GHSA-c7qv-q95q-8v27","Warn: Project is vulnerable to: GHSA-pc5p-h8pf-mvwp","Warn: Project is vulnerable to: GHSA-qqgx-2p2h-9c37","Warn: Project is vulnerable to: GHSA-2pr6-76vf-7546","Warn: Project is vulnerable to: GHSA-8j8c-7jfh-h6hx","Warn: Project is vulnerable to: GHSA-896r-f27r-55mw","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-282f-qqgm-c34q","Warn: Project is vulnerable to: GHSA-6c8f-qphg-qjgp","Warn: Project is vulnerable to: GHSA-7h8x-wmq2-7mff","Warn: Project is vulnerable to: GHSA-jf85-cpcp-j695","Warn: Project is vulnerable to: GHSA-fvqr-27wr-82fm","Warn: Project is vulnerable to: GHSA-4xc9-xhrj-v574","Warn: Project is vulnerable to: GHSA-x5rq-j2xg-h7qm","Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-p6mc-m468-83gw","Warn: Project is vulnerable to: GHSA-4xcv-9jjx-gfj3","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-hxm2-r34f-qmc5","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-vh95-rmgr-6w4m","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-fhjf-83wg-r2j9","Warn: Project is vulnerable to: GHSA-pc58-wgmc-hfjr","Warn: Project is vulnerable to: GHSA-vvv8-xw5f-3f88","Warn: Project is vulnerable to: GHSA-44fp-w29j-9vj5","Warn: Project is vulnerable to: GHSA-76c9-3jph-rj3q","Warn: Project is vulnerable to: GHSA-6fx8-h7jm-663j","Warn: Project is vulnerable to: GHSA-hj48-42vr-x3v9","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-rhx6-c78j-4q9w","Warn: Project is vulnerable to: GHSA-4cpg-3vgw-4877","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-x3m3-4wpv-5vgc","Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-m6fv-jmcg-4jfg","Warn: Project is vulnerable to: GHSA-cm22-4g7w-348p","Warn: Project is vulnerable to: GHSA-4g88-fppr-53pp","Warn: Project is vulnerable to: GHSA-4jqc-8m5r-9rpr","Warn: Project is vulnerable to: GHSA-fxwf-4rqh-v8g3","Warn: Project is vulnerable to: GHSA-25hc-qcg6-38wj","Warn: Project is vulnerable to: GHSA-xfhh-g9f5-x4m4","Warn: Project is vulnerable to: GHSA-qm95-pgcg-qqfq","Warn: Project is vulnerable to: GHSA-cqmj-92xf-r6r9","Warn: Project is vulnerable to: GHSA-29xr-v42j-r956","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-7p7h-4mm5-852v","Warn: Project is vulnerable to: GHSA-xc7v-wxcw-j472","Warn: Project is vulnerable to: GHSA-662x-fhqg-9p8v","Warn: Project is vulnerable to: GHSA-394c-5j6w-4xmx","Warn: Project is vulnerable to: GHSA-78cj-fxph-m83p","Warn: Project is vulnerable to: GHSA-fhg7-m89q-25r3","Warn: Project is vulnerable to: GHSA-cf4h-3jhx-xvhq","Warn: Project is vulnerable to: GHSA-3329-pjwv-fjpg","Warn: Project is vulnerable to: GHSA-p6j9-7xhc-rhwp","Warn: Project is vulnerable to: GHSA-89gv-h8wf-cg8r","Warn: Project is vulnerable to: GHSA-gcv8-gh4r-25x6","Warn: Project is vulnerable to: GHSA-gmv4-r438-p67f","Warn: Project is vulnerable to: GHSA-8h2f-7jc4-7m3m","Warn: Project is vulnerable to: GHSA-3vjf-82ff-p4r3","Warn: Project is vulnerable to: GHSA-g694-m8vq-gv9h","Warn: Project is vulnerable to: GHSA-6fc8-4gx4-v693","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q","Warn: Project is vulnerable to: GHSA-h6q6-9hqw-rwfv","Warn: Project is vulnerable to: GHSA-5fg8-2547-mr8q","Warn: Project is vulnerable to: GHSA-crh6-fp67-6883","Warn: Project is vulnerable to: GHSA-72mh-269x-7mh5","Warn: Project is vulnerable to: GHSA-h4j5-c7cj-74xg","Warn: Project is vulnerable to: GHSA-c4w7-xm78-47vh","Warn: Project is vulnerable to: GHSA-p9pc-299p-vxgp"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-24T02:22:22.188Z","repository_id":58235194,"created_at":"2025-08-24T02:22:22.188Z","updated_at":"2025-08-24T02:22:22.188Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28553055,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T20:59:07.572Z","status":"ssl_error","status_checked_at":"2026-01-18T20:59:02.799Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["es6","mocha","test-runner","test-scenarios"],"created_at":"2024-09-30T15:04:28.096Z","updated_at":"2026-01-18T23:02:43.994Z","avatar_url":"https://github.com/t2ym.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.org/t2ym/scenarist.svg?branch=master)](https://travis-ci.org/t2ym/scenarist)\n[![Coverage Status](https://coveralls.io/repos/github/t2ym/scenarist/badge.svg?branch=master)](https://coveralls.io/github/t2ym/scenarist?branch=master)\n[![npm](https://img.shields.io/npm/v/scenarist.svg)](https://www.npmjs.com/package/scenarist)\n[![Bower](https://img.shields.io/bower/v/scenarist.svg)](https://customelements.io/t2ym/scenarist/)\n\n# scenarist\n\nClass-based branching test scenario runner for mocha\n\n[Demo](https://t2ym.github.io/scenarist/components/scenarist/demo/)\n\n\u003cimg src=\"https://raw.githubusercontent.com/wiki/t2ym/scenarist/scenaristdemo.gif\" width=\"768px\"\u003e\n\n```javascript\n// common-test.js global test classes\nclass ExampleSuite extends Suite {\n  async setup() {\n    await super.setup();\n    this.container = document.querySelector(this.target);\n  }\n  async teardown() {\n    let self = this;\n    await super.teardown();\n    await self.forEvent(self.container, 'dom-change', () =\u003e { self.container.if = false; }, true);\n  }\n}\n```\n\n```javascript\n// example-test.js\n{\n  // example scope\n  let scope = 'example';\n  let example = new Suite(scope, 'Description of Example Suite');\n  // test class mixin in \"example\" scope\n  example.test = (base) =\u003e class TestA extends base {\n    get description() { return 'Description of Test A'; }\n    async operation() {\n      console.log('Test A operation');\n      this.element = document.querySelector('#example')\n    }\n    async checkpoint() {\n      console.log('Checkpoint for Test A');\n      assert.equal(this.element.is, 'example-element', 'Element is instantiated');\n      //assert.isOk(false, 'Failing test A');\n    }\n  }\n  ...\n  // test class in \"example\" scope\n  example.test = class TestE extends ExampleSuite {\n    static get skipAfterFailure() { return true; }\n    async operation() {}\n    async checkpoint() {}\n  }\n  ...\n  // scenarios\n  example.test = {\n    // test class mixins\n    '': [\n      {\n        TestA: {\n          TestB: 'TestAThenB'\n        },\n        TestB: {\n          TestA: 'TestBThenA'\n        },\n      },\n      Suite.repeat('TestAThenB', 3, 'TestAB3')\n    ],\n    // test classes\n    TestC: {\n      TestAThenB: 'TestCAB'\n    },\n    TestD: 'TestDAlias',\n    TestE: [\n      {\n        TestAThenB: 'TestEAB',\n        TestA: {\n          TestB: {\n            Test1: {\n              Test2: 'TestEAB12'\n            }\n          },\n          TestBThenA: 'TestEABA'\n        },\n        TestB: {\n          Test1: ''\n        },\n        TestAB3: 'TestEAB3; Description of \"Test EAB3\"'\n      },\n      Suite.permute([ 'TestA', 'TestB', 'Test1' ], (scenario) =\u003e ({\n        Test2: 'Test_E_' + scenario.map(n =\u003e n.replace(/^Test/,'')).join('_') + '_2'\n      }))\n    ]\n  };\n\n  let match = decodeURIComponent(window.location.href).match(/^.*[^_a-zA-Z0-9]TestSuites=([_a-zA-Z0-9,]*).*$/);\n\n  if (match) {\n    // Runner\n    // match[1] = '0' for the first round of test suites runnable without reloading\n    example.run(match[1], '#example');\n  }\n} // example scope\n```\n\n```javascript\n// In Driver page\n// example for web-component-tester\nvar suites = [];\nfor (var scope in Suite.scopes) {\n  Suite.scopes[scope].test.forEach(function (tests, index) {\n    suites.push(scope + '-test.html?TestSuites=' + index);\n  });\n}\nWCT.loadSuites(suites);\n```\n\n### Design Principles\n\n- Contexts must be explicitly handled in a concise and intuitive way in JavaScript classes\n- `suite()` and `test()` in mocha are wrapped for contexts\n\n### Alternative viewpoints for test scenarios with long and branching operations\n\n- Test target systems are a collection of state machines\n- Operations for test suites are a series of the branches of states\n- Test assertions are targeted \"checkpoints\" for the expected states\n\n### Depicted test scenarios\n\n```\nInitial -(setup op)-\u003e First checkpoint -(op)-\u003e 2nd CP -\u003e ... -\u003e Final CP for scenario A\n                                               +--\u003e ... -\u003e Final CP for scenario B \n                                                ...\n```\n\n- Initial state is without instances of the target system\n- Operations from the initial state set up test fixtures\n- The first checkpoint on the first operation asserts the target instances\n- Setting up the fixtures may take more steps (operations)\n- Successive checkpoints constitute a series of pairs of a (mock) operation and its corresponding test assertion(s)\n- Different test suites can share parts of operations and then branch in the latter parts\n\n### Conceptual mappings for JavaScript classes\n\n- Operation: a (mock) operation\n- Checkpoint: a collection of test assertions for a checkpoint\n- Scenario: a series of Operation and Checkpoint pairs (by a prototype chain of classes)\n- Suite: the base class of test scenarios\n- Driver: driver of test suites\n- Parameters: test parameters handed to constructors of test classes\n\n### Comparison with BDD framework\n\n- \"Branching\" of test contexts: Shared steps and test assertions for scenarios\n\n## Install\n\n### Browsers\n\n```sh\n  bower install --save-dev scenarist\n```\n\n### NodeJS\n\n```sh\n  npm install --save-dev scenarist\n```\n\n## Import\n\n### Browsers\n\n#### Raw ES module class version\n\n```js\n  import Suite from 'scenarist/Suite.esm.js';\n```\n\n#### UMD ES6 class version\n\n```html\n  \u003cscript src=\"path/to/bower_components/scenarist/Suite.js\"\u003e\u003c/script\u003e\n```\n\n#### ES5 version\n\n##### Note: `babel-polyfill/browser.js` is required for the ES5 version to work \n\n```html\n  \u003cscript src=\"path/to/node_modules/babel-polyfill/browser.js\"\u003e\u003c/script\u003e\n  \u003cscript src=\"path/to/bower_components/scenarist/Suite.min.js\"\u003e\u003c/script\u003e\n```\n\n### NodeJS\n\n#### Node 8.x or later\n\n##### Command Line\n```sh\nmocha test.js\n```\n\n##### Test Script\n```javascript\n//require('babel-polyfill');\nconst chai = require('chai');\nconst assert = chai.assert;\nconst Suite = require('scenarist/Suite.js');\n// test classes...\n```\n\n#### Node 4.x - 6.x with Babel es2015 Transpilation\n\n##### Command Line\n```sh\nmocha test.js\n```\n\n##### Test Script\n```javascript\nrequire('babel-polyfill');\nconst chai = require('chai');\nconst assert = chai.assert;\nconst Suite = require('scenarist/Suite.min.js');\n// test classes...\n```\n\n#### Node with Multiple Scopes\n\n##### Command Line\n```sh\nmocha test.js\n```\n\n##### Driver Test Script (test.js)\n```javascript\nrequire('babel-polyfill');\nconst chai = require('chai');\nglobal.assert = chai.assert;\nglobal.Suite = require('scenarist/Suite.min.js');\n\nrequire('./scope1-test.js');\nrequire('./scope2-test.js');\n\nfor (var scope in Suite.scopes) {\n  Suite.scopes[scope].test.forEach(function (tests, index) {\n    Suite.scopes[scope].run(index); // if run() is not called in scope*-test.js\n  });\n}\n```\n\n##### Test Script 1 (scope1-test.js)\n```javascript\nlet scope1 = new Suite('scope1', 'Scope 1 Suites');\n// test classes\nscope1.test = ...\n```\n\n##### Test Script 2 (scope2-test.js)\n```javascript\nlet scope2 = new Suite('scope2', 'Scope 2 Suites');\n// test classes\nscope2.test = ...\n```\n\n## Compatibility\n\n| Version      | Chrome | Firefox | IE/Edge | Safari | Opera | Node        | ECMAScript           |\n|:-------------|:------:|:-------:|:-------:|:------:|:-----:|:-----------:|:--------------------:|\n| Suite.esm.js | 61+    | 60      | 17+     | 10.1+  | 47+   | 10+ w/ esm  | ES6 + ES modules     |\n| Suite.js     | 55+    | 52+     | 17+     | 10.1+  | 42+   | 7+ w/ async | ES6 + async/await    |\n| Suite.min.js | 55+    | 50+     | 11+     | 10+    | 42+   | 4+ w/ babel | ES5 + babel-polyfill |\n\n- Suite.mjs and Suite.esm.js have the same contents but have different file name extensions.\n\n## API\n\n### `Suite` class\n\n- Index\n  - [Scope Object as a `Suite` class instance](#scope-object-as-a-suite-class-instance)\n  - [Scope Object Instance Properties other than `test` property setter](#scope-object-instance-properties-other-than-test-property-setter)\n  - [Test Suite Subclass as a direct or descendent subclass of `Suite` class](#test-suite-subclass-as-a-direct-or-descendent-subclass-of-suite-class)\n  - [Test Runner Subclass as a subclass of `Suite` class](#test-runner-subclass-as-a-subclass-of-suite-class)\n  - [Utility Instance Methods](#utility-instance-methods)\n  - [Utility Static Methods](#utility-static-methods)\n  - [Static Properties for `Suite` class](#static-properties-for-suite-class)\n  - [Static Properties for subclasses](#static-properties-for-subclasses)\n\n#### Scope Object as a `Suite` class instance\n\n- **constructor(scope: string, description: string = scope + ' suite')** - Create a scope typically in a block scope; The scope object is globally accessible via `Suite.scopes[scope]`\n\n```javascript\n    { // \"scope1\" block scope\n      let scope1 = new Suite('scope1', 'Description of scope1 Suite');\n      scope1 === Suite.scopes.scope1; // is true\n    }\n```\n\n- **test** property setter with a class expression - Define a test runner subclass or a test suite subclass in the scope\n```javascript\n    let scope = 'example';\n    let example = new Suite(scope, 'Description of Example Suite');\n    // Defined test suite class is accessible via example.classes.ExampleSuite\n    example.test = class ExampleSuite extends Suite {\n      async setup() { await super.setup(); ... }\n      async teardown() { await super.teardown(); ... }\n    }\n    // Defined test runner class is accessible via example.classes.ExampleTest1\n    example.test = class ExampleTest1 extends example.classes.ExampleSuite {\n      async operation() { ... }\n      async checkpoint() { ... }\n    }\n```\n- **test** property setter with a class expression mixin - Define a test runner class mixin in the scope\n```javascript\n    // Defined test runner class mixin is accessible via example.mixins.ExampleTestMixin1\n    example.test = (base) =\u003e class ExampleTestMixin1 extends base {\n      async operation() { ... }\n      async checkpoint() { ... }\n    }\n```\n- **test** property setter with a test scenario object - Define test scenarios with a scenario object in the scope\n  - Arrays in scenarios iterate over their items\n  - Description can be optionally specified after ';' of the test class name\n  - Default description string is generated by uncamelcasing the test class name, e.g., 'TestAAndB' -\u003e 'test a and b'\n```javascript\n    example.test = {\n      // test class mixins\n      '': {\n        ExampleTestMixin1: {\n          ExampleTestMixin2: 'Test1Then2' // Define Test1Then2 test class mixin\n        }\n      },\n      // test classes\n      ExampleTest1: {\n        Test1Then2: 'Test1ThenTest1Then2' // Define Test1ThenTest1Then2 test class\n      }\n    }\n```\n```javascript\n    example.test = {\n      // test class mixins\n      '': [\n        Suite.repeat('ExampleTestMixin1', 3, 'RepeatTest1_3Times'),\n        Suite.repeat('ExampleTestMixin2', 4, 'RepeatTest2_4Times')\n      ],\n      // test classes\n      ExampleTest1: {\n        Test1Then2: 'Test1ThenTest1Then2; Description of the Test'\n      }\n    }\n```\n- **test** property setter with a class from another scope - Define a subscope of a scope\n\n```\n    // Meta-depiction of Subscoping as Prototype chaining\n    Suite \u003c-- GlobalScope \u003c-- ScopeA \u003c-- SubscopeAB ...\n                                      +- SubscopeAC ...\n```\n\n```html\n    \u003c!-- subscope_ac-test.html --\u003e\n    \u003cscript src=\"scenarist/Suite.js\"\u003e\u003c/script\u003e\n    \u003cscript src=\"global_scope.js\"\u003e\u003c/script\u003e\n    \u003cscript src=\"scope_a.js\"\u003e\u003c/script\u003e\n    \u003cscript src=\"subscope_ac.js\"\u003e\u003c/script\u003e\n    \u003cscript\u003e\n      var match = decodeURIComponent(window.location.href).match(/^.*[^_a-zA-Z0-9]TestSuites=([_a-zA-Z0-9,]*).*$/);\n\n      if (match) {\n        // Runner\n        Suite.scopes.subscope_ac.run(match[1], 'template#container');\n      }\n    \u003c/script\u003e\n```\n\n```javascript\n    // global_scope.js\n    class CommonSuite extends Suite { ... }\n    class Instantiate extends CommonSuite { ... }\n```\n\n```javascript\n    // scope_a.js\n    {\n      let scope_a = new Suite('scope_a');\n      scope_a.test = class ScopeATest1 extends Instantiate { ... }\n      scope_a.test = (base) =\u003e class ScopeATest2 extends base { ... }\n      scope_a.test = {\n        ScopeATest1: {\n          ScopeATest2: 'ExportedScopeATest' // The exported test name is not necessarily explicit\n        }\n      }\n    }\n    // no call of run() in scope_a.js since it will be called in scope_a-test.html\n```\n\n```javascript\n    // subscope_ac.js\n    {\n      let subscope_ac = new Suite('subscope_ac');\n      // Define a subscope 'subscope_ac' of the scope 'scope_a' via 'ExportedScopeATest'\n      subscope_ac.test = Suite.scopes.scope_a.classes.ExportedScopeATest;\n      // Tests for subscope_ac\n      subscope_ac.test = (base) =\u003e class SubscopeACTest1 extends base { ... }\n      subscope_ac.test = (base) =\u003e class SubscopeACTest2 extends base { ... }\n      subscope_ac.test = {\n        ExportedScopeATest: {\n          // Extend scope_a test\n          SubscopeACTest1: 'Instantiate_ScopeATest1_ScopeATest2_SubscopeACTest1_Test',\n          SubscopeACTest2: 'Instantiate_ScopeATest1_ScopeATest2_SubscopeACTest2_Test',\n          ...\n        }\n      }\n    }\n    // no call of run() in subscope_ac.js since it will be called in subscope_ac-test.html\n```\n\n- **testClasses(tests)** instance method - Get Array of test classes\n  - tests as number - List classes in CSV `this.test[tests]`\n  - tests as CSV string - List classes in the CSV\n- **async run(classes, target: string)** instance method - Run the specified test classes in the scope; `target` is handed to constructors of target classes\n  - classes as number - Target classes are `testClasses(test[classes])`\n  - classes as CSV string - Target classes are `testClasses(classes)`\n  - classes as Array of string - Target classes are `classes.map((item) =\u003e self.classes[item])`\n  - classes as Array of classes - Target classes are `classes`\n  - classes as object with class properties - Target classes are properties of `classes`\n  - it can throw MultiError exception, which has Array property `.errors` as `[ [ 'TestClassName', TestClassInstance, Exception ], ... ]`\n```javascript\n    async function runner() {\n      for (let scope in Suite.scopes) {\n        for (let index in Suite.scopes[scope].test) {\n          try {\n            await Suite.scopes[scope].run(index, '#target');\n          }\n          catch (errors) {\n            errors.message === 'Error: Suite.error5.run(Test1,Test2,...): exception(s) thrown. See .errors for details';\n            errors.errors.forEach((item) =\u003e {\n              item[0] === 'TestClassName';\n              item[1] === TestClassInstance;\n              item[2] === ErrorOrUndefined; // undefined for successful tests\n            });\n          }\n        }\n      }\n    }()\n```\n```javascript\n    for (var scope in Suite.scopes) {\n      Suite.scopes[scope].test.forEach(function (tests, index) {\n        Suite.scopes[scope].run(index, '#target')\n          .catch(function(errors) {\n            errors.message === 'Error: Suite.error5.run(Test1,Test2,...): exception(s) thrown. See .errors for details';\n            errors.errors.forEach((item) =\u003e {\n              item[0] === 'TestClassName';\n              item[1] === TestClassInstance;\n              item[2] === ErrorOrUndefined; // undefined for successful tests\n            });\n          });\n      });\n    }\n```\n\n#### Scope Object Instance Properties other than `test` property setter\n\n- **scope** string property - Scope name set by `new Suite(scope)`\n- **description** string property - Default: scope + ' suite'; Scope description set by `new Suite(scope, description)`\n- **classes** object property - Object containing the currently defined test classes\n- **mixins** object property - Object containing the currently defined test class mixins\n- **classSyntaxSupport** boolean property - `true` if ES6 class syntax is natively supported\n- **arrowFunctionSupport** boolean property - `true` if ES6 arrow function syntax is natively supported\n- **leafClasses** object property - Object containing the current leaf (non-redundant) test classes\n- **leafScenarios** object property - Object with CSV strings of leaf test scenarios as its properties\n- **branchScenarios** object property - Object with CSV strings of branch test scenarios as its properties\n- **test** Array property - Array of CSV strings, each of which constitutes a group of reconnectable tests\n```javascript\n    Suite.scopes.example.test returns\n    [\n      'ReconnectableTest1,ReconnectableTest2',\n      'NonReconnectableTest1',\n      'ReconnectableTest3,ReconnectableTest4',\n      ...\n    ]\n    // Test page has to be reloaded for each reconnectable test group\n```\n```javascript\n    // In Driver page\n    // example for web-component-tester\n    var suites = [];\n    for (var scope in Suite.scopes) {\n      Suite.scopes[scope].test.forEach(function (tests, index) {\n        suites.push(scope + '-test.html?TestSuites=' + index);\n      });\n    }\n    WCT.loadSuites(suites);\n```\n\n#### Test Suite Subclass as a direct or descendent subclass of `Suite` class\n\n- **async setup()** instance method - Overridden methods called once at the beginning of each running test via `suiteSetup()`; Overridden methods in subclasses should call `await super.setup()`\n- **async teardown()** instance method - Overridden methods called once at the end of each running test via `suiteTeardown()`; Overridden methods in subclasses should call `await super.teardown()`\n\n```javascript\n    // Define a common test suite as a direct subclass of Suite\n    class CommonTestSuite extends Suite {\n      async setup() { await super.setup(); ... }\n      async teardown() { await super.teardown(); ... }\n      ... // custom methods\n    }\n    { // example scope\n      let example = new Suite('example', 'Example Subsuite');\n      // [Optional] Define a test subsuite class in the 'example' scope \n      example.test = class ExampleSuite extends CommonTestSuite {\n        async setup() { await super.setup(); ... }\n        async teardown() { ...; await super.teardown(); } // may need teardown operation before super.teardown()\n        ... // more custom methods for the subsuite\n      }\n    }\n```\n\n#### Test Runner Subclass as a subclass of `Suite` class\n\n- **constructor(target: string)** - Instantiate a test runner; Optional for overriding\n- **async run()** instance method - Run the test; Not for overriding\n  - It can throw an exception. See `exception()` instance method section below.\n- __* scenario()__ instance generator method - Iterate over tests in the reversed order of the prototype chain of the test; Yield `{ name: string, iteration: function, operation: function, checkpoint: function, ctor: function }` for each test class\n\n```javascript\n    // Simplest example without a test suite subclass and a scope\n    class TestClass extends Suite {} // Define a test runner class\n    let testRunner = new TestClass('#target'); // testRunner.target = '#target'\n    testRunner.run(); // run the test\n```\n```javascript\n    // Example with a scope\n    class ExampleSuite extends Suite { ... }\n    { // example scope\n      let example = new Suite('example');\n      // Define a test class in the scope\n      example.test = class ExampleTest1 extends ExampleSuite {\n        async operation() { ... }\n        async checkpoint() { ... }\n      }\n      // run the test\n      (new example.classes.ExampleTest1('#target')).run();\n    }\n```\n- **async operation(parameters: optional)** instance method - Perform operations of the test; `parameters` argument is omitted if `*iteration()` is not defined\n- **async checkpoint(parameters: optional)** instance method - Perform test assertions of the test; `parameters` argument is omitted if `*iteration()` is not defined\n- __*iteration()__ instance generator method - [Optional] Provide parameters to `operation` and `checkpoint` methods\n```javascript\n    class ExampleTest2 extends ExampleSuite {\n      * iteration() { yield * [ 1, 2, 3 ]; }\n      // parameters iterate through 1 to 3\n      async operation(parameters) { ... }\n      async checkpoint(parameters) { ... }\n    }\n```\n- **exception(reject: function, exception: Error)** instance method - [Optional] Exception handler for errors outside of test callback function\n  - If it calls `reject()`, it must return non-null to tell the runner not to call `resolve()`\n  - The method can be inherited and overridden by subclasses\n```javascript\n    class ExampleTest3 extends ExampleSuite {\n      async operation() { ... }\n      async checkpoint() { ... }\n      exception(reject, exception) {\n        // default action when exception() is not defined\n        reject(exception);\n        return true;\n      }\n    }\n    try {\n      await (new ExampleTest3('#example')).run();\n    }\n    catch (exception) {\n      // Handle exception in runner\n      ...\n    }\n```\n```javascript\n    class ExampleTest4 extends ExampleSuite {\n      async operation() { ... }\n      async checkpoint() { ... }\n      exception(reject, exception) {\n        // Treat the exception as a test failure by mocha\n        (typeof test === 'function' ? test : it)('exception on scenario', function() {\n          throw exception;\n        });\n      }\n    }\n```\n\n#### Utility Instance Methods\n\n- **async forEvent(element: Element, type: string, trigger: function, condition: function)** - Invoke the `trigger()` and wait for the event `type` for the `element` until `condition(element: Element, type: string, event: Event)` returns `true`\n```javascript\n    async operation(parameters) {\n      let self = this;\n      // wait for 'track' event until the event state becomes 'end'\n      await self.forEvent(self.dialog, \n        'track',\n        () =\u003e { MockInteractions.track(self.dialog, parameters.dx, parameters.dy); },\n        (element, type, event) =\u003e event.detail.state === 'end'\n      );\n    }\n```\n\n#### Utility Static Methods\n\n- **Suite.repeat(target: string, count: number, subclass: string/object)** - Repetition operator for scenario objects\n```javascript\n    Suite.repeat('TargetTest', 3, 'RepeatTarget3Times') returns\n    { TargetTest: { TargetTest: { TargetTest: 'RepeatTarget3Times' } } }\n```\n- **Suite.permute(targets: Array of string, subclass: function (scenario: Array of string))** - Permutation operator for scenario objects\n```javascript\n    Suite.permute([ 'TestA', 'TestB', 'TestC' ],\n      (scenario) =\u003e 'Test_' + scenario.map(n =\u003e n.replace(/^Test/,'')).join('_')\n    ) returns\n    {\n      TestA: {\n        TestB: { TestC: \"Test_A_B_C\" },\n        TestC: { TestB: \"Test_A_C_B\" }\n      },\n      TestB: {\n        TestA: { TestC: \"Test_B_A_C\" },\n        TestC: { TestA: \"Test_B_C_A\" }\n      },\n      TestC: {\n        TestB: { TestA: \"Test_C_B_A\" },\n        TestA: { TestB: \"Test_C_A_B\" }\n      }\n    }\n```\n\n#### Static Properties for `Suite` class\n\n- **Suite.scopes** static object property - Object containing scope objects as `Suite.scopes[scope]` such as `Suite.scopes.example` for `'example'` scope\n\n#### Static Properties for subclasses\n\n- **reconnectable** static boolean read-only property - Default: `true`; Override the value as `false` to treat the test and its subclasses are not reconnectable and need page reloading to perform further tests\n```javascript\n    class NonReconnectableSuite extends Suite {\n      get reconnectable() { return false; }\n      ...\n    }\n```\n- **skipAfterFailure** static boolean read-only property - Default: `false`; Override the value as `true` to skip subsequent tests of the suite after an assertion failure\n```javascript\n    class SkipAfterFailureSuite extends Suite {\n      get skipAfterFailure() { return true; }\n      ...\n    }\n```\n\n## Complex Examples\n\n### Test Class Mixin with Parameterized Iterations in [dialog-test.js](https://github.com/t2ym/live-localizer/blob/master/test/src/dialog-test.js) for [live-localizer test suites](https://github.com/t2ym/live-localizer/tree/master/test)\n\n```javascript\n{\n  let example = new Suite('example');\n  example.test = (base) =\u003e class DragDialogTest extends base {\n    * iteration() {\n      let dx = 10;\n      let dy = 10;\n      yield *[\n        { mode: 'position', dx: dx, dy: dy, expected: { x: dx, y: dy, width: 0, height: 0 } },\n        { mode: 'upper-left', dx: -dx, dy: -dy, expected: { x: -dx, y: -dy, width: dx, height: dy } },\n        { mode: 'upper', dx: -dx, dy: -dy, expected: { x: 0, y: -dy, width: 0, height: dy } },\n        { mode: 'upper-right', dx: dx, dy: -dy, expected: { x: 0, y: -dy, width: dx, height: dy } },\n        { mode: 'middle-left', dx: -dx, dy: dy, expected: { x: -dx, y: 0, width: dx, height: 0 } },\n        { mode: 'middle-right', dx: dx, dy: dy, expected: { x: 0, y: 0, width: dx, height: 0 } },\n        { mode: 'lower-left', dx: -dx, dy: dy, expected: { x: -dx, y: 0, width: dx, height: dy } },\n        { mode: 'lower', dx: dx, dy: dy, expected: { x: 0, y: 0, width: 0, height: dy } },\n        { mode: 'lower-right', dx: dx, dy: dy, expected: { x: 0, y: 0, width: dx, height: dy } },\n        { mode: '.title-pad', dx: dx, dy: dy, expected: { x: 0, y: 0, width: 0, height: 0 } }\n      ].map((parameters) =\u003e { parameters.name = 'drag dialog by ' + parameters.mode + ' handle'; return parameters });\n    }\n    async operation(parameters) {\n      let self = this;\n      let handle = self.dialog.$.handle.querySelector(parameters.mode.match(/^[.]/) ? parameters.mode : '[drag-handle-mode=' + parameters.mode + ']');\n      self.origin = {};\n      [ 'x', 'y', 'width', 'height' ].forEach(function (prop) {\n        self.origin[prop] = self.dialog[prop];\n      });\n      handle.dispatchEvent(new MouseEvent('mouseover', {\n        bubbles: true,\n        cancelable: true,\n        clientX: 0,\n        clientY: 0,\n        buttons: 1\n      }));\n      await self.forEvent(self.dialog, 'track', () =\u003e { MockInteractions.track(self.dialog, parameters.dx, parameters.dy); }, (element, type, event) =\u003e event.detail.state === 'end');\n    }\n    async checkpoint(parameters) {\n      for (let prop in parameters.expected) {\n        assert.equal(\n          this.dialog[prop],\n          this.origin[prop] + parameters.expected[prop],\n          'dialog is dragged with ' + parameters.mode + ' handle by ' + parameters.expected[prop] + ' in ' + prop);\n      }\n    }\n  }\n}\n```\n\n### Test Class Mixin Generator for Common Operations and Checkpoints in [demo.js](https://github.com/t2ym/scenarist/blob/master/demo/src/demo.js)\n\n```javascript\nlet demo = new Suite(scope, 'Scenarist Demo Suite');\nconst labels = {\n  // op: [ 'Class', 'id' ]\n  '0': [ 'Number0', '0' ],\n  '1': [ 'Number1', '1' ],\n  ...\n  '=': [ 'Equal', '=' ],\n  'A': [ 'Ac', 'AC' ],\n  'B': [ 'Bs', 'BS' ]\n};\ndemo.expected = {\n  \"AC\": \"0\",\n  \"AC1\": \"1\",\n  \"AC12\": \"12\",\n  \"AC12+\": \"12\",\n  \"AC12+3\": \"3\",\n  \"AC12+34\": \"34\",\n  \"AC12+34=\": \"46\",\n  ...\n};\nfor (let ex in labels) {\n  demo.test = (new Function('demo',\n    (function (subclass, label) { // generate ES5 class by manipulating transpiled func.toString()\n      return 'return ' +\n        ((base) =\u003e class __SUBCLASS__ extends base {\n          get description() { return '__LABEL__'; }\n          async operation() {\n            await this.tap('__LABEL__');\n            this.history = this.state('__LABEL__');\n          }\n          async checkpoint() {\n            assert.equal(this.element.value, demo.expected[this.history], 'Value for scenario ' + this.history + ' is valid');\n          }\n        })\n        .toString()\n        .replace(/__cov_[^. ]*[.][a-z]\\[\\'[0-9]*\\'\\](\\[[0-9]*\\])?\\+\\+[;,]?/g, '') // trim istanbul coverage counters\n        .replace(/__SUBCLASS__/g, subclass)\n        .replace(/__LABEL__/g, label);\n    })(labels[ex][0], labels[ex][1])))(demo);\n}\n```\n\n### Custom Test Scenario Object Operator in [demo.js](https://github.com/t2ym/scenarist/blob/master/demo/src/demo.js)\n\n```javascript\nfunction operations(expression, name) {\n  let result = null;\n  if (!name) {\n    let description = [];\n    name = [];\n    for (let j of expression) {\n      name.push(labels[j][0]);\n      description.push((' ' + labels[j][1] + ' ').replace(/^ ([0-9]) $/, '$1'));\n    }\n    name = name.join('_');\n    description = description.join('').replace(/  /g, ' ').trim();\n    description += (description.match(/=$/) ? ' ' : ' = ') + demo.expected['AC' + description.replace(/ /g, '')];\n    name += '; ' + description;\n  }\n  for (let i = expression.length - 1; i \u003e= 0; i--) {\n    let op = expression[i];\n    let mixin = labels[op][0];\n    if (!mixin) {\n      throw new Error('Invalid operation ' + op + ' in \"' + expression + '\"');\n    }\n    if (result) {\n      result = { [mixin]: result };\n    }\n    else {\n      result = { [mixin]: name };\n    }\n  }\n  return result;\n}\ndemo.test = {\n  // test class mixins\n  '': [\n    operations('12', 'N12'),\n    operations('34', 'N34')\n  ],\n  // test classes\n  Connect: {\n    Ac: [\n      {\n        N12: [ '+', '-', '*', '/' ]\n          .map((op) =\u003e ({ \n            [labels[op][0]]: {\n              N34: {\n                Equal: '_12_' + labels[op][0] + '_34; 12 ' + op + ' 34 = ' + demo.expected['AC12' + op + '34=']\n              }\n            }\n          }))\n      },\n      operations('81SSS'),\n      operations('5M3M2MR'),\n      operations('3*2=M5*4.2=mCR'),\n      operations('200*15%'),\n      operations('54N+12='),\n      operations('1234BB+3*4/12='),\n      operations('12**====')\n    ]\n  }\n};\n```\n\n## License\n\n[BSD-2-Clause](https://github.com/t2ym/scenarist/blob/master/LICENSE.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ft2ym%2Fscenarist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ft2ym%2Fscenarist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ft2ym%2Fscenarist/lists"}