@dfosco/storyboard-core 1.13.0 → 1.15.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.
- package/package.json +1 -1
- package/src/bodyClasses.js +1 -1
- package/src/comments/commentMode.test.js +6 -3
- package/src/comments/ui/authModal.js +29 -18
- package/src/comments/ui/authModal.test.js +166 -0
- package/src/comments/ui/commentsDrawer.js +1 -1
- package/src/comments/ui/mount.js +1 -3
- package/src/comments/ui/mount.test.js +205 -0
- package/src/devtools.js +1 -1
- package/src/hideMode.test.js +0 -4
package/package.json
CHANGED
package/src/bodyClasses.js
CHANGED
|
@@ -35,7 +35,8 @@ describe('commentMode', () => {
|
|
|
35
35
|
|
|
36
36
|
it('toggleCommentMode returns false when not authenticated', () => {
|
|
37
37
|
initCommentsConfig({
|
|
38
|
-
comments: {
|
|
38
|
+
comments: { discussions: { category: 'Test' } },
|
|
39
|
+
repository: { owner: 'o', name: 'r' },
|
|
39
40
|
})
|
|
40
41
|
const result = toggleCommentMode()
|
|
41
42
|
expect(result).toBe(false)
|
|
@@ -44,7 +45,8 @@ describe('commentMode', () => {
|
|
|
44
45
|
|
|
45
46
|
it('toggleCommentMode activates when enabled and authenticated', () => {
|
|
46
47
|
initCommentsConfig({
|
|
47
|
-
comments: {
|
|
48
|
+
comments: { discussions: { category: 'Test' } },
|
|
49
|
+
repository: { owner: 'o', name: 'r' },
|
|
48
50
|
})
|
|
49
51
|
setToken('ghp_test')
|
|
50
52
|
const result = toggleCommentMode()
|
|
@@ -54,7 +56,8 @@ describe('commentMode', () => {
|
|
|
54
56
|
|
|
55
57
|
it('toggleCommentMode toggles off when active', () => {
|
|
56
58
|
initCommentsConfig({
|
|
57
|
-
comments: {
|
|
59
|
+
comments: { discussions: { category: 'Test' } },
|
|
60
|
+
repository: { owner: 'o', name: 'r' },
|
|
58
61
|
})
|
|
59
62
|
setToken('ghp_test')
|
|
60
63
|
toggleCommentMode() // on
|
|
@@ -8,6 +8,10 @@ import { setToken, validateToken, clearToken, getCachedUser } from '../auth.js'
|
|
|
8
8
|
|
|
9
9
|
const MODAL_ID = 'sb-auth-modal'
|
|
10
10
|
|
|
11
|
+
// Mutable ref updated on each openAuthModal() call so the Alpine factory
|
|
12
|
+
// (registered once) always reaches the *current* modal's resolve/backdrop.
|
|
13
|
+
const _ref = { resolve: null, onKeyDown: null, backdrop: null }
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* Open the auth modal. Returns a promise that resolves with the user info
|
|
13
17
|
* on successful sign-in, or null if cancelled.
|
|
@@ -65,27 +69,34 @@ export function openAuthModal() {
|
|
|
65
69
|
|
|
66
70
|
document.body.appendChild(backdrop)
|
|
67
71
|
|
|
68
|
-
// Close on backdrop click
|
|
69
|
-
backdrop.addEventListener('click', (e) => {
|
|
70
|
-
if (e.target === backdrop) {
|
|
71
|
-
backdrop.remove()
|
|
72
|
-
resolve(null)
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
|
|
76
72
|
// Escape key
|
|
77
73
|
function onKeyDown(e) {
|
|
78
74
|
if (e.key === 'Escape') {
|
|
79
75
|
e.preventDefault()
|
|
80
76
|
e.stopPropagation()
|
|
81
|
-
window.removeEventListener('keydown', onKeyDown, true)
|
|
82
|
-
backdrop.remove()
|
|
83
|
-
resolve(null)
|
|
77
|
+
window.removeEventListener('keydown', _ref.onKeyDown, true)
|
|
78
|
+
_ref.backdrop.remove()
|
|
79
|
+
_ref.resolve(null)
|
|
84
80
|
}
|
|
85
81
|
}
|
|
82
|
+
|
|
83
|
+
// Update shared ref so Alpine callbacks always target the current modal
|
|
84
|
+
_ref.resolve = resolve
|
|
85
|
+
_ref.onKeyDown = onKeyDown
|
|
86
|
+
_ref.backdrop = backdrop
|
|
87
|
+
|
|
88
|
+
// Close on backdrop click
|
|
89
|
+
backdrop.addEventListener('click', (e) => {
|
|
90
|
+
if (e.target === backdrop) {
|
|
91
|
+
window.removeEventListener('keydown', _ref.onKeyDown, true)
|
|
92
|
+
_ref.backdrop.remove()
|
|
93
|
+
_ref.resolve(null)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
86
97
|
window.addEventListener('keydown', onKeyDown, true)
|
|
87
98
|
|
|
88
|
-
// Register Alpine component
|
|
99
|
+
// Register Alpine component once — reads _ref for current modal context
|
|
89
100
|
if (!window.Alpine._sbAuthRegistered) {
|
|
90
101
|
window.Alpine.data('sbAuthModal', () => ({
|
|
91
102
|
token: '',
|
|
@@ -112,16 +123,16 @@ export function openAuthModal() {
|
|
|
112
123
|
},
|
|
113
124
|
|
|
114
125
|
done() {
|
|
115
|
-
window.removeEventListener('keydown', onKeyDown, true)
|
|
126
|
+
window.removeEventListener('keydown', _ref.onKeyDown, true)
|
|
116
127
|
const user = this.user
|
|
117
|
-
backdrop.remove()
|
|
118
|
-
resolve(user)
|
|
128
|
+
_ref.backdrop.remove()
|
|
129
|
+
_ref.resolve(user)
|
|
119
130
|
},
|
|
120
131
|
|
|
121
132
|
close() {
|
|
122
|
-
window.removeEventListener('keydown', onKeyDown, true)
|
|
123
|
-
backdrop.remove()
|
|
124
|
-
resolve(null)
|
|
133
|
+
window.removeEventListener('keydown', _ref.onKeyDown, true)
|
|
134
|
+
_ref.backdrop.remove()
|
|
135
|
+
_ref.resolve(null)
|
|
125
136
|
},
|
|
126
137
|
}))
|
|
127
138
|
window.Alpine._sbAuthRegistered = true
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for authModal.js — PAT entry modal lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Alpine.js is stubbed: initTree triggers the Alpine component methods
|
|
5
|
+
* directly so we can test the modal's resolve/reject/close behavior
|
|
6
|
+
* without a real Alpine runtime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { vi } from 'vitest'
|
|
10
|
+
import { clearToken } from '../auth.js' // eslint-disable-line no-unused-vars -- kept for future test coverage
|
|
11
|
+
|
|
12
|
+
// Store the Alpine component factory so tests can call done()/close()/submit()
|
|
13
|
+
let alpineFactory = null
|
|
14
|
+
let alpineInitTree = null
|
|
15
|
+
|
|
16
|
+
vi.mock('alpinejs', () => ({
|
|
17
|
+
default: {
|
|
18
|
+
start: vi.fn(),
|
|
19
|
+
data: vi.fn((name, factory) => { alpineFactory = factory }),
|
|
20
|
+
initTree: vi.fn((el) => { alpineInitTree?.(el) }),
|
|
21
|
+
},
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
describe('authModal.js', () => {
|
|
25
|
+
let openAuthModal, signOut
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
document.body.innerHTML = ''
|
|
29
|
+
localStorage.clear()
|
|
30
|
+
alpineFactory = null
|
|
31
|
+
alpineInitTree = null
|
|
32
|
+
|
|
33
|
+
vi.resetModules()
|
|
34
|
+
|
|
35
|
+
const mockAlpine = {
|
|
36
|
+
_sbAuthRegistered: false,
|
|
37
|
+
start: vi.fn(),
|
|
38
|
+
data: vi.fn((name, factory) => { alpineFactory = factory }),
|
|
39
|
+
initTree: vi.fn((el) => { alpineInitTree?.(el) }),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
vi.doMock('alpinejs', () => ({ default: mockAlpine }))
|
|
43
|
+
|
|
44
|
+
// authModal.js reads window.Alpine directly (set by mount.js at runtime)
|
|
45
|
+
window.Alpine = mockAlpine
|
|
46
|
+
|
|
47
|
+
const mod = await import('./authModal.js')
|
|
48
|
+
openAuthModal = mod.openAuthModal
|
|
49
|
+
signOut = mod.signOut
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.restoreAllMocks()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('creates a backdrop element in the DOM', () => {
|
|
57
|
+
openAuthModal()
|
|
58
|
+
|
|
59
|
+
const backdrop = document.getElementById('sb-auth-modal')
|
|
60
|
+
expect(backdrop).not.toBeNull()
|
|
61
|
+
expect(backdrop.classList.contains('sb-auth-backdrop')).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('removes old modal before creating a new one', () => {
|
|
65
|
+
openAuthModal()
|
|
66
|
+
openAuthModal()
|
|
67
|
+
|
|
68
|
+
const modals = document.querySelectorAll('#sb-auth-modal')
|
|
69
|
+
expect(modals.length).toBe(1)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('resolves null when Escape is pressed', async () => {
|
|
73
|
+
const promise = openAuthModal()
|
|
74
|
+
|
|
75
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
76
|
+
|
|
77
|
+
const result = await promise
|
|
78
|
+
expect(result).toBeNull()
|
|
79
|
+
expect(document.getElementById('sb-auth-modal')).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('resolves null when backdrop is clicked', async () => {
|
|
83
|
+
const promise = openAuthModal()
|
|
84
|
+
|
|
85
|
+
const backdrop = document.getElementById('sb-auth-modal')
|
|
86
|
+
backdrop.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
87
|
+
|
|
88
|
+
const result = await promise
|
|
89
|
+
expect(result).toBeNull()
|
|
90
|
+
expect(document.getElementById('sb-auth-modal')).toBeNull()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('does not close when inner modal content is clicked', () => {
|
|
94
|
+
openAuthModal()
|
|
95
|
+
|
|
96
|
+
const inner = document.querySelector('.sb-auth-modal')
|
|
97
|
+
inner.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
98
|
+
|
|
99
|
+
// Modal should still be present
|
|
100
|
+
expect(document.getElementById('sb-auth-modal')).not.toBeNull()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('registers Alpine component with sbAuthModal name', () => {
|
|
104
|
+
openAuthModal()
|
|
105
|
+
expect(alpineFactory).toBeTypeOf('function')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('Alpine component methods via _ref', () => {
|
|
109
|
+
it('done() resolves promise with user and removes modal', async () => {
|
|
110
|
+
const promise = openAuthModal()
|
|
111
|
+
|
|
112
|
+
// Simulate what Alpine does: create instance from factory, set user, call done()
|
|
113
|
+
const instance = alpineFactory()
|
|
114
|
+
instance.user = { login: 'testuser', avatarUrl: 'https://example.com/avatar.png' }
|
|
115
|
+
instance.done()
|
|
116
|
+
|
|
117
|
+
const result = await promise
|
|
118
|
+
expect(result).toEqual({ login: 'testuser', avatarUrl: 'https://example.com/avatar.png' })
|
|
119
|
+
expect(document.getElementById('sb-auth-modal')).toBeNull()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('close() resolves promise with null and removes modal', async () => {
|
|
123
|
+
const promise = openAuthModal()
|
|
124
|
+
|
|
125
|
+
const instance = alpineFactory()
|
|
126
|
+
instance.close()
|
|
127
|
+
|
|
128
|
+
const result = await promise
|
|
129
|
+
expect(result).toBeNull()
|
|
130
|
+
expect(document.getElementById('sb-auth-modal')).toBeNull()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('done() on second modal resolves the second promise, not the first', async () => {
|
|
134
|
+
// Open first modal, then close via Escape
|
|
135
|
+
const promise1 = openAuthModal()
|
|
136
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
137
|
+
const result1 = await promise1
|
|
138
|
+
expect(result1).toBeNull()
|
|
139
|
+
|
|
140
|
+
// Reset Alpine registration so factory is recaptured on next open
|
|
141
|
+
window.Alpine._sbAuthRegistered = false
|
|
142
|
+
|
|
143
|
+
// Open second modal
|
|
144
|
+
const promise2 = openAuthModal()
|
|
145
|
+
const instance2 = alpineFactory()
|
|
146
|
+
instance2.user = { login: 'user2', avatarUrl: 'https://example.com/u2.png' }
|
|
147
|
+
instance2.done()
|
|
148
|
+
|
|
149
|
+
const result2 = await promise2
|
|
150
|
+
expect(result2).toEqual({ login: 'user2', avatarUrl: 'https://example.com/u2.png' })
|
|
151
|
+
expect(document.getElementById('sb-auth-modal')).toBeNull()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('signOut', () => {
|
|
156
|
+
it('clears the stored token', () => {
|
|
157
|
+
localStorage.setItem('sb-comments-token', 'ghp_test')
|
|
158
|
+
localStorage.setItem('sb-comments-user', JSON.stringify({ login: 'test' }))
|
|
159
|
+
|
|
160
|
+
signOut()
|
|
161
|
+
|
|
162
|
+
expect(localStorage.getItem('sb-comments-token')).toBeNull()
|
|
163
|
+
expect(localStorage.getItem('sb-comments-user')).toBeNull()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -16,7 +16,7 @@ function timeAgo(dateStr) {
|
|
|
16
16
|
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function esc(str) {
|
|
19
|
+
function esc(str) { // eslint-disable-line no-unused-vars
|
|
20
20
|
const d = document.createElement('div')
|
|
21
21
|
d.textContent = str ?? ''
|
|
22
22
|
return d.innerHTML
|
package/src/comments/ui/mount.js
CHANGED
|
@@ -28,14 +28,12 @@ function esc(str) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function getContentContainer() {
|
|
31
|
-
return document.
|
|
31
|
+
return document.body
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function ensureOverlay() {
|
|
35
35
|
if (overlay) return overlay
|
|
36
36
|
const container = getContentContainer()
|
|
37
|
-
const pos = getComputedStyle(container).position
|
|
38
|
-
if (pos === 'static') container.style.position = 'relative'
|
|
39
37
|
|
|
40
38
|
overlay = document.createElement('div')
|
|
41
39
|
overlay.className = 'sb-comment-overlay absolute top-0 right-0 bottom-0 left-0 pe-none'
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for mount.js — comment overlay, banner, and body comment-mode logic.
|
|
3
|
+
*
|
|
4
|
+
* Alpine.js and heavy UI deps are mocked; we test DOM-level behavior of the
|
|
5
|
+
* exported mountComments() plus the internal helpers it exercises.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { vi } from 'vitest'
|
|
9
|
+
|
|
10
|
+
// ---- Mocks (must be before importing mount.js) ----
|
|
11
|
+
|
|
12
|
+
vi.mock('alpinejs', () => ({
|
|
13
|
+
default: {
|
|
14
|
+
start: vi.fn(),
|
|
15
|
+
data: vi.fn(),
|
|
16
|
+
initTree: vi.fn(),
|
|
17
|
+
},
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
vi.mock('../api.js', () => ({
|
|
21
|
+
fetchRouteCommentsSummary: vi.fn(),
|
|
22
|
+
fetchCommentDetail: vi.fn(),
|
|
23
|
+
moveComment: vi.fn(),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
vi.mock('../commentCache.js', () => ({
|
|
27
|
+
getCachedComments: vi.fn(() => null),
|
|
28
|
+
setCachedComments: vi.fn(),
|
|
29
|
+
clearCachedComments: vi.fn(),
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
vi.mock('./composer.js', () => ({
|
|
33
|
+
showComposer: vi.fn(),
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
vi.mock('./authModal.js', () => ({
|
|
37
|
+
openAuthModal: vi.fn(),
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
vi.mock('./commentWindow.js', () => ({
|
|
41
|
+
showCommentWindow: vi.fn(),
|
|
42
|
+
closeCommentWindow: vi.fn(),
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
describe('mount.js', () => {
|
|
46
|
+
// mountComments() is idempotent via a module-level _mounted flag, so we
|
|
47
|
+
// must re-import the module fresh for each test to reset that flag.
|
|
48
|
+
// All sibling modules must also be re-imported so they share the same instances.
|
|
49
|
+
let mountComments
|
|
50
|
+
let setCommentMode
|
|
51
|
+
let isCommentModeActive // eslint-disable-line no-unused-vars
|
|
52
|
+
let initCommentsConfig
|
|
53
|
+
let setToken
|
|
54
|
+
let clearToken
|
|
55
|
+
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
// Reset DOM
|
|
58
|
+
document.body.innerHTML = ''
|
|
59
|
+
document.body.className = ''
|
|
60
|
+
document.body.style.cssText = ''
|
|
61
|
+
|
|
62
|
+
// Fresh import to reset _mounted flag and all module-level state
|
|
63
|
+
vi.resetModules()
|
|
64
|
+
|
|
65
|
+
// Re-mock after resetModules
|
|
66
|
+
vi.doMock('alpinejs', () => ({
|
|
67
|
+
default: {
|
|
68
|
+
start: vi.fn(),
|
|
69
|
+
data: vi.fn(),
|
|
70
|
+
initTree: vi.fn(),
|
|
71
|
+
},
|
|
72
|
+
}))
|
|
73
|
+
vi.doMock('../api.js', () => ({
|
|
74
|
+
fetchRouteCommentsSummary: vi.fn(),
|
|
75
|
+
fetchCommentDetail: vi.fn(),
|
|
76
|
+
moveComment: vi.fn(),
|
|
77
|
+
}))
|
|
78
|
+
vi.doMock('../commentCache.js', () => ({
|
|
79
|
+
getCachedComments: vi.fn(() => null),
|
|
80
|
+
setCachedComments: vi.fn(),
|
|
81
|
+
clearCachedComments: vi.fn(),
|
|
82
|
+
}))
|
|
83
|
+
vi.doMock('./composer.js', () => ({ showComposer: vi.fn() }))
|
|
84
|
+
vi.doMock('./authModal.js', () => ({ openAuthModal: vi.fn() }))
|
|
85
|
+
vi.doMock('./commentWindow.js', () => ({
|
|
86
|
+
showCommentWindow: vi.fn(),
|
|
87
|
+
closeCommentWindow: vi.fn(),
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
// Import everything fresh so mount.js and its deps share the same instances
|
|
91
|
+
const mountMod = await import('./mount.js')
|
|
92
|
+
const commentModeMod = await import('../commentMode.js')
|
|
93
|
+
const configMod = await import('../config.js')
|
|
94
|
+
const authMod = await import('../auth.js')
|
|
95
|
+
|
|
96
|
+
mountComments = mountMod.mountComments
|
|
97
|
+
setCommentMode = commentModeMod.setCommentMode
|
|
98
|
+
isCommentModeActive = commentModeMod.isCommentModeActive
|
|
99
|
+
initCommentsConfig = configMod.initCommentsConfig
|
|
100
|
+
setToken = authMod.setToken
|
|
101
|
+
clearToken = authMod.clearToken
|
|
102
|
+
|
|
103
|
+
// Reset storyboard state
|
|
104
|
+
setCommentMode(false)
|
|
105
|
+
clearToken()
|
|
106
|
+
initCommentsConfig(null)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
vi.restoreAllMocks()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('ensureOverlay (via setBodyCommentMode)', () => {
|
|
114
|
+
it('does not set position:relative on document.body', () => {
|
|
115
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
116
|
+
setToken('ghp_test')
|
|
117
|
+
mountComments()
|
|
118
|
+
|
|
119
|
+
// Activate comment mode — triggers ensureOverlay internally
|
|
120
|
+
setCommentMode(true)
|
|
121
|
+
|
|
122
|
+
// body should NOT get position:relative forced on it
|
|
123
|
+
expect(document.body.style.position).not.toBe('relative')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('appends overlay to document.body when comment mode activates', () => {
|
|
127
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
128
|
+
setToken('ghp_test')
|
|
129
|
+
mountComments()
|
|
130
|
+
|
|
131
|
+
setCommentMode(true)
|
|
132
|
+
|
|
133
|
+
const overlay = document.body.querySelector('.sb-comment-overlay')
|
|
134
|
+
expect(overlay).not.toBeNull()
|
|
135
|
+
expect(overlay.parentElement).toBe(document.body)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('removes overlay when comment mode deactivates', () => {
|
|
139
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
140
|
+
setToken('ghp_test')
|
|
141
|
+
mountComments()
|
|
142
|
+
|
|
143
|
+
setCommentMode(true)
|
|
144
|
+
expect(document.body.querySelector('.sb-comment-overlay')).not.toBeNull()
|
|
145
|
+
|
|
146
|
+
setCommentMode(false)
|
|
147
|
+
expect(document.body.querySelector('.sb-comment-overlay')).toBeNull()
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('banner', () => {
|
|
152
|
+
it('shows banner when comment mode activates', () => {
|
|
153
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
154
|
+
setToken('ghp_test')
|
|
155
|
+
mountComments()
|
|
156
|
+
|
|
157
|
+
setCommentMode(true)
|
|
158
|
+
|
|
159
|
+
const banner = document.body.querySelector('.sb-banner')
|
|
160
|
+
expect(banner).not.toBeNull()
|
|
161
|
+
expect(banner.textContent).toContain('Comment mode')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('removes banner when comment mode deactivates', () => {
|
|
165
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
166
|
+
setToken('ghp_test')
|
|
167
|
+
mountComments()
|
|
168
|
+
|
|
169
|
+
setCommentMode(true)
|
|
170
|
+
setCommentMode(false)
|
|
171
|
+
|
|
172
|
+
expect(document.body.querySelector('.sb-banner')).toBeNull()
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('body class', () => {
|
|
177
|
+
it('adds sb-comment-mode class when comment mode activates', () => {
|
|
178
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
179
|
+
setToken('ghp_test')
|
|
180
|
+
mountComments()
|
|
181
|
+
|
|
182
|
+
setCommentMode(true)
|
|
183
|
+
expect(document.body.classList.contains('sb-comment-mode')).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('removes sb-comment-mode class when comment mode deactivates', () => {
|
|
187
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
188
|
+
setToken('ghp_test')
|
|
189
|
+
mountComments()
|
|
190
|
+
|
|
191
|
+
setCommentMode(true)
|
|
192
|
+
setCommentMode(false)
|
|
193
|
+
expect(document.body.classList.contains('sb-comment-mode')).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('mountComments idempotency', () => {
|
|
198
|
+
it('is safe to call multiple times', () => {
|
|
199
|
+
mountComments()
|
|
200
|
+
mountComments()
|
|
201
|
+
mountComments()
|
|
202
|
+
// No error thrown — _mounted guard prevents double init
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
})
|
package/src/devtools.js
CHANGED
|
@@ -187,7 +187,7 @@ export function mountDevTools(options = {}) {
|
|
|
187
187
|
|
|
188
188
|
let visible = true
|
|
189
189
|
let menuOpen = false
|
|
190
|
-
let panelOpen = false
|
|
190
|
+
let panelOpen = false // eslint-disable-line no-unused-vars
|
|
191
191
|
|
|
192
192
|
// Build DOM
|
|
193
193
|
const wrapper = document.createElement('div')
|
package/src/hideMode.test.js
CHANGED
|
@@ -7,9 +7,6 @@ import {
|
|
|
7
7
|
redo,
|
|
8
8
|
getOverrideHistory,
|
|
9
9
|
getCurrentIndex,
|
|
10
|
-
getNextIndex,
|
|
11
|
-
getCurrentSnapshot,
|
|
12
|
-
getCurrentRoute,
|
|
13
10
|
canUndo,
|
|
14
11
|
canRedo,
|
|
15
12
|
getShadow,
|
|
@@ -17,7 +14,6 @@ import {
|
|
|
17
14
|
removeShadow,
|
|
18
15
|
getAllShadows,
|
|
19
16
|
syncHashToHistory,
|
|
20
|
-
installHistorySync,
|
|
21
17
|
} from './hideMode.js'
|
|
22
18
|
|
|
23
19
|
// ── Hide Mode Toggle ──
|