Introduces a SwiftUI companion app for the Fail Well PWA: menu bar status item, popover with task list / record / history views, command palette, and a Dynamic Island-style notch overlay that shows the active step and timer beside the hardware notch and expands on hover to reveal pause/next controls.
168 lines
6.0 KiB
Swift
168 lines
6.0 KiB
Swift
import Testing
|
|
@testable import FailWell
|
|
|
|
@Suite("StepParser")
|
|
struct StepParserTests {
|
|
@Test func adaptsStepsToTextarea() {
|
|
let steps = [
|
|
Step(id: "a", title: "step 1", estimation: 3),
|
|
Step(id: "b", title: "step 2", estimation: 4),
|
|
Step(id: "c", title: "step 3", estimation: 5)
|
|
]
|
|
#expect(StepParser.adaptStepsToTextarea(steps) == "- step 1 | 3\n- step 2 | 4\n- step 3 | 5")
|
|
}
|
|
|
|
@Test func parsesTextareaIntoSteps() {
|
|
let input = """
|
|
- step 1 | 3
|
|
- step 2 | 4
|
|
- step 3 | 5
|
|
"""
|
|
let steps = StepParser.adaptTextareaToSteps(input)
|
|
#expect(steps.map(\.title) == ["step 1", "step 2", "step 3"])
|
|
#expect(steps.map(\.estimation) == [3, 4, 5])
|
|
}
|
|
|
|
@Test func fallsBackToZeroWhenNoEstimation() {
|
|
let steps = StepParser.adaptTextareaToSteps("- step 1")
|
|
#expect(steps.count == 1)
|
|
#expect(steps[0].title == "step 1")
|
|
#expect(steps[0].estimation == 0)
|
|
}
|
|
|
|
@Test func fallsBackToZeroWhenEstimationUnparseable() {
|
|
let steps = StepParser.adaptTextareaToSteps("- step 1 | not an estimation")
|
|
#expect(steps.count == 1)
|
|
#expect(steps[0].title == "step 1")
|
|
#expect(steps[0].estimation == 0)
|
|
}
|
|
|
|
@Test func skipsEmptyTitles() {
|
|
let steps = StepParser.adaptTextareaToSteps("\n-")
|
|
#expect(steps.isEmpty)
|
|
}
|
|
|
|
@Test func generatesStableIdsFromTitleAlone() {
|
|
// Step identity is title-only (md5(title) + duplicate suffix). Re-estimating
|
|
// the same title must yield the same id so the Step Record survives.
|
|
let single = StepParser.adaptTextareaToSteps("- step 1 | 3")
|
|
#expect(single.first?.id == "7eedced2241bcccda700ccac1b5915d6-1")
|
|
|
|
let duped = StepParser.adaptTextareaToSteps("""
|
|
- step duplicated | 3
|
|
- step duplicated | 3
|
|
""")
|
|
#expect(duped.map(\.id) == [
|
|
"b53d37cc4caaf9d8d4f1e459e335a242-1",
|
|
"b53d37cc4caaf9d8d4f1e459e335a242-2"
|
|
])
|
|
}
|
|
|
|
@Test func reestimationPreservesStepIdentity() {
|
|
// CONTEXT.md §"Re-estimation": changing the estimation never destroys data.
|
|
// Same title with different estimations must produce the same Step id.
|
|
let original = StepParser.adaptTextareaToSteps("- refactor parser | 5")
|
|
let reestimated = StepParser.adaptTextareaToSteps("- refactor parser | 12")
|
|
#expect(original.first?.id == reestimated.first?.id)
|
|
#expect(original.first?.estimation == 5)
|
|
#expect(reestimated.first?.estimation == 12)
|
|
}
|
|
|
|
@Test func parsesUncheckedCheckboxes() {
|
|
let steps = StepParser.adaptTextareaToSteps("- [ ] step with checkbox | 20")
|
|
#expect(steps.count == 1)
|
|
#expect(steps[0].title == "step with checkbox")
|
|
#expect(steps[0].estimation == 20)
|
|
}
|
|
|
|
@Test func parsesCheckedCheckboxes() {
|
|
let steps = StepParser.adaptTextareaToSteps("- [x] completed step | 15")
|
|
#expect(steps.count == 1)
|
|
#expect(steps[0].title == "completed step")
|
|
#expect(steps[0].estimation == 15)
|
|
}
|
|
|
|
@Test func parsesCheckboxesWithoutEstimation() {
|
|
let steps = StepParser.adaptTextareaToSteps("- [ ] step without estimation")
|
|
#expect(steps.count == 1)
|
|
#expect(steps[0].title == "step without estimation")
|
|
#expect(steps[0].estimation == 0)
|
|
}
|
|
|
|
@Suite("subtask support")
|
|
struct SubtaskTests {
|
|
@Test func flattensIndentedSubtasksWithParentPrefix() {
|
|
let input = """
|
|
- Parent task | 5
|
|
- Child task 1 | 3
|
|
- Child task 2 | 2
|
|
"""
|
|
let steps = StepParser.adaptTextareaToSteps(input)
|
|
#expect(steps.map(\.title) == ["(Parent task) - Child task 1", "(Parent task) - Child task 2"])
|
|
#expect(steps.map(\.estimation) == [3, 2])
|
|
}
|
|
|
|
@Test func ignoresParentEstimationWhenItHasChildren() {
|
|
let steps = StepParser.adaptTextareaToSteps("""
|
|
- Parent | 10
|
|
- Child | 3
|
|
""")
|
|
#expect(steps.count == 1)
|
|
#expect(steps[0].title == "(Parent) - Child")
|
|
#expect(steps[0].estimation == 3)
|
|
}
|
|
|
|
@Test func treatsStandaloneItemsAsRegularSteps() {
|
|
let steps = StepParser.adaptTextareaToSteps("- Standalone task | 4")
|
|
#expect(steps.count == 1)
|
|
#expect(steps[0].title == "Standalone task")
|
|
#expect(steps[0].estimation == 4)
|
|
}
|
|
|
|
@Test func handlesMixedSubtasksAndStandaloneItems() {
|
|
let input = """
|
|
- Parent task | 5
|
|
- Child task 1 | 3
|
|
- Child task 2 | 2
|
|
- Standalone | 4
|
|
"""
|
|
let steps = StepParser.adaptTextareaToSteps(input)
|
|
#expect(steps.map(\.title) == [
|
|
"(Parent task) - Child task 1",
|
|
"(Parent task) - Child task 2",
|
|
"Standalone"
|
|
])
|
|
#expect(steps.map(\.estimation) == [3, 2, 4])
|
|
}
|
|
|
|
@Test func handlesTabIndentation() {
|
|
let steps = StepParser.adaptTextareaToSteps("- Parent | 5\n\t- Child | 3")
|
|
#expect(steps.count == 1)
|
|
#expect(steps[0].title == "(Parent) - Child")
|
|
}
|
|
|
|
@Test func treatsOrphanIndentedLineAsRegularStep() {
|
|
let steps = StepParser.adaptTextareaToSteps(" - Orphan indented | 3")
|
|
#expect(steps.count == 1)
|
|
#expect(steps[0].title == "Orphan indented")
|
|
#expect(steps[0].estimation == 3)
|
|
}
|
|
|
|
@Test func handlesMultipleParentsWithChildren() {
|
|
let input = """
|
|
- Parent 1 | 5
|
|
- Child 1a | 2
|
|
- Child 1b | 3
|
|
- Parent 2 | 4
|
|
- Child 2a | 1
|
|
"""
|
|
let steps = StepParser.adaptTextareaToSteps(input)
|
|
#expect(steps.map(\.title) == [
|
|
"(Parent 1) - Child 1a",
|
|
"(Parent 1) - Child 1b",
|
|
"(Parent 2) - Child 2a"
|
|
])
|
|
}
|
|
}
|
|
}
|