Skip to content

Commit

Permalink
feat: write the library
Browse files Browse the repository at this point in the history
  • Loading branch information
alexlafroscia committed Mar 31, 2020
0 parents commit 5b87e87
Show file tree
Hide file tree
Showing 10 changed files with 3,794 additions and 0 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Verify

on: [push]

jobs:
formatting:
name: Check Formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: volta-cli/action@v1
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install
- run: yarn prettier --check .

test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: volta-cli/action@v1
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install
- run: yarn build
- run: yarn test
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Pika Output
pkg/

# Dependencies
node_modules/
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Pika Output
pkg/

# Dependencies
node_modules/

43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# `qunit-wait-for`

> Wait for a QUnit Assertion
## Installation

Install the dependency

```
yarn add -D qunit-wait-for
```

and install the helper in your JavaScript code.

```javascript
import QUnit from 'qunit';
import { installWaitFor } from 'qunit-wait-for';

installWaitFor(QUnit)
```

If you're using Ember, the right place for that snippet is your `tests/test-helper.js`.

## Usage

`qunit-wait-for` allows you to wait for an assertion that might not pass _right now_ but will pass _soon_. The idea is that you write a test where your assertions converge on the desired state of your application, checking over and over again until the criteria are either met or a timeout is reached. This allows you to write tests that can be resilient to slight timing issues (which is common in UI testing) without needing to add explicit timeouts to your tests.

To use it, pass a callback to `assert.waitFor` and within in place your normal assertion:

```javascript
await assert.waitFor(() => {
assert.dom('[data-test-my-element]').exists();
});
```

The resulting promise resolve when either the condition is met or the timeout is reached; this promise should be `await`-ed so ensure one of those two things has happened before moving on.

## Prior Art

* [Converging on a Condition in QUnit](https://alexlafroscia.com/qunit-assert-converge-on/)
A blog post I wrote a while ago, describing a similar API that was not based on using a normal assertion to test that the condition has been met
* [`waitFor` from `@testing-library/dom`](https://testing-library.com/docs/dom-testing-library/api-async#waitfor)
Inspired the API of this library, where you also pass a callback that performs an otherwise normal test assertion
41 changes: 41 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "qunit-wait-for",
"version": "1.0.0",
"description": "Wait for a QUnit assertion",
"author": "Alex LaFroscia <alex@lafroscia.com>",
"license": "MIT",
"scripts": {
"build": "pika build",
"fmt": "prettier --write .",
"test": "qunit tests"
},
"devDependencies": {
"@pika/pack": "^0.5.0",
"@pika/plugin-build-node": "^0.9.2",
"@pika/plugin-build-web": "^0.9.2",
"@pika/plugin-ts-standard-pkg": "^0.9.2",
"@types/qunit": "^2.9.0",
"prettier": "^2.0.2",
"qunit": "^2.9.3",
"testdouble": "^3.13.1",
"testdouble-qunit": "^2.1.1",
"typescript": "^3.8.3"
},
"@pika/pack": {
"pipeline": [
[
"@pika/plugin-ts-standard-pkg"
],
[
"@pika/plugin-build-node"
],
[
"@pika/plugin-build-web"
]
]
},
"volta": {
"node": "12.16.1",
"yarn": "1.22.4"
}
}
51 changes: 51 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { TimeoutError, waitUntil } from "./wait-until";

type AssertionCallback = () => void;
type Options = {
timeout: number;
};

interface Result {
result: boolean;
}

export function installWaitFor(QUnit: QUnit) {
QUnit.extend(QUnit.assert, {
async waitFor(
assertionCallback: AssertionCallback,
{ timeout = 1000 }: Options = { timeout: undefined }
) {
const originalPushResult = this.pushResult;
let lastResult: Result;

this.pushResult = (result: Result) => {
lastResult = result;
};

try {
await waitUntil(() => {
assertionCallback();

return lastResult.result;
}, timeout);
} catch (e) {
if (!(e instanceof TimeoutError)) {
throw e;
}
} finally {
this.pushResult = originalPushResult;
}

this.pushResult(lastResult);
},
});
}

declare global {
interface Assert {
waitFor(
callback: AssertionCallback,
options?: Options
): () => Promise<void>;
}
}
46 changes: 46 additions & 0 deletions src/wait-until.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const futureTick = setTimeout;

const TIMEOUTS = [0, 1, 2, 5, 7];
const MAX_TIMEOUT = 10;

export function waitUntil(callback: () => unknown, timeout: number) {
const waitUntilTimeoutError = new TimeoutError(
"Condition not met within timeout"
);

return new Promise((resolve, reject) => {
let time = 0;

// eslint-disable-next-line require-jsdoc
function scheduleCheck(timeoutsIndex: number) {
let interval = TIMEOUTS[timeoutsIndex];
if (interval === undefined) {
interval = MAX_TIMEOUT;
}

futureTick(function () {
time += interval;

let value: unknown;

try {
value = callback();
} catch (error) {
reject(error);
}

if (value) {
resolve();
} else if (time < timeout) {
scheduleCheck(timeoutsIndex + 1);
} else {
reject(waitUntilTimeoutError);
}
}, interval);
}

scheduleCheck(0);
});
}

export class TimeoutError extends Error {}
51 changes: 51 additions & 0 deletions tests/wait-for-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const QUnit = require("qunit");
const td = require("testdouble");
const installVerify = require("testdouble-qunit");
const { installWaitFor } = require("../pkg");
const { test } = QUnit;

installVerify(QUnit);
installWaitFor(QUnit);

test("it can wait for a condition to be successful", async function (assert) {
const stub = td.when(td.function()()).thenReturn(1, 2);

await assert.waitFor(() => {
assert.equal(stub(), 2);
});
});

test("it throws a non-timeout error", async function (assert) {
assert.expect(1);

const e = new Error("Some Error");
const stub = td.when(td.function()()).thenThrow(e);

try {
await assert.waitFor(() => {
assert.ok(stub());
});
} catch (error) {
assert.equal(error, e);
}
});

test("it can time out while waiting", async function (assert) {
const stub = td.when(td.function()()).thenReturn(1);

td.replace(assert, "pushResult");

await assert.waitFor(() => {
assert.equal(stub(), 2);
});

const {
calls: [{ args }],
} = td.explain(assert.pushResult);

td.reset(); // Let `assert` work again

assert.deepEqual(args, [
{ result: false, actual: 1, expected: 2, message: undefined },
]);
});
7 changes: 7 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext"
},
"files": ["src/index.ts"]
}

0 comments on commit 5b87e87

Please sign in to comment.