Merge pull request #3778 from omnivore-app/fix/filter

add tests for filters
This commit is contained in:
Hongbo Wu 2024-04-04 11:48:01 +08:00 committed by GitHub
commit f8538d2543
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 168 additions and 21 deletions

View file

@ -0,0 +1,140 @@
import { expect } from 'chai'
import 'mocha'
import { filterItemEvents } from '../../src/services/library_item'
import { parseSearchQuery } from '../../src/utils/search'
describe('filterItemEvents', () => {
it('returns events if there are quotation marks in the subscription name', () => {
const query = 'subscription:"Best \\\"Omnivore\\\""'
const ast = parseSearchQuery(query)
const events = [
{
subscription: 'Best "Omnivore"',
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if subscription name equals ignore case', () => {
const query = 'subscription:substack'
const ast = parseSearchQuery(query)
const events = [
{
subscription: 'Substack',
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if site name equals ignore case', () => {
const query = 'site:youtube'
const ast = parseSearchQuery(query)
const events = [
{
siteName: 'YouTube',
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if site name contains the search query', () => {
const query = 'site:standard'
const ast = parseSearchQuery(query)
const events = [
{
siteName: 'Der Standard',
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if domain name contains the search query', () => {
const query = 'site:stackoverflow.com'
const ast = parseSearchQuery(query)
const events = [
{
siteName: 'Stack Overflow',
originalUrl: 'https://stackoverflow.com/questions/123',
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if top level domain matches', () => {
const query = 'site:".com"'
const ast = parseSearchQuery(query)
const events = [
{
siteName: 'Stack Overflow',
originalUrl: 'https://stackoverflow.com/questions/123',
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if labels match the search query', () => {
const query = 'label:foo'
const ast = parseSearchQuery(query)
const events = [
{
labelNames: ['foo'],
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if labels contain quotation marks', () => {
const query = 'label:"foo \\\"bar\\\""'
const ast = parseSearchQuery(query)
const events = [
{
labelNames: ['foo "bar"'],
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if labels contain space', () => {
const query = 'label:"foo bar"'
const ast = parseSearchQuery(query)
const events = [
{
labelNames: ['foo bar'],
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if labels match the search query ignore case', () => {
const query = 'label:Foo'
const ast = parseSearchQuery(query)
const events = [
{
labelNames: ['foo'],
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
it('returns events if labels match the search query with multiple labels', () => {
const query = 'label:foo,bar'
const ast = parseSearchQuery(query)
const events = [
{
labelNames: ['foo', 'bar'],
},
]
const result = filterItemEvents(ast, events)
expect(result).to.eql(events)
})
})

View file

@ -20,6 +20,7 @@ import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
import Link from 'next/link'
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
import { NavMenuFooter } from './Footer'
import { escapeQuotes } from '../../../utils/helper'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
@ -255,7 +256,7 @@ function Subscriptions(
name: name,
keywords: '*' + name,
perform: () => {
props.applySearchQuery(`subscription:\"${name}\"`)
props.applySearchQuery(`subscription:\"${escapeQuotes(name)}\"`)
},
}
}),
@ -291,7 +292,9 @@ function Subscriptions(
return (
<FilterButton
key={item.id}
filterTerm={`in:inbox subscription:\"${item.name}\"`}
filterTerm={`in:inbox subscription:\"${escapeQuotes(
item.name
)}\"`}
text={item.name}
{...props}
/>
@ -507,7 +510,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
const checkboxRef = useRef<HTMLInputElement | null>(null)
const state = useMemo(() => {
const term = props.searchTerm ?? ''
if (term.indexOf(`label:\"${props.label.name}\"`) >= 0) {
if (term.indexOf(`label:\"${escapeQuotes(props.label.name)}\"`) >= 0) {
return 'on'
}
return 'off'
@ -557,7 +560,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
props.applySearchQuery(query.trim())
} else {
props.applySearchQuery(
`${query.trim()} label:\"${props.label.name}\"`
`${query.trim()} label:\"${escapeQuotes(props.label.name)}\"`
)
}
}}
@ -576,16 +579,14 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
type="checkbox"
checked={state === 'on'}
onChange={(e) => {
const escapedName = escapeQuotes(props.label.name)
if (e.target.checked) {
props.applySearchQuery(
`${props.searchTerm ?? ''} label:\"${props.label.name}\"`
`${props.searchTerm ?? ''} label:\"${escapedName}\"`
)
} else {
const query =
props.searchTerm?.replace(
`label:\"${props.label.name}\"`,
''
) ?? ''
props.searchTerm?.replace(`label:\"${escapedName}\"`, '') ?? ''
props.applySearchQuery(query)
}
}}

View file

@ -31,6 +31,7 @@ import { NewsletterIcon } from '../../elements/icons/NewsletterIcon'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { useRouter } from 'next/router'
import { DiscoverIcon } from "../../elements/icons/DiscoverIcon"
import { escapeQuotes } from "../../../utils/helper"
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
@ -221,7 +222,7 @@ const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => {
<NavRedirectButton
{...props}
text="Discover"
redirectLocation={"/discover"}
redirectLocation={'/discover'}
icon={<DiscoverIcon color={theme.colors.discover.toString()} />}
/>
</VStack>
@ -545,7 +546,7 @@ function Subscriptions(
name: name,
keywords: '*' + name,
perform: () => {
props.applySearchQuery(`subscription:\"${name}\"`)
props.applySearchQuery(`subscription:\"${escapeQuotes(name)}\"`)
},
}
}),
@ -581,7 +582,9 @@ function Subscriptions(
return (
<FilterButton
key={item.id}
filterTerm={`in:inbox subscription:\"${item.name}\"`}
filterTerm={`in:inbox subscription:\"${escapeQuotes(
item.name
)}\"`}
text={item.name}
{...props}
/>
@ -735,7 +738,7 @@ type NavButtonRedirectProps = {
}
function NavRedirectButton(props: NavButtonRedirectProps): JSX.Element {
const [selected, setSelected] = useState(false);
const [selected, setSelected] = useState(false)
const router = useRouter()
useEffect(() => {
@ -932,7 +935,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
const checkboxRef = useRef<HTMLInputElement | null>(null)
const state = useMemo(() => {
const term = props.searchTerm ?? ''
if (term.indexOf(`label:\"${props.label.name}\"`) >= 0) {
if (term.indexOf(`label:\"${escapeQuotes(props.label.name)}\"`) >= 0) {
return 'on'
}
return 'off'
@ -982,7 +985,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
props.applySearchQuery(query.trim())
} else {
props.applySearchQuery(
`${query.trim()} label:\"${props.label.name}\"`
`${query.trim()} label:\"${escapeQuotes(props.label.name)}\"`
)
}
}}
@ -1001,14 +1004,15 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
type="checkbox"
checked={state === 'on'}
onChange={(e) => {
const escapedLabelName = escapeQuotes(props.label.name)
if (e.target.checked) {
props.applySearchQuery(
`${props.searchTerm ?? ''} label:\"${props.label.name}\"`
`${props.searchTerm ?? ''} label:\"${escapedLabelName}\"`
)
} else {
const query =
props.searchTerm?.replace(
`label:\"${props.label.name}\"`,
`label:\"${escapedLabelName}\"`,
''
) ?? ''
props.applySearchQuery(query)

View file

@ -21,6 +21,7 @@ import { Label } from '../../lib/networking/fragments/labelFragment'
import { CheckSquare, Circle, Square } from 'phosphor-react'
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
import { usePersistedState } from '../../lib/hooks/usePersistedState'
import { escapeQuotes } from '../../utils/helper'
export type PinnedSearch = {
type: 'saved-search' | 'label'
@ -282,7 +283,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
type: 'label',
itemId: props.label.id,
name: props.label.name,
search: `label:\"${props.label.name}\"`,
search: `label:\"${escapeQuotes(props.label.name)}\"`,
}}
listAction={props.listAction}
>

View file

@ -34,7 +34,7 @@ import { CheckSquare, Square } from 'phosphor-react'
import { Button } from '../../components/elements/Button'
import { styled } from '@stitches/react'
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
import { escapeQuotes } from '../../utils/helper'
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
const SHORTCUTS_KEY = 'library-shortcuts'
@ -365,7 +365,7 @@ const AvailableItems = (props: ListProps): JSX.Element => {
type: 'label',
label: label,
name: label.name,
filter: `label:\"${label.name}\"`,
filter: `label:\"${escapeQuotes(label.name)}\"`,
}
props.dispatchList({
item,
@ -416,7 +416,7 @@ const AvailableItems = (props: ListProps): JSX.Element => {
: 'feed',
filter:
subscription.type == SubscriptionType.NEWSLETTER
? `subscription:\"${subscription.name}\"`
? `subscription:\"${escapeQuotes(subscription.name)}\"`
: `rss:\"${subscription.url}\"`,
}
props.dispatchList({

View file

@ -0,0 +1 @@
export const escapeQuotes = (str: string) => str.replace(/"/g, '\\"')