nico-tools 1.0.0 → 1.1.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/bin/cli.js +9 -13
- package/bin/commands/create-project.js +106 -0
- package/bin/commands/translate.js +12 -0
- package/bin/config/templates.js +9 -0
- package/bin/utils/file-utils.js +84 -0
- package/bin/utils/project-utils.js +49 -0
- package/package.json +4 -2
- package/templates/react-native/expo-clean-architecture/README.md +154 -0
- package/templates/react-native/expo-clean-architecture/app.config.ts +61 -0
- package/templates/react-native/expo-clean-architecture/assets/config/.gitkeep +3 -0
- package/templates/react-native/expo-clean-architecture/assets/fonts/SpaceMono-Regular.ttf +0 -0
- package/templates/react-native/expo-clean-architecture/assets/images/adaptive-icon.png +0 -0
- package/templates/react-native/expo-clean-architecture/assets/images/favicon.png +0 -0
- package/templates/react-native/expo-clean-architecture/assets/images/icon.png +0 -0
- package/templates/react-native/expo-clean-architecture/assets/images/partial-react-logo.png +0 -0
- package/templates/react-native/expo-clean-architecture/assets/images/react-logo.png +0 -0
- package/templates/react-native/expo-clean-architecture/assets/images/react-logo@2x.png +0 -0
- package/templates/react-native/expo-clean-architecture/assets/images/react-logo@3x.png +0 -0
- package/templates/react-native/expo-clean-architecture/assets/images/splash-icon.png +0 -0
- package/templates/react-native/expo-clean-architecture/babel.config.js +11 -0
- package/templates/react-native/expo-clean-architecture/docs/00-introduction.md +3 -0
- package/templates/react-native/expo-clean-architecture/docs/01-architecture.md +107 -0
- package/templates/react-native/expo-clean-architecture/package.json +78 -0
- package/templates/react-native/expo-clean-architecture/scripts/clean-src.sh +48 -0
- package/templates/react-native/expo-clean-architecture/scripts/generate-feature.sh +40 -0
- package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/_layout.tsx +42 -0
- package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/favorites.tsx +72 -0
- package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/home.tsx +122 -0
- package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/settings/_layout.tsx +5 -0
- package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/settings/index.tsx +29 -0
- package/templates/react-native/expo-clean-architecture/src/app/(protected)/(tabs)/settings/profile.tsx +22 -0
- package/templates/react-native/expo-clean-architecture/src/app/(protected)/_layout.tsx +20 -0
- package/templates/react-native/expo-clean-architecture/src/app/(protected)/details.tsx +124 -0
- package/templates/react-native/expo-clean-architecture/src/app/(public)/_layout.tsx +18 -0
- package/templates/react-native/expo-clean-architecture/src/app/(public)/login.tsx +31 -0
- package/templates/react-native/expo-clean-architecture/src/app/_layout.tsx +33 -0
- package/templates/react-native/expo-clean-architecture/src/app/index.tsx +8 -0
- package/templates/react-native/expo-clean-architecture/src/core/constants/api-constants.ts +10 -0
- package/templates/react-native/expo-clean-architecture/src/core/constants/image-constants.ts +3 -0
- package/templates/react-native/expo-clean-architecture/src/core/constants/query-keys.ts +6 -0
- package/templates/react-native/expo-clean-architecture/src/core/constants/storage-keys.ts +3 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/@types/color-scheme-state.ts +35 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/@types/color-scheme.ts +12 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/components/app-icon.tsx +16 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/components/app-separator.tsx +26 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/hooks/use-app-color-scheme.ts +52 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/hooks/use-app-fonts.ts +12 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/hooks/use-app-styles.ts +28 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/theme/app-colors.ts +21 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/theme/app-fonts.ts +16 -0
- package/templates/react-native/expo-clean-architecture/src/core/design/theme/app-sizes.ts +14 -0
- package/templates/react-native/expo-clean-architecture/src/core/di/injection-container.ts +53 -0
- package/templates/react-native/expo-clean-architecture/src/core/errors/index.ts +1 -0
- package/templates/react-native/expo-clean-architecture/src/core/helpers/@types.ts +23 -0
- package/templates/react-native/expo-clean-architecture/src/core/helpers/rest-client.ts +144 -0
- package/templates/react-native/expo-clean-architecture/src/core/helpers/result.ts +37 -0
- package/templates/react-native/expo-clean-architecture/src/core/helpers/usecase.ts +5 -0
- package/templates/react-native/expo-clean-architecture/src/core/hooks/use-network.ts +18 -0
- package/templates/react-native/expo-clean-architecture/src/core/i18n/@types/i18next.d.ts +11 -0
- package/templates/react-native/expo-clean-architecture/src/core/i18n/index.ts +19 -0
- package/templates/react-native/expo-clean-architecture/src/core/i18n/translations/fr.json +12 -0
- package/templates/react-native/expo-clean-architecture/src/core/services/local-storage-service-impl.ts +29 -0
- package/templates/react-native/expo-clean-architecture/src/core/services/local-storage-service.ts +26 -0
- package/templates/react-native/expo-clean-architecture/src/core/services/monitoring-service-impl.ts +15 -0
- package/templates/react-native/expo-clean-architecture/src/core/services/monitoring-service.ts +13 -0
- package/templates/react-native/expo-clean-architecture/src/core/services/network-service-impl.ts +40 -0
- package/templates/react-native/expo-clean-architecture/src/core/services/network-service.ts +16 -0
- package/templates/react-native/expo-clean-architecture/src/features/auth/@types/session-state.ts +38 -0
- package/templates/react-native/expo-clean-architecture/src/features/auth/@types/session-status-enum.ts +16 -0
- package/templates/react-native/expo-clean-architecture/src/features/auth/presentation/hooks/use-session.ts +18 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/data/datasources/favorites-datasource-impl.ts +25 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/data/datasources/favorites-datasource.ts +5 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/data/repositories/favorites-repository-impl.ts +46 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/repositories/favorites-repository.ts +8 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/add-to-favorites-usecase.ts +23 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/clear-favorites-usecase.ts +23 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/get-favorites-usecase.ts +24 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/domain/usecases/remove-from-favorites-usecase.ts +23 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/components/favorites-card.tsx +77 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-add-favorite.ts +24 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-clear-favorites.ts +22 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-favorites.ts +22 -0
- package/templates/react-native/expo-clean-architecture/src/features/favorites/presentation/hooks/use-remove-favorite.ts +24 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/data/datasources/movies-datasource-impl.ts +50 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/data/datasources/movies-datasource.ts +8 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/data/models/movie-details-model.ts +67 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/data/models/movie-model.ts +30 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/data/models/tmdb-response-model.ts +6 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/data/repositories/movies-repository-impl.ts +34 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/domain/entities/movie-details-entity.ts +28 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/domain/entities/movie-entity.ts +6 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/domain/repositories/movies-repository.ts +25 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/domain/usecases/get-movie-details-usecase.ts +26 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/domain/usecases/get-random-movies-usecase.ts +24 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/domain/usecases/search-movies-usecase.ts +23 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/components/movie-tile.tsx +69 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/hooks/use-movie-details.ts +22 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/hooks/use-random-movies.ts +22 -0
- package/templates/react-native/expo-clean-architecture/src/features/movies/presentation/hooks/use-search-movies.ts +22 -0
- package/templates/react-native/expo-clean-architecture/tests/core/services/local-storage-service-impl.test.ts +108 -0
- package/templates/react-native/expo-clean-architecture/tests/core/services/monitoring-service.test.ts +74 -0
- package/templates/react-native/expo-clean-architecture/tests/core/services/network-service.test.ts +117 -0
- package/templates/react-native/expo-clean-architecture/tests/features/auth/presentation/hooks/use-session.test.ts +69 -0
- package/templates/react-native/expo-clean-architecture/tests/features/favorites/data/datasources/favorites-datasource.test.ts +69 -0
- package/templates/react-native/expo-clean-architecture/tests/features/favorites/data/repositories/favorites-repository-impl.test.ts +124 -0
- package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/add-to-favorites-usecase.test.ts +54 -0
- package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/clear-favorites-usecase.test.ts +44 -0
- package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/get-favorites-usecase.test.ts +74 -0
- package/templates/react-native/expo-clean-architecture/tests/features/favorites/domain/usecases/remove-from-favorites-usecase.test.ts +52 -0
- package/templates/react-native/expo-clean-architecture/tests/setup.ts +9 -0
- package/templates/react-native/expo-clean-architecture/tsconfig.json +20 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { LocalStorageServiceImpl } from "@/core/services/local-storage-service-impl";
|
|
2
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
3
|
+
|
|
4
|
+
// Mock AsyncStorage
|
|
5
|
+
jest.mock("@react-native-async-storage/async-storage", () => ({
|
|
6
|
+
getItem: jest.fn(),
|
|
7
|
+
setItem: jest.fn(),
|
|
8
|
+
removeItem: jest.fn(),
|
|
9
|
+
clear: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe("LocalStorageService", () => {
|
|
13
|
+
let service: LocalStorageServiceImpl;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Clear all mocks before each test
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
service = new LocalStorageServiceImpl();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("getItem", () => {
|
|
22
|
+
it("should return stringified value when item exists", async () => {
|
|
23
|
+
const mockValue = { test: "data" };
|
|
24
|
+
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(
|
|
25
|
+
JSON.stringify(mockValue)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const result = await service.getItem("test-key", "");
|
|
29
|
+
expect(result).toEqual(JSON.stringify(mockValue));
|
|
30
|
+
expect(AsyncStorage.getItem).toHaveBeenCalledWith("test-key");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return default value when item doesn't exist", async () => {
|
|
34
|
+
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(null);
|
|
35
|
+
const defaultValue = { default: "value" };
|
|
36
|
+
|
|
37
|
+
const result = await service.getItem(
|
|
38
|
+
"test-key",
|
|
39
|
+
JSON.stringify(defaultValue)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(result).toEqual(JSON.stringify(defaultValue));
|
|
43
|
+
expect(AsyncStorage.getItem).toHaveBeenCalledWith("test-key");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("setItem", () => {
|
|
48
|
+
it("should store stringified value", async () => {
|
|
49
|
+
const value = { test: "data" };
|
|
50
|
+
await service.setItem("test-key", JSON.stringify(value));
|
|
51
|
+
|
|
52
|
+
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
53
|
+
"test-key",
|
|
54
|
+
JSON.stringify(value)
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("removeItem", () => {
|
|
60
|
+
it("should remove item from storage", async () => {
|
|
61
|
+
await service.removeItem("test-key");
|
|
62
|
+
expect(AsyncStorage.removeItem).toHaveBeenCalledWith("test-key");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("clear", () => {
|
|
67
|
+
it("should clear all items from storage", async () => {
|
|
68
|
+
await service.clear();
|
|
69
|
+
expect(AsyncStorage.clear).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("error handling", () => {
|
|
74
|
+
it("should throw error when getItem fails", async () => {
|
|
75
|
+
const error = new Error("Storage error");
|
|
76
|
+
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(error);
|
|
77
|
+
|
|
78
|
+
await expect(service.getItem("test-key", "")).rejects.toThrow(
|
|
79
|
+
"Storage error"
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should throw error when setItem fails", async () => {
|
|
84
|
+
const error = new Error("Storage error");
|
|
85
|
+
(AsyncStorage.setItem as jest.Mock).mockRejectedValue(error);
|
|
86
|
+
|
|
87
|
+
await expect(service.setItem("test-key", "value")).rejects.toThrow(
|
|
88
|
+
"Storage error"
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should throw error when removeItem fails", async () => {
|
|
93
|
+
const error = new Error("Storage error");
|
|
94
|
+
(AsyncStorage.removeItem as jest.Mock).mockRejectedValue(error);
|
|
95
|
+
|
|
96
|
+
await expect(service.removeItem("test-key")).rejects.toThrow(
|
|
97
|
+
"Storage error"
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should throw error when clear fails", async () => {
|
|
102
|
+
const error = new Error("Storage error");
|
|
103
|
+
(AsyncStorage.clear as jest.Mock).mockRejectedValue(error);
|
|
104
|
+
|
|
105
|
+
await expect(service.clear()).rejects.toThrow("Storage error");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { MonitoringServiceImpl } from "@/core/services/monitoring-service-impl";
|
|
2
|
+
import {
|
|
3
|
+
recordError,
|
|
4
|
+
getCrashlytics,
|
|
5
|
+
} from "@react-native-firebase/crashlytics";
|
|
6
|
+
|
|
7
|
+
// Mock @react-native-firebase/crashlytics
|
|
8
|
+
jest.mock("@react-native-firebase/crashlytics", () => ({
|
|
9
|
+
getCrashlytics: jest.fn(),
|
|
10
|
+
recordError: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe("MonitoringServiceImpl", () => {
|
|
14
|
+
let monitoringService: MonitoringServiceImpl;
|
|
15
|
+
let mockCrashlytics: any;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Reset all mocks before each test
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
|
|
21
|
+
// Setup mock crashlytics instance
|
|
22
|
+
mockCrashlytics = {};
|
|
23
|
+
(getCrashlytics as jest.Mock).mockReturnValue(mockCrashlytics);
|
|
24
|
+
|
|
25
|
+
monitoringService = new MonitoringServiceImpl();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("report", () => {
|
|
29
|
+
it("should report error to crashlytics", () => {
|
|
30
|
+
const testError = new Error("Test error");
|
|
31
|
+
|
|
32
|
+
monitoringService.report(testError);
|
|
33
|
+
|
|
34
|
+
expect(getCrashlytics).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(recordError).toHaveBeenCalledWith(mockCrashlytics, testError);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should handle different types of errors", () => {
|
|
39
|
+
// Test with TypeError
|
|
40
|
+
const typeError = new TypeError("Type error");
|
|
41
|
+
monitoringService.report(typeError);
|
|
42
|
+
expect(recordError).toHaveBeenCalledWith(mockCrashlytics, typeError);
|
|
43
|
+
|
|
44
|
+
// Test with ReferenceError
|
|
45
|
+
const referenceError = new ReferenceError("Reference error");
|
|
46
|
+
monitoringService.report(referenceError);
|
|
47
|
+
expect(recordError).toHaveBeenCalledWith(mockCrashlytics, referenceError);
|
|
48
|
+
|
|
49
|
+
// Test with custom error
|
|
50
|
+
const customError = new Error("Custom error");
|
|
51
|
+
customError.name = "CustomError";
|
|
52
|
+
monitoringService.report(customError);
|
|
53
|
+
expect(recordError).toHaveBeenCalledWith(mockCrashlytics, customError);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should handle error with additional properties", () => {
|
|
57
|
+
const error = new Error("Error with properties");
|
|
58
|
+
(error as any).code = "TEST_ERROR";
|
|
59
|
+
(error as any).metadata = { userId: "123" };
|
|
60
|
+
|
|
61
|
+
monitoringService.report(error);
|
|
62
|
+
|
|
63
|
+
expect(recordError).toHaveBeenCalledWith(mockCrashlytics, error);
|
|
64
|
+
// Verify that the error object with additional properties is passed as is
|
|
65
|
+
expect(recordError).toHaveBeenCalledWith(
|
|
66
|
+
mockCrashlytics,
|
|
67
|
+
expect.objectContaining({
|
|
68
|
+
code: "TEST_ERROR",
|
|
69
|
+
metadata: { userId: "123" },
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
package/templates/react-native/expo-clean-architecture/tests/core/services/network-service.test.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { NetworkServiceImpl } from "@/core/services/network-service-impl";
|
|
2
|
+
import {
|
|
3
|
+
getNetworkStateAsync,
|
|
4
|
+
addNetworkStateListener,
|
|
5
|
+
NetworkState,
|
|
6
|
+
} from "expo-network";
|
|
7
|
+
|
|
8
|
+
// Mock expo-network
|
|
9
|
+
jest.mock("expo-network", () => ({
|
|
10
|
+
getNetworkStateAsync: jest.fn(),
|
|
11
|
+
addNetworkStateListener: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe("NetworkServiceImpl", () => {
|
|
15
|
+
let networkService: NetworkServiceImpl;
|
|
16
|
+
let mockNetworkStateListener: (state: NetworkState) => void;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Reset all mocks before each test
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
|
|
22
|
+
// Setup the mock for addNetworkStateListener to capture the callback
|
|
23
|
+
(addNetworkStateListener as jest.Mock).mockImplementation((callback) => {
|
|
24
|
+
mockNetworkStateListener = callback;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
networkService = new NetworkServiceImpl();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("isOnline", () => {
|
|
31
|
+
it("should return true when network is reachable", async () => {
|
|
32
|
+
(getNetworkStateAsync as jest.Mock).mockResolvedValue({
|
|
33
|
+
isInternetReachable: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await networkService.isOnline();
|
|
37
|
+
expect(result).toBe(true);
|
|
38
|
+
expect(getNetworkStateAsync).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should return false when network is not reachable", async () => {
|
|
42
|
+
(getNetworkStateAsync as jest.Mock).mockResolvedValue({
|
|
43
|
+
isInternetReachable: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const result = await networkService.isOnline();
|
|
47
|
+
expect(result).toBe(false);
|
|
48
|
+
expect(getNetworkStateAsync).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should return false when isInternetReachable is null", async () => {
|
|
52
|
+
(getNetworkStateAsync as jest.Mock).mockResolvedValue({
|
|
53
|
+
isInternetReachable: null,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await networkService.isOnline();
|
|
57
|
+
expect(result).toBe(false);
|
|
58
|
+
expect(getNetworkStateAsync).toHaveBeenCalledTimes(1);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("subscribe", () => {
|
|
63
|
+
it("should call callback with initial network state", async () => {
|
|
64
|
+
const mockCallback = jest.fn();
|
|
65
|
+
(getNetworkStateAsync as jest.Mock).mockResolvedValue({
|
|
66
|
+
isInternetReachable: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
networkService.subscribe(mockCallback);
|
|
70
|
+
|
|
71
|
+
// Wait for the initial state check
|
|
72
|
+
await new Promise(process.nextTick);
|
|
73
|
+
|
|
74
|
+
expect(mockCallback).toHaveBeenCalledWith(true);
|
|
75
|
+
expect(getNetworkStateAsync).toHaveBeenCalledTimes(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should notify subscribers when network state changes", async () => {
|
|
79
|
+
const mockCallback1 = jest.fn();
|
|
80
|
+
const mockCallback2 = jest.fn();
|
|
81
|
+
|
|
82
|
+
networkService.subscribe(mockCallback1);
|
|
83
|
+
networkService.subscribe(mockCallback2);
|
|
84
|
+
|
|
85
|
+
// Simulate network state change
|
|
86
|
+
mockNetworkStateListener({ isInternetReachable: true });
|
|
87
|
+
expect(mockCallback1).toHaveBeenCalledWith(true);
|
|
88
|
+
expect(mockCallback2).toHaveBeenCalledWith(true);
|
|
89
|
+
|
|
90
|
+
// Simulate another network state change
|
|
91
|
+
mockNetworkStateListener({ isInternetReachable: false });
|
|
92
|
+
expect(mockCallback1).toHaveBeenCalledWith(false);
|
|
93
|
+
expect(mockCallback2).toHaveBeenCalledWith(false);
|
|
94
|
+
|
|
95
|
+
// Simulate network state change with undefined isInternetReachable
|
|
96
|
+
mockNetworkStateListener({ isInternetReachable: undefined });
|
|
97
|
+
expect(mockCallback1).toHaveBeenCalledWith(false);
|
|
98
|
+
expect(mockCallback2).toHaveBeenCalledWith(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should allow unsubscribing from network state changes", async () => {
|
|
102
|
+
const mockCallback = jest.fn();
|
|
103
|
+
const unsubscribe = networkService.subscribe(mockCallback);
|
|
104
|
+
|
|
105
|
+
// Simulate network state change
|
|
106
|
+
mockNetworkStateListener({ isInternetReachable: true });
|
|
107
|
+
expect(mockCallback).toHaveBeenCalledWith(true);
|
|
108
|
+
|
|
109
|
+
// Unsubscribe
|
|
110
|
+
unsubscribe();
|
|
111
|
+
|
|
112
|
+
// Simulate another network state change
|
|
113
|
+
mockNetworkStateListener({ isInternetReachable: false });
|
|
114
|
+
expect(mockCallback).not.toHaveBeenCalledWith(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useSession } from "@/features/auth/presentation/hooks/use-session";
|
|
2
|
+
import { SessionStatus } from "@/features/auth/@types/session-status-enum";
|
|
3
|
+
|
|
4
|
+
describe("useSession", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Reset the store to its initial state before each test
|
|
7
|
+
useSession.setState({
|
|
8
|
+
status: SessionStatus.UNAUTHENTICATED,
|
|
9
|
+
isInitialized: false,
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("initial state", () => {
|
|
14
|
+
it("should have correct initial state", () => {
|
|
15
|
+
const state = useSession.getState();
|
|
16
|
+
|
|
17
|
+
expect(state.status).toBe(SessionStatus.UNAUTHENTICATED);
|
|
18
|
+
expect(state.isInitialized).toBe(false);
|
|
19
|
+
expect(state.isAuthenticated()).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("login", () => {
|
|
24
|
+
it("should update status to AUTHENTICATED when login is called", async () => {
|
|
25
|
+
await useSession.getState().login();
|
|
26
|
+
|
|
27
|
+
const state = useSession.getState();
|
|
28
|
+
expect(state.status).toBe(SessionStatus.AUTHENTICATED);
|
|
29
|
+
expect(state.isAuthenticated()).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("logout", () => {
|
|
34
|
+
it("should update status to UNAUTHENTICATED when logout is called", async () => {
|
|
35
|
+
// First login to set authenticated state
|
|
36
|
+
await useSession.getState().login();
|
|
37
|
+
expect(useSession.getState().status).toBe(SessionStatus.AUTHENTICATED);
|
|
38
|
+
|
|
39
|
+
// Then logout
|
|
40
|
+
await useSession.getState().logout();
|
|
41
|
+
|
|
42
|
+
const state = useSession.getState();
|
|
43
|
+
expect(state.status).toBe(SessionStatus.UNAUTHENTICATED);
|
|
44
|
+
expect(state.isAuthenticated()).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("silentLogin", () => {
|
|
49
|
+
it("should update status to AUTHENTICATED when silentLogin is called", async () => {
|
|
50
|
+
await useSession.getState().silentLogin();
|
|
51
|
+
|
|
52
|
+
const state = useSession.getState();
|
|
53
|
+
expect(state.status).toBe(SessionStatus.AUTHENTICATED);
|
|
54
|
+
expect(state.isAuthenticated()).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("isAuthenticated", () => {
|
|
59
|
+
it("should return true when status is AUTHENTICATED", () => {
|
|
60
|
+
useSession.setState({ status: SessionStatus.AUTHENTICATED });
|
|
61
|
+
expect(useSession.getState().isAuthenticated()).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should return false when status is UNAUTHENTICATED", () => {
|
|
65
|
+
useSession.setState({ status: SessionStatus.UNAUTHENTICATED });
|
|
66
|
+
expect(useSession.getState().isAuthenticated()).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { LocalStorageService } from "@/core/services/local-storage-service";
|
|
2
|
+
import { STORAGE_KEYS } from "@/core/constants/storage-keys";
|
|
3
|
+
import { FavoritesDatasourceImpl } from "@/features/favorites/data/datasources/favorites-datasource-impl";
|
|
4
|
+
|
|
5
|
+
describe("FavoritesDatasourceImpl", () => {
|
|
6
|
+
let datasource: FavoritesDatasourceImpl;
|
|
7
|
+
let mockLocalStorageService: jest.Mocked<LocalStorageService>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockLocalStorageService = {
|
|
11
|
+
getItem: jest.fn(),
|
|
12
|
+
setItem: jest.fn(),
|
|
13
|
+
removeItem: jest.fn(),
|
|
14
|
+
clear: jest.fn(),
|
|
15
|
+
} as unknown as jest.Mocked<LocalStorageService>;
|
|
16
|
+
|
|
17
|
+
datasource = new FavoritesDatasourceImpl(mockLocalStorageService);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("getFavorites", () => {
|
|
21
|
+
it("should return favorites from localStorage with default empty array", async () => {
|
|
22
|
+
const mockFavorites = '["movie1", "movie2"]';
|
|
23
|
+
mockLocalStorageService.getItem.mockResolvedValue(mockFavorites);
|
|
24
|
+
|
|
25
|
+
const result = await datasource.getFavorites();
|
|
26
|
+
|
|
27
|
+
expect(mockLocalStorageService.getItem).toHaveBeenCalledWith(
|
|
28
|
+
STORAGE_KEYS.FAVORITES,
|
|
29
|
+
"[]"
|
|
30
|
+
);
|
|
31
|
+
expect(result).toBe(mockFavorites);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should return default empty array when no favorites exist", async () => {
|
|
35
|
+
mockLocalStorageService.getItem.mockResolvedValue("[]");
|
|
36
|
+
|
|
37
|
+
const result = await datasource.getFavorites();
|
|
38
|
+
|
|
39
|
+
expect(mockLocalStorageService.getItem).toHaveBeenCalledWith(
|
|
40
|
+
STORAGE_KEYS.FAVORITES,
|
|
41
|
+
"[]"
|
|
42
|
+
);
|
|
43
|
+
expect(result).toBe("[]");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("setFavorites", () => {
|
|
48
|
+
it("should save favorites to localStorage", async () => {
|
|
49
|
+
const mockFavorites = '["movie1", "movie2"]';
|
|
50
|
+
|
|
51
|
+
await datasource.setFavorites(mockFavorites);
|
|
52
|
+
|
|
53
|
+
expect(mockLocalStorageService.setItem).toHaveBeenCalledWith(
|
|
54
|
+
STORAGE_KEYS.FAVORITES,
|
|
55
|
+
mockFavorites
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("clearFavorites", () => {
|
|
61
|
+
it("should remove favorites from localStorage", async () => {
|
|
62
|
+
await datasource.clearFavorites();
|
|
63
|
+
|
|
64
|
+
expect(mockLocalStorageService.removeItem).toHaveBeenCalledWith(
|
|
65
|
+
STORAGE_KEYS.FAVORITES
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { FavoritesRepositoryImpl } from "@/features/favorites/data/repositories/favorites-repository-impl";
|
|
2
|
+
import { FavoritesDatasource } from "@/features/favorites/data/datasources/favorites-datasource";
|
|
3
|
+
import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
|
|
4
|
+
|
|
5
|
+
describe("FavoritesRepositoryImpl", () => {
|
|
6
|
+
let repository: FavoritesRepositoryImpl;
|
|
7
|
+
let mockFavoritesDatasource: jest.Mocked<FavoritesDatasource>;
|
|
8
|
+
|
|
9
|
+
const mockMovie: MovieEntity = {
|
|
10
|
+
id: "1",
|
|
11
|
+
title: "Test Movie",
|
|
12
|
+
description: "Test Description",
|
|
13
|
+
image: "/test.jpg",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const mockFavorites = [mockMovie];
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mockFavoritesDatasource = {
|
|
20
|
+
getFavorites: jest.fn(),
|
|
21
|
+
setFavorites: jest.fn(),
|
|
22
|
+
clearFavorites: jest.fn(),
|
|
23
|
+
} as jest.Mocked<FavoritesDatasource>;
|
|
24
|
+
|
|
25
|
+
repository = new FavoritesRepositoryImpl(mockFavoritesDatasource);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("addFavorite", () => {
|
|
29
|
+
it("should add a movie to favorites", async () => {
|
|
30
|
+
mockFavoritesDatasource.getFavorites.mockResolvedValue(
|
|
31
|
+
JSON.stringify([])
|
|
32
|
+
);
|
|
33
|
+
mockFavoritesDatasource.setFavorites.mockResolvedValue();
|
|
34
|
+
|
|
35
|
+
await repository.addFavorite(mockMovie);
|
|
36
|
+
|
|
37
|
+
expect(mockFavoritesDatasource.getFavorites).toHaveBeenCalled();
|
|
38
|
+
expect(mockFavoritesDatasource.setFavorites).toHaveBeenCalledWith(
|
|
39
|
+
JSON.stringify([mockMovie])
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should append movie to existing favorites", async () => {
|
|
44
|
+
const existingMovie: MovieEntity = {
|
|
45
|
+
...mockMovie,
|
|
46
|
+
id: "2",
|
|
47
|
+
title: "Existing Movie",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
mockFavoritesDatasource.getFavorites.mockResolvedValue(
|
|
51
|
+
JSON.stringify([existingMovie])
|
|
52
|
+
);
|
|
53
|
+
mockFavoritesDatasource.setFavorites.mockResolvedValue();
|
|
54
|
+
|
|
55
|
+
await repository.addFavorite(mockMovie);
|
|
56
|
+
|
|
57
|
+
expect(mockFavoritesDatasource.setFavorites).toHaveBeenCalledWith(
|
|
58
|
+
JSON.stringify([existingMovie, mockMovie])
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("removeFavorite", () => {
|
|
64
|
+
it("should remove a movie from favorites", async () => {
|
|
65
|
+
mockFavoritesDatasource.getFavorites.mockResolvedValue(
|
|
66
|
+
JSON.stringify(mockFavorites)
|
|
67
|
+
);
|
|
68
|
+
mockFavoritesDatasource.setFavorites.mockResolvedValue();
|
|
69
|
+
|
|
70
|
+
await repository.removeFavorite(mockMovie.id);
|
|
71
|
+
|
|
72
|
+
expect(mockFavoritesDatasource.getFavorites).toHaveBeenCalled();
|
|
73
|
+
expect(mockFavoritesDatasource.setFavorites).toHaveBeenCalledWith(
|
|
74
|
+
JSON.stringify([])
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should not modify favorites if movie is not found", async () => {
|
|
79
|
+
mockFavoritesDatasource.getFavorites.mockResolvedValue(
|
|
80
|
+
JSON.stringify(mockFavorites)
|
|
81
|
+
);
|
|
82
|
+
mockFavoritesDatasource.setFavorites.mockResolvedValue();
|
|
83
|
+
|
|
84
|
+
await repository.removeFavorite("non-existent-id");
|
|
85
|
+
|
|
86
|
+
expect(mockFavoritesDatasource.setFavorites).toHaveBeenCalledWith(
|
|
87
|
+
JSON.stringify(mockFavorites)
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("getFavorites", () => {
|
|
93
|
+
it("should return all favorites", async () => {
|
|
94
|
+
mockFavoritesDatasource.getFavorites.mockResolvedValue(
|
|
95
|
+
JSON.stringify(mockFavorites)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const result = await repository.getFavorites();
|
|
99
|
+
|
|
100
|
+
expect(mockFavoritesDatasource.getFavorites).toHaveBeenCalled();
|
|
101
|
+
expect(result).toEqual(mockFavorites);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should return empty array when no favorites exist", async () => {
|
|
105
|
+
mockFavoritesDatasource.getFavorites.mockResolvedValue(
|
|
106
|
+
JSON.stringify([])
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const result = await repository.getFavorites();
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("clearFavorites", () => {
|
|
116
|
+
it("should clear all favorites", async () => {
|
|
117
|
+
mockFavoritesDatasource.clearFavorites.mockResolvedValue();
|
|
118
|
+
|
|
119
|
+
await repository.clearFavorites();
|
|
120
|
+
|
|
121
|
+
expect(mockFavoritesDatasource.clearFavorites).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AddToFavoritesUsecase } from "@/features/favorites/domain/usecases/add-to-favorites-usecase";
|
|
2
|
+
import { FavoritesRepository } from "@/features/favorites/domain/repositories/favorites-repository";
|
|
3
|
+
import { MovieEntity } from "@/features/movies/domain/entities/movie-entity";
|
|
4
|
+
|
|
5
|
+
describe("AddToFavoritesUsecase", () => {
|
|
6
|
+
let usecase: AddToFavoritesUsecase;
|
|
7
|
+
let mockFavoritesRepository: jest.Mocked<FavoritesRepository>;
|
|
8
|
+
|
|
9
|
+
const mockMovie: MovieEntity = {
|
|
10
|
+
id: "123",
|
|
11
|
+
title: "Test Movie",
|
|
12
|
+
description: "Test Description",
|
|
13
|
+
image: "test-image.jpg",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockFavoritesRepository = {
|
|
18
|
+
getFavorites: jest.fn(),
|
|
19
|
+
addFavorite: jest.fn(),
|
|
20
|
+
removeFavorite: jest.fn(),
|
|
21
|
+
clearFavorites: jest.fn(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
usecase = new AddToFavoritesUsecase(mockFavoritesRepository);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should successfully add a movie to favorites", async () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
mockFavoritesRepository.addFavorite.mockResolvedValue();
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const result = await usecase.execute(mockMovie);
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(result.isSuccess).toBe(true);
|
|
36
|
+
expect(mockFavoritesRepository.addFavorite).toHaveBeenCalledWith(mockMovie);
|
|
37
|
+
expect(mockFavoritesRepository.addFavorite).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return failure when repository throws an error", async () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const error = new Error("Failed to add favorite");
|
|
43
|
+
mockFavoritesRepository.addFavorite.mockRejectedValue(error);
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
const result = await usecase.execute(mockMovie);
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(result.isFailure).toBe(true);
|
|
50
|
+
expect(result.error).toBe(error);
|
|
51
|
+
expect(mockFavoritesRepository.addFavorite).toHaveBeenCalledWith(mockMovie);
|
|
52
|
+
expect(mockFavoritesRepository.addFavorite).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ClearFavoritesUsecase } from "@/features/favorites/domain/usecases/clear-favorites-usecase";
|
|
2
|
+
import { FavoritesRepository } from "@/features/favorites/domain/repositories/favorites-repository";
|
|
3
|
+
|
|
4
|
+
describe("ClearFavoritesUsecase", () => {
|
|
5
|
+
let usecase: ClearFavoritesUsecase;
|
|
6
|
+
let mockFavoritesRepository: jest.Mocked<FavoritesRepository>;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockFavoritesRepository = {
|
|
10
|
+
getFavorites: jest.fn(),
|
|
11
|
+
addFavorite: jest.fn(),
|
|
12
|
+
removeFavorite: jest.fn(),
|
|
13
|
+
clearFavorites: jest.fn(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
usecase = new ClearFavoritesUsecase(mockFavoritesRepository);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should successfully clear all favorites", async () => {
|
|
20
|
+
// Arrange
|
|
21
|
+
mockFavoritesRepository.clearFavorites.mockResolvedValue();
|
|
22
|
+
|
|
23
|
+
// Act
|
|
24
|
+
const result = await usecase.execute();
|
|
25
|
+
|
|
26
|
+
// Assert
|
|
27
|
+
expect(result.isSuccess).toBe(true);
|
|
28
|
+
expect(mockFavoritesRepository.clearFavorites).toHaveBeenCalledTimes(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should return failure when repository throws an error", async () => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const error = new Error("Failed to clear favorites");
|
|
34
|
+
mockFavoritesRepository.clearFavorites.mockRejectedValue(error);
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
const result = await usecase.execute();
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(result.isFailure).toBe(true);
|
|
41
|
+
expect(result.error).toBe(error);
|
|
42
|
+
expect(mockFavoritesRepository.clearFavorites).toHaveBeenCalledTimes(1);
|
|
43
|
+
});
|
|
44
|
+
});
|