563 lines
16 KiB
JavaScript
563 lines
16 KiB
JavaScript
import Alpine from 'alpinejs'
|
|
import { wait, fireEvent } from '@testing-library/dom'
|
|
const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
|
|
global.MutationObserver = class {
|
|
observe() {}
|
|
}
|
|
|
|
test('data modified in event listener updates affected attribute bindings', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar' }">
|
|
<button x-on:click="foo = 'baz'"></button>
|
|
|
|
<span x-bind:foo="foo"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
|
|
})
|
|
|
|
test('nested data modified in event listener updates affected attribute bindings', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ nested: { foo: 'bar' } }">
|
|
<button x-on:click="nested.foo = 'baz'"></button>
|
|
|
|
<span x-bind:foo="nested.foo"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
|
|
})
|
|
|
|
test('.passive modifier should disable e.preventDefault()', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ defaultPrevented: null }">
|
|
<button
|
|
x-on:mousedown.passive="
|
|
$event.preventDefault();
|
|
defaultPrevented = $event.defaultPrevented;
|
|
"
|
|
>
|
|
<span></span>
|
|
</button>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(null)
|
|
|
|
fireEvent.mouseDown(document.querySelector('button'))
|
|
|
|
await wait(() => {
|
|
expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(false)
|
|
})
|
|
})
|
|
|
|
test('.stop modifier', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar' }">
|
|
<button x-on:click="foo = 'baz'">
|
|
<span></span>
|
|
</button>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('div').__x.$data.foo).toEqual('bar')
|
|
|
|
document.querySelector('span').click()
|
|
|
|
await wait(() => {
|
|
expect(document.querySelector('div').__x.$data.foo).toEqual('baz')
|
|
})
|
|
})
|
|
|
|
test('.self modifier', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar' }">
|
|
<div x-on:click.self="foo = 'baz'" id="selfTarget">
|
|
<button></button>
|
|
</div>
|
|
<span x-text="foo"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').textContent).toEqual('bar')
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await wait(() => {
|
|
expect(document.querySelector('span').textContent).toEqual('bar')
|
|
})
|
|
|
|
document.querySelector('#selfTarget').click()
|
|
|
|
await wait(() => {
|
|
expect(document.querySelector('span').textContent).toEqual('baz')
|
|
})
|
|
})
|
|
|
|
test('.prevent modifier', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{}">
|
|
<input type="checkbox" x-on:click.prevent>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('input').checked).toEqual(false)
|
|
|
|
document.querySelector('input').click()
|
|
|
|
expect(document.querySelector('input').checked).toEqual(false)
|
|
})
|
|
|
|
test('.window modifier', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar' }">
|
|
<div x-on:click.window="foo = 'baz'"></div>
|
|
|
|
<span x-bind:foo="foo"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
|
|
|
|
document.body.click()
|
|
|
|
await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
|
|
})
|
|
|
|
test('unbind global event handler when element is removed', async () => {
|
|
document._callCount = 0
|
|
|
|
document.body.innerHTML = `
|
|
<div x-data="{}">
|
|
<div x-on:click.window="document._callCount += 1"></div>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
document.body.click()
|
|
|
|
document.body.innerHTML = ''
|
|
|
|
document.body.click()
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1))
|
|
|
|
expect(document._callCount).toEqual(1)
|
|
})
|
|
|
|
test('.document modifier', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar' }">
|
|
<div x-on:click.document="foo = 'baz'"></div>
|
|
|
|
<span x-bind:foo="foo"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
|
|
|
|
document.body.click()
|
|
|
|
await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
|
|
})
|
|
|
|
test('.once modifier', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ count: 0 }">
|
|
<button x-on:click.once="count = count+1"></button>
|
|
|
|
<span x-bind:foo="count"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('0')
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('1') })
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await timeout(25)
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('1')
|
|
})
|
|
|
|
test('.once modifier does not remove listener if false is returned', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ count: 0 }">
|
|
<button x-on:click.once="return ++count === 2"></button>
|
|
|
|
<span x-bind:foo="count"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('0')
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('1') })
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('2') })
|
|
|
|
await timeout(25)
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('2')
|
|
})
|
|
|
|
test('keydown modifiers', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ count: 0 }">
|
|
<input type="text" x-on:keydown="count++" x-on:keydown.enter="count++" x-on:keydown.space="count++">
|
|
|
|
<span x-text="count"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').textContent).toEqual('0')
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: 'Enter' })
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('2') })
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: ' ' })
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('4') })
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: 'Spacebar' })
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('6') })
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: 'Escape' })
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('7') })
|
|
})
|
|
|
|
test('keydown combo modifiers', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ count: 0 }">
|
|
<input type="text" x-on:keydown.cmd.enter="count++">
|
|
|
|
<span x-text="count"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').textContent).toEqual('0')
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: 'Enter' })
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('0') })
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: 'Enter', metaKey: true })
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('1') })
|
|
})
|
|
|
|
test('keydown with specified key and stop modifier only stops for specified key', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ count: 0 }">
|
|
<article x-on:keydown="count++">
|
|
<input type="text" x-on:keydown.enter.stop>
|
|
</article>
|
|
|
|
<span x-text="count"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').textContent).toEqual('0')
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: 'Escape' })
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('1') })
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: 'Enter' })
|
|
|
|
await timeout(25)
|
|
expect(document.querySelector('span').textContent).toEqual('1')
|
|
})
|
|
|
|
test('click away', async () => {
|
|
// Because jsDom doesn't support .offsetHeight and offsetWidth, we have to
|
|
// make our own implementation using a specific class added to the class. Ugh.
|
|
Object.defineProperties(window.HTMLElement.prototype, {
|
|
offsetHeight: {
|
|
get: function () { return this.classList.contains('hidden') ? 0 : 1 }
|
|
},
|
|
offsetWidth: {
|
|
get: function () { return this.classList.contains('hidden') ? 0 : 1 }
|
|
}
|
|
});
|
|
|
|
document.body.innerHTML = `
|
|
<div id="outer">
|
|
<div x-data="{ isOpen: true }">
|
|
<button x-on:click="isOpen = true"></button>
|
|
|
|
<ul x-bind:class="{ 'hidden': ! isOpen }" x-on:click.away="isOpen = false">
|
|
<li>...</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('ul').classList.contains('hidden')).toEqual(false)
|
|
|
|
document.querySelector('li').click()
|
|
|
|
await wait(() => { expect(document.querySelector('ul').classList.contains('hidden')).toEqual(false) })
|
|
|
|
document.querySelector('ul').click()
|
|
|
|
await wait(() => { expect(document.querySelector('ul').classList.contains('hidden')).toEqual(false) })
|
|
|
|
document.querySelector('#outer').click()
|
|
|
|
await wait(() => { expect(document.querySelector('ul').classList.contains('hidden')).toEqual(true) })
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await wait(() => { expect(document.querySelector('ul').classList.contains('hidden')).toEqual(false) })
|
|
})
|
|
|
|
test('.passive + .away modifier still disables e.preventDefault()', async () => {
|
|
// Pretend like all the elements are visible
|
|
Object.defineProperties(window.HTMLElement.prototype, {
|
|
offsetHeight: {
|
|
get: () => 1
|
|
},
|
|
offsetWidth: {
|
|
get: () => 1
|
|
}
|
|
});
|
|
document.body.innerHTML = `
|
|
<div x-data="{ defaultPrevented: null }">
|
|
<button
|
|
x-on:mousedown.away.passive="
|
|
$event.preventDefault();
|
|
defaultPrevented = $event.defaultPrevented;
|
|
"
|
|
></button>
|
|
<span></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(null)
|
|
|
|
fireEvent.mouseDown(document.querySelector('span'))
|
|
|
|
await wait(() => {
|
|
expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(false)
|
|
})
|
|
})
|
|
|
|
test('supports short syntax', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar' }">
|
|
<button @click="foo = 'baz'"></button>
|
|
<span x-bind:foo="foo"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
|
|
})
|
|
|
|
test('event with colon', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar' }">
|
|
<div x-on:my:event.document="foo = 'baz'"></div>
|
|
|
|
<span x-bind:foo="foo"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
|
|
|
|
var event = new CustomEvent('my:event');
|
|
|
|
document.dispatchEvent(event);
|
|
|
|
await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
|
|
})
|
|
|
|
test.skip('prevent default action when an event returns false', async () => {
|
|
// This test is skipped because in a browser this works, but it won't
|
|
// pass in this tests unless we bypass the promise resolving system
|
|
// for the result of an event handler expression.
|
|
window.confirm = jest.fn().mockReturnValue(false)
|
|
|
|
document.body.innerHTML = `
|
|
<div x-data="{}">
|
|
<input type="checkbox" x-on:click="return confirm('are you sure?')">
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('input').checked).toEqual(false)
|
|
|
|
document.querySelector('input').click()
|
|
|
|
expect(document.querySelector('input').checked).toEqual(false)
|
|
|
|
window.confirm = jest.fn().mockReturnValue(true)
|
|
|
|
document.querySelector('input').click()
|
|
|
|
expect(document.querySelector('input').checked).toEqual(true)
|
|
})
|
|
|
|
test('allow method reference to be passed to listeners', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar', changeFoo() { this.foo = 'baz' } }">
|
|
<button x-on:click="changeFoo"></button>
|
|
<span x-text="foo"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').textContent).toEqual('bar')
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1))
|
|
|
|
expect(document.querySelector('span').textContent).toEqual('baz')
|
|
})
|
|
|
|
test('event instance is passed to method reference', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar', changeFoo(e) { this.foo = e.target.id } }">
|
|
<button x-on:click="changeFoo" id="baz"></button>
|
|
<span x-text="foo"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').textContent).toEqual('bar')
|
|
|
|
document.querySelector('button').click()
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1))
|
|
|
|
expect(document.querySelector('span').textContent).toEqual('baz')
|
|
})
|
|
|
|
test('autocomplete event does not trigger keydown with modifier callback', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ count: 0 }">
|
|
<input type="text" x-on:keydown.?="count++">
|
|
|
|
<span x-text="count"></span>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('span').textContent).toEqual('0')
|
|
|
|
const autocompleteEvent = new Event('keydown')
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: 'Enter' })
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('0') })
|
|
|
|
fireEvent.keyDown(document.querySelector('input'), { key: '?' })
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('1') })
|
|
|
|
fireEvent(document.querySelector('input'), autocompleteEvent)
|
|
|
|
await wait(() => { expect(document.querySelector('span').textContent).toEqual('1') })
|
|
})
|
|
|
|
test('.camel modifier correctly binds event listener', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar' }" x-on:event-name.camel.window="foo = 'bob'">
|
|
<button x-on:click="$dispatch('eventName')"></button>
|
|
<p x-text="foo"></p>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('p').textContent).toEqual('bar')
|
|
|
|
document.querySelector('button').click();
|
|
|
|
await wait(() => {
|
|
expect(document.querySelector('p').textContent).toEqual('bob');
|
|
});
|
|
})
|
|
|
|
test('.camel modifier correctly binds event listener with namespace', async () => {
|
|
document.body.innerHTML = `
|
|
<div x-data="{ foo: 'bar' }" x-on:ns:event-name.camel.window="foo = 'bob'">
|
|
<button x-on:click="$dispatch('ns:eventName')"></button>
|
|
<p x-text="foo"></p>
|
|
</div>
|
|
`
|
|
|
|
Alpine.start()
|
|
|
|
expect(document.querySelector('p').textContent).toEqual('bar')
|
|
|
|
document.querySelector('button').click();
|
|
|
|
await wait(() => {
|
|
expect(document.querySelector('p').textContent).toEqual('bob');
|
|
});
|
|
})
|