collavre 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/app/assets/images/collavre/landing/interface-en-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-en.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko.png +0 -0
- data/app/assets/stylesheets/collavre/actiontext.css +251 -90
- data/app/assets/stylesheets/collavre/code_highlight.css +7 -201
- data/app/assets/stylesheets/collavre/comments_popup.css +158 -61
- data/app/assets/stylesheets/collavre/creatives.css +11 -2
- data/app/assets/stylesheets/collavre/landing.css +92 -0
- data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +4 -2
- data/app/assets/stylesheets/collavre/tables.css +91 -0
- data/app/channels/collavre/inbox_badge_channel.rb +30 -0
- data/app/controllers/collavre/admin/settings_controller.rb +52 -67
- data/app/controllers/collavre/api/v1/agents_controller.rb +17 -188
- data/app/controllers/collavre/api/v1/base_controller.rb +9 -27
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +224 -0
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +95 -0
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +31 -0
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +25 -0
- data/app/controllers/collavre/attachments_controller.rb +13 -3
- data/app/controllers/collavre/concerns/slide_viewable.rb +8 -32
- data/app/controllers/collavre/creatives_controller.rb +47 -103
- data/app/controllers/collavre/inbox_items_controller.rb +10 -4
- data/app/controllers/collavre/landing_controller.rb +5 -2
- data/app/controllers/collavre/tasks_controller.rb +13 -4
- data/app/controllers/collavre/topics_controller.rb +54 -1
- data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
- data/app/controllers/concerns/collavre/api/authenticatable.rb +40 -0
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +16 -1
- data/app/controllers/concerns/collavre/users_controller/registration.rb +41 -1
- data/app/errors/collavre/api_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/helpers/collavre/creatives_helper.rb +30 -8
- data/app/javascript/collavre.js +2 -0
- data/app/javascript/components/ImageResizer.jsx +9 -3
- data/app/javascript/components/InlineLexicalEditor.jsx +157 -72
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +87 -0
- data/app/javascript/components/creative_tree_row.js +28 -5
- data/app/javascript/components/plugins/list_tab_indent_plugin.jsx +16 -0
- data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +15 -4
- data/app/javascript/components/plugins/table_hover_actions_plugin.jsx +405 -0
- data/app/javascript/controllers/__tests__/inbox_badge_controller.test.js +73 -0
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +104 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +82 -0
- data/app/javascript/controllers/comment_controller.js +5 -4
- data/app/javascript/controllers/comment_version_controller.js +2 -1
- data/app/javascript/controllers/comments/__tests__/form_controller_double_submit.test.js +159 -0
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +3 -2
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +94 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +61 -0
- data/app/javascript/controllers/comments/form_controller.js +21 -5
- data/app/javascript/controllers/comments/list_controller.js +18 -17
- data/app/javascript/controllers/comments/presence_controller.js +2 -1
- data/app/javascript/controllers/comments/topics_controller.js +61 -8
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +150 -0
- data/app/javascript/controllers/creatives/import_controller.js +2 -1
- data/app/javascript/controllers/creatives/select_mode_controller.js +2 -1
- data/app/javascript/controllers/creatives/tree_controller.js +142 -1
- data/app/javascript/controllers/image_lightbox_controller.js +2 -1
- data/app/javascript/controllers/inbox_badge_controller.js +33 -0
- data/app/javascript/controllers/index.js +7 -1
- data/app/javascript/controllers/search_popup_controller.js +17 -1
- data/app/javascript/controllers/share_modal_controller.js +4 -3
- data/app/javascript/controllers/topic_list_controller.js +78 -0
- data/app/javascript/controllers/topic_search_controller.js +2 -1
- data/app/javascript/creatives/drag_drop/event_handlers.js +14 -5
- data/app/javascript/creatives/topic_move_members_popup.js +156 -0
- data/app/javascript/creatives/tree_renderer.js +11 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +58 -0
- data/app/javascript/lib/__tests__/turbo_confirm.test.js +81 -0
- data/app/javascript/lib/__tests__/typo_correction.test.js +192 -0
- data/app/javascript/lib/api/__tests__/api_error.test.js +96 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +88 -1
- data/app/javascript/lib/api/api_error.js +108 -0
- data/app/javascript/lib/api/queue_manager.js +38 -4
- data/app/javascript/lib/common_popup.js +42 -10
- data/app/javascript/lib/editor/__tests__/code_edit_view_token_parity.test.js +121 -0
- data/app/javascript/lib/editor/__tests__/code_language_roundtrip.test.js +152 -0
- data/app/javascript/lib/editor/__tests__/code_languages.test.js +93 -0
- data/app/javascript/lib/editor/code_languages.js +173 -0
- data/app/javascript/lib/editor/code_token_theme.js +41 -0
- data/app/javascript/lib/lexical/__tests__/code_block_toggle.test.js +326 -0
- data/app/javascript/lib/lexical/__tests__/code_fence_shortcut.test.js +179 -0
- data/app/javascript/lib/lexical/__tests__/image_focus.test.js +139 -0
- data/app/javascript/lib/lexical/__tests__/list_tab_indent.test.js +633 -0
- data/app/javascript/lib/lexical/__tests__/markdown_serialize.test.js +627 -0
- data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +20 -1
- data/app/javascript/lib/lexical/__tests__/selection_boundary.test.js +88 -0
- data/app/javascript/lib/lexical/__tests__/table_transformer.test.js +163 -0
- data/app/javascript/lib/lexical/__tests__/trailing_paragraph.test.js +104 -0
- data/app/javascript/lib/lexical/code_block_toggle.js +130 -0
- data/app/javascript/lib/lexical/code_fence_shortcut.js +60 -0
- data/app/javascript/lib/lexical/list_tab_indent.js +210 -0
- data/app/javascript/lib/lexical/markdown_serialize.js +320 -0
- data/app/javascript/lib/lexical/selection_boundary.js +58 -0
- data/app/javascript/lib/lexical/table_transformer.js +182 -0
- data/app/javascript/lib/lexical/trailing_paragraph.js +29 -0
- data/app/javascript/lib/turbo_confirm.js +46 -0
- data/app/javascript/lib/typo_correction.js +146 -0
- data/app/javascript/lib/utils/__tests__/confirm_dialog.test.js +88 -0
- data/app/javascript/lib/utils/__tests__/dialog.test.js +92 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +153 -0
- data/app/javascript/lib/utils/__tests__/sanitize_description.test.js +68 -0
- data/app/javascript/lib/utils/__tests__/table_download.test.js +93 -0
- data/app/javascript/lib/utils/confirm_dialog.js +10 -0
- data/app/javascript/lib/utils/dialog.js +300 -0
- data/app/javascript/lib/utils/markdown.js +154 -67
- data/app/javascript/lib/utils/sanitize_description.js +31 -0
- data/app/javascript/lib/utils/table_download.js +15 -0
- data/app/javascript/modules/__tests__/creative_inline_payload.test.js +231 -0
- data/app/javascript/modules/__tests__/creative_save_queue.test.js +228 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +425 -0
- data/app/javascript/modules/__tests__/typo_corrector.test.js +365 -0
- data/app/javascript/modules/creative_inline_payload.js +86 -0
- data/app/javascript/modules/creative_row_editor.js +231 -471
- data/app/javascript/modules/creative_row_editor_helpers.js +76 -0
- data/app/javascript/modules/creative_save_queue.js +95 -0
- data/app/javascript/modules/creative_tree_dom.js +260 -0
- data/app/javascript/modules/export_to_markdown.js +2 -1
- data/app/javascript/modules/lexical_inline_editor.jsx +2 -1
- data/app/javascript/modules/slide_view.js +11 -2
- data/app/javascript/modules/typo_corrector.js +534 -0
- data/app/jobs/collavre/ai_agent_job.rb +7 -4
- data/app/jobs/collavre/compress_job.rb +6 -2
- data/app/jobs/collavre/permission_cache_job.rb +20 -2
- data/app/jobs/collavre/trigger_loop_check_job.rb +20 -3
- data/app/jobs/collavre/update_mcp_tools_job.rb +17 -0
- data/app/models/collavre/channel.rb +16 -0
- data/app/models/collavre/comment/approvable.rb +16 -0
- data/app/models/collavre/comment/broadcastable.rb +46 -7
- data/app/models/collavre/comment/notifiable.rb +15 -5
- data/app/models/collavre/comment.rb +87 -32
- data/app/models/collavre/comment_version.rb +1 -1
- data/app/models/collavre/creative/describable.rb +104 -19
- data/app/models/collavre/creative/linkable.rb +9 -0
- data/app/models/collavre/creative/permissible.rb +97 -49
- data/app/models/collavre/creative.rb +73 -5
- data/app/models/collavre/creative_share.rb +78 -26
- data/app/models/collavre/inbox_item.rb +2 -2
- data/app/models/collavre/task.rb +27 -0
- data/app/models/collavre/user.rb +90 -10
- data/app/models/concerns/collavre/indexed_json_columns.rb +58 -0
- data/app/services/collavre/agent_type_classifier.rb +23 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +4 -0
- data/app/services/collavre/ai_agent/message_builder.rb +1 -1
- data/app/services/collavre/ai_agent/session_provisioner.rb +126 -0
- data/app/services/collavre/ai_agent/task_claim_service.rb +96 -0
- data/app/services/collavre/ai_client.rb +81 -12
- data/app/services/collavre/auto_theme_generator.rb +15 -11
- data/app/services/collavre/creatives/children_index.rb +86 -0
- data/app/services/collavre/creatives/comment_badge_index.rb +73 -0
- data/app/services/collavre/creatives/creative_tree_serializer.rb +122 -0
- data/app/services/collavre/creatives/effective_creative_resolution.rb +37 -0
- data/app/services/collavre/creatives/index_query.rb +85 -16
- data/app/services/collavre/creatives/permission_checker.rb +1 -1
- data/app/services/collavre/creatives/permission_filter.rb +144 -17
- data/app/services/collavre/creatives/tree_builder.rb +107 -78
- data/app/services/collavre/gemini_parent_recommender.rb +1 -1
- data/app/services/collavre/http_client.rb +119 -0
- data/app/services/collavre/inbox_reply_service.rb +5 -0
- data/app/services/collavre/link_preview_fetcher.rb +129 -38
- data/app/services/collavre/markdown_converter.rb +13 -3
- data/app/services/collavre/markdown_importer.rb +30 -6
- data/app/services/collavre/mobile/event_summarizer.rb +40 -0
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -15
- data/app/services/collavre/orchestration/agent_orchestrator.rb +34 -8
- data/app/services/collavre/orchestration/arbiter.rb +16 -0
- data/app/services/collavre/orchestration/matcher.rb +79 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +4 -3
- data/app/services/collavre/orchestration/stuck_detector.rb +141 -34
- data/app/services/collavre/system_events/context_builder.rb +1 -14
- data/app/services/collavre/tools/creative_batch_service.rb +3 -2
- data/app/services/collavre/tools/creative_create_service.rb +8 -8
- data/app/services/collavre/tools/creative_update_service.rb +23 -8
- data/app/services/collavre/tools/preview_attach_service.rb +5 -7
- data/app/services/collavre/typo_corrector.rb +188 -0
- data/app/views/collavre/comments/_comment.html.erb +9 -4
- data/app/views/collavre/comments/_comments_popup.html.erb +25 -4
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +10 -0
- data/app/views/collavre/creatives/_topic_move_members_modal.html.erb +42 -0
- data/app/views/collavre/creatives/index.html.erb +14 -1
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/landing/show.html.erb +52 -10
- data/app/views/collavre/users/show.html.erb +3 -0
- data/app/views/collavre/users/typo_correction.html.erb +50 -0
- data/app/views/layouts/collavre/slide.html.erb +1 -0
- data/config/locales/comments.en.yml +16 -0
- data/config/locales/comments.ko.yml +16 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +5 -0
- data/config/locales/integrations.en.yml +1 -1
- data/config/locales/integrations.ko.yml +1 -1
- data/config/locales/landing.en.yml +22 -10
- data/config/locales/landing.ko.yml +22 -10
- data/config/locales/mobile.en.yml +16 -0
- data/config/locales/mobile.ko.yml +16 -0
- data/config/locales/orchestration.en.yml +1 -0
- data/config/locales/orchestration.ko.yml +1 -0
- data/config/locales/users.en.yml +15 -0
- data/config/locales/users.ko.yml +15 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +13 -4
- data/db/migrate/20260612000000_add_topic_concurrency_defer_to_comments.rb +38 -0
- data/db/migrate/20260617090000_add_typo_correction_settings_to_users.rb +18 -0
- data/db/migrate/20260702000001_enforce_system_settings_key_not_null.rb +9 -0
- data/db/migrate/20260702000002_add_creative_index_and_fk_to_tags.rb +11 -0
- data/db/migrate/20260702000003_add_creative_type_index_to_labels.rb +10 -0
- data/db/migrate/20260702000004_add_creative_created_at_index_to_comments.rb +10 -0
- data/db/migrate/20260702000005_promote_channel_config_index_columns.rb +76 -0
- data/db/migrate/20260713000001_add_comments_count_to_creatives.rb +20 -0
- data/db/migrate/20260713000002_add_comment_versions_count_to_comments.rb +20 -0
- data/db/migrate/20260715000000_change_creatives_description_to_plain_text.rb +16 -0
- data/db/seeds.rb +51 -0
- data/lib/collavre/version.rb +1 -1
- data/lib/generators/collavre/install/install_generator.rb +1 -0
- metadata +96 -2
- data/app/services/collavre/tools/description_normalizable.rb +0 -16
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { jest } from '@jest/globals'
|
|
6
|
+
|
|
7
|
+
const createSubscription = jest.fn()
|
|
8
|
+
const renderStreamMessage = jest.fn()
|
|
9
|
+
|
|
10
|
+
jest.unstable_mockModule('../../services/cable', () => ({
|
|
11
|
+
createSubscription,
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
jest.unstable_mockModule('@hotwired/turbo-rails', () => ({
|
|
15
|
+
Turbo: { renderStreamMessage },
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
const { Application } = await import('@hotwired/stimulus')
|
|
19
|
+
const InboxBadgeController = (await import('../inbox_badge_controller')).default
|
|
20
|
+
|
|
21
|
+
describe('InboxBadgeController', () => {
|
|
22
|
+
let application
|
|
23
|
+
let container
|
|
24
|
+
let subscription
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
subscription = { unsubscribe: jest.fn() }
|
|
28
|
+
createSubscription.mockReset()
|
|
29
|
+
renderStreamMessage.mockReset()
|
|
30
|
+
createSubscription.mockReturnValue(subscription)
|
|
31
|
+
|
|
32
|
+
container = document.createElement('div')
|
|
33
|
+
container.setAttribute('data-controller', 'inbox-badge')
|
|
34
|
+
document.body.appendChild(container)
|
|
35
|
+
|
|
36
|
+
application = Application.start()
|
|
37
|
+
application.register('inbox-badge', InboxBadgeController)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
application.stop()
|
|
42
|
+
document.body.innerHTML = ''
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('subscribes to the namespaced InboxBadgeChannel on connect', async () => {
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
47
|
+
|
|
48
|
+
expect(createSubscription).toHaveBeenCalledTimes(1)
|
|
49
|
+
expect(createSubscription).toHaveBeenCalledWith(
|
|
50
|
+
{ channel: 'Collavre::InboxBadgeChannel' },
|
|
51
|
+
expect.objectContaining({ received: expect.any(Function) }),
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('renders the transmitted badge snapshot through Turbo on its own subscription', async () => {
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
57
|
+
|
|
58
|
+
const [, callbacks] = createSubscription.mock.calls[0]
|
|
59
|
+
const snapshot = '<turbo-stream action="replace" target="desktop-inbox-badge"></turbo-stream>'
|
|
60
|
+
callbacks.received(snapshot)
|
|
61
|
+
|
|
62
|
+
expect(renderStreamMessage).toHaveBeenCalledWith(snapshot)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('unsubscribes when the controller disconnects', async () => {
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
67
|
+
|
|
68
|
+
container.remove()
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
70
|
+
|
|
71
|
+
expect(subscription.unsubscribe).toHaveBeenCalledTimes(1)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { jest } from '@jest/globals'
|
|
5
|
+
import { Application } from '@hotwired/stimulus'
|
|
6
|
+
|
|
7
|
+
const { default: SearchPopupController } = await import('../search_popup_controller')
|
|
8
|
+
|
|
9
|
+
describe('SearchPopupController Ctrl+K shortcut', () => {
|
|
10
|
+
let application
|
|
11
|
+
let container
|
|
12
|
+
let popup
|
|
13
|
+
|
|
14
|
+
function pressCtrlK(target) {
|
|
15
|
+
const event = new KeyboardEvent('keydown', {
|
|
16
|
+
key: 'k',
|
|
17
|
+
ctrlKey: true,
|
|
18
|
+
bubbles: true,
|
|
19
|
+
cancelable: true
|
|
20
|
+
})
|
|
21
|
+
target.dispatchEvent(event)
|
|
22
|
+
return event
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
document.body.innerHTML = ''
|
|
27
|
+
|
|
28
|
+
container = document.createElement('div')
|
|
29
|
+
container.innerHTML = `
|
|
30
|
+
<div data-controller="search-popup">
|
|
31
|
+
<input type="text" data-search-popup-target="input" />
|
|
32
|
+
<div data-search-popup-target="popup"></div>
|
|
33
|
+
</div>
|
|
34
|
+
`
|
|
35
|
+
document.body.appendChild(container)
|
|
36
|
+
|
|
37
|
+
application = Application.start()
|
|
38
|
+
application.register('search-popup', SearchPopupController)
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
40
|
+
|
|
41
|
+
popup = container.querySelector('[data-search-popup-target="popup"]')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
application?.stop()
|
|
46
|
+
document.body.innerHTML = ''
|
|
47
|
+
jest.restoreAllMocks()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('opens the popup on Ctrl+K when focus is not in an editable element', () => {
|
|
51
|
+
const event = pressCtrlK(document.body)
|
|
52
|
+
|
|
53
|
+
expect(popup.classList.contains('open')).toBe(true)
|
|
54
|
+
expect(event.defaultPrevented).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('does not open the popup when Ctrl+K is pressed inside a text input', () => {
|
|
58
|
+
const input = document.createElement('input')
|
|
59
|
+
input.type = 'text'
|
|
60
|
+
document.body.appendChild(input)
|
|
61
|
+
input.focus()
|
|
62
|
+
|
|
63
|
+
const event = pressCtrlK(input)
|
|
64
|
+
|
|
65
|
+
expect(popup.classList.contains('open')).toBe(false)
|
|
66
|
+
expect(event.defaultPrevented).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('does not open the popup when Ctrl+K is pressed inside a textarea', () => {
|
|
70
|
+
const textarea = document.createElement('textarea')
|
|
71
|
+
document.body.appendChild(textarea)
|
|
72
|
+
textarea.focus()
|
|
73
|
+
|
|
74
|
+
const event = pressCtrlK(textarea)
|
|
75
|
+
|
|
76
|
+
expect(popup.classList.contains('open')).toBe(false)
|
|
77
|
+
expect(event.defaultPrevented).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('does not open the popup when Ctrl+K is pressed inside a contenteditable element', () => {
|
|
81
|
+
const editable = document.createElement('div')
|
|
82
|
+
editable.setAttribute('contenteditable', 'true')
|
|
83
|
+
document.body.appendChild(editable)
|
|
84
|
+
editable.focus()
|
|
85
|
+
|
|
86
|
+
const event = pressCtrlK(editable)
|
|
87
|
+
|
|
88
|
+
expect(popup.classList.contains('open')).toBe(false)
|
|
89
|
+
expect(event.defaultPrevented).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('does not open the popup when Ctrl+K is pressed inside the inline Lexical editor', () => {
|
|
93
|
+
const lexicalRoot = document.createElement('div')
|
|
94
|
+
lexicalRoot.setAttribute('data-lexical-editor-root', '')
|
|
95
|
+
const inner = document.createElement('p')
|
|
96
|
+
lexicalRoot.appendChild(inner)
|
|
97
|
+
document.body.appendChild(lexicalRoot)
|
|
98
|
+
|
|
99
|
+
const event = pressCtrlK(inner)
|
|
100
|
+
|
|
101
|
+
expect(popup.classList.contains('open')).toBe(false)
|
|
102
|
+
expect(event.defaultPrevented).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { jest } from '@jest/globals'
|
|
5
|
+
import { Application } from '@hotwired/stimulus'
|
|
6
|
+
import TopicListController from '../topic_list_controller'
|
|
7
|
+
|
|
8
|
+
describe('TopicListController', () => {
|
|
9
|
+
let application, controller
|
|
10
|
+
|
|
11
|
+
const mount = () => {
|
|
12
|
+
document.body.innerHTML = `
|
|
13
|
+
<div id="topic-list-modal" class="common-popup" data-controller="topic-list">
|
|
14
|
+
<button data-topic-list-target="close">×</button>
|
|
15
|
+
<input data-topic-list-target="input">
|
|
16
|
+
<ul class="common-popup-list" data-popup-list data-topic-list-target="list"></ul>
|
|
17
|
+
</div>
|
|
18
|
+
`
|
|
19
|
+
application = Application.start()
|
|
20
|
+
application.register('topic-list', TopicListController)
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, 0)).then(() => {
|
|
22
|
+
controller = application.getControllerForElementAndIdentifier(
|
|
23
|
+
document.getElementById('topic-list-modal'), 'topic-list'
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const RECT = { top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }
|
|
29
|
+
const DATA = {
|
|
30
|
+
topics: [{ id: 1, name: 'Main' }, { id: 2, name: 'Alpha' }],
|
|
31
|
+
archivedTopics: [{ id: 3, name: 'Zeta' }],
|
|
32
|
+
mainTopicId: '1',
|
|
33
|
+
allMessagesLabel: 'All Messages'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const items = () => Array.from(document.querySelectorAll('#topic-list-modal li.common-popup-item'))
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
global.requestAnimationFrame = (fn) => { fn(); return 0 }
|
|
40
|
+
return mount()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
document.body.innerHTML = ''
|
|
45
|
+
application.stop()
|
|
46
|
+
jest.clearAllMocks()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('builds items in bar order: main, others, All Messages, archived', () => {
|
|
50
|
+
controller.openForTopics(DATA, RECT, () => {})
|
|
51
|
+
const labels = items().map((li) => li.textContent.trim())
|
|
52
|
+
expect(labels).toEqual(['#Main', '#Alpha', '📋 All Messages', '#Zeta'])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('marks archived items with the distinguishing class', () => {
|
|
56
|
+
controller.openForTopics(DATA, RECT, () => {})
|
|
57
|
+
const archived = items().filter((li) => li.querySelector('.topic-list-item--archived'))
|
|
58
|
+
expect(archived).toHaveLength(1)
|
|
59
|
+
expect(archived[0].textContent).toContain('#Zeta')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('filters by label substring and adds NO create option', () => {
|
|
63
|
+
controller.openForTopics(DATA, RECT, () => {})
|
|
64
|
+
controller.inputTarget.value = 'alpha'
|
|
65
|
+
controller._onInput()
|
|
66
|
+
const labels = items().map((li) => li.textContent.trim())
|
|
67
|
+
expect(labels).toEqual(['#Alpha'])
|
|
68
|
+
|
|
69
|
+
controller.inputTarget.value = 'brandnew'
|
|
70
|
+
controller._onInput()
|
|
71
|
+
expect(items()).toHaveLength(0) // no "create and move" pseudo-item
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('select invokes the callback with the item and closes', () => {
|
|
75
|
+
const cb = jest.fn()
|
|
76
|
+
controller.openForTopics(DATA, RECT, cb)
|
|
77
|
+
const item = { id: 2, label: '#Alpha', archived: false }
|
|
78
|
+
controller.select(item)
|
|
79
|
+
expect(cb).toHaveBeenCalledWith(item)
|
|
80
|
+
expect(controller.popup.isOpen()).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -3,6 +3,7 @@ import { renderCommentMarkdown, renderMermaidDiagrams } from '../lib/utils/markd
|
|
|
3
3
|
import { addTableDownloadButtons } from '../lib/utils/table_download'
|
|
4
4
|
import CommonPopup from '../lib/common_popup'
|
|
5
5
|
import csrfFetch from '../lib/api/csrf_fetch'
|
|
6
|
+
import { alertDialog, confirmDialog } from '../lib/utils/dialog'
|
|
6
7
|
|
|
7
8
|
// Global tracker: persists streaming state across Turbo replacements
|
|
8
9
|
// (each replacement creates a new controller instance, losing instance state)
|
|
@@ -248,7 +249,7 @@ export default class extends Controller {
|
|
|
248
249
|
const data = await response.json()
|
|
249
250
|
this.updateReactionsUI(data)
|
|
250
251
|
} catch (error) {
|
|
251
|
-
|
|
252
|
+
alertDialog(error?.message || 'Failed to update reaction')
|
|
252
253
|
}
|
|
253
254
|
}
|
|
254
255
|
|
|
@@ -495,7 +496,7 @@ export default class extends Controller {
|
|
|
495
496
|
const creativeId = button.dataset.creativeId
|
|
496
497
|
const confirmText = button.dataset.confirmText || 'Restore original messages?'
|
|
497
498
|
|
|
498
|
-
if (!
|
|
499
|
+
if (!(await confirmDialog(confirmText, { danger: true }))) return
|
|
499
500
|
|
|
500
501
|
button.disabled = true
|
|
501
502
|
try {
|
|
@@ -511,12 +512,12 @@ export default class extends Controller {
|
|
|
511
512
|
// the summary comment (this one) will be destroyed via broadcast too
|
|
512
513
|
} else {
|
|
513
514
|
const data = await response.json().catch(() => ({}))
|
|
514
|
-
|
|
515
|
+
alertDialog(data.error || 'Failed to restore')
|
|
515
516
|
button.disabled = false
|
|
516
517
|
}
|
|
517
518
|
} catch (error) {
|
|
518
519
|
console.error('Error restoring snapshot:', error)
|
|
519
|
-
|
|
520
|
+
alertDialog('Failed to restore')
|
|
520
521
|
button.disabled = false
|
|
521
522
|
}
|
|
522
523
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
import { renderCommentMarkdown, renderMermaidDiagrams } from "../lib/utils/markdown"
|
|
3
|
+
import { confirmDialog } from "../lib/utils/dialog"
|
|
3
4
|
|
|
4
5
|
export default class extends Controller {
|
|
5
6
|
static targets = ["prevBtn", "nextBtn", "indicator", "deleteBtn", "selectBtn"]
|
|
@@ -80,7 +81,7 @@ export default class extends Controller {
|
|
|
80
81
|
// Can't delete if it's the only version
|
|
81
82
|
if (versions.length <= 1) return
|
|
82
83
|
|
|
83
|
-
if (!
|
|
84
|
+
if (!(await confirmDialog(this.deleteBtnTarget.dataset.confirmMessage || "Are you sure?", { danger: true }))) return
|
|
84
85
|
|
|
85
86
|
const response = await fetch(
|
|
86
87
|
`${this.versionsUrlValue}/${version.id}`,
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { jest } from '@jest/globals'
|
|
6
|
+
import { Application } from '@hotwired/stimulus'
|
|
7
|
+
import FormController from '../form_controller'
|
|
8
|
+
|
|
9
|
+
// Regression harness for: "slow send API + double Enter => message sent twice".
|
|
10
|
+
//
|
|
11
|
+
// Scenario A proves the in-flight guard blocks a second Enter on the same
|
|
12
|
+
// controller instance. Scenario B reproduces the real-world bug: a Turbo/
|
|
13
|
+
// websocket morph reconnects the Stimulus controller mid-send, and (before the
|
|
14
|
+
// fix) connect() reset this.sending, blowing away the guard so an impatient
|
|
15
|
+
// second Enter re-sent the same message.
|
|
16
|
+
describe('FormController - double submit on slow API', () => {
|
|
17
|
+
let application
|
|
18
|
+
let container
|
|
19
|
+
let controller
|
|
20
|
+
|
|
21
|
+
const FIXTURE = `
|
|
22
|
+
<div id="comments-popup"
|
|
23
|
+
data-controller="comments--form"
|
|
24
|
+
data-review-feedback-placeholder="Write feedback for this quote..."
|
|
25
|
+
data-review-summary-placeholder="Overall comment (optional)..."
|
|
26
|
+
data-review-add-quote="+ Add"
|
|
27
|
+
data-review-send="Send review"
|
|
28
|
+
data-review-send-question="Send question"
|
|
29
|
+
data-review-type-review="💬"
|
|
30
|
+
data-review-type-question="❓"
|
|
31
|
+
data-review-type-review-label="Review"
|
|
32
|
+
data-review-type-question-label="Question">
|
|
33
|
+
<form data-comments--form-target="form">
|
|
34
|
+
<input type="hidden" name="comment[quoted_comment_id]" data-comments--form-target="quotedCommentId" value="" />
|
|
35
|
+
<input type="hidden" name="comment[quoted_text]" data-comments--form-target="quotedText" value="" />
|
|
36
|
+
<div data-comments--form-target="quoteIndicator" style="display:none;">
|
|
37
|
+
<span data-comments--form-target="quoteIndicatorText"></span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="review-quotes-container" data-comments--form-target="reviewQuotesContainer" style="display:none;"></div>
|
|
40
|
+
<textarea data-comments--form-target="textarea" rows="2"></textarea>
|
|
41
|
+
<input type="checkbox" data-comments--form-target="privateCheckbox" />
|
|
42
|
+
<button type="submit" data-comments--form-target="submit">Send</button>
|
|
43
|
+
<button data-comments--form-target="cancel" style="display:none;">Cancel</button>
|
|
44
|
+
<button data-comments--form-target="moveButton" style="display:none;">Move</button>
|
|
45
|
+
<button data-comments--form-target="searchButton" style="display:none;">Search</button>
|
|
46
|
+
<button data-comments--form-target="voiceButton" style="display:none;">Voice</button>
|
|
47
|
+
<input type="file" data-comments--form-target="imageInput" style="display:none;" />
|
|
48
|
+
<button data-comments--form-target="imageButton" style="display:none;">Image</button>
|
|
49
|
+
<div data-comments--form-target="attachmentList"></div>
|
|
50
|
+
</form>
|
|
51
|
+
</div>`
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
const meta = document.createElement('meta')
|
|
55
|
+
meta.name = 'csrf-token'
|
|
56
|
+
meta.content = 'test-token'
|
|
57
|
+
document.head.appendChild(meta)
|
|
58
|
+
|
|
59
|
+
if (!global.DataTransfer) {
|
|
60
|
+
global.DataTransfer = class {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.files = []
|
|
63
|
+
this.items = { add: (file) => this.files.push(file) }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
container = document.createElement('div')
|
|
69
|
+
container.innerHTML = FIXTURE
|
|
70
|
+
document.body.appendChild(container)
|
|
71
|
+
|
|
72
|
+
application = Application.start()
|
|
73
|
+
application.register('comments--form', FormController)
|
|
74
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
75
|
+
controller = application.getControllerForElementAndIdentifier(
|
|
76
|
+
container.querySelector('#comments-popup'),
|
|
77
|
+
'comments--form',
|
|
78
|
+
)
|
|
79
|
+
controller.creativeId = '123'
|
|
80
|
+
controller.currentTopicId = '10073'
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
application.stop()
|
|
85
|
+
document.body.removeChild(container)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const pressEnter = (textarea) => {
|
|
89
|
+
textarea.dispatchEvent(
|
|
90
|
+
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test('SCENARIO A: two Enter presses while a fetch is in-flight => fetch fires only once', () => {
|
|
95
|
+
const fetchSpy = jest.fn(() => new Promise(() => {})) // never resolves: slow API
|
|
96
|
+
global.fetch = fetchSpy
|
|
97
|
+
controller.creativeId = '111'
|
|
98
|
+
|
|
99
|
+
const textarea = controller.textareaTarget
|
|
100
|
+
textarea.value = 'hello world'
|
|
101
|
+
|
|
102
|
+
pressEnter(textarea)
|
|
103
|
+
pressEnter(textarea) // impatient second Enter
|
|
104
|
+
|
|
105
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('SCENARIO B: a controller reconnect (morph) mid-send must NOT re-enable sending', () => {
|
|
109
|
+
const fetchSpy = jest.fn(() => new Promise(() => {})) // never resolves: slow API
|
|
110
|
+
global.fetch = fetchSpy
|
|
111
|
+
controller.creativeId = '222'
|
|
112
|
+
|
|
113
|
+
const textarea = controller.textareaTarget
|
|
114
|
+
textarea.value = 'hello world'
|
|
115
|
+
|
|
116
|
+
pressEnter(textarea)
|
|
117
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
118
|
+
|
|
119
|
+
// A Turbo Stream / websocket broadcast morphs the comments area while the
|
|
120
|
+
// request is still in flight. Stimulus reconnects the controller, which
|
|
121
|
+
// resets the instance-only `this.sending` flag back to false.
|
|
122
|
+
controller.disconnect()
|
|
123
|
+
controller.connect()
|
|
124
|
+
controller.creativeId = '222'
|
|
125
|
+
controller.currentTopicId = '10073'
|
|
126
|
+
|
|
127
|
+
// The morph preserves the user's typed text; the impatient user hits Enter
|
|
128
|
+
// again because the slow API still has not responded.
|
|
129
|
+
controller.textareaTarget.value = 'hello world'
|
|
130
|
+
pressEnter(controller.textareaTarget)
|
|
131
|
+
|
|
132
|
+
// Durable (module-scope) guard must still block the duplicate.
|
|
133
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('SCENARIO C: after the request completes, the next message sends normally', async () => {
|
|
137
|
+
let resolveFetch
|
|
138
|
+
const fetchSpy = jest.fn(
|
|
139
|
+
() => new Promise((resolve) => { resolveFetch = resolve }),
|
|
140
|
+
)
|
|
141
|
+
global.fetch = fetchSpy
|
|
142
|
+
controller.creativeId = '333'
|
|
143
|
+
// Avoid touching the comment list / DOM rendering on success.
|
|
144
|
+
jest.spyOn(controller, 'resetForm').mockImplementation(() => {})
|
|
145
|
+
|
|
146
|
+
const textarea = controller.textareaTarget
|
|
147
|
+
textarea.value = 'first'
|
|
148
|
+
pressEnter(textarea)
|
|
149
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
150
|
+
|
|
151
|
+
// Resolve the first request, then let the promise microtasks (then/finally) run.
|
|
152
|
+
resolveFetch({ ok: true, status: 200, text: () => Promise.resolve('<div></div>') })
|
|
153
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
154
|
+
|
|
155
|
+
controller.textareaTarget.value = 'second'
|
|
156
|
+
pressEnter(controller.textareaTarget)
|
|
157
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -14,7 +14,6 @@ describe('CommentsPresenceController', () => {
|
|
|
14
14
|
beforeEach(async () => {
|
|
15
15
|
document.body.dataset.currentUserId = '7'
|
|
16
16
|
global.fetch = jest.fn()
|
|
17
|
-
global.alert = jest.fn()
|
|
18
17
|
|
|
19
18
|
container = document.createElement('div')
|
|
20
19
|
container.innerHTML = `
|
|
@@ -91,7 +90,9 @@ describe('CommentsPresenceController', () => {
|
|
|
91
90
|
|
|
92
91
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
93
92
|
|
|
94
|
-
|
|
93
|
+
// alertDialog renders an in-app modal (replacing native alert for the
|
|
94
|
+
// Tauri webview); assert the message surfaced there instead.
|
|
95
|
+
expect(document.querySelector('.confirm-dialog-message')?.textContent).toBe('No permission')
|
|
95
96
|
expect(close).toHaveBeenCalled()
|
|
96
97
|
})
|
|
97
98
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { jest } from '@jest/globals'
|
|
6
|
+
|
|
7
|
+
// deleteTopic awaits confirmDialog. The native click dispatch completes during
|
|
8
|
+
// that await, which resets event.currentTarget to null (DOM spec). Mock the
|
|
9
|
+
// dialog so the test controls when it resolves and can null currentTarget in
|
|
10
|
+
// between — reproducing the regression where the topic id was read after await.
|
|
11
|
+
jest.unstable_mockModule('../../../lib/utils/dialog', () => ({
|
|
12
|
+
confirmDialog: jest.fn(),
|
|
13
|
+
alertDialog: jest.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const { Application } = await import('@hotwired/stimulus')
|
|
17
|
+
const TopicsController = (await import('../topics_controller')).default
|
|
18
|
+
const { confirmDialog } = await import('../../../lib/utils/dialog')
|
|
19
|
+
|
|
20
|
+
describe('TopicsController#deleteTopic', () => {
|
|
21
|
+
let application
|
|
22
|
+
let container
|
|
23
|
+
let controller
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
container = document.createElement('div')
|
|
27
|
+
container.innerHTML = `
|
|
28
|
+
<div id="topics" data-controller="comments--topics">
|
|
29
|
+
<div data-comments--topics-target="list"></div>
|
|
30
|
+
</div>
|
|
31
|
+
`
|
|
32
|
+
document.body.appendChild(container)
|
|
33
|
+
|
|
34
|
+
application = Application.start()
|
|
35
|
+
application.register('comments--topics', TopicsController)
|
|
36
|
+
|
|
37
|
+
const meta = document.createElement('meta')
|
|
38
|
+
meta.name = 'csrf-token'
|
|
39
|
+
meta.content = 'test-csrf'
|
|
40
|
+
document.head.appendChild(meta)
|
|
41
|
+
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, 0)).then(() => {
|
|
43
|
+
const element = document.getElementById('topics')
|
|
44
|
+
controller = application.getControllerForElementAndIdentifier(element, 'comments--topics')
|
|
45
|
+
controller.creativeIdValue = '42'
|
|
46
|
+
controller.loadTopics = jest.fn()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
document.body.innerHTML = ''
|
|
52
|
+
document.head.innerHTML = ''
|
|
53
|
+
application.stop()
|
|
54
|
+
jest.clearAllMocks()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('issues DELETE with the topic id even when currentTarget is nulled after the await', async () => {
|
|
58
|
+
const fetchMock = jest.fn().mockResolvedValue({ ok: true })
|
|
59
|
+
global.fetch = fetchMock
|
|
60
|
+
|
|
61
|
+
// Simulate the event whose currentTarget is reset to null once the click
|
|
62
|
+
// dispatch finishes (which happens while confirmDialog is awaited).
|
|
63
|
+
const button = document.createElement('button')
|
|
64
|
+
button.dataset.id = '99'
|
|
65
|
+
const event = { stopPropagation: jest.fn(), currentTarget: button }
|
|
66
|
+
|
|
67
|
+
confirmDialog.mockImplementation(() => {
|
|
68
|
+
event.currentTarget = null // dispatch completed -> currentTarget reset
|
|
69
|
+
return Promise.resolve(true)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await controller.deleteTopic(event)
|
|
73
|
+
|
|
74
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
75
|
+
'/creatives/42/topics/99',
|
|
76
|
+
expect.objectContaining({ method: 'DELETE' })
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('does not issue DELETE when the user cancels the confirm dialog', async () => {
|
|
81
|
+
const fetchMock = jest.fn()
|
|
82
|
+
global.fetch = fetchMock
|
|
83
|
+
|
|
84
|
+
const button = document.createElement('button')
|
|
85
|
+
button.dataset.id = '99'
|
|
86
|
+
const event = { stopPropagation: jest.fn(), currentTarget: button }
|
|
87
|
+
|
|
88
|
+
confirmDialog.mockResolvedValue(false)
|
|
89
|
+
|
|
90
|
+
await controller.deleteTopic(event)
|
|
91
|
+
|
|
92
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { jest } from '@jest/globals'
|
|
5
|
+
import { Application } from '@hotwired/stimulus'
|
|
6
|
+
import TopicsController from '../topics_controller'
|
|
7
|
+
|
|
8
|
+
describe('TopicsController#openTopicListPopup', () => {
|
|
9
|
+
let application, controller
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
global.requestAnimationFrame = (fn) => { fn(); return 0 }
|
|
13
|
+
document.body.innerHTML = `
|
|
14
|
+
<div id="comments-popup" data-controller="comments--topics"
|
|
15
|
+
data-topic-main-text="All Messages"
|
|
16
|
+
data-topic-search-placeholder-text="Search topics...">
|
|
17
|
+
<div data-comments--topics-target="list"></div>
|
|
18
|
+
</div>
|
|
19
|
+
`
|
|
20
|
+
application = Application.start()
|
|
21
|
+
application.register('comments--topics', TopicsController)
|
|
22
|
+
return new Promise((resolve) => setTimeout(resolve, 0)).then(() => {
|
|
23
|
+
controller = application.getControllerForElementAndIdentifier(
|
|
24
|
+
document.getElementById('comments-popup'), 'comments--topics'
|
|
25
|
+
)
|
|
26
|
+
controller.topics = [{ id: 2, name: 'Alpha' }]
|
|
27
|
+
controller.archivedTopics = [{ id: 3, name: 'Zeta' }]
|
|
28
|
+
controller.mainTopicId = null
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
document.body.innerHTML = ''
|
|
34
|
+
application.stop()
|
|
35
|
+
jest.clearAllMocks()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('creates the topic-list modal with the required targets', () => {
|
|
39
|
+
const btn = document.createElement('button')
|
|
40
|
+
document.body.appendChild(btn)
|
|
41
|
+
controller.openTopicListPopup({ currentTarget: btn })
|
|
42
|
+
|
|
43
|
+
const modal = document.getElementById('topic-list-modal')
|
|
44
|
+
expect(modal).not.toBeNull()
|
|
45
|
+
expect(modal.dataset.controller).toBe('topic-list')
|
|
46
|
+
expect(modal.querySelector('[data-topic-list-target="input"]')).not.toBeNull()
|
|
47
|
+
expect(modal.querySelector('[data-topic-list-target="list"]')).not.toBeNull()
|
|
48
|
+
expect(modal.querySelector('[data-topic-list-target="close"]')).not.toBeNull()
|
|
49
|
+
expect(modal.querySelector('input').placeholder).toBe('Search topics...')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('appends the modal inside the chat box so it is caged within it', () => {
|
|
53
|
+
const btn = document.createElement('button')
|
|
54
|
+
document.body.appendChild(btn)
|
|
55
|
+
controller.openTopicListPopup({ currentTarget: btn })
|
|
56
|
+
|
|
57
|
+
const modal = document.getElementById('topic-list-modal')
|
|
58
|
+
// Bounded to the chat popup, not body-level.
|
|
59
|
+
expect(modal.parentElement).toBe(document.getElementById('comments-popup'))
|
|
60
|
+
})
|
|
61
|
+
})
|