diff --git a/Docs/TESTS_COVERAGE.md b/Docs/TESTS_COVERAGE.md
index 9960730..ad8ff1f 100644
--- a/Docs/TESTS_COVERAGE.md
+++ b/Docs/TESTS_COVERAGE.md
@@ -2,7 +2,7 @@
This document tracks unit-test candidates discovered while scanning the QizhKit codebase, grouped by source area. Each entry lists the public APIs worth covering and concrete test ideas to validate their behavior.
-## ⊞ [Components/Airtable/AirtableFormulaBuilder.swift](Components/Airtable/AirtableFormulaBuilder.swift)
+## - [ ] ⊞ [Components/Airtable/AirtableFormulaBuilder.swift](Components/Airtable/AirtableFormulaBuilder.swift)
@@ -24,7 +24,7 @@ This document tracks unit-test candidates discovered while scanning the QizhKit
|
-`testProducesAirtableFriendlyStrings`
+- [ ] `testProducesAirtableFriendlyStrings`
|
`.equals`, `.notEquals`, `.isEmpty`, and `.id`
@@ -35,7 +35,7 @@ Produce Airtable-friendly strings
|
|
-`testCombinesFormulasWithAndOrNot`
+- [ ] `testCombinesFormulasWithAndOrNot`
|
`.and`, `.or`, `.not`
@@ -46,7 +46,7 @@ Validate `.and`, `.or`, and `.not` nest descriptions correctly for multiple chil
|
|
-`testEscapesApostrophesInInterpolation`
+- [ ] `testEscapesApostrophesInInterpolation`
|
`appendInterpolation(_:)`, `withApostrophesEscaped`
@@ -60,7 +60,7 @@ Verify the custom string interpolation paths escape single quotes consistently f
|
-## ⊞ [Components/Random Generators/SeededRandomGenerator.swift](Components/Random%20Generators/SeededRandomGenerator.swift)
+## - [ ] ⊞ [Components/Random Generators/SeededRandomGenerator.swift](Components/Random%20Generators/SeededRandomGenerator.swift)
@@ -80,7 +80,7 @@ Verify the custom string interpolation paths escape single quotes consistently f
|
-`testProducesRepeatableSequence`
+- [ ] `testProducesRepeatableSequence`
|
Confirm identical seeds emit identical sequences across multiple draws
@@ -88,7 +88,7 @@ Confirm identical seeds emit identical sequences across multiple draws
|
|
-`testMixes64BitOutput`
+- [ ] `testMixes64BitOutput`
|
Assert two 32-bit GK samples are combined into varying high/low bits to prevent bias
@@ -96,7 +96,7 @@ Assert two 32-bit GK samples are combined into varying high/low bits to prevent
|
|
-`testAdvancesStateBetweenCalls`
+- [ ] `testAdvancesStateBetweenCalls`
|
Ensure successive `next()` calls mutate generator state (no repeated constant)
@@ -107,7 +107,7 @@ Ensure successive `next()` calls mutate generator state (no repeated constant)
|
-## ⊞ [Extensions/String+/String+modify.swift](Extensions/String+/String+modify.swift)
+## - [ ] ⊞ [Extensions/String+/String+modify.swift](Extensions/String+/String+modify.swift)
@@ -131,7 +131,7 @@ Ensure successive `next()` calls mutate generator state (no repeated constant)
|
-`testReplacesAndTrimsStrings`
+- [ ] `testReplacesAndTrimsStrings`
|
Cover replacements by set/value and trimming behaviors including empty-line removal
@@ -139,7 +139,7 @@ Cover replacements by set/value and trimming behaviors including empty-line remo
|
|
-`testTrimsTrailingCharacters`
+- [ ] `testTrimsTrailingCharacters`
|
Verify targeted trailing whitespace/newline removal paths
@@ -147,7 +147,7 @@ Verify targeted trailing whitespace/newline removal paths
|
|
-`testRepeatsStringWithMultiplicationOperator`
+- [ ] `testRepeatsStringWithMultiplicationOperator`
|
Ensure `"abc" * 3` returns expected concatenation
@@ -155,7 +155,7 @@ Ensure `"abc" * 3` returns expected concatenation
|
|
-`testStringOffsetPresetsEmitExpectedTokens`
+- [ ] `testStringOffsetPresetsEmitExpectedTokens`
|
Validate `StringOffset` preset suffix/prefix strings and computed properties
@@ -163,7 +163,7 @@ Validate `StringOffset` preset suffix/prefix strings and computed properties
|
|
-`testOffsetsMultilineBlocks`
+- [ ] `testOffsetsMultilineBlocks`
|
Assert offsetting helpers pad each line as documented
@@ -174,7 +174,7 @@ Assert offsetting helpers pad each line as documented
|
-## ⊞ [Structures/Dimensions/GeometryReceivers.swift](Structures/Dimensions/GeometryReceivers.swift)
+## - [ ] ⊞ [Structures/Dimensions/GeometryReceivers.swift](Structures/Dimensions/GeometryReceivers.swift)
@@ -194,7 +194,7 @@ Assert offsetting helpers pad each line as documented
|
-`testCapturesWidthAndHeightPreferences`
+- [ ] `testCapturesWidthAndHeightPreferences`
|
Inject test views and confirm bindings receive geometry values once layout occurs
@@ -202,7 +202,7 @@ Inject test views and confirm bindings receive geometry values once layout occur
|
|
-`testInvokesCallbacksOnChange`
+- [ ] `testInvokesCallbacksOnChange`
|
Ensure callbacks fire with updated dimensions when layout changes
@@ -210,7 +210,7 @@ Ensure callbacks fire with updated dimensions when layout changes
|
|
-`testBindsOptionalAndNonoptionalInsets`
+- [ ] `testBindsOptionalAndNonoptionalInsets`
|
Verify both `EdgeInsets` and `EdgeInsets?` bindings are updated through the preference chain
@@ -221,7 +221,7 @@ Verify both `EdgeInsets` and `EdgeInsets?` bindings are updated through the pref
|
-## ⊞ [Structures/Dimensions/RelativeDimension.swift](Structures/Dimensions/RelativeDimension.swift)
+## - [ ] ⊞ [Structures/Dimensions/RelativeDimension.swift](Structures/Dimensions/RelativeDimension.swift)
@@ -243,7 +243,7 @@ Verify both `EdgeInsets` and `EdgeInsets?` bindings are updated through the pref
|
-`testInitializesFromLiterals`
+- [ ] `testInitializesFromLiterals`
|
Confirm float/integer literal initializers map to `.exactly` with converted `CGFloat`
@@ -251,7 +251,7 @@ Confirm float/integer literal initializers map to `.exactly` with converted `CGF
|
|
-`testExposesValueAndMaxValue`
+- [ ] `testExposesValueAndMaxValue`
|
Validate optional outputs for `exactly` vs `maximum` cases
@@ -259,7 +259,7 @@ Validate optional outputs for `exactly` vs `maximum` cases
|
|
-`testMinimumCaseReportsPadding`
+- [ ] `testMinimumCaseReportsPadding`
|
Ensure `.minimum` carries the provided padding
@@ -267,7 +267,7 @@ Ensure `.minimum` carries the provided padding
|
|
-`testComparisonHelpersMatchCases`
+- [ ] `testComparisonHelpersMatchCases`
|
Test `is` and convenience flags across all permutations
@@ -278,7 +278,7 @@ Test `is` and convenience flags across all permutations
|
-## ⊞ [Structures/Type Erase/AnyComparable.swift](Structures/Type%20Erase/AnyComparable.swift)
+## - [ ] ⊞ [Structures/Type Erase/AnyComparable.swift](Structures/Type%20Erase/AnyComparable.swift)
@@ -300,7 +300,7 @@ Test `is` and convenience flags across all permutations
|
-`testComparesBoxedValues`
+- [ ] `testComparesBoxedValues`
|
Assert `<` and `==` use underlying `Comparable` semantics for same-typed boxes
@@ -308,7 +308,7 @@ Assert `<` and `==` use underlying `Comparable` semantics for same-typed boxes
|
|
-`testHandlesCrossTypeComparisonsSafely`
+- [ ] `testHandlesCrossTypeComparisonsSafely`
|
Ensure comparisons with different underlying types return `false` without crashes
@@ -316,7 +316,7 @@ Ensure comparisons with different underlying types return `false` without crashe
|
|
-`testWrapsComparableValues`
+- [ ] `testWrapsComparableValues`
|
Verify `.asAnyComparable()` wraps and preserves ordering in sorted collections
@@ -327,7 +327,7 @@ Verify `.asAnyComparable()` wraps and preserves ordering in sorted collections
|
-## ⊞ [Structures/Type Erase/AnyHashableAndSendable.swift](Structures/Type%20Erase/AnyHashableAndSendable.swift)
+## - [ ] ⊞ [Structures/Type Erase/AnyHashableAndSendable.swift](Structures/Type%20Erase/AnyHashableAndSendable.swift)
@@ -349,7 +349,7 @@ Verify `.asAnyComparable()` wraps and preserves ordering in sorted collections
|
-`testBoxesPreserveHashAndEquality`
+- [ ] `testBoxesPreserveHashAndEquality`
|
Verify wrappers round-trip `Hashable`/`Sendable` values and compare correctly across identical and differing types
@@ -357,7 +357,7 @@ Verify wrappers round-trip `Hashable`/`Sendable` values and compare correctly ac
|
|
-`testEncodesWrappedValues`
+- [ ] `testEncodesWrappedValues`
|
Ensure encodable wrappers forward encoding to the underlying value and produce expected JSON/JSON5 strings
@@ -365,7 +365,7 @@ Ensure encodable wrappers forward encoding to the underlying value and produce e
|
|
-`testSupportsPropertyWrapperInitStyles`
+- [ ] `testSupportsPropertyWrapperInitStyles`
|
Cover both `init(wrappedValue:)` and direct initializers for each wrapper
@@ -373,7 +373,7 @@ Cover both `init(wrappedValue:)` and direct initializers for each wrapper
|
|
-`testHandlesNonEncodableDictionaryEntries`
+- [ ] `testHandlesNonEncodableDictionaryEntries`
|
Assert encoding helpers return the fallback message when dictionary cannot be cast to `Encodable`
@@ -384,7 +384,7 @@ Assert encoding helpers return the fallback message when dictionary cannot be ca
|
-## ⊞ [Ugly/WindowUtils.swift](Ugly/WindowUtils.swift)
+## - [ ] ⊞ [Ugly/WindowUtils.swift](Ugly/WindowUtils.swift)
@@ -405,7 +405,7 @@ Assert encoding helpers return the fallback message when dictionary cannot be ca
|
-`testTracksManuallyAssignedWindow`
+- [ ] `testTracksManuallyAssignedWindow`
|
Confirm `setOriginalWindow` overrides lookup and restores when cleared
@@ -413,7 +413,7 @@ Confirm `setOriginalWindow` overrides lookup and restores when cleared
|
|
-`testResolvesTopViewController`
+- [ ] `testResolvesTopViewController`
|
Simulate navigation/tab/presentation stacks to ensure the traversal selects the visible controller
@@ -421,7 +421,7 @@ Simulate navigation/tab/presentation stacks to ensure the traversal selects the
|
|
-`testEndsEditingThroughCurrentWindow`
+- [ ] `testEndsEditingThroughCurrentWindow`
|
Verify `endEditing(force:)` relays to the active window and respects the `force` flag
@@ -429,7 +429,7 @@ Verify `endEditing(force:)` relays to the active window and respects the `force`
|
|
-`testReportsSafeAreaInsets`
+- [ ] `testReportsSafeAreaInsets`
|
Validate `SafeFrame.currentInsets` mirrors the active window's safe area
@@ -440,7 +440,7 @@ Validate `SafeFrame.currentInsets` mirrors the active window's safe area
|
-## ⊞ [Third Party/Pluralize/Pluralize.swift](Third%20Party/Pluralize/Pluralize.swift)
+## - [x] ⊞ [Third Party/Pluralize/Pluralize.swift](Third%20Party/Pluralize/Pluralize.swift)
@@ -460,7 +460,7 @@ Validate `SafeFrame.currentInsets` mirrors the active window's safe area
|
-`testPluralizesAndSingularizesCommonWords`
+- [x] `testPluralizesAndSingularizesCommonWords` — Implemented in `Tests/QizhKitTests/PluralizeTests.swift`. Verifies regular and irregular word pluralization/singularization including common words, Latin/Greek endings, and the `count`/`with` parameters.
|
Check irregular and regular transformations for representative samples
@@ -468,7 +468,7 @@ Check irregular and regular transformations for representative samples
|
|
-`testHonorsUncountableAndUnchangingLists`
+- [x] `testHonorsUncountableAndUnchangingLists` — Implemented in `Tests/QizhKitTests/PluralizeTests.swift`. Validates that uncountable words (information, equipment, etc.) and unchanging words (sheep, deer, etc.) remain unchanged when pluralized/singularized.
|
Confirm words in those collections return unchanged results
@@ -476,7 +476,7 @@ Confirm words in those collections return unchanged results
|
|
-`testAddsRuntimeRules`
+- [x] `testAddsRuntimeRules` — Implemented in `Tests/QizhKitTests/PluralizeTests.swift`. Verifies that dynamically added plural/singular rules via `Pluralize.rule()`, `Pluralize.singularRule()`, `Pluralize.uncountable()`, and `Pluralize.unchanging()` are applied correctly.
|
Ensure dynamically added plural/singular rules apply ahead of defaults
diff --git a/Tests/QizhKitTests/PluralizeTests.swift b/Tests/QizhKitTests/PluralizeTests.swift
new file mode 100644
index 0000000..540610f
--- /dev/null
+++ b/Tests/QizhKitTests/PluralizeTests.swift
@@ -0,0 +1,153 @@
+//
+// PluralizeTests.swift
+// QizhKit
+//
+// Created by GitHub Copilot on 06.12.2025.
+//
+
+import Testing
+@testable import QizhKit
+
+/// Tests for the Pluralize class and String pluralization extensions.
+/// Validates plural/singular transformations, uncountable/unchanging word handling,
+/// and dynamic rule additions.
+@Suite("Pluralize tests")
+struct PluralizeTests {
+
+ // MARK: - testPluralizesAndSingularizesCommonWords
+
+ /// Verifies that common words are correctly pluralized and singularized,
+ /// including both regular and irregular transformations.
+ @Test func testPluralizesAndSingularizesCommonWords() async throws {
+ // Regular pluralization rules
+ #expect("cat".pluralize() == "cats")
+ #expect("dog".pluralize() == "dogs")
+ #expect("house".pluralize() == "houses")
+
+ // Words ending in 'y' preceded by consonant -> 'ies'
+ #expect("city".pluralize() == "cities")
+ #expect("baby".pluralize() == "babies")
+ #expect("party".pluralize() == "parties")
+
+ // Words ending in 'x' -> 'xes'
+ #expect("box".pluralize() == "boxes")
+ #expect("tax".pluralize() == "taxes")
+
+ // Words ending in 'sh', 'ss', 'zz' -> 'es'
+ #expect("wish".pluralize() == "wishes")
+ #expect("class".pluralize() == "classes")
+ #expect("buzz".pluralize() == "buzzes")
+
+ // Words ending in 'ch' -> 'ches'
+ #expect("watch".pluralize() == "watches")
+ #expect("church".pluralize() == "churches")
+
+ // Irregular plurals
+ #expect("tooth".pluralize() == "teeth")
+ #expect("foot".pluralize() == "feet")
+ #expect("goose".pluralize() == "geese")
+ #expect("mouse".pluralize() == "mice")
+ #expect("child".pluralize() == "children")
+ #expect("person".pluralize() == "people")
+ #expect("man".pluralize() == "men")
+ #expect("woman".pluralize() == "women")
+ #expect("ox".pluralize() == "oxen")
+
+ // Words ending in 'f' or 'fe' -> 'ves'
+ #expect("knife".pluralize() == "knives")
+ #expect("wolf".pluralize() == "wolves")
+ #expect("leaf".pluralize() == "leaves")
+ #expect("thief".pluralize() == "thieves")
+
+ // Latin/Greek endings
+ #expect("cactus".pluralize() == "cacti")
+ #expect("nucleus".pluralize() == "nuclei")
+ #expect("fungus".pluralize() == "fungi")
+ #expect("criterion".pluralize() == "criteria")
+ #expect("phenomenon".pluralize() == "phenomena")
+
+ // Singularization tests
+ #expect("cats".singularize() == "cat")
+ #expect("dogs".singularize() == "dog")
+ #expect("cities".singularize() == "city")
+ #expect("boxes".singularize() == "box")
+ #expect("wishes".singularize() == "wish")
+ #expect("watches".singularize() == "watch")
+ #expect("analyses".singularize() == "analysis")
+ #expect("diagnoses".singularize() == "diagnosis")
+
+ // Count parameter tests
+ #expect("cat".pluralize(count: 1) == "cat")
+ #expect("cat".pluralize(count: 2) == "cats")
+ #expect("cat".pluralize(count: 0) == "cats")
+
+ // Custom plural with 'with' parameter
+ #expect("person".pluralize(count: 2, with: "persons") == "persons")
+ }
+
+ // MARK: - testHonorsUncountableAndUnchangingLists
+
+ /// Verifies that uncountable and unchanging words remain unchanged
+ /// when pluralized or singularized.
+ @Test func testHonorsUncountableAndUnchangingLists() async throws {
+ // Uncountable words should not change
+ #expect("information".pluralize() == "information")
+ #expect("equipment".pluralize() == "equipment")
+ #expect("news".pluralize() == "news")
+ #expect("money".pluralize() == "money")
+ #expect("rice".pluralize() == "rice")
+ #expect("music".pluralize() == "music")
+ #expect("furniture".pluralize() == "furniture")
+ #expect("luggage".pluralize() == "luggage")
+ #expect("advice".pluralize() == "advice")
+ #expect("traffic".pluralize() == "traffic")
+ #expect("software".pluralize() == "software")
+ #expect("research".pluralize() == "research")
+ #expect("knowledge".pluralize() == "knowledge")
+ #expect("education".pluralize() == "education")
+
+ // Unchanging words (same in singular and plural)
+ #expect("sheep".pluralize() == "sheep")
+ #expect("deer".pluralize() == "deer")
+ #expect("moose".pluralize() == "moose")
+ #expect("swine".pluralize() == "swine")
+ #expect("bison".pluralize() == "bison")
+ #expect("corps".pluralize() == "corps")
+ #expect("means".pluralize() == "means")
+ #expect("series".pluralize() == "series")
+ #expect("scissors".pluralize() == "scissors")
+ #expect("species".pluralize() == "species")
+
+ // Uncountable singularization should also return unchanged
+ #expect("information".singularize() == "information")
+ #expect("sheep".singularize() == "sheep")
+
+ // Empty string should return unchanged
+ #expect("".pluralize() == "")
+ #expect("".singularize() == "")
+ }
+
+ // MARK: - testAddsRuntimeRules
+
+ /// Verifies that dynamically added plural and singular rules
+ /// are applied ahead of default rules.
+ @Test func testAddsRuntimeRules() async throws {
+ // Add a custom plural rule for a made-up pattern
+ // Note: Rules are added to the front of the list, so they take precedence
+ // The pattern uses a capturing group for the prefix
+ Pluralize.rule(rule: "(proto)zoon$", with: "$1zoa")
+ #expect("protozoon".pluralize() == "protozoa")
+
+ // Add a custom singular rule
+ Pluralize.singularRule(rule: "(proto)zoa$", with: "$1zoon")
+ #expect("protozoa".singularize() == "protozoon")
+
+ // Add a custom uncountable word
+ Pluralize.uncountable(word: "customword")
+ #expect("customword".pluralize() == "customword")
+
+ // Add a custom unchanging word
+ Pluralize.unchanging(word: "unchangingtest")
+ #expect("unchangingtest".pluralize() == "unchangingtest")
+ }
+}
|