@rokkit/helpers 1.0.0-next.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/matchers/action.d.ts +27 -0
- package/dist/src/matchers/array.d.ts +10 -0
- package/dist/src/matchers/dataset.d.ts +10 -0
- package/dist/src/matchers/event.d.ts +10 -0
- package/dist/src/matchers/index.d.ts +4 -0
- package/dist/src/matchers/internal.d.ts +1 -0
- package/dist/src/mocks/animate.d.ts +1 -0
- package/dist/src/mocks/element.d.ts +56 -0
- package/dist/src/mocks/index.d.ts +2 -0
- package/dist/src/mocks/match-media.d.ts +30 -0
- package/dist/src/mocks/resize-observer.d.ts +9 -0
- package/dist/src/simulators/touch.d.ts +16 -0
- package/dist/vitest.config.d.ts +1 -0
- package/package.json +35 -0
- package/spec/index.spec.js +14 -0
- package/spec/matchers/action.spec.js +102 -0
- package/spec/matchers/array.spec.js +33 -0
- package/spec/matchers/dataset.spec.js +32 -0
- package/spec/matchers/event.spec.js +35 -0
- package/spec/matchers/index.spec.js +15 -0
- package/spec/mocks/animate.spec.js +24 -0
- package/spec/mocks/element.spec.js +136 -0
- package/spec/mocks/index.spec.js +16 -0
- package/spec/mocks/match-media.spec.js +75 -0
- package/spec/mocks/resize-observer.spec.js +52 -0
- package/spec/simulator.spec.js +96 -0
- package/src/components/MockItem.svelte +5 -0
- package/src/index.js +1 -0
- package/src/matchers/action.js +88 -0
- package/src/matchers/array.js +16 -0
- package/src/matchers/dataset.js +18 -0
- package/src/matchers/event.js +18 -0
- package/src/matchers/index.js +4 -0
- package/src/matchers/internal.js +9 -0
- package/src/mocks/animate.js +26 -0
- package/src/mocks/element.js +83 -0
- package/src/mocks/index.js +7 -0
- package/src/mocks/match-media.js +90 -0
- package/src/mocks/resize-observer.js +25 -0
- package/src/simulators/touch.js +67 -0
- package/tsconfig.build.json +11 -0
- package/vitest.config.js +2 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { matchMediaMock, updateMedia } from '../../src/mocks/match-media'
|
|
3
|
+
|
|
4
|
+
describe('match-media', () => {
|
|
5
|
+
describe('matchMediaMock', () => {
|
|
6
|
+
it('should return a mock media query', () => {
|
|
7
|
+
const query = matchMediaMock('(min-width: 100px)')
|
|
8
|
+
expect(query.media).toBe('(min-width: 100px)')
|
|
9
|
+
expect(query.matches).toBe(true)
|
|
10
|
+
expect(query.addListener).toEqual(expect.any(Function))
|
|
11
|
+
expect(query.removeListener).toEqual(expect.any(Function))
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('updateMedia', () => {
|
|
16
|
+
it('should mock min-width media query', () => {
|
|
17
|
+
const query = matchMediaMock('(min-width: 100px)')
|
|
18
|
+
|
|
19
|
+
window.innerWidth = 50
|
|
20
|
+
updateMedia()
|
|
21
|
+
expect(query.matches).toBe(false)
|
|
22
|
+
|
|
23
|
+
window.innerWidth = 200
|
|
24
|
+
updateMedia()
|
|
25
|
+
expect(query.matches).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should mock max-width media query', () => {
|
|
29
|
+
const query = matchMediaMock('(max-width: 100px)')
|
|
30
|
+
|
|
31
|
+
window.innerWidth = 500
|
|
32
|
+
updateMedia()
|
|
33
|
+
expect(query.matches).toBe(false)
|
|
34
|
+
|
|
35
|
+
window.innerWidth = 100
|
|
36
|
+
updateMedia()
|
|
37
|
+
expect(query.matches).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should mock the media queries', () => {
|
|
41
|
+
const query = matchMediaMock('(min-width: 100px) and (max-width: 200px)')
|
|
42
|
+
|
|
43
|
+
window.innerWidth = 50
|
|
44
|
+
updateMedia()
|
|
45
|
+
expect(query.matches).toBe(false)
|
|
46
|
+
|
|
47
|
+
window.innerWidth = 500
|
|
48
|
+
updateMedia()
|
|
49
|
+
expect(query.matches).toBe(false)
|
|
50
|
+
|
|
51
|
+
window.innerWidth = 150
|
|
52
|
+
updateMedia()
|
|
53
|
+
expect(query.matches).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should mock multiple media queries', () => {
|
|
57
|
+
const query1 = matchMediaMock('(min-width: 100px)')
|
|
58
|
+
const query2 = matchMediaMock('(max-width: 100px)')
|
|
59
|
+
|
|
60
|
+
window.innerWidth = 50
|
|
61
|
+
updateMedia()
|
|
62
|
+
expect(query1.matches).toBe(false)
|
|
63
|
+
expect(query2.matches).toBe(true)
|
|
64
|
+
|
|
65
|
+
window.innerWidth = 500
|
|
66
|
+
updateMedia()
|
|
67
|
+
expect(query1.matches).toBe(true)
|
|
68
|
+
expect(query2.matches).toBe(false)
|
|
69
|
+
|
|
70
|
+
// should remove the listener
|
|
71
|
+
query1.removeListener()
|
|
72
|
+
query2.removeListener()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { ResizeObserver } from '../../src/mocks/resize-observer'
|
|
3
|
+
|
|
4
|
+
global.ResizeObserver = ResizeObserver
|
|
5
|
+
|
|
6
|
+
describe('ResizeObserver', () => {
|
|
7
|
+
let resizeObserver
|
|
8
|
+
let callback
|
|
9
|
+
let element
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
callback = vi.fn()
|
|
13
|
+
resizeObserver = new ResizeObserver(callback)
|
|
14
|
+
element = document.createElement('div')
|
|
15
|
+
document.body.appendChild(element)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should observe an element and trigger the callback with initial size', () => {
|
|
19
|
+
resizeObserver.observe(element)
|
|
20
|
+
|
|
21
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
22
|
+
const [entry] = callback.mock.calls[0][0]
|
|
23
|
+
expect(entry.target).toBe(element)
|
|
24
|
+
expect(entry.contentRect).toEqual(element.getBoundingClientRect())
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should unobserve an element', () => {
|
|
28
|
+
resizeObserver.observe(element)
|
|
29
|
+
resizeObserver.unobserve(element)
|
|
30
|
+
|
|
31
|
+
expect(resizeObserver.elements.has(element)).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should disconnect and clear all observed elements', () => {
|
|
35
|
+
resizeObserver.observe(element)
|
|
36
|
+
resizeObserver.disconnect()
|
|
37
|
+
|
|
38
|
+
expect(resizeObserver.elements.size).toBe(0)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should simulate a resize event', () => {
|
|
42
|
+
resizeObserver.observe(element)
|
|
43
|
+
const newSize = { width: 500, height: 300, top: 0, left: 0, right: 500, bottom: 300 }
|
|
44
|
+
|
|
45
|
+
resizeObserver.simulateResize(element, newSize)
|
|
46
|
+
|
|
47
|
+
expect(callback).toHaveBeenCalledTimes(2)
|
|
48
|
+
const [entry] = callback.mock.calls[1][0]
|
|
49
|
+
expect(entry.target).toBe(element)
|
|
50
|
+
expect(entry.contentRect).toEqual(newSize)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { toHaveBeenDispatchedWith } from '../src/matchers/event'
|
|
3
|
+
import {
|
|
4
|
+
simulateMouseEvent,
|
|
5
|
+
simulateTouchEvent,
|
|
6
|
+
simulateMouseSwipe,
|
|
7
|
+
simulateTouchSwipe
|
|
8
|
+
} from '../src/simulators/touch'
|
|
9
|
+
|
|
10
|
+
expect.extend({ toHaveBeenDispatchedWith })
|
|
11
|
+
describe('events', () => {
|
|
12
|
+
const node = document.createElement('div')
|
|
13
|
+
|
|
14
|
+
describe('simulateMouseEvent', () => {
|
|
15
|
+
it('should simulate a mouse event', () => {
|
|
16
|
+
const event = simulateMouseEvent(10, 10)
|
|
17
|
+
expect(event).toEqual({
|
|
18
|
+
clientX: 10,
|
|
19
|
+
clientY: 10,
|
|
20
|
+
stopPropagation: expect.any(Function),
|
|
21
|
+
preventDefault: expect.any(Function)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
describe('simulateTouchEvent', () => {
|
|
26
|
+
it('should simulate a touch event', () => {
|
|
27
|
+
const event = simulateTouchEvent(10, 10)
|
|
28
|
+
|
|
29
|
+
expect(event).toEqual({
|
|
30
|
+
touches: [{ clientX: 10, clientY: 10 }],
|
|
31
|
+
stopPropagation: expect.any(Function),
|
|
32
|
+
preventDefault: expect.any(Function)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('simulateMouseSwipe', () => {
|
|
38
|
+
const events = ['mousedown', 'mouseup']
|
|
39
|
+
|
|
40
|
+
const handlers = {}
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.useFakeTimers()
|
|
44
|
+
events.forEach((event) => {
|
|
45
|
+
handlers[event] = vi.fn()
|
|
46
|
+
node.addEventListener(event, handlers[event])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.useRealTimers()
|
|
51
|
+
events.forEach((event) => node.removeEventListener(event, handlers[event]))
|
|
52
|
+
})
|
|
53
|
+
it('should simulate a mouse swipe', () => {
|
|
54
|
+
simulateMouseSwipe(node, { x: 10, y: 20 })
|
|
55
|
+
expect(handlers.mousedown).toHaveBeenCalledOnce()
|
|
56
|
+
expect(handlers.mouseup).toHaveBeenCalledOnce()
|
|
57
|
+
|
|
58
|
+
let mouseEvent = handlers.mousedown.mock.calls[0][0]
|
|
59
|
+
expect(mouseEvent.clientX).toEqual(0)
|
|
60
|
+
expect(mouseEvent.clientY).toEqual(0)
|
|
61
|
+
|
|
62
|
+
mouseEvent = handlers.mouseup.mock.calls[0][0]
|
|
63
|
+
expect(mouseEvent.clientX).toEqual(10)
|
|
64
|
+
expect(mouseEvent.clientY).toEqual(20)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
describe('simulateTouchSwipe', () => {
|
|
68
|
+
const events = ['touchstart', 'touchend']
|
|
69
|
+
|
|
70
|
+
const handlers = {}
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
vi.useFakeTimers()
|
|
74
|
+
events.forEach((event) => {
|
|
75
|
+
handlers[event] = vi.fn()
|
|
76
|
+
node.addEventListener(event, handlers[event])
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
events.forEach((event) => node.removeEventListener(event, handlers[event]))
|
|
81
|
+
vi.useRealTimers()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should simulate a touch swipe', () => {
|
|
85
|
+
simulateTouchSwipe(node, { x: 100, y: 50 })
|
|
86
|
+
expect(handlers.touchstart).toHaveBeenCalledOnce()
|
|
87
|
+
expect(handlers.touchend).toHaveBeenCalledOnce()
|
|
88
|
+
expect(handlers.touchstart.mock.calls[0][0].touches).toEqual([
|
|
89
|
+
{ clientX: 0, clientY: 0, identifier: 0, target: node }
|
|
90
|
+
])
|
|
91
|
+
expect(handlers.touchend.mock.calls[0][0].changedTouches).toEqual([
|
|
92
|
+
{ clientX: 100, clientY: 50, identifier: 0, target: node }
|
|
93
|
+
])
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
})
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './simulators/touch'
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { getMockNode } from '../mocks'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a message based on the validation state
|
|
5
|
+
* @param {Array<string>} events
|
|
6
|
+
* @param {Array<string>} handlerKeys
|
|
7
|
+
* @param {boolean} pass
|
|
8
|
+
* @param {boolean} validEvents
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function getMessage(events, handlerKeys, pass, validEvents) {
|
|
12
|
+
let message = ''
|
|
13
|
+
const names = events.join(', ')
|
|
14
|
+
const keys = handlerKeys.join(', ')
|
|
15
|
+
|
|
16
|
+
if (pass) {
|
|
17
|
+
message = `Expected other handlers besides [${names}] to be called, but none were`
|
|
18
|
+
} else if (validEvents) {
|
|
19
|
+
message = `Expected only [${names}] to be called once and the other handlers to not be called`
|
|
20
|
+
} else {
|
|
21
|
+
message = `Expected events from [${keys}] but got unexpected events [${names}]`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return message
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Checks if all the given events are registered by the action and cleaned up on destroy.
|
|
29
|
+
*
|
|
30
|
+
* @param {*} action
|
|
31
|
+
* @param {Object<string,any>} options
|
|
32
|
+
* @param {string|Array<string>} events
|
|
33
|
+
* @returns
|
|
34
|
+
*/
|
|
35
|
+
export function toUseHandlersFor(action, options, events) {
|
|
36
|
+
if (typeof events === 'string') {
|
|
37
|
+
events = [events]
|
|
38
|
+
}
|
|
39
|
+
const mock = getMockNode(events)
|
|
40
|
+
const actionHandler = action(mock.node, options)
|
|
41
|
+
let result = events.map((event) => ({
|
|
42
|
+
event,
|
|
43
|
+
created: mock.listeners[event] === 1
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
actionHandler.destroy()
|
|
47
|
+
result = result
|
|
48
|
+
.map((r) => ({ ...r, destroyed: mock.listeners[r.event] === 0 }))
|
|
49
|
+
.map((r) => ({ ...r, pass: r.created && r.destroyed }))
|
|
50
|
+
|
|
51
|
+
const pass = result.every((r) => r.pass)
|
|
52
|
+
const message = [
|
|
53
|
+
'Expected action',
|
|
54
|
+
pass ? 'not to' : 'to',
|
|
55
|
+
'manage handlers for',
|
|
56
|
+
`[${events.join(',')}]`,
|
|
57
|
+
'but result is',
|
|
58
|
+
JSON.stringify(result)
|
|
59
|
+
].join(' ')
|
|
60
|
+
|
|
61
|
+
return { message: () => message, pass }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Verifies that only the specified events are triggered. Expects an object of spies with key as event names.
|
|
66
|
+
*
|
|
67
|
+
* @param {Object<string,any>} handler : Object with keys as event names and values as spies
|
|
68
|
+
* @param {string|Array<string>} events : An event name or an array of event names
|
|
69
|
+
* @returns
|
|
70
|
+
*/
|
|
71
|
+
export function toOnlyTrigger(handler, events) {
|
|
72
|
+
events = Array.isArray(events) ? events : [events]
|
|
73
|
+
const handlerKeys = Object.keys(handler)
|
|
74
|
+
|
|
75
|
+
const isEventValid = (event) => handlerKeys.includes(event)
|
|
76
|
+
const isCallCountCorrect = (key) => {
|
|
77
|
+
const callCount = handler[key].mock.calls.length
|
|
78
|
+
return events.includes(key) ? callCount === 1 : callCount === 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const validEvents = events.every(isEventValid)
|
|
82
|
+
const pass = validEvents && handlerKeys.every(isCallCountCorrect)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
message: () => getMessage(events, handlerKeys, pass, validEvents),
|
|
86
|
+
pass
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getMessage } from './internal'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verify that an array contains all of the expected values
|
|
5
|
+
*
|
|
6
|
+
* @param {Array} received - the array to inspect
|
|
7
|
+
* @param {Array} expected - the values to check for
|
|
8
|
+
*/
|
|
9
|
+
export function toIncludeAll(received, expected) {
|
|
10
|
+
const pass = expected.every((v) => received.includes(v))
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
message: () => getMessage(received, expected, pass, 'include all of'),
|
|
14
|
+
pass
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { equals } from 'ramda'
|
|
2
|
+
import { getMessage } from './internal'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if the element has valid data attributes
|
|
6
|
+
*
|
|
7
|
+
* @param {HTMLElement} received - HTML element to be checked
|
|
8
|
+
* @param {Object} expected - data to be compared
|
|
9
|
+
*/
|
|
10
|
+
export function toHaveValidData(received, expected) {
|
|
11
|
+
const actual = { ...received.dataset }
|
|
12
|
+
const pass = equals(actual, expected)
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
message: () => getMessage(actual, expected, pass),
|
|
16
|
+
pass
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { equals } from '@vitest/expect'
|
|
2
|
+
import { getMessage } from './internal'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Verify that a spy event has been dispatched with data in event detail
|
|
6
|
+
*
|
|
7
|
+
* @param {Function} spy - the event handler to inspect
|
|
8
|
+
* @param {Object} data - data expected inthe event detail
|
|
9
|
+
*/
|
|
10
|
+
export function toHaveBeenDispatchedWith(spy, data) {
|
|
11
|
+
const detail = spy.mock.lastCall[0].detail
|
|
12
|
+
const pass = equals(detail, data)
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
message: () => getMessage(detail, data, pass),
|
|
16
|
+
pass
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Mock the animate function
|
|
4
|
+
if (!global.Element.prototype.animate) {
|
|
5
|
+
global.Element.prototype.animate = vi.fn().mockImplementation(() => {
|
|
6
|
+
return {
|
|
7
|
+
play: vi.fn(),
|
|
8
|
+
pause: vi.fn(),
|
|
9
|
+
finish: vi.fn(),
|
|
10
|
+
cancel: vi.fn(),
|
|
11
|
+
reverse: vi.fn(),
|
|
12
|
+
persist: vi.fn(),
|
|
13
|
+
onfinish: vi.fn(),
|
|
14
|
+
oncancel: vi.fn(),
|
|
15
|
+
currentTime: 0,
|
|
16
|
+
startTime: 0,
|
|
17
|
+
playbackRate: 1,
|
|
18
|
+
playState: 'idle',
|
|
19
|
+
finished: Promise.resolve(),
|
|
20
|
+
effect: {
|
|
21
|
+
getTiming: vi.fn(),
|
|
22
|
+
getComputedTiming: vi.fn()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates an array of elements with the specified size
|
|
5
|
+
*
|
|
6
|
+
* @param {number} count
|
|
7
|
+
* @param {number} size
|
|
8
|
+
* @param {string} [prop='offsetHeight']
|
|
9
|
+
* @returns {Array<Object<string, number>>}
|
|
10
|
+
*/
|
|
11
|
+
export function elementsWithSize(count, size, prop = 'offsetHeight') {
|
|
12
|
+
return Array.from({ length: count }, () => ({
|
|
13
|
+
[prop]: size
|
|
14
|
+
}))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates an array of elements with mixed sizes
|
|
19
|
+
*
|
|
20
|
+
* @param {Array<{count: number, size: number}>} data
|
|
21
|
+
* @param {string} prop
|
|
22
|
+
* @returns {Array<Object<string, number>>}
|
|
23
|
+
*/
|
|
24
|
+
export function mixedSizeElements(data, prop) {
|
|
25
|
+
return data.reduce(
|
|
26
|
+
(elements, { count, size }) => [...elements, ...elementsWithSize(count, size, prop)],
|
|
27
|
+
[]
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a mock node with functions to add and remove event handlers
|
|
33
|
+
*
|
|
34
|
+
* @param {Array<string} events
|
|
35
|
+
* @returns {{node: HTMLElement, listeners: Object<string, integer>}}
|
|
36
|
+
*/
|
|
37
|
+
export function getMockNode(events) {
|
|
38
|
+
const listeners = events.reduce((acc, event) => ({ ...acc, [event]: 0 }), {})
|
|
39
|
+
|
|
40
|
+
const node = {
|
|
41
|
+
dispatchEvent: vi.fn(),
|
|
42
|
+
scrollTo: vi.fn(),
|
|
43
|
+
querySelectorAll: vi.fn().mockImplementation((selector) => {
|
|
44
|
+
return [document.createElement(selector)]
|
|
45
|
+
}),
|
|
46
|
+
querySelector: vi.fn().mockImplementation((selector) => {
|
|
47
|
+
return document.createElement(selector)
|
|
48
|
+
}),
|
|
49
|
+
addEventListener: vi.fn().mockImplementation((name) => ++listeners[name]),
|
|
50
|
+
removeEventListener: vi.fn().mockImplementation((name) => --listeners[name])
|
|
51
|
+
}
|
|
52
|
+
return { node, listeners }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {Object} NestedItem
|
|
57
|
+
* @property {string} name
|
|
58
|
+
* @property {string} [dataPath]
|
|
59
|
+
* @property {string} [id]
|
|
60
|
+
* @property {Array<NestedItem>} [children]
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a nested HTML element structure using the provided data
|
|
65
|
+
*
|
|
66
|
+
* @param {NestedItem} item
|
|
67
|
+
* @returns {HTMLElement}
|
|
68
|
+
*/
|
|
69
|
+
export function createNestedElement(item) {
|
|
70
|
+
const { name, dataPath, id, children } = item
|
|
71
|
+
const element = document.createElement(name)
|
|
72
|
+
|
|
73
|
+
if (dataPath) element.dataset.path = dataPath
|
|
74
|
+
if (id) element.id = id
|
|
75
|
+
|
|
76
|
+
element.scrollIntoView = vi.fn()
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(children)) {
|
|
79
|
+
children.forEach((child) => element.appendChild(createNestedElement(child)))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return element
|
|
83
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const watchMediaQueries = []
|
|
4
|
+
const listeners = []
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} MediaQuery
|
|
8
|
+
* @property {integer} [min-width]
|
|
9
|
+
* @property {integer} [max-width]
|
|
10
|
+
* @property {integer} [min-height]
|
|
11
|
+
* @property {integer} [max-height]
|
|
12
|
+
* @property {integer} [width]
|
|
13
|
+
* @property {integer} [height]
|
|
14
|
+
* @property {integer} [orientation]
|
|
15
|
+
* @property {integer} [aspect-ratio]
|
|
16
|
+
* @property {integer} [min-aspect-ratio]
|
|
17
|
+
* @property {integer} [max-aspect-ratio]
|
|
18
|
+
* @property {integer} [resolution]
|
|
19
|
+
* @property {integer} [min-resolution]
|
|
20
|
+
* @property {integer} [max-resolution]
|
|
21
|
+
* @property {integer} [scan]
|
|
22
|
+
* @property {integer} [grid]
|
|
23
|
+
* @property {integer} [update]
|
|
24
|
+
* @property {integer} [overflow-block]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses a media query string into an object
|
|
29
|
+
*
|
|
30
|
+
* @param {string} mediaQuery
|
|
31
|
+
* @returns {MediaQuery}
|
|
32
|
+
*/
|
|
33
|
+
function parseMediaQuery(mediaQuery) {
|
|
34
|
+
const regex = /\(([^:]+):\s*([^)]+)\)/g
|
|
35
|
+
const result = {}
|
|
36
|
+
|
|
37
|
+
let match = null
|
|
38
|
+
while ((match = regex.exec(mediaQuery)) !== null) {
|
|
39
|
+
const [, property, value] = match
|
|
40
|
+
result[property.trim()] = parseInt(value.trim(), 10)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Simulates the evaluation of a media query
|
|
48
|
+
*
|
|
49
|
+
* @param {MediaQuery} mediaQuery
|
|
50
|
+
* @param {integer} width
|
|
51
|
+
* @returns
|
|
52
|
+
*/
|
|
53
|
+
function evaluateMediaQuery(mediaQuery, width) {
|
|
54
|
+
const { 'min-width': minWidth = 0, 'max-width': maxWidth = width } = mediaQuery
|
|
55
|
+
|
|
56
|
+
return width >= minWidth && width <= maxWidth
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mocks the window.matchMedia function
|
|
61
|
+
* @param {string} query
|
|
62
|
+
* @returns {Object}
|
|
63
|
+
*/
|
|
64
|
+
export const matchMediaMock = vi.fn().mockImplementation((query) => {
|
|
65
|
+
const mediaQuery = parseMediaQuery(query)
|
|
66
|
+
const handler = (width) => evaluateMediaQuery(mediaQuery, width)
|
|
67
|
+
|
|
68
|
+
const queryObject = {
|
|
69
|
+
media: query,
|
|
70
|
+
matches: handler(window.innerWidth),
|
|
71
|
+
addListener: vi.fn().mockImplementation((listener) => listeners.push(listener)),
|
|
72
|
+
removeListener: vi
|
|
73
|
+
.fn()
|
|
74
|
+
.mockImplementation((listener) => listeners.splice(listeners.indexOf(listener), 1)),
|
|
75
|
+
handler
|
|
76
|
+
}
|
|
77
|
+
watchMediaQueries.push(queryObject)
|
|
78
|
+
return watchMediaQueries[watchMediaQueries.length - 1]
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Updates the media query matches
|
|
83
|
+
* @returns {void}
|
|
84
|
+
*/
|
|
85
|
+
export function updateMedia() {
|
|
86
|
+
watchMediaQueries.forEach((query) => {
|
|
87
|
+
query.matches = query.handler(window.innerWidth)
|
|
88
|
+
})
|
|
89
|
+
listeners.forEach((listener) => listener())
|
|
90
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class ResizeObserver {
|
|
2
|
+
constructor(callback) {
|
|
3
|
+
this.callback = callback
|
|
4
|
+
this.elements = new Map()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
observe(element) {
|
|
8
|
+
this.elements.set(element, { contentRect: element.getBoundingClientRect() })
|
|
9
|
+
// Immediately invoke the callback with the initial size
|
|
10
|
+
this.callback([{ target: element, contentRect: element.getBoundingClientRect() }])
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
unobserve(element) {
|
|
14
|
+
this.elements.delete(element)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
this.elements.clear()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Simulate a resize event
|
|
22
|
+
simulateResize(element, contentRect) {
|
|
23
|
+
this.callback([{ target: element, contentRect }])
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
global.Touch = vi.fn().mockImplementation((input) => input)
|
|
4
|
+
|
|
5
|
+
export function simulateMouseEvent(x, y) {
|
|
6
|
+
return {
|
|
7
|
+
clientX: x,
|
|
8
|
+
clientY: y,
|
|
9
|
+
stopPropagation: vi.fn(),
|
|
10
|
+
preventDefault: vi.fn()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function simulateTouchEvent(clientX, clientY) {
|
|
14
|
+
return {
|
|
15
|
+
touches: [{ clientX, clientY }],
|
|
16
|
+
preventDefault: vi.fn(),
|
|
17
|
+
stopPropagation: vi.fn()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function simulateTouchSwipe(node, distance, delay = 0) {
|
|
22
|
+
const touchStart = new Touch({
|
|
23
|
+
identifier: 0,
|
|
24
|
+
target: node,
|
|
25
|
+
clientX: 0,
|
|
26
|
+
clientY: 0
|
|
27
|
+
})
|
|
28
|
+
const touchEnd = new Touch({
|
|
29
|
+
identifier: 0,
|
|
30
|
+
target: node,
|
|
31
|
+
clientX: distance.x,
|
|
32
|
+
clientY: distance.y
|
|
33
|
+
})
|
|
34
|
+
const touchStartEvent = new TouchEvent('touchstart', {
|
|
35
|
+
touches: [touchStart]
|
|
36
|
+
})
|
|
37
|
+
node.dispatchEvent(touchStartEvent)
|
|
38
|
+
vi.advanceTimersByTime(delay)
|
|
39
|
+
const touchEndEvent = new TouchEvent('touchend', {
|
|
40
|
+
changedTouches: [touchEnd]
|
|
41
|
+
})
|
|
42
|
+
node.dispatchEvent(touchEndEvent)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function simulateMouseSwipe(node, distance, delay = 0) {
|
|
46
|
+
node.dispatchEvent(new MouseEvent('mousedown', { clientX: 0, clientY: 0 }))
|
|
47
|
+
node.dispatchEvent(
|
|
48
|
+
new MouseEvent('mousemove', {
|
|
49
|
+
clientX: distance.x / 2,
|
|
50
|
+
clientY: distance.y / 2
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
vi.advanceTimersByTime(delay)
|
|
54
|
+
node.dispatchEvent(new MouseEvent('mouseup', { clientX: distance.x, clientY: distance.y }))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// export function getCustomEventMock() {
|
|
58
|
+
// return vi.fn().mockImplementation((eventType, eventInit) => {
|
|
59
|
+
// class CustomEvent extends Event {
|
|
60
|
+
// constructor(type, init) {
|
|
61
|
+
// super(type, init)
|
|
62
|
+
// this.detail = init
|
|
63
|
+
// }
|
|
64
|
+
// }
|
|
65
|
+
// return new CustomEvent(eventType, eventInit)
|
|
66
|
+
// })
|
|
67
|
+
// }
|