mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
316 lines
13 KiB
TypeScript
316 lines
13 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const viewSource = readFileSync(
|
|
new URL('./SpringboardView.vue', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const appSource = readFileSync(new URL('../App.vue', import.meta.url), 'utf8')
|
|
const appIconSource = readFileSync(
|
|
new URL('../components/AppIcon.vue', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const folderIconSource = readFileSync(
|
|
new URL('../components/HomeFolderIcon.vue', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const mainCss = readFileSync(
|
|
new URL('../assets/main.css', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const tokensCss = readFileSync(
|
|
new URL('../ui/tokens.css', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const foundationCss = readFileSync(
|
|
new URL('../ui/foundation.css', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const builtInWallpaperCss = mainCss.slice(
|
|
mainCss.indexOf('.wallpaper--midnight'),
|
|
mainCss.indexOf('.wallpaper--custom'),
|
|
)
|
|
const folderOverlayBlock =
|
|
viewSource.match(/<HomeFolderOverlay\b[\s\S]*?\/>/)?.[0] ?? ''
|
|
|
|
function handlerName(block: string, event: string): string {
|
|
return (
|
|
block.match(new RegExp(`@${event}="([A-Za-z][A-Za-z0-9_]*)"`))?.[1] ?? ''
|
|
)
|
|
}
|
|
|
|
function functionSource(name: string): string {
|
|
if (!name) return ''
|
|
const start = viewSource.indexOf(`function ${name}(`)
|
|
if (start < 0) return ''
|
|
const next = viewSource.indexOf('\nfunction ', start + 1)
|
|
return viewSource.slice(start, next < 0 ? viewSource.length : next)
|
|
}
|
|
|
|
describe('Springboard page swipe contract', () => {
|
|
it('keeps app and widget labels on the larger shared home typography', () => {
|
|
expect(tokensCss).toContain('--sky-home-label-font-size: 13px;')
|
|
expect(tokensCss).toContain('--sky-home-label-height: 16px;')
|
|
expect(mainCss).toMatch(
|
|
/\.app-icon-label\s*\{[\s\S]*?font-size:\s*var\(--sky-home-label-font-size\);/,
|
|
)
|
|
expect(foundationCss).toMatch(
|
|
/\.sky-widget-frame__label\s*\{[\s\S]*?font-size:\s*var\(--sky-home-label-font-size\);/,
|
|
)
|
|
})
|
|
|
|
it('keeps the built-in wallpapers visually restrained', () => {
|
|
expect(builtInWallpaperCss).not.toMatch(/(?:conic|repeating-\w+)-gradient/)
|
|
expect(builtInWallpaperCss.match(/radial-gradient/g)).toHaveLength(12)
|
|
expect(builtInWallpaperCss.match(/linear-gradient/g)).toHaveLength(12)
|
|
})
|
|
|
|
it('replaces the home status row with the edit controls', () => {
|
|
expect(viewSource).toContain("emit('editModeChange', editing)")
|
|
expect(viewSource).toContain("emit('editModeChange', false)")
|
|
expect(appSource).toContain(
|
|
'@edit-mode-change="springboardEditing = $event"',
|
|
)
|
|
expect(appSource).toMatch(
|
|
/v-if="\s*!isLocked && !\(isHomeRoute && springboardEditing\)\s*"/,
|
|
)
|
|
expect(mainCss).toMatch(
|
|
/\.springboard-edit-add\s*\{[^}]*top:\s*14px;[^}]*left:\s*28px;[^}]*width:\s*58px;[^}]*height:\s*30px;/s,
|
|
)
|
|
expect(mainCss).toMatch(
|
|
/\.springboard-edit-done\s*\{[^}]*top:\s*14px;[^}]*right:\s*28px;[^}]*width:\s*58px;[^}]*height:\s*30px;/s,
|
|
)
|
|
expect(viewSource).toContain(
|
|
'<Check :size="20" :stroke-width="3" aria-hidden="true" />',
|
|
)
|
|
})
|
|
|
|
it('allows page swipes to start on home apps and widget surfaces', () => {
|
|
expect(viewSource).toContain(
|
|
'.springboard-page--apps .app-icon-button, .home-widget',
|
|
)
|
|
expect(viewSource).toContain('springboardSwipeIntent')
|
|
expect(viewSource).not.toContain(
|
|
"target.closest('button, input, .home-widget-shell')",
|
|
)
|
|
})
|
|
|
|
it('keeps blank-surface page swipes available while editing', () => {
|
|
expect(viewSource).not.toMatch(
|
|
/if \(editMode\.value\) return\s+dragging\.value/,
|
|
)
|
|
expect(viewSource).toContain(
|
|
'if (editMode.value || pageSwipeSurface) return',
|
|
)
|
|
})
|
|
|
|
it('suppresses app and folder activation after pointer movement', () => {
|
|
expect(appIconSource).toContain('springboardSwipeIntent')
|
|
expect(folderIconSource).toContain('springboardSwipeIntent')
|
|
expect(appIconSource).toContain('suppressClick.value = true')
|
|
expect(folderIconSource).toContain('suppressClick.value = true')
|
|
})
|
|
|
|
it('keeps app and folder drags alive independently of element capture', () => {
|
|
expect(appIconSource).toContain('bindPointerDragSession(window, pointerId')
|
|
expect(folderIconSource).toContain(
|
|
'bindPointerDragSession(window, pointerId',
|
|
)
|
|
expect(appIconSource).not.toContain('@lostpointercapture')
|
|
expect(folderIconSource).not.toContain('@lostpointercapture')
|
|
})
|
|
|
|
it('converts pointer movement from viewport pixels using measured page geometry', () => {
|
|
expect(appIconSource).toContain('springboardPageDragCompensation')
|
|
expect(folderIconSource).toContain('springboardPageDragCompensation')
|
|
expect(appIconSource).toContain('readSpringboardDragMetrics')
|
|
expect(folderIconSource).toContain('readSpringboardDragMetrics')
|
|
expect(appIconSource).toContain(':style="dragPointerStyle"')
|
|
expect(folderIconSource).toContain(':style="dragPointerStyle"')
|
|
})
|
|
|
|
it('renders the home drag visual through the unzoomed phone portal', () => {
|
|
expect(viewSource).toContain('source.cloneNode(true)')
|
|
expect(viewSource).toContain(
|
|
'<Teleport defer to="#phone-home-drag-portal">',
|
|
)
|
|
expect(viewSource).toContain(
|
|
'<div ref="homeDragLayer" class="home-drag-layer" aria-hidden="true"></div>',
|
|
)
|
|
expect(viewSource).toContain('readPhoneViewportGeometry')
|
|
expect(viewSource).toMatch(/const sourceBounds = \w+\.rect\(source\)/)
|
|
expect(viewSource).toMatch(/const clipBounds = \w+\.rect\(\w+\)/)
|
|
expect(viewSource).not.toContain('homeDragLocalPoint')
|
|
expect(viewSource).not.toContain('springboardViewportToLocal')
|
|
expect(viewSource).toContain("position.className = 'home-drag-position'")
|
|
expect(viewSource).toMatch(
|
|
/homeDragGrip = \{\s*x: event\.clientX - sourceBounds\.left,\s*y: event\.clientY - sourceBounds\.top,/,
|
|
)
|
|
expect(viewSource).toMatch(
|
|
/ghost\.style\.transform = `scale\(\$\{\w+\.scaleX\}, \$\{\w+\.scaleY\}\)`/,
|
|
)
|
|
expect(
|
|
viewSource.match(/:external-drag-visual="homeDragVisualActive"/g),
|
|
).toHaveLength(5)
|
|
expect(viewSource).toContain('updateHomeDragGhost(event)')
|
|
expect(viewSource).toContain('const dropGhost = homeDragGhost')
|
|
expect(viewSource).toContain('const dropOrigin = homeDragPreviewBounds')
|
|
expect(viewSource).not.toContain('dropGhost?.getBoundingClientRect()')
|
|
expect(viewSource).toContain('clearHomeDragGhost(dropGhost)')
|
|
expect(viewSource).not.toContain('draggedElement?.getBoundingClientRect()')
|
|
expect(viewSource).not.toContain('springboard--home-dragging')
|
|
expect(appIconSource).toContain('externalDragVisual?: boolean')
|
|
expect(folderIconSource).toContain('externalDragVisual?: boolean')
|
|
expect(appIconSource).toContain('app-icon-item--drag-source')
|
|
expect(folderIconSource).toContain('app-icon-item--drag-source')
|
|
expect(mainCss).toMatch(
|
|
/\.phone-home-drag-portal\s*\{[^}]*pointer-events:\s*none;/s,
|
|
)
|
|
expect(mainCss).toContain('.home-drag-position')
|
|
expect(mainCss).toContain('.home-drag-ghost')
|
|
expect(mainCss).toContain('.app-icon-item--drag-source')
|
|
expect(mainCss).toMatch(/\.springboard\s*\{[\s\S]*?overflow:\s*hidden;/)
|
|
expect(mainCss).toMatch(
|
|
/\.springboard-page--apps\s*\{[^}]*overflow:\s*hidden;/s,
|
|
)
|
|
})
|
|
|
|
it('owns one external drag session for an app leaving an opened folder', () => {
|
|
expect(folderOverlayBlock).toContain(
|
|
':external-drag-visual="homeDragVisualActive"',
|
|
)
|
|
|
|
const startSource = functionSource(
|
|
handlerName(folderOverlayBlock, 'dragstart'),
|
|
)
|
|
const moveSource = functionSource(
|
|
handlerName(folderOverlayBlock, 'dragmove'),
|
|
)
|
|
const cancelSource = functionSource(
|
|
handlerName(folderOverlayBlock, 'dragcancel'),
|
|
)
|
|
|
|
expect(startSource).toContain('createHomeDragGhost(event)')
|
|
expect(startSource).toMatch(/\.value\s*=\s*\{[\s\S]*?sourceIndex/)
|
|
expect(moveSource).toContain('updateHomeDragGhost(event)')
|
|
expect(moveSource).toContain('folderDraggingOutside.value')
|
|
expect(moveSource).toContain('resolveHomeEdgeTurn(event)')
|
|
expect(moveSource).toContain("queueEdgePageTurn(event, 'app')")
|
|
expect(viewSource).toContain(
|
|
'(draggingHomeApp.value || draggingFolderApp.value)',
|
|
)
|
|
expect(cancelSource).toContain('clearHomeDragGhost()')
|
|
expect(cancelSource).toContain('clearTemporaryHomePage()')
|
|
})
|
|
|
|
it('settles an extracted folder app from the tracked viewport preview', () => {
|
|
const extractSource = functionSource('extractOpenedFolderApp')
|
|
const targetSource = functionSource('folderExtractionDropTarget')
|
|
|
|
expect(extractSource).toContain('const dropGhost = homeDragGhost')
|
|
expect(extractSource).toContain('homeDragPreviewBounds')
|
|
expect(targetSource).toContain('page > appStore.homeLayout.pageCount')
|
|
expect(extractSource).not.toContain('sourceElement')
|
|
expect(extractSource).not.toContain('getBoundingClientRect()')
|
|
expect(extractSource).toMatch(
|
|
/animateHomeItemDrop\([\s\S]*?dropOrigin[\s\S]*?\)\.finally\(/,
|
|
)
|
|
expect(extractSource).toContain('clearHomeDragGhost(dropGhost)')
|
|
expect(extractSource).toMatch(
|
|
/if \(!target\) \{[\s\S]*?clearHomeDragGhost\(\)/,
|
|
)
|
|
})
|
|
|
|
it('cleans the opened-folder drag preview after an internal move, cancellation, or unmount', () => {
|
|
const moveSource = functionSource('moveOpenedFolderApp')
|
|
const cancelSource = functionSource(
|
|
handlerName(folderOverlayBlock, 'dragcancel'),
|
|
)
|
|
|
|
expect(moveSource).toContain('stopOpenedFolderAppDrag()')
|
|
expect(cancelSource).toContain('clearHomeDragGhost()')
|
|
expect(viewSource).toMatch(
|
|
/onBeforeUnmount\(\(\) => \{[\s\S]*?clearHomeDragGhost\(\)/,
|
|
)
|
|
})
|
|
|
|
it('cleans up the external drag visual on every terminal path', () => {
|
|
expect(
|
|
viewSource.match(
|
|
/@dragstart="startHomeDrag\('(grid|dock)', [^,]+, \$event\)"/g,
|
|
),
|
|
).toHaveLength(4)
|
|
expect(viewSource.match(/@dragcancel="stopHomeDrag"/g)).toHaveLength(4)
|
|
expect(viewSource).toContain(
|
|
'if (expectedGhost && homeDragGhost !== expectedGhost) return',
|
|
)
|
|
expect(viewSource).toMatch(
|
|
/if \(!dragged\) \{\s*clearHomeDragGhost\(\)\s*return/,
|
|
)
|
|
expect(viewSource).toMatch(
|
|
/animateHomeItemDrop\(\s*draggedItem,\s*dropArea,\s*dropIndex,\s*dropOrigin,?\s*\)\.finally\(/,
|
|
)
|
|
expect(viewSource).toMatch(
|
|
/function stopHomeDrag[\s\S]*?clearHomeDragGhost\(\)/,
|
|
)
|
|
expect(viewSource).toMatch(
|
|
/if \(folderId\) \{[\s\S]*?clearHomeDragGhost\(\)/,
|
|
)
|
|
expect(viewSource).toMatch(
|
|
/onBeforeUnmount\(\(\) => \{[\s\S]*?clearHomeDragGhost\(\)/,
|
|
)
|
|
})
|
|
|
|
it('targets the active visual page and lets grid or dock drags turn pages', () => {
|
|
expect(viewSource).toContain(':data-home-page="page.page"')
|
|
expect(viewSource).toContain(':data-home-target-offset="cell.targetOffset"')
|
|
expect(viewSource).toContain('nearestGridDropTarget')
|
|
expect(viewSource).toContain('moveHomeAppToGridPage')
|
|
expect(viewSource).not.toContain("draggingHomeApp.value?.area === 'grid'")
|
|
expect(viewSource).toContain('event.clientX < springboardBounds.left')
|
|
expect(viewSource).toContain('event.clientY > springboardBounds.bottom')
|
|
expect(viewSource).toContain('function homeDragViewportRect(')
|
|
expect(viewSource).toContain('if (geometry) return geometry.rect(element)')
|
|
expect(viewSource).toContain('homeDragViewportRect(pageElement, geometry)')
|
|
expect(viewSource).toContain('homeDragViewportRect(springboard, geometry)')
|
|
expect(viewSource).toContain('homeDragViewportRect(slot, geometry)')
|
|
expect(viewSource).toContain('homeDragViewportRect(dock, geometry)')
|
|
expect(viewSource).toMatch(
|
|
/homeDragViewportRect\(\s*appElement,\s*readPhoneViewportGeometry\(appElement\),?\s*\)/,
|
|
)
|
|
expect(viewSource).toContain(
|
|
'springboardBounds.left + (bounds.left - pageBounds.left)',
|
|
)
|
|
expect(viewSource).toContain('nearestSpringboardRectIndex')
|
|
expect(viewSource).toContain("queueEdgePageTurn(lastHomePointer, 'app')")
|
|
expect(viewSource).toContain("edgePageLocked = dragType === 'widget'")
|
|
})
|
|
|
|
it('resolves occupied dock slots from the dock bounds rather than the event target', () => {
|
|
expect(viewSource).toContain("querySelector<HTMLElement>('.app-dock')")
|
|
expect(viewSource).toContain('\'[data-home-area="dock"][data-home-index]\'')
|
|
expect(viewSource).not.toContain(
|
|
'.closest<HTMLElement>(\'[data-home-area="dock"]\')',
|
|
)
|
|
})
|
|
|
|
it('keeps a new trailing page temporary until a valid drop', () => {
|
|
expect(viewSource).toContain('temporaryHomePage')
|
|
expect(viewSource).toContain('clearTemporaryHomePage')
|
|
expect(viewSource).toContain('resolveSpringboardHomeEdgeTurn')
|
|
expect(viewSource).toContain('onBeforeUnmount')
|
|
})
|
|
|
|
it('materializes page-local app positions before widget layout changes', () => {
|
|
expect(viewSource).toContain('function applyWidgetLayout')
|
|
expect(viewSource).toContain('appStore.applyWidgetGridCapacities')
|
|
expect(viewSource).toContain('homeGridCapacitiesFor(widgets.layout)')
|
|
expect(viewSource).toContain('homeGridCapacitiesFor(nextLayout)')
|
|
expect(viewSource).toContain('previewWidgetAdd')
|
|
expect(viewSource).toContain('previewWidgetMove')
|
|
expect(viewSource).toContain('previewWidgetRemove')
|
|
expect(viewSource).toContain('previewWidgetResize')
|
|
})
|
|
})
|