Compare commits
18 Commits
fc3672b17b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2c146a28e | ||
|
|
0b479e10c8 | ||
|
|
0fbdec570a | ||
|
|
e44bb99479 | ||
|
|
dcaecbf185 | ||
|
|
e6f81fa177 | ||
|
|
b3af094eed | ||
|
|
49a58a8977 | ||
|
|
af0bb51f01 | ||
|
|
5e418211bf | ||
|
|
92dc0ffa2d | ||
|
|
b48655e40a | ||
|
|
7a453d0d79 | ||
|
|
2e5970dc97 | ||
|
|
6df720fb8c | ||
|
|
92263bc4ec | ||
|
|
157b577397 | ||
|
|
c47ce57c31 |
53
.github/workflows/android.yaml
vendored
@@ -5,10 +5,9 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_DIR: donext/build/outputs/bundle/release
|
MODULE: donextv2
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -16,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
@@ -24,10 +23,6 @@ jobs:
|
|||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
- name: Replace tokens in build.gradle
|
|
||||||
run: |
|
|
||||||
sed -i 's/#{\([^}]*\)}/${\1}/g' donext/build.gradle
|
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
@@ -38,34 +33,26 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
- name: Build AAB
|
- name: Decode signing key
|
||||||
run: ./gradlew bundleRelease
|
run: echo "${{ secrets.SIGNING_KEY_BASE64 }}" | base64 --decode > ${{ env.MODULE }}/upload.jks
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Set up Ruby
|
||||||
uses: actions/upload-artifact@v4
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
name: release-aab
|
ruby-version: 3.4
|
||||||
path: ${{ env.BUILD_DIR }}/*release*.aab
|
|
||||||
|
|
||||||
deploy-google-play:
|
- name: Install Fastlane
|
||||||
needs: build
|
run: bundle install
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
- name: Configure Google credentials
|
||||||
- name: Download artifact
|
run: echo '${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}' > service-account.json
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-aab
|
|
||||||
path: release
|
|
||||||
|
|
||||||
- name: Deploy to Google Play (production)
|
- name: Run Fastlane
|
||||||
uses: r0adkll/upload-google-play@v1
|
env:
|
||||||
with:
|
MODULE: ${{ env.MODULE }}
|
||||||
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
SUPPLY_JSON_KEY: service-account.json
|
||||||
packageName: com.wismna.geoffroy.donext
|
KEYSTORE_FILE: upload.jks
|
||||||
releaseFiles: release/app-release.aab
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
track: beta
|
ALIAS_NAME: ${{ secrets.ALIAS_NAME }}
|
||||||
metadataRootDirectory: fastlane/metadata/android
|
ALIAS_PASSWORD: ${{ secrets.ALIAS_PASSWORD }}
|
||||||
whatsNewDirectory: fastlane/metadata/android/en-US/changelogs
|
run: bundle exec fastlane internal
|
||||||
status: completed
|
|
||||||
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
*.iml
|
*.iml
|
||||||
*.apk
|
*.apk
|
||||||
|
*.jks
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
@@ -7,3 +8,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
|
/fastlane/keys/
|
||||||
|
/donext*/release/
|
||||||
|
|||||||
52
.idea/androidTestResultsUserPreferences.xml
generated
@@ -29,6 +29,32 @@
|
|||||||
</AndroidTestResultsTableState>
|
</AndroidTestResultsTableState>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry key="-885367779">
|
||||||
|
<value>
|
||||||
|
<AndroidTestResultsTableState>
|
||||||
|
<option name="preferredColumnWidths">
|
||||||
|
<map>
|
||||||
|
<entry key="Duration" value="90" />
|
||||||
|
<entry key="Medium_Phone_API_36.0" value="120" />
|
||||||
|
<entry key="Tests" value="360" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</AndroidTestResultsTableState>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="676896396">
|
||||||
|
<value>
|
||||||
|
<AndroidTestResultsTableState>
|
||||||
|
<option name="preferredColumnWidths">
|
||||||
|
<map>
|
||||||
|
<entry key="Duration" value="90" />
|
||||||
|
<entry key="Medium_Phone_API_36.0" value="120" />
|
||||||
|
<entry key="Tests" value="360" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</AndroidTestResultsTableState>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
<entry key="1337588336">
|
<entry key="1337588336">
|
||||||
<value>
|
<value>
|
||||||
<AndroidTestResultsTableState>
|
<AndroidTestResultsTableState>
|
||||||
@@ -42,6 +68,32 @@
|
|||||||
</AndroidTestResultsTableState>
|
</AndroidTestResultsTableState>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry key="1756619873">
|
||||||
|
<value>
|
||||||
|
<AndroidTestResultsTableState>
|
||||||
|
<option name="preferredColumnWidths">
|
||||||
|
<map>
|
||||||
|
<entry key="Duration" value="90" />
|
||||||
|
<entry key="Medium_Phone_API_36.0" value="120" />
|
||||||
|
<entry key="Tests" value="360" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</AndroidTestResultsTableState>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="2129352037">
|
||||||
|
<value>
|
||||||
|
<AndroidTestResultsTableState>
|
||||||
|
<option name="preferredColumnWidths">
|
||||||
|
<map>
|
||||||
|
<entry key="Duration" value="90" />
|
||||||
|
<entry key="Medium_Phone_API_36.0" value="120" />
|
||||||
|
<entry key="Tests" value="360" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</AndroidTestResultsTableState>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
24
.idea/caches/deviceStreaming.xml
generated
@@ -100,6 +100,18 @@
|
|||||||
<option name="screenX" value="1200" />
|
<option name="screenX" value="1200" />
|
||||||
<option name="screenY" value="1920" />
|
<option name="screenY" value="1920" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="a06" />
|
||||||
|
<option name="id" value="a06" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy A06" />
|
||||||
|
<option name="screenDensity" value="300" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
@@ -184,6 +196,18 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2340" />
|
<option name="screenY" value="2340" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="a56x" />
|
||||||
|
<option name="id" value="a56x" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-A566E" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
|||||||
6
.idea/deploymentTargetSelector.xml
generated
@@ -8,12 +8,6 @@
|
|||||||
<SelectionState runConfigName="donext">
|
<SelectionState runConfigName="donext">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
<SelectionState runConfigName="DatabaseMigrationTest">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="migrate_v6_to_v7_preserves_data_and_transforms_columns()">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
1
Gemfile
@@ -1,3 +1,4 @@
|
|||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
|
|
||||||
gem "fastlane"
|
gem "fastlane"
|
||||||
|
gem 'abbrev'
|
||||||
230
Gemfile.lock
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
CFPropertyList (3.0.7)
|
||||||
|
base64
|
||||||
|
nkf
|
||||||
|
rexml
|
||||||
|
abbrev (0.1.2)
|
||||||
|
addressable (2.8.7)
|
||||||
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
|
artifactory (3.0.17)
|
||||||
|
atomos (0.1.3)
|
||||||
|
aws-eventstream (1.4.0)
|
||||||
|
aws-partitions (1.1181.0)
|
||||||
|
aws-sdk-core (3.236.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
|
jmespath (~> 1, >= 1.6.1)
|
||||||
|
logger
|
||||||
|
aws-sdk-kms (1.117.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.234.0)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sdk-s3 (1.203.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.234.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sigv4 (1.12.1)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
babosa (1.0.4)
|
||||||
|
base64 (0.3.0)
|
||||||
|
bigdecimal (3.3.1)
|
||||||
|
claide (1.1.0)
|
||||||
|
colored (1.2)
|
||||||
|
colored2 (3.1.2)
|
||||||
|
commander (4.6.0)
|
||||||
|
highline (~> 2.0.0)
|
||||||
|
declarative (0.0.20)
|
||||||
|
digest-crc (0.7.0)
|
||||||
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
|
domain_name (0.6.20240107)
|
||||||
|
dotenv (2.8.1)
|
||||||
|
emoji_regex (3.2.3)
|
||||||
|
excon (0.112.0)
|
||||||
|
faraday (1.10.4)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.0)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
|
faraday-retry (~> 1.0)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-cookie_jar (0.0.7)
|
||||||
|
faraday (>= 0.8.0)
|
||||||
|
http-cookie (~> 1.0.0)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.1)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.1.1)
|
||||||
|
multipart-post (~> 2.0)
|
||||||
|
faraday-net_http (1.0.2)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.3)
|
||||||
|
faraday_middleware (1.2.1)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
fastimage (2.4.0)
|
||||||
|
fastlane (2.228.0)
|
||||||
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
addressable (>= 2.8, < 3.0.0)
|
||||||
|
artifactory (~> 3.0)
|
||||||
|
aws-sdk-s3 (~> 1.0)
|
||||||
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
|
colored (~> 1.2)
|
||||||
|
commander (~> 4.6)
|
||||||
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
|
faraday_middleware (~> 1.0)
|
||||||
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
fastlane-sirp (>= 1.0.0)
|
||||||
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||||
|
google-cloud-storage (~> 1.31)
|
||||||
|
highline (~> 2.0)
|
||||||
|
http-cookie (~> 1.0.5)
|
||||||
|
json (< 3.0.0)
|
||||||
|
jwt (>= 2.1.0, < 3)
|
||||||
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
|
naturally (~> 2.2)
|
||||||
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
|
security (= 0.1.5)
|
||||||
|
simctl (~> 1.6.3)
|
||||||
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
|
terminal-table (~> 3)
|
||||||
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
|
word_wrap (~> 1.0.0)
|
||||||
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
|
xcpretty (~> 0.4.1)
|
||||||
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
|
fastlane-sirp (1.0.0)
|
||||||
|
sysrandom (~> 1.0)
|
||||||
|
gh_inspector (1.1.3)
|
||||||
|
google-apis-androidpublisher_v3 (0.54.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-core (0.11.3)
|
||||||
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
representable (~> 3.0)
|
||||||
|
retriable (>= 2.0, < 4.a)
|
||||||
|
rexml
|
||||||
|
google-apis-iamcredentials_v1 (0.17.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-playcustomapp_v1 (0.13.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.31.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-cloud-core (1.8.0)
|
||||||
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-cloud-env (1.6.0)
|
||||||
|
faraday (>= 0.17.3, < 3.0)
|
||||||
|
google-cloud-errors (1.5.0)
|
||||||
|
google-cloud-storage (1.47.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
digest-crc (~> 0.4)
|
||||||
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
|
google-apis-storage_v1 (~> 0.31.0)
|
||||||
|
google-cloud-core (~> 1.6)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
googleauth (1.8.1)
|
||||||
|
faraday (>= 0.17.3, < 3.a)
|
||||||
|
jwt (>= 1.4, < 3.0)
|
||||||
|
multi_json (~> 1.11)
|
||||||
|
os (>= 0.9, < 2.0)
|
||||||
|
signet (>= 0.16, < 2.a)
|
||||||
|
highline (2.0.3)
|
||||||
|
http-cookie (1.0.8)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
httpclient (2.9.0)
|
||||||
|
mutex_m
|
||||||
|
jmespath (1.6.2)
|
||||||
|
json (2.16.0)
|
||||||
|
jwt (2.10.2)
|
||||||
|
base64
|
||||||
|
logger (1.7.0)
|
||||||
|
mini_magick (4.13.2)
|
||||||
|
mini_mime (1.1.5)
|
||||||
|
multi_json (1.17.0)
|
||||||
|
multipart-post (2.4.1)
|
||||||
|
mutex_m (0.3.0)
|
||||||
|
nanaimo (0.4.0)
|
||||||
|
naturally (2.3.0)
|
||||||
|
nkf (0.2.0)
|
||||||
|
optparse (0.8.0)
|
||||||
|
os (1.1.4)
|
||||||
|
plist (3.7.2)
|
||||||
|
public_suffix (6.0.2)
|
||||||
|
rake (13.3.1)
|
||||||
|
representable (3.2.0)
|
||||||
|
declarative (< 0.1.0)
|
||||||
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
|
uber (< 0.2.0)
|
||||||
|
retriable (3.1.2)
|
||||||
|
rexml (3.4.4)
|
||||||
|
rouge (3.28.0)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyzip (2.4.1)
|
||||||
|
security (0.1.5)
|
||||||
|
signet (0.21.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
faraday (>= 0.17.5, < 3.a)
|
||||||
|
jwt (>= 1.5, < 4.0)
|
||||||
|
multi_json (~> 1.10)
|
||||||
|
simctl (1.6.10)
|
||||||
|
CFPropertyList
|
||||||
|
naturally
|
||||||
|
sysrandom (1.0.5)
|
||||||
|
terminal-notifier (2.0.0)
|
||||||
|
terminal-table (3.0.2)
|
||||||
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
|
trailblazer-option (0.1.2)
|
||||||
|
tty-cursor (0.7.1)
|
||||||
|
tty-screen (0.8.2)
|
||||||
|
tty-spinner (0.9.3)
|
||||||
|
tty-cursor (~> 0.7)
|
||||||
|
uber (0.1.0)
|
||||||
|
unicode-display_width (2.6.0)
|
||||||
|
word_wrap (1.0.0)
|
||||||
|
xcodeproj (1.27.0)
|
||||||
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
|
atomos (~> 0.1.3)
|
||||||
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
colored2 (~> 3.1)
|
||||||
|
nanaimo (~> 0.4.0)
|
||||||
|
rexml (>= 3.3.6, < 4.0)
|
||||||
|
xcpretty (0.4.1)
|
||||||
|
rouge (~> 3.28.0)
|
||||||
|
xcpretty-travis-formatter (1.0.1)
|
||||||
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
x64-mingw-ucrt
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
abbrev
|
||||||
|
fastlane
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.7.2
|
||||||
@@ -24,7 +24,7 @@ buildscript {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
|
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
|
||||||
id("com.google.dagger.hilt.android") version "2.57.1" apply false
|
id("com.google.dagger.hilt.android") version "2.57.2" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ dependencies {
|
|||||||
implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||||
|
|
||||||
// Lifecycle components
|
// Lifecycle components
|
||||||
def lifecycleVersion = '2.9.3'
|
def lifecycleVersion = '2.9.4'
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
@@ -18,12 +20,21 @@ android {
|
|||||||
applicationId = "com.wismna.geoffroy.donext"
|
applicationId = "com.wismna.geoffroy.donext"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 34
|
versionCode = 35
|
||||||
versionName = "2.0"
|
versionName = "2.0.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(System.getenv("KEYSTORE_FILE") ?: "./upload.jks")
|
||||||
|
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||||
|
keyAlias = System.getenv("ALIAS_NAME")
|
||||||
|
keyPassword = System.getenv("ALIAS_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas"))
|
getByName("debug").assets.srcDirs(files("$projectDir/schemas"))
|
||||||
}
|
}
|
||||||
@@ -38,14 +49,17 @@ android {
|
|||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
|
signingConfig = signingConfigs["release"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlin {
|
||||||
jvmTarget = "11"
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_11
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
@@ -70,23 +84,24 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4")
|
||||||
implementation("androidx.activity:activity-compose:1.11.0")
|
implementation("androidx.activity:activity-compose:1.11.0")
|
||||||
implementation(platform("androidx.compose:compose-bom:2025.10.01"))
|
implementation(platform("androidx.compose:compose-bom:2025.11.00"))
|
||||||
implementation("androidx.compose.ui:ui")
|
implementation("androidx.compose.ui:ui")
|
||||||
implementation("androidx.compose.ui:ui-graphics")
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation("androidx.compose.material3:material3")
|
implementation("androidx.compose.material3:material3")
|
||||||
implementation("androidx.compose.material3:material3-window-size-class:1.4.0")
|
implementation("androidx.compose.material3:material3-window-size-class:1.4.0")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||||
implementation("androidx.navigation:navigation-compose:2.9.5")
|
implementation("androidx.navigation:navigation-compose:2.9.6")
|
||||||
implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
|
implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
|
||||||
implementation("sh.calvin.reorderable:reorderable:3.0.0")
|
implementation("sh.calvin.reorderable:reorderable:3.0.0")
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("io.mockk:mockk:1.13.12")
|
testImplementation("io.mockk:mockk:1.14.6")
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
|
||||||
|
testImplementation("com.google.truth:truth:1.4.5")
|
||||||
androidTestImplementation("androidx.test.ext:junit-ktx:1.3.0")
|
androidTestImplementation("androidx.test.ext:junit-ktx:1.3.0")
|
||||||
androidTestImplementation(platform("androidx.compose:compose-bom:2025.10.01"))
|
androidTestImplementation(platform("androidx.compose:compose-bom:2025.11.00"))
|
||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
androidTestImplementation("com.google.truth:truth:1.4.4")
|
androidTestImplementation("com.google.truth:truth:1.4.5")
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,11 @@ package com.wismna.geoffroy.donext.data.local.dao
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
||||||
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
||||||
import com.wismna.geoffroy.donext.data.local.AppDatabase
|
import com.wismna.geoffroy.donext.data.local.AppDatabase
|
||||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
import junit.framework.TestCase.assertEquals
|
|
||||||
import junit.framework.TestCase.assertFalse
|
|
||||||
import junit.framework.TestCase.assertNotNull
|
|
||||||
import junit.framework.TestCase.assertNull
|
|
||||||
import junit.framework.TestCase.assertTrue
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@@ -19,6 +15,7 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import kotlin.collections.first
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class TaskDaoTest {
|
class TaskDaoTest {
|
||||||
@@ -61,9 +58,9 @@ class TaskDaoTest {
|
|||||||
val inserted = taskDao.getTasksForList(listId).first().first()
|
val inserted = taskDao.getTasksForList(listId).first().first()
|
||||||
val fetched = taskDao.getTaskById(inserted.id)
|
val fetched = taskDao.getTaskById(inserted.id)
|
||||||
|
|
||||||
assertNotNull(fetched)
|
assertThat(fetched).isNotNull()
|
||||||
assertEquals("Do laundry", fetched!!.name)
|
assertThat(fetched!!.name).isEqualTo("Do laundry")
|
||||||
assertEquals(listId, fetched.taskListId)
|
assertThat(fetched.taskListId).isEqualTo(listId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -77,8 +74,8 @@ class TaskDaoTest {
|
|||||||
taskDao.insertTask(done)
|
taskDao.insertTask(done)
|
||||||
taskDao.insertTask(high)
|
taskDao.insertTask(high)
|
||||||
|
|
||||||
val tasks = taskDao.getTasksForList(listId).first()
|
val taskPriorities = taskDao.getTasksForList(listId).first().map { it.name }
|
||||||
assertEquals(listOf("High", "Normal", "Done"), tasks.map { it.name })
|
assertThat(taskPriorities).containsExactly("High", "Normal", "Done").inOrder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -92,7 +89,7 @@ class TaskDaoTest {
|
|||||||
taskDao.updateTask(updated)
|
taskDao.updateTask(updated)
|
||||||
|
|
||||||
val fetched = taskDao.getTaskById(inserted.id)
|
val fetched = taskDao.getTaskById(inserted.id)
|
||||||
assertEquals("Updated", fetched!!.name)
|
assertThat(fetched!!.name).isEqualTo("Updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -103,10 +100,10 @@ class TaskDaoTest {
|
|||||||
val inserted = taskDao.getTasksForList(listId).first().first()
|
val inserted = taskDao.getTasksForList(listId).first().first()
|
||||||
|
|
||||||
taskDao.toggleTaskDone(inserted.id, true)
|
taskDao.toggleTaskDone(inserted.id, true)
|
||||||
assertTrue(taskDao.getTaskById(inserted.id)!!.isDone)
|
assertThat(taskDao.getTaskById(inserted.id)!!.isDone).isTrue()
|
||||||
|
|
||||||
taskDao.toggleTaskDone(inserted.id, false)
|
taskDao.toggleTaskDone(inserted.id, false)
|
||||||
assertFalse(taskDao.getTaskById(inserted.id)!!.isDone)
|
assertThat(taskDao.getTaskById(inserted.id)!!.isDone).isFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -118,7 +115,7 @@ class TaskDaoTest {
|
|||||||
|
|
||||||
taskDao.toggleTaskDeleted(inserted.id, true)
|
taskDao.toggleTaskDeleted(inserted.id, true)
|
||||||
val deletedTask = taskDao.getTaskById(inserted.id)
|
val deletedTask = taskDao.getTaskById(inserted.id)
|
||||||
assertTrue(deletedTask!!.isDeleted)
|
assertThat(deletedTask!!.isDeleted).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -132,11 +129,10 @@ class TaskDaoTest {
|
|||||||
|
|
||||||
taskDao.toggleAllTasksFromListDeleted(listId, true)
|
taskDao.toggleAllTasksFromListDeleted(listId, true)
|
||||||
val fetched = taskDao.getTasksForList(listId).first()
|
val fetched = taskDao.getTasksForList(listId).first()
|
||||||
assertTrue(fetched.isEmpty()) // filtered by deleted = 0
|
assertThat(fetched).isEmpty()
|
||||||
|
|
||||||
// confirm soft deletion
|
// confirm soft deletion
|
||||||
val softDeleted = fetched.size < 2
|
assertThat(fetched).hasSize(0)
|
||||||
assertTrue(softDeleted)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -147,7 +143,7 @@ class TaskDaoTest {
|
|||||||
val inserted = taskDao.getTasksForList(listId).first().first()
|
val inserted = taskDao.getTasksForList(listId).first().first()
|
||||||
|
|
||||||
taskDao.permanentDeleteTask(inserted.id)
|
taskDao.permanentDeleteTask(inserted.id)
|
||||||
assertNull(taskDao.getTaskById(inserted.id))
|
assertThat(taskDao.getTaskById(inserted.id)).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -160,8 +156,8 @@ class TaskDaoTest {
|
|||||||
|
|
||||||
taskDao.permanentDeleteAllDeletedTasks()
|
taskDao.permanentDeleteAllDeletedTasks()
|
||||||
val remaining = taskDao.getTasksForList(listId).first()
|
val remaining = taskDao.getTasksForList(listId).first()
|
||||||
assertEquals(1, remaining.size)
|
assertThat(remaining).hasSize(1)
|
||||||
assertEquals("Active", remaining.first().name)
|
assertThat(remaining.first().name).isEqualTo("Active")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -171,9 +167,9 @@ class TaskDaoTest {
|
|||||||
taskDao.insertTask(deleted)
|
taskDao.insertTask(deleted)
|
||||||
|
|
||||||
val results = taskDao.getDeletedTasksWithListName().first()
|
val results = taskDao.getDeletedTasksWithListName().first()
|
||||||
assertEquals(1, results.size)
|
assertThat(results).hasSize(1)
|
||||||
assertEquals("Trash", results.first().task.name)
|
assertThat(results.first().task.name).isEqualTo("Trash")
|
||||||
assertEquals("Work", results.first().listName)
|
assertThat(results.first().listName).isEqualTo("Work")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -209,7 +205,7 @@ class TaskDaoTest {
|
|||||||
taskDao.insertTask(tomorrow)
|
taskDao.insertTask(tomorrow)
|
||||||
|
|
||||||
val tasks = taskDao.getDueTodayTasks(todayStart, todayEnd).first()
|
val tasks = taskDao.getDueTodayTasks(todayStart, todayEnd).first()
|
||||||
assertEquals(1, tasks.size)
|
assertThat(tasks).hasSize(1)
|
||||||
assertEquals("Today", tasks.first().name)
|
assertThat(tasks.first().name).isEqualTo("Today")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ package com.wismna.geoffroy.donext.data.local.dao
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
||||||
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
||||||
import com.wismna.geoffroy.donext.data.local.AppDatabase
|
import com.wismna.geoffroy.donext.data.local.AppDatabase
|
||||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
import junit.framework.TestCase.assertEquals
|
|
||||||
import junit.framework.TestCase.assertNotNull
|
|
||||||
import junit.framework.TestCase.assertTrue
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@@ -17,6 +15,7 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import kotlin.collections.first
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class TaskListDaoTest {
|
class TaskListDaoTest {
|
||||||
@@ -46,8 +45,8 @@ class TaskListDaoTest {
|
|||||||
listDao.insertTaskList(taskList)
|
listDao.insertTaskList(taskList)
|
||||||
|
|
||||||
val lists = listDao.getTaskLists().first()
|
val lists = listDao.getTaskLists().first()
|
||||||
assertEquals(1, lists.size)
|
assertThat(lists).hasSize(1)
|
||||||
assertEquals("Personal", lists.first().name)
|
assertThat(lists.first().name).isEqualTo("Personal")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -58,9 +57,9 @@ class TaskListDaoTest {
|
|||||||
val inserted = listDao.getTaskLists().first().first()
|
val inserted = listDao.getTaskLists().first().first()
|
||||||
val fetched = listDao.getTaskListById(inserted.id)
|
val fetched = listDao.getTaskListById(inserted.id)
|
||||||
|
|
||||||
assertNotNull(fetched)
|
assertThat(fetched).isNotNull()
|
||||||
assertEquals("Groceries", fetched!!.name)
|
assertThat(fetched!!.name).isEqualTo("Groceries")
|
||||||
assertEquals(inserted.id, fetched.id)
|
assertThat(fetched.id).isEqualTo(inserted.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -73,7 +72,7 @@ class TaskListDaoTest {
|
|||||||
listDao.updateTaskList(updated)
|
listDao.updateTaskList(updated)
|
||||||
|
|
||||||
val fetched = listDao.getTaskListById(inserted.id)
|
val fetched = listDao.getTaskListById(inserted.id)
|
||||||
assertEquals("Updated Work", fetched!!.name)
|
assertThat(fetched!!.name).isEqualTo("Updated Work")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -86,12 +85,12 @@ class TaskListDaoTest {
|
|||||||
|
|
||||||
// getTaskLists() filters deleted = 0, so result should be empty
|
// getTaskLists() filters deleted = 0, so result should be empty
|
||||||
val activeLists = listDao.getTaskLists().first()
|
val activeLists = listDao.getTaskLists().first()
|
||||||
assertTrue(activeLists.isEmpty())
|
assertThat(activeLists).isEmpty()
|
||||||
|
|
||||||
// But the entity still exists in DB
|
// But the entity still exists in DB
|
||||||
val softDeleted = listDao.getTaskListById(inserted.id)
|
val softDeleted = listDao.getTaskListById(inserted.id)
|
||||||
assertNotNull(softDeleted)
|
assertThat(softDeleted).isNotNull()
|
||||||
assertTrue(softDeleted!!.isDeleted)
|
assertThat(softDeleted!!.isDeleted).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -103,8 +102,8 @@ class TaskListDaoTest {
|
|||||||
listDao.insertTaskList(second)
|
listDao.insertTaskList(second)
|
||||||
listDao.insertTaskList(third)
|
listDao.insertTaskList(third)
|
||||||
|
|
||||||
val lists = listDao.getTaskLists().first()
|
val listNames = listDao.getTaskLists().first().map { it.name }
|
||||||
assertEquals(listOf("Alpha", "Beta", "Zeta"), lists.map { it.name })
|
assertThat(listNames).containsExactly("Alpha", "Beta", "Zeta").inOrder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -152,7 +151,7 @@ class TaskListDaoTest {
|
|||||||
|
|
||||||
val lists = listDao.getTaskListsWithOverdue(now)
|
val lists = listDao.getTaskListsWithOverdue(now)
|
||||||
|
|
||||||
assertEquals(1, lists.first().first().overdueCount)
|
assertThat(lists.first().first().overdueCount).isEqualTo(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -222,7 +221,7 @@ class TaskListDaoTest {
|
|||||||
|
|
||||||
val tasks = taskDao.getDueTodayTasks(todayStart, todayEnd)
|
val tasks = taskDao.getDueTodayTasks(todayStart, todayEnd)
|
||||||
|
|
||||||
assertEquals(1, tasks.first().count())
|
assertThat(tasks.first()).hasSize(1)
|
||||||
assertEquals("Today", tasks.first().first().name)
|
assertThat(tasks.first().first().name).isEqualTo("Today")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.wismna.geoffroy.donext.domain.extension
|
package com.wismna.geoffroy.donext.domain.extension
|
||||||
|
|
||||||
|
import java.time.Clock
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.ZoneId
|
|
||||||
|
|
||||||
fun Long.toLocalDate(): LocalDate =
|
fun Long.toLocalDate(clock: Clock = Clock.systemDefaultZone()): LocalDate =
|
||||||
Instant.ofEpochMilli(this)
|
Instant.ofEpochMilli(this)
|
||||||
.atZone(ZoneId.systemDefault())
|
.atZone(clock.zone)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ fun AppContent(
|
|||||||
is AppDestination.RecycleBin -> {
|
is AppDestination.RecycleBin -> {
|
||||||
EmptyRecycleBinAction()
|
EmptyRecycleBinAction()
|
||||||
}
|
}
|
||||||
else -> null
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -297,7 +297,7 @@ fun AppContent(
|
|||||||
text = { Text(stringResource(R.string.action_create_list)) },
|
text = { Text(stringResource(R.string.action_create_list)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Checklist
|
import androidx.compose.material.icons.filled.Checklist
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
@@ -43,7 +45,8 @@ fun MenuScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CalendarMonth
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
import androidx.compose.material.icons.filled.Clear
|
import androidx.compose.material.icons.filled.Clear
|
||||||
@@ -61,7 +63,7 @@ fun TaskScreen(
|
|||||||
titleFocusRequester.requestFocus()
|
titleFocusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.padding(16.dp)) {
|
Column(Modifier.padding(16.dp).verticalScroll(rememberScrollState())) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
if (viewModel.isDeleted) R.string.task_title_deleted
|
if (viewModel.isDeleted) R.string.task_title_deleted
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ package com.wismna.geoffroy.donext.presentation.viewmodel
|
|||||||
import com.wismna.geoffroy.donext.domain.extension.toLocalDate
|
import com.wismna.geoffroy.donext.domain.extension.toLocalDate
|
||||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
import com.wismna.geoffroy.donext.domain.model.Task
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import java.time.Clock
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.format.FormatStyle
|
import java.time.format.FormatStyle
|
||||||
import java.time.format.TextStyle
|
import java.time.format.TextStyle
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class TaskItemViewModel(task: Task) {
|
class TaskItemViewModel(
|
||||||
|
task: Task,
|
||||||
|
private val clock: Clock = Clock.systemDefaultZone()) {
|
||||||
val id: Long = task.id!!
|
val id: Long = task.id!!
|
||||||
val name: String = task.name
|
val name: String = task.name
|
||||||
val description: String? = task.description
|
val description: String? = task.description
|
||||||
@@ -18,17 +20,17 @@ class TaskItemViewModel(task: Task) {
|
|||||||
val isDeleted: Boolean = task.isDeleted
|
val isDeleted: Boolean = task.isDeleted
|
||||||
val priority: Priority = task.priority
|
val priority: Priority = task.priority
|
||||||
|
|
||||||
val today: LocalDate = LocalDate.now(ZoneId.systemDefault())
|
val today: LocalDate = LocalDate.now(clock)
|
||||||
|
|
||||||
val isOverdue: Boolean = task.dueDate?.let { millis ->
|
val isOverdue: Boolean = task.dueDate?.let { millis ->
|
||||||
val dueDate = millis.toLocalDate()
|
val dueDate = millis.toLocalDate(clock)
|
||||||
dueDate.isBefore(today)
|
dueDate.isBefore(today)
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
||||||
val dueDateText: String? = task.dueDate?.let { formatDueDate(it) }
|
val dueDateText: String? = task.dueDate?.let { formatDueDate(it) }
|
||||||
|
|
||||||
private fun formatDueDate(dueMillis: Long): String {
|
private fun formatDueDate(dueMillis: Long): String {
|
||||||
val dueDate = dueMillis.toLocalDate()
|
val dueDate = dueMillis.toLocalDate(clock)
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
dueDate.isEqual(today) -> "Today"
|
dueDate.isEqual(today) -> "Today"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.wismna.geoffroy.donext.presentation.viewmodel
|
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.wismna.geoffroy.donext.R
|
import com.wismna.geoffroy.donext.R
|
||||||
import com.wismna.geoffroy.donext.domain.model.Priority
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
import com.wismna.geoffroy.donext.domain.model.Task
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
@@ -9,7 +10,6 @@ import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase
|
|||||||
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||||
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import junit.framework.TestCase.assertEquals
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@@ -60,7 +60,7 @@ class DueTodayViewModelTest {
|
|||||||
tasksFlow.emit(taskList)
|
tasksFlow.emit(taskList)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
assertEquals(taskList, viewModel.dueTodayTasks)
|
assertThat(viewModel.dueTodayTasks).isEqualTo(taskList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||||
|
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.AppDestination
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
||||||
|
import io.mockk.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.test.*
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class MainViewModelTest {
|
||||||
|
|
||||||
|
private val dispatcher = StandardTestDispatcher()
|
||||||
|
private val uiEventBus: UiEventBus = mockk(relaxUnitFun = true)
|
||||||
|
private lateinit var getTaskListsFlow: MutableSharedFlow<List<TaskList>>
|
||||||
|
private lateinit var getTaskListsUseCase: GetTaskListsUseCase
|
||||||
|
private lateinit var viewModel: MainViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
Dispatchers.setMain(dispatcher)
|
||||||
|
getTaskListsFlow = MutableSharedFlow()
|
||||||
|
getTaskListsUseCase = mockk {
|
||||||
|
every { this@mockk.invoke() } returns getTaskListsFlow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initially isLoading is true and destinations are empty`() = runTest {
|
||||||
|
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
||||||
|
assertThat(viewModel.isLoading).isTrue()
|
||||||
|
assertThat(viewModel.destinations).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when task lists are emitted they populate destinations and isLoading becomes false`() = runTest {
|
||||||
|
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val lists = listOf(
|
||||||
|
TaskList(id = 1L, name = "Work", isDeleted = false, order = 0),
|
||||||
|
TaskList(id = 2L, name = "Personal", isDeleted = false, order = 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val expectedTaskDestinations = lists.map {
|
||||||
|
AppDestination.TaskList(it.id!!, it.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(viewModel.destinations).containsAtLeastElementsIn(expectedTaskDestinations)
|
||||||
|
assertThat(viewModel.destinations).containsAtLeast(
|
||||||
|
AppDestination.ManageLists,
|
||||||
|
AppDestination.RecycleBin,
|
||||||
|
AppDestination.DueTodayList
|
||||||
|
)
|
||||||
|
assertThat(viewModel.isLoading).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `navigateBack sends UiEvent_NavigateBack`() = runTest {
|
||||||
|
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
||||||
|
|
||||||
|
viewModel.navigateBack()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { uiEventBus.send(UiEvent.NavigateBack) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onNewTaskButtonClicked sets showTaskSheet true and sends CreateNewTask`() = runTest {
|
||||||
|
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
||||||
|
val taskListId = 42L
|
||||||
|
|
||||||
|
viewModel.onNewTaskButtonClicked(taskListId)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.showTaskSheet).isTrue()
|
||||||
|
coVerify { uiEventBus.send(UiEvent.CreateNewTask(taskListId)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onDismissTaskSheet sets showTaskSheet false and clears sticky`() = runTest {
|
||||||
|
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
||||||
|
|
||||||
|
viewModel.showTaskSheet = true
|
||||||
|
viewModel.onDismissTaskSheet()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.showTaskSheet).isFalse()
|
||||||
|
coVerify { uiEventBus.send(UiEvent.CloseTask) }
|
||||||
|
coVerify { uiEventBus.clearSticky() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `doesListExist returns true when taskListId is present`() = runTest {
|
||||||
|
val lists = listOf(TaskList(id = 1L, name = "Work", isDeleted = false, order = 0))
|
||||||
|
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
||||||
|
advanceUntilIdle()
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.doesListExist(1L)).isTrue()
|
||||||
|
assertThat(viewModel.doesListExist(99L)).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setCurrentDestination sets currentDestination based on navBackStackEntry`() = runTest {
|
||||||
|
val lists = listOf(TaskList(id = 1L, name = "Work", isDeleted = false, order = 0))
|
||||||
|
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
||||||
|
advanceUntilIdle()
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val entry = mockk<NavBackStackEntry> {
|
||||||
|
every { destination.route } returns AppDestination.TaskList(1L, "Work").route
|
||||||
|
every { arguments?.getLong("taskListId") } returns 1L
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.setCurrentDestination(entry)
|
||||||
|
assertThat(viewModel.currentDestination).isEqualTo(AppDestination.TaskList(1L, "Work"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.AddTaskListUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.DeleteTaskListUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskListUseCase
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
||||||
|
import com.wismna.geoffroy.donext.R
|
||||||
|
import io.mockk.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class ManageListsViewModelTest {
|
||||||
|
|
||||||
|
private lateinit var getTaskListsUseCase: GetTaskListsUseCase
|
||||||
|
private lateinit var addTaskListUseCase: AddTaskListUseCase
|
||||||
|
private lateinit var updateTaskListUseCase: UpdateTaskListUseCase
|
||||||
|
private lateinit var deleteTaskListUseCase: DeleteTaskListUseCase
|
||||||
|
private lateinit var uiEventBus: UiEventBus
|
||||||
|
|
||||||
|
private lateinit var getTaskListsFlow: MutableSharedFlow<List<TaskList>>
|
||||||
|
private lateinit var viewModel: ManageListsViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this, relaxed = true)
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher())
|
||||||
|
|
||||||
|
getTaskListsUseCase = mockk()
|
||||||
|
addTaskListUseCase = mockk(relaxed = true)
|
||||||
|
updateTaskListUseCase = mockk(relaxed = true)
|
||||||
|
deleteTaskListUseCase = mockk(relaxed = true)
|
||||||
|
uiEventBus = mockk(relaxed = true)
|
||||||
|
|
||||||
|
getTaskListsFlow = MutableSharedFlow()
|
||||||
|
every { getTaskListsUseCase() } returns getTaskListsFlow
|
||||||
|
|
||||||
|
viewModel = ManageListsViewModel(
|
||||||
|
getTaskListsUseCase,
|
||||||
|
addTaskListUseCase,
|
||||||
|
updateTaskListUseCase,
|
||||||
|
deleteTaskListUseCase,
|
||||||
|
uiEventBus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initially has empty task list`() = runTest {
|
||||||
|
assertThat(viewModel.taskLists).isEmpty()
|
||||||
|
assertThat(viewModel.taskCount).isEqualTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emitting lists updates taskLists and taskCount`() = runTest {
|
||||||
|
val lists = listOf(
|
||||||
|
TaskList(id = 1L, name = "Work", isDeleted = false, order = 0),
|
||||||
|
TaskList(id = 2L, name = "Home", isDeleted = false, order = 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.taskLists).isEqualTo(lists)
|
||||||
|
assertThat(viewModel.taskCount).isEqualTo(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `createTaskList calls use case`() = runTest {
|
||||||
|
val title = "Groceries"
|
||||||
|
val order = 1
|
||||||
|
|
||||||
|
viewModel.createTaskList(title, order)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { addTaskListUseCase(title, order) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updateTaskListName calls use case`() = runTest {
|
||||||
|
val taskList = TaskList(id = 1L, name = "Updated", isDeleted = false, order = 0)
|
||||||
|
|
||||||
|
viewModel.updateTaskListName(taskList)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { updateTaskListUseCase(1L, "Updated", 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleteTaskList calls use case and sends snackbar`() = runTest {
|
||||||
|
val taskListId = 10L
|
||||||
|
|
||||||
|
viewModel.deleteTaskList(taskListId)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { deleteTaskListUseCase(taskListId, true) }
|
||||||
|
coVerify {
|
||||||
|
uiEventBus.send(
|
||||||
|
match {
|
||||||
|
it is UiEvent.ShowUndoSnackbar &&
|
||||||
|
it.message == R.string.snackbar_message_task_list_recycle
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `moveTaskList reorders the task list correctly`() = runTest {
|
||||||
|
val lists = listOf(
|
||||||
|
TaskList(id = 1L, name = "A", isDeleted = false, order = 0),
|
||||||
|
TaskList(id = 2L, name = "B", isDeleted = false, order = 1),
|
||||||
|
TaskList(id = 3L, name = "C", isDeleted = false, order = 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.moveTaskList(fromIndex = 0, toIndex = 2)
|
||||||
|
assertThat(viewModel.taskLists.map { it.id }).isEqualTo(listOf(2L, 3L, 1L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `commitTaskListOrder updates only reordered lists`() = runTest {
|
||||||
|
val lists = listOf(
|
||||||
|
TaskList(id = 1L, name = "A", isDeleted = false, order = 0),
|
||||||
|
TaskList(id = 2L, name = "B", isDeleted = false, order = 1),
|
||||||
|
TaskList(id = 3L, name = "C", isDeleted = false, order = 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Simulate reordering
|
||||||
|
viewModel.moveTaskList(fromIndex = 2, toIndex = 0)
|
||||||
|
viewModel.commitTaskListOrder()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { updateTaskListUseCase(3L, "C", 0) }
|
||||||
|
coVerify { updateTaskListUseCase(1L, "A", 1) }
|
||||||
|
coVerify { updateTaskListUseCase(2L, "B", 2) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.GetDueTodayTasksUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsWithOverdueUseCase
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class MenuViewModelTest {
|
||||||
|
|
||||||
|
private lateinit var getTaskListsWithOverdueUseCase: GetTaskListsWithOverdueUseCase
|
||||||
|
private lateinit var getDueTodayTasksUseCase: GetDueTodayTasksUseCase
|
||||||
|
private lateinit var uiEventBus: UiEventBus
|
||||||
|
|
||||||
|
private lateinit var taskListsFlow: MutableSharedFlow<List<TaskListWithOverdue>>
|
||||||
|
private lateinit var dueTodayTasksFlow: MutableSharedFlow<List<Task>>
|
||||||
|
private lateinit var viewModel: MenuViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this, relaxed = true)
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher())
|
||||||
|
|
||||||
|
getTaskListsWithOverdueUseCase = mockk()
|
||||||
|
getDueTodayTasksUseCase = mockk()
|
||||||
|
uiEventBus = mockk(relaxed = true)
|
||||||
|
|
||||||
|
taskListsFlow = MutableSharedFlow()
|
||||||
|
dueTodayTasksFlow = MutableSharedFlow()
|
||||||
|
|
||||||
|
every { getTaskListsWithOverdueUseCase() } returns taskListsFlow
|
||||||
|
every { getDueTodayTasksUseCase() } returns dueTodayTasksFlow
|
||||||
|
|
||||||
|
viewModel = MenuViewModel(
|
||||||
|
getTaskListsWithOverdueUseCase,
|
||||||
|
getDueTodayTasksUseCase,
|
||||||
|
uiEventBus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TESTS ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initially has empty lists and zero due today`() = runTest {
|
||||||
|
assertThat(viewModel.taskLists).isEmpty()
|
||||||
|
assertThat(viewModel.dueTodayTasksCount).isEqualTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emitting task lists updates taskLists`() = runTest {
|
||||||
|
val lists = listOf(
|
||||||
|
TaskListWithOverdue(id = 1L, name = "Work", overdueCount = 2),
|
||||||
|
TaskListWithOverdue(id = 2L, name = "Home", overdueCount = 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
taskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.taskLists).isEqualTo(lists)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emitting due today tasks updates count`() = runTest {
|
||||||
|
val tasks = listOf(
|
||||||
|
Task(id = 1L, name = "Task A", taskListId = 1L, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false),
|
||||||
|
Task(id = 2L, name = "Task B", taskListId = 1L, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false)
|
||||||
|
)
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
dueTodayTasksFlow.emit(tasks)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.dueTodayTasksCount).isEqualTo(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `navigateTo sends UiEvent when route is different`() = runTest {
|
||||||
|
val route = "tasks"
|
||||||
|
val currentRoute = "home"
|
||||||
|
|
||||||
|
viewModel.navigateTo(route, currentRoute)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify {
|
||||||
|
uiEventBus.send(
|
||||||
|
match {
|
||||||
|
it is UiEvent.Navigate && it.route == route
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `navigateTo does nothing when route is the same`() = runTest {
|
||||||
|
val route = "tasks"
|
||||||
|
|
||||||
|
viewModel.navigateTo(route, route)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify(exactly = 0) { uiEventBus.send(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emitting both task lists and due today tasks updates both states`() = runTest {
|
||||||
|
val lists = listOf(
|
||||||
|
TaskListWithOverdue(id = 1L, name = "Work", overdueCount = 3),
|
||||||
|
TaskListWithOverdue(id = 2L, name = "Personal", overdueCount = 1)
|
||||||
|
)
|
||||||
|
val tasks = listOf(
|
||||||
|
Task(id = 10L, name = "Buy groceries", taskListId = 2L, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false),
|
||||||
|
Task(id = 11L, name = "Finish report", taskListId = 1L, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Let the ViewModel collectors start
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Emit from both flows (simulating data updates happening nearly simultaneously)
|
||||||
|
taskListsFlow.emit(lists)
|
||||||
|
dueTodayTasksFlow.emit(tasks)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Verify both internal states are updated independently and correctly
|
||||||
|
assertThat(viewModel.taskLists).isEqualTo(lists)
|
||||||
|
assertThat(viewModel.dueTodayTasksCount).isEqualTo(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.TaskWithListName
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.EmptyRecycleBinUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.GetDeletedTasksUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.PermanentlyDeleteTaskUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
||||||
|
import com.wismna.geoffroy.donext.R
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class RecycleBinViewModelTest {
|
||||||
|
|
||||||
|
private lateinit var getDeletedTasksUseCase: GetDeletedTasksUseCase
|
||||||
|
private lateinit var toggleTaskDeletedUseCase: ToggleTaskDeletedUseCase
|
||||||
|
private lateinit var permanentlyDeleteTaskUseCase: PermanentlyDeleteTaskUseCase
|
||||||
|
private lateinit var emptyRecycleBinUseCase: EmptyRecycleBinUseCase
|
||||||
|
private lateinit var uiEventBus: UiEventBus
|
||||||
|
private lateinit var savedStateHandle: SavedStateHandle
|
||||||
|
|
||||||
|
private lateinit var getDeletedTasksFlow: MutableSharedFlow<List<TaskWithListName>>
|
||||||
|
private lateinit var viewModel: RecycleBinViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this, relaxed = true)
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher())
|
||||||
|
|
||||||
|
getDeletedTasksUseCase = mockk()
|
||||||
|
toggleTaskDeletedUseCase = mockk(relaxed = true)
|
||||||
|
permanentlyDeleteTaskUseCase = mockk(relaxed = true)
|
||||||
|
emptyRecycleBinUseCase = mockk(relaxed = true)
|
||||||
|
uiEventBus = mockk(relaxed = true)
|
||||||
|
savedStateHandle = SavedStateHandle()
|
||||||
|
|
||||||
|
getDeletedTasksFlow = MutableSharedFlow()
|
||||||
|
every { getDeletedTasksUseCase() } returns getDeletedTasksFlow
|
||||||
|
|
||||||
|
viewModel = RecycleBinViewModel(
|
||||||
|
getDeletedTasksUseCase,
|
||||||
|
toggleTaskDeletedUseCase,
|
||||||
|
permanentlyDeleteTaskUseCase,
|
||||||
|
emptyRecycleBinUseCase,
|
||||||
|
uiEventBus,
|
||||||
|
savedStateHandle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TESTS ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state is empty`() = runTest {
|
||||||
|
assertThat(viewModel.deletedTasks).isEmpty()
|
||||||
|
assertThat(viewModel.taskToDeleteFlow.value).isNull()
|
||||||
|
assertThat(viewModel.emptyRecycleBinFlow.value).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emitting deleted tasks updates deletedTasks list`() = runTest {
|
||||||
|
val tasks = listOf(
|
||||||
|
TaskWithListName(Task(id = 1L, name = "Old task", taskListId = 0L, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false), listName = "Work"),
|
||||||
|
TaskWithListName(Task(id = 2L, name = "Done task", taskListId = 0L, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false), listName = "Home")
|
||||||
|
)
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
getDeletedTasksFlow.emit(tasks)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.deletedTasks).isEqualTo(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `restore toggles deletion and shows undo snackbar`() = runTest {
|
||||||
|
val taskId = 5L
|
||||||
|
|
||||||
|
viewModel.restore(taskId)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { toggleTaskDeletedUseCase(taskId, false) }
|
||||||
|
coVerify {
|
||||||
|
uiEventBus.send(
|
||||||
|
match {
|
||||||
|
it is UiEvent.ShowUndoSnackbar &&
|
||||||
|
it.message == R.string.snackbar_message_task_restore
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onTaskClicked sends EditTask UiEvent`() = runTest {
|
||||||
|
val task = Task(id = 1L, name = "T", taskListId = 1L, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false)
|
||||||
|
|
||||||
|
viewModel.onTaskClicked(task)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { uiEventBus.send(UiEvent.EditTask(task)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onEmptyRecycleBinRequest sets flag to true`() = runTest {
|
||||||
|
viewModel.onEmptyRecycleBinRequest()
|
||||||
|
assertThat(viewModel.emptyRecycleBinFlow.value).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onCancelEmptyRecycleBinRequest sets flag to false`() = runTest {
|
||||||
|
savedStateHandle["emptyRecycleBin"] = true
|
||||||
|
viewModel.onCancelEmptyRecycleBinRequest()
|
||||||
|
assertThat(viewModel.emptyRecycleBinFlow.value).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emptyRecycleBin calls use case and resets flag`() = runTest {
|
||||||
|
savedStateHandle["emptyRecycleBin"] = true
|
||||||
|
|
||||||
|
viewModel.emptyRecycleBin()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { emptyRecycleBinUseCase() }
|
||||||
|
assertThat(viewModel.emptyRecycleBinFlow.value).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onTaskDeleteRequest sets taskToDelete id`() = runTest {
|
||||||
|
viewModel.onTaskDeleteRequest(42L)
|
||||||
|
assertThat(viewModel.taskToDeleteFlow.value).isEqualTo(42L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onConfirmDelete calls use case and clears task id`() = runTest {
|
||||||
|
savedStateHandle["taskToDeleteId"] = 7L
|
||||||
|
|
||||||
|
viewModel.onConfirmDelete()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { permanentlyDeleteTaskUseCase(7L) }
|
||||||
|
assertThat(viewModel.taskToDeleteFlow.value).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onCancelDelete clears task id`() = runTest {
|
||||||
|
savedStateHandle["taskToDeleteId"] = 10L
|
||||||
|
|
||||||
|
viewModel.onCancelDelete()
|
||||||
|
assertThat(viewModel.taskToDeleteFlow.value).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `simultaneous flow emissions update deleted tasks and flags independently`() = runTest {
|
||||||
|
val tasks = listOf(TaskWithListName(Task(id = 1L, name = "Trash", taskListId = 0L, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false), listName = "Work"))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Emit tasks while also updating the recycle bin flag
|
||||||
|
getDeletedTasksFlow.emit(tasks)
|
||||||
|
savedStateHandle["emptyRecycleBin"] = true
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.deletedTasks).isEqualTo(tasks)
|
||||||
|
assertThat(viewModel.emptyRecycleBinFlow.value).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `restore snackbar undoAction re-deletes the task`() = runTest {
|
||||||
|
val taskId = 99L
|
||||||
|
val eventSlot = slot<UiEvent>()
|
||||||
|
|
||||||
|
// Intercept UiEvent.ShowUndoSnackbar to get the undoAction
|
||||||
|
coEvery { uiEventBus.send(capture(eventSlot)) } just Runs
|
||||||
|
|
||||||
|
viewModel.restore(taskId)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Ensure the event is a ShowUndoSnackbar
|
||||||
|
val snackbarEvent = eventSlot.captured as UiEvent.ShowUndoSnackbar
|
||||||
|
|
||||||
|
// Run the undo lambda
|
||||||
|
snackbarEvent.undoAction.invoke()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Verify that it re-deletes the task (sets deleted = true again)
|
||||||
|
coVerify { toggleTaskDeletedUseCase(taskId, true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.time.*
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
import java.time.format.TextStyle
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class TaskItemViewModelTest {
|
||||||
|
|
||||||
|
private val fixedClock: Clock = Clock.fixed(
|
||||||
|
LocalDate.of(2025, 1, 10)
|
||||||
|
.atStartOfDay(ZoneId.systemDefault())
|
||||||
|
.toInstant(),
|
||||||
|
ZoneId.systemDefault()
|
||||||
|
)
|
||||||
|
private val today: LocalDate = LocalDate.now(fixedClock)
|
||||||
|
|
||||||
|
private lateinit var baseTask: Task
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
baseTask = Task(
|
||||||
|
id = 1L,
|
||||||
|
taskListId = 1L,
|
||||||
|
name = "Test Task",
|
||||||
|
description = "Description",
|
||||||
|
priority = Priority.NORMAL,
|
||||||
|
isDone = false,
|
||||||
|
isDeleted = false,
|
||||||
|
dueDate = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun millisForDaysFromFixedToday(daysOffset: Long): Long {
|
||||||
|
val targetDate = today.plusDays(daysOffset)
|
||||||
|
return targetDate
|
||||||
|
.atStartOfDay(fixedClock.zone)
|
||||||
|
.toInstant()
|
||||||
|
.toEpochMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initializes fields from Task`() {
|
||||||
|
val viewModel = TaskItemViewModel(baseTask)
|
||||||
|
|
||||||
|
assertThat(viewModel.id).isEqualTo(baseTask.id)
|
||||||
|
assertThat(viewModel.name).isEqualTo(baseTask.name)
|
||||||
|
assertThat(viewModel.description).isEqualTo(baseTask.description)
|
||||||
|
assertThat(viewModel.isDone).isFalse()
|
||||||
|
assertThat(viewModel.isDeleted).isFalse()
|
||||||
|
assertThat(viewModel.priority).isEqualTo(Priority.NORMAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isOverdue is true when due date is before today`() {
|
||||||
|
val overdueTask = baseTask.copy(dueDate = millisForDaysFromFixedToday(-1))
|
||||||
|
val viewModel = TaskItemViewModel(overdueTask)
|
||||||
|
|
||||||
|
assertThat(viewModel.isOverdue).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isOverdue is false when due date is today`() {
|
||||||
|
val dueToday = baseTask.copy(dueDate = millisForDaysFromFixedToday(0))
|
||||||
|
val viewModel = TaskItemViewModel(dueToday, fixedClock)
|
||||||
|
|
||||||
|
assertThat(viewModel.isOverdue).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isOverdue is false when due date is null`() {
|
||||||
|
val viewModel = TaskItemViewModel(baseTask.copy(dueDate = null))
|
||||||
|
|
||||||
|
assertThat(viewModel.isOverdue).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dueDateText is Today when due date is today`() {
|
||||||
|
val dueToday = baseTask.copy(dueDate = millisForDaysFromFixedToday(0))
|
||||||
|
val viewModel = TaskItemViewModel(dueToday, fixedClock)
|
||||||
|
|
||||||
|
assertThat(viewModel.dueDateText).isEqualTo("Today")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dueDateText is Tomorrow when due date is tomorrow`() {
|
||||||
|
val dueTomorrow = baseTask.copy(dueDate = millisForDaysFromFixedToday(1))
|
||||||
|
val viewModel = TaskItemViewModel(dueTomorrow, fixedClock)
|
||||||
|
|
||||||
|
assertThat(viewModel.dueDateText).isEqualTo("Tomorrow")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dueDateText is Yesterday when due date was yesterday`() {
|
||||||
|
val dueYesterday = baseTask.copy(dueDate = millisForDaysFromFixedToday(-1))
|
||||||
|
val viewModel = TaskItemViewModel(dueYesterday, fixedClock)
|
||||||
|
|
||||||
|
assertThat(viewModel.dueDateText).isEqualTo("Yesterday")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dueDateText is day of week when within next 7 days`() {
|
||||||
|
val dueIn3Days = baseTask.copy(dueDate = millisForDaysFromFixedToday(3))
|
||||||
|
val viewModel = TaskItemViewModel(dueIn3Days, fixedClock)
|
||||||
|
|
||||||
|
val expected = today
|
||||||
|
.plusDays(3)
|
||||||
|
.dayOfWeek
|
||||||
|
.getDisplayName(TextStyle.SHORT, Locale.getDefault())
|
||||||
|
|
||||||
|
assertThat(viewModel.dueDateText).isEqualTo(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dueDateText is formatted date when more than 7 days away`() {
|
||||||
|
val dueIn10Days = baseTask.copy(dueDate = millisForDaysFromFixedToday(10))
|
||||||
|
val viewModel = TaskItemViewModel(dueIn10Days)
|
||||||
|
|
||||||
|
val expected = today
|
||||||
|
.plusDays(10)
|
||||||
|
.format(
|
||||||
|
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
|
||||||
|
.withLocale(Locale.getDefault())
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(viewModel.dueDateText).isEqualTo(expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDeletedUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
||||||
|
import com.wismna.geoffroy.donext.R
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class TaskListViewModelTest {
|
||||||
|
|
||||||
|
private lateinit var getTasksForListUseCase: GetTasksForListUseCase
|
||||||
|
private lateinit var toggleTaskDoneUseCase: ToggleTaskDoneUseCase
|
||||||
|
private lateinit var toggleTaskDeletedUseCase: ToggleTaskDeletedUseCase
|
||||||
|
private lateinit var uiEventBus: UiEventBus
|
||||||
|
private lateinit var savedStateHandle: SavedStateHandle
|
||||||
|
|
||||||
|
private lateinit var getTasksFlow: MutableSharedFlow<List<Task>>
|
||||||
|
private lateinit var viewModel: TaskListViewModel
|
||||||
|
|
||||||
|
private val testTaskListId = 100L
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this, relaxed = true)
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher())
|
||||||
|
|
||||||
|
getTasksForListUseCase = mockk()
|
||||||
|
toggleTaskDoneUseCase = mockk(relaxed = true)
|
||||||
|
toggleTaskDeletedUseCase = mockk(relaxed = true)
|
||||||
|
uiEventBus = mockk(relaxed = true)
|
||||||
|
savedStateHandle = SavedStateHandle(mapOf("taskListId" to testTaskListId))
|
||||||
|
|
||||||
|
getTasksFlow = MutableSharedFlow()
|
||||||
|
every { getTasksForListUseCase(testTaskListId) } returns getTasksFlow
|
||||||
|
|
||||||
|
viewModel = TaskListViewModel(
|
||||||
|
savedStateHandle,
|
||||||
|
getTasksForListUseCase,
|
||||||
|
toggleTaskDoneUseCase,
|
||||||
|
toggleTaskDeletedUseCase,
|
||||||
|
uiEventBus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TESTS ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state is loading and tasks empty`() = runTest {
|
||||||
|
assertThat(viewModel.isLoading).isTrue()
|
||||||
|
assertThat(viewModel.tasks).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emitting tasks updates list and stops loading`() = runTest {
|
||||||
|
val tasks = listOf(
|
||||||
|
Task(id = 1L, name = "Write docs", taskListId = testTaskListId, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false),
|
||||||
|
Task(id = 2L, name = "Code review", taskListId = testTaskListId, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false)
|
||||||
|
)
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
getTasksFlow.emit(tasks)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.isLoading).isFalse()
|
||||||
|
assertThat(viewModel.tasks).isEqualTo(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onTaskClicked sends EditTask event`() = runTest {
|
||||||
|
val task = Task(id = 1L, name = "Test task", taskListId = testTaskListId, description = "", priority = Priority.NORMAL, isDone = false, isDeleted = false)
|
||||||
|
|
||||||
|
viewModel.onTaskClicked(task)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { uiEventBus.send(UiEvent.EditTask(task)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updateTaskDone marks task done and sends snackbar with undo`() = runTest {
|
||||||
|
val taskId = 3L
|
||||||
|
|
||||||
|
viewModel.updateTaskDone(taskId, true)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { toggleTaskDoneUseCase(taskId, true) }
|
||||||
|
coVerify {
|
||||||
|
uiEventBus.send(
|
||||||
|
match {
|
||||||
|
it is UiEvent.ShowUndoSnackbar &&
|
||||||
|
it.message == R.string.snackbar_message_task_done
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updateTaskDone undoAction marks task undone`() = runTest {
|
||||||
|
val taskId = 7L
|
||||||
|
val eventSlot = slot<UiEvent>()
|
||||||
|
|
||||||
|
coEvery { uiEventBus.send(capture(eventSlot)) } just Runs
|
||||||
|
|
||||||
|
viewModel.updateTaskDone(taskId, true)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val snackbar = eventSlot.captured as UiEvent.ShowUndoSnackbar
|
||||||
|
snackbar.undoAction.invoke()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { toggleTaskDoneUseCase(taskId, false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleteTask marks task deleted and sends snackbar`() = runTest {
|
||||||
|
val taskId = 9L
|
||||||
|
|
||||||
|
viewModel.deleteTask(taskId)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { toggleTaskDeletedUseCase(taskId, true) }
|
||||||
|
coVerify {
|
||||||
|
uiEventBus.send(
|
||||||
|
match {
|
||||||
|
it is UiEvent.ShowUndoSnackbar &&
|
||||||
|
it.message == R.string.snackbar_message_task_recycle
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleteTask undoAction restores task`() = runTest {
|
||||||
|
val taskId = 10L
|
||||||
|
val eventSlot = slot<UiEvent>()
|
||||||
|
|
||||||
|
coEvery { uiEventBus.send(capture(eventSlot)) } just Runs
|
||||||
|
|
||||||
|
viewModel.deleteTask(taskId)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val snackbar = eventSlot.captured as UiEvent.ShowUndoSnackbar
|
||||||
|
snackbar.undoAction.invoke()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { toggleTaskDeletedUseCase(taskId, false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||||
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class TaskViewModelTest {
|
||||||
|
|
||||||
|
private lateinit var createTaskUseCase: AddTaskUseCase
|
||||||
|
private lateinit var updateTaskUseCase: UpdateTaskUseCase
|
||||||
|
private lateinit var uiEventBus: UiEventBus
|
||||||
|
private lateinit var stickyEventsFlow: MutableSharedFlow<UiEvent>
|
||||||
|
|
||||||
|
private lateinit var viewModel: TaskViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this, relaxed = true)
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher())
|
||||||
|
|
||||||
|
createTaskUseCase = mockk(relaxed = true)
|
||||||
|
updateTaskUseCase = mockk(relaxed = true)
|
||||||
|
uiEventBus = mockk(relaxed = true)
|
||||||
|
|
||||||
|
stickyEventsFlow = MutableSharedFlow()
|
||||||
|
every { uiEventBus.stickyEvents } returns stickyEventsFlow
|
||||||
|
|
||||||
|
viewModel = TaskViewModel(
|
||||||
|
createTaskUseCase,
|
||||||
|
updateTaskUseCase,
|
||||||
|
uiEventBus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TESTS ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state is blank and not editing`() = runTest {
|
||||||
|
assertThat(viewModel.title).isEmpty()
|
||||||
|
assertThat(viewModel.description).isEmpty()
|
||||||
|
assertThat(viewModel.priority).isEqualTo(Priority.NORMAL)
|
||||||
|
assertThat(viewModel.dueDate).isNull()
|
||||||
|
assertThat(viewModel.isDone).isFalse()
|
||||||
|
assertThat(viewModel.isDeleted).isFalse()
|
||||||
|
assertThat(viewModel.taskListId).isNull()
|
||||||
|
assertThat(viewModel.isEditing()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CreateNewTask event resets fields and sets taskListId`() = runTest {
|
||||||
|
stickyEventsFlow.emit(UiEvent.CreateNewTask(42L))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.isEditing()).isFalse()
|
||||||
|
assertThat(viewModel.taskListId).isEqualTo(42L)
|
||||||
|
assertThat(viewModel.title).isEmpty()
|
||||||
|
assertThat(viewModel.description).isEmpty()
|
||||||
|
assertThat(viewModel.priority).isEqualTo(Priority.NORMAL)
|
||||||
|
assertThat(viewModel.dueDate).isNull()
|
||||||
|
assertThat(viewModel.isDeleted).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `EditTask event populates fields from existing task`() = runTest {
|
||||||
|
val task = Task(
|
||||||
|
id = 7L,
|
||||||
|
taskListId = 9L,
|
||||||
|
name = "Fix bug",
|
||||||
|
description = "Null pointer issue",
|
||||||
|
priority = Priority.HIGH,
|
||||||
|
dueDate = Instant.parse("2025-10-01T12:00:00Z").toEpochMilli(),
|
||||||
|
isDone = true,
|
||||||
|
isDeleted = false
|
||||||
|
)
|
||||||
|
|
||||||
|
stickyEventsFlow.emit(UiEvent.EditTask(task))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.isEditing()).isTrue()
|
||||||
|
assertThat(viewModel.editingTaskId).isEqualTo(7L)
|
||||||
|
assertThat(viewModel.taskListId).isEqualTo(9L)
|
||||||
|
assertThat(viewModel.title).isEqualTo("Fix bug")
|
||||||
|
assertThat(viewModel.description).isEqualTo("Null pointer issue")
|
||||||
|
assertThat(viewModel.priority).isEqualTo(Priority.HIGH)
|
||||||
|
assertThat(viewModel.dueDate).isEqualTo(task.dueDate)
|
||||||
|
assertThat(viewModel.isDone).isTrue()
|
||||||
|
assertThat(viewModel.isDeleted).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CloseTask event resets state`() = runTest {
|
||||||
|
// set up as editing
|
||||||
|
stickyEventsFlow.emit(
|
||||||
|
UiEvent.EditTask(
|
||||||
|
Task(id = 1L, taskListId = 2L, name = "T", description = "D", priority = Priority.HIGH, isDone = false, isDeleted = false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
stickyEventsFlow.emit(UiEvent.CloseTask)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.title).isEmpty()
|
||||||
|
assertThat(viewModel.description).isEmpty()
|
||||||
|
assertThat(viewModel.priority).isEqualTo(Priority.NORMAL)
|
||||||
|
assertThat(viewModel.editingTaskId).isNull()
|
||||||
|
assertThat(viewModel.taskListId).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onTitleChanged updates title`() {
|
||||||
|
viewModel.onTitleChanged("New title")
|
||||||
|
assertThat(viewModel.title).isEqualTo("New title")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onDescriptionChanged updates description`() {
|
||||||
|
viewModel.onDescriptionChanged("Some description")
|
||||||
|
assertThat(viewModel.description).isEqualTo("Some description")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onPriorityChanged updates priority`() {
|
||||||
|
viewModel.onPriorityChanged(Priority.HIGH)
|
||||||
|
assertThat(viewModel.priority).isEqualTo(Priority.HIGH)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onDueDateChanged normalizes date to start of day in system timezone`() {
|
||||||
|
val utcMidday = Instant.parse("2025-10-01T12:00:00Z").toEpochMilli()
|
||||||
|
viewModel.onDueDateChanged(utcMidday)
|
||||||
|
|
||||||
|
val expectedStartOfDay =
|
||||||
|
Instant.ofEpochMilli(utcMidday)
|
||||||
|
.atZone(ZoneOffset.UTC)
|
||||||
|
.toLocalDate()
|
||||||
|
.atStartOfDay(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.toEpochMilli()
|
||||||
|
|
||||||
|
assertThat(viewModel.dueDate).isEqualTo(expectedStartOfDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `save with blank title does nothing`() = runTest {
|
||||||
|
stickyEventsFlow.emit(UiEvent.CreateNewTask(1L))
|
||||||
|
advanceUntilIdle()
|
||||||
|
viewModel.save()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify(exactly = 0) { createTaskUseCase(any(), any(), any(), any(), any()) }
|
||||||
|
coVerify(exactly = 0) { updateTaskUseCase(any(), any(), any(), any(), any(), any(), any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `save creates task when not editing`() = runTest {stickyEventsFlow.emit(UiEvent.CreateNewTask(3L))
|
||||||
|
advanceUntilIdle()
|
||||||
|
viewModel.onTitleChanged("New Task")
|
||||||
|
viewModel.onDescriptionChanged("Description")
|
||||||
|
viewModel.onPriorityChanged(Priority.HIGH)
|
||||||
|
val due = Instant.parse("2025-10-01T12:00:00Z").toEpochMilli()
|
||||||
|
viewModel.onDueDateChanged(due)
|
||||||
|
|
||||||
|
viewModel.save()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify {
|
||||||
|
createTaskUseCase(
|
||||||
|
3L,
|
||||||
|
"New Task",
|
||||||
|
"Description",
|
||||||
|
Priority.HIGH,
|
||||||
|
viewModel.dueDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `save updates task when editing`() = runTest {
|
||||||
|
val task = Task(
|
||||||
|
id = 10L,
|
||||||
|
taskListId = 5L,
|
||||||
|
name = "Old Task",
|
||||||
|
description = "Old desc",
|
||||||
|
priority = Priority.NORMAL,
|
||||||
|
dueDate = null,
|
||||||
|
isDone = false,
|
||||||
|
isDeleted = false
|
||||||
|
)
|
||||||
|
|
||||||
|
stickyEventsFlow.emit(UiEvent.EditTask(task))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.onTitleChanged("Updated Task")
|
||||||
|
viewModel.onDescriptionChanged("Updated desc")
|
||||||
|
|
||||||
|
viewModel.save()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify {
|
||||||
|
updateTaskUseCase(
|
||||||
|
10L,
|
||||||
|
5L,
|
||||||
|
"Updated Task",
|
||||||
|
"Updated desc",
|
||||||
|
Priority.NORMAL,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `save calls onDone callback after save completes`() = runTest {
|
||||||
|
var doneCalled = false
|
||||||
|
|
||||||
|
stickyEventsFlow.emit(UiEvent.CreateNewTask(2L))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.onTitleChanged("Task")
|
||||||
|
viewModel.save { doneCalled = true }
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(doneCalled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
json_key_file("../..//Downloads/donext-f9e67-1184ae400b09.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
|
json_key_file(ENV["SUPPLY_JSON_KEY"] || "./fastlane/keys/donext-f9e67-5038064982b0")
|
||||||
package_name("com.wismna.geoffroy.donext") # e.g. com.krausefx.app
|
package_name("com.wismna.geoffroy.donext")
|
||||||
|
|||||||
@@ -1,38 +1,30 @@
|
|||||||
# This file contains the fastlane.tools configuration
|
|
||||||
# You can find the documentation at https://docs.fastlane.tools
|
|
||||||
#
|
|
||||||
# For a list of all available actions, check out
|
|
||||||
#
|
|
||||||
# https://docs.fastlane.tools/actions
|
|
||||||
#
|
|
||||||
# For a list of all available plugins, check out
|
|
||||||
#
|
|
||||||
# https://docs.fastlane.tools/plugins/available-plugins
|
|
||||||
#
|
|
||||||
|
|
||||||
# Uncomment the line if you want fastlane to automatically update itself
|
|
||||||
# update_fastlane
|
|
||||||
|
|
||||||
default_platform(:android)
|
default_platform(:android)
|
||||||
|
|
||||||
platform :android do
|
platform :android do
|
||||||
desc "Runs all the tests"
|
desc "Build, test, and deploy to Google Play"
|
||||||
lane :test do
|
lane :internal do
|
||||||
gradle(task: "test")
|
module_name = ENV["MODULE"]
|
||||||
|
# Read local versionCode
|
||||||
|
project_root = File.expand_path("..", __dir__)
|
||||||
|
gradle_path = File.join(project_root, module_name, "build.gradle.kts")
|
||||||
|
UI.message("Gradle file resolved at: #{gradle_path}")
|
||||||
|
gradle_file = File.read(gradle_path)
|
||||||
|
gradle_version = gradle_file[/versionCode\s*=\s*(\d+)/, 1].to_i
|
||||||
|
|
||||||
|
# Read Play Store versionCode (track internal)
|
||||||
|
play_version = google_play_track_version_codes(
|
||||||
|
track: "internal"
|
||||||
|
).max.to_i
|
||||||
|
|
||||||
|
if gradle_version <= play_version
|
||||||
|
UI.user_error!("VersionCode #{gradle_version} should be higher than Play Store version (#{play_version}). Aborting upload.")
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Submit a new Beta Build to Crashlytics Beta"
|
gradle(task: "testDebugUnitTest")
|
||||||
lane :beta do
|
gradle(task: "clean :#{module_name}:bundleRelease")
|
||||||
gradle(task: "clean assembleRelease")
|
upload_to_play_store(
|
||||||
crashlytics
|
track: "internal",
|
||||||
|
aab: "#{module_name}/build/outputs/bundle/release/#{module_name}-release.aab"
|
||||||
# sh "your_script.sh"
|
)
|
||||||
# You can also use other beta testing services here
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "Deploy a new version to the Google Play"
|
|
||||||
lane :deploy do
|
|
||||||
gradle(task: "clean assembleRelease")
|
|
||||||
upload_to_play_store
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
40
fastlane/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
fastlane documentation
|
||||||
|
----
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Make sure you have the latest version of the Xcode command line tools installed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xcode-select --install
|
||||||
|
```
|
||||||
|
|
||||||
|
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||||
|
|
||||||
|
# Available Actions
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
### android deploy
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Build, test, and deploy the production version to Google Play
|
||||||
|
|
||||||
|
### android internal
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android internal
|
||||||
|
```
|
||||||
|
|
||||||
|
Build, test, and deploy to Google Play
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||||
|
|
||||||
|
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||||
|
|
||||||
|
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||||
@@ -1 +1,8 @@
|
|||||||
Update to SDK version 35
|
Complete UI overhaul that gives DoNext a nice, modern look.
|
||||||
|
Complete code rewrite to Kotlin and Android Jetpack, with a better architecture.
|
||||||
|
|
||||||
|
Task lists are now displayed in a navigation menu
|
||||||
|
Tasks are now ordered by priority and not cycles which were removed
|
||||||
|
All tasks can be Done or Removed instead of only the first one
|
||||||
|
Removed History page and split its features between the regular task lists and the new Recycle Bin
|
||||||
|
Today view is not a separate concept anymore and based on the tasks due date
|
||||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 121 KiB |
@@ -1 +1 @@
|
|||||||
A new way to manage your tasks!
|
A simple and fast app to manage your tasks!
|
||||||
@@ -1 +1 @@
|
|||||||
DoNext
|
DoNext v2
|
||||||
20
fastlane/report.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="fastlane.lanes">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.0023664">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="1: testDebugUnitTest" time="0.0434096">
|
||||||
|
|
||||||
|
<failure message="D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in 'Fastlane::Actions.execute_action' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:255:in 'block in Fastlane::Runner#execute_action' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:229:in 'Dir.chdir' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:229:in 'Fastlane::Runner#execute_action' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:157:in 'Fastlane::Runner#trigger_action_by_name' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/fast_file.rb:159:in 'Fastlane::FastFile#method_missing' Fastfile:16:in 'block (2 levels) in Fastlane::FastFile#parsing_binding' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/lane.rb:41:in 'Fastlane::Lane#call' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:49:in 'block in Fastlane::Runner#execute' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:45:in 'Dir.chdir' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:45:in 'Fastlane::Runner#execute' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/lane_manager.rb:46:in 'Fastlane::LaneManager.cruise_lane' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/command_line_handler.rb:34:in 'Fastlane::CommandLineHandler.handle' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:110:in 'block (2 levels) in Fastlane::CommandsGenerator#run' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/commander-4.6.0/lib/commander/command.rb:187:in 'Commander::Command#call' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/commander-4.6.0/lib/commander/command.rb:157:in 'Commander::Command#run' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/commander-4.6.0/lib/commander/runner.rb:444:in 'Commander::Runner#run_active_command' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in 'Commander::Runner#run!' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/commander-4.6.0/lib/commander/delegates.rb:18:in 'Commander::Delegates#run!' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:363:in 'Fastlane::CommandsGenerator#run' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:43:in 'Fastlane::CommandsGenerator.start' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in 'Fastlane::CLIToolsDistributor.take_off' D:/Ruby34-x64/lib/ruby/gems/3.4.0/gems/fastlane-2.228.0/bin/fastlane:23:in '<top (required)>' D:/Ruby34-x64/bin/fastlane:25:in 'Kernel#load' D:/Ruby34-x64/bin/fastlane:25:in '<main>' Exit status of command 'D:/source/DoNext/gradlew testDebugUnitTest -p .' was 1 instead of 0. ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation. " />
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||