@brightcove/components-embed-code-modal 1.1.2 → 1.1.3

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 (196) hide show
  1. package/.eslintignore +7 -0
  2. package/.eslintrc +23 -0
  3. package/.github/workflows/ci.yml +23 -0
  4. package/.husky/pre-commit +4 -0
  5. package/.nvmrc +1 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +5 -0
  8. package/.release-it.json +40 -0
  9. package/.whitesource +3 -0
  10. package/CHANGELOG.md +423 -0
  11. package/CODEOWNERS +37 -0
  12. package/PULL_REQUEST_TEMPLATE.md +17 -0
  13. package/__tests__/EmbedCode.test.tsx +51 -0
  14. package/__tests__/InteractivityProject.test.tsx +50 -0
  15. package/__tests__/PlayerPreview.test.tsx +23 -0
  16. package/__tests__/PrismLiveModal.test.tsx +85 -0
  17. package/__tests__/PrismPlayerControls.test.tsx +139 -0
  18. package/__tests__/PrismPlayerPreview.test.tsx +168 -0
  19. package/__tests__/TabsHeader.test.tsx +19 -0
  20. package/__tests__/TabsSection.test.tsx +46 -0
  21. package/__tests__/TitleWithTooltip.test.tsx +45 -0
  22. package/__tests__/VideoPlayer.test.tsx +105 -0
  23. package/__tests__/embedCodeGenerator.test.ts +111 -0
  24. package/__tests__/util.test.ts +89 -0
  25. package/babel.config.json +17 -0
  26. package/build/README.md +109 -0
  27. package/build/__tests__/PrismLiveModal.test.d.ts +2 -0
  28. package/build/__tests__/PrismLiveModal.test.d.ts.map +1 -0
  29. package/build/index.js +9 -0
  30. package/build/index.js.map +1 -0
  31. package/build/package.json +100 -0
  32. package/{src → build/src}/js/components/prism/PrismLiveModal.d.ts.map +1 -1
  33. package/{src → build/src}/js/components/prism/PrismPlayerControls.d.ts +2 -1
  34. package/{src → build/src}/js/components/prism/PrismPlayerControls.d.ts.map +1 -1
  35. package/{src → build/src}/js/components/prism/PrismPlayerPreview.d.ts +2 -1
  36. package/build/src/js/components/prism/PrismPlayerPreview.d.ts.map +1 -0
  37. package/build/src/js/hooks/useIsMobile.d.ts +3 -0
  38. package/build/src/js/hooks/useIsMobile.d.ts.map +1 -0
  39. package/{src → build/src}/js/i18n.d.ts +24 -0
  40. package/{src → build/src}/js/i18n.d.ts.map +1 -1
  41. package/etc/wildcard.brightcove.com.key +27 -0
  42. package/etc/wildcard.brightcove.com.pem +96 -0
  43. package/jest/jest.setup.js +90 -0
  44. package/jest/jest.stub.js +4 -0
  45. package/jest.config.js +24 -0
  46. package/package.json +17 -7
  47. package/scripts/create-dist-pkg-prod.js +19 -0
  48. package/src/index.ts +17 -0
  49. package/src/js/EmbedCodeModal.tsx +105 -0
  50. package/src/js/components/CustomSettingsDropdown.tsx +184 -0
  51. package/src/js/components/EmbedCopySection.tsx +130 -0
  52. package/src/js/components/ModalContainer.tsx +367 -0
  53. package/src/js/components/NoPlayerModal.tsx +42 -0
  54. package/src/js/components/PlayerPreview.tsx +116 -0
  55. package/src/js/components/TabsHeader.tsx +31 -0
  56. package/src/js/components/TabsSection.tsx +74 -0
  57. package/src/js/components/TitleWithTooltip.tsx +39 -0
  58. package/src/js/components/VideoLinks.tsx +83 -0
  59. package/src/js/components/VideoPlayer.tsx +324 -0
  60. package/src/js/components/index.ts +9 -0
  61. package/src/js/components/prism/PrismLiveModal.tsx +196 -0
  62. package/src/js/components/prism/PrismPlayerControls.tsx +356 -0
  63. package/src/js/components/prism/PrismPlayerPreview.tsx +200 -0
  64. package/src/js/components/prism/index.ts +3 -0
  65. package/src/js/constants/baseUrls.ts +4 -0
  66. package/src/js/constants/embed.ts +12 -0
  67. package/src/js/constants/player.ts +32 -0
  68. package/src/js/constants/pluginsRegistry.ts +30 -0
  69. package/src/js/hooks/useGetPlayers.ts +12 -0
  70. package/src/js/hooks/useIsMobile.ts +33 -0
  71. package/src/js/i18n.ts +43 -0
  72. package/src/js/services/players-api.ts +26 -0
  73. package/src/js/services/url-shortener.ts +10 -0
  74. package/src/js/types/EmbedCodeModalProps.ts +72 -0
  75. package/src/js/types/EmbedCodeProps.ts +13 -0
  76. package/src/js/types/brightcoveEmbedCode.d.ts +18 -0
  77. package/src/js/types/players.ts +64 -0
  78. package/src/js/types/react-i18next.d.ts +8 -0
  79. package/src/js/types/studioModule.ts +7 -0
  80. package/src/js/utils/addPluginsDataToPlayers.ts +59 -0
  81. package/src/js/utils/call.ts +31 -0
  82. package/src/js/utils/dimensions.ts +53 -0
  83. package/src/js/utils/embedCodeGenerator.ts +120 -0
  84. package/src/js/utils/featureSwitches.ts +16 -0
  85. package/src/js/utils/util.ts +153 -0
  86. package/src/module.tsx +280 -0
  87. package/src/styles/CustomSettingsDropdown.sass +42 -0
  88. package/src/styles/EmbedCode.sass +30 -0
  89. package/src/styles/EmbedCodeModal.sass +76 -0
  90. package/src/styles/base.sass +1 -0
  91. package/src/styles/prism/PrismLiveModal.sass +128 -0
  92. package/src/styles/theme.sass +11 -0
  93. package/src/styles/typings.td.ts +9 -0
  94. package/src/translations/en.json +69 -0
  95. package/src/translations/es.json +69 -0
  96. package/src/translations/fr.json +69 -0
  97. package/src/translations/ja.json +69 -0
  98. package/src/translations/ko.json +69 -0
  99. package/src/translations/zh.json +69 -0
  100. package/src/types/studio.ts +197 -0
  101. package/tsconfig.json +34 -0
  102. package/webpack.config.js +117 -0
  103. package/index.js +0 -9
  104. package/index.js.map +0 -1
  105. package/src/js/components/prism/PrismPlayerPreview.d.ts.map +0 -1
  106. /package/{__tests__ → build/__tests__}/EmbedCode.test.d.ts +0 -0
  107. /package/{__tests__ → build/__tests__}/EmbedCode.test.d.ts.map +0 -0
  108. /package/{__tests__ → build/__tests__}/InteractivityProject.test.d.ts +0 -0
  109. /package/{__tests__ → build/__tests__}/InteractivityProject.test.d.ts.map +0 -0
  110. /package/{__tests__ → build/__tests__}/PlayerPreview.test.d.ts +0 -0
  111. /package/{__tests__ → build/__tests__}/PlayerPreview.test.d.ts.map +0 -0
  112. /package/{__tests__ → build/__tests__}/PrismPlayerControls.test.d.ts +0 -0
  113. /package/{__tests__ → build/__tests__}/PrismPlayerControls.test.d.ts.map +0 -0
  114. /package/{__tests__ → build/__tests__}/PrismPlayerPreview.test.d.ts +0 -0
  115. /package/{__tests__ → build/__tests__}/PrismPlayerPreview.test.d.ts.map +0 -0
  116. /package/{__tests__ → build/__tests__}/TabsHeader.test.d.ts +0 -0
  117. /package/{__tests__ → build/__tests__}/TabsHeader.test.d.ts.map +0 -0
  118. /package/{__tests__ → build/__tests__}/TabsSection.test.d.ts +0 -0
  119. /package/{__tests__ → build/__tests__}/TabsSection.test.d.ts.map +0 -0
  120. /package/{__tests__ → build/__tests__}/TitleWithTooltip.test.d.ts +0 -0
  121. /package/{__tests__ → build/__tests__}/TitleWithTooltip.test.d.ts.map +0 -0
  122. /package/{__tests__ → build/__tests__}/VideoPlayer.test.d.ts +0 -0
  123. /package/{__tests__ → build/__tests__}/VideoPlayer.test.d.ts.map +0 -0
  124. /package/{__tests__ → build/__tests__}/embedCodeGenerator.test.d.ts +0 -0
  125. /package/{__tests__ → build/__tests__}/embedCodeGenerator.test.d.ts.map +0 -0
  126. /package/{__tests__ → build/__tests__}/util.test.d.ts +0 -0
  127. /package/{__tests__ → build/__tests__}/util.test.d.ts.map +0 -0
  128. /package/{src → build/src}/index.d.ts +0 -0
  129. /package/{src → build/src}/index.d.ts.map +0 -0
  130. /package/{src → build/src}/js/EmbedCodeModal.d.ts +0 -0
  131. /package/{src → build/src}/js/EmbedCodeModal.d.ts.map +0 -0
  132. /package/{src → build/src}/js/components/CustomSettingsDropdown.d.ts +0 -0
  133. /package/{src → build/src}/js/components/CustomSettingsDropdown.d.ts.map +0 -0
  134. /package/{src → build/src}/js/components/EmbedCopySection.d.ts +0 -0
  135. /package/{src → build/src}/js/components/EmbedCopySection.d.ts.map +0 -0
  136. /package/{src → build/src}/js/components/ModalContainer.d.ts +0 -0
  137. /package/{src → build/src}/js/components/ModalContainer.d.ts.map +0 -0
  138. /package/{src → build/src}/js/components/NoPlayerModal.d.ts +0 -0
  139. /package/{src → build/src}/js/components/NoPlayerModal.d.ts.map +0 -0
  140. /package/{src → build/src}/js/components/PlayerPreview.d.ts +0 -0
  141. /package/{src → build/src}/js/components/PlayerPreview.d.ts.map +0 -0
  142. /package/{src → build/src}/js/components/TabsHeader.d.ts +0 -0
  143. /package/{src → build/src}/js/components/TabsHeader.d.ts.map +0 -0
  144. /package/{src → build/src}/js/components/TabsSection.d.ts +0 -0
  145. /package/{src → build/src}/js/components/TabsSection.d.ts.map +0 -0
  146. /package/{src → build/src}/js/components/TitleWithTooltip.d.ts +0 -0
  147. /package/{src → build/src}/js/components/TitleWithTooltip.d.ts.map +0 -0
  148. /package/{src → build/src}/js/components/VideoLinks.d.ts +0 -0
  149. /package/{src → build/src}/js/components/VideoLinks.d.ts.map +0 -0
  150. /package/{src → build/src}/js/components/VideoPlayer.d.ts +0 -0
  151. /package/{src → build/src}/js/components/VideoPlayer.d.ts.map +0 -0
  152. /package/{src → build/src}/js/components/index.d.ts +0 -0
  153. /package/{src → build/src}/js/components/index.d.ts.map +0 -0
  154. /package/{src → build/src}/js/components/prism/PrismLiveModal.d.ts +0 -0
  155. /package/{src → build/src}/js/components/prism/index.d.ts +0 -0
  156. /package/{src → build/src}/js/components/prism/index.d.ts.map +0 -0
  157. /package/{src → build/src}/js/constants/baseUrls.d.ts +0 -0
  158. /package/{src → build/src}/js/constants/baseUrls.d.ts.map +0 -0
  159. /package/{src → build/src}/js/constants/embed.d.ts +0 -0
  160. /package/{src → build/src}/js/constants/embed.d.ts.map +0 -0
  161. /package/{src → build/src}/js/constants/player.d.ts +0 -0
  162. /package/{src → build/src}/js/constants/player.d.ts.map +0 -0
  163. /package/{src → build/src}/js/constants/pluginsRegistry.d.ts +0 -0
  164. /package/{src → build/src}/js/constants/pluginsRegistry.d.ts.map +0 -0
  165. /package/{src → build/src}/js/hooks/useGetPlayers.d.ts +0 -0
  166. /package/{src → build/src}/js/hooks/useGetPlayers.d.ts.map +0 -0
  167. /package/{src → build/src}/js/services/players-api.d.ts +0 -0
  168. /package/{src → build/src}/js/services/players-api.d.ts.map +0 -0
  169. /package/{src → build/src}/js/services/url-shortener.d.ts +0 -0
  170. /package/{src → build/src}/js/services/url-shortener.d.ts.map +0 -0
  171. /package/{src → build/src}/js/types/EmbedCodeModalProps.d.ts +0 -0
  172. /package/{src → build/src}/js/types/EmbedCodeModalProps.d.ts.map +0 -0
  173. /package/{src → build/src}/js/types/EmbedCodeProps.d.ts +0 -0
  174. /package/{src → build/src}/js/types/EmbedCodeProps.d.ts.map +0 -0
  175. /package/{src → build/src}/js/types/players.d.ts +0 -0
  176. /package/{src → build/src}/js/types/players.d.ts.map +0 -0
  177. /package/{src → build/src}/js/types/studioModule.d.ts +0 -0
  178. /package/{src → build/src}/js/types/studioModule.d.ts.map +0 -0
  179. /package/{src → build/src}/js/utils/addPluginsDataToPlayers.d.ts +0 -0
  180. /package/{src → build/src}/js/utils/addPluginsDataToPlayers.d.ts.map +0 -0
  181. /package/{src → build/src}/js/utils/call.d.ts +0 -0
  182. /package/{src → build/src}/js/utils/call.d.ts.map +0 -0
  183. /package/{src → build/src}/js/utils/dimensions.d.ts +0 -0
  184. /package/{src → build/src}/js/utils/dimensions.d.ts.map +0 -0
  185. /package/{src → build/src}/js/utils/embedCodeGenerator.d.ts +0 -0
  186. /package/{src → build/src}/js/utils/embedCodeGenerator.d.ts.map +0 -0
  187. /package/{src → build/src}/js/utils/featureSwitches.d.ts +0 -0
  188. /package/{src → build/src}/js/utils/featureSwitches.d.ts.map +0 -0
  189. /package/{src → build/src}/js/utils/util.d.ts +0 -0
  190. /package/{src → build/src}/js/utils/util.d.ts.map +0 -0
  191. /package/{src → build/src}/module.d.ts +0 -0
  192. /package/{src → build/src}/module.d.ts.map +0 -0
  193. /package/{src → build/src}/styles/typings.td.d.ts +0 -0
  194. /package/{src → build/src}/styles/typings.td.d.ts.map +0 -0
  195. /package/{src → build/src}/types/studio.d.ts +0 -0
  196. /package/{src → build/src}/types/studio.d.ts.map +0 -0
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { QueryClientProvider, QueryClient } from 'react-query';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { VideoPlayer } from 'js/components';
5
+ import { EmbedCodeModalTypes } from 'js/types/EmbedCodeModalProps';
6
+ import { PLAYER_LIST_FOR_SELECTION, PLAYERS } from 'js/constants/player';
7
+
8
+ jest.mock('../src/js/utils/featureSwitches');
9
+
10
+ const wrappedComponent = (children: React.ReactElement) => (
11
+ <QueryClientProvider client={new QueryClient()}>
12
+ {children}
13
+ </QueryClientProvider>
14
+ );
15
+
16
+ const DEFAULT_MODAL_COMP = {
17
+ videoId: '123',
18
+ videoName: 'test video',
19
+ interactivityProjectId: '9876',
20
+ interactivityProjectName: 'test project',
21
+ accountId: '456',
22
+ duration: 10,
23
+ };
24
+
25
+ test('renders EmbedCodeModal component with right information', () => {
26
+ render(
27
+ wrappedComponent(
28
+ <VideoPlayer
29
+ video={DEFAULT_MODAL_COMP}
30
+ embedCodeModalType={EmbedCodeModalTypes.PROJECT}
31
+ playerListForSelection={PLAYER_LIST_FOR_SELECTION}
32
+ players={PLAYERS}
33
+ />,
34
+ ),
35
+ );
36
+ expect(
37
+ screen.getByText(DEFAULT_MODAL_COMP.interactivityProjectId),
38
+ ).toBeInTheDocument();
39
+ expect(
40
+ screen.getByText(DEFAULT_MODAL_COMP.interactivityProjectName),
41
+ ).toBeInTheDocument();
42
+ expect(
43
+ screen.queryByText(DEFAULT_MODAL_COMP.videoId),
44
+ ).not.toBeInTheDocument();
45
+ expect(
46
+ screen.queryByText(DEFAULT_MODAL_COMP.videoName),
47
+ ).not.toBeInTheDocument();
48
+ expect(screen.getByRole('tab', { name: /javascript/i })).toBeInTheDocument();
49
+ expect(screen.getByRole('tab', { name: /IFrame/i })).toBeInTheDocument();
50
+ });
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { PlayerPreview } from '../src/js/components';
4
+ import { QueryClientProvider, QueryClient } from 'react-query';
5
+
6
+ const DEFAULT_MODAL_COMP = {
7
+ id: 'default',
8
+ accountId: '456',
9
+ };
10
+
11
+ const wrappedComponent = (children: React.ReactElement) => (
12
+ <QueryClientProvider client={new QueryClient()}>
13
+ {children}
14
+ </QueryClientProvider>
15
+ );
16
+
17
+ test('renders PlayerPreview component with right information', () => {
18
+ render(wrappedComponent(<PlayerPreview {...DEFAULT_MODAL_COMP} />));
19
+ expect(screen.getByText('getURL')).toBeInTheDocument();
20
+ expect(screen.getByText('previewButton')).toBeInTheDocument();
21
+ expect(screen.getByRole('tab', { name: /javascript/i })).toBeInTheDocument();
22
+ expect(screen.getByRole('tab', { name: /IFrame/i })).toBeInTheDocument();
23
+ });
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { PrismLiveModal } from '../src/js/components/prism/PrismLiveModal';
4
+ import { LivePayload } from '../src/js/types/EmbedCodeModalProps';
5
+
6
+ jest.mock('@brightcove/prism-core', () => ({
7
+ PrismModal: ({
8
+ children,
9
+ ...props
10
+ }: {
11
+ children: React.ReactNode;
12
+ } & Record<string, unknown>) => (
13
+ <div data-testid='prism-modal' data-props={JSON.stringify(props)}>
14
+ {children}
15
+ </div>
16
+ ),
17
+ }));
18
+
19
+ jest.mock('../src/js/components/prism/PrismPlayerControls', () => ({
20
+ PrismPlayerControls: () => <div />,
21
+ }));
22
+
23
+ jest.mock('../src/js/components/prism/PrismPlayerPreview', () => ({
24
+ PrismPlayerPreview: () => <div />,
25
+ }));
26
+
27
+ jest.mock('js/utils/util', () => ({
28
+ getVideoPreviewLink: jest.fn(() => ''),
29
+ }));
30
+
31
+ const VIDEO: LivePayload = {
32
+ videoId: '123',
33
+ accountId: '456',
34
+ livePlaybackToken: 'token',
35
+ adConfigId: '',
36
+ };
37
+
38
+ const renderModal = () =>
39
+ render(
40
+ <PrismLiveModal
41
+ isOpen
42
+ onClose={jest.fn()}
43
+ video={VIDEO}
44
+ players={{}}
45
+ playerListForSelection={[]}
46
+ />,
47
+ );
48
+
49
+ const setViewportWidth = (width: number) =>
50
+ Object.defineProperty(window, 'innerWidth', {
51
+ configurable: true,
52
+ value: width,
53
+ });
54
+
55
+ describe('PrismLiveModal', () => {
56
+ afterEach(() => setViewportWidth(1024));
57
+
58
+ const renderProps = () =>
59
+ JSON.parse(
60
+ screen.getByTestId('prism-modal').getAttribute('data-props') as string,
61
+ );
62
+
63
+ test('configures the modal through native Prism props', () => {
64
+ renderModal();
65
+
66
+ expect(renderProps()).toMatchObject({
67
+ size: 'extraLarge',
68
+ forceMaxHeight: true,
69
+ contentScroll: 'none',
70
+ showHeaderBottomBorder: true,
71
+ });
72
+ });
73
+
74
+ test('docks as a bottom sheet and copies the code from the mobile footer', () => {
75
+ setViewportWidth(800);
76
+ renderModal();
77
+
78
+ expect(renderProps()).toMatchObject({
79
+ bottomSheet: true,
80
+ contentScroll: 'auto',
81
+ primaryButtonText: 'copyCode',
82
+ cancelButtonText: 'done',
83
+ });
84
+ });
85
+ });
@@ -0,0 +1,139 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { PrismPlayerControls } from '../src/js/components/prism/PrismPlayerControls';
4
+ import {
5
+ EmbedCodeAspectRatio,
6
+ EmbedCodeUnits,
7
+ } from '../src/js/types/EmbedCodeModalProps';
8
+ import { PLAYER_LIST_FOR_SELECTION, PLAYERS } from '../src/js/constants/player';
9
+
10
+ jest.mock('@brightcove/prism-core', () => ({
11
+ PrismCheckbox: ({
12
+ label,
13
+ checked,
14
+ onChange,
15
+ disabled,
16
+ }: {
17
+ label: string;
18
+ checked: boolean;
19
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
20
+ disabled?: boolean;
21
+ }) => (
22
+ <input
23
+ type='checkbox'
24
+ aria-label={label}
25
+ checked={checked}
26
+ onChange={onChange}
27
+ disabled={disabled}
28
+ />
29
+ ),
30
+ PrismDropDown: () => <div />,
31
+ PrismInput: () => <div />,
32
+ PrismIconButton: () => <button />,
33
+ PrismCodeCard: () => <div />,
34
+ }));
35
+
36
+ const DEFAULT_VIDEO = {
37
+ videoId: '789',
38
+ accountId: '456',
39
+ livePlaybackToken: 'token123',
40
+ adConfigId: '',
41
+ };
42
+
43
+ const DEFAULT_EMBED_PROPERTIES = {
44
+ width: 960,
45
+ height: 540,
46
+ units: EmbedCodeUnits.PX,
47
+ responsive: false,
48
+ aspectRatio: EmbedCodeAspectRatio.CUSTOM,
49
+ };
50
+
51
+ const renderControls = (extraProps = {}) =>
52
+ render(
53
+ <PrismPlayerControls
54
+ video={DEFAULT_VIDEO}
55
+ players={PLAYERS}
56
+ playerListForSelection={PLAYER_LIST_FOR_SELECTION}
57
+ selectedPlayer='default'
58
+ onPlayerChange={jest.fn()}
59
+ embedProperties={DEFAULT_EMBED_PROPERTIES}
60
+ setEmbedProperties={jest.fn()}
61
+ showDVR
62
+ showLowLatency
63
+ dvrTime={300}
64
+ {...extraProps}
65
+ />,
66
+ );
67
+
68
+ describe('embed code reporting', () => {
69
+ test('reports the iframe embed for a plain player', () => {
70
+ const onEmbedCodeChange = jest.fn();
71
+ renderControls({ onEmbedCodeChange });
72
+
73
+ const code = onEmbedCodeChange.mock.calls.at(-1)?.[0] as string;
74
+ expect(code).toContain('<iframe');
75
+ });
76
+
77
+ test('reports the JavaScript embed for a player that requires it', () => {
78
+ const onEmbedCodeChange = jest.fn();
79
+ renderControls({
80
+ players: {
81
+ default: { ...PLAYERS.default, hasCampaignPlugin: true },
82
+ },
83
+ onEmbedCodeChange,
84
+ });
85
+
86
+ const code = onEmbedCodeChange.mock.calls.at(-1)?.[0] as string;
87
+ expect(code).toContain('<video-js');
88
+ });
89
+ });
90
+
91
+ describe('DVR and Low Latency checkboxes are independent', () => {
92
+ test('both checkboxes are enabled when asset supports DVR and Low Latency', () => {
93
+ renderControls();
94
+ expect(
95
+ screen.getByRole('checkbox', { name: 'dvr.allowDVR' }),
96
+ ).not.toBeDisabled();
97
+ expect(
98
+ screen.getByRole('checkbox', { name: 'lowLatency' }),
99
+ ).not.toBeDisabled();
100
+ });
101
+
102
+ test('DVR checkbox starts unchecked when Low Latency is also present', () => {
103
+ renderControls();
104
+ expect(
105
+ screen.getByRole('checkbox', { name: 'dvr.allowDVR' }),
106
+ ).not.toBeChecked();
107
+ expect(screen.getByRole('checkbox', { name: 'lowLatency' })).toBeChecked();
108
+ });
109
+
110
+ test('Low Latency checkbox stays enabled when DVR is checked', () => {
111
+ renderControls();
112
+ fireEvent.click(screen.getByRole('checkbox', { name: 'dvr.allowDVR' }));
113
+ expect(
114
+ screen.getByRole('checkbox', { name: 'lowLatency' }),
115
+ ).not.toBeDisabled();
116
+ });
117
+
118
+ test('DVR checkbox stays enabled when Low Latency is checked', () => {
119
+ renderControls();
120
+ fireEvent.click(screen.getByRole('checkbox', { name: 'lowLatency' }));
121
+ expect(
122
+ screen.getByRole('checkbox', { name: 'dvr.allowDVR' }),
123
+ ).not.toBeDisabled();
124
+ });
125
+
126
+ test('each callback fires independently without affecting the other', () => {
127
+ const onDVRClick = jest.fn();
128
+ const onLowLatencyClick = jest.fn();
129
+ renderControls({ onDVRClick, onLowLatencyClick });
130
+
131
+ fireEvent.click(screen.getByRole('checkbox', { name: 'dvr.allowDVR' }));
132
+ expect(onDVRClick).toHaveBeenCalledTimes(1);
133
+ expect(onLowLatencyClick).not.toHaveBeenCalled();
134
+
135
+ fireEvent.click(screen.getByRole('checkbox', { name: 'lowLatency' }));
136
+ expect(onLowLatencyClick).toHaveBeenCalledTimes(1);
137
+ expect(onDVRClick).toHaveBeenCalledTimes(1);
138
+ });
139
+ });
@@ -0,0 +1,168 @@
1
+ import React from 'react';
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3
+ import { PrismPlayerPreview } from '../src/js/components/prism/PrismPlayerPreview';
4
+ import { shortenUrl } from '../src/js/services/url-shortener';
5
+
6
+ jest.mock('../src/js/services/url-shortener');
7
+
8
+ const mockedShortenUrl = shortenUrl as jest.MockedFunction<typeof shortenUrl>;
9
+
10
+ const PREVIEW_URL =
11
+ 'https://players.brightcove.net/456/default_default/index.html?videoId=123&livePlaybackToken=abc';
12
+ const SHORT_URL = 'https://bcove.video/4whTWpq';
13
+
14
+ const DEFAULT_PROPS = {
15
+ accountId: '456',
16
+ playerId: 'default',
17
+ videoId: '123',
18
+ previewUrl: PREVIEW_URL,
19
+ };
20
+
21
+ const toaster = {
22
+ success: jest.fn(),
23
+ error: jest.fn(),
24
+ };
25
+
26
+ beforeEach(() => {
27
+ Object.assign(window, { brightcove: { Toaster: toaster } });
28
+ Object.assign(navigator, {
29
+ clipboard: {
30
+ writeText: jest.fn(() => Promise.resolve()),
31
+ write: jest.fn(() => Promise.reject(new Error('unsupported'))),
32
+ },
33
+ });
34
+ });
35
+
36
+ afterEach(() => {
37
+ jest.clearAllMocks();
38
+ });
39
+
40
+ const openCopyUrlMenu = () => {
41
+ fireEvent.click(screen.getByRole('button', { name: /prism.copyURL/ }));
42
+ };
43
+
44
+ test('offers both a short and a full URL option', () => {
45
+ render(<PrismPlayerPreview {...DEFAULT_PROPS} />);
46
+
47
+ openCopyUrlMenu();
48
+
49
+ expect(screen.getByText('prism.shortURL')).toBeInTheDocument();
50
+ expect(screen.getByText('prism.fullURL')).toBeInTheDocument();
51
+ });
52
+
53
+ test('copies the full URL as-is without shortening it', async () => {
54
+ render(<PrismPlayerPreview {...DEFAULT_PROPS} />);
55
+
56
+ openCopyUrlMenu();
57
+ fireEvent.click(screen.getByText('prism.fullURL'));
58
+
59
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(PREVIEW_URL);
60
+ expect(mockedShortenUrl).not.toHaveBeenCalled();
61
+ await waitFor(() => expect(screen.getByText('prism.copied')));
62
+ });
63
+
64
+ test('copies the shortened URL when the short URL option is picked', async () => {
65
+ mockedShortenUrl.mockResolvedValue(SHORT_URL);
66
+ render(<PrismPlayerPreview {...DEFAULT_PROPS} />);
67
+
68
+ openCopyUrlMenu();
69
+ fireEvent.click(screen.getByText('prism.shortURL'));
70
+
71
+ await waitFor(() =>
72
+ expect(mockedShortenUrl).toHaveBeenCalledWith({ longUrl: PREVIEW_URL }),
73
+ );
74
+ await waitFor(() =>
75
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(SHORT_URL),
76
+ );
77
+ });
78
+
79
+ test('reports progress on the button while the URL is being shortened', async () => {
80
+ let resolveShortening: (value: string) => void = (value) => {
81
+ throw new Error(`shortening resolved before it started: ${value}`);
82
+ };
83
+ mockedShortenUrl.mockReturnValue(
84
+ new Promise<string>((resolve) => {
85
+ resolveShortening = resolve;
86
+ }),
87
+ );
88
+ render(<PrismPlayerPreview {...DEFAULT_PROPS} />);
89
+
90
+ openCopyUrlMenu();
91
+ fireEvent.click(screen.getByText('prism.shortURL'));
92
+
93
+ await waitFor(() => expect(screen.getByText('prism.copying')));
94
+
95
+ resolveShortening(SHORT_URL);
96
+
97
+ await waitFor(() => expect(screen.getByText('prism.copied')));
98
+ expect(toaster.success).not.toHaveBeenCalled();
99
+ });
100
+
101
+ test('surfaces an error toast when the shortening service fails', async () => {
102
+ mockedShortenUrl.mockRejectedValue(new Error('invalid URL'));
103
+ render(<PrismPlayerPreview {...DEFAULT_PROPS} />);
104
+
105
+ openCopyUrlMenu();
106
+ fireEvent.click(screen.getByText('prism.shortURL'));
107
+
108
+ await waitFor(() =>
109
+ expect(toaster.error).toHaveBeenCalledWith({
110
+ message: 'toasterGetURLError',
111
+ }),
112
+ );
113
+ expect(navigator.clipboard.writeText).not.toHaveBeenCalled();
114
+ expect(toaster.success).not.toHaveBeenCalled();
115
+ });
116
+
117
+ test('ignores a shortening result that lands after the preview URL changed', async () => {
118
+ let resolveShortening: (value: string) => void = (value) => {
119
+ throw new Error(`shortening resolved before it started: ${value}`);
120
+ };
121
+ mockedShortenUrl.mockReturnValue(
122
+ new Promise<string>((resolve) => {
123
+ resolveShortening = resolve;
124
+ }),
125
+ );
126
+ const { rerender } = render(<PrismPlayerPreview {...DEFAULT_PROPS} />);
127
+
128
+ openCopyUrlMenu();
129
+ fireEvent.click(screen.getByText('prism.shortURL'));
130
+ await waitFor(() => expect(screen.getByText('prism.copying')));
131
+
132
+ rerender(
133
+ <PrismPlayerPreview
134
+ {...DEFAULT_PROPS}
135
+ previewUrl={`${PREVIEW_URL}&playerId=other`}
136
+ />,
137
+ );
138
+ resolveShortening(SHORT_URL);
139
+
140
+ await waitFor(() => expect(screen.getByText('prism.copyURL')));
141
+ expect(screen.queryByText('prism.copied')).not.toBeInTheDocument();
142
+ });
143
+
144
+ test('drops the copied state as soon as the preview URL changes', async () => {
145
+ const { rerender } = render(<PrismPlayerPreview {...DEFAULT_PROPS} />);
146
+
147
+ openCopyUrlMenu();
148
+ fireEvent.click(screen.getByText('prism.fullURL'));
149
+ await waitFor(() => expect(screen.getByText('prism.copied')));
150
+
151
+ rerender(
152
+ <PrismPlayerPreview
153
+ {...DEFAULT_PROPS}
154
+ previewUrl={`${PREVIEW_URL}&playerId=other`}
155
+ />,
156
+ );
157
+
158
+ expect(screen.getByText('prism.copyURL')).toBeInTheDocument();
159
+ expect(
160
+ screen.getByRole('button', { name: /prism.copyURL/ }),
161
+ ).not.toBeDisabled();
162
+ });
163
+
164
+ test('disables the copy URL button while no player is selected', () => {
165
+ render(<PrismPlayerPreview {...DEFAULT_PROPS} previewUrl='' />);
166
+
167
+ expect(screen.getByRole('button', { name: /prism.copyURL/ })).toBeDisabled();
168
+ });
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { act, fireEvent, render } from '@testing-library/react';
3
+ import { TabsHeader } from '../src/js/components';
4
+
5
+ const DEFAULT_MODAL_COMP = {
6
+ currentTab: 'iframe',
7
+ handleTabChange: jest.fn(),
8
+ iframeLabel: 'iframe',
9
+ jsLabel: 'js',
10
+ };
11
+
12
+ test('renders TabsHeader clickeable tabs', async () => {
13
+ const { getByRole } = render(<TabsHeader {...DEFAULT_MODAL_COMP} />);
14
+ const jsBtn = getByRole('tab', { name: DEFAULT_MODAL_COMP.jsLabel });
15
+ act(() => {
16
+ fireEvent.click(jsBtn);
17
+ });
18
+ expect(DEFAULT_MODAL_COMP.handleTabChange).toBeCalled();
19
+ });
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { act, fireEvent, render } from '@testing-library/react';
3
+ import { TabsSection } from 'js/components';
4
+
5
+ const DEFAULT_MODAL_COMP = {
6
+ playerId: 'default',
7
+ videoId: '1234',
8
+ accountId: 'default',
9
+ isBcMapTracking: false,
10
+ };
11
+
12
+ test('renders TabsSection clickeable tabs', async () => {
13
+ const { getByRole, getByText } = render(
14
+ <TabsSection {...DEFAULT_MODAL_COMP} isCampaign isViewerIdSource={false} />,
15
+ );
16
+ const jsBtn = getByRole('tab', { name: /javascript/i });
17
+ act(() => {
18
+ fireEvent.click(jsBtn);
19
+ });
20
+ expect(getByText(/data-video-id/i)).toBeInTheDocument();
21
+ });
22
+
23
+ test('renders helper text for campaign players', async () => {
24
+ const { getByText } = render(
25
+ <TabsSection {...DEFAULT_MODAL_COMP} isCampaign isViewerIdSource={false} />,
26
+ );
27
+ expect(getByText('tabs.helperTextWarning')).toBeInTheDocument();
28
+ });
29
+
30
+ test('renders helper text for viewer id source players', async () => {
31
+ const { getByText } = render(
32
+ <TabsSection {...DEFAULT_MODAL_COMP} isCampaign={false} isViewerIdSource />,
33
+ );
34
+ expect(getByText('tabs.viewerIdTextWarning')).toBeInTheDocument();
35
+ });
36
+
37
+ test('renders iframe content if it is not campaign or viewer id', async () => {
38
+ const { getByText } = render(
39
+ <TabsSection
40
+ {...DEFAULT_MODAL_COMP}
41
+ isCampaign={false}
42
+ isViewerIdSource={false}
43
+ />,
44
+ );
45
+ expect(getByText(/allowfullscreen/i)).toBeInTheDocument();
46
+ });
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { TitleWithTooltip } from '../src/js/components';
4
+ import { truncateVideoTitle } from '../src/js/utils/util';
5
+
6
+ const DEFAULT_VIDEO_COMP = {
7
+ videoId: '123',
8
+ videoName:
9
+ 'The cat is a domestic species of small carnivorous mammal. It is the only domesticated species in the family Felidae and is often referred to as the domestic cat to distinguish it from the wild members of the family. ',
10
+ accountId: '456',
11
+ duration: 10,
12
+ };
13
+
14
+ const DEFAULT_PROJECT_COMP = {
15
+ videoId: '456',
16
+ interactivityProjectId: '987',
17
+ interactivityProjectName:
18
+ "The dog is a domesticated descendant of the wolf. Also called the domestic dog, it is derived from the extinct Pleistocene wolf, and the modern wolf is the dog's nearest living relative. ",
19
+ accountId: '456',
20
+ duration: 10,
21
+ };
22
+
23
+ test('renders TitleWithTooltip for Video with truncated title if its too long', () => {
24
+ const truncatedTitle = truncateVideoTitle(DEFAULT_VIDEO_COMP.videoName);
25
+ render(
26
+ <TitleWithTooltip
27
+ name={DEFAULT_VIDEO_COMP.videoName}
28
+ id={DEFAULT_VIDEO_COMP.videoId}
29
+ />,
30
+ );
31
+ expect(screen.getByText(truncatedTitle)).toBeInTheDocument();
32
+ });
33
+
34
+ test('renders TitleWithTooltip for Project with truncated title if its too long', () => {
35
+ const truncatedTitle = truncateVideoTitle(
36
+ DEFAULT_PROJECT_COMP.interactivityProjectName,
37
+ );
38
+ render(
39
+ <TitleWithTooltip
40
+ name={DEFAULT_PROJECT_COMP.interactivityProjectName}
41
+ id={DEFAULT_PROJECT_COMP.videoId}
42
+ />,
43
+ );
44
+ expect(screen.getByText(truncatedTitle)).toBeInTheDocument();
45
+ });
@@ -0,0 +1,105 @@
1
+ import React from 'react';
2
+ import { QueryClientProvider, QueryClient } from 'react-query';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import { VideoPlayer } from '../src/js/components/VideoPlayer';
5
+ import { EmbedCodeModalTypes } from '../src/js/types/EmbedCodeModalProps';
6
+ import { PLAYER_LIST_FOR_SELECTION, PLAYERS } from '../src/js/constants/player';
7
+
8
+ jest.mock('../src/js/utils/featureSwitches');
9
+
10
+ const wrappedComponent = (children: React.ReactElement) => (
11
+ <QueryClientProvider client={new QueryClient()}>
12
+ {children}
13
+ </QueryClientProvider>
14
+ );
15
+
16
+ const DEFAULT_MODAL_COMP = {
17
+ videoId: '123',
18
+ videoName: 'test video',
19
+ accountId: '456',
20
+ duration: 10,
21
+ };
22
+
23
+ const LIVE_MODAL_COMP = {
24
+ videoId: '789',
25
+ accountId: '456',
26
+ livePlaybackToken: 'token123',
27
+ adConfigId: '',
28
+ };
29
+
30
+ test('renders EmbedCodeModal component with right information', () => {
31
+ render(
32
+ wrappedComponent(
33
+ <VideoPlayer
34
+ video={DEFAULT_MODAL_COMP}
35
+ embedCodeModalType={EmbedCodeModalTypes.VIDEO}
36
+ playerListForSelection={PLAYER_LIST_FOR_SELECTION}
37
+ players={PLAYERS}
38
+ />,
39
+ ),
40
+ );
41
+ expect(screen.getByText(DEFAULT_MODAL_COMP.videoId)).toBeInTheDocument();
42
+ expect(screen.getByText(DEFAULT_MODAL_COMP.videoName)).toBeInTheDocument();
43
+ expect(screen.getByRole('tab', { name: /javascript/i })).toBeInTheDocument();
44
+ expect(screen.getByRole('tab', { name: /IFrame/i })).toBeInTheDocument();
45
+ });
46
+
47
+ describe('DVR and Low Latency checkboxes are independent', () => {
48
+ const renderLivePlayer = (extraProps = {}) =>
49
+ render(
50
+ wrappedComponent(
51
+ <VideoPlayer
52
+ video={LIVE_MODAL_COMP}
53
+ embedCodeModalType={EmbedCodeModalTypes.LIVE}
54
+ playerListForSelection={PLAYER_LIST_FOR_SELECTION}
55
+ players={PLAYERS}
56
+ showDVR
57
+ showLowLatency
58
+ {...extraProps}
59
+ />,
60
+ ),
61
+ );
62
+
63
+ const getDVRCheckbox = () =>
64
+ screen.getByRole('checkbox', { name: /dvr\.allowDVR/i });
65
+ const getLowLatencyCheckbox = () =>
66
+ screen.getByRole('checkbox', { name: /lowLatency/i });
67
+
68
+ test('both checkboxes are enabled when asset supports DVR and Low Latency', () => {
69
+ renderLivePlayer();
70
+ expect(getDVRCheckbox()).not.toBeDisabled();
71
+ expect(getLowLatencyCheckbox()).not.toBeDisabled();
72
+ });
73
+
74
+ test('DVR checkbox starts unchecked when Low Latency is also present', () => {
75
+ renderLivePlayer();
76
+ expect(getDVRCheckbox()).not.toBeChecked();
77
+ expect(getLowLatencyCheckbox()).toBeChecked();
78
+ });
79
+
80
+ test('Low Latency checkbox stays enabled when DVR is checked', () => {
81
+ renderLivePlayer();
82
+ fireEvent.click(getDVRCheckbox());
83
+ expect(getLowLatencyCheckbox()).not.toBeDisabled();
84
+ });
85
+
86
+ test('DVR checkbox stays enabled when Low Latency is checked', () => {
87
+ renderLivePlayer();
88
+ fireEvent.click(getLowLatencyCheckbox());
89
+ expect(getDVRCheckbox()).not.toBeDisabled();
90
+ });
91
+
92
+ test('each callback fires independently without affecting the other', () => {
93
+ const onDVRClick = jest.fn();
94
+ const onLowLatencyClick = jest.fn();
95
+ renderLivePlayer({ onDVRClick, onLowLatencyClick });
96
+
97
+ fireEvent.click(getDVRCheckbox());
98
+ expect(onDVRClick).toHaveBeenCalledTimes(1);
99
+ expect(onLowLatencyClick).not.toHaveBeenCalled();
100
+
101
+ fireEvent.click(getLowLatencyCheckbox());
102
+ expect(onLowLatencyClick).toHaveBeenCalledTimes(1);
103
+ expect(onDVRClick).toHaveBeenCalledTimes(1);
104
+ });
105
+ });