@playpilot/tpi 8.13.0-beta.2 → 8.14.0-beta.2

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.
Files changed (30) hide show
  1. package/dist/editorial.mount.js +9 -9
  2. package/dist/link-injections.js +1 -1
  3. package/dist/mount.js +9 -9
  4. package/events.md +0 -5
  5. package/package.json +2 -2
  6. package/src/lib/api/titles.ts +10 -0
  7. package/src/lib/data/translations.ts +4 -4
  8. package/src/lib/enums/TrackingEvent.ts +0 -3
  9. package/src/lib/fakeData.ts +3 -3
  10. package/src/lib/injection.ts +13 -7
  11. package/src/lib/routes.ts +8 -0
  12. package/src/lib/types/config.d.ts +5 -0
  13. package/src/routes/+layout.svelte +0 -2
  14. package/src/routes/components/Debugger.svelte +5 -0
  15. package/src/routes/components/Description.svelte +1 -0
  16. package/src/routes/components/Explore/ExploreRouter.svelte +58 -2
  17. package/src/routes/components/Explore/Routes/ExploreResults.svelte +12 -2
  18. package/src/routes/components/Explore/Routes/ExploreTitle.svelte +94 -0
  19. package/src/routes/components/Modals/RailModal.svelte +6 -3
  20. package/src/routes/components/Playlinks/Playlinks.svelte +1 -0
  21. package/src/routes/components/Rails/Rail.svelte +4 -4
  22. package/src/routes/components/Title.svelte +12 -4
  23. package/src/tests/lib/api/titles.test.js +23 -1
  24. package/src/tests/lib/injection.test.js +44 -3
  25. package/src/tests/lib/routes.test.js +14 -2
  26. package/src/tests/routes/components/Explore/Routes/ExploreTitle.test.js +87 -0
  27. package/src/lib/inTextWidgets.ts +0 -43
  28. package/src/routes/components/Widgets/InjectionsWidgetRail.svelte +0 -51
  29. package/src/tests/lib/inTextWidgets.test.js +0 -160
  30. package/src/tests/routes/components/Widgets/InjectionsWidgetRail.test.js +0 -28
@@ -1,9 +1,10 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { titleUrl } from '$lib/routes'
2
+ import { exploreTitleUrl, titleUrl } from '$lib/routes'
3
3
  import { playPilotBaseUrl } from '$lib/constants'
4
+ import { title } from '$lib/fakeData'
4
5
 
