Skip to content

Commit

Permalink
v2.3.0 (#100)
Browse files Browse the repository at this point in the history
* Fix countdown status, add stop functionality

* Refactorings, doc updates

* Add overtime prop, doc updates

* Add cypress for e2e testing (#102)

* Update travis cfg

* Update test cfgs

* Update travis cfg

* Update e2e refs

* Update dist and clean cmds
  • Loading branch information
ndresx authored Oct 13, 2020
1 parent 91bc40f commit 21d1dc9
Show file tree
Hide file tree
Showing 23 changed files with 5,967 additions and 2,145 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
dist/
node_modules/
coverage/
.cache/
*.DS_Store
*.log
*.idea
Expand Down
3 changes: 3 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
config/
coverage/
cypress/
examples/
node_modules/
src/
.cache/
*.config.js
*.idea
*.lock
Expand All @@ -11,3 +13,4 @@ src/
*.test.ts
*.test.d.ts
*.vscode
cypress.json
14 changes: 14 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@ language: node_js
node_js:
- 'node'

addons:
apt:
packages:
# Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves
- libgconf-2-4

branches:
only:
- master

cache:
directories:
- node_modules
# Cypress binary
- ~/.cache

jobs:
include:
- stage: 'Lint'
Expand All @@ -16,6 +28,8 @@ jobs:
- stage: 'Tests'
name: 'Unit Tests'
script: yarn test && cat ./coverage/lcov.info | node_modules/.bin/coveralls --verbose
- name: 'E2E Tests'
script: yarn build && yarn test:e2e
- stage: 'Build'
name: 'Package'
script: yarn build
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ Make sure that all tests are passing and that the code coverage is close to 100%
```sh
yarn test
```

For End-to-End tests, please run the following command:
```sh
yarn test:e2e
```
89 changes: 66 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,14 @@ ReactDOM.render(
|[**intervalDelay**](#intervaldelay)|`number`|`1000`|Interval delay in milliseconds|
|[**precision**](#precision)|`number`|`0`|The precision on a millisecond basis|
|[**autoStart**](#autostart)|`boolean`|`true`|Countdown auto-start option|
|[**overtime**](#overtime) |`boolean`|`false`|Counts down to infinity|
|[**children**](#children)|`any`|`null`|A React child for the countdown's completed state|
|[**renderer**](#renderer)|`function`|`undefined`|Custom renderer callback|
|[**now**](#now)|`function`|`Date.now`|Alternative handler for the current date|
|[**onMount**](#onmount)|`function`|`undefined`|Callback when component mounts|
|[**onStart**](#onstart)|`function`|`undefined`|Callback when countdown starts|
|[**onPause**](#onpause)|`function`|`undefined`|Callback when countdown pauses|
|[**onStop**](#onstop)|`function`|`undefined`|Callback when countdown stops|
|[**onTick**](#ontick)|`function`|`undefined`|Callback on every interval tick (`controlled` = `false`)|
|[**onComplete**](#oncomplete)|`function`|`undefined`|Callback when countdown ends|

Expand Down Expand Up @@ -180,22 +182,43 @@ In certain cases, you might want to base off the calculations on a millisecond b
### `autoStart`
Defines whether the countdown should start automatically or not. Defaults to `true`.

### `overtime`
Defines whether the countdown can go into overtime by extending its lifetime past the targeted endpoint. Defaults to `false`.

When set to `true`, the countdown timer won't stop when hitting 0, but instead becomes negative and continues to run unless paused/stopped. The [`onComplete`](#oncomplete) callback would still get triggered when the initial countdown phase completes.

> Please note that the [`children`](#children) prop will be ignored if `overtime` is `true`.
### `children`
This component also considers the child that may live within the `<Countdown></Countdown>` element, which, in case it's available, replaces the countdown's component state once it's complete. Moreover, an additional prop called `countdown` is set and contains data similar to what the [`renderer`](#renderer) callback would receive. Here's an [example](#using-a-react-child-for-the-completed-state) that showcases its usage.

_Please note that once a custom `renderer` is defined, the [`children`](#children) prop will be ignored._
> Please note that the [`children`](#children) prop will be ignored if a custom [`renderer`](#renderer) is defined.
### `renderer`
The component's raw render output is kept very simple.

For more advanced countdown displays, a custom `renderer` callback can be defined to return a new React element. It receives the following [render props](#render-props) as the first argument.

#### Render Props

The render props object consists of the current time delta object, the countdown's [`api`](#api-reference), the component [`props`](#props), and last but not least, a [`formatted`](#formattimedelta) object.

<a name="renderer"></a>
### `renderer(props)`
The component's render output is very simple and depends on [`daysInHours`](#daysinhours): _{days}:{hours}:{minutes}:{seconds}_.
If this doesn't fit your needs, a custom `renderer` callback can be defined to return a new React element. It receives an argument that consists of a time delta object (incl. `formatted` values) to build your own representation of the countdown.
```js
{ total, days, hours, minutes, seconds, milliseconds, completed }
{
total: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
completed: true,
api: { ... },
props: { ... },
formatted: { ... }
}
```

The render props also contain the countdown's [`API`](#api-reference) as `api` prop as well as the passed in component [`props`](#props).

_Please note that once a custom `renderer` is defined, the [`children`](#children) prop will be ignored._
> Please note that a defined custom [`renderer`](#renderer) will ignore the [`children`](#children) prop.
### `now`
If the current date and time (determined via a reference to `Date.now`) is not the right thing to compare with for you, a reference to a custom function that returns a similar dynamic value could be provided as an alternative.
Expand All @@ -209,6 +232,9 @@ If the current date and time (determined via a reference to `Date.now`) is not t
### `onPause`
`onPause` is a callback and triggered every time the countdown is paused. It receives the time delta object, which is returned by [`calcTimeDelta`](#calctimedelta).

### `onStop`
`onStop` is a callback and triggered every time the countdown is stopped. It receives the time delta object, which is returned by [`calcTimeDelta`](#calctimedelta).

### `onTick`
`onTick` is a callback and triggered every time a new period is started, based on what the [`intervalDelay`](#intervaldelay)'s value is. It only gets triggered when the countdown's [`controlled`](#controlled) prop is set to `false`, meaning that the countdown has full control over its interval. It receives the time delta object, which is returned by [`calcTimeDelta`](#calctimedelta).

Expand All @@ -220,17 +246,25 @@ If the current date and time (determined via a reference to `Date.now`) is not t
The countdown component exposes a simple API through the `getApi()` function that can be accessed via component `ref`. It is also part (`api`) of the render props passed into [`renderer`](#renderer) if needed.

### `start()`
Starts the countdown in case it is paused or needed when [`autoStart`](#autostart) is set to `false`.
Starts the countdown in case it is paused/stopped or needed when [`autoStart`](#autostart) is set to `false`.

### `pause()`
Pauses the running countdown. This only works as expected if the [`controlled`](#controlled) prop is set to `false` because [`calcTimeDelta`](#calctimedelta) does calculate this offset time internally.
Pauses the running countdown. This only works as expected if the [`controlled`](#controlled) prop is set to `false` because [`calcTimeDelta`](#calctimedelta) calculates an offset time internally.

### `stop()`
Stops the countdown. This only works as expected if the [`controlled`](#controlled) prop is set to `false` because [`calcTimeDelta`](#calctimedelta) calculates an offset time internally.

### `isPaused()`
Returns a `boolean` for whether the countdown has been paused or not.

### `isStopped()`
Returns a `boolean` for whether the countdown has been stopped or not.

### `isCompleted()`
Returns a `boolean` for whether the countdown has been completed or not.

> Please note that this will always return `false` if [`overtime`](#overtime) is `true`. Nevertheless, an into overtime running countdown's completed state can still be looking at the time delta object's `completed` value.
## Helpers

This module also exports three simple helper functions, which can be utilized to build your own countdown custom [`renderer`](#renderer).
Expand All @@ -252,7 +286,7 @@ const renderer = ({ hours, minutes, seconds }) => (

<a name="calctimedelta"></a>
### `calcTimeDelta(date, [options])`
`calcTimeDelta` calculates the time difference between a given end [`date`](#date) and the current date (`now`). It returns, similar to the [`renderer`](#renderer) callback, a custom object (also referred to as **countdown time delta object**) with the following time related data:
`calcTimeDelta` calculates the time difference between a given end [`date`](#date) and the current date (`now`). It returns, similar to the [`renderer`](#renderer) callback, a custom object (also referred to as **countdown time delta object**) with the following time-related data:

```js
{ total, days, hours, minutes, seconds, milliseconds, completed }
Expand All @@ -263,34 +297,43 @@ This function accepts two arguments in total; only the first one is required.
**`date`**
Date or timestamp representation of the end date. See [`date`](#date) prop for more details.

The second argument (`options`) could be an optional object consisting of the following optional keys.
**`options`** The second argument consists of the following optional keys.

**`now = Date.now`**
- **`now = Date.now`**
Alternative function for returning the current date, also see [`now`](#now).

**`precision = 0`**
- **`precision = 0`**
The [`precision`](#precision) on a millisecond basis.

**`controlled = false`**
Defines whether the calculated value is already provided as the time difference or not.
- **`controlled = false`**
Defines whether the calculated value is provided in a [`controlled`](#controlled) environment as the time difference or not.

**`offsetTime = 0`**
- **`offsetTime = 0`**
Defines the offset time that gets added to the start time; only considered if controlled is false.

### `formatTimeDelta(delta, [options])`
- **`overtime = false`**
Defines whether the time delta can go into [`overtime`](#overtime) and become negative or not. When set to `true`, the `total` could become negative at which point `completed` will still be set to `true`.

<a name="formattimedelta"></a>
### `formatTimeDelta(timeDelta, [options])`
`formatTimeDelta` formats a given countdown time delta object. It returns the formatted portion of it, equivalent to:

```js
{ days, hours, minutes, seconds }
{
days: '00',
hours: '00',
minutes: '00',
seconds: '00',
}
```

This function accepts two arguments in total; only the first one is required.

**`delta`**
Time delta object, e.g.: returned by [`calcTimeDelta`](#calctimedelta).
**`timeDelta`**
Time delta object, e.g., returned by [`calcTimeDelta`](#calctimedelta).

**`options`**
The `options` object consists of the following three component props and is used to customize the formatting of the delta object:
The `options` object consists of the following three component props and is used to customize the time delta object's formatting:
* [`daysInHours`](#daysinhours)
* [`zeroPadTime`](#zeropadtime)
* [`zeroPadDays`](#zeropaddays)
Expand Down
5 changes: 5 additions & 0 deletions cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"baseUrl": "http://localhost:1234",
"screenshotOnRunFailure": false,
"video": false
}
5 changes: 5 additions & 0 deletions cypress/fixtures/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
121 changes: 121 additions & 0 deletions cypress/integration/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
describe('<Countdown />', () => {
const ALIAS = 'countdown';

const cyGetAs = (selector: string) => cy.get(selector).as(ALIAS);
const cyGet = (alias = ALIAS) => cy.get(`@${alias}`);

beforeEach(() => {
cy.clock();
cy.visit('/');
});

describe('Basic Usage', () => {
it('should render final state', () => {
cyGetAs('#basic-usage');

for (let i = 5; i > 0; i--) {
cyGet().contains(`00:00:00:0${i}`);
if (i > 0) cy.tick(1000);
}

cyGet().contains('00:00:00:00');
});

it('should render final state when already in the past', () => {
cyGetAs('#basic-usage-past');
cyGet().contains('00:00:00:00');

cy.tick(1000);
cyGet().contains('00:00:00:00');
});
});

describe('Custom & Conditional Rendering', () => {
it('should render completed state', () => {
cyGetAs('#children-completionist');

for (let i = 5; i > 0; i--) {
cyGet().contains(`00:00:00:0${i}`);
if (i > 0) cy.tick(1000);
}

cyGet().contains('You are good to go!');
});
});

describe('Countdown (overtime)', () => {
it('should render infinity', () => {
cyGetAs('#overtime');

for (let i = 5; i > -5; i--) {
cyGet().contains(`${i < 0 ? '-' : ''}00:00:00:0${Math.abs(i)}`);
cy.tick(1000);
}

cy.tick(5000);
cyGet().contains('-00:00:00:10');
});
});

describe('Countdown API', () => {
beforeEach(() => {
cyGetAs('#api');
cyGet().contains('00:00:10');

cyGet()
.find('button')
.contains('Start')
.as('StartBtn')
.click()
.should('have.be.disabled');
});

it('should click the "Start" button and count down 5s', () => {
cy.tick(5000);
cyGet().contains('00:00:05');
});

it('should click the "Start" (10s) => "Pause" (5s) => "Start" (5s) => "Stop" (3s) buttons => 10s', () => {
cy.tick(5000);
cyGet().contains('00:00:05');

cyGet()
.find('button')
.contains('Pause')
.as('PauseBtn')
.click()
.should('have.be.disabled');

cy.tick(2000);
cyGet().contains('00:00:05');

cyGet('StartBtn').click();

cy.tick(2000);
cyGet().contains('00:00:03');

cyGet()
.find('button')
.contains('Stop')
.as('StopBtn')
.click()
.should('have.be.disabled');

cyGet().contains('00:00:10');
});

it('should reset the countdown at 4s => 10s and count down to 7s', () => {
cy.tick(6000);
cyGet().contains('00:00:04');

cyGet()
.find('button')
.contains('Reset')
.as('ResetBtn')
.click();

cy.tick(3000);
cyGet().contains('00:00:07');
});
});
});
21 changes: 21 additions & 0 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
Loading

0 comments on commit 21d1dc9

Please sign in to comment.