feat: add lottie for animations
This commit is contained in:
@@ -112,6 +112,7 @@
|
||||
);
|
||||
name = Gaze;
|
||||
packageProductDependencies = (
|
||||
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
||||
);
|
||||
productName = Gaze;
|
||||
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
||||
@@ -195,6 +196,9 @@
|
||||
);
|
||||
mainGroup = 27A21B332F0F69DC0018C4F3;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -573,6 +577,25 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 4.6.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
27AE10B12F10B1FC00E00DBC /* Lottie */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||
productName = Lottie;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
};
|
||||
rootObject = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"originHash" : "6b3386dc9ff1f3a74f1534de9c41d47137eae0901cfe819ed442f1b241549359",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "lottie-spm",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/airbnb/lottie-spm.git",
|
||||
"state" : {
|
||||
"revision" : "69faaefa7721fba9e434a52c16adf4329c9084db",
|
||||
"version" : "4.6.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
132
Gaze/Animations/blink.json
Normal file
132
Gaze/Animations/blink.json
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"v": "5.7.4",
|
||||
"fr": 30,
|
||||
"ip": 0,
|
||||
"op": 60,
|
||||
"w": 200,
|
||||
"h": 200,
|
||||
"nm": "Blink Animation",
|
||||
"ddd": 0,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 1,
|
||||
"ty": 4,
|
||||
"nm": "Eye Container",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"p": {"a": 0, "k": [100, 100, 0]},
|
||||
"a": {"a": 0, "k": [0, 0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100, 100]}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "el",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [80, 80], "h": 1},
|
||||
{"t": 15, "s": [80, 80], "h": 1},
|
||||
{"t": 20, "s": [80, 10], "h": 1},
|
||||
{"t": 25, "s": [80, 80], "h": 1},
|
||||
{"t": 35, "s": [80, 80], "h": 1},
|
||||
{"t": 40, "s": [80, 10], "h": 1},
|
||||
{"t": 45, "s": [80, 80], "h": 1}
|
||||
]
|
||||
},
|
||||
"nm": "Eye Ellipse"
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||
"o": {"a": 0, "k": 100},
|
||||
"w": {"a": 0, "k": 8},
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"nm": "Stroke"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {"a": 0, "k": 100}
|
||||
}
|
||||
],
|
||||
"nm": "Eye Shape"
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 60,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 2,
|
||||
"ty": 4,
|
||||
"nm": "Pupil",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"p": {"a": 0, "k": [100, 100, 0]},
|
||||
"a": {"a": 0, "k": [0, 0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100, 100]}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "el",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [25, 25]},
|
||||
"nm": "Pupil Ellipse"
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": 1,
|
||||
"nm": "Fill"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [100], "h": 1},
|
||||
{"t": 20, "s": [0], "h": 1},
|
||||
{"t": 25, "s": [100], "h": 1},
|
||||
{"t": 40, "s": [0], "h": 1},
|
||||
{"t": 45, "s": [100], "h": 1}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"nm": "Pupil Shape"
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 60,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": []
|
||||
}
|
||||
319
Gaze/Animations/look-away.json
Normal file
319
Gaze/Animations/look-away.json
Normal file
@@ -0,0 +1,319 @@
|
||||
{
|
||||
"v": "5.7.4",
|
||||
"fr": 30,
|
||||
"ip": 0,
|
||||
"op": 120,
|
||||
"w": 200,
|
||||
"h": 200,
|
||||
"nm": "Look Away Animation",
|
||||
"ddd": 0,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 1,
|
||||
"ty": 4,
|
||||
"nm": "Face Circle",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"p": {"a": 0, "k": [100, 100, 0]},
|
||||
"a": {"a": 0, "k": [0, 0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100, 100]}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "el",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [150, 150]},
|
||||
"nm": "Face Ellipse"
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||
"o": {"a": 0, "k": 100},
|
||||
"w": {"a": 0, "k": 6},
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"nm": "Stroke"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {"a": 0, "k": 100}
|
||||
}
|
||||
],
|
||||
"nm": "Face"
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 120,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 2,
|
||||
"ty": 4,
|
||||
"nm": "Left Eye",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"p": {"a": 0, "k": [70, 85, 0]},
|
||||
"a": {"a": 0, "k": [0, 0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100, 100]}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "el",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [20, 20]},
|
||||
"nm": "Eye Ellipse"
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||
"o": {"a": 0, "k": 100},
|
||||
"w": {"a": 0, "k": 4},
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"nm": "Stroke"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [0, 0], "h": 1},
|
||||
{"t": 20, "s": [-15, 0], "h": 1},
|
||||
{"t": 40, "s": [0, 0], "h": 1},
|
||||
{"t": 60, "s": [15, 0], "h": 1},
|
||||
{"t": 80, "s": [0, 0], "h": 1},
|
||||
{"t": 100, "s": [0, -10], "h": 1},
|
||||
{"t": 120, "s": [0, 0], "h": 1}
|
||||
]
|
||||
},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {"a": 0, "k": 100}
|
||||
}
|
||||
],
|
||||
"nm": "Eye"
|
||||
},
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "el",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [8, 8]},
|
||||
"nm": "Pupil Ellipse"
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": 1,
|
||||
"nm": "Fill"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [0, 0], "h": 1},
|
||||
{"t": 20, "s": [-15, 0], "h": 1},
|
||||
{"t": 40, "s": [0, 0], "h": 1},
|
||||
{"t": 60, "s": [15, 0], "h": 1},
|
||||
{"t": 80, "s": [0, 0], "h": 1},
|
||||
{"t": 100, "s": [0, -10], "h": 1},
|
||||
{"t": 120, "s": [0, 0], "h": 1}
|
||||
]
|
||||
},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {"a": 0, "k": 100}
|
||||
}
|
||||
],
|
||||
"nm": "Pupil"
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 120,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 3,
|
||||
"ty": 4,
|
||||
"nm": "Right Eye",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"p": {"a": 0, "k": [130, 85, 0]},
|
||||
"a": {"a": 0, "k": [0, 0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100, 100]}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "el",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [20, 20]},
|
||||
"nm": "Eye Ellipse"
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||
"o": {"a": 0, "k": 100},
|
||||
"w": {"a": 0, "k": 4},
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"nm": "Stroke"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [0, 0], "h": 1},
|
||||
{"t": 20, "s": [-15, 0], "h": 1},
|
||||
{"t": 40, "s": [0, 0], "h": 1},
|
||||
{"t": 60, "s": [15, 0], "h": 1},
|
||||
{"t": 80, "s": [0, 0], "h": 1},
|
||||
{"t": 100, "s": [0, -10], "h": 1},
|
||||
{"t": 120, "s": [0, 0], "h": 1}
|
||||
]
|
||||
},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {"a": 0, "k": 100}
|
||||
}
|
||||
],
|
||||
"nm": "Eye"
|
||||
},
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "el",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [8, 8]},
|
||||
"nm": "Pupil Ellipse"
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": 1,
|
||||
"nm": "Fill"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [0, 0], "h": 1},
|
||||
{"t": 20, "s": [-15, 0], "h": 1},
|
||||
{"t": 40, "s": [0, 0], "h": 1},
|
||||
{"t": 60, "s": [15, 0], "h": 1},
|
||||
{"t": 80, "s": [0, 0], "h": 1},
|
||||
{"t": 100, "s": [0, -10], "h": 1},
|
||||
{"t": 120, "s": [0, 0], "h": 1}
|
||||
]
|
||||
},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {"a": 0, "k": 100}
|
||||
}
|
||||
],
|
||||
"nm": "Pupil"
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 120,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 4,
|
||||
"ty": 4,
|
||||
"nm": "Smile",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"p": {"a": 0, "k": [100, 120, 0]},
|
||||
"a": {"a": 0, "k": [0, 0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100, 100]}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "sh",
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [[0, 0], [-20, 10], [20, 10]],
|
||||
"o": [[20, 10], [20, -10], [0, 0]],
|
||||
"v": [[-30, 0], [0, 15], [30, 0]],
|
||||
"c": false
|
||||
}
|
||||
},
|
||||
"nm": "Smile Path"
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||
"o": {"a": 0, "k": 100},
|
||||
"w": {"a": 0, "k": 4},
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"nm": "Stroke"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {"a": 0, "k": 100}
|
||||
}
|
||||
],
|
||||
"nm": "Smile"
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 120,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": []
|
||||
}
|
||||
171
Gaze/Animations/posture.json
Normal file
171
Gaze/Animations/posture.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"v": "5.7.4",
|
||||
"fr": 30,
|
||||
"ip": 0,
|
||||
"op": 90,
|
||||
"w": 200,
|
||||
"h": 400,
|
||||
"nm": "Posture Arrow Animation",
|
||||
"ddd": 0,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 1,
|
||||
"ty": 4,
|
||||
"nm": "Arrow",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [0], "h": 1},
|
||||
{"t": 10, "s": [100], "h": 1},
|
||||
{"t": 60, "s": [100], "h": 1},
|
||||
{"t": 70, "s": [0], "h": 1}
|
||||
]
|
||||
},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [100, 200, 0], "h": 1},
|
||||
{"t": 60, "s": [100, 200, 0], "h": 1},
|
||||
{"t": 90, "s": [100, -100, 0], "h": 1}
|
||||
]
|
||||
},
|
||||
"a": {"a": 0, "k": [0, 0, 0]},
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [0, 0, 100], "h": 1},
|
||||
{"t": 10, "s": [100, 100, 100], "h": 1},
|
||||
{"t": 60, "s": [100, 100, 100], "h": 1},
|
||||
{"t": 70, "s": [50, 50, 100], "h": 1}
|
||||
]
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "sh",
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]],
|
||||
"o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]],
|
||||
"v": [
|
||||
[0, -40],
|
||||
[-30, -10],
|
||||
[-12, -10],
|
||||
[-12, 40],
|
||||
[12, 40],
|
||||
[12, -10],
|
||||
[30, -10]
|
||||
],
|
||||
"c": true
|
||||
}
|
||||
},
|
||||
"nm": "Arrow Path"
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {"a": 0, "k": [0, 0, 0, 1]},
|
||||
"o": {"a": 0, "k": 100},
|
||||
"r": 1,
|
||||
"nm": "Fill"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {"a": 0, "k": 100}
|
||||
}
|
||||
],
|
||||
"nm": "Arrow Shape"
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 90,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 2,
|
||||
"ty": 4,
|
||||
"nm": "Circle Background",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [0], "h": 1},
|
||||
{"t": 10, "s": [100], "h": 1},
|
||||
{"t": 60, "s": [100], "h": 1},
|
||||
{"t": 70, "s": [0], "h": 1}
|
||||
]
|
||||
},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [100, 200, 0], "h": 1},
|
||||
{"t": 60, "s": [100, 200, 0], "h": 1},
|
||||
{"t": 90, "s": [100, -100, 0], "h": 1}
|
||||
]
|
||||
},
|
||||
"a": {"a": 0, "k": [0, 0, 0]},
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{"t": 0, "s": [0, 0, 100], "h": 1},
|
||||
{"t": 10, "s": [100, 100, 100], "h": 1},
|
||||
{"t": 60, "s": [100, 100, 100], "h": 1},
|
||||
{"t": 70, "s": [50, 50, 100], "h": 1}
|
||||
]
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "el",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [120, 120]},
|
||||
"nm": "Circle Ellipse"
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {"a": 0, "k": [0.9, 0.9, 0.9, 1]},
|
||||
"o": {"a": 0, "k": 30},
|
||||
"r": 1,
|
||||
"nm": "Fill"
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {"a": 0, "k": [0, 0]},
|
||||
"a": {"a": 0, "k": [0, 0]},
|
||||
"s": {"a": 0, "k": [100, 100]},
|
||||
"r": {"a": 0, "k": 0},
|
||||
"o": {"a": 0, "k": 100}
|
||||
}
|
||||
],
|
||||
"nm": "Circle"
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 90,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": []
|
||||
}
|
||||
18
Gaze/Models/AnimationAsset.swift
Normal file
18
Gaze/Models/AnimationAsset.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// AnimationAsset.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AnimationAsset: String {
|
||||
case blink = "blink"
|
||||
case lookAway = "look-away"
|
||||
case posture = "posture"
|
||||
|
||||
var fileName: String {
|
||||
return self.rawValue
|
||||
}
|
||||
}
|
||||
67
Gaze/Views/Components/LottieView.swift
Normal file
67
Gaze/Views/Components/LottieView.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// LottieView.swift
|
||||
// Gaze
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Lottie
|
||||
|
||||
struct LottieView: NSViewRepresentable {
|
||||
let animationName: String
|
||||
let loopMode: LottieLoopMode
|
||||
let animationSpeed: CGFloat
|
||||
|
||||
init(
|
||||
animationName: String,
|
||||
loopMode: LottieLoopMode = .playOnce,
|
||||
animationSpeed: CGFloat = 1.0
|
||||
) {
|
||||
self.animationName = animationName
|
||||
self.loopMode = loopMode
|
||||
self.animationSpeed = animationSpeed
|
||||
}
|
||||
|
||||
func makeNSView(context: Context) -> LottieAnimationView {
|
||||
let animationView = LottieAnimationView()
|
||||
animationView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
if let animation = LottieAnimation.named(animationName) {
|
||||
animationView.animation = animation
|
||||
animationView.loopMode = loopMode
|
||||
animationView.animationSpeed = animationSpeed
|
||||
animationView.backgroundBehavior = .pauseAndRestore
|
||||
animationView.play()
|
||||
}
|
||||
|
||||
return animationView
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: LottieAnimationView, context: Context) {
|
||||
guard nsView.animation == nil || nsView.isAnimationPlaying == false else {
|
||||
return
|
||||
}
|
||||
|
||||
if let animation = LottieAnimation.named(animationName) {
|
||||
nsView.animation = animation
|
||||
nsView.loopMode = loopMode
|
||||
nsView.animationSpeed = animationSpeed
|
||||
nsView.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Lottie Preview") {
|
||||
VStack(spacing: 20) {
|
||||
LottieView(animationName: "blink")
|
||||
.frame(width: 200, height: 200)
|
||||
|
||||
LottieView(animationName: "look-away", loopMode: .loop)
|
||||
.frame(width: 200, height: 200)
|
||||
|
||||
LottieView(animationName: "posture")
|
||||
.frame(width: 200, height: 200)
|
||||
}
|
||||
.frame(width: 600, height: 800)
|
||||
}
|
||||
@@ -6,39 +6,26 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Lottie
|
||||
|
||||
struct BlinkReminderView: View {
|
||||
var onDismiss: () -> Void
|
||||
|
||||
@State private var opacity: Double = 0
|
||||
@State private var scale: CGFloat = 0
|
||||
@State private var blinkProgress: Double = 0
|
||||
@State private var blinkCount = 0
|
||||
|
||||
private let screenHeight = NSScreen.main?.frame.height ?? 800
|
||||
private let screenWidth = NSScreen.main?.frame.width ?? 1200
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// Custom eye design for more polished look
|
||||
ZStack {
|
||||
// Eye outline
|
||||
Circle()
|
||||
.stroke(Color.accentColor, lineWidth: 4)
|
||||
.frame(width: scale * 1.2, height: scale * 1.2)
|
||||
|
||||
// Iris
|
||||
Circle()
|
||||
.fill(Color.accentColor)
|
||||
.frame(width: scale * 0.6, height: scale * 0.6)
|
||||
|
||||
// Pupil that moves with blink
|
||||
Circle()
|
||||
.fill(.black)
|
||||
.frame(width: scale * 0.25, height: scale * 0.25)
|
||||
.offset(y: blinkProgress * -scale * 0.1) // Vertical movement during blink
|
||||
}
|
||||
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
|
||||
LottieView(
|
||||
animationName: AnimationAsset.blink.fileName,
|
||||
loopMode: .playOnce,
|
||||
animationSpeed: 1.0
|
||||
)
|
||||
.frame(width: scale, height: scale)
|
||||
.shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 2)
|
||||
}
|
||||
.opacity(opacity)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
@@ -49,60 +36,25 @@ struct BlinkReminderView: View {
|
||||
}
|
||||
|
||||
private func startAnimation() {
|
||||
// Fade in and grow with spring animation for natural feel
|
||||
withAnimation(.spring(duration: 0.5, bounce: 0.2)) {
|
||||
// Fade in and grow
|
||||
withAnimation(.easeOut(duration: 0.3)) {
|
||||
opacity = 1.0
|
||||
scale = screenWidth * 0.12
|
||||
scale = screenWidth * 0.15
|
||||
}
|
||||
|
||||
// Start blinking after fade in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||
performBlinks()
|
||||
}
|
||||
}
|
||||
|
||||
private func performBlinks() {
|
||||
let blinkDuration = 0.15
|
||||
let pauseBetweenBlinks = 0.2
|
||||
|
||||
func blink() {
|
||||
// Close eyes with spring animation for natural movement
|
||||
withAnimation(.spring(duration: blinkDuration, bounce: 0.0)) {
|
||||
blinkProgress = 1.0
|
||||
}
|
||||
|
||||
// Open eyes
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + blinkDuration) {
|
||||
withAnimation(.spring(duration: blinkDuration, bounce: 0.0)) {
|
||||
blinkProgress = 0.0
|
||||
}
|
||||
|
||||
blinkCount += 1
|
||||
|
||||
if blinkCount < 2 {
|
||||
// Pause before next blink
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + pauseBetweenBlinks) {
|
||||
blink()
|
||||
}
|
||||
} else {
|
||||
// Fade out after all blinks with smooth spring animation
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
// Animation duration (2 seconds for double blink) + hold time
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.3) {
|
||||
fadeOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blink()
|
||||
}
|
||||
|
||||
private func fadeOut() {
|
||||
withAnimation(.spring(duration: 0.5, bounce: 0.2)) {
|
||||
withAnimation(.easeOut(duration: 0.3)) {
|
||||
opacity = 0
|
||||
scale = screenWidth * 0.08
|
||||
scale = screenWidth * 0.1
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
@@ -112,3 +64,8 @@ struct BlinkReminderView: View {
|
||||
BlinkReminderView(onDismiss: {})
|
||||
.frame(width: 800, height: 600)
|
||||
}
|
||||
|
||||
#Preview("Blink Reminder") {
|
||||
BlinkReminderView(onDismiss: {})
|
||||
.frame(width: 800, height: 600)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Lottie
|
||||
|
||||
struct LookAwayReminderView: View {
|
||||
let countdownSeconds: Int
|
||||
@@ -35,7 +36,12 @@ struct LookAwayReminderView: View {
|
||||
.font(.system(size: 28))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
AnimatedFaceView(size: 200)
|
||||
LottieView(
|
||||
animationName: AnimationAsset.lookAway.fileName,
|
||||
loopMode: .loop,
|
||||
animationSpeed: 1.0
|
||||
)
|
||||
.frame(width: 200, height: 200)
|
||||
.padding(.vertical, 30)
|
||||
|
||||
// Countdown display
|
||||
|
||||
53
GazeTests/Models/AnimationAssetTests.swift
Normal file
53
GazeTests/Models/AnimationAssetTests.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// AnimationAssetTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Gaze
|
||||
|
||||
final class AnimationAssetTests: XCTestCase {
|
||||
|
||||
func testRawValues() {
|
||||
XCTAssertEqual(AnimationAsset.blink.rawValue, "blink")
|
||||
XCTAssertEqual(AnimationAsset.lookAway.rawValue, "look-away")
|
||||
XCTAssertEqual(AnimationAsset.posture.rawValue, "posture")
|
||||
}
|
||||
|
||||
func testFileNames() {
|
||||
XCTAssertEqual(AnimationAsset.blink.fileName, "blink")
|
||||
XCTAssertEqual(AnimationAsset.lookAway.fileName, "look-away")
|
||||
XCTAssertEqual(AnimationAsset.posture.fileName, "posture")
|
||||
}
|
||||
|
||||
func testFileNameMatchesRawValue() {
|
||||
XCTAssertEqual(AnimationAsset.blink.fileName, AnimationAsset.blink.rawValue)
|
||||
XCTAssertEqual(AnimationAsset.lookAway.fileName, AnimationAsset.lookAway.rawValue)
|
||||
XCTAssertEqual(AnimationAsset.posture.fileName, AnimationAsset.posture.rawValue)
|
||||
}
|
||||
|
||||
func testInitFromRawValue() {
|
||||
XCTAssertEqual(AnimationAsset(rawValue: "blink"), .blink)
|
||||
XCTAssertEqual(AnimationAsset(rawValue: "look-away"), .lookAway)
|
||||
XCTAssertEqual(AnimationAsset(rawValue: "posture"), .posture)
|
||||
XCTAssertNil(AnimationAsset(rawValue: "invalid"))
|
||||
}
|
||||
|
||||
func testEquality() {
|
||||
XCTAssertEqual(AnimationAsset.blink, AnimationAsset.blink)
|
||||
XCTAssertNotEqual(AnimationAsset.blink, AnimationAsset.lookAway)
|
||||
XCTAssertNotEqual(AnimationAsset.lookAway, AnimationAsset.posture)
|
||||
}
|
||||
|
||||
func testAllCasesExist() {
|
||||
let blink = AnimationAsset.blink
|
||||
let lookAway = AnimationAsset.lookAway
|
||||
let posture = AnimationAsset.posture
|
||||
|
||||
XCTAssertNotNil(blink)
|
||||
XCTAssertNotNil(lookAway)
|
||||
XCTAssertNotNil(posture)
|
||||
}
|
||||
}
|
||||
175
GazeTests/Models/AppSettingsTests.swift
Normal file
175
GazeTests/Models/AppSettingsTests.swift
Normal file
@@ -0,0 +1,175 @@
|
||||
//
|
||||
// AppSettingsTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Gaze
|
||||
|
||||
final class AppSettingsTests: XCTestCase {
|
||||
|
||||
func testDefaultSettings() {
|
||||
let settings = AppSettings.defaults
|
||||
|
||||
XCTAssertTrue(settings.lookAwayTimer.enabled)
|
||||
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 20 * 60)
|
||||
XCTAssertEqual(settings.lookAwayCountdownSeconds, 20)
|
||||
|
||||
XCTAssertTrue(settings.blinkTimer.enabled)
|
||||
XCTAssertEqual(settings.blinkTimer.intervalSeconds, 5 * 60)
|
||||
|
||||
XCTAssertTrue(settings.postureTimer.enabled)
|
||||
XCTAssertEqual(settings.postureTimer.intervalSeconds, 30 * 60)
|
||||
|
||||
XCTAssertFalse(settings.hasCompletedOnboarding)
|
||||
XCTAssertFalse(settings.launchAtLogin)
|
||||
XCTAssertTrue(settings.playSounds)
|
||||
}
|
||||
|
||||
func testEquality() {
|
||||
let settings1 = AppSettings.defaults
|
||||
let settings2 = AppSettings.defaults
|
||||
|
||||
XCTAssertEqual(settings1, settings2)
|
||||
}
|
||||
|
||||
func testInequalityWhenLookAwayTimerDiffers() {
|
||||
var settings1 = AppSettings.defaults
|
||||
var settings2 = AppSettings.defaults
|
||||
|
||||
settings2.lookAwayTimer.enabled = false
|
||||
XCTAssertNotEqual(settings1, settings2)
|
||||
|
||||
settings2.lookAwayTimer.enabled = true
|
||||
settings2.lookAwayTimer.intervalSeconds = 10 * 60
|
||||
XCTAssertNotEqual(settings1, settings2)
|
||||
}
|
||||
|
||||
func testInequalityWhenCountdownDiffers() {
|
||||
var settings1 = AppSettings.defaults
|
||||
var settings2 = AppSettings.defaults
|
||||
|
||||
settings2.lookAwayCountdownSeconds = 30
|
||||
XCTAssertNotEqual(settings1, settings2)
|
||||
}
|
||||
|
||||
func testInequalityWhenBlinkTimerDiffers() {
|
||||
var settings1 = AppSettings.defaults
|
||||
var settings2 = AppSettings.defaults
|
||||
|
||||
settings2.blinkTimer.enabled = false
|
||||
XCTAssertNotEqual(settings1, settings2)
|
||||
}
|
||||
|
||||
func testInequalityWhenPostureTimerDiffers() {
|
||||
var settings1 = AppSettings.defaults
|
||||
var settings2 = AppSettings.defaults
|
||||
|
||||
settings2.postureTimer.intervalSeconds = 60 * 60
|
||||
XCTAssertNotEqual(settings1, settings2)
|
||||
}
|
||||
|
||||
func testInequalityWhenOnboardingDiffers() {
|
||||
var settings1 = AppSettings.defaults
|
||||
var settings2 = AppSettings.defaults
|
||||
|
||||
settings2.hasCompletedOnboarding = true
|
||||
XCTAssertNotEqual(settings1, settings2)
|
||||
}
|
||||
|
||||
func testInequalityWhenLaunchAtLoginDiffers() {
|
||||
var settings1 = AppSettings.defaults
|
||||
var settings2 = AppSettings.defaults
|
||||
|
||||
settings2.launchAtLogin = true
|
||||
XCTAssertNotEqual(settings1, settings2)
|
||||
}
|
||||
|
||||
func testInequalityWhenPlaySoundsDiffers() {
|
||||
var settings1 = AppSettings.defaults
|
||||
var settings2 = AppSettings.defaults
|
||||
|
||||
settings2.playSounds = false
|
||||
XCTAssertNotEqual(settings1, settings2)
|
||||
}
|
||||
|
||||
func testCodableEncoding() throws {
|
||||
let settings = AppSettings.defaults
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(settings)
|
||||
|
||||
XCTAssertNotNil(data)
|
||||
XCTAssertGreaterThan(data.count, 0)
|
||||
}
|
||||
|
||||
func testCodableDecoding() throws {
|
||||
let settings = AppSettings.defaults
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(settings)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let decoded = try decoder.decode(AppSettings.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded, settings)
|
||||
}
|
||||
|
||||
func testCodableRoundTripWithModifiedSettings() throws {
|
||||
var settings = AppSettings.defaults
|
||||
settings.lookAwayTimer.enabled = false
|
||||
settings.lookAwayCountdownSeconds = 30
|
||||
settings.blinkTimer.intervalSeconds = 10 * 60
|
||||
settings.postureTimer.enabled = false
|
||||
settings.hasCompletedOnboarding = true
|
||||
settings.launchAtLogin = true
|
||||
settings.playSounds = false
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(settings)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let decoded = try decoder.decode(AppSettings.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded, settings)
|
||||
XCTAssertFalse(decoded.lookAwayTimer.enabled)
|
||||
XCTAssertEqual(decoded.lookAwayCountdownSeconds, 30)
|
||||
XCTAssertEqual(decoded.blinkTimer.intervalSeconds, 10 * 60)
|
||||
XCTAssertFalse(decoded.postureTimer.enabled)
|
||||
XCTAssertTrue(decoded.hasCompletedOnboarding)
|
||||
XCTAssertTrue(decoded.launchAtLogin)
|
||||
XCTAssertFalse(decoded.playSounds)
|
||||
}
|
||||
|
||||
func testMutability() {
|
||||
var settings = AppSettings.defaults
|
||||
|
||||
settings.lookAwayTimer.enabled = false
|
||||
XCTAssertFalse(settings.lookAwayTimer.enabled)
|
||||
|
||||
settings.lookAwayCountdownSeconds = 30
|
||||
XCTAssertEqual(settings.lookAwayCountdownSeconds, 30)
|
||||
|
||||
settings.hasCompletedOnboarding = true
|
||||
XCTAssertTrue(settings.hasCompletedOnboarding)
|
||||
|
||||
settings.launchAtLogin = true
|
||||
XCTAssertTrue(settings.launchAtLogin)
|
||||
|
||||
settings.playSounds = false
|
||||
XCTAssertFalse(settings.playSounds)
|
||||
}
|
||||
|
||||
func testBoundaryValues() {
|
||||
var settings = AppSettings.defaults
|
||||
|
||||
settings.lookAwayTimer.intervalSeconds = 0
|
||||
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 0)
|
||||
|
||||
settings.lookAwayCountdownSeconds = 0
|
||||
XCTAssertEqual(settings.lookAwayCountdownSeconds, 0)
|
||||
|
||||
settings.lookAwayTimer.intervalSeconds = Int.max
|
||||
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, Int.max)
|
||||
}
|
||||
}
|
||||
115
GazeTests/Models/ReminderEventTests.swift
Normal file
115
GazeTests/Models/ReminderEventTests.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// ReminderEventTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Gaze
|
||||
|
||||
final class ReminderEventTests: XCTestCase {
|
||||
|
||||
func testLookAwayTriggeredCreation() {
|
||||
let event = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
|
||||
|
||||
if case .lookAwayTriggered(let countdown) = event {
|
||||
XCTAssertEqual(countdown, 20)
|
||||
} else {
|
||||
XCTFail("Expected lookAwayTriggered event")
|
||||
}
|
||||
}
|
||||
|
||||
func testBlinkTriggeredCreation() {
|
||||
let event = ReminderEvent.blinkTriggered
|
||||
|
||||
if case .blinkTriggered = event {
|
||||
XCTAssertTrue(true)
|
||||
} else {
|
||||
XCTFail("Expected blinkTriggered event")
|
||||
}
|
||||
}
|
||||
|
||||
func testPostureTriggeredCreation() {
|
||||
let event = ReminderEvent.postureTriggered
|
||||
|
||||
if case .postureTriggered = event {
|
||||
XCTAssertTrue(true)
|
||||
} else {
|
||||
XCTFail("Expected postureTriggered event")
|
||||
}
|
||||
}
|
||||
|
||||
func testTypePropertyForLookAway() {
|
||||
let event = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
|
||||
XCTAssertEqual(event.type, .lookAway)
|
||||
}
|
||||
|
||||
func testTypePropertyForBlink() {
|
||||
let event = ReminderEvent.blinkTriggered
|
||||
XCTAssertEqual(event.type, .blink)
|
||||
}
|
||||
|
||||
func testTypePropertyForPosture() {
|
||||
let event = ReminderEvent.postureTriggered
|
||||
XCTAssertEqual(event.type, .posture)
|
||||
}
|
||||
|
||||
func testEquality() {
|
||||
let event1 = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
|
||||
let event2 = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
|
||||
let event3 = ReminderEvent.lookAwayTriggered(countdownSeconds: 30)
|
||||
let event4 = ReminderEvent.blinkTriggered
|
||||
let event5 = ReminderEvent.blinkTriggered
|
||||
let event6 = ReminderEvent.postureTriggered
|
||||
|
||||
XCTAssertEqual(event1, event2)
|
||||
XCTAssertNotEqual(event1, event3)
|
||||
XCTAssertNotEqual(event1, event4)
|
||||
XCTAssertEqual(event4, event5)
|
||||
XCTAssertNotEqual(event4, event6)
|
||||
}
|
||||
|
||||
func testDifferentCountdownValues() {
|
||||
let event1 = ReminderEvent.lookAwayTriggered(countdownSeconds: 0)
|
||||
let event2 = ReminderEvent.lookAwayTriggered(countdownSeconds: 10)
|
||||
let event3 = ReminderEvent.lookAwayTriggered(countdownSeconds: 60)
|
||||
|
||||
XCTAssertNotEqual(event1, event2)
|
||||
XCTAssertNotEqual(event2, event3)
|
||||
XCTAssertNotEqual(event1, event3)
|
||||
|
||||
XCTAssertEqual(event1.type, .lookAway)
|
||||
XCTAssertEqual(event2.type, .lookAway)
|
||||
XCTAssertEqual(event3.type, .lookAway)
|
||||
}
|
||||
|
||||
func testNegativeCountdown() {
|
||||
let event = ReminderEvent.lookAwayTriggered(countdownSeconds: -5)
|
||||
|
||||
if case .lookAwayTriggered(let countdown) = event {
|
||||
XCTAssertEqual(countdown, -5)
|
||||
} else {
|
||||
XCTFail("Expected lookAwayTriggered event")
|
||||
}
|
||||
}
|
||||
|
||||
func testSwitchExhaustivenessWithAllCases() {
|
||||
let events: [ReminderEvent] = [
|
||||
.lookAwayTriggered(countdownSeconds: 20),
|
||||
.blinkTriggered,
|
||||
.postureTriggered
|
||||
]
|
||||
|
||||
for event in events {
|
||||
switch event {
|
||||
case .lookAwayTriggered:
|
||||
XCTAssertEqual(event.type, .lookAway)
|
||||
case .blinkTriggered:
|
||||
XCTAssertEqual(event.type, .blink)
|
||||
case .postureTriggered:
|
||||
XCTAssertEqual(event.type, .posture)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
GazeTests/Models/TimerConfigurationTests.swift
Normal file
124
GazeTests/Models/TimerConfigurationTests.swift
Normal file
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// TimerConfigurationTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Gaze
|
||||
|
||||
final class TimerConfigurationTests: XCTestCase {
|
||||
|
||||
func testInitialization() {
|
||||
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||
|
||||
XCTAssertTrue(config.enabled)
|
||||
XCTAssertEqual(config.intervalSeconds, 1200)
|
||||
}
|
||||
|
||||
func testInitializationDisabled() {
|
||||
let config = TimerConfiguration(enabled: false, intervalSeconds: 600)
|
||||
|
||||
XCTAssertFalse(config.enabled)
|
||||
XCTAssertEqual(config.intervalSeconds, 600)
|
||||
}
|
||||
|
||||
func testIntervalMinutesGetter() {
|
||||
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||
XCTAssertEqual(config.intervalMinutes, 20)
|
||||
}
|
||||
|
||||
func testIntervalMinutesSetter() {
|
||||
var config = TimerConfiguration(enabled: true, intervalSeconds: 0)
|
||||
config.intervalMinutes = 15
|
||||
|
||||
XCTAssertEqual(config.intervalMinutes, 15)
|
||||
XCTAssertEqual(config.intervalSeconds, 900)
|
||||
}
|
||||
|
||||
func testIntervalMinutesConversion() {
|
||||
var config = TimerConfiguration(enabled: true, intervalSeconds: 0)
|
||||
|
||||
config.intervalMinutes = 1
|
||||
XCTAssertEqual(config.intervalSeconds, 60)
|
||||
|
||||
config.intervalMinutes = 60
|
||||
XCTAssertEqual(config.intervalSeconds, 3600)
|
||||
|
||||
config.intervalMinutes = 0
|
||||
XCTAssertEqual(config.intervalSeconds, 0)
|
||||
}
|
||||
|
||||
func testEquality() {
|
||||
let config1 = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||
let config2 = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||
let config3 = TimerConfiguration(enabled: false, intervalSeconds: 1200)
|
||||
let config4 = TimerConfiguration(enabled: true, intervalSeconds: 600)
|
||||
|
||||
XCTAssertEqual(config1, config2)
|
||||
XCTAssertNotEqual(config1, config3)
|
||||
XCTAssertNotEqual(config1, config4)
|
||||
}
|
||||
|
||||
func testCodableEncoding() throws {
|
||||
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(config)
|
||||
|
||||
XCTAssertNotNil(data)
|
||||
XCTAssertGreaterThan(data.count, 0)
|
||||
}
|
||||
|
||||
func testCodableDecoding() throws {
|
||||
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(config)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let decoded = try decoder.decode(TimerConfiguration.self, from: data)
|
||||
|
||||
XCTAssertEqual(decoded, config)
|
||||
}
|
||||
|
||||
func testCodableRoundTrip() throws {
|
||||
let configs = [
|
||||
TimerConfiguration(enabled: true, intervalSeconds: 300),
|
||||
TimerConfiguration(enabled: false, intervalSeconds: 1200),
|
||||
TimerConfiguration(enabled: true, intervalSeconds: 1800),
|
||||
TimerConfiguration(enabled: false, intervalSeconds: 0)
|
||||
]
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
for config in configs {
|
||||
let data = try encoder.encode(config)
|
||||
let decoded = try decoder.decode(TimerConfiguration.self, from: data)
|
||||
XCTAssertEqual(decoded, config)
|
||||
}
|
||||
}
|
||||
|
||||
func testMutability() {
|
||||
var config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||
|
||||
config.enabled = false
|
||||
XCTAssertFalse(config.enabled)
|
||||
|
||||
config.intervalSeconds = 600
|
||||
XCTAssertEqual(config.intervalSeconds, 600)
|
||||
XCTAssertEqual(config.intervalMinutes, 10)
|
||||
}
|
||||
|
||||
func testZeroInterval() {
|
||||
let config = TimerConfiguration(enabled: true, intervalSeconds: 0)
|
||||
XCTAssertEqual(config.intervalSeconds, 0)
|
||||
XCTAssertEqual(config.intervalMinutes, 0)
|
||||
}
|
||||
|
||||
func testLargeInterval() {
|
||||
let config = TimerConfiguration(enabled: true, intervalSeconds: 86400)
|
||||
XCTAssertEqual(config.intervalSeconds, 86400)
|
||||
XCTAssertEqual(config.intervalMinutes, 1440)
|
||||
}
|
||||
}
|
||||
93
GazeTests/Models/TimerStateTests.swift
Normal file
93
GazeTests/Models/TimerStateTests.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// TimerStateTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Gaze
|
||||
|
||||
final class TimerStateTests: XCTestCase {
|
||||
|
||||
func testInitialization() {
|
||||
let state = TimerState(type: .lookAway, intervalSeconds: 1200)
|
||||
|
||||
XCTAssertEqual(state.type, .lookAway)
|
||||
XCTAssertEqual(state.remainingSeconds, 1200)
|
||||
XCTAssertFalse(state.isPaused)
|
||||
XCTAssertTrue(state.isActive)
|
||||
}
|
||||
|
||||
func testInitializationWithPausedState() {
|
||||
let state = TimerState(type: .blink, intervalSeconds: 300, isPaused: true)
|
||||
|
||||
XCTAssertEqual(state.type, .blink)
|
||||
XCTAssertEqual(state.remainingSeconds, 300)
|
||||
XCTAssertTrue(state.isPaused)
|
||||
XCTAssertTrue(state.isActive)
|
||||
}
|
||||
|
||||
func testInitializationWithInactiveState() {
|
||||
let state = TimerState(type: .posture, intervalSeconds: 1800, isPaused: false, isActive: false)
|
||||
|
||||
XCTAssertEqual(state.type, .posture)
|
||||
XCTAssertEqual(state.remainingSeconds, 1800)
|
||||
XCTAssertFalse(state.isPaused)
|
||||
XCTAssertFalse(state.isActive)
|
||||
}
|
||||
|
||||
func testMutability() {
|
||||
var state = TimerState(type: .lookAway, intervalSeconds: 1200)
|
||||
|
||||
state.remainingSeconds = 600
|
||||
XCTAssertEqual(state.remainingSeconds, 600)
|
||||
|
||||
state.isPaused = true
|
||||
XCTAssertTrue(state.isPaused)
|
||||
|
||||
state.isActive = false
|
||||
XCTAssertFalse(state.isActive)
|
||||
}
|
||||
|
||||
func testEquality() {
|
||||
let state1 = TimerState(type: .lookAway, intervalSeconds: 1200)
|
||||
let state2 = TimerState(type: .lookAway, intervalSeconds: 1200)
|
||||
let state3 = TimerState(type: .blink, intervalSeconds: 1200)
|
||||
let state4 = TimerState(type: .lookAway, intervalSeconds: 600)
|
||||
|
||||
XCTAssertEqual(state1, state2)
|
||||
XCTAssertNotEqual(state1, state3)
|
||||
XCTAssertNotEqual(state1, state4)
|
||||
}
|
||||
|
||||
func testEqualityWithDifferentPausedState() {
|
||||
let state1 = TimerState(type: .lookAway, intervalSeconds: 1200, isPaused: false)
|
||||
let state2 = TimerState(type: .lookAway, intervalSeconds: 1200, isPaused: true)
|
||||
|
||||
XCTAssertNotEqual(state1, state2)
|
||||
}
|
||||
|
||||
func testEqualityWithDifferentActiveState() {
|
||||
let state1 = TimerState(type: .lookAway, intervalSeconds: 1200, isActive: true)
|
||||
let state2 = TimerState(type: .lookAway, intervalSeconds: 1200, isActive: false)
|
||||
|
||||
XCTAssertNotEqual(state1, state2)
|
||||
}
|
||||
|
||||
func testZeroRemainingSeconds() {
|
||||
let state = TimerState(type: .lookAway, intervalSeconds: 0)
|
||||
XCTAssertEqual(state.remainingSeconds, 0)
|
||||
}
|
||||
|
||||
func testNegativeRemainingSeconds() {
|
||||
var state = TimerState(type: .lookAway, intervalSeconds: 10)
|
||||
state.remainingSeconds = -5
|
||||
XCTAssertEqual(state.remainingSeconds, -5)
|
||||
}
|
||||
|
||||
func testLargeIntervalSeconds() {
|
||||
let state = TimerState(type: .posture, intervalSeconds: 86400)
|
||||
XCTAssertEqual(state.remainingSeconds, 86400)
|
||||
}
|
||||
}
|
||||
68
GazeTests/Models/TimerTypeTests.swift
Normal file
68
GazeTests/Models/TimerTypeTests.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// TimerTypeTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Gaze
|
||||
|
||||
final class TimerTypeTests: XCTestCase {
|
||||
|
||||
func testAllCases() {
|
||||
let allCases = TimerType.allCases
|
||||
XCTAssertEqual(allCases.count, 3)
|
||||
XCTAssertTrue(allCases.contains(.lookAway))
|
||||
XCTAssertTrue(allCases.contains(.blink))
|
||||
XCTAssertTrue(allCases.contains(.posture))
|
||||
}
|
||||
|
||||
func testRawValues() {
|
||||
XCTAssertEqual(TimerType.lookAway.rawValue, "lookAway")
|
||||
XCTAssertEqual(TimerType.blink.rawValue, "blink")
|
||||
XCTAssertEqual(TimerType.posture.rawValue, "posture")
|
||||
}
|
||||
|
||||
func testDisplayNames() {
|
||||
XCTAssertEqual(TimerType.lookAway.displayName, "Look Away")
|
||||
XCTAssertEqual(TimerType.blink.displayName, "Blink")
|
||||
XCTAssertEqual(TimerType.posture.displayName, "Posture")
|
||||
}
|
||||
|
||||
func testIconNames() {
|
||||
XCTAssertEqual(TimerType.lookAway.iconName, "eye.fill")
|
||||
XCTAssertEqual(TimerType.blink.iconName, "eye.circle")
|
||||
XCTAssertEqual(TimerType.posture.iconName, "figure.stand")
|
||||
}
|
||||
|
||||
func testIdentifiable() {
|
||||
XCTAssertEqual(TimerType.lookAway.id, "lookAway")
|
||||
XCTAssertEqual(TimerType.blink.id, "blink")
|
||||
XCTAssertEqual(TimerType.posture.id, "posture")
|
||||
}
|
||||
|
||||
func testCodable() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
for timerType in TimerType.allCases {
|
||||
let encoded = try encoder.encode(timerType)
|
||||
let decoded = try decoder.decode(TimerType.self, from: encoded)
|
||||
XCTAssertEqual(decoded, timerType)
|
||||
}
|
||||
}
|
||||
|
||||
func testEquality() {
|
||||
XCTAssertEqual(TimerType.lookAway, TimerType.lookAway)
|
||||
XCTAssertNotEqual(TimerType.lookAway, TimerType.blink)
|
||||
XCTAssertNotEqual(TimerType.blink, TimerType.posture)
|
||||
}
|
||||
|
||||
func testInitFromRawValue() {
|
||||
XCTAssertEqual(TimerType(rawValue: "lookAway"), .lookAway)
|
||||
XCTAssertEqual(TimerType(rawValue: "blink"), .blink)
|
||||
XCTAssertEqual(TimerType(rawValue: "posture"), .posture)
|
||||
XCTAssertNil(TimerType(rawValue: "invalid"))
|
||||
}
|
||||
}
|
||||
71
GazeTests/Services/LaunchAtLoginManagerTests.swift
Normal file
71
GazeTests/Services/LaunchAtLoginManagerTests.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// LaunchAtLoginManagerTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Gaze
|
||||
|
||||
final class LaunchAtLoginManagerTests: XCTestCase {
|
||||
|
||||
func testIsEnabledReturnsBool() {
|
||||
let isEnabled = LaunchAtLoginManager.isEnabled
|
||||
XCTAssertNotNil(isEnabled)
|
||||
}
|
||||
|
||||
func testIsEnabledOnMacOS13AndLater() {
|
||||
if #available(macOS 13.0, *) {
|
||||
let isEnabled = LaunchAtLoginManager.isEnabled
|
||||
XCTAssert(isEnabled == true || isEnabled == false)
|
||||
}
|
||||
}
|
||||
|
||||
func testIsEnabledOnOlderMacOS() {
|
||||
if #unavailable(macOS 13.0) {
|
||||
let isEnabled = LaunchAtLoginManager.isEnabled
|
||||
XCTAssertFalse(isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
func testEnableThrowsOnUnsupportedOS() {
|
||||
if #unavailable(macOS 13.0) {
|
||||
XCTAssertThrowsError(try LaunchAtLoginManager.enable()) { error in
|
||||
XCTAssertTrue(error is LaunchAtLoginError)
|
||||
if let launchError = error as? LaunchAtLoginError {
|
||||
XCTAssertEqual(launchError, .unsupportedOS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDisableThrowsOnUnsupportedOS() {
|
||||
if #unavailable(macOS 13.0) {
|
||||
XCTAssertThrowsError(try LaunchAtLoginManager.disable()) { error in
|
||||
XCTAssertTrue(error is LaunchAtLoginError)
|
||||
if let launchError = error as? LaunchAtLoginError {
|
||||
XCTAssertEqual(launchError, .unsupportedOS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testToggleDoesNotCrash() {
|
||||
LaunchAtLoginManager.toggle()
|
||||
}
|
||||
|
||||
func testLaunchAtLoginErrorCases() {
|
||||
let unsupportedError = LaunchAtLoginError.unsupportedOS
|
||||
let registrationError = LaunchAtLoginError.registrationFailed
|
||||
|
||||
XCTAssertNotEqual(unsupportedError, registrationError)
|
||||
}
|
||||
|
||||
func testLaunchAtLoginErrorEquality() {
|
||||
let error1 = LaunchAtLoginError.unsupportedOS
|
||||
let error2 = LaunchAtLoginError.unsupportedOS
|
||||
|
||||
XCTAssertEqual(error1, error2)
|
||||
}
|
||||
}
|
||||
137
GazeTests/Services/MigrationManagerTests.swift
Normal file
137
GazeTests/Services/MigrationManagerTests.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// MigrationManagerTests.swift
|
||||
// GazeTests
|
||||
//
|
||||
// Created by Mike Freno on 1/8/26.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Gaze
|
||||
|
||||
final class MigrationManagerTests: XCTestCase {
|
||||
|
||||
var migrationManager: MigrationManager!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
migrationManager = MigrationManager()
|
||||
UserDefaults.standard.removeObject(forKey: "app_version")
|
||||
UserDefaults.standard.removeObject(forKey: "gazeAppSettings")
|
||||
UserDefaults.standard.removeObject(forKey: "gazeAppSettings_backup")
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
UserDefaults.standard.removeObject(forKey: "app_version")
|
||||
UserDefaults.standard.removeObject(forKey: "gazeAppSettings")
|
||||
UserDefaults.standard.removeObject(forKey: "gazeAppSettings_backup")
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testGetCurrentVersionDefaultsToZero() {
|
||||
let version = migrationManager.getCurrentVersion()
|
||||
XCTAssertEqual(version, "0.0.0")
|
||||
}
|
||||
|
||||
func testSetCurrentVersion() {
|
||||
migrationManager.setCurrentVersion("1.2.3")
|
||||
let version = migrationManager.getCurrentVersion()
|
||||
XCTAssertEqual(version, "1.2.3")
|
||||
}
|
||||
|
||||
func testMigrateSettingsReturnsNilWhenNoSettings() throws {
|
||||
let result = try migrationManager.migrateSettingsIfNeeded()
|
||||
XCTAssertNil(result)
|
||||
}
|
||||
|
||||
func testMigrateSettingsReturnsExistingDataWhenUpToDate() throws {
|
||||
let testData: [String: Any] = ["test": "value"]
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: testData)
|
||||
UserDefaults.standard.set(jsonData, forKey: "gazeAppSettings")
|
||||
|
||||
if let bundleVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
|
||||
migrationManager.setCurrentVersion(bundleVersion)
|
||||
}
|
||||
|
||||
let result = try migrationManager.migrateSettingsIfNeeded()
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?["test"] as? String, "value")
|
||||
}
|
||||
|
||||
func testMigrationErrorTypes() {
|
||||
let migrationFailed = MigrationError.migrationFailed("test")
|
||||
let invalidData = MigrationError.invalidDataStructure
|
||||
let versionMismatch = MigrationError.versionMismatch
|
||||
let noBackup = MigrationError.noBackupAvailable
|
||||
|
||||
switch migrationFailed {
|
||||
case .migrationFailed(let message):
|
||||
XCTAssertEqual(message, "test")
|
||||
default:
|
||||
XCTFail("Expected migrationFailed error")
|
||||
}
|
||||
|
||||
XCTAssertNotNil(invalidData.errorDescription)
|
||||
XCTAssertNotNil(versionMismatch.errorDescription)
|
||||
XCTAssertNotNil(noBackup.errorDescription)
|
||||
}
|
||||
|
||||
func testMigrationErrorDescriptions() {
|
||||
let errors: [MigrationError] = [
|
||||
.migrationFailed("test message"),
|
||||
.invalidDataStructure,
|
||||
.versionMismatch,
|
||||
.noBackupAvailable
|
||||
]
|
||||
|
||||
for error in errors {
|
||||
XCTAssertNotNil(error.errorDescription)
|
||||
XCTAssertFalse(error.errorDescription!.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func testVersion101MigrationTargetVersion() {
|
||||
let migration = Version101Migration()
|
||||
XCTAssertEqual(migration.targetVersion, "1.0.1")
|
||||
}
|
||||
|
||||
func testVersion101MigrationPreservesData() throws {
|
||||
let migration = Version101Migration()
|
||||
let testData: [String: Any] = ["key1": "value1", "key2": 42]
|
||||
|
||||
let result = try migration.migrate(testData)
|
||||
|
||||
XCTAssertEqual(result["key1"] as? String, "value1")
|
||||
XCTAssertEqual(result["key2"] as? Int, 42)
|
||||
}
|
||||
|
||||
func testMigrationThrowsOnInvalidData() {
|
||||
UserDefaults.standard.set(Data("invalid json".utf8), forKey: "gazeAppSettings")
|
||||
migrationManager.setCurrentVersion("0.0.0")
|
||||
|
||||
XCTAssertThrowsError(try migrationManager.migrateSettingsIfNeeded()) { error in
|
||||
XCTAssertTrue(error is MigrationError)
|
||||
}
|
||||
}
|
||||
|
||||
func testVersionComparison() throws {
|
||||
migrationManager.setCurrentVersion("1.0.0")
|
||||
let current = migrationManager.getCurrentVersion()
|
||||
XCTAssertEqual(current, "1.0.0")
|
||||
|
||||
migrationManager.setCurrentVersion("1.2.3")
|
||||
let updated = migrationManager.getCurrentVersion()
|
||||
XCTAssertEqual(updated, "1.2.3")
|
||||
}
|
||||
|
||||
func testBackupIsCreatedDuringMigration() throws {
|
||||
let testData: [String: Any] = ["test": "backup"]
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: testData)
|
||||
UserDefaults.standard.set(jsonData, forKey: "gazeAppSettings")
|
||||
migrationManager.setCurrentVersion("0.0.0")
|
||||
|
||||
_ = try? migrationManager.migrateSettingsIfNeeded()
|
||||
|
||||
let backupData = UserDefaults.standard.data(forKey: "gazeAppSettings_backup")
|
||||
XCTAssertNotNil(backupData)
|
||||
}
|
||||
}
|
||||
@@ -122,4 +122,101 @@ final class SettingsManagerTests: XCTestCase {
|
||||
config.intervalMinutes = 20
|
||||
XCTAssertEqual(config.intervalSeconds, 1200)
|
||||
}
|
||||
|
||||
func testSettingsAutoSaveOnChange() {
|
||||
var settings = AppSettings.defaults
|
||||
settings.playSounds = false
|
||||
|
||||
settingsManager.settings = settings
|
||||
|
||||
let savedData = UserDefaults.standard.data(forKey: "gazeAppSettings")
|
||||
XCTAssertNotNil(savedData)
|
||||
}
|
||||
|
||||
func testMultipleTimerConfigurationUpdates() {
|
||||
let config1 = TimerConfiguration(enabled: false, intervalSeconds: 600)
|
||||
settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config1)
|
||||
|
||||
let config2 = TimerConfiguration(enabled: true, intervalSeconds: 900)
|
||||
settingsManager.updateTimerConfiguration(for: .blink, configuration: config2)
|
||||
|
||||
let config3 = TimerConfiguration(enabled: false, intervalSeconds: 2400)
|
||||
settingsManager.updateTimerConfiguration(for: .posture, configuration: config3)
|
||||
|
||||
XCTAssertEqual(settingsManager.timerConfiguration(for: .lookAway).intervalSeconds, 600)
|
||||
XCTAssertEqual(settingsManager.timerConfiguration(for: .blink).intervalSeconds, 900)
|
||||
XCTAssertEqual(settingsManager.timerConfiguration(for: .posture).intervalSeconds, 2400)
|
||||
|
||||
XCTAssertFalse(settingsManager.timerConfiguration(for: .lookAway).enabled)
|
||||
XCTAssertTrue(settingsManager.timerConfiguration(for: .blink).enabled)
|
||||
XCTAssertFalse(settingsManager.timerConfiguration(for: .posture).enabled)
|
||||
}
|
||||
|
||||
func testSettingsPersistenceAcrossReloads() {
|
||||
var settings = AppSettings.defaults
|
||||
settings.lookAwayCountdownSeconds = 45
|
||||
settings.playSounds = false
|
||||
|
||||
settingsManager.settings = settings
|
||||
settingsManager.load()
|
||||
|
||||
XCTAssertEqual(settingsManager.settings.lookAwayCountdownSeconds, 45)
|
||||
XCTAssertFalse(settingsManager.settings.playSounds)
|
||||
}
|
||||
|
||||
func testInvalidDataDoesNotCrashLoad() {
|
||||
UserDefaults.standard.set(Data("invalid".utf8), forKey: "gazeAppSettings")
|
||||
|
||||
settingsManager.load()
|
||||
|
||||
XCTAssertEqual(settingsManager.settings, .defaults)
|
||||
}
|
||||
|
||||
func testAllTimerTypesHaveConfiguration() {
|
||||
for timerType in TimerType.allCases {
|
||||
let config = settingsManager.timerConfiguration(for: timerType)
|
||||
XCTAssertNotNil(config)
|
||||
}
|
||||
}
|
||||
|
||||
func testUpdateTimerConfigurationPersists() {
|
||||
let newConfig = TimerConfiguration(enabled: false, intervalSeconds: 7200)
|
||||
settingsManager.updateTimerConfiguration(for: .posture, configuration: newConfig)
|
||||
|
||||
settingsManager.load()
|
||||
|
||||
let retrieved = settingsManager.timerConfiguration(for: .posture)
|
||||
XCTAssertEqual(retrieved.intervalSeconds, 7200)
|
||||
XCTAssertFalse(retrieved.enabled)
|
||||
}
|
||||
|
||||
func testResetToDefaultsClearsAllChanges() {
|
||||
settingsManager.settings.lookAwayTimer.enabled = false
|
||||
settingsManager.settings.lookAwayCountdownSeconds = 60
|
||||
settingsManager.settings.blinkTimer.intervalSeconds = 10 * 60
|
||||
settingsManager.settings.postureTimer.enabled = false
|
||||
settingsManager.settings.hasCompletedOnboarding = true
|
||||
settingsManager.settings.launchAtLogin = true
|
||||
settingsManager.settings.playSounds = false
|
||||
|
||||
settingsManager.resetToDefaults()
|
||||
|
||||
let defaults = AppSettings.defaults
|
||||
XCTAssertEqual(settingsManager.settings, defaults)
|
||||
}
|
||||
|
||||
func testConcurrentAccessDoesNotCrash() {
|
||||
let expectation = XCTestExpectation(description: "Concurrent access")
|
||||
expectation.expectedFulfillmentCount = 10
|
||||
|
||||
for i in 0..<10 {
|
||||
Task { @MainActor in
|
||||
let config = TimerConfiguration(enabled: true, intervalSeconds: 300 * (i + 1))
|
||||
settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config)
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user