5
6
  describe('$lib/routes', () => {
6
- describe('mergePlaylinks', () => {
7
+ describe('titleUrl', () => {
7
8
  it('Should return url for given title', () => {
8
9
  // @ts-ignore
9
10
  expect(titleUrl({ type: 'series', slug: 'some-slug' })).toBe(`${playPilotBaseUrl}/series/some-slug/`)
@@ -12,4 +13,15 @@ describe('$lib/routes', () => {
12
13
  expect(titleUrl({ type: 'movie', slug: 'some-other-slug' })).toBe(`${playPilotBaseUrl}/movie/some-other-slug/`)
13
14
  })
14
15
  })
16
+
17
+ describe('exploreTitleUrl', () => {
18
+ it('Should return url for given title', () => {
19
+ window.PlayPilotLinkInjections.config = {
20
+ open_tpi_links_in_explore: true,
21
+ explore_navigation_path: 'https://some-path.com/explore',
22
+ }
23
+
24
+ expect(exploreTitleUrl(title)).toBe(`https://some-path.com/explore?route=title&sid=${title.sid}`)
25
+ })
26
+ })
15
27
  })
@@ -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
+ })
@@ -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>
@@ -1,160 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest'
2
- import { mount, unmount } from 'svelte'
3
- import { generateInjection } from '../helpers'
4
- import { insertInTextWidgets, clearInTextWidgets, inTextWidgetInsertedComponents } from '$lib/inTextWidgets'
5
-
6
- vi.mock('svelte', () => ({
7
- mount: vi.fn(),
8
- unmount: vi.fn(),
9
- }))
10
-
11
- describe('inTextWidgets.ts', () => {
12
- beforeEach(() => {
13
- vi.resetAllMocks()
14
- document.body.innerHTML = ''
15
-
16
- clearInTextWidgets()
17
- })
18
-
19
- describe('insertInTextWidgets', () => {
20
- it('Should not mount any component if no linkInjections are given', () => {
21
- document.body.innerHTML = '<div data-playpilot-widget="tpi-rail"></div>'
22
-
23
- insertInTextWidgets([])
24
-
25
- expect(mount).not.toHaveBeenCalled()
26
- })
27
-
28
- it('Should not mount any component if no widget targets exist', () => {
29
- document.body.innerHTML = '<div>No widgets here</div>'
30
-
31
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
32
-
33
- insertInTextWidgets([injection])
34
-
35
- expect(mount).not.toHaveBeenCalled()
36
- })
37
-
38
- it('Should mount a component for a known widget type', () => {
39
- document.body.innerHTML = '<div data-playpilot-widget="tpi-rail"></div>'
40
-
41
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
42
-
43
- insertInTextWidgets([injection])
44
-
45
- expect(mount).toHaveBeenCalledOnce()
46
- expect(mount).toHaveBeenCalledWith(
47
- expect.anything(),
48
- expect.objectContaining({
49
- target: document.querySelector('[data-playpilot-widget="tpi-rail"]'),
50
- props: { linkInjections: [injection] },
51
- }),
52
- )
53
- })
54
-
55
- it('Should not mount a component for an unknown widget type', () => {
56
- document.body.innerHTML = '<div data-playpilot-widget="unknown-widget"></div>'
57
-
58
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
59
-
60
- insertInTextWidgets([injection])
61
-
62
- expect(mount).not.toHaveBeenCalled()
63
- })
64
-
65
- it('Should mount components for multiple widget targets on the same page', () => {
66
- document.body.innerHTML = `
67
- <div data-playpilot-widget="tpi-rail"></div>
68
- <div data-playpilot-widget="tpi-rail"></div>
69
- `
70
-
71
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
72
-
73
- insertInTextWidgets([injection])
74
-
75
- expect(mount).toHaveBeenCalledTimes(2)
76
- })
77
-
78
- it('Should skip unknown widget targets and mount known ones', () => {
79
- document.body.innerHTML = `
80
- <div data-playpilot-widget="tpi-rail"></div>
81
- <div data-playpilot-widget="unknown-widget"></div>
82
- `
83
-
84
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
85
-
86
- insertInTextWidgets([injection])
87
-
88
- expect(mount).toHaveBeenCalledOnce()
89
- })
90
-
91
- it('Should track inserted components', () => {
92
- document.body.innerHTML = `
93
- <div data-playpilot-widget="tpi-rail"></div>
94
- <div data-playpilot-widget="tpi-rail"></div>
95
- `
96
-
97
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
98
-
99
- insertInTextWidgets([injection])
100
-
101
- expect(inTextWidgetInsertedComponents).toHaveLength(2)
102
- })
103
-
104
- it('Should clear previously inserted widgets before inserting new ones', () => {
105
- document.body.innerHTML = '<div data-playpilot-widget="tpi-rail"></div>'
106
-
107
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
108
-
109
- insertInTextWidgets([injection])
110
- insertInTextWidgets([injection])
111
-
112
- expect(unmount).toHaveBeenCalled()
113
- })
114
- })
115
-
116
- describe('clearInTextWidgets', () => {
117
- it('Should unmount all inserted components', () => {
118
- document.body.innerHTML = `
119
- <div data-playpilot-widget="tpi-rail"></div>
120
- <div data-playpilot-widget="tpi-rail"></div>
121
- `
122
-
123
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
124
- insertInTextWidgets([injection])
125
-
126
- clearInTextWidgets()
127
-
128
- expect(unmount).toHaveBeenCalled()
129
- })
130
-
131
- it('Should clear innerHTML of all widget elements', () => {
132
- document.body.innerHTML = `
133
- <div data-playpilot-widget="tpi-rail"><span>leftover content</span></div>
134
- `
135
-
136
- clearInTextWidgets()
137
-
138
- expect(/** @type {HTMLElement} */ (document.querySelector('[data-playpilot-widget]')).innerHTML).toBe('')
139
- })
140
-
141
- it('Should reset inTextWidgetInsertedComponents to empty array', () => {
142
- document.body.innerHTML = '<div data-playpilot-widget="tpi-rail"></div>'
143
-
144
- const injection = generateInjection('This is a sentence with an injection.', 'an injection')
145
- insertInTextWidgets([injection])
146
-
147
- expect(inTextWidgetInsertedComponents.length).toBe(1)
148
-
149
- clearInTextWidgets()
150
-
151
- expect(inTextWidgetInsertedComponents.length).toBe(0)
152
- })
153
-
154
- it('Should do nothing if no components are present', () => {
155
- clearInTextWidgets()
156
-
157
- expect(unmount).not.toHaveBeenCalled()
158
- })
159
- })
160
- })
@@ -1,28 +0,0 @@
1
- import { render } from '@testing-library/svelte'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import InjectionsWidgetRail from '../../../../routes/components/Widgets/InjectionsWidgetRail.svelte'
5
- import { linkInjections } from '$lib/fakeData'
6
-
7
- describe('InjectionsWidgetRail.svelte', () => {
8
- it('Should render unique titles for given injections', () => {
9
- const injections = [linkInjections[0], linkInjections[0], linkInjections[1], linkInjections[2], linkInjections[2]]
10
-
11
- const { getAllByTestId } = render(InjectionsWidgetRail, { linkInjections: injections })
12
-
13
- expect(getAllByTestId('title')).toHaveLength(3)
14
- })
15
-
16
- it('Should not render if no injections were given', () => {
17
- const { queryByTestId } = render(InjectionsWidgetRail, { linkInjections: [] })
18
-
19
- expect(queryByTestId('widget')).not.toBeTruthy()
20
- })
21
-
22
- it('Should not render if injections contained no valid titles', () => {
23
- // @ts-ignore
24
- const { queryByTestId } = render(InjectionsWidgetRail, { linkInjections: [{ ...linkInjections, title_details: null }] })
25
-
26
- expect(queryByTestId('widget')).not.toBeTruthy()
27
- })
28
- })