salat 4.10.0 → 5.0.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/README.md +2 -7
- package/dist/commands/times.js +5 -3
- package/dist/components/CitiesApp.js +2 -2
- package/dist/components/CitySelect.js +22 -0
- package/dist/components/CitySelect.test.js +40 -0
- package/dist/components/HijriApp.js +1 -1
- package/dist/components/TimesApp.js +45 -11
- package/dist/components/TimesApp.test.js +15 -8
- package/dist/components/TimesCommandWrapper.js +10 -0
- package/dist/components/TimesCommandWrapper.test.js +35 -0
- package/dist/data/cities.json +1323 -197
- package/dist/hooks/useHijriDate.js +2 -2
- package/dist/hooks/useHijriDate.test.js +45 -17
- package/dist/hooks/usePrayerTimes.js +1 -11
- package/dist/hooks/usePrayerTimes.test.js +62 -0
- package/dist/services/constants.js +3 -3
- package/dist/services/constants.test.js +20 -0
- package/dist/services/utils/api.js +11 -8
- package/dist/services/utils/api.test.js +24 -13
- package/dist/services/utils/city.js +1 -1
- package/dist/services/utils/city.test.js +39 -37
- package/dist/services/utils/hijri.js +2 -4
- package/dist/services/utils/hijri.test.js +2 -2
- package/dist/services/utils/index.js +1 -2
- package/dist/services/utils/parseHijri.js +34 -0
- package/dist/services/utils/parseHijri.test.js +19 -0
- package/dist/services/utils/time.js +6 -1
- package/dist/services/utils/time.test.js +8 -1
- package/package.json +4 -4
- package/dist/data/prayers.json +0 -20
- package/dist/services/utils/cache.js +0 -28
- package/dist/services/utils/cleanData.js +0 -8
- package/dist/services/utils/cleanData.test.js +0 -59
- package/dist/services/utils/parser.js +0 -20
- package/dist/services/utils/parser.test.js +0 -33
package/README.md
CHANGED
|
@@ -62,9 +62,6 @@ Commands:
|
|
|
62
62
|
hijri Display the current hijri date
|
|
63
63
|
guide Show a rich visual guide to using salat-cli
|
|
64
64
|
cities Display the list of available city names
|
|
65
|
-
help [command] ] [city] Get prayer times for a city
|
|
66
|
-
guide Show a rich visual guide to using salat-cli
|
|
67
|
-
cities Display the list of available city names
|
|
68
65
|
help [command] display help for command
|
|
69
66
|
```
|
|
70
67
|
|
|
@@ -75,13 +72,11 @@ Commands:
|
|
|
75
72
|
## 🏗 Dependencies
|
|
76
73
|
|
|
77
74
|
This project is built on the shoulders of giants:
|
|
78
|
-
|
|
79
|
-
- [**React Query**](https://tanstack.com/query/latest) - Data synchronization library for managing server state.
|
|
80
75
|
- [**Commander.js**](https://github.com/tj/commander) - The complete solution for node.js command-line interfaces.
|
|
76
|
+
- [**Ink**](https://github.com/vadimdemedes/ink) - React for interactive command-line apps.
|
|
77
|
+
- [**React Query**](https://tanstack.com/query/latest) - Data synchronization library for managing server state.
|
|
81
78
|
- [**date-fns**](https://date-fns.org/) - Modern JavaScript date utility library.
|
|
82
|
-
- [**node-fetch**](https://github.com/node-fetch/node-fetch) - A light-weight module that brings `window.fetch` to Node.js.
|
|
83
79
|
- [**domino**](https://github.com/fent/domino) - Server-side DOM implementation for parsing API response
|
|
84
|
-
- [**node-localstorage**](https://github.com/lmaccherone/node-localstorage) - LocalStorage implementation for Node.js.
|
|
85
80
|
|
|
86
81
|
## 🤝 Contributing
|
|
87
82
|
|
package/dist/commands/times.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import App from "#components/TimesApp";
|
|
1
|
+
import TimesCommandWrapper from "#components/TimesCommandWrapper";
|
|
3
2
|
import { Command } from "commander";
|
|
4
3
|
import { render } from "ink";
|
|
5
4
|
import React from "react";
|
|
@@ -8,5 +7,8 @@ export const timesCommand = new Command("times")
|
|
|
8
7
|
.argument("[city]", "City name")
|
|
9
8
|
.option("-1, --once", "Run once and exit", false)
|
|
10
9
|
.action((city, options) => {
|
|
11
|
-
render(React.createElement(
|
|
10
|
+
render(React.createElement(TimesCommandWrapper, {
|
|
11
|
+
initialCity: city,
|
|
12
|
+
once: options.once,
|
|
13
|
+
}));
|
|
12
14
|
});
|
|
@@ -4,7 +4,7 @@ import citiesData from "../data/cities.json" with { type: "json" };
|
|
|
4
4
|
const CitiesApp = () => {
|
|
5
5
|
const cities = citiesData;
|
|
6
6
|
// Sort cities alphabetically
|
|
7
|
-
const sortedCities = [...cities].sort((a, b) => a.
|
|
8
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: ["\uD83C\uDF0D Available Cities in Morocco (", cities.length, ")"] }) }), _jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: sortedCities.map((city) => (_jsxs(Box, { width: 25, marginBottom: 0, children: [_jsx(Text, { color: "gray", children: "- " }), _jsx(Text, { children: city.
|
|
7
|
+
const sortedCities = [...cities].sort((a, b) => a.frenchName.localeCompare(b.frenchName));
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: ["\uD83C\uDF0D Available Cities in Morocco (", cities.length, ")"] }) }), _jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: sortedCities.map((city) => (_jsxs(Box, { width: 25, marginBottom: 0, children: [_jsx(Text, { color: "gray", children: "- " }), _jsx(Text, { children: city.frenchName })] }, city.id))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", children: ["Tip: Use these names with the 'times' command, e.g., 'salat times", " ", sortedCities[0]?.frenchName, "'"] }) })] }));
|
|
9
9
|
};
|
|
10
10
|
export default CitiesApp;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import SelectInput from "ink-select-input";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import { useMemo, useState } from "react";
|
|
6
|
+
import citiesData from "../data/cities.json" with { type: "json" };
|
|
7
|
+
const cities = citiesData;
|
|
8
|
+
const CitySelect = ({ onSelect }) => {
|
|
9
|
+
const [query, setQuery] = useState("");
|
|
10
|
+
const filteredCities = useMemo(() => {
|
|
11
|
+
if (!query)
|
|
12
|
+
return cities;
|
|
13
|
+
const lowerQuery = query.toLowerCase();
|
|
14
|
+
return cities.filter((c) => c.frenchName.toLowerCase().includes(lowerQuery));
|
|
15
|
+
}, [query]);
|
|
16
|
+
const items = filteredCities.map((c) => ({
|
|
17
|
+
label: `${c.frenchName}`,
|
|
18
|
+
value: c.frenchName,
|
|
19
|
+
}));
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, width: 60, borderStyle: "round", borderColor: "green", children: [_jsx(Box, { flexDirection: "column", alignItems: "center", marginBottom: 1, children: _jsx(Box, { borderStyle: "single", borderColor: "green", paddingX: 2, children: _jsx(Text, { bold: true, color: "green", children: "SELECT YOUR CITY" }) }) }), _jsxs(Box, { marginBottom: 1, paddingX: 2, children: [_jsx(Text, { color: "white", bold: true, children: "Filter: " }), _jsx(TextInput, { value: query, onChange: setQuery, placeholder: "Type city name..." })] }), _jsx(Box, { borderStyle: "single", borderColor: "gray", padding: 1, children: items.length > 0 ? (_jsx(SelectInput, { items: items.sort((a, b) => a.label.localeCompare(b.label)), onSelect: (item) => onSelect(item.value), limit: 8, indicatorComponent: ({ isSelected }) => (_jsx(Text, { color: "yellow", children: isSelected ? "> " : " " })), itemComponent: ({ label, isSelected }) => (_jsx(Text, { color: isSelected ? "yellow" : "white", children: label })) })) : (_jsx(Box, { justifyContent: "center", width: "100%", children: _jsx(Text, { color: "red", children: "No cities found." }) })) }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { dimColor: true, color: "gray", children: "Use arrow keys to select \u2022 Enter to confirm" }) })] }));
|
|
21
|
+
};
|
|
22
|
+
export default CitySelect;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "ink-testing-library";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import CitySelect from "./CitySelect.js";
|
|
5
|
+
// Mock data
|
|
6
|
+
vi.mock("../data/cities.json", () => ({
|
|
7
|
+
default: [
|
|
8
|
+
{ frenchName: "Casablanca", arabicName: "الدار البيضاء" },
|
|
9
|
+
{ frenchName: "Rabat", arabicName: "الرباط" },
|
|
10
|
+
{ frenchName: "Tangier", arabicName: "طنجة" },
|
|
11
|
+
],
|
|
12
|
+
}));
|
|
13
|
+
describe("CitySelect", () => {
|
|
14
|
+
it("should render input and list", () => {
|
|
15
|
+
const { lastFrame } = render(_jsx(CitySelect, { onSelect: () => { } }));
|
|
16
|
+
expect(lastFrame()).toContain("SELECT YOUR CITY");
|
|
17
|
+
expect(lastFrame()).toContain("Casablanca");
|
|
18
|
+
expect(lastFrame()).toContain("Rabat");
|
|
19
|
+
});
|
|
20
|
+
it("should show no results message when no match", async () => {
|
|
21
|
+
const { lastFrame, stdin } = render(_jsx(CitySelect, { onSelect: () => { } }));
|
|
22
|
+
stdin.write("NonExistentCity");
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
24
|
+
expect(lastFrame()).toContain("No cities found");
|
|
25
|
+
});
|
|
26
|
+
it("should filter cities by french name", async () => {
|
|
27
|
+
const { lastFrame, stdin } = render(_jsx(CitySelect, { onSelect: () => { } }));
|
|
28
|
+
stdin.write("Rab");
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
30
|
+
expect(lastFrame()).toContain("Rabat");
|
|
31
|
+
expect(lastFrame()).not.toContain("Casablanca");
|
|
32
|
+
});
|
|
33
|
+
it("should select a city", () => {
|
|
34
|
+
const onSelect = vi.fn();
|
|
35
|
+
const { stdin, lastFrame } = render(_jsx(CitySelect, { onSelect: onSelect }));
|
|
36
|
+
// Select first item (Casablanca)
|
|
37
|
+
stdin.write("\r");
|
|
38
|
+
expect(onSelect).toHaveBeenCalledWith("Casablanca");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -9,6 +9,6 @@ const HijriApp = () => {
|
|
|
9
9
|
if (error) {
|
|
10
10
|
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
11
11
|
}
|
|
12
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "blue", children: "\uD83D\uDD4C Hijri Date" }) }), _jsx(Box, { padding: 1, children: _jsx(Text, { children:
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "blue", children: "\uD83D\uDD4C Hijri Date" }) }), _jsx(Box, { padding: 1, children: _jsx(Text, { children: `${hijriDate}` }) })] }));
|
|
13
13
|
};
|
|
14
14
|
export default HijriApp;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useHijriDate } from "#hooks/useHijriDate";
|
|
2
3
|
import { usePrayerTimes } from "#hooks/usePrayerTimes";
|
|
3
|
-
import { getNextPrayer, tConv24 } from "#services/utils/time";
|
|
4
|
-
import { format } from "date-fns";
|
|
5
|
-
import { Box, Text, useApp } from "ink";
|
|
4
|
+
import { getImsakTime, getNextPrayer, tConv24 } from "#services/utils/time";
|
|
5
|
+
import { differenceInSeconds, format, parse, subDays } from "date-fns";
|
|
6
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
6
7
|
import { useEffect, useState } from "react";
|
|
7
|
-
const
|
|
8
|
+
const ProgressBar = ({ progress, width, }) => {
|
|
9
|
+
const filledWidth = Math.max(0, Math.min(width, Math.round(progress * width)));
|
|
10
|
+
const emptyWidth = width - filledWidth;
|
|
11
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "█".repeat(filledWidth) }), _jsx(Text, { color: "gray", children: "░".repeat(emptyWidth) })] }));
|
|
12
|
+
};
|
|
13
|
+
const App = ({ cityNameArg, once, onReset }) => {
|
|
8
14
|
const { exit } = useApp();
|
|
9
15
|
const { prayerTimes, error, loading, resolvedCityName } = usePrayerTimes({
|
|
10
16
|
cityNameArg,
|
|
11
17
|
});
|
|
18
|
+
const { hijriDate } = useHijriDate();
|
|
12
19
|
const [currentTime, setCurrentTime] = useState(new Date());
|
|
20
|
+
useInput((input) => {
|
|
21
|
+
const key = input.toLowerCase();
|
|
22
|
+
if (key === "c" && onReset) {
|
|
23
|
+
onReset();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
13
26
|
useEffect(() => {
|
|
14
27
|
const timer = setInterval(() => {
|
|
15
28
|
setCurrentTime(new Date());
|
|
@@ -25,17 +38,38 @@ const App = ({ cityNameArg, once }) => {
|
|
|
25
38
|
}
|
|
26
39
|
}, [once, loading, prayerTimes, error, exit]);
|
|
27
40
|
if (loading) {
|
|
28
|
-
return _jsx(Text, { children: "Loading prayer times..." });
|
|
41
|
+
return (_jsx(Box, { padding: 1, children: _jsxs(Text, { color: "yellow", children: ["Loading prayer times for ", resolvedCityName, "..."] }) }));
|
|
29
42
|
}
|
|
30
43
|
if (error) {
|
|
31
|
-
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
44
|
+
return (_jsx(Box, { padding: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["Error: ", error] }) }));
|
|
32
45
|
}
|
|
33
46
|
if (!prayerTimes) {
|
|
34
|
-
return _jsx(Text, { color: "red", children: "Could not fetch prayer times." });
|
|
47
|
+
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "red", bold: true, children: "Could not fetch prayer times." }) }));
|
|
48
|
+
}
|
|
49
|
+
const nextPrayerData = getNextPrayer(prayerTimes, currentTime);
|
|
50
|
+
// Calculate progress
|
|
51
|
+
const prayerOrder = ["Fajr", "Chorouq", "Dhuhr", "Asr", "Maghrib", "Ishae"];
|
|
52
|
+
const nextIndex = prayerOrder.indexOf(nextPrayerData.prayer);
|
|
53
|
+
const prevIndex = (nextIndex - 1 + prayerOrder.length) % prayerOrder.length;
|
|
54
|
+
const prevPrayerName = prayerOrder[prevIndex];
|
|
55
|
+
let prevDate = parse(prayerTimes[prevPrayerName], "HH:mm", currentTime);
|
|
56
|
+
let nextDate = parse(nextPrayerData.time, "HH:mm", currentTime);
|
|
57
|
+
if (nextPrayerData.prayer === "Fajr" && currentTime.getHours() >= 12) {
|
|
58
|
+
nextDate = parse(nextPrayerData.time, "HH:mm", new Date(currentTime.getTime() + 24 * 60 * 60 * 1000));
|
|
59
|
+
}
|
|
60
|
+
else if (nextPrayerData.prayer === "Fajr" && currentTime.getHours() < 12) {
|
|
61
|
+
prevDate = subDays(parse(prayerTimes.Ishae, "HH:mm", currentTime), 1);
|
|
35
62
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
const totalSeconds = differenceInSeconds(nextDate, prevDate);
|
|
64
|
+
const elapsedSeconds = differenceInSeconds(currentTime, prevDate);
|
|
65
|
+
const progress = Math.max(0, Math.min(1, elapsedSeconds / totalSeconds));
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, borderStyle: "round", borderColor: "green", width: 60, children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", gap: 1, children: [_jsx(Box, { borderStyle: "single", borderColor: "green", children: _jsxs(Text, { bold: true, color: "green", children: ["\uD83C\uDDF2\uD83C\uDDE6 ", resolvedCityName, ", Morocco \uD83C\uDDF2\uD83C\uDDE6"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "white", bold: true, children: ["\uD83D\uDCC5 ", format(currentTime, "EEEE, MMMM do yyyy")] }) }), hijriDate && (_jsxs(Text, { color: "gray", dimColor: true, children: ["\uD83D\uDD4C ", hijriDate] }))] }), _jsx(Box, { flexDirection: "column", marginY: 1, children: Object.entries({
|
|
67
|
+
"Imsak *": getImsakTime(prayerTimes.Fajr),
|
|
68
|
+
...prayerTimes,
|
|
69
|
+
}).map(([prayer, time]) => {
|
|
70
|
+
const isNext = prayer === nextPrayerData.prayer;
|
|
71
|
+
const isImsak = prayer === "Imsak *";
|
|
72
|
+
return (_jsxs(Box, { justifyContent: "space-between", paddingX: 2, backgroundColor: isNext ? "green" : undefined, children: [_jsx(Box, { children: _jsxs(Text, { color: isNext ? "white" : (isImsak ? "gray" : "white"), bold: isNext, children: [isNext ? "> " : " ", prayer.padEnd(12)] }) }), _jsx(Box, { children: _jsx(Text, { color: isNext ? "white" : (isImsak ? "gray" : "white"), bold: isNext, children: tConv24(time) }) })] }, prayer));
|
|
73
|
+
}) }), _jsx(Box, { paddingX: 2, marginTop: 1, children: _jsx(Text, { dimColor: true, color: "gray", italic: true, children: "* Imsak is 10 min before Fajr for safety" }) }), _jsxs(Box, { marginTop: 1, paddingX: 2, paddingY: 1, flexDirection: "column", alignItems: "center", borderStyle: "single", borderColor: "yellow", children: [_jsxs(Text, { children: ["Next: ", _jsx(Text, { color: "green", bold: true, children: nextPrayerData.prayer }), " in ", _jsx(Text, { color: "yellow", bold: true, children: nextPrayerData.timeLeft })] }), _jsxs(Box, { flexDirection: "column", alignItems: "center", marginY: 1, children: [_jsx(ProgressBar, { progress: progress, width: 50 }), _jsxs(Box, { justifyContent: "space-between", width: 50, marginTop: 0, children: [_jsx(Text, { color: "gray", dimColor: true, children: "0%" }), _jsxs(Text, { color: "green", bold: true, children: [Math.round(progress * 100), "%"] }), _jsx(Text, { color: "gray", dimColor: true, children: "100%" })] })] })] }), !once && (_jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { dimColor: true, color: "gray", children: "[C] Change City \u2022 [Ctrl+C] Exit" }) }))] }));
|
|
40
74
|
};
|
|
41
75
|
export default App;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useHijriDate } from "#hooks/useHijriDate";
|
|
2
3
|
import { usePrayerTimes } from "#hooks/usePrayerTimes";
|
|
3
4
|
import { render } from "ink-testing-library";
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
6
|
import TimesApp from "./TimesApp.js";
|
|
6
7
|
vi.mock("#hooks/usePrayerTimes", () => ({
|
|
7
8
|
usePrayerTimes: vi.fn(),
|
|
8
9
|
}));
|
|
10
|
+
vi.mock("#hooks/useHijriDate", () => ({
|
|
11
|
+
useHijriDate: vi.fn(),
|
|
12
|
+
}));
|
|
9
13
|
vi.mock("ink", async () => {
|
|
10
14
|
const actual = await vi.importActual("ink");
|
|
11
15
|
return {
|
|
@@ -16,15 +20,22 @@ vi.mock("ink", async () => {
|
|
|
16
20
|
};
|
|
17
21
|
});
|
|
18
22
|
describe("TimesApp", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.mocked(useHijriDate).mockReturnValue({
|
|
25
|
+
hijriDate: "18 Sha'ban 1447",
|
|
26
|
+
error: null,
|
|
27
|
+
loading: false,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
19
30
|
it("should render loading state", () => {
|
|
20
31
|
vi.mocked(usePrayerTimes).mockReturnValue({
|
|
21
32
|
prayerTimes: null,
|
|
22
33
|
error: null,
|
|
23
34
|
loading: true,
|
|
24
|
-
resolvedCityName: "",
|
|
35
|
+
resolvedCityName: "Marrakech",
|
|
25
36
|
});
|
|
26
37
|
const { lastFrame } = render(_jsx(TimesApp, {}));
|
|
27
|
-
expect(lastFrame()).toContain("Loading prayer times
|
|
38
|
+
expect(lastFrame()).toContain("Loading prayer times for Marrakech");
|
|
28
39
|
});
|
|
29
40
|
it("should render error state", () => {
|
|
30
41
|
vi.mocked(usePrayerTimes).mockReturnValue({
|
|
@@ -62,12 +73,8 @@ describe("TimesApp", () => {
|
|
|
62
73
|
resolvedCityName: "Marrakech",
|
|
63
74
|
});
|
|
64
75
|
const { lastFrame } = render(_jsx(TimesApp, {}));
|
|
65
|
-
expect(lastFrame()).toContain("Marrakech
|
|
76
|
+
expect(lastFrame()).toContain("Marrakech");
|
|
66
77
|
expect(lastFrame()).toContain("Fajr");
|
|
67
78
|
expect(lastFrame()).toContain("05:00 AM");
|
|
68
|
-
// Check for prayer times rendering
|
|
69
|
-
expect(lastFrame()).toContain("Next Prayer");
|
|
70
|
-
expect(lastFrame()).toContain("Dhuhr");
|
|
71
|
-
expect(lastFrame()).toContain("12:30 PM");
|
|
72
79
|
});
|
|
73
80
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import CitySelect from "#components/CitySelect";
|
|
3
|
+
import { QueryProvider } from "#components/QueryProvider";
|
|
4
|
+
import App from "#components/TimesApp";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
const TimesCommandWrapper = ({ initialCity, once, }) => {
|
|
7
|
+
const [city, setCity] = useState(initialCity);
|
|
8
|
+
return (_jsx(QueryProvider, { children: city ? (_jsx(App, { cityNameArg: city, once: once, onReset: () => setCity(undefined) })) : (_jsx(CitySelect, { onSelect: setCity })) }));
|
|
9
|
+
};
|
|
10
|
+
export default TimesCommandWrapper;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "ink-testing-library";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import TimesCommandWrapper from "./TimesCommandWrapper.js";
|
|
5
|
+
// Mock dependencies
|
|
6
|
+
vi.mock("#components/CitySelect", async () => {
|
|
7
|
+
const { Text } = await import("ink");
|
|
8
|
+
return {
|
|
9
|
+
default: ({ onSelect }) => {
|
|
10
|
+
// Trigger onSelect immediately? No, we need to test rendering.
|
|
11
|
+
return _jsx(Text, { children: "CitySelect Mock" });
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
vi.mock("#components/TimesApp", async () => {
|
|
16
|
+
const { Text } = await import("ink");
|
|
17
|
+
return {
|
|
18
|
+
default: ({ cityNameArg }) => (_jsxs(Text, { children: ["TimesApp Mock: ", cityNameArg] })),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
vi.mock("#components/QueryProvider", () => ({
|
|
22
|
+
QueryProvider: ({ children }) => (_jsx(_Fragment, { children: children })),
|
|
23
|
+
}));
|
|
24
|
+
describe("TimesCommandWrapper", () => {
|
|
25
|
+
it("should render TimesApp if initialCity is provided", () => {
|
|
26
|
+
const { lastFrame } = render(_jsx(TimesCommandWrapper, { initialCity: "Casablanca" }));
|
|
27
|
+
expect(lastFrame()).toContain("TimesApp Mock: Casablanca");
|
|
28
|
+
expect(lastFrame()).not.toContain("CitySelect Mock");
|
|
29
|
+
});
|
|
30
|
+
it("should render CitySelect if no initialCity provided", () => {
|
|
31
|
+
const { lastFrame } = render(_jsx(TimesCommandWrapper, {}));
|
|
32
|
+
expect(lastFrame()).toContain("CitySelect Mock");
|
|
33
|
+
expect(lastFrame()).not.toContain("TimesApp Mock");
|
|
34
|
+
});
|
|
35
|
+
});
|