[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2\njobs:\n  build:\n    docker:\n      - image: circleci/node:8-browsers\n        environment:\n          DATABASE_URL: sqlite:./todo-subsecond.sqlite\n    working_directory: ~/repo\n    steps:\n      - run:\n          name: check version\n          command: |\n            node --version\n            yarn --version\n      - checkout\n      - restore_cache:\n          keys:\n          - todo-subsecond-{{ arch }}-{{ .Branch }}-{{ checksum \"package.json\" }}\n      - run:\n          name: install dependencies\n          command: yarn install\n      - save_cache:\n          paths:\n            - node_modules\n          key: todo-subsecond-{{ arch }}-{{ .Branch }}-{{ checksum \"package.json\" }}\n      - run: yarn test\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\nlocal.log\nbrowserstack.err\n.DS_Store\ntodo-subsecond.sqlite\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) Sub-Second TDD Organization\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "Procfile",
    "content": "web: node lib/server/start.js\n"
  },
  {
    "path": "README.md",
    "content": "# TODO Subsecond\n\n[![CircleCI](https://circleci.com/gh/subsecondtdd/todo-subsecond.svg?style=svg)](https://circleci.com/gh/subsecondtdd/todo-subsecond)\n\nThis is a tiny application with full-stack acceptance tests that can run in milliseconds. The purpose is to illustrate\nthe essential techniques to achieve this in any system.\n\nThrough a series of exercises you'll get familiar with a particular way of doing TDD/BDD:\n\n1. Small increments. Work at a single layer at a time.\n2. Fast (sub-second) feedback loop.\n3. UI-agnostic scenarios. Describe system behaviour rather than implemention.\n4. Testing through the UI without the typical drawbacks (slow, brittle).\n\nFast and stable full-stack tests that go through the UI are possible when the following criteria are satisfied:\n\n* No I/O whatsoever\n  * No I/O between your test and a browser automation tool like Selenium WebDriver\n  * No I/O between Selenium WebDriver and a browser\n  * No I/O between the browser application and the server\n  * No I/O between the server and a database\n* Clearly defined interfaces/contracts that allow swapping out the slow I/O bound components above\n* UI and server run in the same process\n  * This is possible thanks to [cucumber-electron](https://github.com/cucumber/cucumber-electron)\n* Tests/Scenarios are decoupled from the UI, allowing the UI to change with minimal impact on tests\n\n## Credits and inspiration\n\nThe techniques illustrated by this application are inspired by:\n\n* [Nat Pryce's \"Having Our Cake and Eating it](https://youtu.be/Fk4rCn4YLLU) - slides [here](https://speakerdeck.com/npryce/having-our-cake-and-eating-it-1)\n* [Jan Molak's \"Testing modern web apps. At scale.\"](http://agileonthebeach.com/testing-modern-web-apps-scale-jan-molak-software-delivery-2017/)\n* Josh's colleagues at [Featurist](https://www.featurist.co.uk/)\n* Aslak's colleagues at [Cucumber Ltd](https://cucumber.io)\n\n## Install Software\n\n* [Node.js 8.x or higher](https://nodejs.org/en/download/current/)\n\n## Install dependencies\n\n    npm install\n\n## Create a database\n\nAlthough this repository is intended as a demonstration of a design pattern, we want it to be as realistic as possible. So ideally you should use a database that you might use in production, like the [instance we deployed with a postgres database](https://todo-subsecond.herokuapp.com).\n\nIf you have Postgres installed you can use that. Otherwise you can use SQlite (this is simpler as it does not require a separate installation):\n\n### Postgres\n\n    createdb todo-subsecond\n\n### SQlite\n\n    # Linux/OSX:\n    export DATABASE_URL=sqlite:./todo-subsecond.sqlite\n\n    # Windows:\n    set DATABASE_URL=sqlite:./todo-subsecond.sqlite\n\n## Run tests\n\nRun all the tests in a single command:\n\n    # Linux/OSX:\n    features/run/all\n\n    # Windows:\n    features\\run\\all\n\nOr just one assembly at a time (we'll explain assemblies further down):\n\n    # Linux/OSX:\n    features/run/memory\n\n    # Windows:\n    features\\run\\memory\n\n### Change browser\n\nThe `webdriver-*` assemblies will use Chrome by default. You can change to another browser if you want:\n\n    # Linux/OSX:\n    brew install geckodriver\n    export SELENIUM_BROWSER=firefox\n\n    # Windows:\n    set SELENIUM_BROWSER=firefox\n\n## Start the server\n\nThe application is a small web application that renders a simple single-page application. Fire it up:\n\n    npm start\n\nOpen `http://localhost:8666` in your browser and add a couple of todos.\n\n![screenshot](docs/screenshot.png)\n\nCheck one of them and refresh the page. As you'll see, the application doesn't remember that a todo is marked as done.\n\n## Implement a new feature\n\nWe're going to improve the application to make it remember the state of todos.\n\nUncomment the three steps in `features/todo.feature` and run `features/run/all` again. Cucumber will tell you that\ntwo steps are undefined. Copy the first snippet and paste it into `features/step_definitions/todo_steps.js`.\n\nModify it so it looks like this:\n\n```javascript\nWhen('I mark the {ordinal} todo as done', async function (index) {\n  const todoList = await this.actionTodoList()\n  await todoList.markAsDone(index)\n})\n```\n\nRun Cucumber again. This time you'll get an error. Open `test_support/MemoryTodoList.js` and implement a new\n`markAsDone` method.\n\nRun `features/run/all` again until you've managed to get the 2nd step to pass.\n\nNow, paste the snippet for the last undefined step and modify it:\n\n```javascript\nThen('the {ordinal} todo should be marked as done', async function (index) {\n  const todoList = await this.outcomeTodoList()\n  const todos = await todoList.getTodos()\n  assert.equal(todos[index].done, true)\n})\n```\n\nRun `features/run/all` again until all steps pass. When they do, Cucumber will run the same scenario in a new configuration,\n`dom-memory`. This will fail. Now you need to make changes to `DomTodoList.js`.\n\nContinue until everything is green. Every time you have a failing step you can make small increments in a single layer\nof the application. Eventually all scenarios should pass.\n\nWhen they do, you can do a final manual test just to give yourself confidence that everything works as expected.\n\nThat's it - subsecond TDD with acceptance tests that run in milliseconds while you're developing. You shouldn't have to\nrely on the slow acceptance tests (`webdriver-memory` and `webdriver-http-database`) while you're implementing the new\nfunctionality. You should only run them at the end to give you full confidence, and they should just pass without\nrequiring you to make any changes.\n\n## Assemblies\n\nWhen you look at `features/todo.feature` and `features/step_definitions/todo_steps.js` you'll notice that the scenarios\nand step definitions make no assumptions about how to interact with the application.\n\nWe're interacting with the application *through a contract*, or an *interface* - `addTodo` and `getTodos`. Think of your\nscenarios as a lego-like block that can be connected to another block that satisfies this contract:\n\n![lego](docs/test.png)\n\nIn a statically typed language such as Java, C# or TypeScript, this would be an interface. With JavaScript it's just\n*implicitly* defined.\n\nSeveral parts of the application as well as additional test code can implement this interface/contract:\n\n![lego](docs/lego.png)\n\nThere are three kinds of blocks:\n\n* Green blocks represent *test code*\n* Blue blocks represent *application code*\n* Pink blocks represent *infrastructure*\n\nTests can be connected to the top of any stack of blocks and test different assemblies\nof the application.\n\nA tall stack of blocks gives more confidence at the cost of speed.\n\nThe speed doesn't depend directly on the number of blocks, but rather on the\namount of I/O happening *between* each block and the amount of CPU processing\nhappening *inside* each block.\n"
  },
  {
    "path": "cucumber",
    "content": "#!/usr/bin/env bash\nnode node_modules/.bin/cucumber-js \"$@\"\n"
  },
  {
    "path": "cucumber-electron",
    "content": "#!/usr/bin/env bash\nnode node_modules/.bin/cucumber-electron \"$@\"\n"
  },
  {
    "path": "cucumber.js",
    "content": "module.exports = {\n  default: '--format-options \\'{\"snippetInterface\": \"promise\"}\\''\n}\n"
  },
  {
    "path": "features/run/all",
    "content": "#!/usr/bin/env bash\nfeatures/run/memory && \\\nfeatures/run/dom-memory && \\\nfeatures/run/http-memory && \\\nfeatures/run/dom-http-memory && \\\nfeatures/run/database && \\\nfeatures/run/webdriver-memory && \\\nfeatures/run/webdriver-http-database && \\\nfeatures/run/browserstack-memory\n"
  },
  {
    "path": "features/run/all.cmd",
    "content": "@echo off\ncall features\\run\\memory\nif %errorlevel% neq 0 exit /b %errorlevel%\ncall features\\run\\dom-memory\nif %errorlevel% neq 0 exit /b %errorlevel%\ncall features\\run\\http-memory\nif %errorlevel% neq 0 exit /b %errorlevel%\ncall features\\run\\dom-http-memory\nif %errorlevel% neq 0 exit /b %errorlevel%\ncall features\\run\\database\nif %errorlevel% neq 0 exit /b %errorlevel%\ncall features\\run\\webdriver-memory\nif %errorlevel% neq 0 exit /b %errorlevel%\ncall features\\run\\webdriver-http-database\nif %errorlevel% neq 0 exit /b %errorlevel%\ncall features\\run\\browserstack-memory\nif %errorlevel% neq 0 exit /b %errorlevel%\n"
  },
  {
    "path": "features/run/browserstack-memory",
    "content": "#!/usr/bin/env bash\nif [[ -z \"${BROWSERSTACK_USER}\" ]]; then\n  echo \"set BROWSERSTACK_USER and BROWSERSTACK_KEY to run features on browserstack\"\nelse\n  CUCUMBER_ASSEMBLY=browserstack-memory node node_modules/.bin/cucumber-js \"$@\"\nfi\n"
  },
  {
    "path": "features/run/browserstack-memory.cmd",
    "content": "@echo off\nIF \"%BROWSERSTACK_USER%\"==\"\" (\n  echo set BROWSERSTACK_USER and BROWSERSTACK_KEY to run features on browserstack\n) ELSE (\n  set CUCUMBER_ASSEMBLY=browserstack-memory\n  node_modules\\.bin\\cucumber-js %*\n)\n"
  },
  {
    "path": "features/run/database",
    "content": "#!/usr/bin/env bash\nCUCUMBER_ASSEMBLY=database node node_modules/.bin/cucumber-js \"$@\"\n"
  },
  {
    "path": "features/run/database.cmd",
    "content": "@echo off\nset CUCUMBER_ASSEMBLY=database\nnode_modules\\.bin\\cucumber-js %*\n"
  },
  {
    "path": "features/run/dom-http-memory",
    "content": "#!/usr/bin/env bash\nCUCUMBER_ASSEMBLY=dom-http-memory node node_modules/.bin/cucumber-electron \"$@\"\n"
  },
  {
    "path": "features/run/dom-http-memory.cmd",
    "content": "@echo off\nset CUCUMBER_ASSEMBLY=dom-http-memory\nnode_modules\\.bin\\cucumber-electron %*\n"
  },
  {
    "path": "features/run/dom-memory",
    "content": "#!/usr/bin/env bash\nCUCUMBER_ASSEMBLY=dom-memory node node_modules/.bin/cucumber-electron \"$@\"\n"
  },
  {
    "path": "features/run/dom-memory.cmd",
    "content": "@echo off\nset CUCUMBER_ASSEMBLY=dom-memory\nnode_modules\\.bin\\cucumber-electron %*\n"
  },
  {
    "path": "features/run/http-memory",
    "content": "#!/usr/bin/env bash\nCUCUMBER_ASSEMBLY=http-memory node node_modules/.bin/cucumber-js \"$@\"\n"
  },
  {
    "path": "features/run/http-memory.cmd",
    "content": "@echo off\nset CUCUMBER_ASSEMBLY=http-memory\nnode_modules\\.bin\\cucumber-js %*\n"
  },
  {
    "path": "features/run/memory",
    "content": "#!/usr/bin/env bash\nCUCUMBER_ASSEMBLY=memory node node_modules/.bin/cucumber-js \"$@\"\n"
  },
  {
    "path": "features/run/memory.cmd",
    "content": "@echo off\nset CUCUMBER_ASSEMBLY=memory\nnode_modules\\.bin\\cucumber-js %*\n"
  },
  {
    "path": "features/run/webdriver-http-database",
    "content": "#!/usr/bin/env bash\nCUCUMBER_ASSEMBLY=webdriver-http-database node node_modules/.bin/cucumber-js \"$@\"\n"
  },
  {
    "path": "features/run/webdriver-http-database.cmd",
    "content": "@echo off\nset CUCUMBER_ASSEMBLY=webdriver-http-database\nnode_modules\\.bin\\cucumber-js %*\n"
  },
  {
    "path": "features/run/webdriver-memory",
    "content": "#!/usr/bin/env bash\nCUCUMBER_ASSEMBLY=webdriver-memory node node_modules/.bin/cucumber-js \"$@\"\n"
  },
  {
    "path": "features/run/webdriver-memory.cmd",
    "content": "@echo off\nset CUCUMBER_ASSEMBLY=webdriver-memory\nnode_modules\\.bin\\cucumber-js %*\n"
  },
  {
    "path": "features/step_definitions/todo_steps.js",
    "content": "const assert = require('assert')\nconst { Given, When, Then, setDefaultTimeout } = require('cucumber')\n\nsetDefaultTimeout(60000)\n\nGiven('there is/are already {int} todo(s)', async function(count) {\n  const todoList = await this.contextTodoList()\n  for (let i = 0; i < count; i++)\n    await todoList.addTodo({ text: `Todo ${i}` })\n})\n\nWhen('I add {string}', async function(text) {\n  const todoList = await this.actionTodoList()\n  await todoList.addTodo({ text })\n})\n\nThen('the text of the {ordinal} todo should be {string}', async function(index, text) {\n  const todoList = await this.outcomeTodoList()\n  const todos = await todoList.getTodos()\n  assert.equal(todos[index].text, text)\n})\n"
  },
  {
    "path": "features/support/assemblies/browserstack-memory.js",
    "content": "const BrowserStackTodoList = require('../../../test_support/BrowserStackTodoList')\nconst MemoryTodoList = require('../../../test_support/MemoryTodoList')\nconst WebApp = require('../../../lib/server/WebApp')\n\nmodule.exports = class BrowserStackMemoryAssembly {\n  async start () {\n    this._memoryTodoList = new MemoryTodoList()\n    this._webApp = new WebApp({ todoList: this._memoryTodoList, serveClient: true })\n    const port = await this._webApp.listen(0)\n    this._browserStackTodoList = new BrowserStackTodoList(`http://localhost:${port}`)\n    await this._browserStackTodoList.start()\n  }\n\n  async stop () {\n    await this._browserStackTodoList.stop()\n    await this._webApp.stop()\n  }\n\n  async contextTodoList() {\n    return this._memoryTodoList\n  }\n\n  async actionTodoList() {\n    return this._browserStackTodoList\n  }\n\n  async outcomeTodoList() {\n    return this._browserStackTodoList\n  }\n}\n"
  },
  {
    "path": "features/support/assemblies/database.js",
    "content": "const DatabaseTodoList = require('../../../lib/server/DatabaseTodoList')\n\nmodule.exports = class DatabaseAssembly {\n  async start () {\n    this._databaseTodoList = new DatabaseTodoList()\n    await this._databaseTodoList.start(true)\n  }\n\n  async stop() {\n    await this._databaseTodoList.stop()\n  }\n\n  async contextTodoList() {\n    return this._databaseTodoList\n  }\n\n  async actionTodoList() {\n    return this._databaseTodoList\n  }\n\n  async outcomeTodoList() {\n    return this._databaseTodoList\n  }\n}\n"
  },
  {
    "path": "features/support/assemblies/dom-http-memory.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst DomTodoList = require('../../../test_support/DomTodoList')\nconst MemoryTodoList = require('../../../test_support/MemoryTodoList')\nconst HttpTodoList = require('../../../lib/client/HttpTodoList')\nconst BrowserApp = require('../../../lib/client/BrowserApp')\nconst WebApp = require('../../../lib/server/WebApp')\n\nmodule.exports = class DomHttpMemoryAssembly {\n  async start () {\n    this._memoryTodoList = new MemoryTodoList()\n    this.webApp = new WebApp({ todoList: this._memoryTodoList, serveClient: false })\n  }\n\n  async stop() {}\n\n  async contextTodoList() {\n    return this._memoryTodoList\n  }\n\n  async actionTodoList() {\n    return await this._makeDomTodoList()\n  }\n\n  async outcomeTodoList() {\n    return await this._makeDomTodoList()\n  }\n\n  async _makeDomTodoList() {\n    const publicIndexHtmlPath = path.join(__dirname, '..', '..', '..', 'public', 'index.html')\n    const html = fs.readFileSync(publicIndexHtmlPath, 'utf-8')\n    const domNode = document.createElement('div')\n    domNode.innerHTML = html\n    document.body.appendChild(domNode)\n    await new BrowserApp({ domNode, todoList: await this._makeHttpTodoList() }).mount()\n    return new DomTodoList(domNode)\n  }\n\n  async _makeHttpTodoList() {\n    const port = await this.webApp.listen(0)\n    return new HttpTodoList(`http://localhost:${port}`)\n  }\n}\n"
  },
  {
    "path": "features/support/assemblies/dom-memory.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst DomTodoList = require('../../../test_support/DomTodoList')\nconst MemoryTodoList = require('../../../test_support/MemoryTodoList')\nconst BrowserApp = require('../../../lib/client/BrowserApp')\n\nmodule.exports = class DomMemoryAssembly {\n  async start () {\n    this._memoryTodoList = new MemoryTodoList()\n  }\n\n  async stop() {}\n\n  async contextTodoList() {\n    return this._memoryTodoList\n  }\n\n  async actionTodoList() {\n    return await this._makeDomTodoList()\n  }\n\n  async outcomeTodoList() {\n    return await this._makeDomTodoList()\n  }\n\n  async _makeDomTodoList() {\n    const publicIndexHtmlPath = path.join(__dirname, '..', '..', '..', 'public', 'index.html')\n    const html = fs.readFileSync(publicIndexHtmlPath, 'utf-8')\n    const domNode = document.createElement('div')\n    domNode.innerHTML = html\n    document.body.appendChild(domNode)\n    await new BrowserApp({ domNode, todoList: this._memoryTodoList }).mount()\n    return new DomTodoList(domNode)\n  }\n}\n"
  },
  {
    "path": "features/support/assemblies/http-memory.js",
    "content": "const MemoryTodoList = require('../../../test_support/MemoryTodoList')\nconst WebApp = require('../../../lib/server/WebApp')\nconst HttpTodoList = require('../../../lib/client/HttpTodoList')\n\nmodule.exports = class HttpMemoryAssembly {\n  async start () {\n    this._memoryTodoList = new MemoryTodoList()\n    this._webApp = new WebApp({ todoList: this._memoryTodoList, serveClient: false })\n    const port = await this._webApp.listen(0)\n    this._httpTodoList = new HttpTodoList(`http://localhost:${port}`)\n  }\n\n  async stop () {\n    await this._webApp.stop()\n  }\n\n  async contextTodoList() {\n    return this._memoryTodoList\n  }\n\n  async actionTodoList() {\n    return this._httpTodoList\n  }\n\n  async outcomeTodoList() {\n    return this._httpTodoList\n  }\n}\n"
  },
  {
    "path": "features/support/assemblies/memory.js",
    "content": "const MemoryTodoList = require('../../../test_support/MemoryTodoList')\n\nmodule.exports = class MemoryAssembly {\n  async start () {\n    this._memoryTodoList = new MemoryTodoList()\n  }\n\n  async stop() {}\n\n  async contextTodoList() {\n    return this._memoryTodoList\n  }\n\n  async actionTodoList() {\n    return this._memoryTodoList\n  }\n\n  async outcomeTodoList() {\n    return this._memoryTodoList\n  }\n}\n"
  },
  {
    "path": "features/support/assemblies/webdriver-http-database.js",
    "content": "const WebDriverTodoList = require('../../../test_support/WebDriverTodoList')\nconst DatabaseTodoList = require('../../../lib/server/DatabaseTodoList')\nconst WebApp = require('../../../lib/server/WebApp')\n\nmodule.exports = class WebDriverDatabaseAssembly {\n  async start () {\n    this._databaseTodoList = new DatabaseTodoList()\n    await this._databaseTodoList.start(true)\n    this._webApp = new WebApp({ todoList: this._databaseTodoList, serveClient: true })\n    const port = await this._webApp.listen(0)\n    this._webDriverTodoList = new WebDriverTodoList(`http://localhost:${port}`)\n  }\n\n  async stop () {\n    await this._webDriverTodoList.stop()\n    await this._webApp.stop()\n  }\n\n  async contextTodoList() {\n    return this._databaseTodoList\n  }\n\n  async actionTodoList() {\n    await this._webDriverTodoList.start()\n    return this._webDriverTodoList\n  }\n\n  async outcomeTodoList() {\n    return this._webDriverTodoList\n  }\n}\n"
  },
  {
    "path": "features/support/assemblies/webdriver-memory.js",
    "content": "const WebDriverTodoList = require('../../../test_support/WebDriverTodoList')\nconst MemoryTodoList = require('../../../test_support/MemoryTodoList')\nconst WebApp = require('../../../lib/server/WebApp')\n\nmodule.exports = class WebDriverMemoryAssembly {\n  async start () {\n    this._memoryTodoList = new MemoryTodoList()\n    this._webApp = new WebApp({ todoList: this._memoryTodoList, serveClient: true })\n    const port = await this._webApp.listen(0)\n    this._webDriverTodoList = new WebDriverTodoList(`http://localhost:${port}`)\n  }\n\n  async stop () {\n    await this._webDriverTodoList.stop()\n    await this._webApp.stop()\n  }\n\n  async contextTodoList() {\n    return this._memoryTodoList\n  }\n\n  async actionTodoList() {\n    await this._webDriverTodoList.start()\n    return this._webDriverTodoList\n  }\n\n  async outcomeTodoList() {\n    return this._webDriverTodoList\n  }\n}\n"
  },
  {
    "path": "features/support/parameter_types.js",
    "content": "const { defineParameterType } = require('cucumber')\n\ndefineParameterType({\n  name: 'ordinal',\n  regexp: /(\\d+)(?:st|nd|rd|th)/,\n  transformer(s) {\n    return parseInt(s) - 1\n  }\n})"
  },
  {
    "path": "features/support/world.js",
    "content": "const { setWorldConstructor, Before, After } = require('cucumber')\n\nconst assemblyName = process.env.CUCUMBER_ASSEMBLY || 'memory'\nconsole.log(`🥒 ${assemblyName}`)\n\nconst AssemblyModule = require(`./assemblies/${assemblyName}`)\nconst assembly = new AssemblyModule()\n\nclass TodoWorld {\n  constructor() {\n    this.contextTodoList = () => assembly.contextTodoList()\n    this.actionTodoList = () => assembly.actionTodoList()\n    this.outcomeTodoList = () => assembly.outcomeTodoList()\n  }\n}\n\nsetWorldConstructor(TodoWorld)\n\nBefore(async function() {\n  await assembly.start()\n})\n\nAfter(async function() {\n  await assembly.stop()\n})\n"
  },
  {
    "path": "features/todo.feature",
    "content": "Feature: Todo\n\n  Scenario: Create Todo\n    Given there is already 1 todo\n    When I add \"get milk\"\n    Then the text of the 2nd todo should be \"get milk\"\n\n  Scenario: Mark Todo Done\n    # Given there are already 2 todos\n    # When I mark the 2nd todo as done\n    # Then the 2nd todo should be marked as done\n\n  Scenario: Mark Todo Undone\n  Scenario: Delete Todo\n"
  },
  {
    "path": "lib/client/BrowserApp.js",
    "content": "module.exports = class BrowserApp {\n  constructor({ domNode, todoList }) {\n    this._domNode = domNode\n    this._todoList = todoList\n\n    this._addTodoButton = this._domNode.querySelector('[aria-label=\"Add Todo\"]')\n    this._todoTextField = this._domNode.querySelector('[aria-label=\"New Todo Text\"]')\n    this._todos = this._domNode.querySelector('[aria-label=\"Todos\"]')\n\n    this._addTodoButton.addEventListener('click', event => {\n      event.preventDefault()\n      const text = this._todoTextField.value\n      this.addTodo({ text })\n        .then(() => {\n          this._todoTextField.value = ''\n        })\n        .catch(err => console.error(err))\n    })\n  }\n\n  async mount() {\n    this.renderTodos(await this._todoList.getTodos())\n  }\n\n  async addTodo({ text }) {\n    await this._todoList.addTodo({ text })\n\n    const todos = await this._todoList.getTodos()\n    await this.renderTodos(todos)\n\n    this._domNode.dispatchEvent(\n      new window.CustomEvent('todos:todo:added', {\n        bubbles: true,\n      })\n    )\n  }\n\n  renderTodos(todos) {\n    // Remove old todos\n    while (this._todos.hasChildNodes()) {\n      this._todos.removeChild(this._todos.lastChild)\n    }\n\n    const ol = document.createElement('ol')\n    this._todos.appendChild(ol)\n\n    // Render new ones\n    for (let i = 0; i < todos.length; i++) {\n      const todo = todos[i]\n      const li = document.createElement('li')\n      const label = document.createElement('label')\n      const checkbox = document.createElement('input')\n      checkbox.type = 'checkbox'\n      const textNode = document.createTextNode(todo.text)\n\n      label.appendChild(checkbox)\n      label.appendChild(textNode)\n      li.appendChild(label)\n      ol.appendChild(li)\n    }\n  }\n}\n"
  },
  {
    "path": "lib/client/HttpTodoList.js",
    "content": "module.exports = class HttpTodoList {\n  constructor(baseUrl, fetch) {\n    this._baseUrl = baseUrl\n    this._fetch = fetch || require('node-fetch')\n  }\n\n  async addTodo({ text }) {\n    const res = await this._fetch(`${this._baseUrl}/todos`, {\n      method: 'post',\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({ text })\n    })\n    if (res.status !== 201)\n      throw new Error('Failed to create TODO')\n  }\n\n  async getTodos() {\n    const res = await this._fetch(`${this._baseUrl}/todos`)\n    if (res.status !== 200)\n      throw new Error('Failed to get TODOs')\n    return res.json()\n  }\n}\n"
  },
  {
    "path": "lib/client/client.js",
    "content": "const BrowserApp = require('./BrowserApp')\nconst HttpTodoList = require('./HttpTodoList')\n\nnew BrowserApp({\n  domNode: document.body,\n  todoList: new HttpTodoList('', window.fetch.bind(window))\n}).mount()\n"
  },
  {
    "path": "lib/server/DatabaseTodoList.js",
    "content": "const Sequelize = require('sequelize');\n\nmodule.exports = class DatabaseTodoList {\n\n  constructor() {\n    const databaseUrl = process.env.DATABASE_URL || 'postgres://localhost/todo-subsecond'\n    this._sequelize = new Sequelize(databaseUrl, { logging: false })\n\n    this.Todo = this._sequelize.define('todo', {\n      text: Sequelize.STRING,\n      // Use Sequelize.BOOLEAN for the done field\n    })\n  }\n\n  async start(truncate) {\n    if (truncate) {\n      await this._sequelize.sync({ force: true })\n    }\n  }\n\n  async stop(truncate) {\n    await this._sequelize.close()\n  }\n\n  async addTodo({ text }) {\n    await this.Todo.create({ text })\n  }\n\n  async getTodos() {\n    return this.Todo.findAll().map(record => ({\n      text: record.text\n    }))\n  }\n\n  // async markAsDone(index) {\n  //   const todo = .....\n  //   await todo.update({ done: true })\n  // }\n}\n"
  },
  {
    "path": "lib/server/WebApp.js",
    "content": "const http = require('http')\nconst express = require('express')\nconst bodyParser = require('body-parser')\nconst asyncExpress = require('./asyncExpress')\n\nmodule.exports = class WebApp {\n  constructor({ todoList, serveClient }) {\n    this._todoList = todoList\n    this._serveClient = serveClient\n  }\n\n  async buildApp() {\n    const app = express()\n    app.use(bodyParser.json())\n\n    app.use(express.static('./public'))\n\n    if (this._serveClient) {\n      const browserify = require('browserify-middleware')\n      app.get('/client.js', browserify(__dirname + '/../client/client.js'))\n    }\n\n    const asyncApp = asyncExpress(app)\n\n    asyncApp.post('/todos', async (req, res) => {\n      const { text } = req.body\n      await this._todoList.addTodo({ text })\n      res.status(201).end()\n    })\n\n    asyncApp.get('/todos', async (req, res) => {\n      res.setHeader('Content-Type', 'application/json')\n      const todos = await this._todoList.getTodos()\n      res.status(200).end(JSON.stringify(todos))\n    })\n\n    return app\n  }\n\n  async listen(port) {\n    const app = await this.buildApp()\n    this._server = http.createServer(app)\n    return new Promise((resolve, reject) => {\n      this._server.listen(port, err => {\n        if (err) return reject(err)\n        resolve(this._server.address().port)\n      })\n    })\n  }\n\n  async stop() {\n    await new Promise((resolve, reject) => {\n      this._server.close(err => {\n        if (err) return reject(err)\n        resolve()\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "lib/server/asyncExpress.js",
    "content": "const wrap = asyncFn => (req, res, next) => {\n  asyncFn(req, res, next).catch(err => next(err))\n}\n\nmodule.exports = function(app) {\n  return {\n    delete: (path, fn) => app.delete(path, wrap(fn)),\n    get: (path, fn) => app.get(path, wrap(fn)),\n    post: (path, fn) => app.post(path, wrap(fn)),\n    put: (path, fn) => app.put(path, wrap(fn)),\n  }\n}\n"
  },
  {
    "path": "lib/server/start.js",
    "content": "const WebApp = require('./WebApp')\nconst DatabaseTodoList = require('./DatabaseTodoList')\n\nconst port = process.env.PORT || 8666\n\nconst todoList = new DatabaseTodoList()\n\ntodoList.start()\n  .then(() => new WebApp({ todoList, serveClient: true }).listen(port))\n  .then(() => console.log(`http://localhost:${port}`))\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"todo-subsecond\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"contributors\": [\n    \"Josh Chisholm <joshuachisholm@gmail.com>\",\n    \"Aslak Hellesøy <aslak@cucumber.io>\"\n  ],\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"node lib/server/start.js\",\n    \"mocha\": \"mocha\",\n    \"test\": \"features/run/all\",\n    \"time\": \"time features/run/memory && time features/run/webdriver-http-database\"\n  },\n  \"dependencies\": {\n    \"body-parser\": \"^1.19.0\",\n    \"browserify\": \"^16.5.0\",\n    \"browserify-middleware\": \"^8.1.1\",\n    \"express\": \"^4.17.1\",\n    \"pg\": \"^7.18.2\",\n    \"pg-hstore\": \"^2.3.3\",\n    \"sequelize\": \"^5.21.5\",\n    \"sqlite3\": \"^4.1.1\"\n  },\n  \"devDependencies\": {\n    \"browserstack-local\": \"^1.4.5\",\n    \"cucumber\": \"^6.0.5\",\n    \"cucumber-electron\": \"^2.7.0\",\n    \"electron\": \"^8.1.1\",\n    \"mocha\": \"^7.1.0\",\n    \"node-fetch\": \"^2.6.0\",\n    \"selenium-webdriver\": \"^4.0.0-alpha.7\"\n  }\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>todos</title>\n    <link rel=\"stylesheet\" href=\"/todo.css\" />\n  </head>\n  <body>\n    <form>\n      <input type=\"text\" aria-label=\"New Todo Text\"  title=\"New Todo Text\"/>\n      <input type=\"submit\" value=\"Add\" aria-label=\"Add Todo\" />\n    </form>\n\n    <div aria-label=\"Todos\">\n    </div>\n    <script src=\"/client.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "public/todo.css",
    "content": "* {\n  box-sizing: border-box;\n}\nbody {\n  font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;\n  background-color: rgb(245, 245, 245);\n  color: #333;\n}\nbody::before {\n  content: 'todos';\n  display: block;\n  text-align: center;\n  padding: 20px;\n  font-size: 100px;\n  color: rgba(175, 47, 47, 0.3);\n  font-weight: 100;\n}\nform {\n  text-align: center;\n  width: 600px;\n  margin: auto;\n  padding: 20px;\n  background-color: #fff;\n  border: 3px solid #efefef;\n  border-bottom: none;\n  position: relative;\n}\ninput[type=\"checkbox\"] {\n  transform:scale(2, 2);\n  position: absolute;\n  left: 32px;\n  top: 28px;\n}\nform input, form button, ol li label {\n  font-size: 30px;\n  font-weight: 400;\n}\nform input[type=\"text\"], form input[type=\"submit\"] {\n  border: 2px solid #efefef;\n  padding: 10px;\n}\n\nform input[type=\"text\"] {\n  width: 400px;\n  text-indent: 20px;\n}\nform input[type=\"submit\"] {\n  width: 120px;\n  margin-left: 5px;\n}\nol {\n  margin: 0;\n  width: 600px;\n  margin: auto;\n  padding: 0;\n  border: 3px solid #efefef;\n  border-top: none;\n}\nol li {\n  list-style-type: none;\n  border-top: 2px solid #efefef;\n  position: relative;\n}\nol li label {\n  width: 100%;\n  display: block;\n  padding: 20px;\n  background-color: #fff;\n  color: #333;\n  padding-left: 80px;\n}\nol li label:hover {\n  background-color: rgba(250, 250, 250, 0.15);\n}\nol li input {\n  font-size: 30px;\n}\n"
  },
  {
    "path": "test/MemoryTodoListTest.js",
    "content": "const assert = require('assert')\nconst MemoryTodoList = require('../test_support/MemoryTodoList')\n\ndescribe('MemoryTodoList', () => {\n  it('stores and retrieves TODOs', async () => {\n    const todoList = new MemoryTodoList()\n    await todoList.addTodo({ text: 'Get milk' })\n    const todos = await todoList.getTodos()\n    assert.deepEqual(todos, [{text: 'Get milk'}])\n  })\n})"
  },
  {
    "path": "test_support/BrowserStackTodoList.js",
    "content": "const WebDriverTodoList = require('./WebDriverTodoList')\nconst browserstack = require('browserstack-local')\n\nmodule.exports = class BrowserStackTodoList extends WebDriverTodoList {\n  buildDriver(builder) {\n    return builder\n      .usingServer('http://hub-cloud.browserstack.com/wd/hub')\n      .withCapabilities({\n        'browserName': 'firefox',\n        'browserstack.user': process.env.BROWSERSTACK_USER,\n        'browserstack.key': process.env.BROWSERSTACK_KEY,\n        'browserstack.local': 'true'\n      })\n  }\n\n  async start() {\n    this.browserstackLocal = new browserstack.Local()\n    await new Promise((resolve, reject) => {\n      this.browserstackLocal.start({ key: process.env.BROWSERSTACK_KEY }, () => {\n        resolve()\n      })\n    })\n    await super.start()\n  }\n\n  async stop() {\n    await super.stop()\n    await new Promise((resolve, reject) => {\n      this.browserstackLocal.stop(() => {\n        resolve()\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "test_support/DomTodoList.js",
    "content": "module.exports = class DomTodoList {\n  constructor(domNode) {\n    this._domNode = domNode\n  }\n\n  async addTodo({ text }) {\n    return new Promise(resolve => {\n      this._domNode.addEventListener('todos:todo:added', () => {\n        resolve()\n      })\n\n      this._domNode.querySelector('[aria-label=\"New Todo Text\"]').value = text\n      this._domNode.querySelector('[aria-label=\"Add Todo\"]').click()\n    })\n  }\n\n  async getTodos() {\n    return [...this._domNode.querySelectorAll('[aria-label=\"Todos\"] li label')].map(label => ({\n      text: label.innerText\n    }))\n  }\n}\n"
  },
  {
    "path": "test_support/MemoryTodoList.js",
    "content": "module.exports = class MemoryTodoList {\n  constructor() {\n    this._todos = []\n  }\n\n  async addTodo({ text }) {\n    this._todos.push({ text })\n  }\n\n  async getTodos() {\n    return this._todos\n  }\n}\n"
  },
  {
    "path": "test_support/WebDriverTodoList.js",
    "content": "const webdriver = require('selenium-webdriver')\nconst { By, until } = webdriver\n\nmodule.exports = class WebDriverTodoList {\n  constructor(baseUrl) {\n    this._baseUrl = baseUrl\n    this._driver = this.buildDriver(new webdriver.Builder()).build()\n  }\n\n  buildDriver(builder) {\n    return builder.forBrowser('firefox')\n  }\n\n  async start() {\n    await this._driver.get(this._baseUrl + '/')\n    const todoListLocator = By.css('[aria-label=\"Todos\"] ol')\n    await this._driver.wait(until.elementLocated(todoListLocator), 10000)\n  }\n\n  async addTodo({ text }) {\n    const textField = this._driver.findElement(By.css('[aria-label=\"New Todo Text\"]'))\n    await textField.sendKeys(text)\n    const count = (await this.getTodos()).length\n    await this._driver.findElement(By.css('[aria-label=\"Add Todo\"]')).click()\n    await this._driver.wait(async () => (await this.getTodos()).length > count, 10000)\n  }\n\n  async getTodos() {\n    return this._driver.executeScript(() =>\n      [...document.querySelectorAll('[aria-label=\"Todos\"] li label')].map(label => ({\n        text: label.innerText\n      }))\n    )\n  }\n\n  async stop() {\n    await this._driver.quit()\n  }\n}\n"
  }
]