');
+ let $ = document.querySelector('[contenteditable]');
+
+ await I.find($)
+ .then.press('KeyO');
+
+ await assert($.innerText === 'Foo',
+ 'Expected content editable text to be "Foo"');
+
+ await I.find($)
+ .then.press('!', { replace: true });
+
+ await assert($.innerText === '!',
+ 'Expected content editable text to be "!"');
+
+ $.innerText = 'Bar';
+
+ await I.find($)
+ .then.press('z', { range: [2, 3] });
+
+ await assert($.innerText === 'Baz',
+ 'Expected content editable text to be "Baz"');
+ });
+
+ it('replaces any existing text selection', async () => {
+ fixture('
Qoox
');
+ let $ = document.querySelector('[contenteditable]');
+
+ let range = document.createRange();
+ range.setStart($.firstChild, 1);
+ range.setEnd($.firstChild, 3);
+ let sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+
+ await I.find($)
+ .then.press('u');
+
+ await assert($.innerText === 'Qux',
+ 'Expected content editable text to be "Qux"');
+ });
+
+ it('can delete text within relevant elements', async () => {
+ fixture('');
+
+ await I.find('Foo')
+ .then.press('Backspace')
+ .then.press('Delete', { range: 0 });
+
+ await assert(document.querySelector('input').value === 'Foo',
+ 'Expected "Foo" value to be "Foo"');
+ });
+
+ it('can hold a key in an interaction until the next press', async () => {
+ fixture('');
+
+ await I.find('Baz')
+ .then.press('Shift', { hold: true })
+ .then.press('KeyB')
+ .then.press('Shift')
+ .then.press(['KeyA', 'KeyZ'])
+ .then.press(['Shift', 'Digit1']);
+
+ await assert(document.querySelector('input').value === 'Baz!',
+ 'Expected "Baz" to have a value of "Baz!"');
+ });
+
+ it('retains any custom value descriptors for relevant elements', async () => {
+ fixture('');
+ let $input = document.querySelector('input');
+ let value = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
+
+ Object.defineProperty($input, 'value', {
+ ...value, get: () => value.get.apply($input) + 'oo'
+ });
+
+ await I.find('Qux')
+ .then.press('KeyF');
+
+ await assert($input.value === 'foo',
+ 'Expected "Qux" value to be "foo"');
+ });
+});
diff --git a/tests/actions/trigger.test.js b/tests/actions/trigger.test.js
new file mode 100644
index 00000000..511f235e
--- /dev/null
+++ b/tests/actions/trigger.test.js
@@ -0,0 +1,33 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture, listen } from '../helpers';
+
+describe('Actions | #trigger(eventName, options?)', () => {
+ beforeEach(() => {
+ fixture('
Foo
');
+ });
+
+ it('triggers an arbitrary event on the current element', async () => {
+ let event = listen('.foo', 'foobar');
+
+ await I.find('Foo')
+ .then.trigger('foobar');
+
+ await assert(event.calls.length === 1,
+ 'Expected "foobar" event to be triggered');
+ });
+
+ it('bubbles and is cancelable by default', async () => {
+ let event = listen('.foo', 'xyzzy');
+
+ await I.find('Foo')
+ .then.trigger('xyzzy', { foo: 'bar' })
+ .then.trigger('xyzzy', { bubbles: null, cancelable: false });
+
+ await assert(event.calls[0][0].foo === 'bar',
+ 'Expected "xyzzy" event to include event properties');
+ await assert(event.calls[0][0].bubbles && event.calls[0][0].cancelable,
+ 'Expected "xyzzy" event to bubble and be cancelable');
+ await assert(!event.calls[1][0].bubbles && !event.calls[1][0].cancelable,
+ 'Expected "xyzzy" event to not bubble and not be cancelable');
+ });
+});
diff --git a/tests/actions/type.test.js b/tests/actions/type.test.js
new file mode 100644
index 00000000..65cb3ca0
--- /dev/null
+++ b/tests/actions/type.test.js
@@ -0,0 +1,60 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture, listen } from '../helpers';
+
+describe('Actions | #type(string, options?)', () => {
+ beforeEach(() => {
+ fixture('');
+ });
+
+ it('types text into the current or specified element', async () => {
+ await I.find('Foo').then.type('Foo');
+ await I.type('Bar').into('Foo');
+
+ await assert(document.querySelector('input').value === 'FooBar',
+ 'Expected input value to be "FooBar"');
+ });
+
+ it('focuses and blurs the current element', async () => {
+ let focus = listen('input', 'focus');
+ let blur = listen('input', 'blur');
+
+ await I.find('Foo')
+ .then.type('Bar');
+
+ await assert(focus.calls.length === 1,
+ 'Expected focus event to be triggered');
+ await assert(blur.calls.length === 1,
+ 'Expected blur event to be triggered');
+ });
+
+ it('triggers a change event for inputs', async () => {
+ let event = listen('input', 'change');
+
+ await I.find('Foo')
+ .then.type('Bar');
+
+ await assert(event.calls.length === 1,
+ 'Expected change event to be triggered');
+
+ fixture('');
+ event = listen('[contenteditable]', 'change');
+
+ await I.find('$([contenteditable])')
+ .then.type('Baz');
+
+ await assert(event.calls.length === 0,
+ 'Expected change event not to be triggered');
+ });
+
+ it('can delay between keystrokes', async () => {
+ let start = Date.now();
+
+ await I.find('Foo')
+ .then.type('Bar', { delay: 50 });
+
+ let delta = Date.now() - start;
+
+ await assert(delta > 100 && delta < 150,
+ 'Expected total delay to be between 100-150ms');
+ });
+});
diff --git a/tests/assertions/attribute.test.js b/tests/assertions/attribute.test.js
new file mode 100644
index 00000000..67a7c036
--- /dev/null
+++ b/tests/assertions/attribute.test.js
@@ -0,0 +1,24 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #attribute(name, expected)', () => {
+ beforeEach(() => {
+ fixture('
Foo
');
+ });
+
+ it('asserts that the element has an attribute with an expected value', async () => {
+ await I.find('Foo').attribute('data-foo', 'bar');
+
+ await assert.throws(
+ I.find('Foo').attribute('data-foo', 'baz'),
+ '"Foo" `data-foo` attribute is "bar" but expected "baz"');
+ });
+
+ it('can assert that the element does not have an attribute with an expected value', async () => {
+ await I.find('Foo').not.attribute('data-foo', 'baz');
+
+ await assert.throws(
+ I.find('Foo').not.attribute('data-foo', 'bar'),
+ '"Foo" `data-foo` attribute is "bar" but expected it not to be');
+ });
+});
diff --git a/tests/assertions/checked.test.js b/tests/assertions/checked.test.js
new file mode 100644
index 00000000..0e1cb718
--- /dev/null
+++ b/tests/assertions/checked.test.js
@@ -0,0 +1,27 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #checked()', () => {
+ beforeEach(() => {
+ fixture(`
+
+
+
+
+ `);
+ });
+
+ it('asserts that the element is checked', async () => {
+ await I.find('Foo').checked();
+
+ await assert.throws(I.find('Bar').checked(),
+ '"Bar" is not checked');
+ });
+
+ it('can assert that the element is not checked', async () => {
+ await I.find('Bar').not.checked();
+
+ await assert.throws(I.find('Foo').not.checked(),
+ '"Foo" is checked');
+ });
+});
diff --git a/tests/assertions/disabled.test.js b/tests/assertions/disabled.test.js
new file mode 100644
index 00000000..e0ef5531
--- /dev/null
+++ b/tests/assertions/disabled.test.js
@@ -0,0 +1,25 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #disabled()', () => {
+ beforeEach(() => {
+ fixture(`
+
+
+ `);
+ });
+
+ it('asserts that the element is disabled', async () => {
+ await I.find('Foo').disabled();
+
+ await assert.throws(I.find('Bar').disabled(),
+ '"Bar" is not disabled');
+ });
+
+ it('can assert that the element is not disabled', async () => {
+ await I.find('Bar').not.disabled();
+
+ await assert.throws(I.find('Foo').not.disabled(),
+ '"Foo" is disabled');
+ });
+});
diff --git a/tests/assertions/exists.test.js b/tests/assertions/exists.test.js
new file mode 100644
index 00000000..eea4ee5b
--- /dev/null
+++ b/tests/assertions/exists.test.js
@@ -0,0 +1,22 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #exists()', () => {
+ beforeEach(() => {
+ fixture('
Foo
');
+ });
+
+ it('asserts that the element exists', async () => {
+ await I.find('Foo').exists();
+
+ await assert.throws(I.find('Bar').exists(),
+ 'Could not find "Bar"');
+ });
+
+ it('can assert that the element does not exist', async () => {
+ await I.find('Bar').not.exists();
+
+ await assert.throws(I.find('Foo').not.exists(),
+ 'Found "Foo" but expected not to');
+ });
+});
diff --git a/tests/assertions/focusable.test.js b/tests/assertions/focusable.test.js
new file mode 100644
index 00000000..4eee937a
--- /dev/null
+++ b/tests/assertions/focusable.test.js
@@ -0,0 +1,44 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #focusable()', () => {
+ beforeEach(() => {
+ fixture(`
+
Foo
+
+
Baz
+ `);
+ });
+
+ it('asserts that the element is focusable', async () => {
+ await I.find('Foo').focusable();
+ await I.find('Bar').focusable();
+
+ await assert.throws(I.find('Baz').focusable(),
+ '"Baz" is not focusable');
+ });
+
+ it('asserts the document is focusable', async () => {
+ fixture('');
+
+ let $f = document.querySelector('iframe');
+ $f.focus = () => {};
+
+ let F = new I.constructor({
+ assert: { timeout: 100, reliability: 0 },
+ root: () => $f.contentDocument.body
+ });
+
+ await assert.throws(F.focus('Foo'),
+ 'The document is not focusable');
+ });
+
+ it('can assert that the element is not focusable', async () => {
+ await I.find('Baz').not.focusable();
+
+ await assert.throws(I.find('Bar').not.focusable(),
+ '"Bar" is focusable');
+ await assert.throws(I.find('Foo').not.focusable(),
+ '"Foo" is focusable');
+ });
+});
diff --git a/tests/assertions/focused.test.js b/tests/assertions/focused.test.js
new file mode 100644
index 00000000..ee6ffab8
--- /dev/null
+++ b/tests/assertions/focused.test.js
@@ -0,0 +1,27 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #focused()', () => {
+ beforeEach(() => {
+ fixture(`
+
+
+ `);
+
+ document.querySelector('button').focus();
+ });
+
+ it('asserts that the element is focused', async () => {
+ await I.find('Foo').focused();
+
+ await assert.throws(I.find('Bar').focused(),
+ '"Bar" is not focused');
+ });
+
+ it('can assert that the element is not focused', async () => {
+ await I.find('Bar').not.focused();
+
+ await assert.throws(I.find('Foo').not.focused(),
+ '"Foo" is focused');
+ });
+});
diff --git a/tests/assertions/matches.test.js b/tests/assertions/matches.test.js
new file mode 100644
index 00000000..affe511d
--- /dev/null
+++ b/tests/assertions/matches.test.js
@@ -0,0 +1,22 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #matches(selector)', () => {
+ beforeEach(() => {
+ fixture('
Foo
');
+ });
+
+ it('asserts that the current element matches the selector', async () => {
+ await I.find('Foo').matches('.foo');
+
+ await assert.throws(I.find('Foo').matches('.bar'),
+ '"Foo" does not match `.bar`');
+ });
+
+ it('can assert that the current element does not match the selector', async () => {
+ await I.find('Foo').not.matches('.bar');
+
+ await assert.throws(I.find('Foo').not.matches('.foo'),
+ '"Foo" matches `.foo`');
+ });
+});
diff --git a/tests/assertions/overflows.test.js b/tests/assertions/overflows.test.js
new file mode 100644
index 00000000..48a555fa
--- /dev/null
+++ b/tests/assertions/overflows.test.js
@@ -0,0 +1,58 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #overflows(axis?)', () => {
+ beforeEach(() => {
+ fixture(`
+
+
+
+
+
+ `);
+ });
+
+ it('asserts that the current element has overflow', async () => {
+ await I.find('$(.overflow.x)').overflows();
+ await I.find('$(.overflow.y)').overflows();
+ await I.find('$(.overflow.x.y)').overflows();
+
+ await assert.throws(I.find('$(.overflow.none)').overflows(),
+ '$(.overflow.none) has no overflow');
+ });
+
+ it('asserts that the current element has overflow on a specific axis', async () => {
+ await I.find('$(.overflow.x)').overflows('x');
+ await I.find('$(.overflow.y)').overflows('y');
+ await I.find('$(.overflow.x.y)').overflows('x');
+ await I.find('$(.overflow.x.y)').overflows('y');
+
+ await assert.throws(I.find('$(.overflow.x)').overflows('y'),
+ '$(.overflow.x) has no overflow-y');
+ await assert.throws(I.find('$(.overflow.y)').overflows('x'),
+ '$(.overflow.y) has no overflow-x');
+ });
+
+ it('can assert that the current element does not have overflow', async () => {
+ await I.find('$(.overflow.none)').not.overflows('x');
+ await I.find('$(.overflow.none)').not.overflows('y');
+ await I.find('$(.overflow.none)').not.overflows();
+
+ await assert.throws(I.find('$(.overflow.x)').not.overflows('x'),
+ '$(.overflow.x) has overflow-x');
+ await assert.throws(I.find('$(.overflow.y)').not.overflows('y'),
+ '$(.overflow.y) has overflow-y');
+ await assert.throws(I.find('$(.overflow.x.y)').not.overflows(),
+ '$(.overflow.x.y) has overflow');
+ });
+
+ it('does not accept an unknown axis', async () => {
+ await assert.throws(() => I.find('$(.overflow)').overflows('z'),
+ 'Invalid overflow axis: z');
+ });
+});
diff --git a/tests/assertions/property.test.js b/tests/assertions/property.test.js
new file mode 100644
index 00000000..a90d2894
--- /dev/null
+++ b/tests/assertions/property.test.js
@@ -0,0 +1,24 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #property(name, expected)', () => {
+ beforeEach(() => {
+ fixture('
Foo
');
+ });
+
+ it('asserts that the element has an property with an expected value', async () => {
+ await I.find('Foo').property('dataset.foo', 'bar');
+
+ await assert.throws(
+ I.find('Foo').property('dataset.foo', 'baz'),
+ '"Foo" `dataset.foo` is `bar` but expected `baz`');
+ });
+
+ it('can assert that the element does not have an property with an expected value', async () => {
+ await I.find('Foo').not.property('dataset.foo', 'baz');
+
+ await assert.throws(
+ I.find('Foo').not.property('dataset.foo', 'bar'),
+ '"Foo" `dataset.foo` is `bar` but expected it not to be');
+ });
+});
diff --git a/tests/assertions/scrollable.test.js b/tests/assertions/scrollable.test.js
new file mode 100644
index 00000000..6c3a17a6
--- /dev/null
+++ b/tests/assertions/scrollable.test.js
@@ -0,0 +1,66 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #scrollable(axis?)', () => {
+ beforeEach(() => {
+ fixture(`
+
+
+
+
+
+
+ `);
+ });
+
+ it('asserts that the current element is scrollable', async () => {
+ await I.find('$(.scrollable.x)').scrollable();
+ await I.find('$(.scrollable.y)').scrollable();
+ await I.find('$(.scrollable.x.y)').scrollable();
+
+ await assert.throws(I.find('$(.scrollable.hidden)').scrollable(),
+ '$(.scrollable.hidden) is not scrollable');
+ await assert.throws(I.find('$(.scrollable.none)').scrollable(),
+ '$(.scrollable.none) has no overflow');
+ });
+
+ it('asserts that the current element is scrollable on a specific axis', async () => {
+ await I.find('$(.scrollable.x)').scrollable('x');
+ await I.find('$(.scrollable.y)').scrollable('y');
+ await I.find('$(.scrollable.x.y)').scrollable('x');
+ await I.find('$(.scrollable.x.y)').scrollable('y');
+
+ await assert.throws(I.find('$(.scrollable.x)').scrollable('y'),
+ '$(.scrollable.x) is not scrollable vertically');
+ await assert.throws(I.find('$(.scrollable.y)').scrollable('x'),
+ '$(.scrollable.y) is not scrollable horizontally');
+ });
+
+ it('can assert that the current element is not scrollable', async () => {
+ await I.find('$(.scrollable.hidden)').not.scrollable('x');
+ await I.find('$(.scrollable.none)').not.scrollable('x');
+ await I.find('$(.scrollable.hidden)').not.scrollable('y');
+ await I.find('$(.scrollable.none)').not.scrollable('y');
+ await I.find('$(.scrollable.hidden)').not.scrollable();
+ await I.find('$(.scrollable.none)').not.scrollable();
+
+ await assert.throws(I.find('$(.scrollable.x)').not.scrollable('x'),
+ '$(.scrollable.x) is scrollable horizontally');
+ await assert.throws(I.find('$(.scrollable.y)').not.scrollable('y'),
+ '$(.scrollable.y) is scrollable vertically');
+ await assert.throws(I.find('$(.scrollable.x.y)').not.scrollable(),
+ '$(.scrollable.x.y) is scrollable');
+ });
+
+ it('does not accept an unknown axis', async () => {
+ await assert.throws(() => I.find('$(.scrollable)').scrollable('z'),
+ 'Invalid scroll axis: z');
+ });
+});
diff --git a/tests/assertions/selected.test.js b/tests/assertions/selected.test.js
new file mode 100644
index 00000000..f33b4f7a
--- /dev/null
+++ b/tests/assertions/selected.test.js
@@ -0,0 +1,27 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #selected()', () => {
+ beforeEach(() => {
+ fixture(`
+
+ `);
+ });
+
+ it('asserts that the element is selected', async () => {
+ await I.find('Foo').selected();
+
+ await assert.throws(I.find('Bar').selected(),
+ '"Bar" is not selected');
+ });
+
+ it('can assert that the element is not selected', async () => {
+ await I.find('Bar').not.selected();
+
+ await assert.throws(I.find('Foo').not.selected(),
+ '"Foo" is selected');
+ });
+});
diff --git a/tests/assertions/text.test.js b/tests/assertions/text.test.js
new file mode 100644
index 00000000..23460663
--- /dev/null
+++ b/tests/assertions/text.test.js
@@ -0,0 +1,22 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #text(expected)', () => {
+ beforeEach(() => {
+ fixture('
Foo
');
+ });
+
+ it('asserts that the current element has the expected text', async () => {
+ await I.find('Foo').text('Foo');
+
+ await assert.throws(I.find('Foo').text('Bar'),
+ '"Foo" text is "Foo" but expected "Bar"');
+ });
+
+ it('can assert that the current element does not have the expected text', async () => {
+ await I.find('Foo').not.text('Bar');
+
+ await assert.throws(I.find('Foo').not.text('Foo'),
+ '"Foo" text is "Foo" but expected it not to be');
+ });
+});
diff --git a/tests/assertions/value.test.js b/tests/assertions/value.test.js
new file mode 100644
index 00000000..b3253e38
--- /dev/null
+++ b/tests/assertions/value.test.js
@@ -0,0 +1,22 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #value(expected)', () => {
+ beforeEach(() => {
+ fixture('');
+ });
+
+ it('asserts that the current element has an expected value', async () => {
+ await I.find('Foo').value('Bar');
+
+ await assert.throws(I.find('Foo').value('Baz'),
+ '"Foo" value is "Bar" but expected "Baz"');
+ });
+
+ it('can assert that the current element does not have an expected value', async () => {
+ await I.find('Foo').not.value('Baz');
+
+ await assert.throws(I.find('Foo').not.value('Bar'),
+ '"Foo" value is "Bar" but expected it not to be');
+ });
+});
diff --git a/tests/assertions/visible.test.js b/tests/assertions/visible.test.js
new file mode 100644
index 00000000..26ccc3cb
--- /dev/null
+++ b/tests/assertions/visible.test.js
@@ -0,0 +1,70 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { I, assert, fixture } from '../helpers';
+
+describe('Assert | #visible()', () => {
+ beforeEach(() => {
+ fixture(`
+
+
Hidden
+
Invisible
+
Opacity 0
+
Width 0
+
Height 0
+
Above Screen
+
Below Screen
+
Left of Screen
+
Right of Screen
+
+
+
Fully Covered
+
Peek Top
+
Peek Bottom
+
Peek Left
+
Peek Right
+
Visible
+
+ `);
+ });
+
+ const shouldBeVisible = [
+ 'Peek Top',
+ 'Peek Bottom',
+ 'Peek Left',
+ 'Peek Right',
+ 'Visible',
+ '$(.visible)'
+ ];
+
+ const shouldNotBeVisible = [
+ 'Hidden',
+ '$(.invisible)',
+ 'Opacity 0',
+ 'Width 0',
+ 'Height 0',
+ 'Above Screen',
+ 'Below Screen',
+ 'Left of Screen',
+ 'Right of Screen',
+ 'Fully Covered'
+ ];
+
+ it('asserts that the element is visible', async () => {
+ for (let item of shouldBeVisible)
+ await I.find(item).visible();
+
+ for (let item of shouldNotBeVisible) {
+ await assert.throws(I.find(item).visible(),
+ `${item.startsWith('$') ? item : `"${item}"`} is not visible`);
+ }
+ });
+
+ it('can assert that the element is not visible', async () => {
+ for (let item of shouldNotBeVisible)
+ await I.find(item).not.visible();
+
+ for (let item of shouldBeVisible) {
+ await assert.throws(I.find(item).not.visible(),
+ `${item.startsWith('$') ? item : `"${item}"`} is visible`);
+ }
+ });
+});
diff --git a/tests/helpers.js b/tests/helpers.js
index 21c71558..9a857a36 100644
--- a/tests/helpers.js
+++ b/tests/helpers.js
@@ -1,29 +1,13 @@
import { createTestHook } from 'moonshiner/utils';
-import { strict as assert } from 'assert';
+import { Interactor } from 'interactor.js';
-const { assign, defineProperty } = Object;
-
-// Returns true when running in jsdom
-export function jsdom() {
- return (jsdom.result = jsdom.result ?? navigator.userAgent.includes('jsdom'));
-}
-
-// In jsdom, when elements lose focus they nullify the _lastFocusedElement which is reflected in
-// document.hasFocus(); this sets focus back to the body so that hasFocus() is accurate.
-function jsdomCaptureFocus(e) {
- if (e.relatedTarget === e.currentTarget.ownerDocument)
- e.currentTarget.ownerDocument.body.focus();
-}
-
-// In jsdom, the dom is not automatically focused and window.focus() is not implemented;
-// additionally, even though the body has a default tabindex of -1, jsdom will not focus the body
-// unless it has an explicit tabindex attribute.
-function jsdomFocusDocument(doc) {
- doc.body.setAttribute('tabindex', -1);
- doc.body.focus();
-};
+// Test interactor scoped to the testing fixtures
+export const I = new Interactor({
+ root: () => document.getElementById('testing-root'),
+ assert: { timeout: 100, reliability: 0 }
+});
-// A testing hook which injects HTML into the document body and removes it upon the next call.
+// Test hook which sets up and tears down HTML fixtures
export const fixture = createTestHook(innerHTML => {
let $test = document.createElement('div');
@@ -32,122 +16,52 @@ export const fixture = createTestHook(innerHTML => {
innerHTML = innerHTML.replace(new RegExp(`^\\s{${ind}}`, 'mg'), '').trim();
// assign HTML and append to the body
- assign($test, { id: 'test', innerHTML });
+ Object.assign($test, { id: 'testing-root', innerHTML });
document.body.appendChild($test);
- if (jsdom()) {
- // apply focus hacks to the current document
- if (!document.body.hasAttribute('tabindex'))
- document.body.addEventListener('focusout', jsdomCaptureFocus);
-
- // jsdom doesn't support srcdoc or sandbox
- for (let $f of $test.querySelectorAll('iframe')) {
- // polyfill srcdoc
- $f.setAttribute('src', `data:text/html;charset=utf-8,${
- encodeURI($f.getAttribute('srcdoc'))
- }`);
-
- if ($f.getAttribute('sandbox') != null) {
- // simulate sandbox without breaking jsdom
- defineProperty($f, 'contentDocument', { value: null });
- } else {
- // apply the focus hacks to frame documents
- $f.addEventListener('load', () => {
- $f.contentDocument.body.addEventListener('focusout', jsdomCaptureFocus);
-
- $f.addEventListener('focus', e => {
- if (!e.defaultPrevented) jsdomFocusDocument($f.contentDocument);
- });
- });
- }
- }
-
- // jsdom doesn't support isContentEditable
- for (let $e of $test.querySelectorAll('[contenteditable]'))
- defineProperty($e, 'isContentEditable', { value: true });
-
- // autofocus the document
- jsdomFocusDocument(document);
- }
-
+ // return a cleanup function
return () => $test.remove();
});
-// Helper function useful for testing error messages while being slightly more readable.
-export function e(name, message) {
- return { name, message };
-}
-
-// Helper function that attaches an event listener to an element and returns a reference to
-// collected results of the event. The return value references the element being listened on and a
-// count of triggered events. If a function is provided, it is also called on each event.
+// Test helper to create an event listener spy
export function listen(selector, event, fn) {
- let $el = document.querySelector(selector);
- let results = { count: 0, $el };
+ let $ = document.querySelector(selector);
+ let results = { $, calls: [] };
- $el.addEventListener(event, function(evt) {
- return (results.count++, fn?.call(this, evt));
+ $.addEventListener(event, function(...args) {
+ results.calls.push(args);
+ if (fn) return fn.apply(this, args);
});
return results;
}
-// Mock console methods for testing.
-export function mockConsole() {
- let names = ['warn'];
- let mock = {};
-
- let og = names.reduce((o, name) => (
- assign(o, { [name]: console[name] })
- ), {});
-
- beforeEach(() => {
- names.forEach(name => {
- mock[name] = console[name] = msg => mock[name].calls.push(msg);
- mock[name].calls = [];
- });
- });
+// Test helper to throw an error if an assertion fails
+export async function assert(assertion, failureMessage) {
+ let result = false;
- afterEach(() => {
- names.forEach(name => (console[name] = og[name]));
- });
+ if (typeof assertion === 'function')
+ result = await assertion();
+ else if (typeof assertion?.then === 'function')
+ result = await assertion;
+ else result = !!assertion;
- return mock;
+ if (!result) throw new Error(failureMessage);
}
-// Extend the assert function with other useful assertions.
-assign(assert, {
- typeOf(subj, expected, err) {
- let actual = typeof subj;
-
- return assert.equal(actual, expected, err || (
- new assert.AssertionError({
- operator: 'typeof',
- expected,
- actual
- })
- ));
- },
-
- instanceOf(subj, expected, err) {
- return assert(subj instanceof expected, err || (
- new assert.AssertionError({
- operator: 'instanceof',
- actual: subj,
- expected
- })
- ));
- },
-
- notInstanceOf(subj, expected, err) {
- return assert(!(subj instanceof expected), err || (
- new assert.AssertionError({
- operator: '!instanceof',
- actual: subj,
- expected
- })
- ));
- }
-});
+// Test helper to assert against thrown error messages
+assert.throws =
+ async function assertThrows(func, expectedMessage) {
+ try {
+ if (typeof func === 'function') await func();
+ else await func;
+ } catch (error) {
+ if (error.message === expectedMessage) return;
+
+ throw new Error(`Unexpected error: ${error.message}`, {
+ cause: error
+ });
+ }
-export { assert };
+ throw new Error('Expected an error to be thrown');
+ };
diff --git a/tests/index.js b/tests/index.js
index 3a714f21..02a01441 100644
--- a/tests/index.js
+++ b/tests/index.js
@@ -1,13 +1,10 @@
import { use, configure, run } from 'moonshiner';
import reporters from 'moonshiner/reporters';
-import middlewares from 'moonshiner/middlewares';
use(reporters.remote(event => Object.assign(event, {
__coverage__: globalThis.__coverage__
})));
-use(middlewares.bind(globalThis));
-
import('./**/*.test.js').then(() => {
configure({ timeout: 10_000 });
run();
diff --git a/tests/interactor.test.js b/tests/interactor.test.js
new file mode 100644
index 00000000..af48b87f
--- /dev/null
+++ b/tests/interactor.test.js
@@ -0,0 +1,605 @@
+import { describe, it, beforeEach } from 'moonshiner';
+import { Interactor, Interaction, Assert, Assertion } from 'interactor.js';
+import { I, fixture, assert } from './helpers';
+
+describe('Interactor', () => {
+ it('exports a default interactor instance', async () => {
+ let exports = await import('interactor.js');
+
+ await assert(exports.default instanceof Interactor,
+ 'Default export is not an interactor instance');
+ });
+
+ it('has default context options', async () => {
+ let ctx = new Interactor()[Interactor.Context.Symbol];
+
+ await assert(ctx.root() === document.body,
+ 'Expected the default root return the document body');
+ await assert(ctx.selector() === document.body,
+ 'Expected the default selector to return the root element');
+ await assert(ctx.assert.timeout === 1000,
+ 'Expected the default assert timeout to be 1000');
+ await assert(ctx.assert.frequency === 60,
+ 'Expected the default assert frequency to be 60');
+ await assert(ctx.assert.reliability === 1,
+ 'Expected the default assert reliability to be 1');
+ });
+
+ it('can be initialized with custom context options', async () => {
+ fixture('
test
');
+
+ let ctx = new Interactor({
+ assert: { timeout: 100, frequency: 10, reliability: 5 },
+ root: () => document.getElementById('testing-root'),
+ selector: 'test'
+ })[Interactor.Context.Symbol];
+
+ await assert(ctx.root()?.id === 'testing-root',
+ 'Expected the root id to be "testing-root"');
+ await assert(ctx.selector().tagName.toLowerCase() === 'p',
+ 'Expected the selector to return a paragraph element');
+ await assert(ctx.assert.timeout === 100,
+ 'Expected the assert timeout to be 100');
+ await assert(ctx.assert.frequency === 10,
+ 'Expected the assert frequency to be 10');
+ await assert(ctx.assert.reliability === 5,
+ 'Expected the assert reliability to be 5');
+ });
+
+ it('accepts an abort signal when awaiting on interactions', async () => {
+ let ctrl = new AbortController();
+ let test = 1;
+
+ await I.act(() => test++)
+ .then({ signal: ctrl.signal });
+
+ await assert.throws(
+ I.act(() => test++)
+ .then.act(() => test++)
+ .then.act(() => ctrl.abort(
+ new Error('Interaction aborted')))
+ .then.assert(() => test === 0)
+ .then({ signal: ctrl.signal }),
+ 'Interaction aborted');
+
+ await assert(test === 4,
+ 'Expected test to be 4');
+ });
+
+ describe('.defineAction(name, action)', () => {
+ it('defines an action', async () => {
+ class TestInteractor extends Interactor {}
+ let T = new TestInteractor();
+ let pass = false;
+
+ TestInteractor.defineAction('test', b => {
+ return pass = b;
+ });
+
+ await assert(typeof T.test === 'function',
+ 'Expected test to be a function');
+ await assert(T.test(true) instanceof Interaction,
+ 'Expected test to return an Interaction instance');
+ await assert(!pass,
+ 'Expected test not to pass');
+ await assert(await T.test(true) === true,
+ 'Expected test interaction to resolve true');
+ await assert(pass,
+ 'Expected test to pass');
+ });
+
+ it('accepts an interaction class', async () => {
+ class TestInteractor extends Interactor {}
+ let T = new TestInteractor();
+ let pass = false;
+
+ class TestInteraction extends Interaction {
+ constructor(b) { super(() => pass = b); }
+ }
+
+ TestInteractor.defineAction('test', TestInteraction);
+
+ await assert(typeof T.test === 'function',
+ 'Expected test to be a function');
+ await assert(T.test(true) instanceof TestInteraction,
+ 'Expected test to return an TestInteraction instance');
+ await assert(!pass,
+ 'Expected test not to pass');
+ await assert(await T.test(true) === true,
+ 'Expected test interaction to resolve true');
+ await assert(pass,
+ 'Expected test to pass');
+ });
+ });
+
+ describe('.defineActions(actions)', () => {
+ it('calls .defineAction() for every action', async () => {
+ let calls = [];
+
+ class TestInteractor extends Interactor {
+ static defineAction(...args) {
+ calls.push(args);
+ return this;
+ }
+ }
+
+ let foo = () => {};
+ let bar = class TestInteraction extends Interaction {};
+ TestInteractor.defineActions({ foo, bar });
+
+ await assert(calls.length === 2,
+ 'Expected .defineAction() to be called 2 times');
+ await assert(calls[0][0] === 'foo' && calls[0][1] === foo,
+ 'Expected .defineAction() to be called with foo');
+ await assert(calls[1][0] === 'bar' && calls[1][1] === bar,
+ 'Expected .defineAction() to be called with bar');
+ });
+ });
+
+ describe('.defineAssertion(name, assertion)', () => {
+ it('calls Assert.defineAssertion()', async () => {
+ let calls = [];
+
+ class TestInteractor extends Interactor {
+ static Assert = class TestAssert extends Assert {
+ static defineAssertion(...args) {
+ calls.push(args);
+ return this;
+ }
+ };
+ }
+
+ let test = () => {};
+ TestInteractor.defineAssertion('test', test);
+
+ await assert(calls.length === 1,
+ 'Expected .defineAssertion() to be called 1 time');
+ await assert(calls[0][0] === 'test' && calls[0][1] === test,
+ 'Expected .defineAssertion() to be called with test');
+ });
+ });
+
+ describe('.defineAssertions(assertions)', () => {
+ it('calls Assert.defineAssertions()', async () => {
+ let calls = [];
+
+ class TestInteractor extends Interactor {
+ static Assert = class TestAssert extends Assert {
+ static defineAssertions(...args) {
+ calls.push(args);
+ return this;
+ }
+ };
+ }
+
+ let assertions = { test: () => {} };
+ TestInteractor.defineAssertions(assertions);
+
+ await assert(calls.length === 1,
+ 'Expected .defineAssertion() to be called 1 time');
+ await assert(calls[0][0] === assertions,
+ 'Expected .defineAssertion() to be called with assertions');
+ });
+ });
+
+ describe('#act(interaction)', () => {
+ it('creates a new interaction', async () => {
+ await assert(I.act(() => {}) instanceof Interaction,
+ 'Expected action to be an Interaction instance');
+ await assert(await I.act(() => 'test') === 'test',
+ 'Expected action to return "test"');
+ });
+
+ it('accepts an interaction instance', async () => {
+ let interaction = new Interaction(() => 'test');
+ await assert(I.act(interaction) instanceof Interaction,
+ 'Expected action to be an Interaction instance');
+ await assert(await I.act(interaction) === 'test',
+ 'Expected action to return "test"');
+ });
+
+ it('can be chained with other interactions', async () => {
+ let test = [];
+
+ let interaction = I.act(() => test.push('foo'))
+ .then.assert(() => test.push('bar'));
+
+ await assert(interaction instanceof Assertion,
+ 'Expected interaction to be an Assertion instance');
+ await assert(await interaction && test.length === 2,
+ 'Expected interaction test to have 2 values');
+ await assert(test[0] === 'foo' && test[1] === 'bar',
+ 'Expected test array to contain "foo", "bar"');
+ });
+
+ it('can set context properties for nested interactions', async () => {
+ let interaction = I.act(({ set }) => {
+ set({ test: 'foobar' });
+ return I.act(({ test }) => test);
+ });
+
+ await assert(await interaction === 'foobar',
+ 'Expected action to return "foobar"');
+ });
+
+ it('references the root element by default', async () => {
+ await assert(await I.act(({ $ }) => $.id) === 'testing-root',
+ 'Expected root element id to be "testing-root"');
+ await assert(await I.act(({ $$ }) => $$.length) === 1,
+ 'Expected a single root element');
+ });
+ });
+
+ describe('#assert(assertion)', () => {
+ it('creates a new assertion', async () => {
+ await assert(I.assert(true) instanceof Assertion,
+ 'Expected assertion to be an Assertion instance');
+ await assert(I.assert(() => true),
+ 'Expected assertion to pass when true');
+ await assert(I.assert(() => {}),
+ 'Expected assertion to pass when undefined');
+ });
+
+ it('accepts a failure message', async () => {
+ await assert.throws(I.assert(false),
+ 'Assertion failed');
+ await assert.throws(I.assert(() => false, 'Epic fail'),
+ 'Epic fail');
+ });
+
+ it('accepts an assertion instance', async () => {
+ let assertion = new Assertion(() => true);
+ await assert(I.assert(assertion) instanceof Assertion,
+ 'Expected assertion to be an Assertion instance');
+ await assert(await I.assert(assertion),
+ 'Expected assertion to pass');
+ });
+
+ it('can be chained with other interactions', async () => {
+ let test = [];
+
+ let interaction = I.assert(() => test.push('foo'))
+ .then.act(() => test.push('bar'));
+
+ await assert(interaction instanceof Interaction,
+ 'Expected interaction to be an Interaction instance');
+ await assert(await interaction && test.length === 2,
+ 'Expected interaction test to have 2 values');
+ await assert(test[0] === 'foo' && test[1] === 'bar',
+ 'Expected test array to contain "foo", "bar"');
+ });
+
+ it('retries assertions until passing', async () => {
+ let attempts = 1;
+
+ await I.assert(() => (attempts += 1) === 5);
+
+ await assert(attempts === 5,
+ 'Expected 5 assertion attempts');
+ });
+
+ it('runs assertions again after passing', async () => {
+ let T = new Interactor({ assert: { reliability: 5 } });
+ let attempts = 0;
+
+ await T.assert(() => (attempts += 1) >= 5);
+
+ await assert(attempts === 10,
+ 'Expected 10 assertion attempts');
+ });
+
+ it('references the root element by default', async () => {
+ await assert(I.assert(({ $ }) => $.id === 'testing-root'),
+ 'Expected root element id to be "testing-root"');
+
+ await assert.throws(
+ I.assert(({ $ }) => $.id !== 'testing-root',
+ 'Expected #{this} id to be "testing-root"'),
+ 'Expected root element id to be "testing-root"');
+ });
+ });
+
+ describe('#find(selector)', () => {
+ beforeEach(() => {
+ fixture(`
+
Foo
+
Bar
+
Baz
+
Qux
+
Foo
+ `);
+ });
+
+ it('creates a new find assert', async () => {
+ await assert(I.find('Foo') instanceof Assert,
+ 'Expected #find() to return an Assert instance');
+ await assert((await I.find('Foo')).innerText === 'Foo',
+ 'Expected to find the "Foo" element');
+ await assert.throws(I.find('foo'),
+ 'Could not find "foo"');
+ });
+
+ it('accepts a query selector syntax', async () => {
+ await assert((await I.find('$(.bar)')).innerText === 'Bar',
+ 'Expected to find the "Bar" element');
+ });
+
+ it('accepts a test attribute selector syntax', async () => {
+ await assert((await I.find('::(baz)')).innerText === 'Baz',
+ 'Expected to find the "Baz" element');
+ await assert((await I.find('::qux')).innerText === 'Qux',
+ 'Expected to find the "Qux" element');
+ });
+
+ it('accepts a combined selector syntax', async () => {
+ let $foo = document.querySelector('p:last-child');
+
+ await assert(await I.find('"Foo" $(:last-child)') === $foo,
+ 'Expected to find the last "Foo" element');
+ });
+
+ it('accepts a selector function', async () => {
+ let $bar = document.querySelector('.bar');
+
+ await assert((await I.find(() => $bar)) === $bar,
+ 'Expected to resolve the "Bar" element');
+ });
+
+ it('accepts element instances', async () => {
+ let $baz = document.querySelector('[data-test="baz"]');
+
+ await assert((await I.find($baz)) === $baz,
+ 'Expected to resolve the "Baz" element');
+ });
+
+ it('does not accept an invalid selector', async () => {
+ await assert.throws(() => I.find('$(.bar) ::baz'),
+ 'Invalid selector: $(.bar) ::baz');
+ });
+
+ it('uses the selector in error messages', async () => {
+ let $qux = document.querySelector('[data-test-qux]');
+
+ await assert.throws(
+ I.find('::qux').then.assert(false, '#{this} is "Qux"'),
+ '::qux is "Qux"');
+ await assert.throws(
+ I.find(() => $qux).then.assert(false, '#{this} is "Qux"'),
+ 'element is "Qux"');
+ await assert.throws(
+ I.find($qux).then.assert(false, '#{this} is "Qux"'),
+ 'p element is "Qux"');
+ });
+
+ it('finds form elements by label', async () => {
+ fixture(`
+
+
+
+ `);
+
+ await assert((await I.find('Foo')).value === 'Bar',
+ 'Expected to find the input associated with the label');
+ await assert((await I.find('"Foo" $(label)')).control?.value === 'Bar',
+ 'Expected to find the label element itself');
+ await assert((await I.find('Baz')).htmlFor === 'nothing',
+ 'Expected to find the label element without an associated input');
+ });
+
+ it('finds form elements by placeholder', async () => {
+ fixture('');
+
+ await assert((await I.find('Foo')).placeholder === 'Foo',
+ 'Expected to find the "Foo" input by placeholder');
+ });
+
+ it('does not search for elements outside of the root element', async () => {
+ fixture(`
+
Foo
+
Bar
+ `);
+
+ let F = new Interactor({
+ root: () => document.querySelector('.foo-root'),
+ assert: { timeout: 100, reliability: 0 }
+ });
+
+ await F.find('Foo');
+ await assert.throws(() => F.find('Bar'),
+ 'Could not find "Bar"');
+
+ let B = new Interactor({
+ root: () => document.querySelector('.bar-root'),
+ assert: { timeout: 100, reliability: 0 }
+ });
+
+ await B.find('Bar');
+ await assert.throws(() => B.find('Foo'),
+ 'Could not find "Foo"');
+ });
+
+ it('uses the found element for the rest of the interaction', async () => {
+ let test = I.find('Foo')
+ .then.act(({ $ }) => $.innerText);
+
+ await assert(await test === 'Foo',
+ 'Expected to resolve to "Foo" inner text');
+ });
+
+ it('finds subsequent elements when chained', async () => {
+ let $foo = document.querySelector('p:last-child');
+
+ await assert(await I.find('Foo').then.find('Foo') === $foo,
+ 'Expected to find the second "Foo" element');
+ });
+
+ describe('#times(number)', () => {
+ it('creates a new assertion to expect many elements', async () => {
+ await assert(I.find('Foo').times(2) instanceof Assertion,
+ 'Expected .times() to return an Assertion instance');
+
+ await I.find('Foo').times(2);
+ await I.find('Bar').not.times(2);
+
+ await assert.throws(I.find('Foo').not.times(2),
+ 'Expected not to find "Foo" 2 times');
+ await assert.throws(I.find('Bar').times(3),
+ 'Expected to find "Bar" 3 times');
+ });
+ });
+ });
+});
+
+describe('Assert', () => {
+ it('is accessible via the #assert property', async () => {
+ await assert(
+ I.assert instanceof Assert,
+ 'I.assert is not an Assert instance');
+ });
+
+ describe('.defineAssertion(name, assertion)', () => {
+ let TestAssert, TestInteractor, T;
+
+ beforeEach(() => {
+ TestAssert = class TestAssert extends Assert {};
+ TestInteractor = class TestInteractor extends Interactor {
+ static Assert = TestAssert;
+ };
+
+ T = new TestInteractor({
+ assert: { timeout: 100, reliability: 0 }
+ });
+ });
+
+ it('defines an assertion', async () => {
+ let pass = false;
+
+ TestAssert.defineAssertion('test', b => {
+ return pass = b;
+ });
+
+ await assert(typeof T.assert.test === 'function',
+ 'Expected test to be a function');
+ await assert(T.assert.test(true) instanceof Assertion,
+ 'Expected test to return an Interaction instance');
+ await assert(!pass,
+ 'Expected test not to pass');
+ await assert(await T.assert.test(true) === true,
+ 'Expected test assertion to resolve true');
+ await assert(pass,
+ 'Expected test to pass');
+ });
+
+ it('accepts failure and negated messages', async () => {
+ TestAssert.defineAssertion('test',
+ b => b,
+ 'Failure message',
+ 'Negated message'
+ );
+
+ await assert(typeof T.assert.test === 'function',
+ 'Expected test to be a function');
+ await assert(T.assert.test(true) instanceof Assertion,
+ 'Expected test to return an Interaction instance');
+ await assert.throws(T.assert.test(false),
+ 'Failure message');
+ await assert.throws(T.assert.not.test(true),
+ 'Negated message');
+ });
+
+ it('accepts an assertion object', async () => {
+ TestAssert.defineAssertion('test', {
+ assertion: b => b,
+ failureMessage: 'Failure message',
+ negatedMessage: 'Negated message'
+ });
+
+ await assert(typeof T.assert.test === 'function',
+ 'Expected test to be a function');
+ await assert(T.assert.test(true) instanceof Assertion,
+ 'Expected test to return an Interaction instance');
+ await assert.throws(T.assert.test(false),
+ 'Failure message');
+ await assert.throws(T.assert.not.test(true),
+ 'Negated message');
+ });
+
+ it('accepts an assertion class', async () => {
+ let pass = false;
+
+ class TestAssertion extends Assertion {
+ constructor(b) { super(() => pass = b); }
+ }
+
+ TestAssert.defineAssertion('test', TestAssertion);
+
+ await assert(typeof T.assert.test === 'function',
+ 'Expected test to be a function');
+ await assert(T.assert.test(true) instanceof TestAssertion,
+ 'Expected test to return an TestInteraction instance');
+ await assert(!pass,
+ 'Expected test not to pass');
+ await assert(await T.assert.test(true) === true,
+ 'Expected test assertion to resolve true');
+ await assert(pass,
+ 'Expected test to pass');
+ });
+ });
+
+ describe('.defineAssertions(assertions)', () => {
+ it('calls .defineAssertion() for every assertion', async () => {
+ let calls = [];
+
+ class TestAssert extends Assert {
+ static defineAssertion(...args) {
+ calls.push(args);
+ return this;
+ }
+ }
+
+ let foo = () => {};
+ let bar = { assertion: () => {} };
+ let baz = class TestAssertion extends Assertion {};
+ TestAssert.defineAssertions({ foo, bar, baz });
+
+ await assert(calls.length === 3,
+ 'Expected .defineAssertion() to be called 3 times');
+ await assert(calls[0][0] === 'foo' && calls[0][1] === foo,
+ 'Expected .defineAssertion() to be called with foo');
+ await assert(calls[1][0] === 'bar' && calls[1][1] === bar,
+ 'Expected .defineAssertion() to be called with bar');
+ await assert(calls[2][0] === 'baz' && calls[2][1] === baz,
+ 'Expected .defineAssertion() to be called with baz');
+ });
+ });
+
+ describe('#not', () => {
+ it('negates the following assertion', async () => {
+ await I.assert.not(false);
+ await I.assert.not(() =>
+ Promise.reject(new Error('test')));
+ await I.assert.not(new Assertion({
+ assertion: () => false
+ }));
+
+ await assert.throws(
+ I.assert.not(() => true),
+ 'Expected assertion to fail');
+ await assert.throws(
+ I.assert.not(true, 'But is true'),
+ 'But is true');
+ await assert.throws(
+ I.assert.not(new Assertion({
+ assertion: () => true,
+ negatedMessage: 'But is true'
+ })),
+ 'But is true');
+ });
+
+ it('cannot be used more than once for an assertion', async () => {
+ await assert.throws(
+ () => I.assert.not.not,
+ 'Double negative');
+ });
+ });
+});
diff --git a/tests/start.js b/tests/run.js
similarity index 89%
rename from tests/start.js
rename to tests/run.js
index 397b7a75..0d239262 100644
--- a/tests/start.js
+++ b/tests/run.js
@@ -5,7 +5,9 @@ import launch from 'moonshiner/launchers';
import cov from 'istanbul-lib-coverage';
// create test server
-const testServer = createTestServer();
+const testServer = createTestServer({
+ debug: process.argv.includes('--debug')
+});
// use reporters
testServer.use(reporters.emoji());
@@ -22,10 +24,7 @@ testServer.use(reporters.createReporter({
// use launchers
testServer.use(launch.firefox());
-testServer.use(launch.chrome());
-testServer.use(launch.fork('JSDOM', {
- modulePath: './tests/jsdom.js'
-}));
+// testServer.use(launch.chrome());
// use bundler
testServer.use(middlewares.listen(async () => {
diff --git a/tests/tsconfig.json b/tests/tsconfig.json
new file mode 100644
index 00000000..1a97a65a
--- /dev/null
+++ b/tests/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "include": ["./**/*.test.js"],
+ "references": [{ "path": ".." }],
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "lib": ["esnext", "dom"],
+ "allowJs": true,
+ "checkJs": true,
+ "noEmit": true,
+ "paths": {
+ "interactor.js": ["../types/index.d.ts"]
+ }
+ }
+}