@playpilot/tpi 8.8.0 → 8.9.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/dist/editorial.mount.js +9 -9
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +7 -7
- package/events.md +2 -0
- package/package.json +1 -1
- package/src/lib/api/titles.ts +1 -3
- package/src/lib/data/translations.ts +15 -0
- package/src/lib/enums/TrackingEvent.ts +2 -0
- package/src/lib/filter.ts +12 -0
- package/src/lib/types/filter.d.ts +3 -1
- package/src/routes/components/Explore/ExploreLayout.svelte +7 -1
- package/src/routes/components/Explore/Filter/ActiveFilterItems.svelte +76 -0
- package/src/routes/components/Explore/Filter/Filter.svelte +31 -2
- package/src/routes/components/Explore/Filter/FilterItem.svelte +5 -15
- package/src/routes/components/Explore/Routes/ExploreHome.svelte +6 -1
- package/src/routes/components/Explore/Routes/ExploreResults.svelte +9 -19
- package/src/routes/components/Rails/TitlesRail.svelte +1 -0
- package/src/tests/lib/filter.test.js +35 -0
- package/src/tests/routes/components/Explore/Filter/ActiveFilterItems.test.js +120 -0
- package/src/tests/routes/components/Explore/Filter/Filter.test.js +28 -0
package/events.md
CHANGED
|
@@ -77,6 +77,8 @@ Event | Action | Info | Payload
|
|
|
77
77
|
`ali_manual_report` | _Fires only through manual action when reporting issues with an injection via the Editor._ | | `Title`, `report_reason`, `sid` (of injection), `title` (of injection), `sentence`, `failed` (true or false), `failed_message` (reason for failure as given in the editor), `manual` (true or false)
|
|
78
78
|
`ali_editor_error` | _Fires whenever an error occurs within the Editor._ | | `Title`, `phrase`, `sentence`
|
|
79
79
|
`ali_injection_error` | _Fires whenever an error occurs during injection_ | This includes fetching the injections as well as actually injecting itself. Does not include fetching of the config object. | `message` (error message as given by the browser)
|
|
80
|
+
`venus_explore_results_error` | _Fires whenever an error occurs when fetching titles on the results page of the streaming guide_ | | `message` (error message as given by the browser), `params`
|
|
81
|
+
`venus_explore_home_rail_error` | _Fires whenever an error when fetching titles for any rail on the streaming guide homepage_ | | `message` (error message as given by the browser), `params`
|
|
80
82
|
|
|
81
83
|
### Split Testing
|
|
82
84
|
Event | Action | Info | Payload
|
package/package.json
CHANGED
package/src/lib/api/titles.ts
CHANGED
|
@@ -19,9 +19,7 @@ export async function fetchTitles(params: Record<string, any> = {}): Promise<API
|
|
|
19
19
|
|
|
20
20
|
if (params.region !== null) params.region = params.region || await getRegionBasedOnIp()
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return response
|
|
22
|
+
return await api<APIPaginatedResult<TitleData>>(`/titles/browse?api-token=${apiToken}&` + paramsToString(params))
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
export async function fetchSimilarTitles(title: TitleData): Promise<TitleData[]> {
|
|
@@ -231,6 +231,21 @@ export const translations = {
|
|
|
231
231
|
[Language.Swedish]: 'Toppbetyg',
|
|
232
232
|
[Language.Danish]: 'Højst vurderet',
|
|
233
233
|
},
|
|
234
|
+
'-popularity': {
|
|
235
|
+
[Language.English]: 'Popularity',
|
|
236
|
+
[Language.Swedish]: 'Popularitet',
|
|
237
|
+
[Language.Danish]: 'Popularitet',
|
|
238
|
+
},
|
|
239
|
+
'-new': {
|
|
240
|
+
[Language.English]: 'New',
|
|
241
|
+
[Language.Swedish]: 'Nytt',
|
|
242
|
+
[Language.Danish]: 'Nyt',
|
|
243
|
+
},
|
|
244
|
+
'-best': {
|
|
245
|
+
[Language.English]: 'Top Rated',
|
|
246
|
+
[Language.Swedish]: 'Toppbetyg',
|
|
247
|
+
[Language.Danish]: 'Højst vurderet',
|
|
248
|
+
},
|
|
234
249
|
'Search': {
|
|
235
250
|
[Language.English]: 'Search',
|
|
236
251
|
[Language.Swedish]: 'Sök',
|
|
@@ -40,6 +40,8 @@ export const TrackingEvent = {
|
|
|
40
40
|
ManualReport: 'ali_manual_report',
|
|
41
41
|
EditorError: 'ali_editor_error',
|
|
42
42
|
InjectionError: 'ali_injection_error',
|
|
43
|
+
ExploreResultsError: 'venus_explore_results_error',
|
|
44
|
+
ExploreHomeRailError: 'venus_explore_home_rail_error',
|
|
43
45
|
|
|
44
46
|
// Ads
|
|
45
47
|
TopScrollView: 'ali_top_scroll_view',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExploreFilterItem } from './types/filter'
|
|
2
|
+
|
|
3
|
+
export function isFilterItemActive(item: ExploreFilterItem, range?: [number, number] | null): boolean {
|
|
4
|
+
if (!item) return false
|
|
5
|
+
|
|
6
|
+
const { type, value } = item
|
|
7
|
+
|
|
8
|
+
if (type === 'array' && !((value as Array<any>).length)) return false
|
|
9
|
+
if (type === 'range' && JSON.stringify(value) === JSON.stringify(range)) return false
|
|
10
|
+
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export type ExploreFilterType = 'array' | 'range' | 'string'
|
|
2
|
-
export type
|
|
2
|
+
export type ExploreFilterItem = { type: ExploreFilterType, value: unknown }
|
|
3
|
+
export type ExploreFilter = Record<string, ExploreFilterItem>
|
|
4
|
+
export type ExploreFilterItemArrayValue = { label: string, value: string }
|
|
@@ -57,7 +57,13 @@
|
|
|
57
57
|
|
|
58
58
|
{#if useExploreRouter()}
|
|
59
59
|
<div class="filter">
|
|
60
|
-
<Filter
|
|
60
|
+
<Filter
|
|
61
|
+
{filter}
|
|
62
|
+
{searchQuery}
|
|
63
|
+
limit={clientWidth < 500}
|
|
64
|
+
showSorting={!searchQuery}
|
|
65
|
+
onchange={() => navigate('results')}
|
|
66
|
+
onempty={() => navigate('home')} />
|
|
61
67
|
</div>
|
|
62
68
|
{/if}
|
|
63
69
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { isFilterItemActive } from '$lib/filter'
|
|
3
|
+
import { t } from '$lib/localization'
|
|
4
|
+
import type { ExploreFilter, ExploreFilterItemArrayValue } from '$lib/types/filter'
|
|
5
|
+
import IconClose from '../../Icons/IconClose.svelte'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
filter: ExploreFilter
|
|
9
|
+
items: any
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { filter, items }: Props = $props()
|
|
13
|
+
|
|
14
|
+
function removeFilterItem(param: string): void {
|
|
15
|
+
delete filter[param]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function truncate(text: string): string {
|
|
19
|
+
let truncated = text.slice(0, 30).trim()
|
|
20
|
+
if (text.length > truncated.length) truncated += '...'
|
|
21
|
+
|
|
22
|
+
return truncated
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="active-filter">
|
|
27
|
+
{#each Object.entries(filter) as [param, item]}
|
|
28
|
+
{@const { label, range, data, fetchData } = items.find((i: any) => param === i.param) || {}}
|
|
29
|
+
|
|
30
|
+
{#if isFilterItemActive(item, range)}
|
|
31
|
+
<button class="item" onclick={() => removeFilterItem(param)}>
|
|
32
|
+
{#if item.type === 'array'}
|
|
33
|
+
{#await fetchData?.() || data then data}
|
|
34
|
+
{truncate(data?.filter((d: ExploreFilterItemArrayValue) => (item.value as string[])
|
|
35
|
+
.includes(d.value))
|
|
36
|
+
.map((d: ExploreFilterItemArrayValue) => d.label)
|
|
37
|
+
.join(' + '))}
|
|
38
|
+
{/await}
|
|
39
|
+
{:else if item.type === 'range'}
|
|
40
|
+
{label}: {(item.value as number[]).join(' - ')}
|
|
41
|
+
{:else}
|
|
42
|
+
{t(item.value as string)}
|
|
43
|
+
{/if}
|
|
44
|
+
|
|
45
|
+
<IconClose size={8} />
|
|
46
|
+
</button>
|
|
47
|
+
{/if}
|
|
48
|
+
{/each}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<style lang="scss">
|
|
52
|
+
.active-filter {
|
|
53
|
+
display: flex;
|
|
54
|
+
gap: margin(0.25);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.item {
|
|
58
|
+
appearance: none;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: margin(0.5);
|
|
62
|
+
padding: margin(0.15) margin(0.5);
|
|
63
|
+
border: 1px solid theme(content);
|
|
64
|
+
border-radius: theme(border-radius-large);
|
|
65
|
+
background: theme(lighter);
|
|
66
|
+
color: theme(text-color-alt);
|
|
67
|
+
font-family: inherit;
|
|
68
|
+
font-size: theme(font-size-small);
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
|
|
71
|
+
&:hover {
|
|
72
|
+
border-color: theme(content-light);
|
|
73
|
+
color: theme(text-color);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -10,15 +10,26 @@
|
|
|
10
10
|
import IconArrow from '../../Icons/IconArrow.svelte'
|
|
11
11
|
import IconFilter from '../../Icons/IconFilter.svelte'
|
|
12
12
|
import { fly, scale } from 'svelte/transition'
|
|
13
|
+
import ActiveFilterItems from './ActiveFilterItems.svelte'
|
|
14
|
+
import { isFilterItemActive } from '$lib/filter'
|
|
13
15
|
|
|
14
16
|
interface Props {
|
|
15
17
|
filter: ExploreFilter
|
|
16
|
-
onchange?: () => void
|
|
17
18
|
limit?: boolean
|
|
18
19
|
showSorting?: boolean
|
|
20
|
+
searchQuery?: string
|
|
21
|
+
onchange?: () => void
|
|
22
|
+
onempty?: () => void
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
const {
|
|
25
|
+
const {
|
|
26
|
+
filter,
|
|
27
|
+
limit = true,
|
|
28
|
+
showSorting = true,
|
|
29
|
+
searchQuery = '',
|
|
30
|
+
onchange = () => null,
|
|
31
|
+
onempty = () => null,
|
|
32
|
+
}: Props = $props()
|
|
22
33
|
|
|
23
34
|
const shownItemsLimit = 3
|
|
24
35
|
const items = [{
|
|
@@ -49,6 +60,22 @@
|
|
|
49
60
|
}]
|
|
50
61
|
|
|
51
62
|
let limited = $state(limit)
|
|
63
|
+
let wasPreviouslyActive = false
|
|
64
|
+
|
|
65
|
+
$effect(() => {
|
|
66
|
+
const hasAnyActiveFilter = Object.keys(filter).some((param) => {
|
|
67
|
+
const { range } = items.find((i: any) => param === i.param) || {}
|
|
68
|
+
|
|
69
|
+
return isFilterItemActive(filter[param], range)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (hasAnyActiveFilter || searchQuery) {
|
|
73
|
+
wasPreviouslyActive = true
|
|
74
|
+
} else if (wasPreviouslyActive) {
|
|
75
|
+
wasPreviouslyActive = false
|
|
76
|
+
onempty()
|
|
77
|
+
}
|
|
78
|
+
})
|
|
52
79
|
</script>
|
|
53
80
|
|
|
54
81
|
<div class="filter" role="navigation" class:limit>
|
|
@@ -77,6 +104,8 @@
|
|
|
77
104
|
</div>
|
|
78
105
|
</div>
|
|
79
106
|
|
|
107
|
+
<ActiveFilterItems {filter} {items} />
|
|
108
|
+
|
|
80
109
|
<style lang="scss">
|
|
81
110
|
.filter {
|
|
82
111
|
position: relative;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { ExploreFilter, ExploreFilterType } from '$lib/types/filter'
|
|
2
|
+
import type { ExploreFilter, ExploreFilterItemArrayValue, ExploreFilterType } from '$lib/types/filter'
|
|
3
3
|
import Button from '../../Button.svelte'
|
|
4
4
|
import IconArrow from '../../Icons/IconArrow.svelte'
|
|
5
5
|
import TogglesWithSearch from './TogglesWithSearch.svelte'
|
|
@@ -8,16 +8,15 @@
|
|
|
8
8
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
9
9
|
import { track } from '$lib/tracking'
|
|
10
10
|
import { t } from '$lib/localization'
|
|
11
|
-
|
|
12
|
-
type Item = { label: string, value: string }
|
|
11
|
+
import { isFilterItemActive } from '$lib/filter'
|
|
13
12
|
|
|
14
13
|
interface Props {
|
|
15
14
|
filter: ExploreFilter
|
|
16
15
|
label: string
|
|
17
16
|
param: string
|
|
18
17
|
onchange?: () => void
|
|
19
|
-
data?:
|
|
20
|
-
fetchData?: (() => Promise<
|
|
18
|
+
data?: ExploreFilterItemArrayValue[] | null
|
|
19
|
+
fetchData?: (() => Promise<ExploreFilterItemArrayValue[]>) | null
|
|
21
20
|
range?: [number, number] | null
|
|
22
21
|
valueAppend?: string
|
|
23
22
|
}
|
|
@@ -33,16 +32,7 @@
|
|
|
33
32
|
valueAppend = '',
|
|
34
33
|
}: Props = $props()
|
|
35
34
|
|
|
36
|
-
const active = $derived
|
|
37
|
-
if (!filter[param]) return false
|
|
38
|
-
|
|
39
|
-
const { type, value } = filter[param]
|
|
40
|
-
|
|
41
|
-
if (type === 'array' && !((value as Array<any>).length)) return false
|
|
42
|
-
if (type === 'range' && JSON.stringify(value) === JSON.stringify(range)) return false
|
|
43
|
-
|
|
44
|
-
return true
|
|
45
|
-
})
|
|
35
|
+
const active = $derived(isFilterItemActive(filter[param], range))
|
|
46
36
|
|
|
47
37
|
let loading = $state(!!fetchData)
|
|
48
38
|
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
}]
|
|
34
34
|
|
|
35
35
|
async function getListTitles(params: Record<string, any> = {}): Promise<TitleData[]> {
|
|
36
|
-
|
|
36
|
+
try {
|
|
37
|
+
return (await fetchTitles(params))?.results
|
|
38
|
+
} catch (error: any) {
|
|
39
|
+
track(TrackingEvent.ExploreHomeRailError, null, { message: error.message, params })
|
|
40
|
+
throw new Error(error)
|
|
41
|
+
}
|
|
37
42
|
}
|
|
38
43
|
</script>
|
|
39
44
|
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import type { ExploreFilter } from '$lib/types/filter'
|
|
8
8
|
import type { TitleData } from '$lib/types/title'
|
|
9
9
|
import { hasConsentedTo } from '$lib/consent'
|
|
10
|
-
import { useExploreRouter } from '$lib/explore'
|
|
11
10
|
import { t } from '$lib/localization'
|
|
12
11
|
import { trackViaPixel } from '@playpilot/retargeting-tracking'
|
|
13
12
|
import Button from '../../Button.svelte'
|
|
@@ -16,7 +15,6 @@
|
|
|
16
15
|
import ListTitle from '../../ListTitle.svelte'
|
|
17
16
|
import ListTitleSkeleton from '../../ListTitleSkeleton.svelte'
|
|
18
17
|
import Empty from '../Empty.svelte'
|
|
19
|
-
import IconArrow from '../../Icons/IconArrow.svelte'
|
|
20
18
|
|
|
21
19
|
interface Props {
|
|
22
20
|
searchQuery?: string,
|
|
@@ -24,7 +22,7 @@
|
|
|
24
22
|
navigate?: (key: string) => void
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
const { searchQuery = '', filter = {}
|
|
25
|
+
const { searchQuery = '', filter = {} }: Props = $props()
|
|
28
26
|
|
|
29
27
|
// svelte-ignore non_reactive_update
|
|
30
28
|
let page = 1
|
|
@@ -74,11 +72,16 @@
|
|
|
74
72
|
}
|
|
75
73
|
})
|
|
76
74
|
|
|
77
|
-
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetchTitles(params) || { next: null, previous: null, results: [] }
|
|
78
77
|
|
|
79
|
-
|
|
78
|
+
if (requestId === latestRequestId) titles = [...titles, ...response.results]
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
return response
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
track(TrackingEvent.ExploreResultsError, null, { message: error.message, params })
|
|
83
|
+
throw new Error(error)
|
|
84
|
+
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
async function search(query: string): Promise<void> {
|
|
@@ -121,12 +124,6 @@
|
|
|
121
124
|
track(TrackingEvent.ExploreTitleClick, title)
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
function clearFilter(): void {
|
|
125
|
-
for (const key of Object.keys(filter)) {
|
|
126
|
-
delete filter[key]
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
127
|
function isNewRequest(): boolean {
|
|
131
128
|
const requestAsString = JSON.stringify(filter) + searchQuery
|
|
132
129
|
if (requestAsString === lastRequestAsString) return false
|
|
@@ -136,13 +133,6 @@
|
|
|
136
133
|
}
|
|
137
134
|
</script>
|
|
138
135
|
|
|
139
|
-
{#if useExploreRouter()}
|
|
140
|
-
<Button variant="link" onclick={() => { navigate('home'); clearFilter() }}>
|
|
141
|
-
<IconArrow direction="left" />
|
|
142
|
-
{t('Home')}
|
|
143
|
-
</Button>
|
|
144
|
-
{/if}
|
|
145
|
-
|
|
146
136
|
<div class="titles" class:grid role="main" style:--grid-columns={gridColumns} bind:clientWidth={width} data-testid="explore-results">
|
|
147
137
|
{#each titles as title}
|
|
148
138
|
{#key title.sid}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { isFilterItemActive } from '$lib/filter'
|
|
3
|
+
|
|
4
|
+
describe('filter.ts', () => {
|
|
5
|
+
describe('isFilterItemActive', () => {
|
|
6
|
+
it('Should return false if type is array and value is empty', () => {
|
|
7
|
+
expect(isFilterItemActive({ type: 'array', value: [] })).toBe(false)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('Should return true if type is array and value is not empty', () => {
|
|
11
|
+
expect(isFilterItemActive({ type: 'array', value: ['some-value'] })).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('Should return false if type is range and value matches given range', () => {
|
|
15
|
+
expect(isFilterItemActive({ type: 'range', value: [0, 100] }, [0, 100])).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('Should return true if type is range and value does not match given range', () => {
|
|
19
|
+
expect(isFilterItemActive({ type: 'range', value: [0, 100] }, [0, 50])).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('Should return true if type is range and no range is provided', () => {
|
|
23
|
+
expect(isFilterItemActive({ type: 'range', value: [0, 100] })).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('Should return true if type is range and range is null', () => {
|
|
27
|
+
expect(isFilterItemActive({ type: 'range', value: [0, 100] }, null)).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('Should return true for other types regardless of value', () => {
|
|
31
|
+
expect(isFilterItemActive({ type: 'string', value: 'some-value' })).toBe(true)
|
|
32
|
+
expect(isFilterItemActive({ type: 'string', value: '' })).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { fireEvent, render, waitFor } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import ActiveFilter from '../../../../../routes/components/Explore/Filter/ActiveFilterItems.svelte'
|
|
4
|
+
|
|
5
|
+
vi.mock('$lib/localization', () => ({
|
|
6
|
+
t: vi.fn((key) => key),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
describe('ActiveFilter.svelte', () => {
|
|
10
|
+
const arrayItem = {
|
|
11
|
+
param: 'genres',
|
|
12
|
+
label: 'Genres',
|
|
13
|
+
data: [
|
|
14
|
+
{ label: 'Action', value: 'action' },
|
|
15
|
+
{ label: 'Comedy', value: 'comedy' },
|
|
16
|
+
],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const rangeItem = {
|
|
20
|
+
param: 'year',
|
|
21
|
+
label: 'Year',
|
|
22
|
+
range: [1900, 2025],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stringItem = {
|
|
26
|
+
param: 'ordering',
|
|
27
|
+
label: 'Sort by',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
it('Should not render any buttons if filter is empty', () => {
|
|
31
|
+
const { container } = render(ActiveFilter, { filter: {}, items: [] })
|
|
32
|
+
|
|
33
|
+
expect(container.querySelectorAll('button')).toHaveLength(0)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('Should not render button if filter item is not active', () => {
|
|
37
|
+
const filter = { genres: { type: 'array', value: [] } }
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
const { container } = render(ActiveFilter, { filter, items: [arrayItem] })
|
|
40
|
+
|
|
41
|
+
expect(container.querySelectorAll('button')).toHaveLength(0)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('Should render button for active array filter item', async () => {
|
|
45
|
+
const filter = { genres: { type: 'array', value: ['action'] } }
|
|
46
|
+
// @ts-ignore
|
|
47
|
+
const { findByText } = render(ActiveFilter, { filter, items: [arrayItem] })
|
|
48
|
+
|
|
49
|
+
expect(await findByText('Action')).toBeTruthy()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('Should render joined labels for multiple active array values', async () => {
|
|
53
|
+
const filter = { genres: { type: 'array', value: ['action', 'comedy'] } }
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
const { findByText } = render(ActiveFilter, { filter, items: [arrayItem] })
|
|
56
|
+
|
|
57
|
+
expect(await findByText('Action + Comedy')).toBeTruthy()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('Should render label and range values for active range filter item', () => {
|
|
61
|
+
const filter = { year: { type: 'range', value: [2000, 2020] } }
|
|
62
|
+
// @ts-ignore
|
|
63
|
+
const { getByText } = render(ActiveFilter, { filter, items: [rangeItem] })
|
|
64
|
+
|
|
65
|
+
expect(getByText('Year: 2000 - 2020')).toBeTruthy()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('Should not render button if range value matches the default range', () => {
|
|
69
|
+
const filter = { year: { type: 'range', value: [1900, 2025] } }
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
const { container } = render(ActiveFilter, { filter, items: [rangeItem] })
|
|
72
|
+
|
|
73
|
+
expect(container.querySelectorAll('button')).toHaveLength(0)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('Should remove filter item on button click', async () => {
|
|
77
|
+
const filter = { genres: { type: 'array', value: ['action'] } }
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
const { container } = render(ActiveFilter, { filter, items: [arrayItem] })
|
|
80
|
+
|
|
81
|
+
expect(container.querySelectorAll('button')).toHaveLength(1)
|
|
82
|
+
|
|
83
|
+
await fireEvent.click(/** @type {HTMLButtonElement} */ (container.querySelector('button')))
|
|
84
|
+
|
|
85
|
+
expect(container.querySelectorAll('button')).toHaveLength(0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('Should render buttons for multiple active filter items', async () => {
|
|
89
|
+
const filter = {
|
|
90
|
+
genres: { type: 'array', value: ['action'] },
|
|
91
|
+
year: { type: 'range', value: [2000, 2020] },
|
|
92
|
+
}
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
const { findByText, getByText } = render(ActiveFilter, { filter, items: [arrayItem, rangeItem] })
|
|
95
|
+
|
|
96
|
+
expect(await findByText('Action')).toBeTruthy()
|
|
97
|
+
expect(getByText('Year: 2000 - 2020')).toBeTruthy()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('Should render translated value for active string filter item', () => {
|
|
101
|
+
const filter = { ordering: { type: 'string', value: '-new' } }
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
const { getByText } = render(ActiveFilter, { filter, items: [stringItem] })
|
|
104
|
+
|
|
105
|
+
expect(getByText('-new')).toBeTruthy()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('Should resolve array data from fetchData if provided', async () => {
|
|
109
|
+
const fetchData = vi.fn().mockResolvedValue([{ label: 'Action', value: 'action' }])
|
|
110
|
+
const itemWithFetch = { param: 'genres', label: 'Genres', fetchData }
|
|
111
|
+
const filter = { genres: { type: 'array', value: ['action'] } }
|
|
112
|
+
|
|
113
|
+
// @ts-ignore
|
|
114
|
+
const { findByText } = render(ActiveFilter, { filter, items: [itemWithFetch] })
|
|
115
|
+
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(findByText('Action')).toBeTruthy()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -67,4 +67,32 @@ describe('Filter.svelte', () => {
|
|
|
67
67
|
|
|
68
68
|
expect(container.querySelector('.sorting')).toBeTruthy()
|
|
69
69
|
})
|
|
70
|
+
|
|
71
|
+
it('Should call onempty after filter is removed', async () => {
|
|
72
|
+
const onempty = vi.fn()
|
|
73
|
+
|
|
74
|
+
const { getByText } = render(Filter, { filter: { genres: { type: 'array', value: ['101'] } }, onempty })
|
|
75
|
+
|
|
76
|
+
await fireEvent.click(getByText('Action'))
|
|
77
|
+
|
|
78
|
+
expect(onempty).toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('Should not call onempty if filter is empty on mount', () => {
|
|
82
|
+
const onempty = vi.fn()
|
|
83
|
+
|
|
84
|
+
render(Filter, { filter: {}, onempty })
|
|
85
|
+
|
|
86
|
+
expect(onempty).not.toHaveBeenCalled()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('Should not call onempty if filter is emptied but searchQuery is not', async () => {
|
|
90
|
+
const onempty = vi.fn()
|
|
91
|
+
|
|
92
|
+
const { getByText } = render(Filter, { filter: { genres: { type: 'array', value: ['101'] } }, onempty, searchQuery: 'thing' })
|
|
93
|
+
|
|
94
|
+
await fireEvent.click(getByText('Action'))
|
|
95
|
+
|
|
96
|
+
expect(onempty).not.toHaveBeenCalled()
|
|
97
|
+
})
|
|
70
98
|
})
|