{"id":15714570,"url":"https://github.com/spiffre-oss/test-scenarii","last_synced_at":"2026-04-15T07:36:53.536Z","repository":{"id":57376796,"uuid":"172138239","full_name":"spiffre-oss/test-scenarii","owner":"spiffre-oss","description":"test-scenarii offers a way to write scenarized tests with maximum flexibility and code reuse, while retaining great readability","archived":false,"fork":false,"pushed_at":"2019-02-26T21:21:50.000Z","size":138,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-01T09:42:18.812Z","etag":null,"topics":["jest","testing"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spiffre-oss.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-02-22T21:39:36.000Z","updated_at":"2019-02-26T21:32:27.000Z","dependencies_parsed_at":"2022-09-19T16:52:06.922Z","dependency_job_id":null,"html_url":"https://github.com/spiffre-oss/test-scenarii","commit_stats":null,"previous_names":["spiffre/test-scenarii"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/spiffre-oss/test-scenarii","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spiffre-oss%2Ftest-scenarii","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spiffre-oss%2Ftest-scenarii/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spiffre-oss%2Ftest-scenarii/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spiffre-oss%2Ftest-scenarii/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spiffre-oss","download_url":"https://codeload.github.com/spiffre-oss/test-scenarii/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spiffre-oss%2Ftest-scenarii/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31831847,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T07:17:56.427Z","status":"ssl_error","status_checked_at":"2026-04-15T07:17:30.007Z","response_time":63,"last_error":"SSL_read: 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":["jest","testing"],"created_at":"2024-10-03T21:38:29.644Z","updated_at":"2026-04-15T07:36:53.504Z","avatar_url":"https://github.com/spiffre-oss.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# test-scenarii\n\nThis package offers a way to write scenarized tests with maximum flexibility and code reuse, while retaining great readability.  \n\nThe classic use case is to run several tests with very similar workflows — only with a twist each time.  \n\nHere's an example with a Todo App, where the creation of a TodoItem can be done 2 different ways: by setting a timestamp first, then the text or vice versa.  \n\n```js\nconst { createTestChain, setChainProps } = require('test-scenarii')\n\nconst testStep = require('./test-steps.js')\n\ndescribe(`Setting datetime then text`, () =\u003e\n{\n    let testChain = null\n\n    beforeAll( () =\u003e\n    {\n        const browser = /* a puppeteer browser */\n        const page = /* the current page */\n        const webServicesURL = /* the test-only URL for webservices */\n\n        testChain = createTestChain({ browser, page, webServicesURL }, {})\n    })\n\n    it(`must create a TodoItem, by setting its text first, then the firedate`, () =\u003e\n    {\n        return testChain(\n            testStep.openApplication(),\n            testStep.clickTodoItemCreateButton(),\n            testStep.clickTimeTag(),\n            testStep.setTodoItemText(),\n            testStep.clickValidationButton()\n        )\n    })\n\n    it(`must create a TodoItem, by setting its firedate first, then the text`, () =\u003e\n    {\n        return testChain(\n            testStep.openApplication(),\n            testStep.clickTodoItemCreateButton(),\n            testStep.setTodoItemText(),                     // Notice clickTimeTag() and setTodoItemText()\n            testStep.clickTimeTag(),                        // are inverted here, compared to the previous test\n            testStep.clickValidationButton()\n        )\n    })\n})\n```\n\n## Why not just use functions?\n\nAny real-life test workflow will rely on information being shared among individual tests. When using vanilla functions, this information (most of it details), will have to be lifted back up to the caller, before being passed to the next function.  \nThis leads to the accumulation of a lot noise at the top, right where we want a clear outline of what the test does. This clarity is needed in order to catch (at a glance) what the differences are between entiere series of similar workflows.  \n\n`test-scenarii` offers this clarity by providing a means for tests steps to communicate with each others while only exposing crucial information at the top.  \n\n\n## Table of Content\n\n- [Installation](#installation)\n- [API](#api)\n    - [`createTestChain()`](#createtestchain)\n    - [`createTestChainSync()`](#createtestchainsync)\n    - [`setChainProps()`](#setchainprops)\n    - [`.cached()`](#.cached)\n- [Test Steps](#test-steps)\n    - [What they are](#what-they-are)\n    - [How to write one](#how-to-write-one)\n- [Cookbook](#cookbook)\n    - [`Prevent redundant tests`](#prevent-redundant-tests)\n    - [`Communication between tests`](#communication-between-tests)\n    - [`Conditional test steps`](#conditional-test-steps)\n- [License](#license)\n\n## Installation\n\nThe test-scenarii package is distributed via npm. To install it, run:\n\n```\nnpm install -D test-scenarii\n```\n\nThe package will be installed in the `devDependencies`, alongside the test-runner you want to use it with, whether Jest or Mocha.\n\n## API\n\nThe library exposes only a few functions:  \n\n### `createTestChain`\n\n`createTestChain()` creates and initializes an asynchronous test chain with a context and a set of props. The returned chain can be run multiple times with different test steps.  \n\nSee [`createTestChainSync()`](#createTestChainSync) for the synchronous version.  \n\nThe new API acknowledges that the props passed from test to test have different update rates: some of them have no reason to change (references to the browser, the webservices URL, etc), while others are meant to be updated as a way to communicate between test steps (whether to takeScreenshots or not, to run tests, some data created in a test step reused in another, etc).  \n- The first parameter, the `context`, is the one not meant to be updated. `Object.freeze()` is used on the `context` as soon as it is passed to the chain creator. Because `Object.freeze()` only freezes shallowly, it is still possible to change context values at a deeper level. The freeze is only there to safeguard against a distraction mistake.  \n- The second parameter, `props`, is meant as a channel between test steps. Its values can be updated by simply returning an object from the test step; this object is shallow merged with the exisiting props and passed to the next test step.  \n\n\n### `createTestChainSync`\n\n`createTestChainSync()` works exactly the same as `createTestChain()`, except it only handles synchronous test steps :\n\n```js\nit(`should work exactly the same, except synchronously`, () =\u003e\n{\n    testChain(\n        testStep.some(),\n        testStep.synchronous(),\n        testStep.sequence(),\n        testStep.of(),\n        testStep.actions()\n    )\n})\n```\nNotice the absence of the `return` keyword before the call to `testChain()`: a test chain ends up returning the final value of the `props` object, possibly modified by each test step. But Jest will throw an error if anything other than `undefined` or a Promise is returned from a test.\n\n### `setChainProps`\n\nThe `setChainProps()` helper updates prop values somewhere along the chain—but from the outside.  \n\nWhile updating props is done primarily from inside the test steps, it can be super convenient to set some props from the outside. At the very least, it can help readability at times.  \n\n```js\nlet testChain = createTestChain({}, { runProfiling : false })\n\nit(`makes no difference whether you use setChainProps() or a regular test step`, () =\u003e\n{\n    testChain(\n        (ctx, props) =\u003e console.log(props.runProfiling),         // Prints: false\n        setChainProps({ runProfiling : true }),\n        (ctx, props) =\u003e console.log(props.runProfiling),         // Prints: true\n    )\n}\n```\n\nThe `setChainProps()` helper is actually implemented as an empty test step: it has no testing or action inside, but returns the new values for the passed props.\n\n### `.cached`\n\nThe `.cached()` helper is available on both versions: `createTestChain.cached()` and `createTestChainSync.cached()`. It creates and initializes a test chain with only a list of test steps, and doesn't run it immediately.  \nInstead, it can be injected as if it were a regular test step. The `context` and `props` passed inside this cached chain are inherited from the previous test step.\n\n```js\nconst testChain = createTestChain(null, { count : 0 })\n\nit(`should run a cached test chain as if it were any other test step`, () =\u003e\n{\n    // Create and cache a secondary chain. It will inherit the context and the props when the time comes\n    const cachedChain = createTestChainSync.cached(\n        (ctx, props) =\u003e ({ count : props.count + 2 }),\n        (ctx, props) =\u003e ({ count : props.count + 2 })\n    )\n\n    // Run the primary chain, which will execute the cached chain in between regular test steps\n    return testChain(\n        (ctx, props) =\u003e ({ count : props.count + 1 }),          // props.count: 0 =\u003e 1\n        cachedChain,                                            // props.count: 1 =\u003e 3 =\u003e 5\n        (ctx, props) =\u003e ({ count : props.count + 1 }),          // props.count: 5 =\u003e 6\n        (ctx, props) =\u003e expect(props.count).toBe(6)\n    )\n})\n```\n\n## Test Steps\n\n### What they are\n\nA test step is simply a function, which accepts 2 objects as its parameters (`ctx` and `props`).  \n\nAnything else will cause an error to be thrown, with the exception of `null`: when encountering a `null` test step, test-scenarii will silently skip it. This facilitates the use of [conditional test steps](#conditional-test-steps).  \n\nAnonymous functions will work, but named functions are recommended: their name will appear in error messages, which improves debuggability.  \n\n### How to write one\n\nThe way the test steps are written is key. In order to maximize flexibility and reuse, it's a good practice to separate the action they perform from the test(s). Running the tests should be conditioned via props such as `runTests` or `takeScreenshots`. This will allow high level test chains to reuse low-level ones without necessarily re-performing all the tests they list. This is of course highly dependent on the knowledge the developer has of what is susceptible to fail when performing specific actions.  \n\nThe following example use Puppeteer to navigate a project.  \n\n```js\nmodule.exports =\n{\n    clickTimeTag (timeTag)\n    {\n        return async function clickTimeTag (ctx, props) =\u003e\n        {\n            // The Action\n            await ctx.page.click(`button[data-time-tag=${timeTag}`)\n            await wait(300)\n\n            // The Test\n            if (props.runTests)\n            {\n                // Snapshot-test the UI (toggleable at will via context props)\n                const html = await ctx.page.$eval('.whole-panel', (el) =\u003e el.outerHTML)\n                expect(html).toMatchSnapshot()\n            }\n\n            // Another Test\n            if (props.runTests \u0026\u0026 props.takeScreenshots)\n            {\n                // Screenshot-test the UI (also toggleable at will via context props)\n                await ctx.page.screenshot({ path : `clickTimeTag-${Date.now()}` })\n            }\n        }\n    }\n\n    // [...] Other test steps\n}\n```\n\nCan be used via the following test chain:\n\n```js\nconst page = await browser.newPage()\nawait page.goto('http://localhost:3000')\n\nconst testChain = createTestChain({ /* ctx */ page }, { /* props */ runTests : true,  takeScreenshots : false })\n```\n\n\n## Cookbook\n\n###  Prevent redundant tests\n\nIn our initial example, note that the first steps (`openApplication()` and `clickTodoItemCreateButton()`) occur in both tests. They themselves contain expect statements, possibly snapshot or screenshot comparison tests. Written as they are, those tests steps will generate identical screenshots or snapshots.  \nThere's an easy way to prevent that:\n\n```js\n    beforeAll( () =\u003e\n    {\n        const browser = /* a puppeteer browser */\n        const page = /* the current page */\n        const webServicesURL = /* the test-only URL for webservices */\n\n        testChain = createTestChain({ browser, page, webServicesURL }, { runTests : true, takeScreenshots : true })\n    })\n\n    it(`must create a TodoItem, by setting its text first, then the firedate`, () =\u003e\n    {\n        return testChain(\n            testStep.openApplication(),\n            testStep.clickTodoItemCreateButton(),\n            testStep.clickTimeTag(),\n            testStep.setTodoItemText(),\n            testStep.clickValidationButton()\n        )\n    })\n\n    it(`must create a TodoItem, by setting its firedate first, then the text`, () =\u003e\n    {\n        return testChain(\n            setChainProps({ runTests : false, takeScreenshots : false })\n            testStep.openApplication(),\n            testStep.clickTodoItemCreateButton(),\n            setChainProps({ runTests : true, takeScreenshots : true })\n            testStep.setTodoItemText(),                     // Notice clickTimeTag() and setTodoItemText()\n            testStep.clickTimeTag(),                        // are inverted here, compared to the previous test\n            testStep.clickValidationButton()\n        )\n    })\n```\n\nIf the test steps are written properly, the testing, which is prop-controlled via `runTests`, and the screenshot-taking, which is prop-controlled via `takeScreenshots`, is going to be bypassed the second time around. Although this is a contrived example, it is clear how it will save time and resources in the case of numerous scenarii sharing a common base.\n\n### Communication between tests\n\nOften enough, a test step is going to result in applicative-side data being generated, and that information will be needed by as subsequent test step. Let's take the example of generated UUID.\n\n```js\n\nfunction createTodoItem ({ text, timestamp })\n{\n    return async function createTodoItem_TS (ctx, props) =\u003e\n    {\n        // Create the todo item\n        // [...]\n\n        // Get the most recently created item\n        const latestTodoItem = services.TodoManager.getLatest()\n        \n        // Return its UUID\n        return {\n            createdItemUUID : latestTodoItem.uuid\n        }\n    }\n}\n\nfunction selectTodoItem ()\n{\n    return async function selectTodoItemByUUID_TS (ctx, props) =\u003e\n    {\n        const selector = `li[data-uuid=${props.createdItemUUID}`\n        await ctx.page.click(selector)\n    }\n}\n```\n\nThis lets any test chain handle a \"previously created\" todo item, without having itself any knowledge of the UUID.\n\n### Conditional test steps\n\nWhen creating test chains, it can be convenient to condition some steps. It can be done easily, thanks to `null` being a valid value for a test step.  \nHere's an example with a parameterized cached test chain:\n\n```js\n/**\n * Create a todo with text and a timetag. Optionally close the panel\n * @param {object} options\n * @param {string} options.text\n * @param {string} options.timeTag\n * @param {boolean} [options.validateCreation]\n */\nfunction createTodoItem (options = {})\n{\n    const { text, timeTag, validateCreation : true } = options\n\n    return createTestChain.cached(\n        testStep.clickTodoItemCreateButton(),\n        testStep.clickTimeTag(timeTag),\n        testStep.setTodoItemText(text),\n        validateCreation ? testStep.clickValidationButton() : null\n    )\n}\n```\n\nThis cached test chain can be used in 2 different ways:\n\n```js\nit(`should create a todo item`, () =\u003e\n{\n    return testChain(\n        testStep.openApplication(),\n        testStep.createTodoItem({ text : 'The laundry!', timetag : 'yesterday', validateCreation : true })\n        (ctx, props) =\u003e\n        {\n            // Perform some checks\n\n        }\n    )\n}\n\nit(`should cancel right before creating a todo item`, () =\u003e\n{\n    return testChain(\n        testStep.openApplication(),\n        testStep.createTodoItem({ text : 'The laundry!', timetag : 'yesterday', validateCreation : false })\n        testStep.clickCancelButton()\n        (ctx, props) =\u003e\n        {\n            // Perform some checks\n\n        }\n    )\n}\n```\n\n## License\n\ntest-scenarii is distributed under MIT license","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspiffre-oss%2Ftest-scenarii","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspiffre-oss%2Ftest-scenarii","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspiffre-oss%2Ftest-scenarii/lists"}