@playpilot/tpi 8.13.0-beta.2 → 8.14.0-beta.1
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 +8 -8
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +9 -9
- package/events.md +0 -5
- package/package.json +2 -2
- package/src/lib/api/titles.ts +10 -0
- package/src/lib/data/translations.ts +4 -4
- package/src/lib/enums/TrackingEvent.ts +0 -3
- package/src/lib/fakeData.ts +3 -3
- package/src/lib/injection.ts +0 -4
- package/src/routes/+layout.svelte +0 -2
- package/src/routes/components/Description.svelte +1 -0
- package/src/routes/components/Explore/ExploreRouter.svelte +26 -1
- package/src/routes/components/Explore/Routes/ExploreResults.svelte +12 -2
- package/src/routes/components/Explore/Routes/ExploreTitle.svelte +94 -0
- package/src/routes/components/Playlinks/Playlinks.svelte +1 -0
- package/src/routes/components/Rails/Rail.svelte +4 -4
- package/src/routes/components/Title.svelte +12 -4
- package/src/tests/lib/api/titles.test.js +23 -1
- package/src/tests/routes/components/Explore/Routes/ExploreTitle.test.js +87 -0
- package/src/lib/inTextWidgets.ts +0 -43
- package/src/routes/components/Widgets/InjectionsWidgetRail.svelte +0 -51
- package/src/tests/lib/inTextWidgets.test.js +0 -160
- package/src/tests/routes/components/Widgets/InjectionsWidgetRail.test.js +0 -28
package/events.md
CHANGED
|
@@ -100,11 +100,6 @@ Event | Action | Info | Payload
|
|
|
100
100
|
`ali_display_ad_playlink_click` | _Fires any time the playlink linked to the display ad is clicked_ | | `campaign_name`
|
|
101
101
|
`ali_ads_fetch_failed` | _Fires whenever ads tried to but failed to fetch_ | | `status` (Response status code)
|
|
102
102
|
|
|
103
|
-
## Widgets
|
|
104
|
-
Event | Action | Info | Payload
|
|
105
|
-
--- | --- | --- | ---
|
|
106
|
-
`ali_widget_article_rail_title_click` | _Fires when a title in an tpi rail widget is clicked_ | | `Title`
|
|
107
|
-
|
|
108
103
|
### Various
|
|
109
104
|
Event | Action | Info | Payload
|
|
110
105
|
--- | --- | --- | ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playpilot/tpi",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.14.0-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"svelte": "5.44.1",
|
|
37
37
|
"svelte-check": "^4.0.0",
|
|
38
38
|
"svelte-preprocess": "^6.0.3",
|
|
39
|
-
"svelte-tiny-slider": "^2.7.
|
|
39
|
+
"svelte-tiny-slider": "^2.7.1",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
41
|
"typescript-eslint": "^8.59.2",
|
|
42
42
|
"vite": "^5.4.21",
|
package/src/lib/api/titles.ts
CHANGED
|
@@ -27,3 +27,13 @@ export async function fetchSimilarTitles(title: TitleData): Promise<TitleData[]>
|
|
|
27
27
|
|
|
28
28
|
return response.results
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
export async function fetchTitleBySid(sid: string): Promise<TitleData> {
|
|
32
|
+
const data = await fetchTitles({ sids: sid })
|
|
33
|
+
|
|
34
|
+
const title = data.results[0]
|
|
35
|
+
|
|
36
|
+
if (!title) throw new Error('No title was returned')
|
|
37
|
+
|
|
38
|
+
return title
|
|
39
|
+
}
|
|
@@ -281,10 +281,10 @@ export const translations = {
|
|
|
281
281
|
[Language.Swedish]: 'Utforska',
|
|
282
282
|
[Language.Danish]: 'Udforsk',
|
|
283
283
|
},
|
|
284
|
-
'
|
|
285
|
-
[Language.English]: '
|
|
286
|
-
[Language.Swedish]: '
|
|
287
|
-
[Language.Danish]: '
|
|
284
|
+
'Page Not Found': {
|
|
285
|
+
[Language.English]: 'Page not found',
|
|
286
|
+
[Language.Swedish]: 'Sidan hittades inte',
|
|
287
|
+
[Language.Danish]: 'Siden blev ikke fundet',
|
|
288
288
|
},
|
|
289
289
|
|
|
290
290
|
// List titles
|
|
@@ -55,9 +55,6 @@ export const TrackingEvent = {
|
|
|
55
55
|
SplitTestView: 'ali_split_test_view',
|
|
56
56
|
SplitTestAction: 'ali_split_test_action',
|
|
57
57
|
|
|
58
|
-
// Widgets
|
|
59
|
-
WidgetArticleRailTitleClick: 'ali_widget_article_rail_title_click',
|
|
60
|
-
|
|
61
58
|
// Various
|
|
62
59
|
ShareTitle: 'ali_share_title',
|
|
63
60
|
SaveTitle: 'ali_save_title',
|
package/src/lib/fakeData.ts
CHANGED
|
@@ -49,7 +49,7 @@ export const linkInjections: LinkInjection[] = [{
|
|
|
49
49
|
sentence: 'In an interview with Epire Magazine, Quan reveals he quested starring in Love Hurts',
|
|
50
50
|
playpilot_url: 'https://playpilot.com/movie/example/',
|
|
51
51
|
key: 'some-key-1',
|
|
52
|
-
title_details:
|
|
52
|
+
title_details: title,
|
|
53
53
|
}, {
|
|
54
54
|
sid: '2',
|
|
55
55
|
title: 'The Long Kiss Goodnight',
|
|
@@ -57,7 +57,7 @@ export const linkInjections: LinkInjection[] = [{
|
|
|
57
57
|
playpilot_url: 'https://playpilot.com/movie/example-2/',
|
|
58
58
|
key: 'some-key-2',
|
|
59
59
|
after_article: false,
|
|
60
|
-
title_details:
|
|
60
|
+
title_details: title,
|
|
61
61
|
}, {
|
|
62
62
|
sid: '3',
|
|
63
63
|
title: 'Nobody',
|
|
@@ -65,7 +65,7 @@ export const linkInjections: LinkInjection[] = [{
|
|
|
65
65
|
playpilot_url: 'https://playpilot.com/movie/example-3/',
|
|
66
66
|
key: 'some-key-3',
|
|
67
67
|
after_article: true,
|
|
68
|
-
title_details:
|
|
68
|
+
title_details: title,
|
|
69
69
|
manual: false,
|
|
70
70
|
}, {
|
|
71
71
|
sid: '4',
|
package/src/lib/injection.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { destroyAllModals, openModalForInjectedLink } from './modal'
|
|
|
5
5
|
import { clearCurrentlyHoveredInjection, destroyLinkPopover, destroyLinkPopoverOnMouseleave, isPopoverActive, openPopoverForInjectedLink } from './popover'
|
|
6
6
|
import { clearAfterArticlePlaylinks, insertAfterArticlePlaylinks } from './afterArticle'
|
|
7
7
|
import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
|
|
8
|
-
import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
|
|
9
8
|
|
|
10
9
|
export const keyDataAttribute = 'data-playpilot-injection-key'
|
|
11
10
|
export const keySelector = `[${keyDataAttribute}]`
|
|
@@ -169,8 +168,6 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
169
168
|
// The function itself will decide whether or not it should actually insert the component based on the config.
|
|
170
169
|
if (document.querySelector(keySelector)) insertInTextDisclaimer(elements)
|
|
171
170
|
|
|
172
|
-
insertInTextWidgets(foundInjections)
|
|
173
|
-
|
|
174
171
|
return mergedInjections.filter(i => i.title_details).map((injection, index) => {
|
|
175
172
|
const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
|
|
176
173
|
const duplicate = injection.duplicate ?? hasManualEquivalent
|
|
@@ -336,7 +333,6 @@ export function clearLinkInjections(): void {
|
|
|
336
333
|
|
|
337
334
|
clearAfterArticlePlaylinks()
|
|
338
335
|
clearInTextDisclaimer()
|
|
339
|
-
clearInTextWidgets()
|
|
340
336
|
destroyAllModals(false)
|
|
341
337
|
destroyLinkPopover(false)
|
|
342
338
|
}
|
|
@@ -70,8 +70,6 @@
|
|
|
70
70
|
<p>De tre ’Jurassic World’-film kunne have givet indtryk af, at der ikke skal meget til, før dinosaurer igen ville kunne dominere kloden. Men det har vist sig ikke at holde stik.</p>
|
|
71
71
|
<p>Het komt elk jaar voor dat films met torenhoge budgetten flink floppen aan de box-office, ongeacht de kwaliteit van de film. Dit is het geval bij een aantal films die dit jaar zijn uitgekomen. Denk aan Mickey 17, Black Bag en de onlangs uitgebrachte animatiefilm Elio. In 2015 was de superheldenfilm Fantastic Four een van de grootste flops.</p>
|
|
72
72
|
|
|
73
|
-
<div data-playpilot-widget="tpi-rail"></div>
|
|
74
|
-
|
|
75
73
|
<h2>A matching link is already present</h2>
|
|
76
74
|
<p>Following their post-credits scene in <a href="/">John Wick</a>, in a new John Wick spinoff.</p>
|
|
77
75
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import ExploreHome from './Routes/ExploreHome.svelte'
|
|
8
8
|
import ExploreResults from './Routes/ExploreResults.svelte'
|
|
9
9
|
import ExploreLayout from './ExploreLayout.svelte'
|
|
10
|
+
import ExploreTitle from './Routes/ExploreTitle.svelte'
|
|
10
11
|
|
|
11
12
|
const routes: ExploreRoute[] = [
|
|
12
13
|
{
|
|
@@ -20,9 +21,16 @@
|
|
|
20
21
|
key: 'home',
|
|
21
22
|
component: ExploreHome,
|
|
22
23
|
})
|
|
24
|
+
|
|
25
|
+
routes.push({
|
|
26
|
+
key: 'title',
|
|
27
|
+
component: ExploreTitle,
|
|
28
|
+
})
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
const initialRouteKey = getCurrentRouteParam() || routes[0].key
|
|
32
|
+
|
|
33
|
+
let currentRoute: ExploreRoute = $state(routes.find(({ key }) => key === initialRouteKey) || routes[0])
|
|
26
34
|
let searchQuery: string = $state('')
|
|
27
35
|
let filter: ExploreFilter = $state({})
|
|
28
36
|
|
|
@@ -35,10 +43,27 @@
|
|
|
35
43
|
function navigate(key: string): void {
|
|
36
44
|
currentRoute = routes.find(route => route.key === key) || routes[0]
|
|
37
45
|
|
|
46
|
+
const currentUrl = new URL(document.location.toString())
|
|
47
|
+
|
|
48
|
+
if (key === routes[0].key) currentUrl.searchParams.delete('route')
|
|
49
|
+
else currentUrl.searchParams.set('route', currentRoute.key)
|
|
50
|
+
|
|
51
|
+
history.pushState({}, '', currentUrl)
|
|
52
|
+
|
|
38
53
|
track(TrackingEvent.ExploreNavigate, null, { route: currentRoute.key })
|
|
39
54
|
}
|
|
55
|
+
|
|
56
|
+
function onhashchange(): void {
|
|
57
|
+
navigate(getCurrentRouteParam())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getCurrentRouteParam(): string {
|
|
61
|
+
return new URL(document.location.toString()).searchParams.get('route') || routes[0].key
|
|
62
|
+
}
|
|
40
63
|
</script>
|
|
41
64
|
|
|
65
|
+
<svelte:window on:popstate={onhashchange} />
|
|
66
|
+
|
|
42
67
|
<ExploreLayout {navigate} bind:searchQuery bind:filter>
|
|
43
68
|
<CurrentRouteComponent {searchQuery} {filter} {navigate} />
|
|
44
69
|
</ExploreLayout>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte'
|
|
2
3
|
import { fetchTitles } from '$lib/api/titles'
|
|
3
4
|
import { MetaEvent, TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
4
5
|
import { openModal } from '$lib/modal'
|
|
@@ -18,8 +19,7 @@
|
|
|
18
19
|
|
|
19
20
|
interface Props {
|
|
20
21
|
searchQuery?: string,
|
|
21
|
-
filter?: ExploreFilter
|
|
22
|
-
navigate?: (key: string) => void
|
|
22
|
+
filter?: ExploreFilter
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const { searchQuery = '', filter = {} }: Props = $props()
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
if (filter) setFilter()
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
+
onDestroy(() => {
|
|
55
|
+
emptyFilter()
|
|
56
|
+
})
|
|
57
|
+
|
|
54
58
|
async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
|
|
55
59
|
latestRequestId += 1
|
|
56
60
|
const requestId = latestRequestId
|
|
@@ -108,6 +112,12 @@
|
|
|
108
112
|
promise = getTitlesForFilter()
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
function emptyFilter(): void {
|
|
116
|
+
for (const key of Object.keys(filter)) {
|
|
117
|
+
delete filter[key]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
111
121
|
function resetTitles(): void {
|
|
112
122
|
page = 1
|
|
113
123
|
titles = []
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fetchTitleBySid } from '$lib/api/titles'
|
|
3
|
+
import { mobileBreakpoint } from '$lib/constants'
|
|
4
|
+
import { t } from '$lib/localization'
|
|
5
|
+
import Button from '../../Button.svelte'
|
|
6
|
+
import IconArrow from '../../Icons/IconArrow.svelte'
|
|
7
|
+
import Title from '../../Title.svelte'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
navigate?: (key: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { navigate = () => null }: Props = $props()
|
|
14
|
+
|
|
15
|
+
let screenWidth = $state(window.innerWidth)
|
|
16
|
+
|
|
17
|
+
const isMobile = $derived(screenWidth < mobileBreakpoint)
|
|
18
|
+
const sid = new URL(document.location.toString()).searchParams.get('sid')
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<svelte:window bind:innerWidth={screenWidth} />
|
|
22
|
+
|
|
23
|
+
{#snippet empty()}
|
|
24
|
+
{@render back()}
|
|
25
|
+
|
|
26
|
+
<p class="empty">{t('Page Not Found')}</p>
|
|
27
|
+
{/snippet}
|
|
28
|
+
|
|
29
|
+
{#snippet back(offset = false)}
|
|
30
|
+
<div class="back" class:offset>
|
|
31
|
+
<Button variant="link" onclick={() => { navigate('home') }}>
|
|
32
|
+
<IconArrow direction="left" />
|
|
33
|
+
{t('Home')}
|
|
34
|
+
</Button>
|
|
35
|
+
</div>
|
|
36
|
+
{/snippet}
|
|
37
|
+
|
|
38
|
+
{#if sid}
|
|
39
|
+
{#await fetchTitleBySid(sid)}
|
|
40
|
+
{@render back()}
|
|
41
|
+
|
|
42
|
+
Loading...
|
|
43
|
+
{:then title}
|
|
44
|
+
{@render back(true)}
|
|
45
|
+
|
|
46
|
+
<div class="title" data-testid="title">
|
|
47
|
+
<Title {title} useVideoBackground={isMobile} />
|
|
48
|
+
</div>
|
|
49
|
+
{:catch}
|
|
50
|
+
{@render empty()}
|
|
51
|
+
{/await}
|
|
52
|
+
{:else}
|
|
53
|
+
{@render empty()}
|
|
54
|
+
{/if}
|
|
55
|
+
|
|
56
|
+
<style lang="scss">
|
|
57
|
+
.title {
|
|
58
|
+
--playpilot-description-max-width: 600px;
|
|
59
|
+
--playpilot-playlinks-max-width: 600px;
|
|
60
|
+
position: relative;
|
|
61
|
+
border-radius: theme(border-radius);
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
margin-top: margin(1);
|
|
64
|
+
|
|
65
|
+
@include desktop() {
|
|
66
|
+
--playpilot-detail-header-offset: #{margin(5)};
|
|
67
|
+
--playpilot-detail-background-height: #{margin(35)};
|
|
68
|
+
--playpilot-detail-background-opacity: 0.5;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.back {
|
|
73
|
+
--playpilot-button-text-color: white;
|
|
74
|
+
font-weight: theme(font-bold);
|
|
75
|
+
|
|
76
|
+
&.offset {
|
|
77
|
+
z-index: 1;
|
|
78
|
+
position: absolute;
|
|
79
|
+
margin-left: margin(1);
|
|
80
|
+
margin-top: margin(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.empty {
|
|
85
|
+
margin-top: margin(1);
|
|
86
|
+
padding: margin(1);
|
|
87
|
+
border: 1px solid theme(content);
|
|
88
|
+
max-width: margin(20);
|
|
89
|
+
border-radius: theme(border-radius);
|
|
90
|
+
font-size: theme(font-size-large);
|
|
91
|
+
font-weight: theme(font-bold);
|
|
92
|
+
color: theme(text-color);
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -58,15 +58,15 @@
|
|
|
58
58
|
.rail {
|
|
59
59
|
--gap: var(--rail-gap, #{margin(0.5)});
|
|
60
60
|
position: relative;
|
|
61
|
-
width: calc(100% +
|
|
62
|
-
margin: 0
|
|
61
|
+
width: calc(100% + margin(2));
|
|
62
|
+
margin: 0 margin(-1);
|
|
63
63
|
|
|
64
64
|
:global(.slider) {
|
|
65
|
-
padding: 0
|
|
65
|
+
padding: 0 margin(1);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
:global(.slider-content > :last-child) {
|
|
69
|
-
margin-right:
|
|
69
|
+
margin-right: margin(2);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
|
|
136
136
|
.content {
|
|
137
137
|
position: relative;
|
|
138
|
-
padding: margin(1);
|
|
138
|
+
padding: theme(detail-padding, margin(1));
|
|
139
139
|
color: theme(detail-text-color, text-color);
|
|
140
140
|
font-family: theme(detail-font-family, font-family);
|
|
141
141
|
font-weight: theme(detail-font-weight, normal);
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
|
|
173
173
|
@include desktop {
|
|
174
174
|
display: block;
|
|
175
|
-
padding-top: margin(3);
|
|
175
|
+
padding-top: theme(title-header-offset, margin(3));
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
.small & {
|
|
@@ -217,6 +217,11 @@
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
.main {
|
|
221
|
+
z-index: 1;
|
|
222
|
+
position: relative;
|
|
223
|
+
}
|
|
224
|
+
|
|
220
225
|
.imdb {
|
|
221
226
|
display: flex;
|
|
222
227
|
align-items: center;
|
|
@@ -234,6 +239,8 @@
|
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
.actions {
|
|
242
|
+
z-index: 1;
|
|
243
|
+
position: relative;
|
|
237
244
|
grid-area: actions;
|
|
238
245
|
display: flex;
|
|
239
246
|
gap: margin(0.5);
|
|
@@ -245,14 +252,15 @@
|
|
|
245
252
|
top: 0;
|
|
246
253
|
left: 0;
|
|
247
254
|
width: 100%;
|
|
248
|
-
height: margin(20);
|
|
255
|
+
height: theme(detail-background-height, margin(20));
|
|
249
256
|
border-radius: theme(detail-background-border-radius, 0px);
|
|
250
257
|
overflow: hidden;
|
|
251
258
|
background: theme(detail-background, lighter);
|
|
252
259
|
mask-image: linear-gradient(to bottom, black 40%, transparent);
|
|
260
|
+
opacity: theme(detail-background-opacity, 1);
|
|
253
261
|
|
|
254
262
|
@include desktop() {
|
|
255
|
-
height: margin(12);
|
|
263
|
+
height: theme(detail-background-height, margin(12));
|
|
256
264
|
mask-image: linear-gradient(to bottom, black 60%, transparent);
|
|
257
265
|
}
|
|
258
266
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import { api } from '$lib/api/api'
|
|
4
|
-
import { fetchSimilarTitles, fetchTitles } from '$lib/api/titles'
|
|
4
|
+
import { fetchSimilarTitles, fetchTitleBySid, fetchTitles } from '$lib/api/titles'
|
|
5
5
|
import { title } from '$lib/fakeData'
|
|
6
6
|
import { getApiToken } from '$lib/token'
|
|
7
7
|
import { fakeFetch } from '../../helpers'
|
|
@@ -74,4 +74,26 @@ describe('$lib/api/titles', () => {
|
|
|
74
74
|
await expect(async () => await fetchSimilarTitles(title)).rejects.toThrow()
|
|
75
75
|
})
|
|
76
76
|
})
|
|
77
|
+
|
|
78
|
+
describe('fetchTitleBySid', () => {
|
|
79
|
+
it('Should call api with given sid and return the first result', async () => {
|
|
80
|
+
vi.mocked(api).mockResolvedValueOnce({ results: [title] })
|
|
81
|
+
|
|
82
|
+
const response = await fetchTitleBySid(title.sid)
|
|
83
|
+
|
|
84
|
+
expect(api).toHaveBeenCalledWith(`/titles/browse?api-token=some-token&sids=${title.sid}&language=en-US&include_count=false`)
|
|
85
|
+
expect(response).toEqual(title)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('Should throw when no title is returned', async () => {
|
|
89
|
+
vi.mocked(api).mockResolvedValueOnce({ results: [] })
|
|
90
|
+
|
|
91
|
+
await expect(async () => await fetchTitleBySid(title.sid)).rejects.toThrow('No title was returned')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('Should throw when api returns error', async () => {
|
|
95
|
+
vi.mocked(api).mockRejectedValueOnce({ error: 'message' })
|
|
96
|
+
await expect(async () => await fetchTitleBySid(title.sid)).rejects.toThrow()
|
|
97
|
+
})
|
|
98
|
+
})
|
|
77
99
|
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { render, waitFor, fireEvent } from '@testing-library/svelte'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import TitleDetail from '../../../../../routes/components/Explore/Routes/ExploreTitle.svelte'
|
|
5
|
+
import { fetchTitleBySid } from '$lib/api/titles'
|
|
6
|
+
import { title } from '$lib/fakeData'
|
|
7
|
+
|
|
8
|
+
vi.mock('$lib/api/titles', () => ({
|
|
9
|
+
fetchTitleBySid: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('/src/routes/components/Title.svelte', () => ({
|
|
13
|
+
default: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('ExploreTitle.svelte', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.resetAllMocks()
|
|
19
|
+
vi.mocked(fetchTitleBySid).mockResolvedValue(title)
|
|
20
|
+
|
|
21
|
+
history.pushState({}, '', '/')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('Should render empty state when no sid is in the URL', async () => {
|
|
25
|
+
const { getByText } = render(TitleDetail)
|
|
26
|
+
|
|
27
|
+
await waitFor(() => {
|
|
28
|
+
expect(getByText('Page not found')).toBeTruthy()
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('Should call fetchTitleBySid with sid from URL', async () => {
|
|
33
|
+
history.pushState({}, '', '?sid=some-sid')
|
|
34
|
+
|
|
35
|
+
render(TitleDetail)
|
|
36
|
+
|
|
37
|
+
expect(fetchTitleBySid).toHaveBeenCalledWith('some-sid')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('Should render loading state while fetching', async () => {
|
|
41
|
+
history.pushState({}, '', '?sid=some-sid')
|
|
42
|
+
vi.mocked(fetchTitleBySid).mockReturnValue(new Promise(() => {}))
|
|
43
|
+
|
|
44
|
+
const { getByText } = render(TitleDetail)
|
|
45
|
+
|
|
46
|
+
expect(getByText('Loading...')).toBeTruthy()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('Should render the title when fetchTitleBySid resolves', async () => {
|
|
50
|
+
history.pushState({}, '', '?sid=some-sid')
|
|
51
|
+
|
|
52
|
+
const { getByTestId } = render(TitleDetail)
|
|
53
|
+
|
|
54
|
+
await waitFor(() => {
|
|
55
|
+
expect(getByTestId('title')).toBeTruthy()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('Should render empty state when fetchTitleBySid rejects', async () => {
|
|
60
|
+
history.pushState({}, '', '?sid=some-sid')
|
|
61
|
+
vi.mocked(fetchTitleBySid).mockRejectedValue(new Error('Not found'))
|
|
62
|
+
|
|
63
|
+
const { getByText } = render(TitleDetail)
|
|
64
|
+
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
expect(getByText('Page not found')).toBeTruthy()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('Should call navigate with "home" when back button is clicked', async () => {
|
|
71
|
+
history.pushState({}, '', '?sid=some-sid')
|
|
72
|
+
|
|
73
|
+
const navigate = vi.fn()
|
|
74
|
+
|
|
75
|
+
const { getByText } = render(TitleDetail, { navigate })
|
|
76
|
+
|
|
77
|
+
await fireEvent.click(getByText('Home'))
|
|
78
|
+
|
|
79
|
+
expect(navigate).toHaveBeenCalledWith('home')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('Should not call fetchTitleBySid when no sid is in the URL', async () => {
|
|
83
|
+
render(TitleDetail)
|
|
84
|
+
|
|
85
|
+
expect(fetchTitleBySid).not.toHaveBeenCalled()
|
|
86
|
+
})
|
|
87
|
+
})
|
package/src/lib/inTextWidgets.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { mount, unmount } from 'svelte'
|
|
2
|
-
import type { LinkInjection } from './types/injection'
|
|
3
|
-
import InjectionsWidgetRail from '../routes/components/Widgets/InjectionsWidgetRail.svelte'
|
|
4
|
-
|
|
5
|
-
const widgets: Record<string, any> = {
|
|
6
|
-
'tpi-rail': InjectionsWidgetRail,
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const inTextWidgetSelector = '[data-playpilot-widget]'
|
|
10
|
-
|
|
11
|
-
export let inTextWidgetInsertedComponents: any[] = []
|
|
12
|
-
|
|
13
|
-
export function insertInTextWidgets(linkInjections: LinkInjection[]): void {
|
|
14
|
-
clearInTextWidgets()
|
|
15
|
-
|
|
16
|
-
if (!linkInjections.length) return
|
|
17
|
-
|
|
18
|
-
const targets = document.querySelectorAll<HTMLElement>(inTextWidgetSelector)
|
|
19
|
-
|
|
20
|
-
targets.forEach(target => {
|
|
21
|
-
const widget = target.dataset.playpilotWidget || ''
|
|
22
|
-
|
|
23
|
-
const component = widgets[widget]
|
|
24
|
-
|
|
25
|
-
if (!component) return
|
|
26
|
-
|
|
27
|
-
const insertedComponent = mount(component, {
|
|
28
|
-
target,
|
|
29
|
-
props: {
|
|
30
|
-
linkInjections,
|
|
31
|
-
},
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
inTextWidgetInsertedComponents.push(insertedComponent)
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function clearInTextWidgets(): void {
|
|
39
|
-
inTextWidgetInsertedComponents.forEach(component => unmount(component))
|
|
40
|
-
document.querySelectorAll('[data-playpilot-widget]').forEach(element => element.innerHTML = '')
|
|
41
|
-
|
|
42
|
-
inTextWidgetInsertedComponents = []
|
|
43
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
|
-
import { t } from '$lib/localization'
|
|
4
|
-
import { track } from '$lib/tracking'
|
|
5
|
-
import type { LinkInjection } from '$lib/types/injection'
|
|
6
|
-
import type { TitleData } from '$lib/types/title'
|
|
7
|
-
import TitlesRail from '../Rails/TitlesRail.svelte'
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
linkInjections: LinkInjection[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const { linkInjections }: Props = $props()
|
|
14
|
-
|
|
15
|
-
let element: HTMLElement | null = $state(null)
|
|
16
|
-
|
|
17
|
-
// @ts-ignore
|
|
18
|
-
const heading = $derived(element?.parentNode?.dataset.heading || t('Mentioned In This Article'))
|
|
19
|
-
|
|
20
|
-
const titles: TitleData[] = $derived.by(() => {
|
|
21
|
-
const uniqueTitles: TitleData[] = []
|
|
22
|
-
|
|
23
|
-
linkInjections.forEach(injection => {
|
|
24
|
-
const title = injection.title_details!
|
|
25
|
-
|
|
26
|
-
if (!title) return
|
|
27
|
-
if (uniqueTitles.some(t => t.sid === title.sid)) return
|
|
28
|
-
|
|
29
|
-
uniqueTitles.push(title)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
return uniqueTitles
|
|
33
|
-
})
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
{#if titles.length}
|
|
37
|
-
<div class="widget" bind:this={element} data-testid="widget">
|
|
38
|
-
<TitlesRail {titles} {heading} onclick={(title) => track(TrackingEvent.WidgetArticleRailTitleClick, title)} />
|
|
39
|
-
</div>
|
|
40
|
-
{/if}
|
|
41
|
-
|
|
42
|
-
<style lang="scss">
|
|
43
|
-
.widget {
|
|
44
|
-
--rail-margin: 0;
|
|
45
|
-
--playpilot-rails-arrow-background: black;
|
|
46
|
-
--playpilot-detail-background-light: black;
|
|
47
|
-
--playpilot-rail-title-text-color: currentColor;
|
|
48
|
-
--playpilot-rail-text-color: currentColor;
|
|
49
|
-
margin: margin(1) 0;
|
|
50
|
-
}
|
|
51
|
-
</style>
|