|
|
@@ -0,0 +1,131 @@
|
|
|
+// @vitest-environment jsdom
|
|
|
+
|
|
|
+import { describe, expect, it } from 'vitest'
|
|
|
+import { isSafariBrowser, repairSafariTextareaLayout } from '../src/client/skeleton/safari.ts'
|
|
|
+
|
|
|
+describe('Safari browser detection', () => {
|
|
|
+ it.each([
|
|
|
+ {
|
|
|
+ name: 'desktop Safari',
|
|
|
+ vendor: 'Apple Computer, Inc.',
|
|
|
+ userAgent: 'Mozilla/5.0 (Macintosh) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15',
|
|
|
+ expected: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'mobile Safari',
|
|
|
+ vendor: 'Apple Computer, Inc.',
|
|
|
+ userAgent: 'Mozilla/5.0 (iPhone) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1',
|
|
|
+ expected: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'desktop Chromium',
|
|
|
+ vendor: 'Google Inc.',
|
|
|
+ userAgent: 'Mozilla/5.0 (Macintosh) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
|
|
+ expected: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'Chrome on iOS',
|
|
|
+ vendor: 'Apple Computer, Inc.',
|
|
|
+ userAgent: 'Mozilla/5.0 (iPhone) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.0.0 Mobile/15E148 Safari/604.1',
|
|
|
+ expected: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'Edge on iOS with Safari tokens',
|
|
|
+ vendor: 'Apple Computer, Inc.',
|
|
|
+ userAgent: 'Mozilla/5.0 (iPhone) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 EdgiOS/140.0 Mobile/15E148 Safari/604.1',
|
|
|
+ expected: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'Opera on iOS with Safari tokens',
|
|
|
+ vendor: 'Apple Computer, Inc.',
|
|
|
+ userAgent: 'Mozilla/5.0 (iPhone) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 OPiOS/6.0 Mobile/15E148 Safari/604.1',
|
|
|
+ expected: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'Apple web view',
|
|
|
+ vendor: 'Apple Computer, Inc.',
|
|
|
+ userAgent: 'Mozilla/5.0 (iPhone) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148',
|
|
|
+ expected: false,
|
|
|
+ },
|
|
|
+ ])('identifies $name', ({ vendor, userAgent, expected }) => {
|
|
|
+ expect(isSafariBrowser({ vendor, userAgent })).toBe(expected)
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe('Safari textarea layout recovery', () => {
|
|
|
+ it('does nothing while the textarea owns no scrollable overflow', () => {
|
|
|
+ const input = document.createElement('textarea')
|
|
|
+ Object.defineProperty(input, 'clientHeight', { value: 28 })
|
|
|
+ Object.defineProperty(input, 'scrollHeight', { value: 28 })
|
|
|
+
|
|
|
+ repairSafariTextareaLayout(input)
|
|
|
+
|
|
|
+ expect(input.style.height).toBe('')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('invalidates a stale native layout and restores the owned height', () => {
|
|
|
+ const input = document.createElement('textarea')
|
|
|
+ const scrollport = document.createElement('div')
|
|
|
+ scrollport.setAttribute('data-input-scroll', '')
|
|
|
+ scrollport.appendChild(input)
|
|
|
+ input.value = 'abcdef'
|
|
|
+ input.setSelectionRange(3, 3)
|
|
|
+ input.style.height = '100%'
|
|
|
+ scrollport.style.height = '100%'
|
|
|
+ let inputRepaired = false
|
|
|
+ let scrollportRepaired = false
|
|
|
+ const inputLayouts: string[] = []
|
|
|
+ const scrollportLayouts: string[] = []
|
|
|
+ Object.defineProperty(input, 'clientHeight', {
|
|
|
+ get: () => input.style.height === '29px' ? 29 : 28,
|
|
|
+ })
|
|
|
+ Object.defineProperty(input, 'scrollHeight', {
|
|
|
+ get: () => inputRepaired ? 28 : 52,
|
|
|
+ })
|
|
|
+ Object.defineProperty(input, 'offsetHeight', {
|
|
|
+ get: () => {
|
|
|
+ inputLayouts.push(input.style.height)
|
|
|
+ if (input.style.height === '100%') inputRepaired = true
|
|
|
+ return input.clientHeight
|
|
|
+ },
|
|
|
+ })
|
|
|
+ Object.defineProperty(scrollport, 'clientHeight', {
|
|
|
+ get: () => {
|
|
|
+ if (scrollport.style.height === '53px') return 53
|
|
|
+ if (inputRepaired && !scrollportRepaired) return 52
|
|
|
+ return 28
|
|
|
+ },
|
|
|
+ })
|
|
|
+ Object.defineProperty(scrollport, 'offsetHeight', {
|
|
|
+ get: () => {
|
|
|
+ scrollportLayouts.push(scrollport.style.height)
|
|
|
+ if (scrollport.style.height === '100%') scrollportRepaired = true
|
|
|
+ return scrollport.clientHeight
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ repairSafariTextareaLayout(input)
|
|
|
+
|
|
|
+ expect(inputLayouts).toEqual(['29px', '100%'])
|
|
|
+ expect(scrollportLayouts).toEqual(['53px', '100%'])
|
|
|
+ expect(input.style.height).toBe('100%')
|
|
|
+ expect(scrollport.style.height).toBe('100%')
|
|
|
+ expect(input.scrollHeight).toBe(input.clientHeight)
|
|
|
+ expect(scrollport.clientHeight).toBe(28)
|
|
|
+ expect([input.selectionStart, input.selectionEnd]).toEqual([3, 3])
|
|
|
+ })
|
|
|
+
|
|
|
+ it('does nothing outside the composer scrollport', () => {
|
|
|
+ const input = document.createElement('textarea')
|
|
|
+ Object.defineProperty(input, 'clientHeight', { value: 28 })
|
|
|
+ Object.defineProperty(input, 'scrollHeight', { value: 52 })
|
|
|
+
|
|
|
+ repairSafariTextareaLayout(input)
|
|
|
+
|
|
|
+ expect(input.style.height).toBe('')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('accepts an absent textarea during teardown', () => {
|
|
|
+ expect(() => { repairSafariTextareaLayout(null) }).not.toThrow()
|
|
|
+ })
|
|
|
+})
|