@applicaster/quick-brick-player 16.0.0-rc.59 → 16.0.0-rc.60

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-player",
3
- "version": "16.0.0-rc.59",
3
+ "version": "16.0.0-rc.60",
4
4
  "description": "Quick Brick Player",
5
5
  "main": "./src/index.ts",
6
6
  "types": "index.d.ts",
@@ -35,11 +35,11 @@
35
35
  },
36
36
  "homepage": "https://github.com/applicaster/quickbrick#readme",
37
37
  "dependencies": {
38
- "@applicaster/quick-brick-mobile-transport-controls": "16.0.0-rc.52",
39
- "@applicaster/quick-brick-tv-transport-controls": "16.0.0-rc.52",
40
- "@applicaster/zapp-react-native-tvos-app": "16.0.0-rc.59",
41
- "@applicaster/zapp-react-native-ui-components": "16.0.0-rc.59",
42
- "@applicaster/zapp-react-native-utils": "16.0.0-rc.59",
38
+ "@applicaster/quick-brick-mobile-transport-controls": "16.0.0-rc.60",
39
+ "@applicaster/quick-brick-tv-transport-controls": "16.0.0-rc.60",
40
+ "@applicaster/zapp-react-native-tvos-app": "16.0.0-rc.60",
41
+ "@applicaster/zapp-react-native-ui-components": "16.0.0-rc.60",
42
+ "@applicaster/zapp-react-native-utils": "16.0.0-rc.60",
43
43
  "query-string": "7.1.3",
44
44
  "shaka-player": "4.3.5",
45
45
  "typeface-montserrat": "^0.0.54",
@@ -1,10 +1,9 @@
1
1
  import React from "react";
2
2
  import { StyleSheet, Text, TextStyle, View } from "react-native";
3
3
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
4
+ import { usePlayerContent } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerContent";
4
5
 
5
6
  type AudioDescriptionProps = {
6
- title: string | number;
7
- summary: string | number;
8
7
  layoutState: {
9
8
  inline: boolean;
10
9
  docked: boolean;
@@ -96,17 +95,48 @@ const getSummaryStyles = (value: GetValue, prefix: string): TextStyle =>
96
95
  }),
97
96
  }) as const;
98
97
 
99
- const getHeight = (titleLineHeight: number, subtitleLineHeight) =>
100
- titleLineHeight * TITLE_LINES + subtitleLineHeight + TITLE_PADDING_BOTTOM;
98
+ const getHeight = ({
99
+ showTitle,
100
+ showSummary,
101
+ titleLineHeight,
102
+ subtitleLineHeight,
103
+ }: {
104
+ showTitle: boolean;
105
+ showSummary: boolean;
106
+ titleLineHeight: number;
107
+ subtitleLineHeight: number;
108
+ }) => {
109
+ const titleHeight = showTitle
110
+ ? titleLineHeight * TITLE_LINES + TITLE_PADDING_BOTTOM
111
+ : 0;
112
+
113
+ const summaryHeight = showSummary ? subtitleLineHeight : 0;
114
+
115
+ return titleHeight + summaryHeight;
116
+ };
101
117
 
102
118
  export const AudioDescription = (props: AudioDescriptionProps) => {
103
- const { title, summary, layoutState, value, maxWidth } = props;
119
+ const { layoutState, value, maxWidth } = props;
120
+ const { title, summary } = usePlayerContent();
104
121
  const prefix = layoutState.docked ? "docked_player_" : "";
105
122
 
106
123
  const summaryStyles = getSummaryStyles(value, prefix);
107
124
  const titleStyles = getTitleStyles(value, prefix);
108
125
 
109
- const height = getHeight(titleStyles.lineHeight, summaryStyles.lineHeight);
126
+ // Both depend on the Studio toggle alone, never on whether the text has
127
+ // arrived yet. Live audio resolves its title/summary asynchronously (the
128
+ // channel feed is polled after playback starts), so gating on the value
129
+ // would collapse the row on first render and expand it a second later -
130
+ // the control has to hold its place and render an empty line instead.
131
+ const showTitle = Boolean(value(`${prefix}title`));
132
+ const showSummary = Boolean(value(`${prefix}subtitle`));
133
+
134
+ const height = getHeight({
135
+ showTitle,
136
+ showSummary,
137
+ titleLineHeight: titleStyles.lineHeight,
138
+ subtitleLineHeight: summaryStyles.lineHeight,
139
+ });
110
140
 
111
141
  return (
112
142
  <View
@@ -118,15 +148,20 @@ export const AudioDescription = (props: AudioDescriptionProps) => {
118
148
  },
119
149
  ]}
