134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
import type { YunleHostEnv, YunleJSAPI } from '../apps/web/app/utils/yunleHost'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
applyYunleHostLayout,
|
|
capsuleInset,
|
|
getYunleHost,
|
|
hostLayoutVars,
|
|
resetYunleHostCache,
|
|
resolveDisplayMode,
|
|
waitForYunleHost,
|
|
yunleHaptic,
|
|
} from '../apps/web/app/utils/yunleHost'
|
|
|
|
function env(overrides: Partial<YunleHostEnv> = {}): YunleHostEnv {
|
|
return {
|
|
platform: 'ios',
|
|
appId: 'cook',
|
|
displayMode: 'immersive',
|
|
trusted: true,
|
|
capsuleVisible: true,
|
|
titleBarVisible: false,
|
|
safeAreaInsets: { top: 47, bottom: 34, left: 0, right: 0 },
|
|
capsuleRect: { x: 283, y: 51, width: 84, height: 32 },
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function host(overrides: Partial<YunleJSAPI> = {}): YunleJSAPI {
|
|
return {
|
|
version: 3,
|
|
inYunleApp: true,
|
|
canIUse: vi.fn(async () => true),
|
|
getEnv: vi.fn(async () => env()),
|
|
haptic: vi.fn(async () => {}),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
delete window.ylf
|
|
resetYunleHostCache()
|
|
document.documentElement.className = ''
|
|
document.documentElement.removeAttribute('style')
|
|
})
|
|
|
|
describe('yunle host detection', () => {
|
|
it('ignores plain browsers and look-alike objects', () => {
|
|
expect(getYunleHost({})).toBeUndefined()
|
|
expect(getYunleHost({ ylf: { inYunleApp: false } as unknown as YunleJSAPI })).toBeUndefined()
|
|
expect(getYunleHost({ ylf: { inYunleApp: true } as unknown as YunleJSAPI })).toBeUndefined()
|
|
})
|
|
|
|
it('detects the injected JSAPI', () => {
|
|
const api = host()
|
|
expect(getYunleHost({ ylf: api })).toBe(api)
|
|
})
|
|
|
|
it('waits for late injection after load', async () => {
|
|
vi.useFakeTimers()
|
|
const win = {
|
|
ylf: undefined as YunleJSAPI | undefined,
|
|
document: { readyState: 'loading' },
|
|
addEventListener: vi.fn((_: string, listener: () => void) => {
|
|
win.ylf = host()
|
|
listener()
|
|
}),
|
|
setTimeout: (fn: () => void, ms: number) => setTimeout(fn, ms),
|
|
}
|
|
const pending = waitForYunleHost(win as never)
|
|
await vi.runAllTimersAsync()
|
|
await expect(pending).resolves.toBe(win.ylf)
|
|
vi.useRealTimers()
|
|
})
|
|
})
|
|
|
|
describe('yunle host layout', () => {
|
|
it('treats a missing display mode as immersive', () => {
|
|
expect(resolveDisplayMode({})).toBe('immersive')
|
|
expect(resolveDisplayMode({ displayMode: 'titlebar' })).toBe('titlebar')
|
|
})
|
|
|
|
it('reserves space for a visible capsule only', () => {
|
|
expect(capsuleInset(env(), 375)).toBe(100)
|
|
expect(capsuleInset(env({ capsuleVisible: false }), 375)).toBe(0)
|
|
// 标题栏显示时胶囊在视口上方
|
|
expect(capsuleInset(env({ capsuleRect: { x: 283, y: -40, width: 84, height: 32 } }), 375)).toBe(0)
|
|
})
|
|
|
|
it('maps host insets to Ionic safe-area variables', () => {
|
|
expect(hostLayoutVars(env(), 375)).toEqual({
|
|
'--ion-safe-area-top': '47px',
|
|
'--ion-safe-area-bottom': '34px',
|
|
'--ion-safe-area-left': '0px',
|
|
'--ion-safe-area-right': '0px',
|
|
'--cook-host-capsule-inset': '100px',
|
|
})
|
|
})
|
|
|
|
it('marks the root with the display mode', () => {
|
|
const root = document.documentElement
|
|
applyYunleHostLayout(root, env({ displayMode: 'titlebar', safeAreaInsets: { top: 0, bottom: 34, left: 0, right: 0 } }), 375)
|
|
expect(root.classList.contains('in-yunle-app')).toBe(true)
|
|
expect(root.classList.contains('ylf-mode-titlebar')).toBe(true)
|
|
expect(root.style.getPropertyValue('--ion-safe-area-top')).toBe('0px')
|
|
|
|
applyYunleHostLayout(root, env(), 375)
|
|
expect(root.classList.contains('ylf-mode-titlebar')).toBe(false)
|
|
expect(root.classList.contains('ylf-mode-immersive')).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('yunle host haptic', () => {
|
|
it('does nothing outside the host', async () => {
|
|
await expect(yunleHaptic()).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('calls haptic once permission is confirmed', async () => {
|
|
const api = host()
|
|
window.ylf = api
|
|
await yunleHaptic('light')
|
|
await yunleHaptic('success')
|
|
expect(api.canIUse).toHaveBeenCalledTimes(1)
|
|
expect(api.haptic).toHaveBeenNthCalledWith(1, 'light')
|
|
expect(api.haptic).toHaveBeenNthCalledWith(2, 'success')
|
|
})
|
|
|
|
it('skips untrusted pages without throwing', async () => {
|
|
const api = host({ canIUse: vi.fn(async () => false) })
|
|
window.ylf = api
|
|
await yunleHaptic()
|
|
expect(api.haptic).not.toHaveBeenCalled()
|
|
})
|
|
})
|