diff --git a/app/static/commands.js b/app/static/commands.js index cefc2cb..4953b1d 100644 --- a/app/static/commands.js +++ b/app/static/commands.js @@ -158,8 +158,8 @@ } function onKeyDown(ev){ - // Check if typing in input field - if (['input','textarea'].includes(ev.target.tagName.toLowerCase())) return; + // Check if typing in input field or editor + if (isTypingInField(ev)) return; // Note: ? key (Shift+/) is now handled by keyboard-shortcuts-advanced.js for shortcuts panel // Command palette is opened with Ctrl+K @@ -172,11 +172,49 @@ let seq = []; let seqTimer = null; function resetSeq(){ seq = []; if (seqTimer) { clearTimeout(seqTimer); seqTimer = null; } } + + // Check if user is typing in input field or rich text editor + function isTypingInField(ev){ + const target = ev.target; + const tag = (target.tagName || '').toLowerCase(); + + // Check standard inputs + if (['input','textarea','select'].includes(tag) || target.isContentEditable) { + return true; + } + + // Check for rich text editors (Toast UI Editor, etc.) + const editorSelectors = [ + '.toastui-editor', '.toastui-editor-contents', '.ProseMirror', + '.CodeMirror', '.ql-editor', '.tox-edit-area', '.note-editable', + '[contenteditable="true"]', '.toastui-editor-ww-container', + '.toastui-editor-md-container' + ]; + + for (let i = 0; i < editorSelectors.length; i++) { + if (target.closest && target.closest(editorSelectors[i])) { + console.log('[Commands.js] Blocked - inside editor:', editorSelectors[i]); + return true; + } + } + + return false; + } + function sequenceHandler(ev){ if (ev.repeat) return; const key = ev.key.toLowerCase(); - if (['input','textarea'].includes(ev.target.tagName.toLowerCase())) return; // ignore typing fields + + // Check if typing in any input field or editor + if (isTypingInField(ev)) { + console.log('[Commands.js] Blocked - user is typing'); + resetSeq(); // Clear any partial sequence + return; + } + if (ev.ctrlKey || ev.metaKey || ev.altKey) return; // only plain keys + + console.log('[Commands.js] Processing key in sequence:', key, 'current seq:', seq); seq.push(key); if (seq.length > 2) seq.shift(); if (seq.length === 1 && seq[0] === 'g'){ @@ -185,6 +223,7 @@ } if (seq.length === 2 && seq[0] === 'g'){ const second = seq[1]; + console.log('[Commands.js] Executing navigation for g +', second); resetSeq(); if (second === 'd') return nav('/'); if (second === 'p') return nav('/projects'); diff --git a/app/static/keyboard-shortcuts-advanced.js b/app/static/keyboard-shortcuts-advanced.js index 4baa4dd..1897c9f 100644 --- a/app/static/keyboard-shortcuts-advanced.js +++ b/app/static/keyboard-shortcuts-advanced.js @@ -201,16 +201,32 @@ class KeyboardShortcutManager { * Handle key press */ handleKeyPress(e) { + // AGGRESSIVE DEBUG LOGGING + const debugInfo = { + key: e.key, + target: e.target, + tagName: e.target.tagName, + classList: e.target.classList ? Array.from(e.target.classList) : [], + isContentEditable: e.target.isContentEditable + }; + console.log('[KS-Advanced] Key pressed:', debugInfo); + // When palette is open, do not trigger a second open; let commands.js handle focus const palette = document.getElementById('commandPaletteModal'); const paletteOpen = palette && !palette.classList.contains('hidden'); - // Ignore if typing in input/textarea except for allowed global combos - if (this.isTyping(e)) { + // Check if typing in input field + const isTypingInInput = this.isTyping(e); + console.log('[KS-Advanced] isTyping result:', isTypingInInput); + + // If typing in input/textarea, ONLY allow specific global combos + if (isTypingInInput) { + console.log('[KS-Advanced] BLOCKED - User is typing in input field'); // Allow Ctrl+/ to focus search even when typing if ((e.ctrlKey || e.metaKey) && e.key === '/') { e.preventDefault(); this.toggleSearch(); + return; } // Allow Ctrl+K to open/focus palette even when typing else if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) { @@ -222,9 +238,20 @@ class KeyboardShortcutManager { } else { this.openCommandPalette(); } + return; } + // Allow Shift+? for shortcuts panel + else if (e.key === '?' && e.shiftKey) { + e.preventDefault(); + this.showShortcutsPanel(); + return; + } + // Block ALL other shortcuts when typing + console.log('[KS-Advanced] Blocking shortcut - in input field'); return; } + + console.log('[KS-Advanced] NOT blocked - processing shortcut'); const key = this.getKeyCombo(e); const normalizedKey = this.normalizeKey(key); @@ -310,40 +337,55 @@ class KeyboardShortcutManager { } /** - * Check if user is typing + * Check if user is typing in an input field */ isTyping(e) { const target = e.target; const tagName = target.tagName.toLowerCase(); - const isInput = tagName === 'input' || tagName === 'textarea' || target.isContentEditable; - // Don't block anything if not in an input - if (!isInput) { - return false; + console.log('[KS-Advanced isTyping] Checking:', { + tagName: tagName, + isContentEditable: target.isContentEditable, + classList: target.classList ? Array.from(target.classList) : [] + }); + + // Check standard input elements + if (tagName === 'input' || + tagName === 'textarea' || + tagName === 'select' || + target.isContentEditable) { + console.log('[KS-Advanced isTyping] TRUE - standard input'); + return true; } - // Allow Escape in search inputs to close/clear - if (target.type === 'search' && e.key === 'Escape') { - return false; + // Check for rich text editors (Toast UI Editor, TinyMCE, CodeMirror, etc.) + const richEditorSelectors = [ + '.toastui-editor', + '.toastui-editor-contents', + '.ProseMirror', + '.CodeMirror', + '.ql-editor', // Quill + '.tox-edit-area', // TinyMCE + '.note-editable', // Summernote + '[contenteditable="true"]', + // Additional Toast UI Editor specific selectors + '.toastui-editor-ww-container', + '.toastui-editor-md-container', + '.te-editor', + '.te-ww-container', + '.te-md-container' + ]; + + // Check if target is within any rich text editor + for (const selector of richEditorSelectors) { + if (target.closest && target.closest(selector)) { + console.log('[KS-Advanced isTyping] TRUE - inside editor:', selector); + return true; + } } - // Allow Ctrl+/ and Cmd+/ even in inputs for search - if (e.key === '/' && (e.ctrlKey || e.metaKey)) { - return false; - } - - // Allow Ctrl+K and Cmd+K even in inputs for command palette - if (e.key === 'k' && (e.ctrlKey || e.metaKey)) { - return false; - } - - // Allow Shift+? for shortcuts panel - if (e.key === '?' && e.shiftKey) { - return false; - } - - // Block all other keys when typing - return true; + console.log('[KS-Advanced isTyping] FALSE - not in input'); + return false; } /** diff --git a/app/static/keyboard-shortcuts-enhanced.js b/app/static/keyboard-shortcuts-enhanced.js index 3493499..4c737ff 100644 --- a/app/static/keyboard-shortcuts-enhanced.js +++ b/app/static/keyboard-shortcuts-enhanced.js @@ -395,6 +395,8 @@ * Handle key down event */ handleKeyDown(e) { + console.log('[KS-Enhanced] Key pressed:', e.key); + // Track pressed keys this.pressedKeys.add(e.key.toLowerCase()); @@ -404,16 +406,23 @@ return; } - // Skip if typing in input (except for allowed combos) - if (this.isTypingContext(e)) { - // Allow specific combos even in inputs - const combo = this.getKeyCombo(e); + // Check if typing in input first + const isTyping = this.isTypingContext(e); + const combo = this.getKeyCombo(e); + + console.log('[KS-Enhanced] isTyping:', isTyping, 'combo:', combo); + + // If typing in input, ONLY allow specific combos + if (isTyping) { if (!this.isAllowedInInput(combo)) { + console.log('[KS-Enhanced] BLOCKED - typing in input, not allowed combo'); + // Clear any key sequence when user is typing + this.resetSequence(); return; } + console.log('[KS-Enhanced] Allowed combo in input:', combo); } - const combo = this.getKeyCombo(e); const normalizedCombo = this.normalizeKeys(combo); // Check for custom shortcut override @@ -450,9 +459,12 @@ } } - // Handle key sequences (like 'g d') - if (!e.ctrlKey && !e.metaKey && !e.altKey && e.key.length === 1) { + // Handle key sequences (like 'g d') - but NOT if typing in input + if (!e.ctrlKey && !e.metaKey && !e.altKey && e.key.length === 1 && !isTyping) { + console.log('[KS-Enhanced] Processing sequence for key:', e.key); this.handleSequence(e); + } else { + console.log('[KS-Enhanced] NOT processing sequence - modifiers or typing'); } } @@ -467,7 +479,11 @@ * Handle key sequences like 'g d' or 'g p' */ handleSequence(e) { - if (this.isTypingContext(e)) return; + // Double-check: should never be called if typing, but just in case + if (this.isTypingContext(e)) { + this.resetSequence(); + return; + } clearTimeout(this.sequenceTimeout); @@ -540,10 +556,41 @@ isTypingContext(e) { const target = e.target; const tagName = target.tagName.toLowerCase(); - return tagName === 'input' || - tagName === 'textarea' || - tagName === 'select' || - target.isContentEditable; + + // Check standard input elements + if (tagName === 'input' || + tagName === 'textarea' || + tagName === 'select' || + target.isContentEditable) { + return true; + } + + // Check for rich text editors (Toast UI Editor, TinyMCE, CodeMirror, etc.) + const richEditorSelectors = [ + '.toastui-editor', + '.toastui-editor-contents', + '.ProseMirror', + '.CodeMirror', + '.ql-editor', // Quill + '.tox-edit-area', // TinyMCE + '.note-editable', // Summernote + '[contenteditable="true"]', + // Additional Toast UI Editor specific selectors + '.toastui-editor-ww-container', + '.toastui-editor-md-container', + '.te-editor', + '.te-ww-container', + '.te-md-container' + ]; + + // Check if target is within any rich text editor + for (const selector of richEditorSelectors) { + if (target.closest && target.closest(selector)) { + return true; + } + } + + return false; } /** diff --git a/app/static/keyboard-shortcuts.js b/app/static/keyboard-shortcuts.js index 7252f5e..2759b98 100644 --- a/app/static/keyboard-shortcuts.js +++ b/app/static/keyboard-shortcuts.js @@ -194,32 +194,35 @@ let sequenceTimer = null; document.addEventListener('keydown', (e) => { - // Ignore if typing in input - if (this.isTyping(e)) { - return; - } - - // Focus search with Ctrl+K or Cmd+K + // Focus search with Ctrl+K or Cmd+K (allowed even in inputs) if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); this.focusSearch(); return; } - // Open command palette with ? (main entry point) - if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) { + // Open command palette with ? (allowed even in inputs, but only if Shift is pressed) + if (e.key === '?' && e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); this.openCommandPalette(); return; } - // Help with Shift+? (or Ctrl/Cmd+?) + // Help with Shift+? (or Ctrl/Cmd+?) (allowed even in inputs) if ((e.key === '?' && e.shiftKey) || (e.key === '/' && e.ctrlKey)) { e.preventDefault(); this.showHelp(); return; } + // Ignore ALL other shortcuts if typing in input + if (this.isTyping(e)) { + // Clear any existing key sequence when user starts typing + keySequence = []; + clearTimeout(sequenceTimer); + return; + } + // Handle key sequences (like 'g' then 'd') clearTimeout(sequenceTimer); keySequence.push(e.key.toLowerCase()); @@ -571,12 +574,53 @@ isTyping(event) { const target = event.target; const tagName = target.tagName.toLowerCase(); - return ( - tagName === 'input' || + + // Debug logging (can be removed after testing) + if (window.location.pathname.includes('create') || window.location.pathname.includes('edit')) { + console.log('[Keyboard Shortcuts Debug]', { + target: target, + tagName: tagName, + classList: target.classList ? Array.from(target.classList) : [], + isContentEditable: target.isContentEditable, + parentClasses: target.parentElement ? Array.from(target.parentElement.classList || []) : [] + }); + } + + // Check standard input elements + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select' || - target.isContentEditable - ); + target.isContentEditable) { + return true; + } + + // Check for rich text editors (Toast UI Editor, TinyMCE, CodeMirror, etc.) + const richEditorSelectors = [ + '.toastui-editor', + '.toastui-editor-contents', + '.ProseMirror', + '.CodeMirror', + '.ql-editor', // Quill + '.tox-edit-area', // TinyMCE + '.note-editable', // Summernote + '[contenteditable="true"]', + // Additional Toast UI Editor specific selectors + '.toastui-editor-ww-container', + '.toastui-editor-md-container', + '.te-editor', + '.te-ww-container', + '.te-md-container' + ]; + + // Check if target is within any rich text editor + for (const selector of richEditorSelectors) { + if (target.closest && target.closest(selector)) { + console.log('[Keyboard Shortcuts] Blocked - inside editor:', selector); + return true; + } + } + + return false; } detectKeyboardNavigation() { diff --git a/app/templates/base.html b/app/templates/base.html index 587d690..339c1c5 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -407,14 +407,27 @@ - + - - + + diff --git a/tests/__pycache__/conftest.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/conftest.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..98a7e06 Binary files /dev/null and b/tests/__pycache__/conftest.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/smoke_test_email.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/smoke_test_email.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..d1cc3b2 Binary files /dev/null and b/tests/__pycache__/smoke_test_email.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_admin_email_routes.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_admin_email_routes.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..c864700 Binary files /dev/null and b/tests/__pycache__/test_admin_email_routes.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_admin_settings_logo.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_admin_settings_logo.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..1bd4c5a Binary files /dev/null and b/tests/__pycache__/test_admin_settings_logo.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_admin_users.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_admin_users.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..ab15345 Binary files /dev/null and b/tests/__pycache__/test_admin_users.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_analytics.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_analytics.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..57c55cc Binary files /dev/null and b/tests/__pycache__/test_analytics.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_api_comprehensive.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_api_comprehensive.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..8ce6eaa Binary files /dev/null and b/tests/__pycache__/test_api_comprehensive.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_api_v1.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_api_v1.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..b8c0413 Binary files /dev/null and b/tests/__pycache__/test_api_v1.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_basic.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_basic.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..b6289c9 Binary files /dev/null and b/tests/__pycache__/test_basic.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_calendar_event_model.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_calendar_event_model.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..789da0b Binary files /dev/null and b/tests/__pycache__/test_calendar_event_model.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_calendar_routes.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_calendar_routes.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..03f5217 Binary files /dev/null and b/tests/__pycache__/test_calendar_routes.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_client_note_model.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_client_note_model.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..2154c24 Binary files /dev/null and b/tests/__pycache__/test_client_note_model.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_client_notes_routes.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_client_notes_routes.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..60c6008 Binary files /dev/null and b/tests/__pycache__/test_client_notes_routes.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_comprehensive_tracking.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_comprehensive_tracking.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..2e49e08 Binary files /dev/null and b/tests/__pycache__/test_comprehensive_tracking.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_delete_actions.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_delete_actions.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..72e91b7 Binary files /dev/null and b/tests/__pycache__/test_delete_actions.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_email.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_email.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..fa32e8b Binary files /dev/null and b/tests/__pycache__/test_email.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_enhanced_ui.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_enhanced_ui.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..69147e0 Binary files /dev/null and b/tests/__pycache__/test_enhanced_ui.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_expenses.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_expenses.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..18fb22e Binary files /dev/null and b/tests/__pycache__/test_expenses.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_extra_good_model.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_extra_good_model.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..39a2ddc Binary files /dev/null and b/tests/__pycache__/test_extra_good_model.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_favorite_projects.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_favorite_projects.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..3100714 Binary files /dev/null and b/tests/__pycache__/test_favorite_projects.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_installation_config.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_installation_config.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..5e98ef9 Binary files /dev/null and b/tests/__pycache__/test_installation_config.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_invoice_currency_fix.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_invoice_currency_fix.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..f7c1b20 Binary files /dev/null and b/tests/__pycache__/test_invoice_currency_fix.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_invoice_currency_smoke.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_invoice_currency_smoke.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..aa6d96e Binary files /dev/null and b/tests/__pycache__/test_invoice_currency_smoke.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_invoice_expenses.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_invoice_expenses.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..ed3728e Binary files /dev/null and b/tests/__pycache__/test_invoice_expenses.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_invoices.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_invoices.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..4f3ea48 Binary files /dev/null and b/tests/__pycache__/test_invoices.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_keyboard_shortcuts.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_keyboard_shortcuts.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..c6efa41 Binary files /dev/null and b/tests/__pycache__/test_keyboard_shortcuts.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_keyboard_shortcuts_input_fix.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_keyboard_shortcuts_input_fix.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..c482689 Binary files /dev/null and b/tests/__pycache__/test_keyboard_shortcuts_input_fix.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_models_comprehensive.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_models_comprehensive.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..00e6201 Binary files /dev/null and b/tests/__pycache__/test_models_comprehensive.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_models_extended.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_models_extended.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..ff5ac6c Binary files /dev/null and b/tests/__pycache__/test_models_extended.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_new_features.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_new_features.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..0072b79 Binary files /dev/null and b/tests/__pycache__/test_new_features.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_oidc_logout.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_oidc_logout.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..ec94f69 Binary files /dev/null and b/tests/__pycache__/test_oidc_logout.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_overtime.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_overtime.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..b4667db Binary files /dev/null and b/tests/__pycache__/test_overtime.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_overtime_smoke.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_overtime_smoke.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..7dfe699 Binary files /dev/null and b/tests/__pycache__/test_overtime_smoke.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_payment_model.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_payment_model.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..9ec3cf2 Binary files /dev/null and b/tests/__pycache__/test_payment_model.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_payment_routes.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_payment_routes.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..87d31f6 Binary files /dev/null and b/tests/__pycache__/test_payment_routes.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_payment_smoke.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_payment_smoke.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..b370f9d Binary files /dev/null and b/tests/__pycache__/test_payment_smoke.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_pdf_layout.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_pdf_layout.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..c00980f Binary files /dev/null and b/tests/__pycache__/test_pdf_layout.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_permissions.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_permissions.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..b1f6cec Binary files /dev/null and b/tests/__pycache__/test_permissions.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_permissions_routes.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_permissions_routes.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..fe87f11 Binary files /dev/null and b/tests/__pycache__/test_permissions_routes.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_profile_avatar.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_profile_avatar.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..e74384c Binary files /dev/null and b/tests/__pycache__/test_profile_avatar.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_project_archiving.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_project_archiving.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..d5dd969 Binary files /dev/null and b/tests/__pycache__/test_project_archiving.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_project_archiving_models.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_project_archiving_models.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..617c235 Binary files /dev/null and b/tests/__pycache__/test_project_archiving_models.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_project_costs.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_project_costs.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..684e60b Binary files /dev/null and b/tests/__pycache__/test_project_costs.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_project_inactive_status.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_project_inactive_status.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..fc8452b Binary files /dev/null and b/tests/__pycache__/test_project_inactive_status.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_routes.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_routes.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..d7ab9db Binary files /dev/null and b/tests/__pycache__/test_routes.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_security.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_security.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..24bce1f Binary files /dev/null and b/tests/__pycache__/test_security.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_task_edit_project.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_task_edit_project.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..13b0808 Binary files /dev/null and b/tests/__pycache__/test_task_edit_project.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_tasks_filters_ui.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_tasks_filters_ui.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..079d4db Binary files /dev/null and b/tests/__pycache__/test_tasks_filters_ui.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_tasks_templates.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_tasks_templates.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..e4aaecd Binary files /dev/null and b/tests/__pycache__/test_tasks_templates.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_telemetry.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_telemetry.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..c916544 Binary files /dev/null and b/tests/__pycache__/test_telemetry.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_time_entry_duplication.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_time_entry_duplication.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..45c97d9 Binary files /dev/null and b/tests/__pycache__/test_time_entry_duplication.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_time_entry_templates.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_time_entry_templates.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..424dc0a Binary files /dev/null and b/tests/__pycache__/test_time_entry_templates.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_time_rounding.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_time_rounding.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..d4ef0c0 Binary files /dev/null and b/tests/__pycache__/test_time_rounding.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_timezone.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_timezone.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..8e41be7 Binary files /dev/null and b/tests/__pycache__/test_timezone.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_ui_quick_wins.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_ui_quick_wins.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..d30fab5 Binary files /dev/null and b/tests/__pycache__/test_ui_quick_wins.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_uploads_persistence.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_uploads_persistence.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..3ff3ac6 Binary files /dev/null and b/tests/__pycache__/test_uploads_persistence.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_utils.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_utils.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..4dee97b Binary files /dev/null and b/tests/__pycache__/test_utils.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_version_reading.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_version_reading.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..63df3dd Binary files /dev/null and b/tests/__pycache__/test_version_reading.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/__pycache__/test_weekly_goals.cpython-312-pytest-7.4.3.pyc b/tests/__pycache__/test_weekly_goals.cpython-312-pytest-7.4.3.pyc new file mode 100644 index 0000000..4747dee Binary files /dev/null and b/tests/__pycache__/test_weekly_goals.cpython-312-pytest-7.4.3.pyc differ diff --git a/tests/conftest.py b/tests/conftest.py index 81f9ba5..9e5a6f7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,7 @@ def app_config(): 'SERVER_NAME': 'localhost:5000', 'APPLICATION_ROOT': '/', 'PREFERRED_URL_SCHEME': 'http', + 'SESSION_COOKIE_HTTPONLY': True, } diff --git a/tests/smoke_test_email.py b/tests/smoke_test_email.py index 6b92897..b35c09c 100644 --- a/tests/smoke_test_email.py +++ b/tests/smoke_test_email.py @@ -11,68 +11,45 @@ from flask import url_for class TestEmailSmokeTests: """Smoke tests for email feature integration""" - def test_email_support_page_loads(self, client, admin_user): + def test_email_support_page_loads(self, admin_authenticated_client): """Smoke test: Email support page loads without errors""" - # Login as admin - with client: - login_response = client.post('/auth/login', data={ - 'username': admin_user.username, - 'password': 'password' - }, follow_redirects=True) - - assert login_response.status_code == 200 - - # Access email support page - response = client.get('/admin/email') - - # Page should load successfully - assert response.status_code == 200 - - # Check for key elements - assert b'Email Configuration' in response.data or b'email' in response.data.lower() - assert b'Test Email' in response.data or b'test' in response.data.lower() + # Access email support page + response = admin_authenticated_client.get('/admin/email') + + # Page should load successfully + assert response.status_code == 200 + + # Check for key elements + assert b'Email Configuration' in response.data or b'email' in response.data.lower() + assert b'Test Email' in response.data or b'test' in response.data.lower() - def check_email_configuration_status_api(self, client, admin_user): + def check_email_configuration_status_api(self, admin_authenticated_client): """Smoke test: Email configuration status API works""" - # Login as admin - with client: - client.post('/auth/login', data={ - 'username': admin_user.username, - 'password': 'password' - }, follow_redirects=True) - - # Get configuration status - response = client.get('/admin/email/config-status') - - # API should respond successfully - assert response.status_code == 200 - - # Response should be JSON - data = response.get_json() - assert data is not None - - # Should contain required fields - assert 'configured' in data - assert 'settings' in data - assert 'errors' in data - assert 'warnings' in data + # Get configuration status + response = admin_authenticated_client.get('/admin/email/config-status') + + # API should respond successfully + assert response.status_code == 200 + + # Response should be JSON + data = response.get_json() + assert data is not None + + # Should contain required fields + assert 'configured' in data + assert 'settings' in data + assert 'errors' in data + assert 'warnings' in data - def test_admin_dashboard_integration(self, client, admin_user): + def test_admin_dashboard_integration(self, admin_authenticated_client): """Smoke test: Email feature integrates with admin dashboard""" - # Login as admin - with client: - client.post('/auth/login', data={ - 'username': admin_user.username, - 'password': 'password' - }, follow_redirects=True) - - # Access admin dashboard - response = client.get('/admin') - - assert response.status_code == 200 - - # Admin dashboard should load successfully - assert b'Admin' in response.data + # Access admin dashboard + response = admin_authenticated_client.get('/admin') + + assert response.status_code == 200 + + # Admin dashboard should load successfully + assert b'Admin' in response.data def test_email_utilities_importable(self): """Smoke test: Email utilities can be imported""" @@ -186,16 +163,3 @@ class TestEmailFeatureIntegrity: # This is a basic check - the route should be registered assert callable(email_support) - -# Fixtures -@pytest.fixture -def admin_user(db): - """Create an admin user for testing""" - from app.models import User - user = User(username='admin_smoke', role='admin') - user.set_password('password') - user.is_active = True - db.session.add(user) - db.session.commit() - return user - diff --git a/tests/test_invoice_expenses.py b/tests/test_invoice_expenses.py index 0989c47..74e76c5 100644 --- a/tests/test_invoice_expenses.py +++ b/tests/test_invoice_expenses.py @@ -12,9 +12,8 @@ from app.models import Invoice, InvoiceItem, Expense, User, Project, Client def test_user(app): """Create a test user""" with app.app_context(): - user = User(username='testuser', email='test@example.com') + user = User(username='testuser', email='test@example.com', role='admin') user.set_password('testpass') - user.is_admin = True # Set after initialization db.session.add(user) db.session.commit() yield user diff --git a/tests/test_keyboard_shortcuts_input_fix.py b/tests/test_keyboard_shortcuts_input_fix.py new file mode 100644 index 0000000..e6bbd32 --- /dev/null +++ b/tests/test_keyboard_shortcuts_input_fix.py @@ -0,0 +1,290 @@ +""" +Test suite for keyboard shortcuts input field fix. + +This test ensures that keyboard shortcuts (like 'gr') do not trigger +when typing in input fields, textareas, or other editable elements. +""" + +import pytest +from flask import url_for +from app import db +from app.models import User, Project + + +class TestKeyboardShortcutsInputFix: + """Test keyboard shortcuts behavior in input fields.""" + + @pytest.fixture(autouse=True) + def setup(self, authenticated_client, user): + """Set up test fixtures.""" + self.client = authenticated_client + self.user = user + + def test_create_project_page_loads(self): + """Test that create project page loads successfully.""" + response = self.client.get('/projects/create') + assert response.status_code == 200 + assert b'Create Project' in response.data or b'New Project' in response.data + + def test_create_project_with_gr_in_name(self): + """Test creating a project with 'gr' in the name (e.g., 'program').""" + response = self.client.post('/projects/create', data={ + 'name': 'Program Development', + 'description': 'A program for testing', + 'status': 'active', + 'hourly_rate': '50.00' + }, follow_redirects=True) + + # Should successfully create the project + assert response.status_code == 200 + + # Verify project was created + project = Project.query.filter_by(name='Program Development').first() + assert project is not None + assert project.name == 'Program Development' + assert 'program' in project.description.lower() + + def test_create_task_with_trigger_in_name(self): + """Test creating a task with shortcut trigger keys in the name.""" + # First create a project + project = Project( + name='Test Project', + description='Test', + created_by=self.user.id, + status='active' + ) + db.session.add(project) + db.session.commit() + + # Create task with 'gr' in the name + response = self.client.post('/tasks/create', data={ + 'name': 'Upgrade system', + 'description': 'Migrate program to new version', + 'project_id': project.id, + 'status': 'todo', + 'priority': 'medium' + }, follow_redirects=True) + + assert response.status_code == 200 + + def test_project_name_with_multiple_triggers(self): + """Test project names containing multiple keyboard shortcut triggers.""" + test_names = [ + 'Program Graphics Design', # Contains 'gr' + 'Client Portal Development', # Contains 'port' + 'Integration Testing', # Contains 'int' + 'Graphics Rendering Engine', # Contains 'gr' + ] + + for name in test_names: + response = self.client.post('/projects/create', data={ + 'name': name, + 'description': f'Testing {name}', + 'status': 'active', + 'hourly_rate': '50.00' + }, follow_redirects=True) + + # Should successfully create without triggering shortcuts + project = Project.query.filter_by(name=name).first() + assert project is not None, f"Failed to create project: {name}" + + def test_keyboard_shortcuts_js_loaded(self): + """Test that keyboard shortcuts JavaScript files are loaded.""" + response = self.client.get('/') + assert response.status_code == 200 + + # Check that at least one keyboard shortcuts file is referenced + # (The actual check depends on how the JS is loaded in your templates) + data = response.data.decode('utf-8') + assert 'keyboard' in data.lower() or 'shortcut' in data.lower() + + +class TestKeyboardShortcutsJavaScriptLogic: + """Test JavaScript keyboard shortcuts logic (documentation tests).""" + + def test_istyping_method_exists(self): + """ + Documentation test: Verify isTyping/isTypingContext methods exist. + + The keyboard shortcut files should have methods to detect + when user is typing in an input field: + - keyboard-shortcuts.js: isTyping() + - keyboard-shortcuts-enhanced.js: isTypingContext() + - keyboard-shortcuts-advanced.js: isTyping() + """ + # Read the JavaScript files with UTF-8 encoding + with open('app/static/keyboard-shortcuts.js', 'r', encoding='utf-8') as f: + content = f.read() + assert 'isTyping' in content, "isTyping method not found in keyboard-shortcuts.js" + assert 'tagName === \'input\'' in content or 'tagname === "input"' in content.lower() + + with open('app/static/keyboard-shortcuts-enhanced.js', 'r', encoding='utf-8') as f: + content = f.read() + assert 'isTypingContext' in content, "isTypingContext method not found" + assert 'tagName' in content or 'tagname' in content.lower() + + with open('app/static/keyboard-shortcuts-advanced.js', 'r', encoding='utf-8') as f: + content = f.read() + assert 'isTyping' in content, "isTyping method not found in keyboard-shortcuts-advanced.js" + + def test_input_check_before_sequences(self): + """ + Documentation test: Verify input checks happen before sequence handling. + + The keyboard shortcut handlers should check if user is typing + BEFORE processing key sequences like 'g r'. + """ + with open('app/static/keyboard-shortcuts.js', 'r', encoding='utf-8') as f: + content = f.read() + # Should check isTyping before handling sequences + assert 'isTyping' in content + assert 'keySequence' in content + + def test_sequence_cleared_when_typing(self): + """ + Documentation test: Verify key sequences are cleared when typing. + + When user starts typing in an input field, any existing + key sequence should be cleared to prevent partial matches. + """ + with open('app/static/keyboard-shortcuts.js', 'r', encoding='utf-8') as f: + content = f.read() + # Look for sequence clearing logic + assert 'keySequence = []' in content or 'keySequence=[]' in content + + def test_allowed_shortcuts_in_inputs(self): + """ + Documentation test: Verify certain shortcuts are allowed in inputs. + + Some shortcuts like Ctrl+K (command palette), Ctrl+/ (search), + and Shift+? (help) should work even when in an input field. + """ + # Check keyboard-shortcuts-enhanced.js for allowed shortcuts + with open('app/static/keyboard-shortcuts-enhanced.js', 'r', encoding='utf-8') as f: + content = f.read() + assert 'isAllowedInInput' in content, "isAllowedInInput method not found" + # Should include ctrl+k, ctrl+/, shift+? + assert 'ctrl+k' in content.lower() or 'ctrlKey' in content + + def test_contenteditable_check(self): + """ + Documentation test: Verify contentEditable elements are handled. + + The isTyping check should also cover contentEditable elements, + not just input and textarea. + """ + with open('app/static/keyboard-shortcuts.js', 'r', encoding='utf-8') as f: + content = f.read() + assert 'isContentEditable' in content or 'contentEditable' in content + + def test_rich_text_editor_detection(self): + """ + Documentation test: Verify rich text editors are detected. + + The isTyping check should detect popular rich text editors like: + - Toast UI Editor (used in this project) + - TinyMCE + - Quill + - CodeMirror + """ + with open('app/static/keyboard-shortcuts.js', 'r', encoding='utf-8') as f: + content = f.read() + # Should check for Toast UI Editor + assert 'toastui-editor' in content.lower(), "Toast UI Editor detection not found" + # Should check for other popular editors + assert 'CodeMirror' in content or 'codemirror' in content.lower() + assert 'closest' in content, "Should use closest() to check parent elements" + + with open('app/static/keyboard-shortcuts-enhanced.js', 'r', encoding='utf-8') as f: + content = f.read() + assert 'toastui-editor' in content.lower(), "Toast UI Editor detection not found in enhanced" + + with open('app/static/keyboard-shortcuts-advanced.js', 'r', encoding='utf-8') as f: + content = f.read() + assert 'toastui-editor' in content.lower(), "Toast UI Editor detection not found in advanced" + + +class TestKeyboardShortcutsBugScenarios: + """Test specific bug scenarios reported by users.""" + + @pytest.fixture(autouse=True) + def setup(self, authenticated_client, user): + """Set up test fixtures.""" + self.client = authenticated_client + self.user = user + + def test_reported_bug_typing_program(self): + """ + Test the exact bug scenario: typing 'program' in create project. + + Bug report: When typing 'gr' in text field (e.g., in 'program'), + it triggers the 'g r' shortcut (Go to Reports). + + Expected: Should NOT trigger the shortcut, should type normally. + """ + response = self.client.post('/projects/create', data={ + 'name': 'New program', + 'description': 'program for programming', + 'status': 'active', + 'hourly_rate': '75.00' + }, follow_redirects=True) + + # Should create project successfully without being redirected to reports + assert response.status_code == 200 + + # Verify we're not on the reports page + assert b'Reports' not in response.data or b'New program' in response.data + + # Verify project was created + project = Project.query.filter_by(name='New program').first() + assert project is not None + assert 'program' in project.description + + def test_all_shortcut_triggers_in_text(self): + """ + Test typing text that contains all common shortcut triggers. + + Common shortcuts: + - g d: Go to Dashboard + - g p: Go to Projects + - g t: Go to Tasks + - g r: Go to Reports + - g i: Go to Invoices + """ + test_text = "a program with great ideas and graphics" + + response = self.client.post('/projects/create', data={ + 'name': test_text, + 'description': 'This project has: goals, graphics, great progress, grand ideas', + 'status': 'active', + 'hourly_rate': '60.00' + }, follow_redirects=True) + + assert response.status_code == 200 + + # Verify project was created with the full text + project = Project.query.filter_by(name=test_text).first() + assert project is not None + + +def test_smoke_keyboard_shortcuts_on_multiple_pages(authenticated_client): + """ + Smoke test: Verify keyboard shortcuts don't interfere on multiple pages. + + This test loads various pages to ensure the keyboard shortcuts + JavaScript is properly loaded and configured on all pages. + """ + # Test pages that have text inputs + pages_with_inputs = [ + '/projects/create', + '/tasks/create', + '/clients/create', + '/timer/manual-entry', + ] + + for page in pages_with_inputs: + response = authenticated_client.get(page) + # Should load successfully (200) or redirect to valid page (302) + assert response.status_code in [200, 302, 404], \ + f"Page {page} returned unexpected status: {response.status_code}" + diff --git a/tests/test_pdf_layout.py b/tests/test_pdf_layout.py index 7bbb069..7308d69 100644 --- a/tests/test_pdf_layout.py +++ b/tests/test_pdf_layout.py @@ -99,145 +99,103 @@ def test_pdf_layout_page_requires_admin(client, regular_user): @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_page_accessible_to_admin(client, admin_user): +def test_pdf_layout_page_accessible_to_admin(admin_authenticated_client): """Test that PDF layout page is accessible to admin.""" - with client: - # Login as admin - client.post('/auth/login', data={ - 'username': 'admin', - 'password': 'password123' - }) - - # Access PDF layout page - response = client.get('/admin/pdf-layout') - - assert response.status_code == 200 - assert b'PDF Layout Editor' in response.data or b'pdf' in response.data.lower() + # Access PDF layout page + response = admin_authenticated_client.get('/admin/pdf-layout') + + assert response.status_code == 200 + assert b'PDF Layout Editor' in response.data or b'pdf' in response.data.lower() @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_save_custom_template(client, admin_user, app): +def test_pdf_layout_save_custom_template(admin_authenticated_client, app): """Test saving custom PDF layout templates.""" - with client: - # Login as admin - client.post('/auth/login', data={ - 'username': 'admin', - 'password': 'password123' - }) - - custom_html = '

{{ invoice.invoice_number }}

' - custom_css = '.custom-invoice { color: red; }' - - # Save custom template - response = client.post('/admin/pdf-layout', data={ - 'invoice_pdf_template_html': custom_html, - 'invoice_pdf_template_css': custom_css - }, follow_redirects=True) - - assert response.status_code == 200 - - # Verify settings were saved - settings = Settings.get_settings() - assert settings.invoice_pdf_template_html == custom_html - assert settings.invoice_pdf_template_css == custom_css + custom_html = '

{{ invoice.invoice_number }}

' + custom_css = '.custom-invoice { color: red; }' + + # Save custom template + response = admin_authenticated_client.post('/admin/pdf-layout', data={ + 'invoice_pdf_template_html': custom_html, + 'invoice_pdf_template_css': custom_css + }, follow_redirects=True) + + assert response.status_code == 200 + + # Verify settings were saved + settings = Settings.get_settings() + assert settings.invoice_pdf_template_html == custom_html + assert settings.invoice_pdf_template_css == custom_css @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_reset_to_defaults(client, admin_user, app): +def test_pdf_layout_reset_to_defaults(admin_authenticated_client, app): """Test resetting PDF layout to defaults.""" - with client: - # Login as admin - client.post('/auth/login', data={ - 'username': 'admin', - 'password': 'password123' - }) - - # First, set custom templates - settings = Settings.get_settings() - settings.invoice_pdf_template_html = '
Custom HTML
' - settings.invoice_pdf_template_css = 'body { color: blue; }' - db.session.commit() - - # Reset to defaults - response = client.post('/admin/pdf-layout/reset', follow_redirects=True) - - assert response.status_code == 200 - - # Verify templates were cleared - settings = Settings.get_settings() - assert settings.invoice_pdf_template_html == '' - assert settings.invoice_pdf_template_css == '' + # First, set custom templates + settings = Settings.get_settings() + settings.invoice_pdf_template_html = '
Custom HTML
' + settings.invoice_pdf_template_css = 'body { color: blue; }' + db.session.commit() + + # Reset to defaults + response = admin_authenticated_client.post('/admin/pdf-layout/reset', follow_redirects=True) + + assert response.status_code == 200 + + # Verify templates were cleared + settings = Settings.get_settings() + assert settings.invoice_pdf_template_html == '' + assert settings.invoice_pdf_template_css == '' @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_get_defaults(client, admin_user): +def test_pdf_layout_get_defaults(admin_authenticated_client): """Test getting default PDF layout templates.""" - with client: - # Login as admin - client.post('/auth/login', data={ - 'username': 'admin', - 'password': 'password123' - }) - - # Get default templates - response = client.get('/admin/pdf-layout/default') - - assert response.status_code == 200 - assert response.is_json - - data = response.get_json() - assert 'html' in data - assert 'css' in data + # Get default templates + response = admin_authenticated_client.get('/admin/pdf-layout/default') + + assert response.status_code == 200 + assert response.is_json + + data = response.get_json() + assert 'html' in data + assert 'css' in data @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_preview(client, admin_user, sample_invoice): +def test_pdf_layout_preview(admin_authenticated_client, sample_invoice): """Test PDF layout preview functionality.""" - with client: - # Login as admin - client.post('/auth/login', data={ - 'username': 'admin', - 'password': 'password123' - }) - - # Test preview with custom HTML/CSS - response = client.post('/admin/pdf-layout/preview', data={ - 'html': '

Test Invoice {{ invoice.invoice_number }}

', - 'css': 'h1 { color: red; }', - 'invoice_id': sample_invoice.id - }) - - assert response.status_code == 200 - # Should return HTML content - assert b'Test Invoice' in response.data or b'INV-2024-001' in response.data + # Test preview with custom HTML/CSS + response = admin_authenticated_client.post('/admin/pdf-layout/preview', data={ + 'html': '

Test Invoice {{ invoice.invoice_number }}

', + 'css': 'h1 { color: red; }', + 'invoice_id': sample_invoice.id + }) + + assert response.status_code == 200 + # Should return HTML content + assert b'Test Invoice' in response.data or b'INV-2024-001' in response.data @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_preview_with_mock_invoice(client, admin_user, app): +def test_pdf_layout_preview_with_mock_invoice(admin_authenticated_client, app): """Test PDF layout preview with mock invoice when no real invoice exists.""" - with client: - # Login as admin - client.post('/auth/login', data={ - 'username': 'admin', - 'password': 'password123' - }) - - # Delete all invoices - Invoice.query.delete() - db.session.commit() - - # Test preview should still work with mock invoice - response = client.post('/admin/pdf-layout/preview', data={ - 'html': '

{{ invoice.invoice_number }}

', - 'css': 'h1 { color: blue; }' - }) - - assert response.status_code == 200 + # Delete all invoices + Invoice.query.delete() + db.session.commit() + + # Test preview should still work with mock invoice + response = admin_authenticated_client.post('/admin/pdf-layout/preview', data={ + 'html': '

{{ invoice.invoice_number }}

', + 'css': 'h1 { color: blue; }' + }) + + assert response.status_code == 200 @pytest.mark.models @@ -315,40 +273,26 @@ def test_pdf_generation_with_default_template(app, sample_invoice): @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_navigation_link_exists(client, admin_user): +def test_pdf_layout_navigation_link_exists(admin_authenticated_client): """Test that PDF layout link exists in admin navigation.""" - with client: - # Login as admin - client.post('/auth/login', data={ - 'username': 'admin', - 'password': 'password123' - }) - - # Access admin dashboard or any admin page - response = client.get('/admin/settings') - - assert response.status_code == 200 - # Should contain link to PDF layout page - # The link might be in the navigation or as a menu item + # Access admin dashboard or any admin page + response = admin_authenticated_client.get('/admin/settings') + + assert response.status_code == 200 + # Should contain link to PDF layout page + # The link might be in the navigation or as a menu item @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_form_csrf_protection(client, admin_user): +def test_pdf_layout_form_csrf_protection(admin_authenticated_client): """Test that PDF layout form has CSRF protection.""" - with client: - # Login as admin - client.post('/auth/login', data={ - 'username': 'admin', - 'password': 'password123' - }) - - # Get the PDF layout page - response = client.get('/admin/pdf-layout') - - assert response.status_code == 200 - # Should contain CSRF token - assert b'csrf_token' in response.data or b'name="csrf_token"' in response.data + # Get the PDF layout page + response = admin_authenticated_client.get('/admin/pdf-layout') + + assert response.status_code == 200 + # Should contain CSRF token + assert b'csrf_token' in response.data or b'name="csrf_token"' in response.data @pytest.mark.integration @@ -379,26 +323,19 @@ def test_pdf_layout_jinja_variable_rendering(app, sample_invoice): @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_rate_limiting(client, admin_user): +def test_pdf_layout_rate_limiting(admin_authenticated_client): """Test that PDF layout endpoints have rate limiting.""" - with client: - # Login as admin - client.post('/auth/login', data={ - 'username': 'admin', - 'password': 'password123' + # Make multiple rapid requests to preview endpoint + for i in range(65): # Exceeds the 60 per minute limit + response = admin_authenticated_client.post('/admin/pdf-layout/preview', data={ + 'html': '

Test

', + 'css': 'h1 { color: red; }' }) - # Make multiple rapid requests to preview endpoint - for i in range(65): # Exceeds the 60 per minute limit - response = client.post('/admin/pdf-layout/preview', data={ - 'html': '

Test

', - 'css': 'h1 { color: red; }' - }) - - # After 60 requests, should be rate limited - if i >= 60: - assert response.status_code == 429 # Too Many Requests - break + # After 60 requests, should be rate limited + if i >= 60: + assert response.status_code == 429 # Too Many Requests + break @pytest.mark.integration diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 48c47f1..e24828f 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -200,11 +200,14 @@ class TestTelemetryMarker: """Test that telemetry shouldn't be sent when marker exists""" with tempfile.NamedTemporaryFile(delete=False) as tmp: marker_path = tmp.name + try: + with patch.dict(os.environ, {'ENABLE_TELEMETRY': 'true'}): + assert should_send_telemetry(marker_path) is False + finally: try: - with patch.dict(os.environ, {'ENABLE_TELEMETRY': 'true'}): - assert should_send_telemetry(marker_path) is False - finally: os.unlink(marker_path) + except (PermissionError, OSError): + pass # Ignore Windows file permission errors def test_mark_telemetry_sent_creates_file(self): """Test that marking telemetry as sent creates marker file""" @@ -257,14 +260,17 @@ class TestCheckAndSendTelemetry: """Test that telemetry is not sent when already marked as sent""" with tempfile.NamedTemporaryFile(delete=False) as tmp: marker_path = tmp.name + try: + with patch.dict(os.environ, { + 'ENABLE_TELEMETRY': 'true', + 'TELEMETRY_MARKER_FILE': marker_path + }): + result = check_and_send_telemetry() + assert result is False + assert not mock_send.called + finally: try: - with patch.dict(os.environ, { - 'ENABLE_TELEMETRY': 'true', - 'TELEMETRY_MARKER_FILE': marker_path - }): - result = check_and_send_telemetry() - assert result is False - assert not mock_send.called - finally: os.unlink(marker_path) + except (PermissionError, OSError): + pass # Ignore Windows file permission errors diff --git a/tests/test_timezone.py b/tests/test_timezone.py index 55e6b73..2745fac 100644 --- a/tests/test_timezone.py +++ b/tests/test_timezone.py @@ -12,7 +12,8 @@ def app(): 'TESTING': True, 'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:', 'WTF_CSRF_ENABLED': False, - 'SECRET_KEY': 'test-secret-key' + 'SECRET_KEY': 'test-secret-key-for-testing-at-least-32-chars', + 'FLASK_ENV': 'testing' }) with app.app_context():