Skip to content
This repository has been archived by the owner on Jan 5, 2023. It is now read-only.

Commit

Permalink
feat(wait-for) (#51)
Browse files Browse the repository at this point in the history
* add waitFor

* add wait-for tests

* tests + docs for waitFor
  • Loading branch information
HugoDF authored Sep 1, 2020
1 parent 6f35956 commit e116427
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 19 deletions.
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ For more complex use cases, please see [USE_CASES.md](./USE_CASES.md) or for the
| [render](#render) | Render & run Alpine.js component markup |
| [load](#loadloadsync) | Extract Alpine.js component markup from files |
| [loadSync](#loadloadsync) | Synchronous variant of `load` |
| [waitFor](#waitfor) | Wait for an assertion to pass |
| [$nextTick](#nexttick) | Wait for a re-render or async work to happen |
| [setGlobal](#setglobal) | Override globals using an object |
| [setMutationObserver](#setmutationobserver) | Set a custom MutationObserver implementation |
Expand Down Expand Up @@ -137,8 +138,41 @@ test('my test', async () => {
});
```

## waitFor

Wait until assertions pass, wrapper for [wait-for-expect](https://github.com/TheBrainFamily/wait-for-expect).

Parameters:

- callback containing the assertions. "predicate" that has to complete without throwing
- timeout - Optional, Number - Maximum wait interval, 4500ms by default
- interval - Optional, Number - Wait interval, 50ms by default

Returns: Promise that resolves/rejects based on whether the assertions eventually pass.


Usage example: for more advanced use-cases see [Clicking a button to toggle visibility](./USE_CASES.md#clicking-a-button-to-toggle-visibility) and [Intercepting `fetch` calls & waiting for re-renders](./USE_CASES.md#intercepting-fetch-calls--waiting-for-re-renders)

```js
test('clicking a button to toggle visibility', async () => {
const component = render(`<div x-data="{ isOpen: false }">
<button @click="isOpen = !isOpen"></button>
<span x-show="isOpen"></span>
</div>`);

expect(component.querySelector('span').style.display).toEqual('none');
component.querySelector('button').click();
await waitFor(() => {
expect(component.querySelector('span').style.display).toEqual('');
});
});
```


## $nextTick

> Note: prefer [`waitFor`](#waitfor) it's more flexible and accurate.
Function to wait until a render/async operation happens.

Parameters: none.
Expand All @@ -149,8 +183,6 @@ Returns:

> Note this exported as a global from the Alpine Test Utils module _and_ is attached to components during `render`, see [render](#render).
Usage example: for more advanced use-cases see [Clicking a button to toggle visibility](./USE_CASES.md#clicking-a-button-to-toggle-visibility) and [Intercepting `fetch` calls & waiting for re-renders](./USE_CASES.md#intercepting-fetch-calls--waiting-for-re-renders)

```js
test('clicking a button to toggle visibility', async () => {
const component = render(`<div x-data="{ isOpen: false }">
Expand Down
26 changes: 14 additions & 12 deletions USE_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This documentation provides examples for specific use cases in Node.js. Please [

```js
import test from 'ava';
import {render} from 'alpine-test-utils';
import {render, waitFor} from 'alpine-test-utils';

test('use-case - clicking a button to toggle visibility', async (t) => {
const component = render(`<div x-data="{ isOpen: false }">
Expand All @@ -22,16 +22,17 @@ test('use-case - clicking a button to toggle visibility', async (t) => {

t.is(component.querySelector('span').style.display, 'none');
component.querySelector('button').click();
await component.$nextTick();
t.is(component.querySelector('span').style.display, '');
await waitFor(() => {
t.is(component.querySelector('span').style.display, '');
});
});
```

### Intercepting `fetch` calls & waiting for re-renders

```js
import test from 'ava';
import {render, setGlobal} from 'alpine-test-utils';
import {render, setGlobal, waitFor} from 'alpine-test-utils';

test('use-case - intercepting fetch calls', async (t) => {
setGlobal({
Expand All @@ -51,14 +52,15 @@ test('use-case - intercepting fetch calls', async (t) => {
</template>
</div>`);
// Flushes the Promises
await component.$nextTick();
t.deepEqual(component.$data.data, ['data-1', 'data-2']);
// Lets the re-render run
await component.$nextTick();
const textNodes = component.querySelectorAll('[data-testid=text-el]');
t.is(textNodes.length, 2);
t.is(textNodes[0].innerText, 'data-1');
t.is(textNodes[1].innerText, 'data-2');
await waitFor(() => {
t.deepEqual(component.$data.data, ['data-1', 'data-2']);
})
await waitFor(() => {
const textNodes = component.querySelectorAll('[data-testid=text-el]');
t.is(textNodes.length, 2);
t.is(textNodes[0].innerText, 'data-1');
t.is(textNodes[1].innerText, 'data-2');
});
});
```

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"alpinejs": "^2.3.1"
},
"dependencies": {
"jsdom": "^16.2.2"
"jsdom": "^16.2.2",
"wait-for-expect": "^3.0.2"
},
"devDependencies": {
"alpinejs": "^2.3.1",
Expand Down
4 changes: 3 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check
const fs = require('fs');
const waitFor = require('wait-for-expect');
const {promisify} = require('util');
const readFile = promisify(fs.readFile);
const {JSDOM} = require('jsdom');
Expand Down Expand Up @@ -139,5 +140,6 @@ module.exports = {
setGlobal,
load,
loadSync,
render
render,
waitFor
};
48 changes: 45 additions & 3 deletions tests/unit/use-cases.js → tests/ava/use-cases.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import test from 'ava';
import path from 'path';
import {load, loadSync, render, setGlobal} from '../../src/main';
import {load, loadSync, render, setGlobal, waitFor} from '../../src/main';
console.warn = () => {};

test('use-case - clicking a button to toggle visibility', async (t) => {
test('[deprecated] use-case - clicking a button to toggle visibility', async (t) => {
const component = render(`<div x-data="{ isOpen: false }">
<button @click="isOpen = !isOpen"></button>
<span x-show="isOpen"></span>
Expand All @@ -14,7 +15,20 @@ test('use-case - clicking a button to toggle visibility', async (t) => {
t.is(component.querySelector('span').style.display, '');
});

test('use-case - intercepting fetch calls', async (t) => {
test('use-case - clicking a button to toggle visibility', async (t) => {
const component = render(`<div x-data="{ isOpen: false }">
<button @click="isOpen = !isOpen"></button>
<span x-show="isOpen"></span>
</div>`);

t.is(component.querySelector('span').style.display, 'none');
component.querySelector('button').click();
await waitFor(() => {
t.is(component.querySelector('span').style.display, '');
});
});

test('[deprecated] use-case - intercepting fetch calls - $nextTick', async (t) => {
setGlobal({
fetch: () =>
Promise.resolve({
Expand Down Expand Up @@ -42,6 +56,34 @@ test('use-case - intercepting fetch calls', async (t) => {
t.is(textNodes[1].innerText, 'data-2');
});

test('use-case - intercepting fetch calls - waitFor', async (t) => {
setGlobal({
fetch: () =>
Promise.resolve({
json: () => Promise.resolve(['data-1', 'data-2'])
})
});
const component = render(`<div
x-data="{ data: [] }"
x-init="fetch().then(r => r.json()).then(d => {
data = d;
})"
>
<template x-for="d in data" :key="d">
<span data-testid="text-el" x-text="d"></span>
</template>
</div>`);
await waitFor(() => {
t.deepEqual(component.$data.data, ['data-1', 'data-2']);
});
await waitFor(() => {
const textNodes = component.querySelectorAll('[data-testid=text-el]');
t.is(textNodes.length, 2);
t.is(textNodes[0].innerText, 'data-1');
t.is(textNodes[1].innerText, 'data-2');
});
});

test('use-case - PHP template - async', async (t) => {
const markup = await load(path.join(__dirname, '../fixtures/template.php'));
// Overwrite `x-data` since it's set by a PHP expression
Expand Down
43 changes: 43 additions & 0 deletions tests/jest/use-cases.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable no-undef */
const {render, waitFor, setGlobal} = require('../../src/main');

test('use-case - clicking a button to toggle visibility', async () => {
const component = render(`<div x-data="{ isOpen: false }">
<button @click="isOpen = !isOpen"></button>
<span x-show="isOpen"></span>
</div>`);

expect(component.querySelector('span').style.display).toEqual('none');
component.querySelector('button').click();
await waitFor(() => {
expect(component.querySelector('span').style.display).toEqual('');
});
});

test('use-case - intercepting fetch calls - waitFor', async () => {
setGlobal({
fetch: () =>
Promise.resolve({
json: () => Promise.resolve(['data-1', 'data-2'])
})
});
const component = render(`<div
x-data="{ data: [] }"
x-init="fetch().then(r => r.json()).then(d => {
data = d;
})"
>
<template x-for="d in data" :key="d">
<span data-testid="text-el" x-text="d"></span>
</template>
</div>`);
await waitFor(() => {
expect(component.$data.data).toEqual(['data-1', 'data-2']);
});
await waitFor(() => {
const textNodes = component.querySelectorAll('[data-testid=text-el]');
expect(textNodes).toHaveLength(2);
expect(textNodes[0].innerText).toEqual('data-1');
expect(textNodes[1].innerText).toEqual('data-2');
});
});
26 changes: 26 additions & 0 deletions tests/unit/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,44 @@ test('load - file with single component', async (t) => {
});

test('loadSync - file with multiple components', (t) => {
const calls = [];
const realWarn = console.warn;
console.warn = (...args) => {
calls.push(args);
};

const components = loadSync(
path.join(__dirname, '../fixtures/multiple-components.html')
);
t.is(components.length, 3);
t.is(components[0], `<div x-data="" name="component-1"></div>`);
t.is(components[1], `<div x-data="" name="component-2"></div>`);
t.is(components[2], `<div x-data="" name="component-3"></div>`);
// Check warning
t.deepEqual(calls, [
[
'alpine-test-utils: loadSync() can cause performance issues, prefer async "load()"'
]
]);
console.warn = realWarn;
});

test('loadSync - file with single component', (t) => {
const calls = [];
const realWarn = console.warn;
console.warn = (...args) => {
calls.push(args);
};

const component = loadSync(
path.join(__dirname, '../fixtures/single-component.html')
);
t.is(component, `<div x-data="" name="component-1"></div>`);
// Check warning
t.deepEqual(calls, [
[
'alpine-test-utils: loadSync() can cause performance issues, prefer async "load()"'
]
]);
console.warn = realWarn;
});
15 changes: 15 additions & 0 deletions tests/unit/wait-for.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import test from 'ava';
import {render, waitFor} from '../../src/main';

test('waitFor - x-show toggles style.display', async (t) => {
const component = render(`<div x-data="{ isOpen: false }">
<button @click="isOpen = !isOpen"></button>
<span x-show="isOpen"></span>
</div>`);

t.is(component.querySelector('span').style.display, 'none');
component.querySelector('button').click();
await waitFor(() => {
t.is(component.querySelector('span').style.display, '');
});
});
78 changes: 78 additions & 0 deletions tests/uvu/use-cases.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {test} from 'uvu';
import * as assert from 'uvu/assert';
import path from 'path';
import {load, loadSync, render, setGlobal, waitFor} from '../../src/main';

console.warn = () => {};

test('use-case - clicking a button to toggle visibility', async () => {
const component = render(`<div x-data="{ isOpen: false }">
<button @click="isOpen = !isOpen"></button>
<span x-show="isOpen"></span>
</div>`);
assert.is(component.querySelector('span').style.display, 'none');
component.querySelector('button').click();
await waitFor(() => {
assert.is(component.querySelector('span').style.display, '');
});
});

test('use-case - intercepting fetch calls', async () => {
setGlobal({
fetch: () =>
Promise.resolve({
json: () => Promise.resolve(['data-1', 'data-2'])
})
});
const component = render(`<div
x-data="{ data: [] }"
x-init="fetch().then(r => r.json()).then(d => {
data = d;
})"
>
<template x-for="d in data" :key="d">
<span data-testid="text-el" x-text="d"></span>
</template>
</div>`);
await waitFor(() => {
assert.equal(component.$data.data, ['data-1', 'data-2']);
});
await waitFor(() => {
const textNodes = component.querySelectorAll('[data-testid=text-el]');
assert.is(textNodes.length, 2);
assert.is(textNodes[0].innerText, 'data-1');
assert.is(textNodes[1].innerText, 'data-2');
});
});

test('use-case - PHP template - async', async () => {
const markup = await load(path.join(__dirname, '../fixtures/template.php'));
// Overwrite `x-data` since it's set by a PHP expression
const component = render(markup, {
foo: 'baz'
});
assert.is(component.querySelector('span').innerText, 'baz');
});

test('use-case - PHP template - sync', () => {
const markup = loadSync(path.join(__dirname, '../fixtures/template.php'));
// Overwrite `x-data` since it's set by a PHP expression
const component = render(markup, {
foo: 'baz'
});
assert.is(component.querySelector('span').innerText, 'baz');
});

test('use-case - load from HTML file - async', async () => {
const markup = await load(path.join(__dirname, '../fixtures/template.html'));
const component = render(markup);
assert.is(component.querySelector('span').innerText, 'bar');
});

test('use-case - load from HTML file - sync', () => {
const markup = loadSync(path.join(__dirname, '../fixtures/template.html'));
const component = render(markup);
assert.is(component.querySelector('span').innerText, 'bar');
});

test.run();
Loading

0 comments on commit e116427

Please sign in to comment.