feat: add hashtag autocomplete (#1567)
* feat: add hashtag autocomplete fixes #1209 * add test and tweak aria label and styles * do not prefer lowercase * Change text
This commit is contained in:
parent
138fe83082
commit
8b3842f15a
|
@ -54,5 +54,6 @@ module.exports = [
|
||||||
{ id: 'fa-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/plus.svg' },
|
{ id: 'fa-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/plus.svg' },
|
||||||
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' },
|
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' },
|
||||||
{ id: 'fa-crosshairs', src: 'src/thirdparty/font-awesome-svg-png/white/svg/crosshairs.svg' },
|
{ id: 'fa-crosshairs', src: 'src/thirdparty/font-awesome-svg-png/white/svg/crosshairs.svg' },
|
||||||
{ id: 'fa-magic', src: 'src/thirdparty/font-awesome-svg-png/white/svg/magic.svg' }
|
{ id: 'fa-magic', src: 'src/thirdparty/font-awesome-svg-png/white/svg/magic.svg' },
|
||||||
|
{ id: 'fa-hashtag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/hashtag.svg' }
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,49 +1,54 @@
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
|
|
||||||
export async function insertUsername (realm, username, startIndex, endIndex) {
|
const emojiMapper = emoji => `:${emoji.shortcode}:`
|
||||||
|
const hashtagMapper = hashtag => `#${hashtag.name}`
|
||||||
|
const accountMapper = account => `@${account.acct}`
|
||||||
|
|
||||||
|
async function insertTextAtPosition (realm, text, startIndex, endIndex) {
|
||||||
const { currentInstance } = store.get()
|
const { currentInstance } = store.get()
|
||||||
const oldText = store.getComposeData(realm, 'text')
|
const oldText = store.getComposeData(realm, 'text')
|
||||||
const pre = oldText.substring(0, startIndex)
|
const pre = oldText.substring(0, startIndex)
|
||||||
const post = oldText.substring(endIndex)
|
const post = oldText.substring(endIndex)
|
||||||
const newText = `${pre}@${username} ${post}`
|
const newText = `${pre}${text} ${post}`
|
||||||
store.setComposeData(realm, { text: newText })
|
store.setComposeData(realm, { text: newText })
|
||||||
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function insertUsername (realm, account, startIndex, endIndex) {
|
||||||
|
await insertTextAtPosition(realm, accountMapper(account), startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertHashtag (realm, hashtag, startIndex, endIndex) {
|
||||||
|
await insertTextAtPosition(realm, hashtagMapper(hashtag), startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertEmojiAtPosition (realm, emoji, startIndex, endIndex) {
|
||||||
|
await insertTextAtPosition(realm, emojiMapper(emoji), startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickSelectedItem (realm, resultMapper) {
|
||||||
|
const {
|
||||||
|
composeSelectionStart,
|
||||||
|
autosuggestSearchText,
|
||||||
|
autosuggestSelected,
|
||||||
|
autosuggestSearchResults
|
||||||
|
} = store.get()
|
||||||
|
const result = autosuggestSearchResults[autosuggestSelected]
|
||||||
|
const startIndex = composeSelectionStart - autosuggestSearchText.length
|
||||||
|
const endIndex = composeSelectionStart
|
||||||
|
await insertTextAtPosition(realm, resultMapper(result), startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
export async function clickSelectedAutosuggestionUsername (realm) {
|
export async function clickSelectedAutosuggestionUsername (realm) {
|
||||||
const {
|
return clickSelectedItem(realm, accountMapper)
|
||||||
composeSelectionStart,
|
|
||||||
autosuggestSearchText,
|
|
||||||
autosuggestSelected,
|
|
||||||
autosuggestSearchResults
|
|
||||||
} = store.get()
|
|
||||||
const account = autosuggestSearchResults[autosuggestSelected]
|
|
||||||
const startIndex = composeSelectionStart - autosuggestSearchText.length
|
|
||||||
const endIndex = composeSelectionStart
|
|
||||||
await insertUsername(realm, account.acct, startIndex, endIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertEmojiAtPosition (realm, emoji, startIndex, endIndex) {
|
export async function clickSelectedAutosuggestionHashtag (realm) {
|
||||||
const { currentInstance } = store.get()
|
return clickSelectedItem(realm, hashtagMapper)
|
||||||
const oldText = store.getComposeData(realm, 'text') || ''
|
|
||||||
const pre = oldText.substring(0, startIndex)
|
|
||||||
const post = oldText.substring(endIndex)
|
|
||||||
const newText = `${pre}:${emoji.shortcode}: ${post}`
|
|
||||||
store.setComposeData(realm, { text: newText })
|
|
||||||
store.setForAutosuggest(currentInstance, realm, { autosuggestSearchResults: [] })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickSelectedAutosuggestionEmoji (realm) {
|
export async function clickSelectedAutosuggestionEmoji (realm) {
|
||||||
const {
|
return clickSelectedItem(realm, emojiMapper)
|
||||||
composeSelectionStart,
|
|
||||||
autosuggestSearchText,
|
|
||||||
autosuggestSelected,
|
|
||||||
autosuggestSearchResults
|
|
||||||
} = store.get()
|
|
||||||
const emoji = autosuggestSearchResults[autosuggestSelected]
|
|
||||||
const startIndex = composeSelectionStart - autosuggestSearchText.length
|
|
||||||
const endIndex = composeSelectionStart
|
|
||||||
await insertEmojiAtPosition(realm, emoji, startIndex, endIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectAutosuggestItem (item) {
|
export function selectAutosuggestItem (item) {
|
||||||
|
@ -55,8 +60,10 @@ export function selectAutosuggestItem (item) {
|
||||||
const startIndex = composeSelectionStart - autosuggestSearchText.length
|
const startIndex = composeSelectionStart - autosuggestSearchText.length
|
||||||
const endIndex = composeSelectionStart
|
const endIndex = composeSelectionStart
|
||||||
if (item.acct) {
|
if (item.acct) {
|
||||||
/* no await */ insertUsername(currentComposeRealm, item.acct, startIndex, endIndex)
|
/* no await */ insertUsername(currentComposeRealm, item, startIndex, endIndex)
|
||||||
} else {
|
} else if (item.shortcode) {
|
||||||
/* no await */ insertEmojiAtPosition(currentComposeRealm, item, startIndex, endIndex)
|
/* no await */ insertEmojiAtPosition(currentComposeRealm, item, startIndex, endIndex)
|
||||||
|
} else { // hashtag
|
||||||
|
/* no await */ insertHashtag(currentComposeRealm, item, startIndex, endIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
51
src/routes/_actions/autosuggestHashtagSearch.js
Normal file
51
src/routes/_actions/autosuggestHashtagSearch.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { search } from '../_api/search'
|
||||||
|
import { store } from '../_store/store'
|
||||||
|
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||||
|
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
|
||||||
|
import { PromiseThrottler } from '../_utils/PromiseThrottler'
|
||||||
|
|
||||||
|
const promiseThrottler = new PromiseThrottler(200) // Mastodon FE also uses 200ms
|
||||||
|
|
||||||
|
export function doHashtagSearch (searchText) {
|
||||||
|
let canceled = false
|
||||||
|
const { currentInstance, accessToken } = store.get()
|
||||||
|
let controller = typeof AbortController === 'function' && new AbortController()
|
||||||
|
|
||||||
|
function abortFetch () {
|
||||||
|
if (controller) {
|
||||||
|
controller.abort()
|
||||||
|
controller = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchHashtagsRemotely (searchText) {
|
||||||
|
// Throttle our XHRs to be a good citizen and not spam the server with one XHR per keystroke
|
||||||
|
await promiseThrottler.next()
|
||||||
|
if (canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const searchPromise = search(
|
||||||
|
currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, controller && controller.signal
|
||||||
|
)
|
||||||
|
const results = (await searchPromise).hashtags
|
||||||
|
store.setForCurrentAutosuggest({
|
||||||
|
autosuggestType: 'hashtag',
|
||||||
|
autosuggestSelected: 0,
|
||||||
|
autosuggestSearchResults: results
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleIdleTask(() => {
|
||||||
|
if (canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
/* no await */ searchHashtagsRemotely(searchText)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
cancel: () => {
|
||||||
|
canceled = true
|
||||||
|
abortFetch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
class="compose-autosuggest-list"
|
class="compose-autosuggest-list"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
>
|
>
|
||||||
{#each items as item, i (item.shortcode ? `emoji-${item.shortcode}` : `account-${item.id}`)}
|
{#each items as item, i (item.shortcode || item.id || item.name)}
|
||||||
<li id="{i === selected ? `compose-autosuggest-active-item-${realm}` : ''}"
|
<li id="{i === selected ? `compose-autosuggest-active-item-${realm}` : ''}"
|
||||||
class="compose-autosuggest-list-item {i === selected ? 'selected' : ''}"
|
class="compose-autosuggest-list-item {i === selected ? 'selected' : ''}"
|
||||||
role="option"
|
role="option"
|
||||||
|
@ -13,24 +13,34 @@
|
||||||
>
|
>
|
||||||
<div class="compose-autosuggest-list-grid" aria-hidden="true">
|
<div class="compose-autosuggest-list-grid" aria-hidden="true">
|
||||||
{#if type === 'account'}
|
{#if type === 'account'}
|
||||||
<div class="compose-autosuggest-list-item-avatar">
|
<div class="compose-autosuggest-list-item-avatar">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="{$isVeryTinyMobileSize ? 'extra-small' : 'small'}"
|
size="{$isVeryTinyMobileSize ? 'extra-small' : 'small'}"
|
||||||
account={item}
|
account={item}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="compose-autosuggest-list-display-name">
|
||||||
|
<AccountDisplayName account={item} />
|
||||||
|
</span>
|
||||||
|
<span class="compose-autosuggest-list-username">
|
||||||
|
{'@' + item.acct}
|
||||||
|
</span>
|
||||||
|
{:elseif type === 'hashtag'}
|
||||||
|
<SvgIcon
|
||||||
|
href="#fa-hashtag"
|
||||||
|
ariaHidden={true}
|
||||||
|
className="compose-autosuggest-list-item-icon"
|
||||||
/>
|
/>
|
||||||
</div>
|
<span class="compose-autosuggest-list-display-name compose-autosuggest-list-display-name-single">
|
||||||
<span class="compose-autosuggest-list-display-name">
|
{item.name.toLowerCase()}
|
||||||
<AccountDisplayName account={item} />
|
</span>
|
||||||
</span>
|
|
||||||
<span class="compose-autosuggest-list-username">
|
|
||||||
{'@' + item.acct}
|
|
||||||
</span>
|
|
||||||
{:else}
|
{:else}
|
||||||
<img src={$autoplayGifs ? item.url : item.static_url}
|
<img src={$autoplayGifs ? item.url : item.static_url}
|
||||||
class="compose-autosuggest-list-item-icon"
|
class="compose-autosuggest-list-item-icon"
|
||||||
alt="{':' + item.shortcode + ':'}"
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span class="compose-autosuggest-list-display-name">
|
<span class="compose-autosuggest-list-display-name compose-autosuggest-list-display-name-single">
|
||||||
{':' + item.shortcode + ':'}
|
{':' + item.shortcode + ':'}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -70,11 +80,12 @@
|
||||||
:global(.compose-autosuggest-list-item-avatar) {
|
:global(.compose-autosuggest-list-item-avatar) {
|
||||||
grid-area: icon;
|
grid-area: icon;
|
||||||
}
|
}
|
||||||
.compose-autosuggest-list-item-icon {
|
:global(.compose-autosuggest-list-item-icon) {
|
||||||
grid-area: icon;
|
grid-area: icon;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
fill: var(--deemphasized-text-color);
|
||||||
}
|
}
|
||||||
.compose-autosuggest-list-display-name {
|
.compose-autosuggest-list-display-name {
|
||||||
grid-area: display-name;
|
grid-area: display-name;
|
||||||
|
@ -85,6 +96,10 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
.compose-autosuggest-list-display-name-single {
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
.compose-autosuggest-list-username {
|
.compose-autosuggest-list-username {
|
||||||
grid-area: username;
|
grid-area: username;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
@ -108,6 +123,10 @@
|
||||||
.compose-autosuggest-list-grid {
|
.compose-autosuggest-list-grid {
|
||||||
grid-column-gap: 5px;
|
grid-column-gap: 5px;
|
||||||
}
|
}
|
||||||
|
:global(.compose-autosuggest-list-item-icon) {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
@ -115,6 +134,7 @@
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
import AccountDisplayName from '../profile/AccountDisplayName.html'
|
import AccountDisplayName from '../profile/AccountDisplayName.html'
|
||||||
import { createAutosuggestAccessibleLabel } from '../../_utils/createAutosuggestAccessibleLabel'
|
import { createAutosuggestAccessibleLabel } from '../../_utils/createAutosuggestAccessibleLabel'
|
||||||
|
import SvgIcon from '../SvgIcon.html'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
store: () => store,
|
store: () => store,
|
||||||
|
@ -134,7 +154,8 @@
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Avatar,
|
Avatar,
|
||||||
AccountDisplayName
|
AccountDisplayName,
|
||||||
|
SvgIcon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
import { selectionChange } from '../../_utils/events'
|
import { selectionChange } from '../../_utils/events'
|
||||||
import {
|
import {
|
||||||
clickSelectedAutosuggestionUsername,
|
clickSelectedAutosuggestionUsername,
|
||||||
clickSelectedAutosuggestionEmoji
|
clickSelectedAutosuggestionEmoji, clickSelectedAutosuggestionHashtag
|
||||||
} from '../../_actions/autosuggest'
|
} from '../../_actions/autosuggest'
|
||||||
import { observe } from 'svelte-extras'
|
import { observe } from 'svelte-extras'
|
||||||
import { get } from '../../_utils/lodash-lite'
|
import { get } from '../../_utils/lodash-lite'
|
||||||
|
@ -209,6 +209,8 @@
|
||||||
this.store.setForCurrentAutosuggest({ autosuggestSelecting: true })
|
this.store.setForCurrentAutosuggest({ autosuggestSelecting: true })
|
||||||
if (autosuggestType === 'account') {
|
if (autosuggestType === 'account') {
|
||||||
await clickSelectedAutosuggestionUsername(realm)
|
await clickSelectedAutosuggestionUsername(realm)
|
||||||
|
} else if (autosuggestType === 'hashtag') {
|
||||||
|
await clickSelectedAutosuggestionHashtag(realm)
|
||||||
} else { // emoji
|
} else { // emoji
|
||||||
await clickSelectedAutosuggestionEmoji(realm)
|
await clickSelectedAutosuggestionEmoji(realm)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { mark, stop } from '../../_utils/marks'
|
||||||
const MIN_PREFIX_LENGTH = 2
|
const MIN_PREFIX_LENGTH = 2
|
||||||
// Technically mastodon accounts allow dots, but it would be weird to do an autosuggest search if it ends with a dot.
|
// Technically mastodon accounts allow dots, but it would be weird to do an autosuggest search if it ends with a dot.
|
||||||
// Also this is rare. https://github.com/tootsuite/mastodon/pull/6844
|
// Also this is rare. https://github.com/tootsuite/mastodon/pull/6844
|
||||||
const VALID_ACCOUNT_AND_EMOJI_CHAR = '\\w'
|
const VALID_CHARS = '\\w'
|
||||||
const ACCOUNT_SEARCH_REGEX = new RegExp(`(?:\\s|^)(@${VALID_ACCOUNT_AND_EMOJI_CHAR}{${MIN_PREFIX_LENGTH},})$`)
|
const PREFIXES = '(?:@|:|#)'
|
||||||
const EMOJI_SEARCH_REGEX = new RegExp(`(?:\\s|^)(:${VALID_ACCOUNT_AND_EMOJI_CHAR}{${MIN_PREFIX_LENGTH},})$`)
|
const REGEX = new RegExp(`(?:\\s|^)(${PREFIXES}${VALID_CHARS}{${MIN_PREFIX_LENGTH},})$`)
|
||||||
|
|
||||||
function computeForAutosuggest (store, key, defaultValue) {
|
function computeForAutosuggest (store, key, defaultValue) {
|
||||||
store.compute(key,
|
store.compute(key,
|
||||||
|
@ -43,7 +43,7 @@ export function autosuggestComputations (store) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const textUpToCursor = currentComposeText.substring(0, selectionStart)
|
const textUpToCursor = currentComposeText.substring(0, selectionStart)
|
||||||
const match = textUpToCursor.match(ACCOUNT_SEARCH_REGEX) || textUpToCursor.match(EMOJI_SEARCH_REGEX)
|
const match = textUpToCursor.match(REGEX)
|
||||||
return (match && match[1]) || ''
|
return (match && match[1]) || ''
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { store } from '../store'
|
import { store } from '../store'
|
||||||
import { doEmojiSearch } from '../../_actions/autosuggestEmojiSearch'
|
import { doEmojiSearch } from '../../_actions/autosuggestEmojiSearch'
|
||||||
import { doAccountSearch } from '../../_actions/autosuggestAccountSearch'
|
import { doAccountSearch } from '../../_actions/autosuggestAccountSearch'
|
||||||
|
import { doHashtagSearch } from '../../_actions/autosuggestHashtagSearch'
|
||||||
|
|
||||||
export function autosuggestObservers () {
|
export function autosuggestObservers () {
|
||||||
let lastSearch
|
let lastSearch
|
||||||
|
@ -19,11 +20,11 @@ export function autosuggestObservers () {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const autosuggestType = autosuggestSearchText.startsWith('@') ? 'account' : 'emoji'
|
if (autosuggestSearchText.startsWith(':')) { // emoji
|
||||||
|
|
||||||
if (autosuggestType === 'emoji') {
|
|
||||||
lastSearch = doEmojiSearch(autosuggestSearchText)
|
lastSearch = doEmojiSearch(autosuggestSearchText)
|
||||||
} else {
|
} else if (autosuggestSearchText.startsWith('#')) { // hashtag
|
||||||
|
lastSearch = doHashtagSearch(autosuggestSearchText)
|
||||||
|
} else { // account
|
||||||
lastSearch = doAccountSearch(autosuggestSearchText)
|
lastSearch = doAccountSearch(autosuggestSearchText)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,6 +7,8 @@ export function createAutosuggestAccessibleLabel (
|
||||||
let label
|
let label
|
||||||
if (autosuggestType === 'emoji') {
|
if (autosuggestType === 'emoji') {
|
||||||
label = `${selected.shortcode}`
|
label = `${selected.shortcode}`
|
||||||
|
} else if (autosuggestType === 'hashtag') {
|
||||||
|
label = `#${selected.name}`
|
||||||
} else { // account
|
} else { // account
|
||||||
let displayName = selected.display_name || selected.username
|
let displayName = selected.display_name || selected.username
|
||||||
const emojis = selected.emojis || []
|
const emojis = selected.emojis || []
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import {
|
import {
|
||||||
composeInput, getNthAutosuggestionResult, getNthComposeReplyInput, getNthReplyButton, getNthStatus, sleep
|
composeInput,
|
||||||
|
composeLengthIndicator,
|
||||||
|
getNthAutosuggestionResult,
|
||||||
|
getNthComposeReplyInput,
|
||||||
|
getNthReplyButton,
|
||||||
|
getNthStatus,
|
||||||
|
sleep
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { Selector as $ } from 'testcafe'
|
import { Selector as $ } from 'testcafe'
|
||||||
import { loginAsFoobar } from '../roles'
|
import { loginAsFoobar } from '../roles'
|
||||||
|
@ -16,6 +22,7 @@ test('autosuggests user handles', async t => {
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
await t
|
await t
|
||||||
.typeText(composeInput, 'hey @qu')
|
.typeText(composeInput, 'hey @qu')
|
||||||
|
.expect(getNthAutosuggestionResult(1).getAttribute('aria-label')).contains('@quux')
|
||||||
.click(getNthAutosuggestionResult(1), { timeout })
|
.click(getNthAutosuggestionResult(1), { timeout })
|
||||||
.expect(composeInput.value).eql('hey @quux ')
|
.expect(composeInput.value).eql('hey @quux ')
|
||||||
.typeText(composeInput, 'and also @adm')
|
.typeText(composeInput, 'and also @adm')
|
||||||
|
@ -39,6 +46,7 @@ test('autosuggests custom emoji', async t => {
|
||||||
.click(getNthAutosuggestionResult(1))
|
.click(getNthAutosuggestionResult(1))
|
||||||
.expect(composeInput.value).eql(':blobnom: ')
|
.expect(composeInput.value).eql(':blobnom: ')
|
||||||
.typeText(composeInput, 'and :blob')
|
.typeText(composeInput, 'and :blob')
|
||||||
|
.expect(getNthAutosuggestionResult(1).getAttribute('aria-label')).contains('blobnom')
|
||||||
.expect(getNthAutosuggestionResult(1).innerText).contains(':blobnom:', { timeout })
|
.expect(getNthAutosuggestionResult(1).innerText).contains(':blobnom:', { timeout })
|
||||||
.expect(getNthAutosuggestionResult(2).innerText).contains(':blobpats:')
|
.expect(getNthAutosuggestionResult(2).innerText).contains(':blobpats:')
|
||||||
.expect(getNthAutosuggestionResult(3).innerText).contains(':blobpeek:')
|
.expect(getNthAutosuggestionResult(3).innerText).contains(':blobpeek:')
|
||||||
|
@ -121,3 +129,13 @@ test('autosuggest only shows for one input part 2', async t => {
|
||||||
await t.pressKey('backspace')
|
await t.pressKey('backspace')
|
||||||
.expect($('.compose-autosuggest.shown').exists).notOk()
|
.expect($('.compose-autosuggest.shown').exists).notOk()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('autocomplete disappears on blur', async t => {
|
||||||
|
await loginAsFoobar(t)
|
||||||
|
await t
|
||||||
|
.hover(composeInput)
|
||||||
|
.typeText(composeInput, '@adm')
|
||||||
|
.expect($('.compose-autosuggest.shown').exists).ok({ timeout })
|
||||||
|
.click(composeLengthIndicator)
|
||||||
|
.expect($('.compose-autosuggest.shown').exists).notOk()
|
||||||
|
})
|
||||||
|
|
34
tests/spec/131-compose-autosuggest.js
Normal file
34
tests/spec/131-compose-autosuggest.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import {
|
||||||
|
composeInput, getNthAutosuggestionResult, sleep
|
||||||
|
} from '../utils'
|
||||||
|
import { loginAsFoobar } from '../roles'
|
||||||
|
import { postAs } from '../serverActions'
|
||||||
|
|
||||||
|
fixture`131-compose-autosuggest.js`
|
||||||
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
|
const timeout = 30000
|
||||||
|
|
||||||
|
test('autosuggests hashtags', async t => {
|
||||||
|
await postAs('admin', 'hello #blank and hello #blanka')
|
||||||
|
await sleep(1000)
|
||||||
|
await loginAsFoobar(t)
|
||||||
|
await t
|
||||||
|
.hover(composeInput)
|
||||||
|
await sleep(1000)
|
||||||
|
await t
|
||||||
|
.typeText(composeInput, 'hey #bl')
|
||||||
|
.expect(getNthAutosuggestionResult(1).innerText).contains('blank', { timeout })
|
||||||
|
.expect(getNthAutosuggestionResult(2).innerText).contains('blanka', { timeout })
|
||||||
|
.expect(getNthAutosuggestionResult(1).getAttribute('aria-label')).contains('#blank', { timeout })
|
||||||
|
.expect(getNthAutosuggestionResult(2).getAttribute('aria-label')).contains('#blanka', { timeout })
|
||||||
|
.click(getNthAutosuggestionResult(1), { timeout })
|
||||||
|
.expect(composeInput.value).eql('hey #blank ')
|
||||||
|
.typeText(composeInput, 'and also #BL')
|
||||||
|
.click(getNthAutosuggestionResult(1), { timeout })
|
||||||
|
.expect(composeInput.value).eql('hey #blank and also #blank ')
|
||||||
|
.typeText(composeInput, 'and also #blanka')
|
||||||
|
.expect(getNthAutosuggestionResult(1).innerText).contains('blanka', { timeout })
|
||||||
|
.pressKey('enter')
|
||||||
|
.expect(composeInput.value).eql('hey #blank and also #blank and also #blanka ')
|
||||||
|
})
|
Loading…
Reference in a new issue