@playpilot/tpi 6.7.0 → 6.8.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/link-injections.js +8 -8
- package/events.md +3 -0
- package/package.json +1 -1
- package/src/lib/enums/SplitTest.ts +5 -0
- package/src/lib/enums/TrackingEvent.ts +1 -0
- package/src/routes/components/Description.svelte +13 -3
- package/src/routes/components/Playlinks/Playlinks.svelte +33 -15
- package/src/routes/components/Share.svelte +2 -2
- package/src/routes/components/Title.svelte +24 -8
- package/src/routes/components/TitleModal.svelte +3 -1
- package/src/routes/components/TitlePopover.svelte +3 -1
- package/src/routes/components/Trailer.svelte +2 -2
- package/src/tests/routes/components/Description.test.js +10 -1
- package/src/tests/routes/components/Playlinks/Playlinks.test.js +1 -2
- package/src/tests/routes/components/Share.test.js +12 -12
- package/src/tests/routes/components/Title.test.js +4 -4
- package/src/tests/routes/components/TitleModal.test.js +1 -1
- package/src/tests/routes/components/TitlePopover.test.js +1 -1
package/events.md
CHANGED
|
@@ -90,6 +90,9 @@ Event | Action | Info | Payload
|
|
|
90
90
|
### Various
|
|
91
91
|
Event | Action | Info | Payload
|
|
92
92
|
--- | --- | --- | ---
|
|
93
|
+
`ali_share_title` | _Fires any time the share button is clicked for a title_ | | `Title`, `method` ('native', 'copy', or 'email')
|
|
94
|
+
`ali_trailer_button_click` | _Fires any time the trailer button is clicked for a title_ | | `Title`
|
|
95
|
+
`ali_expand_title_description` | _Fires any time the "show more" button is clicked for a title description_ | | `Title`
|
|
93
96
|
`ali_region_request_failed` | _Fires when requests to external service for getting the user region fails_ | | `message` (error message as returned by the request)
|
|
94
97
|
|
|
95
98
|
### Explore
|
package/package.json
CHANGED
|
@@ -4,4 +4,9 @@ export const SplitTest = {
|
|
|
4
4
|
numberOfVariants: 2,
|
|
5
5
|
variantNames: ['Separated', 'Inline'] as string[],
|
|
6
6
|
},
|
|
7
|
+
TitleDescriptionPlacement: {
|
|
8
|
+
key: 'title_description_placement',
|
|
9
|
+
numberOfVariants: 2,
|
|
10
|
+
variantNames: ['Top', 'Bottom'] as string[],
|
|
11
|
+
},
|
|
7
12
|
} as const
|
|
@@ -58,6 +58,7 @@ export const TrackingEvent = {
|
|
|
58
58
|
SaveTitle: 'ali_save_title',
|
|
59
59
|
NotifyTitle: 'ali_notify_title',
|
|
60
60
|
TrailerClick: 'ali_trailer_button_click',
|
|
61
|
+
ExpandTitleDescription: 'ali_expand_title_description',
|
|
61
62
|
RegionRequestFailed: 'ali_region_request_failed',
|
|
62
63
|
|
|
63
64
|
// Explore
|
|
@@ -3,21 +3,27 @@
|
|
|
3
3
|
text: string
|
|
4
4
|
blurb?: string
|
|
5
5
|
limit?: number
|
|
6
|
+
onclick?: () => void
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
const { text, blurb = '', limit = 200 }: Props = $props()
|
|
9
|
+
const { text, blurb = '', limit = 200, onclick = () => null }: Props = $props()
|
|
9
10
|
|
|
10
11
|
let expanded = $state(false)
|
|
11
12
|
|
|
12
13
|
const limitedText = $derived(text.slice(0, limit))
|
|
14
|
+
|
|
15
|
+
function expand(): void {
|
|
16
|
+
expanded = true
|
|
17
|
+
onclick()
|
|
18
|
+
}
|
|
13
19
|
</script>
|
|
14
20
|
|
|
15
|
-
<div>
|
|
21
|
+
<div class="description">
|
|
16
22
|
<span class="paragraph" role="paragraph">
|
|
17
23
|
{expanded ? text : limitedText}{#if !expanded && text.length > limit}...{/if}
|
|
18
24
|
|
|
19
25
|
{#if !expanded && (text.length > limit || blurb)}
|
|
20
|
-
<button class="show-more" onclick={() =>
|
|
26
|
+
<button class="show-more" onclick={() => expand()}>Show more</button>
|
|
21
27
|
{/if}
|
|
22
28
|
</span>
|
|
23
29
|
|
|
@@ -27,6 +33,10 @@
|
|
|
27
33
|
</div>
|
|
28
34
|
|
|
29
35
|
<style lang="scss">
|
|
36
|
+
.description:first-child {
|
|
37
|
+
margin: margin(-1) 0 margin(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
.paragraph {
|
|
31
41
|
display: block;
|
|
32
42
|
margin: margin(1) 0 0;
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import { campaignToPlaylink, getFirstAdOfType } from '$lib/api/ads'
|
|
10
10
|
import { getContext } from 'svelte'
|
|
11
11
|
import Playlink from './Playlink.svelte'
|
|
12
|
-
import Notify from '../Notify.svelte'
|
|
13
12
|
|
|
14
13
|
interface Props {
|
|
15
14
|
playlinks: PlaylinkData[]
|
|
@@ -36,27 +35,31 @@
|
|
|
36
35
|
</script>
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
{
|
|
40
|
-
<div class="heading" use:heading={3}>{t('Where To Stream Online')}</div>
|
|
38
|
+
<div class="heading" use:heading={3}>{t('Where To Stream Online')}</div>
|
|
41
39
|
|
|
40
|
+
{#if mergedPlaylinks.length}
|
|
42
41
|
<div class="disclaimer" data-testid="commission-disclaimer">
|
|
43
42
|
{window?.PlayPilotLinkInjections?.config?.playlinks_disclaimer_text || t('Commission Disclaimer')}
|
|
44
43
|
<a href="https://playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>
|
|
45
44
|
</div>
|
|
45
|
+
{/if}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
<div class="playlinks" class:list bind:clientWidth={outerWidth}>
|
|
48
|
+
{#each mergedPlaylinks as playlink, index}
|
|
49
|
+
<Playlink {playlink} onclick={() => onclick(playlink.name)} />
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
{
|
|
51
|
+
<!-- A fake highlighted playlink as part of the display ad, to be shown after the first playlink -->
|
|
52
|
+
{#if displayAd && (index === 0)}
|
|
53
|
+
<Playlink playlink={campaignToPlaylink(displayAd)} onclick={() => track(TrackingEvent.DisplayedAdPlaylickClick, title, { campaign_name: displayAd.campaign_name })} hideCategory disclaimer={displayAd.disclaimer || ''} />
|
|
54
|
+
{/if}
|
|
55
|
+
{/each}
|
|
56
|
+
|
|
57
|
+
{#if !mergedPlaylinks.length}
|
|
58
|
+
<div class="empty" data-testid="playlinks-empty">
|
|
59
|
+
{t('Title Unavailable')}
|
|
60
|
+
</div>
|
|
61
|
+
{/if}
|
|
62
|
+
</div>
|
|
60
63
|
|
|
61
64
|
<style lang="scss">
|
|
62
65
|
.heading {
|
|
@@ -100,4 +103,19 @@
|
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
}
|
|
106
|
+
|
|
107
|
+
.empty {
|
|
108
|
+
grid-column: span 2;
|
|
109
|
+
padding: margin(0.75);
|
|
110
|
+
margin-top: margin(0.5);
|
|
111
|
+
background: theme(playlink-background, lighter);
|
|
112
|
+
box-shadow: theme(playlink-shadow, shadow);
|
|
113
|
+
border-radius: theme(playlink-border-radius, border-radius);
|
|
114
|
+
white-space: initial;
|
|
115
|
+
line-height: 1.35;
|
|
116
|
+
|
|
117
|
+
.list & {
|
|
118
|
+
grid-column: 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
103
121
|
</style>
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
|
|
50
50
|
<ContextMenu>
|
|
51
51
|
{#snippet button({ toggle })}
|
|
52
|
-
<Button onclick={(event) => shareApiOrToggle(event, toggle)}
|
|
53
|
-
<IconShare />
|
|
52
|
+
<Button onclick={(event) => shareApiOrToggle(event, toggle)}>
|
|
53
|
+
<IconShare /> {t('Share')}
|
|
54
54
|
</Button>
|
|
55
55
|
{/snippet}
|
|
56
56
|
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import ParticipantsRail from './Rails/ParticipantsRail.svelte'
|
|
7
7
|
import SimilarRail from './Rails/SimilarRail.svelte'
|
|
8
8
|
import TitlePoster from './TitlePoster.svelte'
|
|
9
|
-
import Save from './Save.svelte'
|
|
10
9
|
import Share from './Share.svelte'
|
|
11
10
|
import Trailer from './Trailer.svelte'
|
|
12
11
|
import { t } from '$lib/localization'
|
|
@@ -14,7 +13,11 @@
|
|
|
14
13
|
import { heading } from '$lib/actions/heading'
|
|
15
14
|
import { removeImageUrlPrefix } from '$lib/image'
|
|
16
15
|
import { titleUrl } from '$lib/routes'
|
|
17
|
-
import {
|
|
16
|
+
import { isInSplitTestVariant, trackSplitTestView } from '$lib/splitTest'
|
|
17
|
+
import { onMount, setContext } from 'svelte'
|
|
18
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
19
|
+
import { track } from '$lib/tracking'
|
|
20
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
18
21
|
|
|
19
22
|
interface Props {
|
|
20
23
|
title: TitleData
|
|
@@ -25,11 +28,23 @@
|
|
|
25
28
|
|
|
26
29
|
setContext('title', title)
|
|
27
30
|
|
|
31
|
+
const showDescription = $derived(!small && title.description)
|
|
32
|
+
|
|
28
33
|
let posterLoaded = $state(false)
|
|
29
34
|
let backgroundLoaded = $state(false)
|
|
30
35
|
let useBackgroundFallback = $state(false)
|
|
36
|
+
|
|
37
|
+
onMount(() => {
|
|
38
|
+
if (showDescription) trackSplitTestView(SplitTest.TitleDescriptionPlacement)
|
|
39
|
+
})
|
|
31
40
|
</script>
|
|
32
41
|
|
|
42
|
+
{#snippet description()}
|
|
43
|
+
{#if showDescription}
|
|
44
|
+
<Description text={title.description!} blurb={title.blurb} onclick={() => track(TrackingEvent.ExpandTitleDescription, title)} />
|
|
45
|
+
{/if}
|
|
46
|
+
{/snippet}
|
|
47
|
+
|
|
33
48
|
<div class="content" class:small>
|
|
34
49
|
<div class="header">
|
|
35
50
|
<div class="poster" class:loaded={posterLoaded}>
|
|
@@ -59,18 +74,19 @@
|
|
|
59
74
|
<Trailer {title} />
|
|
60
75
|
{/if}
|
|
61
76
|
|
|
62
|
-
<
|
|
63
|
-
<Save {title} />
|
|
64
|
-
<Share title={title.title} url={titleUrl(title)} />
|
|
65
|
-
</div>
|
|
77
|
+
<Share title={title.title} url={titleUrl(title)} />
|
|
66
78
|
</div>
|
|
67
79
|
</div>
|
|
68
80
|
|
|
69
81
|
<div class="main">
|
|
82
|
+
{#if isInSplitTestVariant(SplitTest.TitleDescriptionPlacement, 0)}
|
|
83
|
+
{@render description()}
|
|
84
|
+
{/if}
|
|
85
|
+
|
|
70
86
|
<Playlinks playlinks={title.providers} {title} />
|
|
71
87
|
|
|
72
|
-
{#if
|
|
73
|
-
|
|
88
|
+
{#if isInSplitTestVariant(SplitTest.TitleDescriptionPlacement, 1)}
|
|
89
|
+
{@render description()}
|
|
74
90
|
{/if}
|
|
75
91
|
|
|
76
92
|
<ParticipantsRail {title} />
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import TopScroll from './Ads/TopScroll.svelte'
|
|
10
10
|
import Display from './Ads/Display.svelte'
|
|
11
11
|
import ExploreCallToAction from './Explore/ExploreCallToAction.svelte'
|
|
12
|
+
import { getSplitTestVariantName } from '$lib/splitTest'
|
|
13
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
12
14
|
|
|
13
15
|
interface Props {
|
|
14
16
|
title: TitleData
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
onMount(() => {
|
|
29
31
|
const openTimestamp = Date.now()
|
|
30
32
|
|
|
31
|
-
return () => track(TrackingEvent.TitleModalClose, title, { time_spent: Date.now() - openTimestamp })
|
|
33
|
+
return () => track(TrackingEvent.TitleModalClose, title, { time_spent: Date.now() - openTimestamp, split_test: getSplitTestVariantName(SplitTest.TitleDescriptionPlacement) })
|
|
32
34
|
})
|
|
33
35
|
|
|
34
36
|
function onscroll(): void {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import Title from './Title.svelte'
|
|
9
9
|
import TopScroll from './Ads/TopScroll.svelte'
|
|
10
10
|
import Display from './Ads/Display.svelte'
|
|
11
|
+
import { getSplitTestVariantName } from '$lib/splitTest'
|
|
12
|
+
import { SplitTest } from '$lib/enums/SplitTest'
|
|
11
13
|
|
|
12
14
|
interface Props {
|
|
13
15
|
event: MouseEvent
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
setOffset()
|
|
28
30
|
|
|
29
31
|
const openTimestamp = Date.now()
|
|
30
|
-
return () => track(TrackingEvent.TitlePopoverClose, title, { time_spent: Date.now() - openTimestamp })
|
|
32
|
+
return () => track(TrackingEvent.TitlePopoverClose, title, { time_spent: Date.now() - openTimestamp, split_test: getSplitTestVariantName(SplitTest.TitleDescriptionPlacement) })
|
|
31
33
|
})
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fireEvent, render } from '@testing-library/svelte'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import Description from '../../../routes/components/Description.svelte'
|
|
5
5
|
|
|
@@ -55,4 +55,13 @@ describe('Description.svelte', () => {
|
|
|
55
55
|
|
|
56
56
|
expect(queryByText('Show more')).not.toBeTruthy()
|
|
57
57
|
})
|
|
58
|
+
|
|
59
|
+
it('Should fire given onclick function after expanding if given', async () => {
|
|
60
|
+
const onclick = vi.fn()
|
|
61
|
+
const { queryByRole } = render(Description, { text: 'Some test description', limit: 5, onclick })
|
|
62
|
+
|
|
63
|
+
await fireEvent.click(/** @type {Node} **/(queryByRole('button')))
|
|
64
|
+
|
|
65
|
+
expect(onclick).toHaveBeenCalled()
|
|
66
|
+
})
|
|
58
67
|
})
|
|
@@ -68,8 +68,7 @@ describe('Playlinks.svelte', () => {
|
|
|
68
68
|
const playlinks = []
|
|
69
69
|
const { queryByTestId, getByText } = render(Playlinks, { playlinks, title })
|
|
70
70
|
|
|
71
|
-
expect(getByText(
|
|
72
|
-
expect(getByText('Notify me when available')).toBeTruthy()
|
|
71
|
+
expect(getByText('This title is not currently available to stream.')).toBeTruthy()
|
|
73
72
|
expect(queryByTestId('commission-disclaimer')).not.toBeTruthy()
|
|
74
73
|
})
|
|
75
74
|
|
|
@@ -21,57 +21,57 @@ describe('Share.svelte', () => {
|
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
it('Should open context menu on click', async () => {
|
|
24
|
-
const {
|
|
24
|
+
const { getByText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
25
25
|
|
|
26
26
|
expect(queryByText('Copy URL')).not.toBeTruthy()
|
|
27
27
|
expect(queryByText('Email')).not.toBeTruthy()
|
|
28
28
|
|
|
29
|
-
await fireEvent.click(
|
|
29
|
+
await fireEvent.click(getByText('Share'))
|
|
30
30
|
|
|
31
31
|
expect(queryByText('Copy URL')).toBeTruthy()
|
|
32
32
|
expect(queryByText('Email')).toBeTruthy()
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
it('Should close context menu on click of items', async () => {
|
|
36
|
-
const {
|
|
36
|
+
const { getByText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
37
37
|
|
|
38
|
-
await fireEvent.click(
|
|
38
|
+
await fireEvent.click(getByText('Share'))
|
|
39
39
|
await fireEvent.click(getByText('Copy URL'))
|
|
40
40
|
|
|
41
41
|
expect(queryByText('Copy URL')).not.toBeTruthy()
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
it('Should close context menu on click of body', async () => {
|
|
45
|
-
const {
|
|
45
|
+
const { getByText, queryByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
46
46
|
|
|
47
|
-
await fireEvent.click(
|
|
47
|
+
await fireEvent.click(getByText('Share'))
|
|
48
48
|
await fireEvent.click(document.body)
|
|
49
49
|
|
|
50
50
|
expect(queryByText('Copy URL')).not.toBeTruthy()
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
it('Should fire copyToClipboard on click of button', async () => {
|
|
54
|
-
const {
|
|
54
|
+
const { getByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
55
55
|
|
|
56
|
-
await fireEvent.click(
|
|
56
|
+
await fireEvent.click(getByText('Share'))
|
|
57
57
|
await fireEvent.click(getByText('Copy URL'))
|
|
58
58
|
|
|
59
59
|
expect(copyToClipboard).toHaveBeenCalledWith('some-url?utm_source=tpi')
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it('Should fire track function on click of copy URL button', async () => {
|
|
63
|
-
const {
|
|
63
|
+
const { getByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
64
64
|
|
|
65
|
-
await fireEvent.click(
|
|
65
|
+
await fireEvent.click(getByText('Share'))
|
|
66
66
|
await fireEvent.click(getByText('Copy URL'))
|
|
67
67
|
|
|
68
68
|
expect(track).toHaveBeenCalledWith(TrackingEvent.ShareTitle, null, { title: 'Some title', url: 'http://localhost:3000/', method: 'copy' })
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
it('Should fire track function on click of email button', async () => {
|
|
72
|
-
const {
|
|
72
|
+
const { getByText } = render(Share, { title: 'Some title', url: 'some-url' })
|
|
73
73
|
|
|
74
|
-
await fireEvent.click(
|
|
74
|
+
await fireEvent.click(getByText('Share'))
|
|
75
75
|
await fireEvent.click(getByText('Email'))
|
|
76
76
|
|
|
77
77
|
expect(track).toHaveBeenCalledWith(TrackingEvent.ShareTitle, null, { title: 'Some title', url: 'http://localhost:3000/', method: 'email' })
|
|
@@ -94,14 +94,14 @@ describe('Title.svelte', () => {
|
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
it('Should show trailer button when embeddable_url is given', () => {
|
|
97
|
-
const {
|
|
97
|
+
const { getByText } = render(Title, { title: { ...title, embeddable_url: 'some-url' } })
|
|
98
98
|
|
|
99
|
-
expect(
|
|
99
|
+
expect(getByText('Watch trailer')).toBeTruthy()
|
|
100
100
|
})
|
|
101
101
|
|
|
102
102
|
it('Should not show trailer button when embeddable_url is not given', () => {
|
|
103
|
-
const {
|
|
103
|
+
const { queryByText } = render(Title, { title })
|
|
104
104
|
|
|
105
|
-
expect(
|
|
105
|
+
expect(queryByText('Watch trailer')).not.toBeTruthy()
|
|
106
106
|
})
|
|
107
107
|
})
|
|
@@ -56,7 +56,7 @@ describe('TitleModal.svelte', () => {
|
|
|
56
56
|
vi.advanceTimersByTime(200)
|
|
57
57
|
unmount()
|
|
58
58
|
|
|
59
|
-
expect(track).toHaveBeenCalledWith(TrackingEvent.TitleModalClose, title, { time_spent: 200 })
|
|
59
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.TitleModalClose, title, expect.objectContaining({ time_spent: 200 }))
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it('Should render top scroll ad when given', () => {
|
|
@@ -50,7 +50,7 @@ describe('TitlePopover.svelte', () => {
|
|
|
50
50
|
vi.advanceTimersByTime(200)
|
|
51
51
|
unmount()
|
|
52
52
|
|
|
53
|
-
expect(track).toHaveBeenCalledWith(TrackingEvent.TitlePopoverClose, title, { time_spent: 200 })
|
|
53
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.TitlePopoverClose, title, expect.objectContaining({ time_spent: 200 }))
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
it('Should render top scroll ad when given', () => {
|