Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Jérome Perrin
gitlab-ce
Commits
7abe27b4
Commit
7abe27b4
authored
Jun 05, 2017
by
Kushal Pandya
Committed by
Phil Hughes
Jun 05, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Improve user experience around slash commands in instant comments
parent
f7110642
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
232 additions
and
56 deletions
+232
-56
app/assets/javascripts/gfm_auto_complete.js
app/assets/javascripts/gfm_auto_complete.js
+12
-3
app/assets/javascripts/lib/utils/ajax_cache.js
app/assets/javascripts/lib/utils/ajax_cache.js
+2
-2
app/assets/javascripts/notes.js
app/assets/javascripts/notes.js
+87
-14
changelogs/unreleased/27614-improve-instant-comments-exp.yml
changelogs/unreleased/27614-improve-instant-comments-exp.yml
+4
-0
spec/javascripts/lib/utils/ajax_cache_spec.js
spec/javascripts/lib/utils/ajax_cache_spec.js
+31
-0
spec/javascripts/notes_spec.js
spec/javascripts/notes_spec.js
+96
-37
No files found.
app/assets/javascripts/gfm_auto_complete.js
View file @
7abe27b4
...
@@ -2,6 +2,7 @@ import emojiMap from 'emojis/digests.json';
...
@@ -2,6 +2,7 @@ import emojiMap from 'emojis/digests.json';
import
emojiAliases
from
'
emojis/aliases.json
'
;
import
emojiAliases
from
'
emojis/aliases.json
'
;
import
{
glEmojiTag
}
from
'
~/behaviors/gl_emoji
'
;
import
{
glEmojiTag
}
from
'
~/behaviors/gl_emoji
'
;
import
glRegexp
from
'
~/lib/utils/regexp
'
;
import
glRegexp
from
'
~/lib/utils/regexp
'
;
import
AjaxCache
from
'
~/lib/utils/ajax_cache
'
;
function
sanitize
(
str
)
{
function
sanitize
(
str
)
{
return
str
.
replace
(
/<
(?:
.|
\n)
*
?
>/gm
,
''
);
return
str
.
replace
(
/<
(?:
.|
\n)
*
?
>/gm
,
''
);
...
@@ -35,6 +36,7 @@ class GfmAutoComplete {
...
@@ -35,6 +36,7 @@ class GfmAutoComplete {
// This triggers at.js again
// This triggers at.js again
// Needed for slash commands with suffixes (ex: /label ~)
// Needed for slash commands with suffixes (ex: /label ~)
$input
.
on
(
'
inserted-commands.atwho
'
,
$input
.
trigger
.
bind
(
$input
,
'
keyup
'
));
$input
.
on
(
'
inserted-commands.atwho
'
,
$input
.
trigger
.
bind
(
$input
,
'
keyup
'
));
$input
.
on
(
'
clear-commands-cache.atwho
'
,
()
=>
this
.
clearCache
());
});
});
}
}
...
@@ -375,11 +377,14 @@ class GfmAutoComplete {
...
@@ -375,11 +377,14 @@ class GfmAutoComplete {
}
else
if
(
GfmAutoComplete
.
atTypeMap
[
at
]
===
'
emojis
'
)
{
}
else
if
(
GfmAutoComplete
.
atTypeMap
[
at
]
===
'
emojis
'
)
{
this
.
loadData
(
$input
,
at
,
Object
.
keys
(
emojiMap
).
concat
(
Object
.
keys
(
emojiAliases
)));
this
.
loadData
(
$input
,
at
,
Object
.
keys
(
emojiMap
).
concat
(
Object
.
keys
(
emojiAliases
)));
}
else
{
}
else
{
$
.
getJSON
(
this
.
dataSources
[
GfmAutoComplete
.
atTypeMap
[
at
]],
(
data
)
=>
{
AjaxCache
.
retrieve
(
this
.
dataSources
[
GfmAutoComplete
.
atTypeMap
[
at
]],
true
)
.
then
((
data
)
=>
{
this
.
loadData
(
$input
,
at
,
data
);
this
.
loadData
(
$input
,
at
,
data
);
}).
fail
(()
=>
{
this
.
isLoadingData
[
at
]
=
false
;
});
})
.
catch
(()
=>
{
this
.
isLoadingData
[
at
]
=
false
;
});
}
}
}
}
loadData
(
$input
,
at
,
data
)
{
loadData
(
$input
,
at
,
data
)
{
this
.
isLoadingData
[
at
]
=
false
;
this
.
isLoadingData
[
at
]
=
false
;
this
.
cachedData
[
at
]
=
data
;
this
.
cachedData
[
at
]
=
data
;
...
@@ -389,6 +394,10 @@ class GfmAutoComplete {
...
@@ -389,6 +394,10 @@ class GfmAutoComplete {
return
$input
.
trigger
(
'
keyup
'
);
return
$input
.
trigger
(
'
keyup
'
);
}
}
clearCache
()
{
this
.
cachedData
=
{};
}
static
isLoading
(
data
)
{
static
isLoading
(
data
)
{
let
dataToInspect
=
data
;
let
dataToInspect
=
data
;
if
(
data
&&
data
.
length
>
0
)
{
if
(
data
&&
data
.
length
>
0
)
{
...
...
app/assets/javascripts/lib/utils/ajax_cache.js
View file @
7abe27b4
...
@@ -6,8 +6,8 @@ class AjaxCache extends Cache {
...
@@ -6,8 +6,8 @@ class AjaxCache extends Cache {
this
.
pendingRequests
=
{
};
this
.
pendingRequests
=
{
};
}
}
retrieve
(
endpoint
)
{
retrieve
(
endpoint
,
forceRetrieve
)
{
if
(
this
.
hasData
(
endpoint
))
{
if
(
this
.
hasData
(
endpoint
)
&&
!
forceRetrieve
)
{
return
Promise
.
resolve
(
this
.
get
(
endpoint
));
return
Promise
.
resolve
(
this
.
get
(
endpoint
));
}
}
...
...
app/assets/javascripts/notes.js
View file @
7abe27b4
...
@@ -16,6 +16,7 @@ import autosize from 'vendor/autosize';
...
@@ -16,6 +16,7 @@ import autosize from 'vendor/autosize';
import
Dropzone
from
'
dropzone
'
;
import
Dropzone
from
'
dropzone
'
;
import
'
vendor/jquery.caret
'
;
// required by jquery.atwho
import
'
vendor/jquery.caret
'
;
// required by jquery.atwho
import
'
vendor/jquery.atwho
'
;
import
'
vendor/jquery.atwho
'
;
import
AjaxCache
from
'
~/lib/utils/ajax_cache
'
;
import
CommentTypeToggle
from
'
./comment_type_toggle
'
;
import
CommentTypeToggle
from
'
./comment_type_toggle
'
;
import
'
./autosave
'
;
import
'
./autosave
'
;
import
'
./dropzone_input
'
;
import
'
./dropzone_input
'
;
...
@@ -66,7 +67,6 @@ const normalizeNewlines = function(str) {
...
@@ -66,7 +67,6 @@ const normalizeNewlines = function(str) {
this
.
notesCountBadge
||
(
this
.
notesCountBadge
=
$
(
'
.issuable-details
'
).
find
(
'
.notes-tab .badge
'
));
this
.
notesCountBadge
||
(
this
.
notesCountBadge
=
$
(
'
.issuable-details
'
).
find
(
'
.notes-tab .badge
'
));
this
.
basePollingInterval
=
15000
;
this
.
basePollingInterval
=
15000
;
this
.
maxPollingSteps
=
4
;
this
.
maxPollingSteps
=
4
;
this
.
flashErrors
=
[];
this
.
cleanBinding
();
this
.
cleanBinding
();
this
.
addBinding
();
this
.
addBinding
();
...
@@ -325,6 +325,9 @@ const normalizeNewlines = function(str) {
...
@@ -325,6 +325,9 @@ const normalizeNewlines = function(str) {
if
(
Notes
.
isNewNote
(
noteEntity
,
this
.
note_ids
))
{
if
(
Notes
.
isNewNote
(
noteEntity
,
this
.
note_ids
))
{
this
.
note_ids
.
push
(
noteEntity
.
id
);
this
.
note_ids
.
push
(
noteEntity
.
id
);
if
(
$notesList
.
length
)
{
$notesList
.
find
(
'
.system-note.being-posted
'
).
remove
();
}
const
$newNote
=
Notes
.
animateAppendNote
(
noteEntity
.
html
,
$notesList
);
const
$newNote
=
Notes
.
animateAppendNote
(
noteEntity
.
html
,
$notesList
);
this
.
setupNewNote
(
$newNote
);
this
.
setupNewNote
(
$newNote
);
...
@@ -1118,12 +1121,14 @@ const normalizeNewlines = function(str) {
...
@@ -1118,12 +1121,14 @@ const normalizeNewlines = function(str) {
};
};
Notes
.
prototype
.
addFlash
=
function
(...
flashParams
)
{
Notes
.
prototype
.
addFlash
=
function
(...
flashParams
)
{
this
.
flash
Errors
.
push
(
new
Flash
(...
flashParams
)
);
this
.
flash
Instance
=
new
Flash
(...
flashParams
);
};
};
Notes
.
prototype
.
clearFlash
=
function
()
{
Notes
.
prototype
.
clearFlash
=
function
()
{
this
.
flashErrors
.
forEach
(
flash
=>
flash
.
flashContainer
.
remove
());
if
(
this
.
flashInstance
&&
this
.
flashInstance
.
flashContainer
)
{
this
.
flashErrors
=
[];
this
.
flashInstance
.
flashContainer
.
hide
();
this
.
flashInstance
=
null
;
}
};
};
Notes
.
prototype
.
cleanForm
=
function
(
$form
)
{
Notes
.
prototype
.
cleanForm
=
function
(
$form
)
{
...
@@ -1187,7 +1192,7 @@ const normalizeNewlines = function(str) {
...
@@ -1187,7 +1192,7 @@ const normalizeNewlines = function(str) {
Notes
.
prototype
.
getFormData
=
function
(
$form
)
{
Notes
.
prototype
.
getFormData
=
function
(
$form
)
{
return
{
return
{
formData
:
$form
.
serialize
(),
formData
:
$form
.
serialize
(),
formContent
:
$form
.
find
(
'
.js-note-text
'
).
val
(
),
formContent
:
_
.
escape
(
$form
.
find
(
'
.js-note-text
'
).
val
()
),
formAction
:
$form
.
attr
(
'
action
'
),
formAction
:
$form
.
attr
(
'
action
'
),
};
};
};
};
...
@@ -1206,20 +1211,47 @@ const normalizeNewlines = function(str) {
...
@@ -1206,20 +1211,47 @@ const normalizeNewlines = function(str) {
return
formContent
.
replace
(
REGEX_SLASH_COMMANDS
,
''
).
trim
();
return
formContent
.
replace
(
REGEX_SLASH_COMMANDS
,
''
).
trim
();
};
};
/**
* Gets appropriate description from slash commands found in provided `formContent`
*/
Notes
.
prototype
.
getSlashCommandDescription
=
function
(
formContent
,
availableSlashCommands
=
[])
{
let
tempFormContent
;
// Identify executed slash commands from `formContent`
const
executedCommands
=
availableSlashCommands
.
filter
((
command
,
index
)
=>
{
const
commandRegex
=
new
RegExp
(
`/
${
command
.
name
}
`
);
return
commandRegex
.
test
(
formContent
);
});
if
(
executedCommands
&&
executedCommands
.
length
)
{
if
(
executedCommands
.
length
>
1
)
{
tempFormContent
=
'
Applying multiple commands
'
;
}
else
{
const
commandDescription
=
executedCommands
[
0
].
description
.
toLowerCase
();
tempFormContent
=
`Applying command to
${
commandDescription
}
`
;
}
}
else
{
tempFormContent
=
'
Applying command
'
;
}
return
tempFormContent
;
};
/**
/**
* Create placeholder note DOM element populated with comment body
* Create placeholder note DOM element populated with comment body
* that we will show while comment is being posted.
* that we will show while comment is being posted.
* Once comment is _actually_ posted on server, we will have final element
* Once comment is _actually_ posted on server, we will have final element
* in response that we will show in place of this temporary element.
* in response that we will show in place of this temporary element.
*/
*/
Notes
.
prototype
.
createPlaceholderNote
=
function
({
formContent
,
uniqueId
,
isDiscussionNote
,
currentUsername
,
currentUserFullname
})
{
Notes
.
prototype
.
createPlaceholderNote
=
function
({
formContent
,
uniqueId
,
isDiscussionNote
,
currentUsername
,
currentUserFullname
,
currentUserAvatar
})
{
const
discussionClass
=
isDiscussionNote
?
'
discussion
'
:
''
;
const
discussionClass
=
isDiscussionNote
?
'
discussion
'
:
''
;
const
escapedFormContent
=
_
.
escape
(
formContent
);
const
$tempNote
=
$
(
const
$tempNote
=
$
(
`<li id="
${
uniqueId
}
" class="note being-posted fade-in-half timeline-entry">
`<li id="
${
uniqueId
}
" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<div class="timeline-icon">
<a href="/
${
currentUsername
}
"><span class="avatar dummy-avatar"></span></a>
<a href="/
${
currentUsername
}
">
<img class="avatar s40" src="
${
currentUserAvatar
}
">
</a>
</div>
</div>
<div class="timeline-content
${
discussionClass
}
">
<div class="timeline-content
${
discussionClass
}
">
<div class="note-header">
<div class="note-header">
...
@@ -1232,7 +1264,7 @@ const normalizeNewlines = function(str) {
...
@@ -1232,7 +1264,7 @@ const normalizeNewlines = function(str) {
</div>
</div>
<div class="note-body">
<div class="note-body">
<div class="note-text">
<div class="note-text">
<p>
${
escapedF
ormContent
}
</p>
<p>
${
f
ormContent
}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -1243,6 +1275,23 @@ const normalizeNewlines = function(str) {
...
@@ -1243,6 +1275,23 @@ const normalizeNewlines = function(str) {
return
$tempNote
;
return
$tempNote
;
};
};
/**
* Create Placeholder System Note DOM element populated with slash command description
*/
Notes
.
prototype
.
createPlaceholderSystemNote
=
function
({
formContent
,
uniqueId
})
{
const
$tempNote
=
$
(
`<li id="
${
uniqueId
}
" class="note system-note timeline-entry being-posted fade-in-half">
<div class="timeline-entry-inner">
<div class="timeline-content">
<i>
${
formContent
}
</i>
</div>
</div>
</li>`
);
return
$tempNote
;
};
/**
/**
* This method does following tasks step-by-step whenever a new comment
* This method does following tasks step-by-step whenever a new comment
* is submitted by user (both main thread comments as well as discussion comments).
* is submitted by user (both main thread comments as well as discussion comments).
...
@@ -1274,7 +1323,9 @@ const normalizeNewlines = function(str) {
...
@@ -1274,7 +1323,9 @@ const normalizeNewlines = function(str) {
const
isDiscussionForm
=
$form
.
hasClass
(
'
js-discussion-note-form
'
);
const
isDiscussionForm
=
$form
.
hasClass
(
'
js-discussion-note-form
'
);
const
isDiscussionResolve
=
$submitBtn
.
hasClass
(
'
js-comment-resolve-button
'
);
const
isDiscussionResolve
=
$submitBtn
.
hasClass
(
'
js-comment-resolve-button
'
);
const
{
formData
,
formContent
,
formAction
}
=
this
.
getFormData
(
$form
);
const
{
formData
,
formContent
,
formAction
}
=
this
.
getFormData
(
$form
);
const
uniqueId
=
_
.
uniqueId
(
'
tempNote_
'
);
let
noteUniqueId
;
let
systemNoteUniqueId
;
let
hasSlashCommands
=
false
;
let
$notesContainer
;
let
$notesContainer
;
let
tempFormContent
;
let
tempFormContent
;
...
@@ -1295,16 +1346,28 @@ const normalizeNewlines = function(str) {
...
@@ -1295,16 +1346,28 @@ const normalizeNewlines = function(str) {
tempFormContent
=
formContent
;
tempFormContent
=
formContent
;
if
(
this
.
hasSlashCommands
(
formContent
))
{
if
(
this
.
hasSlashCommands
(
formContent
))
{
tempFormContent
=
this
.
stripSlashCommands
(
formContent
);
tempFormContent
=
this
.
stripSlashCommands
(
formContent
);
hasSlashCommands
=
true
;
}
}
if
(
tempFormContent
)
{
// Show placeholder note
// Show placeholder note
if
(
tempFormContent
)
{
noteUniqueId
=
_
.
uniqueId
(
'
tempNote_
'
);
$notesContainer
.
append
(
this
.
createPlaceholderNote
({
$notesContainer
.
append
(
this
.
createPlaceholderNote
({
formContent
:
tempFormContent
,
formContent
:
tempFormContent
,
uniqueId
,
uniqueId
:
noteUniqueId
,
isDiscussionNote
,
isDiscussionNote
,
currentUsername
:
gon
.
current_username
,
currentUsername
:
gon
.
current_username
,
currentUserFullname
:
gon
.
current_user_fullname
,
currentUserFullname
:
gon
.
current_user_fullname
,
currentUserAvatar
:
gon
.
current_user_avatar_url
,
}));
}
// Show placeholder system note
if
(
hasSlashCommands
)
{
systemNoteUniqueId
=
_
.
uniqueId
(
'
tempSystemNote_
'
);
$notesContainer
.
append
(
this
.
createPlaceholderSystemNote
({
formContent
:
this
.
getSlashCommandDescription
(
formContent
,
AjaxCache
.
get
(
gl
.
GfmAutoComplete
.
dataSources
.
commands
)),
uniqueId
:
systemNoteUniqueId
,
}));
}));
}
}
...
@@ -1322,7 +1385,13 @@ const normalizeNewlines = function(str) {
...
@@ -1322,7 +1385,13 @@ const normalizeNewlines = function(str) {
gl
.
utils
.
ajaxPost
(
formAction
,
formData
)
gl
.
utils
.
ajaxPost
(
formAction
,
formData
)
.
then
((
note
)
=>
{
.
then
((
note
)
=>
{
// Submission successful! remove placeholder
// Submission successful! remove placeholder
$notesContainer
.
find
(
`#
${
uniqueId
}
`
).
remove
();
$notesContainer
.
find
(
`#
${
noteUniqueId
}
`
).
remove
();
// Reset cached commands list when command is applied
if
(
hasSlashCommands
)
{
$form
.
find
(
'
textarea.js-note-text
'
).
trigger
(
'
clear-commands-cache.atwho
'
);
}
// Clear previous form errors
// Clear previous form errors
this
.
clearFlashWrapper
();
this
.
clearFlashWrapper
();
...
@@ -1359,7 +1428,11 @@ const normalizeNewlines = function(str) {
...
@@ -1359,7 +1428,11 @@ const normalizeNewlines = function(str) {
$form
.
trigger
(
'
ajax:success
'
,
[
note
]);
$form
.
trigger
(
'
ajax:success
'
,
[
note
]);
}).
fail
(()
=>
{
}).
fail
(()
=>
{
// Submission failed, remove placeholder note and show Flash error message
// Submission failed, remove placeholder note and show Flash error message
$notesContainer
.
find
(
`#
${
uniqueId
}
`
).
remove
();
$notesContainer
.
find
(
`#
${
noteUniqueId
}
`
).
remove
();
if
(
hasSlashCommands
)
{
$notesContainer
.
find
(
`#
${
systemNoteUniqueId
}
`
).
remove
();
}
// Show form again on UI on failure
// Show form again on UI on failure
if
(
isDiscussionForm
&&
$notesContainer
.
length
)
{
if
(
isDiscussionForm
&&
$notesContainer
.
length
)
{
...
...
changelogs/unreleased/27614-improve-instant-comments-exp.yml
0 → 100644
View file @
7abe27b4
---
title
:
Improve user experience around slash commands in instant comments
merge_request
:
11612
author
:
spec/javascripts/lib/utils/ajax_cache_spec.js
View file @
7abe27b4
...
@@ -154,5 +154,36 @@ describe('AjaxCache', () => {
...
@@ -154,5 +154,36 @@ describe('AjaxCache', () => {
.
then
(
done
)
.
then
(
done
)
.
catch
(
fail
);
.
catch
(
fail
);
});
});
it
(
'
makes Ajax call even if matching data exists when forceRequest parameter is provided
'
,
(
done
)
=>
{
const
oldDummyResponse
=
{
important
:
'
old dummy data
'
,
};
AjaxCache
.
internalStorage
[
dummyEndpoint
]
=
oldDummyResponse
;
ajaxSpy
=
(
url
)
=>
{
expect
(
url
).
toBe
(
dummyEndpoint
);
const
deferred
=
$
.
Deferred
();
deferred
.
resolve
(
dummyResponse
);
return
deferred
.
promise
();
};
// Call without forceRetrieve param
AjaxCache
.
retrieve
(
dummyEndpoint
)
.
then
((
data
)
=>
{
expect
(
data
).
toBe
(
oldDummyResponse
);
})
.
then
(
done
)
.
catch
(
fail
);
// Call with forceRetrieve param
AjaxCache
.
retrieve
(
dummyEndpoint
,
true
)
.
then
((
data
)
=>
{
expect
(
data
).
toBe
(
dummyResponse
);
})
.
then
(
done
)
.
catch
(
fail
);
});
});
});
});
});
spec/javascripts/notes_spec.js
View file @
7abe27b4
...
@@ -13,6 +13,23 @@ import '~/notes';
...
@@ -13,6 +13,23 @@ import '~/notes';
window
.
gl
=
window
.
gl
||
{};
window
.
gl
=
window
.
gl
||
{};
gl
.
utils
=
gl
.
utils
||
{};
gl
.
utils
=
gl
.
utils
||
{};
const
htmlEscape
=
(
comment
)
=>
{
const
escapedString
=
comment
.
replace
(
/
[
"&'<>
]
/g
,
(
a
)
=>
{
const
escapedToken
=
{
'
&
'
:
'
&
'
,
'
<
'
:
'
<
'
,
'
>
'
:
'
>
'
,
'
"
'
:
'
"
'
,
"
'
"
:
'
'
'
,
'
`
'
:
'
`
'
}[
a
];
return
escapedToken
;
});
return
escapedString
;
};
describe
(
'
Notes
'
,
function
()
{
describe
(
'
Notes
'
,
function
()
{
const
FLASH_TYPE_ALERT
=
'
alert
'
;
const
FLASH_TYPE_ALERT
=
'
alert
'
;
var
commentsTemplate
=
'
issues/issue_with_comment.html.raw
'
;
var
commentsTemplate
=
'
issues/issue_with_comment.html.raw
'
;
...
@@ -445,11 +462,17 @@ import '~/notes';
...
@@ -445,11 +462,17 @@ import '~/notes';
});
});
describe
(
'
getFormData
'
,
()
=>
{
describe
(
'
getFormData
'
,
()
=>
{
it
(
'
should return form metadata object from form reference
'
,
()
=>
{
let
$form
;
let
sampleComment
;
beforeEach
(()
=>
{
this
.
notes
=
new
Notes
(
''
,
[]);
this
.
notes
=
new
Notes
(
''
,
[]);
const
$form
=
$
(
'
form
'
);
$form
=
$
(
'
form
'
);
const
sampleComment
=
'
foobar
'
;
sampleComment
=
'
foobar
'
;
});
it
(
'
should return form metadata object from form reference
'
,
()
=>
{
$form
.
find
(
'
textarea.js-note-text
'
).
val
(
sampleComment
);
$form
.
find
(
'
textarea.js-note-text
'
).
val
(
sampleComment
);
const
{
formData
,
formContent
,
formAction
}
=
this
.
notes
.
getFormData
(
$form
);
const
{
formData
,
formContent
,
formAction
}
=
this
.
notes
.
getFormData
(
$form
);
...
@@ -457,6 +480,18 @@ import '~/notes';
...
@@ -457,6 +480,18 @@ import '~/notes';
expect
(
formContent
).
toEqual
(
sampleComment
);
expect
(
formContent
).
toEqual
(
sampleComment
);
expect
(
formAction
).
toEqual
(
$form
.
attr
(
'
action
'
));
expect
(
formAction
).
toEqual
(
$form
.
attr
(
'
action
'
));
});
});
it
(
'
should return form metadata with sanitized formContent from form reference
'
,
()
=>
{
spyOn
(
_
,
'
escape
'
).
and
.
callFake
(
htmlEscape
);
sampleComment
=
'
<script>alert("Boom!");</script>
'
;
$form
.
find
(
'
textarea.js-note-text
'
).
val
(
sampleComment
);
const
{
formContent
}
=
this
.
notes
.
getFormData
(
$form
);
expect
(
_
.
escape
).
toHaveBeenCalledWith
(
sampleComment
);
expect
(
formContent
).
toEqual
(
'
<script>alert("Boom!");</script>
'
);
});
});
});
describe
(
'
hasSlashCommands
'
,
()
=>
{
describe
(
'
hasSlashCommands
'
,
()
=>
{
...
@@ -512,30 +547,42 @@ import '~/notes';
...
@@ -512,30 +547,42 @@ import '~/notes';
});
});
});
});
describe
(
'
getSlashCommandDescription
'
,
()
=>
{
const
availableSlashCommands
=
[
{
name
:
'
close
'
,
description
:
'
Close this issue
'
,
params
:
[]
},
{
name
:
'
title
'
,
description
:
'
Change title
'
,
params
:
[{}]
},
{
name
:
'
estimate
'
,
description
:
'
Set time estimate
'
,
params
:
[{}]
}
];
beforeEach
(()
=>
{
this
.
notes
=
new
Notes
();
});
it
(
'
should return executing slash command description when note has single slash command
'
,
()
=>
{
const
sampleComment
=
'
/close
'
;
expect
(
this
.
notes
.
getSlashCommandDescription
(
sampleComment
,
availableSlashCommands
)).
toBe
(
'
Applying command to close this issue
'
);
});
it
(
'
should return generic multiple slash command description when note has multiple slash commands
'
,
()
=>
{
const
sampleComment
=
'
/close
\n
/title [Duplicate] Issue foobar
'
;
expect
(
this
.
notes
.
getSlashCommandDescription
(
sampleComment
,
availableSlashCommands
)).
toBe
(
'
Applying multiple commands
'
);
});
it
(
'
should return generic slash command description when available slash commands list is not populated
'
,
()
=>
{
const
sampleComment
=
'
/close
\n
/title [Duplicate] Issue foobar
'
;
expect
(
this
.
notes
.
getSlashCommandDescription
(
sampleComment
)).
toBe
(
'
Applying command
'
);
});
});
describe
(
'
createPlaceholderNote
'
,
()
=>
{
describe
(
'
createPlaceholderNote
'
,
()
=>
{
const
sampleComment
=
'
foobar
'
;
const
sampleComment
=
'
foobar
'
;
const
uniqueId
=
'
b1234-a4567
'
;
const
uniqueId
=
'
b1234-a4567
'
;
const
currentUsername
=
'
root
'
;
const
currentUsername
=
'
root
'
;
const
currentUserFullname
=
'
Administrator
'
;
const
currentUserFullname
=
'
Administrator
'
;
const
currentUserAvatar
=
'
avatar_url
'
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
this
.
notes
=
new
Notes
(
''
,
[]);
this
.
notes
=
new
Notes
(
''
,
[]);
spyOn
(
_
,
'
escape
'
).
and
.
callFake
((
comment
)
=>
{
const
escapedString
=
comment
.
replace
(
/
[
"&'<>
]
/g
,
(
a
)
=>
{
const
escapedToken
=
{
'
&
'
:
'
&
'
,
'
<
'
:
'
<
'
,
'
>
'
:
'
>
'
,
'
"
'
:
'
"
'
,
"
'
"
:
'
'
'
,
'
`
'
:
'
`
'
}[
a
];
return
escapedToken
;
});
return
escapedString
;
});
});
});
it
(
'
should return constructed placeholder element for regular note based on form contents
'
,
()
=>
{
it
(
'
should return constructed placeholder element for regular note based on form contents
'
,
()
=>
{
...
@@ -544,46 +591,59 @@ import '~/notes';
...
@@ -544,46 +591,59 @@ import '~/notes';
uniqueId
,
uniqueId
,
isDiscussionNote
:
false
,
isDiscussionNote
:
false
,
currentUsername
,
currentUsername
,
currentUserFullname
currentUserFullname
,
currentUserAvatar
,
});
});
const
$tempNoteHeader
=
$tempNote
.
find
(
'
.note-header
'
);
const
$tempNoteHeader
=
$tempNote
.
find
(
'
.note-header
'
);
expect
(
$tempNote
.
prop
(
'
nodeName
'
)).
toEqual
(
'
LI
'
);
expect
(
$tempNote
.
prop
(
'
nodeName
'
)).
toEqual
(
'
LI
'
);
expect
(
$tempNote
.
attr
(
'
id
'
)).
toEqual
(
uniqueId
);
expect
(
$tempNote
.
attr
(
'
id
'
)).
toEqual
(
uniqueId
);
expect
(
$tempNote
.
hasClass
(
'
being-posted
'
)).
toBeTruthy
();
expect
(
$tempNote
.
hasClass
(
'
fade-in-half
'
)).
toBeTruthy
();
$tempNote
.
find
(
'
.timeline-icon > a, .note-header-info > a
'
).
each
(
function
()
{
$tempNote
.
find
(
'
.timeline-icon > a, .note-header-info > a
'
).
each
(
function
()
{
expect
(
$
(
this
).
attr
(
'
href
'
)).
toEqual
(
`/
${
currentUsername
}
`
);
expect
(
$
(
this
).
attr
(
'
href
'
)).
toEqual
(
`/
${
currentUsername
}
`
);
});
});
expect
(
$tempNote
.
find
(
'
.timeline-icon .avatar
'
).
attr
(
'
src
'
)).
toEqual
(
currentUserAvatar
);
expect
(
$tempNote
.
find
(
'
.timeline-content
'
).
hasClass
(
'
discussion
'
)).
toBeFalsy
();
expect
(
$tempNote
.
find
(
'
.timeline-content
'
).
hasClass
(
'
discussion
'
)).
toBeFalsy
();
expect
(
$tempNoteHeader
.
find
(
'
.hidden-xs
'
).
text
().
trim
()).
toEqual
(
currentUserFullname
);
expect
(
$tempNoteHeader
.
find
(
'
.hidden-xs
'
).
text
().
trim
()).
toEqual
(
currentUserFullname
);
expect
(
$tempNoteHeader
.
find
(
'
.note-headline-light
'
).
text
().
trim
()).
toEqual
(
`@
${
currentUsername
}
`
);
expect
(
$tempNoteHeader
.
find
(
'
.note-headline-light
'
).
text
().
trim
()).
toEqual
(
`@
${
currentUsername
}
`
);
expect
(
$tempNote
.
find
(
'
.note-body .note-text p
'
).
text
().
trim
()).
toEqual
(
sampleComment
);
expect
(
$tempNote
.
find
(
'
.note-body .note-text p
'
).
text
().
trim
()).
toEqual
(
sampleComment
);
});
});
it
(
'
should escape HTML characters from note based on form contents
'
,
()
=>
{
it
(
'
should return constructed placeholder element for discussion note based on form contents
'
,
()
=>
{
const
commentWithHtml
=
'
<script>alert("Boom!");</script>
'
;
const
$tempNote
=
this
.
notes
.
createPlaceholderNote
({
const
$tempNote
=
this
.
notes
.
createPlaceholderNote
({
formContent
:
commentWithHtml
,
formContent
:
sampleComment
,
uniqueId
,
uniqueId
,
isDiscussionNote
:
fals
e
,
isDiscussionNote
:
tru
e
,
currentUsername
,
currentUsername
,
currentUserFullname
currentUserFullname
});
});
expect
(
_
.
escape
).
toHaveBeenCalledWith
(
commentWithHtml
);
expect
(
$tempNote
.
prop
(
'
nodeName
'
)).
toEqual
(
'
LI
'
);
expect
(
$tempNote
.
find
(
'
.note-body .note-text p
'
).
html
()).
toEqual
(
'
<script>alert("Boom!");</script>
'
);
expect
(
$tempNote
.
find
(
'
.timeline-content
'
).
hasClass
(
'
discussion
'
)).
toBeTruthy
();
});
});
});
it
(
'
should return constructed placeholder element for discussion note based on form contents
'
,
()
=>
{
describe
(
'
createPlaceholderSystemNote
'
,
()
=>
{
const
$tempNote
=
this
.
notes
.
createPlaceholderNote
({
const
sampleCommandDescription
=
'
Applying command to close this issue
'
;
formContent
:
sampleComment
,
const
uniqueId
=
'
b1234-a4567
'
;
beforeEach
(()
=>
{
this
.
notes
=
new
Notes
(
''
,
[]);
spyOn
(
_
,
'
escape
'
).
and
.
callFake
(
htmlEscape
);
});
it
(
'
should return constructed placeholder element for system note based on form contents
'
,
()
=>
{
const
$tempNote
=
this
.
notes
.
createPlaceholderSystemNote
({
formContent
:
sampleCommandDescription
,
uniqueId
,
uniqueId
,
isDiscussionNote
:
true
,
currentUsername
,
currentUserFullname
});
});
expect
(
$tempNote
.
prop
(
'
nodeName
'
)).
toEqual
(
'
LI
'
);
expect
(
$tempNote
.
prop
(
'
nodeName
'
)).
toEqual
(
'
LI
'
);
expect
(
$tempNote
.
find
(
'
.timeline-content
'
).
hasClass
(
'
discussion
'
)).
toBeTruthy
();
expect
(
$tempNote
.
attr
(
'
id
'
)).
toEqual
(
uniqueId
);
expect
(
$tempNote
.
hasClass
(
'
being-posted
'
)).
toBeTruthy
();
expect
(
$tempNote
.
hasClass
(
'
fade-in-half
'
)).
toBeTruthy
();
expect
(
$tempNote
.
find
(
'
.timeline-content i
'
).
text
().
trim
()).
toEqual
(
sampleCommandDescription
);
});
});
});
});
...
@@ -595,7 +655,7 @@ import '~/notes';
...
@@ -595,7 +655,7 @@ import '~/notes';
it
(
'
shows a flash message
'
,
()
=>
{
it
(
'
shows a flash message
'
,
()
=>
{
this
.
notes
.
addFlash
(
'
Error message
'
,
FLASH_TYPE_ALERT
,
this
.
notes
.
parentTimeline
);
this
.
notes
.
addFlash
(
'
Error message
'
,
FLASH_TYPE_ALERT
,
this
.
notes
.
parentTimeline
);
expect
(
document
.
querySelectorAll
(
'
.flash-alert
'
).
length
).
toBe
(
1
);
expect
(
$
(
'
.flash-alert
'
).
is
(
'
:visible
'
)).
toBeTruthy
(
);
});
});
});
});
...
@@ -605,13 +665,12 @@ import '~/notes';
...
@@ -605,13 +665,12 @@ import '~/notes';
this
.
notes
=
new
Notes
();
this
.
notes
=
new
Notes
();
});
});
it
(
'
removes all the associated flash messages
'
,
()
=>
{
it
(
'
hides visible flash message
'
,
()
=>
{
this
.
notes
.
addFlash
(
'
Error message 1
'
,
FLASH_TYPE_ALERT
,
this
.
notes
.
parentTimeline
);
this
.
notes
.
addFlash
(
'
Error message 1
'
,
FLASH_TYPE_ALERT
,
this
.
notes
.
parentTimeline
);
this
.
notes
.
addFlash
(
'
Error message 2
'
,
FLASH_TYPE_ALERT
,
this
.
notes
.
parentTimeline
);
this
.
notes
.
clearFlash
();
this
.
notes
.
clearFlash
();
expect
(
document
.
querySelectorAll
(
'
.flash-alert
'
).
length
).
toBe
(
0
);
expect
(
$
(
'
.flash-alert
'
).
is
(
'
:visible
'
)).
toBeFalsy
(
);
});
});
});
});
});
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment