From eb102e35e66d28b8e522dbfaba477680b36d23c6 Mon Sep 17 00:00:00 2001 From: Wil Wilsman Date: Tue, 6 Feb 2024 12:19:50 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Rewrite=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 +- tests/actions/blur.test.js | 31 ++ tests/actions/click.test.js | 127 ++++++ tests/actions/focus.test.js | 30 ++ tests/actions/press.test.js | 150 +++++++ tests/actions/trigger.test.js | 33 ++ tests/actions/type.test.js | 60 +++ tests/assertions/attribute.test.js | 24 ++ tests/assertions/checked.test.js | 27 ++ tests/assertions/disabled.test.js | 25 ++ tests/assertions/exists.test.js | 22 + tests/assertions/focusable.test.js | 44 ++ tests/assertions/focused.test.js | 27 ++ tests/assertions/matches.test.js | 22 + tests/assertions/overflows.test.js | 58 +++ tests/assertions/property.test.js | 24 ++ tests/assertions/scrollable.test.js | 66 +++ tests/assertions/selected.test.js | 27 ++ tests/assertions/text.test.js | 22 + tests/assertions/value.test.js | 22 + tests/assertions/visible.test.js | 70 ++++ tests/helpers.js | 164 ++------ tests/index.js | 3 - tests/interactor.test.js | 605 ++++++++++++++++++++++++++++ tests/{start.js => run.js} | 9 +- tests/tsconfig.json | 16 + 26 files changed, 1578 insertions(+), 136 deletions(-) create mode 100644 tests/actions/blur.test.js create mode 100644 tests/actions/click.test.js create mode 100644 tests/actions/focus.test.js create mode 100644 tests/actions/press.test.js create mode 100644 tests/actions/trigger.test.js create mode 100644 tests/actions/type.test.js create mode 100644 tests/assertions/attribute.test.js create mode 100644 tests/assertions/checked.test.js create mode 100644 tests/assertions/disabled.test.js create mode 100644 tests/assertions/exists.test.js create mode 100644 tests/assertions/focusable.test.js create mode 100644 tests/assertions/focused.test.js create mode 100644 tests/assertions/matches.test.js create mode 100644 tests/assertions/overflows.test.js create mode 100644 tests/assertions/property.test.js create mode 100644 tests/assertions/scrollable.test.js create mode 100644 tests/assertions/selected.test.js create mode 100644 tests/assertions/text.test.js create mode 100644 tests/assertions/value.test.js create mode 100644 tests/assertions/visible.test.js create mode 100644 tests/interactor.test.js rename tests/{start.js => run.js} (89%) create mode 100644 tests/tsconfig.json diff --git a/package.json b/package.json index 9cc665a7..86bb37f0 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "test": "npm run lint && npm run test:types && npm run test:coverage", "test:coverage": "nyc npm run test:only", "test:debug": "npm run test:only -- --debug", - "test:only": "NODE_ENV=test node tests/start.js", - "test:types": "tsc --build tests", + "test:only": "NODE_ENV=test node tests/run.js", + "test:types": "tsc --project tests", "lint": "eslint --ignore-path .gitignore --ignore-pattern www ." }, "devDependencies": { @@ -44,7 +44,7 @@ "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.1.1", "istanbul-lib-coverage": "^3.2.2", - "moonshiner": "^0.14.0", + "moonshiner": "file:../moonshiner", "nyc": "^15.1.0", "rollup": "^4.9.6", "rollup-plugin-glob-import": "^0.5.0", diff --git a/tests/actions/blur.test.js b/tests/actions/blur.test.js new file mode 100644 index 00000000..14afe122 --- /dev/null +++ b/tests/actions/blur.test.js @@ -0,0 +1,31 @@ +import { describe, it, beforeEach } from 'moonshiner'; +import { I, assert, fixture } from '../helpers'; + +describe('Actions | #blur(selector?)', () => { + beforeEach(() => { + fixture(` +

Foo

+ + `); + }); + + it('blurs the current element', async () => { + let $foo = document.querySelector('h1'); + let $bar = document.querySelector('button'); + + $foo.focus(); + await I.blur('Foo'); + await assert(document.activeElement !== $foo, + 'Expected "Foo" to not be focused'); + + $bar.focus(); + await I.find('Bar').then.blur(); + await assert(document.activeElement !== $bar, + 'Expected "Bar" to not be focused'); + }); + + it('asserts the element is focused', async () => { + await assert.throws(I.blur('Foo'), + '"Foo" is not focused'); + }); +}); diff --git a/tests/actions/click.test.js b/tests/actions/click.test.js new file mode 100644 index 00000000..b9cc1224 --- /dev/null +++ b/tests/actions/click.test.js @@ -0,0 +1,127 @@ +import { describe, it } from 'moonshiner'; +import { I, assert, fixture, listen } from '../helpers'; + +describe('Actions | #click(selector?)', () => { + it('clicks the current element', async () => { + fixture(''); + let event = listen('button', 'click'); + + await I.click('Foo'); + await assert(event.calls.length === 1, + 'Expected to click "Foo"'); + + await I.find('Foo').then.click(); + await assert(event.calls.length === 2, + 'Expected to click "Foo" a second time'); + }); + + it('asserts the element is not disabled', async () => { + fixture(''); + let event = listen('button', 'click'); + + await assert.throws(I.click('Bar'), + '"Bar" is disabled'); + await assert(event.calls.length === 0, + 'Expected "Bar" to not be clicked'); + }); + + it('checks and unchecks checkbox and radio elements', async () => { + fixture(` + + + + + + + `); + + await I.click('Baz'); + await assert(document.getElementById('check').checked === true, + 'Expected "Baz" to be checked'); + + await I.click('Baz'); + await assert(document.getElementById('check').checked === false, + 'Expected "Baz" to not be checked'); + + await I.click('Qux'); + await assert(document.getElementById('radio-1').checked === true, + 'Expected "Qux" to be checked'); + await assert(document.getElementById('radio-2').checked === false, + 'Expected "Xyzzy" to not be checked'); + + await I.click('Xyzzy'); + await assert(document.getElementById('radio-1').checked === false, + 'Expected "Qux" to not be checked'); + await assert(document.getElementById('radio-2').checked === true, + 'Expected "Xyzzy" to be checked'); + }); + + it('selects options within select elements', async () => { + fixture(` + + `); + + let input = listen('select', 'input'); + let change = listen('select', 'change'); + + await I.click('Foo'); + await assert(input.$.value === 'Foo', + 'Expected "Foo" to be selected'); + + await I.click('Bar'); + await assert(input.$.value === 'Bar', + 'Expected "Bar" to be selected'); + + await assert(input.calls.length === 2, + 'Expected input events when selecting options'); + await assert(change.calls.length === 2, + 'Expected change events when selecting options'); + }); + + it('asserts the option select element is not disabled', async () => { + fixture(` + + `); + + let event = listen('select', 'change'); + + await assert.throws(I.click('Foo'), + '"Foo" select is disabled'); + await assert(event.calls.length === 0, + 'Expected change event not to be triggered'); + }); + + it('selects and deselects options within multi-select elements', async () => { + fixture(` + + `); + + let event = listen('select', 'change'); + + await I.click('Foo'); + await I.click('Bar'); + + await assert( + event.$.selectedOptions[0].value === 'Foo' && + event.$.selectedOptions[1].value === 'Bar', + 'Expected "Foo" and "Bar" to be selected'); + + await I.click('Foo'); + + await assert( + event.$.selectedOptions[0].value === 'Bar' && + !event.$.selectedOptions[1], + 'Expected only "Bar" to be selected'); + }); +}); diff --git a/tests/actions/focus.test.js b/tests/actions/focus.test.js new file mode 100644 index 00000000..869e8bef --- /dev/null +++ b/tests/actions/focus.test.js @@ -0,0 +1,30 @@ +import { describe, it, beforeEach } from 'moonshiner'; +import { I, assert, fixture } from '../helpers'; + +describe('Actions | #focus(selector?)', () => { + beforeEach(() => { + fixture(` +

Foo

+ + + `); + }); + + it('focuses the current element', async () => { + let $foo = document.querySelector('h1'); + let $bar = document.querySelector('button'); + + await I.focus('Foo'); + await assert(document.activeElement === $foo, + 'Expected "Foo" to have focus'); + + await I.find('Bar').then.focus(); + await assert(document.activeElement === $bar, + 'Expected "Bar" to have focus'); + }); + + it('asserts the element is focusable', async () => { + await assert.throws(I.focus('Baz'), + '"Baz" is not focusable'); + }); +}); diff --git a/tests/actions/press.test.js b/tests/actions/press.test.js new file mode 100644 index 00000000..96e01c80 --- /dev/null +++ b/tests/actions/press.test.js @@ -0,0 +1,150 @@ +import { describe, it } from 'moonshiner'; +import { I, assert, fixture, listen } from '../helpers'; + +describe('Actions | #press(keys, options?)', () => { + it('triggers keyboard events for the current element', async () => { + fixture('
Foo
'); + + let events = { + keydown: listen('.foo', 'keydown'), + beforeinput: listen('.foo', 'beforeinput'), + input: listen('.foo', 'input'), + keyup: listen('.foo', 'keyup') + }; + + await I.find('Foo') + .then.press('Delete'); + + for (let event in events) { + if (event === 'keydown' || event === 'keyup') { + await assert( + events[event].calls.length === 1 && + events[event].calls[0][0].key === 'Delete', + `Expected \`${event}\` event for "Delete"`); + } else { + await assert(events[event].calls.length === 0, + `Unexpected \`${event}\` event for "Delete"`); + } + } + }); + + it('triggers input events for the current input element', async () => { + fixture(''); + + let events = { + keydown: listen('input', 'keydown'), + beforeinput: listen('input', 'beforeinput', e => { + if (e.key === 'C') e.preventDefault(); + }), + input: listen('input', 'input'), + keyup: listen('input', 'keyup') + }; + + await I.find('Bar') + .then.press('B'); + + for (let event in events) { + await assert( + events[event].calls.length === 1 && + events[event].calls[0][0].key === 'B', + `Expected \`${event}\` event for "B"`); + } + + await I.find('Bar') + .then.press('C'); + + await assert(events.beforeinput.calls.length === 2, + 'Expected a second beforeinput event'); + await assert(events.input.calls.length === 1, + 'Expected second input event to be canceled'); + }); + + it('does not accept arbitrary key names', async () => { + await assert.throws(() => I.press('KeyFoo'), + 'Unknown key `KeyFoo`'); + }); + + it('can insert or replace text within relevant elements', async () => { + fixture('
Fo
'); + 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('