|
1 | 1 | import React from 'react'; |
2 | | -import { describe, expect, it } from 'vitest'; |
| 2 | +import { afterEach, describe, expect, it, vi } from 'vitest'; |
3 | 3 |
|
4 | 4 | import { bindCreateFixtures } from '@/test/create-fixtures'; |
5 | | -import { render } from '@/test/utils'; |
| 5 | +import { fireEvent, render } from '@/test/utils'; |
6 | 6 |
|
7 | 7 | import { ImpersonationFab } from '../'; |
8 | 8 |
|
9 | 9 | const { createFixtures } = bindCreateFixtures('UserButton'); |
10 | 10 |
|
11 | 11 | describe('ImpersonationFab', () => { |
| 12 | + const topProperty = '--cl-impersonation-fab-top'; |
| 13 | + const rightProperty = '--cl-impersonation-fab-right'; |
| 14 | + const cssVar = (name: string) => document.documentElement.style.getPropertyValue(name); |
| 15 | + |
| 16 | + afterEach(() => { |
| 17 | + pointer(window, 'pointercancel', {}); |
| 18 | + document.documentElement.style.removeProperty(topProperty); |
| 19 | + document.documentElement.style.removeProperty(rightProperty); |
| 20 | + vi.restoreAllMocks(); |
| 21 | + }); |
| 22 | + |
| 23 | + const pointer = ( |
| 24 | + target: EventTarget, |
| 25 | + type: string, |
| 26 | + init: MouseEventInit & { movementX?: number; movementY?: number }, |
| 27 | + ) => { |
| 28 | + const event = new MouseEvent(type, { bubbles: true, cancelable: true, ...init }); |
| 29 | + Object.defineProperties(event, { |
| 30 | + pointerId: { value: 1 }, |
| 31 | + movementX: { value: init.movementX ?? 0 }, |
| 32 | + movementY: { value: init.movementY ?? 0 }, |
| 33 | + }); |
| 34 | + fireEvent(target, event); |
| 35 | + }; |
| 36 | + |
| 37 | + const renderDraggableFab = async () => { |
| 38 | + const { wrapper, fixtures } = await createFixtures(f => { |
| 39 | + f.withUser({ email_addresses: ['test@clerk.com'], actor: { sub: 'user_impersonated' } }); |
| 40 | + }); |
| 41 | + const result = render(<ImpersonationFab />, { wrapper }); |
| 42 | + const fab = document.getElementById('cl-impersonationEye')?.parentElement; |
| 43 | + if (!fab) { |
| 44 | + throw new Error('Impersonation button was not rendered'); |
| 45 | + } |
| 46 | + |
| 47 | + const top = () => parseFloat(cssVar(topProperty)) || 109; |
| 48 | + const right = () => parseFloat(cssVar(rightProperty)) || 23; |
| 49 | + let width = 200; |
| 50 | + Object.defineProperties(fab, { |
| 51 | + offsetTop: { get: top }, |
| 52 | + offsetLeft: { get: () => window.innerWidth - right() - width }, |
| 53 | + offsetWidth: { get: () => width }, |
| 54 | + offsetHeight: { get: () => 64 }, |
| 55 | + setPointerCapture: { value: vi.fn() }, |
| 56 | + hasPointerCapture: { value: () => false }, |
| 57 | + }); |
| 58 | + vi.spyOn(fab, 'getBoundingClientRect').mockImplementation(() => |
| 59 | + DOMRect.fromRect({ x: window.innerWidth - right() - width, y: top(), width, height: 64 }), |
| 60 | + ); |
| 61 | + return { ...result, fixtures, fab, setWidth: (value: number) => (width = value) }; |
| 62 | + }; |
| 63 | + |
| 64 | + it('follows the pointer position rather than movement values, across hover width changes', async () => { |
| 65 | + const { fab, setWidth } = await renderDraggableFab(); |
| 66 | + pointer(fab, 'pointerdown', { clientX: 900, clientY: 130 }); |
| 67 | + pointer(window, 'pointermove', { clientX: 880, clientY: 160, movementX: -40, movementY: 60 }); |
| 68 | + expect(cssVar(rightProperty)).toBe('43px'); |
| 69 | + expect(cssVar(topProperty)).toBe('139px'); |
| 70 | + |
| 71 | + setWidth(64); |
| 72 | + pointer(window, 'pointermove', { clientX: 870, clientY: 165, movementX: -20, movementY: 10 }); |
| 73 | + expect(cssVar(rightProperty)).toBe('53px'); |
| 74 | + expect(cssVar(topProperty)).toBe('144px'); |
| 75 | + }); |
| 76 | + |
| 77 | + it('keeps the sign-out action clickable', async () => { |
| 78 | + const { fixtures, getByText, userEvent } = await renderDraggableFab(); |
| 79 | + await userEvent.click(getByText('Sign out')); |
| 80 | + expect(fixtures.clerk.signOut).toHaveBeenCalled(); |
| 81 | + }); |
| 82 | + |
| 83 | + it('settles inside the viewport inset when released off-screen', async () => { |
| 84 | + const { fab } = await renderDraggableFab(); |
| 85 | + pointer(fab, 'pointerdown', { clientX: 900, clientY: 130 }); |
| 86 | + pointer(window, 'pointermove', { clientX: 1200, clientY: -100 }); |
| 87 | + pointer(window, 'pointerup', {}); |
| 88 | + expect(cssVar(rightProperty)).toBe('20px'); |
| 89 | + expect(cssVar(topProperty)).toBe('20px'); |
| 90 | + }); |
| 91 | + |
| 92 | + it('keeps the position when released inside the viewport inset', async () => { |
| 93 | + const { fab } = await renderDraggableFab(); |
| 94 | + pointer(fab, 'pointerdown', { clientX: 900, clientY: 130 }); |
| 95 | + pointer(window, 'pointermove', { clientX: 880, clientY: 160 }); |
| 96 | + pointer(window, 'pointerup', {}); |
| 97 | + expect(cssVar(rightProperty)).toBe('43px'); |
| 98 | + expect(cssVar(topProperty)).toBe('139px'); |
| 99 | + }); |
| 100 | + |
| 101 | + it('resets an out-of-viewport position on resize', async () => { |
| 102 | + await renderDraggableFab(); |
| 103 | + document.documentElement.style.setProperty(topProperty, '1000px'); |
| 104 | + fireEvent(window, new Event('resize')); |
| 105 | + expect(cssVar(topProperty)).toBe('109px'); |
| 106 | + }); |
| 107 | + |
12 | 108 | it('does not render when user has no actor', async () => { |
13 | 109 | const { wrapper } = await createFixtures(f => { |
14 | 110 | f.withUser({ email_addresses: ['test@clerk.com'] }); |
|
0 commit comments