@applicaster/zapp-react-native-utils 16.0.0-alpha.5803191443 → 16.0.0-alpha.6466900632
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/actionUtils/index.ts +0 -23
- package/appUtils/contextKeysManager/utils/index.ts +38 -25
- package/appUtils/playerManager/__tests__/playerFactory.test.ts +150 -0
- package/appUtils/playerManager/playerFactory.ts +17 -34
- package/package.json +2 -2
- package/playerUtils/index.ts +20 -0
- package/actionUtils/__tests__/index.test.ts +0 -65
package/actionUtils/index.ts
CHANGED
|
@@ -1,30 +1,7 @@
|
|
|
1
1
|
import { Image } from "react-native";
|
|
2
2
|
import { isTV } from "../reactUtils";
|
|
3
3
|
|
|
4
|
-
export type Asset = [string, string] | [string];
|
|
5
|
-
|
|
6
4
|
export const resolveDefaultAssetUri = (source: string | number): string =>
|
|
7
5
|
isTV()
|
|
8
6
|
? (source as string)
|
|
9
7
|
: Image.resolveAssetSource(source as number)?.uri || "";
|
|
10
|
-
|
|
11
|
-
const isConfiguredAsset = (asset?: string | null): asset is string =>
|
|
12
|
-
typeof asset === "string" && asset.trim().length > 0;
|
|
13
|
-
|
|
14
|
-
export const resolveConfiguredAsset = (
|
|
15
|
-
configuredAsset: string | undefined,
|
|
16
|
-
fallbackAsset: string
|
|
17
|
-
): string =>
|
|
18
|
-
isConfiguredAsset(configuredAsset) ? configuredAsset : fallbackAsset;
|
|
19
|
-
|
|
20
|
-
/** Resolves assets in the following order: configured asset(can be empty), fallback asset(cell asset, can be empty), default asset */
|
|
21
|
-
export const resolveAssets = <T extends Asset>(
|
|
22
|
-
flavour1Asset: string | undefined,
|
|
23
|
-
flavour2Asset: string | undefined,
|
|
24
|
-
defaultAssets: T,
|
|
25
|
-
fallbackAssetSrc?: string
|
|
26
|
-
): T =>
|
|
27
|
-
[
|
|
28
|
-
resolveConfiguredAsset(flavour1Asset, fallbackAssetSrc || defaultAssets[0]),
|
|
29
|
-
resolveConfiguredAsset(flavour2Asset, fallbackAssetSrc || defaultAssets[1]),
|
|
30
|
-
] as T;
|
|
@@ -1,40 +1,53 @@
|
|
|
1
|
-
import * as R from "ramda";
|
|
2
1
|
import { DEFAULT_NAMESPACE } from "../consts";
|
|
3
2
|
import { KeyName, KeyNameObj } from "../index";
|
|
4
3
|
|
|
5
|
-
const lastDotRegex = /\.([^.]+)$/;
|
|
6
|
-
const splitByLastDot = R.compose(R.init, R.split(lastDotRegex));
|
|
7
|
-
|
|
8
4
|
export function getNamespaceAndKey(namespacedKey: KeyName): KeyNameObj {
|
|
9
5
|
if (typeof namespacedKey !== "string") return namespacedKey;
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
if (!namespacedKey.includes(".")) {
|
|
8
|
+
return { namespace: DEFAULT_NAMESPACE, key: namespacedKey };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// split on the last dot - the namespace itself may contain dots
|
|
12
|
+
const lastDotIndex = namespacedKey.lastIndexOf(".");
|
|
14
13
|
|
|
15
|
-
return
|
|
14
|
+
return {
|
|
15
|
+
namespace: namespacedKey.slice(0, lastDotIndex),
|
|
16
|
+
key: namespacedKey.slice(lastDotIndex + 1),
|
|
17
|
+
};
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
+
const hasOwn = (obj: object, prop: string): boolean =>
|
|
21
|
+
Object.prototype.hasOwnProperty.call(obj, prop);
|
|
22
|
+
|
|
23
|
+
const isEmpty = (value: unknown): boolean => {
|
|
24
|
+
if (typeof value === "string" || Array.isArray(value)) {
|
|
25
|
+
return value.length === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (value !== null && typeof value === "object") {
|
|
29
|
+
return Object.keys(value).length === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const keyIsValid = (parsedKey: unknown): boolean => {
|
|
36
|
+
if (parsedKey == null) return false;
|
|
37
|
+
if (typeof parsedKey === "string") return false;
|
|
38
|
+
if (Array.isArray(parsedKey)) return false;
|
|
20
39
|
|
|
21
|
-
const
|
|
40
|
+
const obj = parsedKey as Record<string, unknown>;
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
propIsNotNullOrUndefined("key"),
|
|
30
|
-
propIsNotNullOrUndefined("namespace"),
|
|
31
|
-
propIsNotEmpty("key"),
|
|
32
|
-
propIsNotEmpty("namespace"),
|
|
33
|
-
]);
|
|
42
|
+
if (!hasOwn(obj, "key") || !hasOwn(obj, "namespace")) return false;
|
|
43
|
+
if (obj.key == null || obj.namespace == null) return false;
|
|
44
|
+
if (isEmpty(obj.key) || isEmpty(obj.namespace)) return false;
|
|
45
|
+
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
34
48
|
|
|
35
49
|
export const buildNamespaceKey = (key: string, namespace: string) =>
|
|
36
50
|
`${namespace}.${key}`;
|
|
37
51
|
|
|
38
|
-
export const savingResultIsSuccess = (result: unknown): boolean =>
|
|
39
|
-
|
|
40
|
-
};
|
|
52
|
+
export const savingResultIsSuccess = (result: unknown): boolean =>
|
|
53
|
+
typeof result === "boolean" && result === true;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const mockAppStoreGet = jest.fn();
|
|
2
|
+
const mockFindPluginByIdentifier = jest.fn();
|
|
3
|
+
|
|
4
|
+
jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
|
|
5
|
+
appStore: { get: (key: string) => mockAppStoreGet(key) },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
jest.mock("@applicaster/zapp-react-native-utils/pluginUtils", () => ({
|
|
9
|
+
findPluginByIdentifier: (id: string, plugins: any) =>
|
|
10
|
+
mockFindPluginByIdentifier(id, plugins),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { PlayerRole } from "../conts";
|
|
14
|
+
import { playerFactory } from "../playerFactory";
|
|
15
|
+
|
|
16
|
+
const fakeComponent = () => null;
|
|
17
|
+
|
|
18
|
+
class FakeController {
|
|
19
|
+
receivedConfig: any;
|
|
20
|
+
constructor(config: any) {
|
|
21
|
+
this.receivedConfig = config;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const baseConfig = {
|
|
26
|
+
player: {},
|
|
27
|
+
playerId: "p1",
|
|
28
|
+
autoplay: false,
|
|
29
|
+
entry: { id: "entry-1" } as any,
|
|
30
|
+
muted: false,
|
|
31
|
+
playerPluginId: "test-plugin",
|
|
32
|
+
screenConfig: { foo: "bar" },
|
|
33
|
+
playerRole: PlayerRole.Cell,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const installPluginWithProtocol = (
|
|
37
|
+
protocolReturn: any | ((screenConfig: any, entry: any) => any)
|
|
38
|
+
) => {
|
|
39
|
+
const playerProtocol =
|
|
40
|
+
typeof protocolReturn === "function"
|
|
41
|
+
? protocolReturn
|
|
42
|
+
: () => protocolReturn;
|
|
43
|
+
|
|
44
|
+
mockAppStoreGet.mockImplementation((key: string) =>
|
|
45
|
+
key === "plugins" ? { "test-plugin": {} } : null
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
mockFindPluginByIdentifier.mockReturnValue({
|
|
49
|
+
module: { playerProtocol },
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe("playerFactory", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
mockAppStoreGet.mockReset();
|
|
56
|
+
mockFindPluginByIdentifier.mockReset();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("resolves with controller and Component when playerProtocol returns synchronously", async () => {
|
|
60
|
+
installPluginWithProtocol({
|
|
61
|
+
Component: fakeComponent,
|
|
62
|
+
controllerClass: FakeController,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const item = await playerFactory(baseConfig);
|
|
66
|
+
|
|
67
|
+
expect(item).not.toBeNull();
|
|
68
|
+
expect(item?.Component).toBe(fakeComponent);
|
|
69
|
+
expect(item?.controller).toBeInstanceOf(FakeController);
|
|
70
|
+
|
|
71
|
+
expect((item?.controller as FakeController).receivedConfig).toBe(
|
|
72
|
+
baseConfig
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("resolves with controller and Component when playerProtocol returns a Promise (delayed import)", async () => {
|
|
77
|
+
installPluginWithProtocol(async () => ({
|
|
78
|
+
Component: fakeComponent,
|
|
79
|
+
controllerClass: FakeController,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
const item = await playerFactory(baseConfig);
|
|
83
|
+
|
|
84
|
+
expect(item).not.toBeNull();
|
|
85
|
+
expect(item?.Component).toBe(fakeComponent);
|
|
86
|
+
expect(item?.controller).toBeInstanceOf(FakeController);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("passes screenConfig and entry through to playerProtocol", async () => {
|
|
90
|
+
const playerProtocol = jest.fn(() => ({
|
|
91
|
+
Component: fakeComponent,
|
|
92
|
+
controllerClass: FakeController,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
mockAppStoreGet.mockImplementation((key: string) =>
|
|
96
|
+
key === "plugins" ? { "test-plugin": {} } : null
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
mockFindPluginByIdentifier.mockReturnValue({ module: { playerProtocol } });
|
|
100
|
+
|
|
101
|
+
await playerFactory(baseConfig);
|
|
102
|
+
|
|
103
|
+
expect(playerProtocol).toHaveBeenCalledWith(
|
|
104
|
+
baseConfig.screenConfig,
|
|
105
|
+
baseConfig.entry
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("resolves to null when playerPluginId is missing", async () => {
|
|
110
|
+
expect(
|
|
111
|
+
await playerFactory({ ...baseConfig, playerPluginId: "" } as any)
|
|
112
|
+
).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("resolves to null when appStore has no plugins", async () => {
|
|
116
|
+
mockAppStoreGet.mockReturnValue(null);
|
|
117
|
+
|
|
118
|
+
expect(await playerFactory(baseConfig)).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("resolves to null when the plugin has no playerProtocol", async () => {
|
|
122
|
+
mockAppStoreGet.mockImplementation((key: string) =>
|
|
123
|
+
key === "plugins" ? { "test-plugin": {} } : null
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
mockFindPluginByIdentifier.mockReturnValue({ module: {} });
|
|
127
|
+
|
|
128
|
+
expect(await playerFactory(baseConfig)).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("resolves to null when playerProtocol returns without a Component", async () => {
|
|
132
|
+
installPluginWithProtocol({ controllerClass: FakeController });
|
|
133
|
+
|
|
134
|
+
expect(await playerFactory(baseConfig)).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("resolves to null when playerProtocol returns without a controllerClass", async () => {
|
|
138
|
+
installPluginWithProtocol({ Component: fakeComponent });
|
|
139
|
+
|
|
140
|
+
expect(await playerFactory(baseConfig)).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("rejects when playerProtocol throws synchronously", async () => {
|
|
144
|
+
installPluginWithProtocol(() => {
|
|
145
|
+
throw new Error("boom");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await expect(playerFactory(baseConfig)).rejects.toThrow("boom");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -19,52 +19,35 @@ type PlayerFactoryProps = {
|
|
|
19
19
|
playerRole: PlayerRole;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
interface
|
|
22
|
+
interface PlayerControllerConstructor {
|
|
23
23
|
new (params: any): Player;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
type PlayerProtocol = {
|
|
27
|
+
Component: any;
|
|
28
|
+
controllerClass: PlayerControllerConstructor;
|
|
29
|
+
};
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
export const playerFactory = async (
|
|
32
|
+
config: PlayerFactoryProps
|
|
33
|
+
): Promise<PlayerFactoryItem | null> => {
|
|
34
|
+
if (!config?.playerPluginId) return null;
|
|
34
35
|
|
|
35
36
|
const plugins = appStore.get("plugins");
|
|
37
|
+
if (!plugins) return null;
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const playerPlugin = findPluginByIdentifier(IDENTIFIER, plugins);
|
|
39
|
+
const playerPlugin = findPluginByIdentifier(config.playerPluginId, plugins);
|
|
40
|
+
if (!playerPlugin?.module?.playerProtocol) return null;
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const playerProtocol: PlayerConstructor = playerPlugin.module.playerProtocol(
|
|
48
|
-
config?.screenConfig,
|
|
49
|
-
config?.entry
|
|
50
|
-
);
|
|
42
|
+
const playerProtocol: PlayerProtocol | null =
|
|
43
|
+
await playerPlugin.module.playerProtocol(config.screenConfig, config.entry);
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!playerView) {
|
|
45
|
+
if (!playerProtocol?.Component || !playerProtocol.controllerClass) {
|
|
55
46
|
return null;
|
|
56
47
|
}
|
|
57
48
|
|
|
58
|
-
const Controller = (playerProtocol as any)?.controllerClass || null;
|
|
59
|
-
|
|
60
|
-
if (Controller === null) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const controller = new Controller(config);
|
|
65
|
-
|
|
66
49
|
return {
|
|
67
|
-
controller,
|
|
68
|
-
Component:
|
|
50
|
+
controller: new playerProtocol.controllerClass(config),
|
|
51
|
+
Component: playerProtocol.Component,
|
|
69
52
|
};
|
|
70
53
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "16.0.0-alpha.
|
|
3
|
+
"version": "16.0.0-alpha.6466900632",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "16.0.0-alpha.
|
|
30
|
+
"@applicaster/applicaster-types": "16.0.0-alpha.6466900632",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
package/playerUtils/index.ts
CHANGED
|
@@ -9,6 +9,16 @@ import { Dimensions } from "react-native";
|
|
|
9
9
|
|
|
10
10
|
export { getPlayerActionButtons } from "./getPlayerActionButtons";
|
|
11
11
|
|
|
12
|
+
export const HLS_MIME_TYPES = [
|
|
13
|
+
"application/x-mpegURL",
|
|
14
|
+
"application/vnd.apple.mpegurl",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const STREAMING_VIDEO_MIME_TYPES = [
|
|
18
|
+
...HLS_MIME_TYPES,
|
|
19
|
+
"application/dash+xml",
|
|
20
|
+
];
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* Gets duration value from player manager, and from extensions
|
|
14
24
|
* then checks whether the value from either is a not a valid number
|
|
@@ -80,6 +90,16 @@ export function isLive(entry: ZappEntry): boolean {
|
|
|
80
90
|
return isEntryLive(entry) || isLiveByManager();
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
export const isVideoItem = (item: Option<ZappEntry>) => {
|
|
94
|
+
const contentType = item?.content?.type;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
isString(contentType) &&
|
|
98
|
+
(contentType.includes("video") ||
|
|
99
|
+
STREAMING_VIDEO_MIME_TYPES.includes(contentType))
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
83
103
|
export const isAudioItem = (item: Option<ZappEntry>) => {
|
|
84
104
|
if (
|
|
85
105
|
isString(item?.content?.type) &&
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
resolveAssets,
|
|
3
|
-
resolveConfiguredAsset,
|
|
4
|
-
} from "@applicaster/zapp-react-native-utils/actionUtils";
|
|
5
|
-
|
|
6
|
-
describe("actionUtils", () => {
|
|
7
|
-
describe("resolveConfiguredAsset", () => {
|
|
8
|
-
it("returns configured asset when value is a non-empty string", () => {
|
|
9
|
-
expect(resolveConfiguredAsset("configured", "fallback")).toBe(
|
|
10
|
-
"configured"
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
expect(
|
|
14
|
-
resolveConfiguredAsset("https://example.com/icon.png", "fallback")
|
|
15
|
-
).toBe("https://example.com/icon.png");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("returns fallback when configured asset is empty or undefined", () => {
|
|
19
|
-
expect(resolveConfiguredAsset("", "fallback")).toBe("fallback");
|
|
20
|
-
expect(resolveConfiguredAsset(" ", "fallback")).toBe("fallback");
|
|
21
|
-
expect(resolveConfiguredAsset(" ", "fallback")).toBe("fallback");
|
|
22
|
-
expect(resolveConfiguredAsset(undefined, "fallback")).toBe("fallback");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("returns fallback for non-string configured asset values", () => {
|
|
26
|
-
expect(resolveConfiguredAsset(null as any, "fallback")).toBe("fallback");
|
|
27
|
-
expect(resolveConfiguredAsset(123 as any, "fallback")).toBe("fallback");
|
|
28
|
-
expect(resolveConfiguredAsset({} as any, "fallback")).toBe("fallback");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("resolveAssets", () => {
|
|
33
|
-
it("returns configured assets when both flavour values are configured", () => {
|
|
34
|
-
expect(
|
|
35
|
-
resolveAssets("asset-1", "asset-2", ["default-1", "default-2"])
|
|
36
|
-
).toEqual(["asset-1", "asset-2"]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("falls back to code defaults when configured flavour values are empty", () => {
|
|
40
|
-
expect(resolveAssets("", undefined, ["default-1", "default-2"])).toEqual([
|
|
41
|
-
"default-1",
|
|
42
|
-
"default-2",
|
|
43
|
-
]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("falls back per flavour independently", () => {
|
|
47
|
-
expect(resolveAssets("", "asset-2", ["default-1", "default-2"])).toEqual([
|
|
48
|
-
"default-1",
|
|
49
|
-
"asset-2",
|
|
50
|
-
]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("uses the cell fallback before code defaults", () => {
|
|
54
|
-
expect(
|
|
55
|
-
resolveAssets("", undefined, ["default-1", "default-2"], "cell")
|
|
56
|
-
).toEqual(["cell", "cell"]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("preserves configured assets before using the cell fallback", () => {
|
|
60
|
-
expect(
|
|
61
|
-
resolveAssets("asset-1", "", ["default-1", "default-2"], "cell")
|
|
62
|
-
).toEqual(["asset-1", "cell"]);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
});
|