feat: ship as a real iOS-installable PWA, restructure bottom nav, fix safe-area
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
- Add manifest.webmanifest with standalone display + warm-craft theme colors, apple-touch-icon, and 192/512/512-maskable icons (frame-with-sunset glyph). - Add PWA meta tags + viewport-fit=cover so add-to-home-screen produces a true standalone app on iOS instead of a Safari bookmark. - Drop the Shared bottom-nav tab; the in-page sub-tabs already cover that. Three nav tabs total (Home / Library / Settings); pending-share badge moves to the Library tab. Predicate-based isActive() now correctly disambiguates /library vs /library?tab=shared. - Safe-area handling: bottom nav, bottom sheet, upload overlay, and #app respect env(safe-area-inset-*); sticky Library tabs anchor below the iPhone status bar. Introduces --bottom-nav-height token consumed by Settings, Library, and the toast. - LibraryView reactively follows route.query.tab so deep-linking /library?tab=shared lands on the right sub-tab. - Theme-color meta syncs client-side via useTheme.applyTheme so the user's chosen theme follows them into Android Chrome's chrome bar. Test suite expanded to 278 tests / 100% line coverage (99.84% statements, 99.78% branches). Remaining gaps are unreachable defensive code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import UploadView from '@/views/UploadView.vue'
|
||||
import { useUploadStore } from '@/stores/upload'
|
||||
import { useDevicesStore } from '@/stores/devices'
|
||||
import { useImagesStore } from '@/stores/images'
|
||||
import type { Device } from '@/types'
|
||||
|
||||
const routerReplace = vi.fn()
|
||||
|
||||
vi.mock('@/components/CropEditor.vue', () => ({
|
||||
default: {
|
||||
name: 'CropEditor',
|
||||
template: '<div class="crop-editor-stub" />',
|
||||
props: ['src', 'orientation', 'deviceName', 'initialParams', 'initialOrientation'],
|
||||
emits: ['crop'],
|
||||
},
|
||||
}))
|
||||
vi.mock('@/components/StickerCanvas.vue', () => ({
|
||||
default: {
|
||||
name: 'StickerCanvas',
|
||||
template: '<div class="sticker-canvas-stub" />',
|
||||
props: ['croppedUrl', 'orientation', 'stickers'],
|
||||
emits: ['add-sticker', 'update-sticker', 'remove-sticker', 'done'],
|
||||
},
|
||||
}))
|
||||
vi.mock('@/components/DevicePicker.vue', () => ({
|
||||
default: {
|
||||
name: 'DevicePicker',
|
||||
template: '<div class="device-picker-stub" />',
|
||||
props: ['modelValue', 'devices', 'selected', 'uploading'],
|
||||
emits: ['update:modelValue', 'update:selected', 'confirm'],
|
||||
},
|
||||
}))
|
||||
vi.mock('@/components/BaseButton.vue', () => ({
|
||||
default: {
|
||||
name: 'BaseButton',
|
||||
template: '<button @click="$emit(\'click\')"><slot /></button>',
|
||||
props: ['variant', 'disabled'],
|
||||
emits: ['click'],
|
||||
},
|
||||
}))
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({ replace: routerReplace, push: vi.fn() }),
|
||||
}))
|
||||
|
||||
const toastShow = vi.fn()
|
||||
vi.mock('@/stores/toast', () => ({
|
||||
useToastStore: () => ({ show: toastShow }),
|
||||
}))
|
||||
|
||||
const makeDevice = (overrides: Partial<Device> = {}): Device => ({
|
||||
id: 1,
|
||||
mac: 'AA',
|
||||
name: 'Living Room',
|
||||
orientation: 'landscape',
|
||||
rotationIntervalMinutes: 60,
|
||||
wakeHour: null,
|
||||
timezone: 'UTC',
|
||||
uniquenessWindow: 30,
|
||||
linkedAt: '2026-01-01T00:00:00Z',
|
||||
lastSeenAt: null,
|
||||
lockedImageId: null,
|
||||
currentImageId: null,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
function primeUploadStore(file?: File) {
|
||||
const upload = useUploadStore()
|
||||
upload.originalFile = file ?? new File(['x'], 'orig.jpg', { type: 'image/jpeg' })
|
||||
upload.originalUrl = 'blob:original'
|
||||
return upload
|
||||
}
|
||||
|
||||
describe('UploadView', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
routerReplace.mockClear()
|
||||
toastShow.mockClear()
|
||||
vi.unstubAllGlobals()
|
||||
vi.stubGlobal('URL', {
|
||||
createObjectURL: vi.fn(() => 'blob:mock'),
|
||||
revokeObjectURL: vi.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to / when no upload is in progress', async () => {
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
mount(UploadView)
|
||||
await flushPromises()
|
||||
expect(routerReplace).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('starts on the crop step with the original URL passed to CropEditor', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
const crop = wrapper.findComponent({ name: 'CropEditor' })
|
||||
expect(crop.exists()).toBe(true)
|
||||
expect(crop.props('src')).toBe('blob:original')
|
||||
expect(wrapper.text()).toContain('Crop photo')
|
||||
})
|
||||
|
||||
it('uses "Edit crop" as the step label when editing', async () => {
|
||||
const upload = primeUploadStore()
|
||||
upload.editingImageId = 7
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('Edit crop')
|
||||
})
|
||||
|
||||
it('falls back to landscape orientation when no devices are loaded', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'CropEditor' }).props('orientation')).toBe('landscape')
|
||||
})
|
||||
|
||||
it('uses the context device orientation when contextDeviceId matches', async () => {
|
||||
const upload = primeUploadStore()
|
||||
upload.contextDeviceId = 2
|
||||
const devices = useDevicesStore()
|
||||
devices.devices = [makeDevice({ id: 1, orientation: 'landscape' }), makeDevice({ id: 2, orientation: 'portrait' })]
|
||||
vi.spyOn(devices, 'fetchDevices').mockResolvedValue()
|
||||
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'CropEditor' }).props('orientation')).toBe('portrait')
|
||||
expect(wrapper.findComponent({ name: 'CropEditor' }).props('deviceName')).toBe(devices.devices[1].name)
|
||||
})
|
||||
|
||||
it('uses the first device as the context when no contextDeviceId is set', async () => {
|
||||
primeUploadStore()
|
||||
const devices = useDevicesStore()
|
||||
devices.devices = [makeDevice({ id: 1, orientation: 'portrait' })]
|
||||
vi.spyOn(devices, 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'CropEditor' }).props('orientation')).toBe('portrait')
|
||||
})
|
||||
|
||||
it('crop emit advances to stickers step and stores the crop on the upload store', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
|
||||
const blob = new Blob(['x'])
|
||||
const params = { natX: 0, natY: 0, natW: 100, natH: 50 }
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', { blob, params, orientation: 'landscape' })
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'StickerCanvas' }).exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Add stickers')
|
||||
})
|
||||
|
||||
it('skip on stickers step opens the device picker for new uploads', async () => {
|
||||
const upload = primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
const blob = new Blob(['x'])
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', { blob, params: { natX: 0, natY: 0, natW: 1, natH: 1 }, orientation: 'landscape' })
|
||||
upload.croppedBlob = blob
|
||||
await flushPromises()
|
||||
|
||||
const skip = wrapper.find('.upload-view__skip')
|
||||
await skip.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(true)
|
||||
})
|
||||
|
||||
it('skip on stickers triggers reprocess directly when editing', async () => {
|
||||
const upload = primeUploadStore()
|
||||
upload.editingImageId = 11
|
||||
const devices = useDevicesStore()
|
||||
vi.spyOn(devices, 'fetchDevices').mockResolvedValue()
|
||||
const images = useImagesStore()
|
||||
const reprocess = vi.spyOn(images, 'reprocessImage').mockResolvedValue({ id: 11 } as any)
|
||||
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
const blob = new Blob(['x'])
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', { blob, params: { natX: 0, natY: 0, natW: 1, natH: 1 }, orientation: 'landscape' })
|
||||
upload.croppedBlob = blob
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.find('.upload-view__skip').trigger('click')
|
||||
await flushPromises()
|
||||
expect(reprocess).toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('Photo updated!')
|
||||
})
|
||||
|
||||
it('skip is a no-op when there is no cropped blob yet', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
// Force into stickers without having a croppedBlob
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', { blob: null as any, params: null as any, orientation: 'landscape' })
|
||||
// Even if we click skip, no DevicePicker should open
|
||||
const skip = wrapper.find('.upload-view__skip')
|
||||
if (skip.exists()) await skip.trigger('click')
|
||||
expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(false)
|
||||
})
|
||||
|
||||
it('stickers done opens the picker for new uploads', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(true)
|
||||
})
|
||||
|
||||
it('stickers done triggers reprocess directly when editing', async () => {
|
||||
const upload = primeUploadStore()
|
||||
upload.editingImageId = 22
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const reprocess = vi.spyOn(useImagesStore(), 'reprocessImage').mockResolvedValue({ id: 22 } as any)
|
||||
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
|
||||
await flushPromises()
|
||||
expect(reprocess).toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('Photo updated!')
|
||||
})
|
||||
|
||||
it('back from crop cleans up and routes to /library', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.find('.upload-view__back').trigger('click')
|
||||
expect(routerReplace).toHaveBeenCalledWith('/library')
|
||||
})
|
||||
|
||||
it('back from stickers returns to the crop step', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('Add stickers')
|
||||
await wrapper.find('.upload-view__back').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('Crop photo')
|
||||
})
|
||||
|
||||
it('confirm in the device picker uploads, sets approvals, and shows the done step', async () => {
|
||||
const upload = primeUploadStore()
|
||||
upload.selectedDeviceIds = [1, 2]
|
||||
const images = useImagesStore()
|
||||
const uploadSpy = vi.spyOn(images, 'uploadImage').mockResolvedValue({ id: 100 } as any)
|
||||
const approvalSpy = vi.spyOn(images, 'setApproval').mockResolvedValue()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
|
||||
expect(uploadSpy).toHaveBeenCalled()
|
||||
expect(approvalSpy).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.text()).toContain('Photo added!')
|
||||
})
|
||||
|
||||
it('upload errors surface as a toast', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useImagesStore(), 'uploadImage').mockRejectedValue(new Error('disk full'))
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(toastShow).toHaveBeenCalledWith('disk full', 'error')
|
||||
})
|
||||
|
||||
it('falls back to a generic message for non-Error rejections', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useImagesStore(), 'uploadImage').mockRejectedValue('weird')
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(toastShow).toHaveBeenCalledWith('Upload failed', 'error')
|
||||
})
|
||||
|
||||
it('Done button on the success step routes to /library', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useImagesStore(), 'uploadImage').mockResolvedValue({ id: 1 } as any)
|
||||
vi.spyOn(useImagesStore(), 'setApproval').mockResolvedValue()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
|
||||
routerReplace.mockClear()
|
||||
await wrapper.find('.upload-view__done-btn').trigger('click')
|
||||
expect(routerReplace).toHaveBeenCalledWith('/library')
|
||||
})
|
||||
|
||||
it('updates selectedDeviceIds when the picker emits update:selected', async () => {
|
||||
const upload = primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('update:selected', [3, 4])
|
||||
expect(upload.selectedDeviceIds).toEqual([3, 4])
|
||||
})
|
||||
|
||||
it('forwards sticker emits to the upload store', async () => {
|
||||
const upload = primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
const canvas = wrapper.findComponent({ name: 'StickerCanvas' })
|
||||
const sticker = { id: 's1', type: 'emoji', x: 0, y: 0, scale: 1, rotation: 0 } as any
|
||||
await canvas.vm.$emit('add-sticker', sticker)
|
||||
expect(upload.stickers).toHaveLength(1)
|
||||
await canvas.vm.$emit('update-sticker', 's1', { x: 9 })
|
||||
expect(upload.stickers[0].x).toBe(9)
|
||||
await canvas.vm.$emit('remove-sticker', 's1')
|
||||
expect(upload.stickers).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('closes the device picker when it emits update:modelValue=false', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
const picker = wrapper.findComponent({ name: 'DevicePicker' })
|
||||
expect(picker.props('modelValue')).toBe(false)
|
||||
await picker.vm.$emit('update:modelValue', true)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(true)
|
||||
await picker.vm.$emit('update:modelValue', false)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findComponent({ name: 'DevicePicker' }).props('modelValue')).toBe(false)
|
||||
})
|
||||
|
||||
it('confirm fired before any stickers/crop is a no-op (defensive guard)', async () => {
|
||||
primeUploadStore()
|
||||
const uploadSpy = vi.spyOn(useImagesStore(), 'uploadImage').mockResolvedValue({ id: 1 } as any)
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
// Picker exists but finalBlob is null — confirm should early-return
|
||||
await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(uploadSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reprocess on edit forwards undefined when cropParams/cropOrientation are null', async () => {
|
||||
const upload = primeUploadStore()
|
||||
upload.editingImageId = 33
|
||||
upload.cropParams = null
|
||||
upload.cropOrientation = null
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const reprocess = vi.spyOn(useImagesStore(), 'reprocessImage').mockResolvedValue({ id: 33 } as any)
|
||||
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
// After crop, upload.cropParams is set by setCrop. Reset to null to exercise null branch.
|
||||
upload.cropParams = null
|
||||
upload.cropOrientation = null
|
||||
await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
|
||||
await flushPromises()
|
||||
expect(reprocess).toHaveBeenCalledWith(33, expect.any(File), expect.objectContaining({
|
||||
cropParams: undefined,
|
||||
cropOrientation: undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
it('uploadImage on a new upload forwards undefined for null optional fields', async () => {
|
||||
const upload = primeUploadStore()
|
||||
upload.originalFile = null
|
||||
upload.cropParams = null
|
||||
upload.cropOrientation = null
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const uploadSpy = vi.spyOn(useImagesStore(), 'uploadImage').mockResolvedValue({ id: 1 } as any)
|
||||
|
||||
// No originalFile → onMounted will redirect; instead set it after the redirect would have run
|
||||
upload.originalFile = new File(['x'], 'x.jpg')
|
||||
upload.originalUrl = 'blob:x'
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
// Reset to null after onCrop sets these via the store's setCrop
|
||||
upload.cropParams = null
|
||||
upload.cropOrientation = null
|
||||
upload.originalFile = null
|
||||
await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
|
||||
expect(uploadSpy).toHaveBeenCalledWith(expect.any(File), expect.objectContaining({
|
||||
original: undefined,
|
||||
cropParams: undefined,
|
||||
cropOrientation: undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
it('does not render the device picker when in edit mode', async () => {
|
||||
const upload = primeUploadStore()
|
||||
upload.editingImageId = 12
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent({ name: 'DevicePicker' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('hides the back button on the done step', async () => {
|
||||
primeUploadStore()
|
||||
vi.spyOn(useImagesStore(), 'uploadImage').mockResolvedValue({ id: 1 } as any)
|
||||
vi.spyOn(useImagesStore(), 'setApproval').mockResolvedValue()
|
||||
vi.spyOn(useDevicesStore(), 'fetchDevices').mockResolvedValue()
|
||||
|
||||
const wrapper = mount(UploadView)
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'CropEditor' }).vm.$emit('crop', {
|
||||
blob: new Blob(['x']),
|
||||
params: { natX: 0, natY: 0, natW: 1, natH: 1 },
|
||||
orientation: 'landscape',
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'StickerCanvas' }).vm.$emit('done', new Blob(['final']))
|
||||
await flushPromises()
|
||||
await wrapper.findComponent({ name: 'DevicePicker' }).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.upload-view__back').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('Added')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user