[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n  pull_request:\n\njobs:\n  test:\n    name: Lint & Test (Node ${{ matrix.node-version }})\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        node-version: [20, 22, 24]\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Run linter\n        run: npm run lint\n\n      - name: Run tests\n        run: npm test\n\n      - name: Build\n        run: npm run build\n\n  release:\n    name: Build & Release\n    runs-on: ubuntu-latest\n    needs: test\n\n    # Only run build on push to master\n    if: github.event_name == 'push' && github.ref == 'refs/heads/master'\n\n    permissions:\n      contents: write # to be able to publish a GitHub release\n      issues: write # to be able to comment on released issues\n      pull-requests: write # to be able to comment on released pull requests\n      id-token: write # to enable use of OIDC for trusted publishing and npm provenance\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Build\n        run: npm run build\n\n      - name: Release\n        run: npx semantic-release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n\n# MacOS\n.DS_Store\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "npx --no -- commitlint --edit \"${1}\"\n\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "npx lint-staged\n"
  },
  {
    "path": "LICENCE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013-2015 Yuriy Husnay\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\nall copies 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\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Timer.js\n\nSimple and lightweight, zero-dependency library to create and manage, well, _timers_.\n\n- Chainable API\n- No runtime dependencies\n- UMD build for browsers and CommonJS/ESM interop for Node.js\n- TypeScript definitions included\n\n## Install\n\n```sh\nnpm install timer.js\n```\n\n## Quick start\n\n```js\nconst Timer = require(\"timer.js\");\n\nconst pizzaTimer = new Timer({\n  onend: () => console.log(\"Pizza is ready\"),\n});\n\npizzaTimer.start(15 * 60);\n```\n\n## Usage\n\nTimer.js is published as a UMD bundle and works in browsers and Node.js.\n\n### Node.js (CommonJS)\n\n```js\nconst Timer = require(\"timer.js\");\nconst timer = new Timer();\n```\n\n### Node.js (ESM)\n\n```js\nimport Timer from \"timer.js\";\nconst timer = new Timer();\n```\n\n### Browser (UMD)\n\n```html\n<script src=\"./node_modules/timer.js/dist/timer.js\"></script>\n<script>\n  const timer = new Timer();\n</script>\n```\n\n## API\n\nAll methods return `this` for chaining.\n\n```js\nmyTimer.start(10).on(\"pause\", doSmth).pause().on(\"end\", doSmthElse).start(); // and so on\n```\n\nAll callbacks and event handlers receive the timer instance as `this`.\n\n### Constructor\n\n```js\nconst timer = new Timer(options);\n```\n\n### Options\n\n| Option    | Type                   | Default | Notes                                                |\n| --------- | ---------------------- | ------- | ---------------------------------------------------- |\n| `tick`    | `number`               | `1`     | Tick interval in seconds.                            |\n| `onstart` | `(ms: number) => void` | `null`  | Called when the timer starts. Receives remaining ms. |\n| `ontick`  | `(ms: number) => void` | `null`  | Called on each tick. Receives remaining ms.          |\n| `onpause` | `() => void`           | `null`  | Called when the timer pauses.                        |\n| `onstop`  | `() => void`           | `null`  | Called when the timer stops.                         |\n| `onend`   | `() => void`           | `null`  | Called when the timer ends naturally.                |\n\n### Events\n\nYou can register handlers with `on()` using either `\"event\"` or `\"onevent\"` names.\n\n- `start` or `onstart`\n- `tick` or `ontick`\n- `pause` or `onpause`\n- `stop` or `onstop`\n- `end` or `onend`\n\n### Methods\n\n- `start(durationSeconds?)` Start a countdown in seconds. If omitted, it reuses the last duration (e.g., resume after pause).\n- `pause()` Pause a running timer and preserve remaining time (no-op if not started).\n- `stop()` Stop the timer and reset remaining time to 0 (no-op if not started or paused).\n- `getDuration()` Return remaining time in milliseconds.\n- `getStatus()` Return `\"initialized\" | \"started\" | \"paused\" | \"stopped\"`.\n- `options(options | key, value)` Set one or many options.\n- `on(event, handler)` Register an event handler.\n- `off(event)` Remove handlers; `\"all\"` resets everything to defaults.\n- `measureStart(label?)` Start a measurement timer (label optional).\n- `measureStop(label?)` Stop the measurement and return elapsed ms.\n\n### Behavior notes\n\n- `start()` without a valid duration only works when resuming a paused timer.\n- `start()` while already started is a no-op.\n- `getDuration()` returns `0` when the timer is not started or paused.\n\n## TypeScript\n\nType definitions are shipped in `dist/timer.d.ts`.\n\n```ts\nimport Timer from \"timer.js\";\n\nconst timer = new Timer({\n  ontick: (ms) => console.log(ms),\n});\n```\n\n## Releasing\n\nReleases are automated via `semantic-release` on the `master` branch and require\nconventional commit messages. Commit hooks are enforced via Husky + lint-staged.\n\n## Contributing\n\nIssues and pull requests are welcome. Feel free to create an issue or a pull request.\nA minimal repro when creating an issue is much appreciated.\n\n## License\n\n[MIT](LICENCE)\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import js from \"@eslint/js\";\nimport globals from \"globals\";\nimport { defineConfig } from \"eslint/config\";\n\nexport default defineConfig([\n  {\n    files: [\"**/*.{js,mjs,cjs}\"],\n    plugins: { js },\n    extends: [\"js/recommended\"],\n    languageOptions: { globals: { ...globals.browser, ...globals.node } },\n  },\n]);\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"timer.js\",\n  \"version\": \"1.0.4\",\n  \"description\": \"Simple and lighweight but powerfull eventdriven JavaScript timer\",\n  \"main\": \"dist/timer.js\",\n  \"types\": \"dist/timer.d.ts\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git@github.com:husa/timer.js.git\"\n  },\n  \"bugs\": {\n    \"url\": \"http://github.com/husa/timer.js/issues\"\n  },\n  \"homepage\": \"https://github.com/husa/timer.js\",\n  \"keywords\": [\n    \"timer.js\",\n    \"timer\",\n    \"time\",\n    \"measure\",\n    \"start\",\n    \"stop\",\n    \"pause\"\n  ],\n  \"author\": \"husa\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"lint\": \"prettier --check src && eslint src\",\n    \"test\": \"node --test src/timer.test.js\",\n    \"prepare\": \"husky\",\n    \"build\": \"mkdir -p dist && cp src/timer.js dist/timer.js && cp src/timer.d.ts dist/timer.d.ts\"\n  },\n  \"lint-staged\": {\n    \"*.{js,jsx,ts,tsx}\": [\n      \"prettier --write\",\n      \"eslint --fix\"\n    ],\n    \"*.{css,scss,md}\": \"prettier --write\"\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ]\n  },\n  \"release\": {\n    \"branches\": [\n      \"master\"\n    ],\n    \"plugins\": [\n      \"@semantic-release/commit-analyzer\",\n      \"@semantic-release/release-notes-generator\",\n      \"@semantic-release/npm\",\n      \"@semantic-release/github\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"^20.3.1\",\n    \"@commitlint/config-conventional\": \"^20.3.1\",\n    \"@eslint/js\": \"^9.39.2\",\n    \"eslint\": \"^9.39.2\",\n    \"globals\": \"^17.2.0\",\n    \"husky\": \"^9.1.7\",\n    \"lint-staged\": \"^16.2.7\",\n    \"prettier\": \"^3.8.1\"\n  }\n}\n"
  },
  {
    "path": "src/timer.d.ts",
    "content": "declare namespace Timer {\n  /** Timer state values. */\n  type Status = \"initialized\" | \"started\" | \"paused\" | \"stopped\";\n\n  /** Called when the timer starts. Receives remaining duration in ms. */\n  type StartHandler = (durationMs: number) => void;\n  /** Called on each tick. Receives remaining duration in ms. */\n  type TickHandler = (remainingMs: number) => void;\n  /** Called when the timer pauses. */\n  type PauseHandler = () => void;\n  /** Called when the timer stops. */\n  type StopHandler = () => void;\n  /** Called when the timer ends naturally. */\n  type EndHandler = () => void;\n\n  interface Options {\n    /** Tick interval in seconds. */\n    tick?: number;\n    /** Start callback. */\n    onstart?: StartHandler | null;\n    /** Tick callback. */\n    ontick?: TickHandler | null;\n    /** Pause callback. */\n    onpause?: PauseHandler | null;\n    /** Stop callback. */\n    onstop?: StopHandler | null;\n    /** End callback. */\n    onend?: EndHandler | null;\n  }\n\n  /** Event names without the \"on\" prefix. */\n  type EventName = \"start\" | \"tick\" | \"pause\" | \"stop\" | \"end\";\n  /** Event names with the \"on\" prefix. */\n  type EventKey = \"onstart\" | \"ontick\" | \"onpause\" | \"onstop\" | \"onend\";\n}\n\n/** Lightweight countdown timer. */\ninterface Timer {\n  /**\n   * Start the timer.\n   * @param durationSeconds Optional duration in seconds. If omitted, reuses the last duration.\n   * @example\n   * const timer = Timer();\n   * timer.start(5);\n   */\n  start(durationSeconds?: number): this;\n  /** Pause the timer, preserving remaining time. */\n  pause(): this;\n  /** Stop the timer and reset remaining time to 0. */\n  stop(): this;\n  /** Remaining time in milliseconds. */\n  getDuration(): number;\n  /** Current lifecycle status. */\n  getStatus(): Timer.Status;\n\n  /** Get or set options. */\n  options(): this;\n  /** Set options in bulk. */\n  options(options: Timer.Options): this;\n  /** Set a single option. */\n  options<K extends keyof Timer.Options>(\n    option: K,\n    value: Timer.Options[K],\n  ): this;\n  /** Set a custom option by name. */\n  options(option: string, value: unknown): this;\n\n  /** Register a start handler. */\n  on(event: \"start\" | \"onstart\", handler: Timer.StartHandler): this;\n  /** Register a tick handler. */\n  on(event: \"tick\" | \"ontick\", handler: Timer.TickHandler): this;\n  /** Register a pause handler. */\n  on(event: \"pause\" | \"onpause\", handler: Timer.PauseHandler): this;\n  /** Register a stop handler. */\n  on(event: \"stop\" | \"onstop\", handler: Timer.StopHandler): this;\n  /** Register an end handler. */\n  on(event: \"end\" | \"onend\", handler: Timer.EndHandler): this;\n  /** Register a handler for a custom event key. */\n  on(event: string, handler: (...args: unknown[]) => void): this;\n\n  /** Remove all handlers. */\n  off(): this;\n  /** Remove handlers for a specific event. */\n  off(event: \"all\" | Timer.EventName | Timer.EventKey): this;\n  /** Remove handlers for a custom event key. */\n  off(event: string): this;\n\n  /** Start a measurement timer for the given label. */\n  measureStart(label?: string): this;\n  /** Stop a measurement timer and return elapsed ms. */\n  measureStop(label?: string): number;\n}\n\n/** Timer constructor. */\ndeclare const Timer: {\n  /** Create a new timer instance. */\n  new (options?: Timer.Options): Timer;\n  /** Create a new timer instance without `new`. */\n  (options?: Timer.Options): Timer;\n  prototype: Timer;\n};\n\nexport = Timer;\nexport as namespace Timer;\n"
  },
  {
    "path": "src/timer.js",
    "content": "(function (root, factory) {\n  \"use strict\";\n  // eslint-disable-next-line no-undef\n  if (typeof define === \"function\" && define.amd) define([], factory);\n  else if (typeof exports === \"object\") module.exports = factory();\n  else root.Timer = factory();\n})(this, function () {\n  \"use strict\";\n\n  var defaultOptions = {\n    tick: 1,\n    onstart: null,\n    ontick: null,\n    onpause: null,\n    onstop: null,\n    onend: null,\n  };\n\n  var Timer = function (options) {\n    if (!(this instanceof Timer)) return new Timer(options);\n    this._ = {\n      id: +new Date(),\n      options: {},\n      duration: 0,\n      status: \"initialized\",\n      start: 0,\n      measures: [],\n    };\n    for (var prop in defaultOptions)\n      this._.options[prop] = defaultOptions[prop];\n    this.options(options);\n  };\n\n  Timer.prototype.start = function (duration) {\n    // timer already running\n    if (this._.timeout && this._.status === \"started\") return this;\n    const isDurationValid = typeof duration === \"number\" && duration > 0;\n    // no valid duration provided and timer isn't \"on pause\"\n    if (!isDurationValid && !this._.duration) return this;\n    this._.duration = duration * 1000 || this._.duration;\n    this._.timeout = setTimeout(end.bind(this), this._.duration);\n    if (typeof this._.options.ontick === \"function\")\n      this._.interval = setInterval(\n        function () {\n          trigger.call(this, \"ontick\", this.getDuration());\n        }.bind(this),\n        +this._.options.tick * 1000,\n      );\n    this._.start = +new Date();\n    this._.status = \"started\";\n    trigger.call(this, \"onstart\", this.getDuration());\n    return this;\n  };\n\n  Timer.prototype.pause = function () {\n    if (this._.status !== \"started\") return this;\n    this._.duration -= +new Date() - this._.start;\n    clear.call(this, false);\n    this._.status = \"paused\";\n    trigger.call(this, \"onpause\");\n    return this;\n  };\n\n  Timer.prototype.stop = function () {\n    if (!/started|paused/.test(this._.status)) return this;\n    clear.call(this, true);\n    this._.status = \"stopped\";\n    trigger.call(this, \"onstop\");\n    return this;\n  };\n\n  Timer.prototype.getDuration = function () {\n    if (this._.status === \"started\")\n      return this._.duration - (+new Date() - this._.start);\n    if (this._.status === \"paused\") return this._.duration;\n    return 0;\n  };\n\n  Timer.prototype.getStatus = function () {\n    return this._.status;\n  };\n\n  Timer.prototype.options = function (option, value) {\n    if (option && value) this._.options[option] = value;\n    if (!value && typeof option === \"object\")\n      for (var prop in option)\n        if (Object.prototype.hasOwnProperty.call(this._.options, prop))\n          this._.options[prop] = option[prop];\n    return this;\n  };\n\n  Timer.prototype.on = function (option, value) {\n    if (typeof option !== \"string\" || typeof value !== \"function\") return this;\n    if (!/^on/.test(option)) option = \"on\" + option;\n    if (Object.prototype.hasOwnProperty.call(this._.options, option))\n      this._.options[option] = value;\n    return this;\n  };\n\n  Timer.prototype.off = function (option) {\n    if (typeof option !== \"string\") return this;\n    option = option.toLowerCase();\n    if (option === \"all\") {\n      this._.options = defaultOptions;\n      return this;\n    }\n    if (!/^on/.test(option)) option = \"on\" + option;\n    if (Object.prototype.hasOwnProperty.call(this._.options, option))\n      this._.options[option] = defaultOptions[option];\n    return this;\n  };\n\n  Timer.prototype.measureStart = function (label) {\n    this._.measures[label || \"\"] = +new Date();\n    return this;\n  };\n\n  Timer.prototype.measureStop = function (label) {\n    return +new Date() - this._.measures[label || \"\"];\n  };\n\n  function end() {\n    clear.call(this);\n    this._.status = \"stopped\";\n    trigger.call(this, \"onend\");\n  }\n\n  function trigger(event) {\n    var callback = this._.options[event],\n      args = [].slice.call(arguments, 1);\n    typeof callback === \"function\" && callback.apply(this, args);\n  }\n\n  function clear(clearDuration) {\n    clearTimeout(this._.timeout);\n    clearInterval(this._.interval);\n    if (clearDuration === true) this._.duration = 0;\n  }\n\n  return Timer;\n});\n"
  },
  {
    "path": "src/timer.test.js",
    "content": "const assert = require(\"node:assert/strict\");\nconst { describe, it, beforeEach, afterEach, mock } = require(\"node:test\");\nconst Timer = require(\"./timer\");\n\ndescribe(\"Timer\", () => {\n  let timer;\n  let start;\n  let stop;\n  let pause;\n  let end;\n  let tick;\n\n  beforeEach(() => {\n    timer = new Timer();\n    start = mock.fn();\n    pause = mock.fn();\n    stop = mock.fn();\n    tick = mock.fn();\n    end = mock.fn();\n\n    mock.timers.enable({\n      apis: [\"setTimeout\", \"setInterval\", \"Date\"],\n    });\n    mock.timers.setTime(0);\n  });\n\n  afterEach(() => {\n    mock.timers.reset();\n  });\n\n  it(\"should be available as global\", () => {\n    assert.ok(Timer);\n  });\n\n  describe(\"#constructor\", () => {\n    it('should self invoke without \"new\" keyword', () => {\n      const timer = new Timer();\n      const timer2 = Timer();\n\n      assert.ok(timer instanceof Timer);\n      assert.ok(timer2 instanceof Timer);\n    });\n\n    it(\"should accept object as arguments\", () => {\n      timer = new Timer({\n        onstart: start,\n        onpause: pause,\n        onstop: stop,\n      });\n\n      timer.start();\n      assert.equal(start.mock.callCount(), 0);\n      timer.start(10);\n      assert.equal(start.mock.callCount(), 1);\n      timer.pause();\n      assert.equal(pause.mock.callCount(), 1);\n      timer.stop();\n      assert.equal(stop.mock.callCount(), 1);\n    });\n  });\n\n  describe(\"#getStatus\", () => {\n    it(\"should always be string\", () => {\n      assert.equal(typeof timer.getStatus(), \"string\");\n    });\n\n    it(\"should be valid status\", () => {\n      const match = /^(initialized|started|paused|stopped|finished)$/;\n      assert.match(timer.getStatus(), match);\n    });\n  });\n\n  describe(\"#getDuration\", () => {\n    it(\"should return 0 if timer isn't started or paused\", () => {\n      assert.equal(timer.getDuration(), 0);\n      timer.start(10);\n      assert.equal(timer.getDuration(), 10000);\n      timer.pause();\n      assert.equal(timer.getDuration(), 10000);\n      timer.stop();\n      assert.equal(timer.getDuration(), 0);\n    });\n\n    it(\"should return actual value\", () => {\n      timer.start(10);\n      mock.timers.tick(100);\n      assert.equal(timer.getDuration(), 9900);\n      mock.timers.tick(1100);\n      assert.equal(timer.getDuration(), 8800);\n      timer.pause();\n      mock.timers.tick(100);\n      assert.equal(timer.getDuration(), 8800);\n      timer.start();\n      mock.timers.tick(100);\n      assert.equal(timer.getDuration(), 8700);\n    });\n  });\n\n  describe(\"#start\", () => {\n    it(\"should not change status if no arguments\", () => {\n      timer.start();\n      assert.equal(timer.getStatus(), \"initialized\");\n      timer.start(10);\n      assert.equal(timer.getStatus(), \"started\");\n      timer.start();\n      assert.equal(timer.getStatus(), \"started\");\n    });\n\n    it(\"should ignore zero duration when no previous duration\", () => {\n      timer.on(\"start\", start);\n      timer.on(\"end\", end);\n      timer.start(0);\n      assert.equal(timer.getStatus(), \"initialized\");\n      assert.equal(start.mock.callCount(), 0);\n      mock.timers.tick(1000);\n      assert.equal(end.mock.callCount(), 0);\n    });\n\n    it(\"should treat negative duration the same as zero\", () => {\n      timer.on(\"start\", start);\n      timer.on(\"end\", end);\n      timer.start(-5);\n      assert.equal(timer.getStatus(), \"initialized\");\n      assert.equal(start.mock.callCount(), 0);\n      mock.timers.tick(1000);\n      assert.equal(end.mock.callCount(), 0);\n    });\n\n    it('should change status to \"started\" if valid arguments', () => {\n      assert.equal(timer.getStatus(), \"initialized\");\n      timer.start(10);\n      assert.equal(timer.getStatus(), \"started\");\n    });\n\n    it('should trigger \"onstart\" callback', () => {\n      timer.on(\"start\", start);\n      timer.start(1);\n      assert.equal(start.mock.callCount(), 1);\n      assert.deepEqual(start.mock.calls[0].arguments, [1000]);\n    });\n\n    it(\"should resume timer after pause\", () => {\n      timer.on(\"end\", end);\n      timer.start(5);\n      mock.timers.tick(1000);\n      timer.pause();\n      assert.equal(timer.getStatus(), \"paused\");\n      timer.start();\n      assert.equal(timer.getStatus(), \"started\");\n      mock.timers.tick(3900);\n      assert.equal(timer.getDuration(), 100);\n      assert.equal(timer.getStatus(), \"started\");\n      mock.timers.tick(101);\n      assert.equal(timer.getStatus(), \"stopped\");\n      assert.equal(end.mock.callCount(), 1);\n    });\n\n    it(\"should restart timer if argument provided after pause\", () => {\n      timer.on(\"end\", end);\n      timer.start(5);\n      mock.timers.tick(1000);\n      timer.pause();\n      assert.equal(timer.getStatus(), \"paused\");\n      timer.start(10);\n      assert.equal(timer.getDuration(), 10000);\n      mock.timers.tick(4001);\n      assert.equal(end.mock.callCount(), 0);\n      assert.equal(timer.getDuration(), 5999);\n      mock.timers.tick(6000);\n      assert.equal(end.mock.callCount(), 1);\n    });\n\n    it('should ignore start calls if already \"started\"', () => {\n      timer.on(\"start\", start);\n      timer.start(5);\n      assert.equal(timer.getDuration(), 5000);\n      assert.equal(start.mock.callCount(), 1);\n      // any new .start calls are ignored\n      timer.start(10);\n      assert.equal(start.mock.callCount(), 1);\n      assert.equal(timer.getDuration(), 5000);\n      timer.start([{ what: \"ever\" }]);\n      assert.equal(start.mock.callCount(), 1);\n      assert.equal(timer.getDuration(), 5000);\n    });\n  });\n\n  describe(\"#pause\", () => {\n    it(\"should return if timer hasn't started\", () => {\n      timer.on(\"pause\", pause);\n      timer.pause();\n      assert.equal(timer.getStatus(), \"initialized\");\n      assert.equal(pause.mock.callCount(), 0);\n    });\n\n    it('should change status to \"paused\"', () => {\n      timer.start(1);\n      timer.pause();\n      assert.equal(timer.getStatus(), \"paused\");\n    });\n\n    it('should trigger \"onpause\" callback', () => {\n      timer.on(\"pause\", pause);\n      timer.start(1);\n      timer.pause();\n      assert.equal(pause.mock.callCount(), 1);\n      timer.pause();\n      assert.equal(pause.mock.callCount(), 1);\n    });\n  });\n\n  describe(\"#stop\", () => {\n    it(\"should return if timer hasn't started\", () => {\n      timer.on(\"stop\", stop);\n      timer.stop();\n      assert.equal(timer.getStatus(), \"initialized\");\n      assert.equal(stop.mock.callCount(), 0);\n    });\n\n    it('should change status to \"stopped\" after start', () => {\n      timer.on(\"stop\", stop);\n      timer.start(1);\n      timer.stop();\n      assert.equal(timer.getStatus(), \"stopped\");\n      assert.equal(stop.mock.callCount(), 1);\n    });\n\n    it('should change status to \"stopped\" after pause', () => {\n      timer.on(\"stop\", stop);\n      timer.start(1);\n      timer.pause();\n      timer.stop();\n      assert.equal(timer.getStatus(), \"stopped\");\n      assert.equal(stop.mock.callCount(), 1);\n    });\n\n    it('should trigger \"onstop\" callback', () => {\n      timer.on(\"stop\", stop);\n      timer.start(1);\n      timer.stop();\n      assert.equal(stop.mock.callCount(), 1);\n      timer.stop();\n      assert.equal(stop.mock.callCount(), 1);\n    });\n  });\n\n  describe(\"#on\", () => {\n    it(\"should attach start callback\", () => {\n      timer.on(\"start\", start);\n      timer.start(1);\n      assert.equal(start.mock.callCount(), 1);\n    });\n\n    it(\"should attach pause callback\", () => {\n      timer.on(\"pause\", pause);\n      timer.start(1);\n      timer.pause();\n      assert.equal(pause.mock.callCount(), 1);\n    });\n\n    it(\"should attach stop callback\", () => {\n      timer.on(\"stop\", stop);\n      timer.start(1);\n      timer.stop();\n      assert.equal(stop.mock.callCount(), 1);\n    });\n\n    it(\"should attach end callback\", () => {\n      timer.on(\"end\", end);\n      timer.start(1);\n      mock.timers.tick(1001);\n      assert.equal(end.mock.callCount(), 1);\n    });\n\n    it(\"should attach tick callback\", () => {\n      timer.on(\"tick\", tick);\n      timer.start(2);\n      mock.timers.tick(1001);\n      assert.equal(tick.mock.callCount(), 1);\n    });\n\n    it('should accept options with/without \"on\"', () => {\n      timer.on(\"tick\", tick);\n      timer.on(\"onstart\", start);\n      timer.on(\"onstop\", stop);\n      timer.start(2);\n      mock.timers.tick(1001);\n      timer.stop();\n      assert.equal(start.mock.callCount(), 1);\n      assert.equal(tick.mock.callCount(), 1);\n      assert.equal(stop.mock.callCount(), 1);\n    });\n  });\n\n  describe(\"#off\", () => {\n    beforeEach(() => {\n      timer.on(\"tick\", tick);\n      timer.on(\"onstart\", start);\n      timer.on(\"stop\", stop);\n    });\n\n    it(\"should remove callbacks\", () => {\n      timer.off(\"tick\");\n      timer.off(\"onstart\");\n      timer.off(\"stop\");\n      timer.start(2);\n      mock.timers.tick(1900);\n      timer.stop();\n      assert.equal(start.mock.callCount(), 0);\n      assert.equal(tick.mock.callCount(), 0);\n      assert.equal(stop.mock.callCount(), 0);\n    });\n\n    it('should remove all callbacks if \"all\" passed', () => {\n      timer.off(\"all\");\n      timer.start(2);\n      mock.timers.tick(1900);\n      timer.stop();\n      assert.equal(start.mock.callCount(), 0);\n      assert.equal(tick.mock.callCount(), 0);\n      assert.equal(stop.mock.callCount(), 0);\n    });\n  });\n\n  describe(\"#callbacks execution\", () => {\n    beforeEach(() => {\n      timer.options({\n        onstart: start,\n        ontick: tick,\n        onpause: pause,\n        onend: end,\n        onstop: stop,\n      });\n    });\n\n    it('should trigger \"tick\" every second', () => {\n      timer.start(3);\n      mock.timers.tick(1001);\n      assert.equal(tick.mock.callCount(), 1);\n      mock.timers.tick(1000);\n      assert.equal(tick.mock.callCount(), 2);\n      mock.timers.tick(1000);\n      assert.equal(tick.mock.callCount(), 2);\n    });\n\n    it('should trigger \"end\"', () => {\n      timer.start(2);\n      mock.timers.tick(2001);\n      assert.equal(end.mock.callCount(), 1);\n    });\n\n    it('should not trigger \"end\" if stopped', () => {\n      timer.start(2);\n      mock.timers.tick(1900);\n      timer.stop();\n      mock.timers.tick(1000);\n      assert.equal(end.mock.callCount(), 0);\n      assert.equal(stop.mock.callCount(), 1);\n    });\n  });\n\n  describe(\"#chaining\", () => {\n    it(\"should chain any way\", () => {\n      assert.doesNotThrow(() => {\n        timer\n          .pause()\n          .stop()\n          .start()\n          .start(20)\n          .stop()\n          .pause()\n          .start()\n          .on()\n          .off()\n          .options()\n          .stop();\n      });\n    });\n  });\n});\n"
  }
]