120
150
  >
121
- <Text style={[styles.titleText, titleStyles]} numberOfLines={TITLE_LINES}>
122
- {title}
123
- </Text>
124
- {summary ? (
151
+ {showTitle ? (
152
+ <Text
153
+ style={[styles.titleText, titleStyles]}
154
+ numberOfLines={TITLE_LINES}
155
+ >
156
+ {title ?? ""}
157
+ </Text>
158
+ ) : null}
159
+ {showSummary ? (
125
160
  <Text
126
161
  style={[styles.summaryText, summaryStyles]}
127
162
  numberOfLines={SUBTITLE_LINES}
128
163
  >
129
- {summary}
164
+ {summary ?? ""}
130
165
  </Text>
131
166
  ) : null}
132
167
  </View>
@@ -12,7 +12,6 @@ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
12
12
  import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
13
13
  import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
14
14
  import { formatSpeed } from "./utils";
15
- import { useTitleSummaryOverlay } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerTitleSummaryOverlay";
16
15
  import { toNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
17
16
  import { DEFAULT_SPEED, PlaybackSpeed, SpeedController } from "./SpeedManager";
18
17
  import {
@@ -83,11 +82,27 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
83
82
  playNextData = null,
84
83
  } = props;
85
84
 
86
- const { title, summary } = useTitleSummaryOverlay(
87
- entry,
88
- playerState?.isLive,
89
- configuration?.audio_live_player_update_interval
90
- );
85
+ // For live audio the programme changes while the entry stays the same, so
86
+ // the player polls the channel's programme feed and republishes its content.
87
+ // Everything about that - whether the content is live at all, where the feed
88
+ // lives, how often to poll - is decided by `OverlaysObserver`, which loads
89
+ // the feed without React. This effect owns only the lifecycle: keep the
90
+ // metadata fresh while this player is mounted. Consumers read the result
91
+ // through `usePlayerContent` like any other player content.
92
+ React.useEffect(() => {
93
+ if (!player) {
94
+ return;
95
+ }
96
+
97
+ const observer = player.getOverlayObservable();
98
+
99
+ observer.startLiveContentUpdates();
100
+
101
+ return () => observer.stopLiveContentUpdates();
102
+ // Keyed on the entry's id rather than the object: starting the updates
103
+ // fires an immediate request, so a re-created `entry` prop with identical
104
+ // contents would re-fetch the feed on every render.
105
+ }, [player, playerState?.isLive, entry?.id]);
91
106
 
92
107
  const [speedManager, setSpeedManager] = useState<SpeedController | null>(
93
108
  null
@@ -259,8 +274,6 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
259
274
  configuration={configuration}
260
275
  dimensions={dimensions}
261
276
  playNextData={playNextData}
262
- title={title}
263
- summary={summary}
264
277
  />
265
278
  </View>
266
279
  </SafeAreaView>
@@ -0,0 +1,66 @@
1
+ import {
2
+ nextTrackButton,
3
+ previousTrackButton,
4
+ usesPlaylistControls,
5
+ } from "../utils";
6
+
7
+ describe("usesPlaylistControls", () => {
8
+ const configuredAs = (layout?: unknown): GetValue =>
9
+ ((key: string) =>
10
+ key === "player_controls_layout" ? layout : null) as unknown as GetValue;
11
+
12
+ it("opts in only for the playlist controls value", () => {
13
+ expect(usesPlaylistControls(configuredAs("playlist_controls"))).toBe(true);
14
+ });
15
+
16
+ it("keeps the original controls for the default value", () => {
17
+ expect(
18
+ usesPlaylistControls(configuredAs("playback_speed_and_sleep_timer"))
19
+ ).toBe(false);
20
+ });
21
+
22
+ it.each([
23
+ ["an undeclared key", undefined],
24
+ ["a null value", null],
25
+ ["an empty string", ""],
26
+ ["an unrecognised value", "something_else"],
27
+ ])("keeps the original controls for %s", (_label, layout) => {
28
+ expect(usesPlaylistControls(configuredAs(layout))).toBe(false);
29
+ });
30
+ });
31
+
32
+ describe("track control buttons", () => {
33
+ it("builds a previous button using the previous_track icon", () => {
34
+ const onPress = jest.fn();
35
+
36
+ expect(previousTrackButton({ isTablet: false, onPress })).toMatchObject({
37
+ size: "small",
38
+ icon: "previous_track",
39
+ onPress,
40
+ });
41
+ });
42
+
43
+ it("builds a next button using the next_track icon", () => {
44
+ const onPress = jest.fn();
45
+
46
+ expect(nextTrackButton({ isTablet: false, onPress })).toMatchObject({
47
+ size: "small",
48
+ icon: "next_track",
49
+ onPress,
50
+ });
51
+ });
52
+
53
+ it("propagates the disabled flag", () => {
54
+ expect(
55
+ nextTrackButton({ isTablet: false, onPress: jest.fn(), disabled: true })
56
+ ).toMatchObject({ disabled: true });
57
+
58
+ expect(
59
+ previousTrackButton({
60
+ isTablet: false,
61
+ onPress: jest.fn(),
62
+ disabled: true,
63
+ })
64
+ ).toMatchObject({ disabled: true });
65
+ });
66
+ });
@@ -15,14 +15,18 @@ import { VerticalGutter } from "./VerticalGutter";
15
15
 
16
16
  import {
17
17
  forwardButton,
18
+ nextTrackButton,
18
19
  playbackButton,
20
+ previousTrackButton,
19
21
  rewindButton,
20
22
  skipToLiveButton,
21
23
  skipToStartButton,
22
24
  sleepButton,
23
25
  speedButton,
26
+ usesPlaylistControls,
24
27
  } from "./utils";
25
28
  import { useSleepTimerModal } from "../../useSleepTimerModal";
29
+ import { usePlaylistNavigation } from "../../usePlaylistNavigation";
26
30
 
27
31
  const styles = StyleSheet.create({
28
32
  flex: {
@@ -58,8 +62,6 @@ type Props = {
58
62
  layoutState: QuickBrickPlayer.LayoutState;
59
63
  playerState: QuickBrickPlayer.PlayerState;
60
64
  value: GetValue;
61
- title: string | number;
62
- summary: string | number;
63
65
 
64
66
  playbackSpeedName: string;
65
67
 
@@ -82,8 +84,6 @@ export const Controls = (props: Props) => {
82
84
  layoutState,
83
85
  playerState,
84
86
  value,
85
- title,
86
- summary,
87
87
 
88
88
  playbackSpeedName,
89
89
 
@@ -101,6 +101,13 @@ export const Controls = (props: Props) => {
101
101
  playerManager.getActivePlayer()
102
102
  );
103
103
 
104
+ const { hasNext, hasPrevious, goNext, goPrevious } = usePlaylistNavigation(
105
+ entry,
106
+ playerState
107
+ );
108
+
109
+ const isPlaylistControlsLayout = usesPlaylistControls(value);
110
+
104
111
  const timerRemaining =
105
112
  sleepTimerState.timerDate !== 0 && +sleepTimerState.timerDate > Date.now()
106
113
  ? sleepTimerState.timerDate
@@ -197,6 +204,22 @@ export const Controls = (props: Props) => {
197
204
  return [skipToStart, rewind, play, forward, skipToLive];
198
205
  }
199
206
 
207
+ if (isPlaylistControlsLayout) {
208
+ const previous = previousTrackButton({
209
+ isTablet,
210
+ onPress: goPrevious,
211
+ disabled: !hasPrevious,
212
+ });
213
+
214
+ const next = nextTrackButton({
215
+ isTablet,
216
+ onPress: goNext,
217
+ disabled: !hasNext,
218
+ });
219
+
220
+ return [previous, rewind, play, forward, next];
221
+ }
222
+
200
223
  return [speed, rewind, play, forward, sleep];
201
224
  }, [
202
225
  playerState,
@@ -214,6 +237,11 @@ export const Controls = (props: Props) => {
214
237
  playbackSleepName,
215
238
  controller,
216
239
  isLiveShowFullControls,
240
+ isPlaylistControlsLayout,
241
+ hasNext,
242
+ hasPrevious,
243
+ goNext,
244
+ goPrevious,
217
245
  ]);
218
246
 
219
247
  return (
@@ -221,8 +249,6 @@ export const Controls = (props: Props) => {
221
249
  <VerticalGutter />
222
250
 
223
251
  <AudioDescription
224
- title={title}
225
- summary={summary}
226
252
  value={value}
227
253
  layoutState={layoutState}
228
254
  maxWidth={maxWidth}
@@ -103,3 +103,29 @@ export const speedButton = (props: ButtonsProps): ControlsButton =>
103
103
 
104
104
  export const sleepButton = (props: ButtonsProps): ControlsButton =>
105
105
  sleepButtonByName(props);
106
+
107
+ // Opt-in: a player plugin that does not declare this key returns undefined,
108
+ // which has to read as the default rather than as an opt-in.
109
+ export const usesPlaylistControls = (value: GetValue): boolean =>
110
+ value("player_controls_layout") === "playlist_controls";
111
+
112
+ type TrackButtonProps = ButtonsProps & { disabled?: boolean };
113
+
114
+ const trackButtonByName = ({
115
+ name,
116
+ onPress,
117
+ style,
118
+ disabled,
119
+ }: TrackButtonProps & { name: string }): ControlsButton => ({
120
+ size: "small",
121
+ icon: name,
122
+ onPress,
123
+ style,
124
+ disabled,
125
+ });
126
+
127
+ export const previousTrackButton = (props: TrackButtonProps): ControlsButton =>
128
+ trackButtonByName({ ...props, name: "previous_track" });
129
+
130
+ export const nextTrackButton = (props: TrackButtonProps): ControlsButton =>
131
+ trackButtonByName({ ...props, name: "next_track" });
@@ -18,7 +18,6 @@ import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Compo
18
18
  import { useSafeAreaInsets } from "react-native-safe-area-context";
19
19
  import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation";
20
20
  import { getTabBarHeight } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/getTabBarHeight";
21
- import { useTitleSummaryOverlay } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerTitleSummaryOverlay";
22
21
  import { usePlayerState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState";
23
22
  import { useConfiguration } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/utils";
24
23
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
@@ -200,12 +199,6 @@ export const DockedControls = (props: Props) => {
200
199
  [...Object.values(configuration), playerState.seekableDuration]
201
200
  );
202
201
 
203
- const { title, summary } = useTitleSummaryOverlay(
204
- entry,
205
- playerState?.isLive,
206
- configuration?.audio_live_player_update_interval
207
- );
208
-
209
202
  const isTablet = useIsTablet();
210
203
 
211
204
  const liveButton = Boolean(value("live_badge_enabled"));
@@ -261,11 +254,6 @@ export const DockedControls = (props: Props) => {
261
254
  };
262
255
  }, [bottomTabsVisible, insets.bottom]);
263
256
 
264
- const contentInfoContent = React.useMemo(
265
- () => ({ ...entry, title, summary }),
266
- [title, summary, entry]
267
- );
268
-
269
257
  return (
270
258
  <>
271
259
  <View
@@ -297,7 +285,7 @@ export const DockedControls = (props: Props) => {
297
285
  testID="content-info"
298
286
  style={[styles.contentInfo, backgroundColorStyle]}
299
287
  >
300
- <ContentInfo content={contentInfoContent} value={value} docked />
288
+ <ContentInfo value={value} docked />
301
289
  </View>
302
290
  </View>
303
291
  <View
@@ -78,8 +78,6 @@ export function MobileLayout({
78
78
  onSpeedButtonPress,
79
79
  playbackSpeedName,
80
80
  configuration,
81
- title,
82
- summary,
83
81
  }: Props) {
84
82
  const { docked } = layoutState;
85
83
 
@@ -165,8 +163,6 @@ export function MobileLayout({
165
163
  layoutState={layoutState}
166
164
  playerState={playerState}
167
165
  value={value}
168
- title={title}
169
- summary={summary}
170
166
  playbackSpeedName={playbackSpeedName}
171
167
  onPlaybackButtonPress={onPlaybackButtonPress}
172
168
  onRewindButtonPress={onRewindButtonPress}
@@ -75,8 +75,6 @@ export function TabletLandscapeLayout({
75
75
  onSpeedButtonPress,
76
76
  playbackSpeedName,
77
77
  configuration,
78
- title,
79
- summary,
80
78
  }: Props) {
81
79
  const [imageWidth, setImageWidth] = React.useState(0);
82
80
  const { docked } = layoutState;
@@ -132,8 +130,6 @@ export function TabletLandscapeLayout({
132
130
  layoutState={layoutState}
133
131
  playerState={playerState}
134
132
  value={value}
135
- title={title}
136
- summary={summary}
137
133
  playbackSpeedName={playbackSpeedName}
138
134
  onPlaybackButtonPress={onPlaybackButtonPress}
139
135
  onRewindButtonPress={onRewindButtonPress}
@@ -81,8 +81,6 @@ export function TabletPortraitLayout({
81
81
  onSpeedButtonPress,
82
82
  playbackSpeedName,
83
83
  configuration,
84
- title,
85
- summary,
86
84
  }: Props) {
87
85
  const { docked } = layoutState;
88
86
 
@@ -157,8 +155,6 @@ export function TabletPortraitLayout({
157
155
  layoutState={layoutState}
158
156
  playerState={playerState}
159
157
  value={value}
160
- title={title}
161
- summary={summary}
162
158
  playbackSpeedName={playbackSpeedName}
163
159
  onPlaybackButtonPress={onPlaybackButtonPress}
164
160
  onRewindButtonPress={onRewindButtonPress}
@@ -0,0 +1,147 @@
1
+ import React from "react";
2
+ import { StyleSheet } from "react-native";
3
+ import { render } from "@testing-library/react-native";
4
+
5
+ import { AudioDescription } from "../AudioDescription";
6
+
7
+ // `title`/`summary` come from the player manager's content channel rather than
8
+ // from props or context, so the hook is mocked here. Read lazily inside the
9
+ // factory: `jest.mock` is hoisted above the `let`, so capturing eagerly would
10
+ // freeze it at `undefined`.
11
+ let mockContent: {
12
+ title: Option<string | number>;
13
+ summary: Option<string | number>;
14
+ } = { title: "A Title", summary: "A Summary" };
15
+
16
+ jest.mock(
17
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerContent",
18
+ () => ({ usePlayerContent: () => mockContent })
19
+ );
20
+
21
+ beforeEach(() => {
22
+ mockContent = { title: "A Title", summary: "A Summary" };
23
+ });
24
+
25
+ const layoutState = { inline: false, docked: false, isModal: true };
26
+
27
+ const valueFrom =
28
+ (config: Record<string, unknown>): GetValue =>
29
+ (key: string) =>
30
+ (config[key] ?? null) as never;
31
+
32
+ const renderDescription = (
33
+ config: Record<string, unknown>,
34
+ state = layoutState
35
+ ) =>
36
+ render(
37
+ <AudioDescription
38
+ layoutState={state}
39
+ value={valueFrom(config)}
40
+ maxWidth={400}
41
+ />
42
+ );
43
+
44
+ // The wrapper's style prop is an array, so flatten before reading height.
45
+ const wrapperHeight = (tree) =>
46
+ StyleSheet.flatten(tree.toJSON().props.style).height;
47
+
48
+ describe("AudioDescription", () => {
49
+ it("renders both texts when both toggles are on", () => {
50
+ const { queryByText } = renderDescription({ title: true, subtitle: true });
51
+
52
+ expect(queryByText("A Title")).toBeTruthy();
53
+ expect(queryByText("A Summary")).toBeTruthy();
54
+ });
55
+
56
+ it("hides the title when the title toggle is off", () => {
57
+ const { queryByText } = renderDescription({ title: false, subtitle: true });
58
+
59
+ expect(queryByText("A Title")).toBeNull();
60
+ expect(queryByText("A Summary")).toBeTruthy();
61
+ });
62
+
63
+ it("hides the summary when the subtitle toggle is off", () => {
64
+ const { queryByText } = renderDescription({ title: true, subtitle: false });
65
+
66
+ expect(queryByText("A Title")).toBeTruthy();
67
+ expect(queryByText("A Summary")).toBeNull();
68
+ });
69
+
70
+ it("renders neither text when both toggles are off", () => {
71
+ const { queryByText } = renderDescription({
72
+ title: false,
73
+ subtitle: false,
74
+ });
75
+
76
+ expect(queryByText("A Title")).toBeNull();
77
+ expect(queryByText("A Summary")).toBeNull();
78
+ });
79
+
80
+ it("reserves no height when both toggles are off", () => {
81
+ const tree = renderDescription({ title: false, subtitle: false });
82
+
83
+ expect(wrapperHeight(tree)).toBe(0);
84
+ });
85
+
86
+ it("reserves less height when the title is hidden than when it is shown", () => {
87
+ const withTitle = renderDescription({ title: true, subtitle: true });
88
+ const withoutTitle = renderDescription({ title: false, subtitle: true });
89
+
90
+ expect(wrapperHeight(withoutTitle)).toBeLessThan(wrapperHeight(withTitle));
91
+ });
92
+
93
+ it("reserves less height when the subtitle is hidden than when it is shown", () => {
94
+ const withSubtitle = renderDescription({ title: true, subtitle: true });
95
+ const withoutSubtitle = renderDescription({ title: true, subtitle: false });
96
+
97
+ expect(wrapperHeight(withoutSubtitle)).toBeLessThan(
98
+ wrapperHeight(withSubtitle)
99
+ );
100
+ });
101
+
102
+ it("reads the docked toggle keys when docked", () => {
103
+ const { queryByText } = renderDescription(
104
+ { docked_player_title: true, docked_player_subtitle: false },
105
+ { ...layoutState, docked: true }
106
+ );
107
+
108
+ expect(queryByText("A Title")).toBeTruthy();
109
+ expect(queryByText("A Summary")).toBeNull();
110
+ });
111
+
112
+ /**
113
+ * Live audio resolves its text asynchronously: the channel feed is polled
114
+ * after playback starts, so the first render has no programme title yet. The
115
+ * row must hold its place across that transition instead of collapsing and
116
+ * expanding a second later.
117
+ */
118
+ describe("layout stability while the text is still missing", () => {
119
+ it("reserves the same height with and without text", () => {
120
+ const withText = renderDescription({ title: true, subtitle: true });
121
+
122
+ mockContent = { title: null, summary: null };
123
+
124
+ const withoutText = renderDescription({ title: true, subtitle: true });
125
+
126
+ expect(wrapperHeight(withoutText)).toBe(wrapperHeight(withText));
127
+ });
128
+
129
+ it("still renders both rows when the text has not arrived", () => {
130
+ mockContent = { title: null, summary: null };
131
+
132
+ const tree = renderDescription({ title: true, subtitle: true });
133
+
134
+ expect(wrapperHeight(tree)).toBeGreaterThan(0);
135
+ });
136
+
137
+ it("reserves height for the summary even when only the summary is missing", () => {
138
+ const withSummary = renderDescription({ title: true, subtitle: true });
139
+
140
+ mockContent = { title: "A Title", summary: null };
141
+
142
+ const withoutSummary = renderDescription({ title: true, subtitle: true });
143
+
144
+ expect(wrapperHeight(withoutSummary)).toBe(wrapperHeight(withSummary));
145
+ });
146
+ });
147
+ });