chore: merge executor worktree (worktree-agent-acd1ece9cc8092915)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
@click.self="handleOverlayClick"
|
||||
@keydown.escape.window="handleEscape"
|
||||
>
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6">
|
||||
<div data-test="cloud-credential-modal-panel" class="bg-white rounded-xl shadow-xl w-full max-w-md p-6 max-h-[90vh] overflow-y-auto">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h3 class="text-xl font-semibold text-gray-900">
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
vi.mock('../../../api/client.js', () => ({
|
||||
getConnectionConfig: vi.fn().mockResolvedValue({ connection_username: '', server_url: '' }),
|
||||
connectWebDav: vi.fn(),
|
||||
}))
|
||||
|
||||
import CloudCredentialModal from '../CloudCredentialModal.vue'
|
||||
|
||||
const globalStubs = {
|
||||
AppIcon: true,
|
||||
}
|
||||
|
||||
const testProvider = { key: 'webdav', label: 'WebDAV' }
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('RESP-04: CloudCredentialModal mobile-safe panel', () => {
|
||||
it('panel has max-h-[90vh] class for mobile scroll safety', () => {
|
||||
const wrapper = mount(CloudCredentialModal, {
|
||||
props: { show: true, provider: testProvider, existing: null },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
const panel = wrapper.find('[data-test="cloud-credential-modal-panel"]')
|
||||
expect(panel.exists()).toBe(true)
|
||||
expect(panel.classes()).toContain('max-h-[90vh]')
|
||||
})
|
||||
|
||||
it('panel has overflow-y-auto class for mobile scroll', () => {
|
||||
const wrapper = mount(CloudCredentialModal, {
|
||||
props: { show: true, provider: testProvider, existing: null },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
const panel = wrapper.find('[data-test="cloud-credential-modal-panel"]')
|
||||
expect(panel.classes()).toContain('overflow-y-auto')
|
||||
})
|
||||
|
||||
it('panel not rendered when show=false', () => {
|
||||
const wrapper = mount(CloudCredentialModal, {
|
||||
props: { show: false, provider: testProvider, existing: null },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
// When show=false the v-if hides the entire overlay
|
||||
const panel = wrapper.find('[data-test="cloud-credential-modal-panel"]')
|
||||
expect(panel.exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@
|
||||
<!-- Overlay -->
|
||||
<div
|
||||
ref="overlayRef"
|
||||
data-test="document-preview-modal"
|
||||
class="fixed inset-0 bg-black/60 z-50 flex flex-col"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@@ -9,7 +10,7 @@
|
||||
@click="handleOverlayClick"
|
||||
>
|
||||
<!-- Header bar -->
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-3 flex items-center justify-between shrink-0">
|
||||
<div data-test="preview-modal-header" class="bg-white border-b border-gray-200 px-4 sm:px-6 py-3 flex items-center justify-between shrink-0">
|
||||
<span class="text-sm font-medium text-gray-900 truncate max-w-xs">{{ doc.filename || doc.original_name }}</span>
|
||||
<button
|
||||
@click="emit('close')"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
vi.mock('../../../api/client.js', () => ({
|
||||
fetchDocumentContent: vi.fn().mockResolvedValue({ ok: true, blob: () => Promise.resolve(new Blob()) }),
|
||||
}))
|
||||
|
||||
import DocumentPreviewModal from '../DocumentPreviewModal.vue'
|
||||
|
||||
const globalStubs = {
|
||||
AppIcon: true,
|
||||
}
|
||||
|
||||
const testDoc = { id: 'doc-1', filename: 'test.pdf', original_name: 'test.pdf' }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Mock URL.createObjectURL since it's not available in jsdom
|
||||
vi.stubGlobal('URL', {
|
||||
createObjectURL: vi.fn().mockReturnValue('blob:mock-url'),
|
||||
revokeObjectURL: vi.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
describe('RESP-04: DocumentPreviewModal mobile-safe sizing', () => {
|
||||
it('renders full-screen overlay (fixed inset-0)', () => {
|
||||
const wrapper = mount(DocumentPreviewModal, {
|
||||
props: { doc: testDoc },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
const overlay = wrapper.find('[data-test="document-preview-modal"]')
|
||||
expect(overlay.exists()).toBe(true)
|
||||
expect(overlay.classes()).toContain('fixed')
|
||||
expect(overlay.classes()).toContain('inset-0')
|
||||
})
|
||||
|
||||
it('header has responsive horizontal padding (px-4 at mobile, sm:px-6 at >=640px)', () => {
|
||||
const wrapper = mount(DocumentPreviewModal, {
|
||||
props: { doc: testDoc },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
const header = wrapper.find('[data-test="preview-modal-header"]')
|
||||
expect(header.exists()).toBe(true)
|
||||
expect(header.classes()).toContain('px-4')
|
||||
expect(header.classes()).toContain('sm:px-6')
|
||||
})
|
||||
|
||||
it('header filename is truncated with truncate class to prevent overflow', () => {
|
||||
const wrapper = mount(DocumentPreviewModal, {
|
||||
props: { doc: testDoc },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
const header = wrapper.find('[data-test="preview-modal-header"]')
|
||||
// The filename span should have truncate class
|
||||
const filenameSpan = header.find('span.truncate')
|
||||
expect(filenameSpan.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,8 @@
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="delete-modal-title"
|
||||
class="bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4"
|
||||
data-test="folder-delete-modal-panel"
|
||||
class="bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<!-- Warning icon -->
|
||||
<div class="flex justify-center">
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
import FolderDeleteModal from '../FolderDeleteModal.vue'
|
||||
|
||||
const globalStubs = {
|
||||
AppIcon: true,
|
||||
}
|
||||
|
||||
const testFolder = { id: 'f1', name: 'Projects', doc_count: 3 }
|
||||
|
||||
describe('RESP-04: FolderDeleteModal mobile-safe panel', () => {
|
||||
it('panel has max-h-[90vh] class for mobile scroll safety', () => {
|
||||
const wrapper = mount(FolderDeleteModal, {
|
||||
props: { folder: testFolder },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
const panel = wrapper.find('[data-test="folder-delete-modal-panel"]')
|
||||
expect(panel.exists()).toBe(true)
|
||||
expect(panel.classes()).toContain('max-h-[90vh]')
|
||||
})
|
||||
|
||||
it('panel has overflow-y-auto class for mobile scroll', () => {
|
||||
const wrapper = mount(FolderDeleteModal, {
|
||||
props: { folder: testFolder },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
const panel = wrapper.find('[data-test="folder-delete-modal-panel"]')
|
||||
expect(panel.classes()).toContain('overflow-y-auto')
|
||||
})
|
||||
|
||||
it('panel has mx-4 horizontal margin so it fits within 375px viewport', () => {
|
||||
const wrapper = mount(FolderDeleteModal, {
|
||||
props: { folder: testFolder },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
const panel = wrapper.find('[data-test="folder-delete-modal-panel"]')
|
||||
expect(panel.classes()).toContain('mx-4')
|
||||
})
|
||||
|
||||
it('confirm and cancel buttons are accessible (text, role)', () => {
|
||||
const wrapper = mount(FolderDeleteModal, {
|
||||
props: { folder: testFolder },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
expect(wrapper.text()).toContain('Keep folder')
|
||||
expect(wrapper.text()).toContain('Delete folder and documents')
|
||||
})
|
||||
})
|
||||
@@ -9,7 +9,8 @@
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="share-modal-title"
|
||||
class="bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4 relative"
|
||||
data-test="share-modal-panel"
|
||||
class="bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4 relative max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<!-- Close button -->
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
// Mock stores and API so mount doesn't error out
|
||||
vi.mock('../../../stores/documents.js', () => ({
|
||||
useDocumentsStore: () => ({
|
||||
listShares: vi.fn().mockResolvedValue([]),
|
||||
shareDocument: vi.fn(),
|
||||
updateSharePermission: vi.fn(),
|
||||
revokeShare: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../stores/toast.js', () => ({
|
||||
useToastStore: () => ({ show: vi.fn() }),
|
||||
}))
|
||||
|
||||
import ShareModal from '../ShareModal.vue'
|
||||
|
||||
const globalStubs = {
|
||||
AppIcon: true,
|
||||
}
|
||||
|
||||
const testDoc = { id: 'doc-1', original_name: 'test.pdf' }
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('VISUAL-02: ShareModal form baseline (@tailwindcss/forms consistent pattern)', () => {
|
||||
it('text input uses consistent ring-2 focus class pattern', async () => {
|
||||
const wrapper = mount(ShareModal, {
|
||||
props: { doc: testDoc },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
await flushPromises()
|
||||
const input = wrapper.find('input[type="text"]')
|
||||
expect(input.exists()).toBe(true)
|
||||
expect(input.classes()).toContain('focus:ring-2')
|
||||
expect(input.classes()).toContain('focus:outline-none')
|
||||
})
|
||||
|
||||
it('select uses consistent ring-2 focus class pattern', async () => {
|
||||
const wrapper = mount(ShareModal, {
|
||||
props: { doc: testDoc },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
await flushPromises()
|
||||
const select = wrapper.find('select')
|
||||
expect(select.exists()).toBe(true)
|
||||
expect(select.classes()).toContain('focus:ring-2')
|
||||
expect(select.classes()).toContain('focus:outline-none')
|
||||
})
|
||||
})
|
||||
|
||||
describe('RESP-04: ShareModal mobile-safe panel', () => {
|
||||
it('panel has max-h-[90vh] class for mobile scroll safety', async () => {
|
||||
const wrapper = mount(ShareModal, {
|
||||
props: { doc: testDoc },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
await flushPromises()
|
||||
const panel = wrapper.find('[data-test="share-modal-panel"]')
|
||||
expect(panel.exists()).toBe(true)
|
||||
expect(panel.classes()).toContain('max-h-[90vh]')
|
||||
})
|
||||
|
||||
it('panel has overflow-y-auto class for mobile scroll', async () => {
|
||||
const wrapper = mount(ShareModal, {
|
||||
props: { doc: testDoc },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
await flushPromises()
|
||||
const panel = wrapper.find('[data-test="share-modal-panel"]')
|
||||
expect(panel.classes()).toContain('overflow-y-auto')
|
||||
})
|
||||
|
||||
it('panel has mx-4 horizontal margin so it fits within 375px viewport', async () => {
|
||||
const wrapper = mount(ShareModal, {
|
||||
props: { doc: testDoc },
|
||||
global: { stubs: globalStubs },
|
||||
})
|
||||
await flushPromises()
|
||||
const panel = wrapper.find('[data-test="share-modal-panel"]')
|
||||
expect(panel.classes()).toContain('mx-4')
|
||||
})
|
||||
})
|
||||
@@ -119,7 +119,8 @@
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cloud-delete-modal-title"
|
||||
class="bg-white rounded-2xl shadow-xl p-6 max-w-sm w-full mx-4"
|
||||
data-test="cloud-delete-modal-panel"
|
||||
class="bg-white rounded-2xl shadow-xl p-6 max-w-sm w-full mx-4 max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<AppIcon name="warning" class="w-5 h-5 text-amber-500 shrink-0" />
|
||||
|
||||
Reference in New Issue
Block a user