Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Polish Terms of Use screen padding to match iOS #903

### Added
- Headlines home screen widget with v61 wide and compact layouts, including redesigned in-app preview and edit screens #919
- Home screen widgets foundation with Glance, including price widget as the first implementation #895

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ class HeadlineCardTest {

@Test
fun testHeadlineCardWithAllElements() {
// Arrange & Act
composeTestRule.setContent {
AppThemeSurface {
HeadlineCard(
showWidgetTitle = true,
showTime = true,
showSource = true,
time = testTime,
Expand All @@ -34,57 +32,21 @@ class HeadlineCardTest {
}
}

// Assert all elements exist
composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("widget_title_icon", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("widget_title_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_label", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertExists()

// Verify text content
composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertTextEquals(testTime)
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertTextEquals(testHeadline)
composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertTextEquals(testSource)
}

@Test
fun testHeadlineCardWithoutWidgetTitle() {
// Arrange & Act
composeTestRule.setContent {
AppThemeSurface {
HeadlineCard(
showWidgetTitle = false,
showTime = true,
showSource = true,
time = testTime,
headline = testHeadline,
source = testSource,
link = testLink
)
}
}

// Assert main elements exist
composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists()

// Assert widget title elements do not exist
composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertDoesNotExist()
composeTestRule.onNodeWithTag("widget_title_icon", useUnmergedTree = true).assertDoesNotExist()
composeTestRule.onNodeWithTag("widget_title_text", useUnmergedTree = true).assertDoesNotExist()
}

@Test
fun testHeadlineCardWithoutTime() {
// Arrange & Act
composeTestRule.setContent {
AppThemeSurface {
HeadlineCard(
showWidgetTitle = true,
showTime = false,
showSource = true,
time = testTime,
Expand All @@ -95,22 +57,18 @@ class HeadlineCardTest {
}
}

// Assert main elements exist
composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertExists()

// Assert time element does not exist
composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertDoesNotExist()
}

@Test
fun testHeadlineCardWithoutSource() {
// Arrange & Act
composeTestRule.setContent {
AppThemeSurface {
HeadlineCard(
showWidgetTitle = true,
showTime = true,
showSource = false,
time = testTime,
Expand All @@ -121,24 +79,17 @@ class HeadlineCardTest {
}
}

// Assert main elements exist
composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists()

// Assert source elements do not exist
composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertDoesNotExist()
composeTestRule.onNodeWithTag("source_label", useUnmergedTree = true).assertDoesNotExist()
composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertDoesNotExist()
}

@Test
fun testHeadlineCardMinimal() {
// Arrange & Act - Only headline shown
composeTestRule.setContent {
AppThemeSurface {
HeadlineCard(
showWidgetTitle = false,
showTime = false,
showSource = false,
time = testTime,
Expand All @@ -149,55 +100,45 @@ class HeadlineCardTest {
}
}

// Assert only essential elements exist
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists()

// Assert optional elements do not exist
composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertDoesNotExist()
composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertDoesNotExist()
composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertDoesNotExist()
composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertDoesNotExist()

// Verify headline text
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertTextEquals(testHeadline)
}

@Test
fun testHeadlineCardWithEmptyTime() {
// Arrange & Act - Time is empty string
composeTestRule.setContent {
AppThemeSurface {
HeadlineCard(
showWidgetTitle = true,
showTime = true,
showSource = true,
time = "", // Empty time
time = "",
headline = testHeadline,
source = testSource,
link = testLink
)
}
}

// Assert main elements exist
composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertExists()

// Assert time element does not exist when time is empty
composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertDoesNotExist()
}

@Test
fun testHeadlineCardWithLongHeadline() {
// Arrange
val longHeadline =
"This is a very long headline that should be truncated because it exceeds the maximum number of lines allowed in the headline card component and should show ellipsis"

// Act
composeTestRule.setContent {
AppThemeSurface {
HeadlineCard(
showWidgetTitle = true,
showTime = true,
showSource = true,
time = testTime,
Expand All @@ -208,35 +149,46 @@ class HeadlineCardTest {
}
}

// Assert headline exists and contains the text (may be truncated)
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists()
}

@Test
fun testAllElementsExistInFullConfiguration() {
// Arrange & Act
fun testHeadlineCardSmallWithTime() {
composeTestRule.setContent {
AppThemeSurface {
HeadlineCard(
showWidgetTitle = true,
HeadlineCardSmall(
showTime = true,
showSource = true,
time = testTime,
headline = testHeadline,
source = testSource,
link = testLink
)
}
}

// Assert all tagged elements exist
composeTestRule.onNodeWithTag("widget_title_row", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("widget_title_icon", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("widget_title_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_label", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("source_text", useUnmergedTree = true).assertExists()
composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertExists()

composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertTextEquals(testTime)
composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertTextEquals(testHeadline)
}

@Test
fun testHeadlineCardSmallWithoutTime() {
composeTestRule.setContent {
AppThemeSurface {
HeadlineCardSmall(
showTime = false,
time = testTime,
headline = testHeadline,
link = testLink
)
}
}

composeTestRule.onNodeWithTag("headline_text", useUnmergedTree = true).assertExists()

composeTestRule.onNodeWithTag("time_text", useUnmergedTree = true).assertDoesNotExist()
composeTestRule.onNodeWithTag("source_row", useUnmergedTree = true).assertDoesNotExist()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = { editClicked = true },
onClickDelete = { deleteClicked = true },
onClickSave = { saveClicked = true },
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand Down Expand Up @@ -97,7 +96,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = { editClicked = true },
onClickDelete = { deleteClicked = true },
onClickSave = { saveClicked = true },
showWidgetTitles = false,
isHeadlinesImplemented = false,
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand Down Expand Up @@ -134,7 +132,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = customPreferences,
article = mockArticle
Expand All @@ -158,7 +155,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand Down Expand Up @@ -194,7 +190,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand All @@ -219,7 +214,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = false,
isHeadlinesImplemented = false,
headlinePreferences = minimalPreferences,
article = mockArticle
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,19 @@
android:resource="@xml/appwidget_info_price" />
</receiver>

<!-- Headlines Widget -->
<receiver
android:name=".appwidget.ui.headlines.HeadlinesGlanceReceiver"
android:exported="true"
android:label="@string/widgets__news__name">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info_headlines" />
</receiver>

</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package to.bitkit.appwidget

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import to.bitkit.data.dto.ArticleDTO
import to.bitkit.data.dto.price.GraphPeriod
import to.bitkit.data.dto.price.PriceDTO
import to.bitkit.data.widgets.NewsService
import to.bitkit.data.widgets.PriceService
import to.bitkit.di.IoDispatcher
import javax.inject.Inject
Expand All @@ -13,9 +15,15 @@ import javax.inject.Singleton
class AppWidgetDataRepository @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val priceService: PriceService,
private val newsService: NewsService,
) {
suspend fun fetchPriceData(period: GraphPeriod = GraphPeriod.ONE_DAY): Result<PriceDTO> =
withContext(ioDispatcher) {
priceService.fetchData(period)
}

suspend fun fetchArticles(): Result<List<ArticleDTO>> =
withContext(ioDispatcher) {
newsService.fetchData()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.map
import to.bitkit.appwidget.model.AppWidgetData
import to.bitkit.appwidget.model.AppWidgetEntry
import to.bitkit.appwidget.model.AppWidgetType
import to.bitkit.data.dto.ArticleDTO
import to.bitkit.data.dto.price.GraphPeriod
import to.bitkit.data.dto.price.PriceDTO
import to.bitkit.data.serializers.AppWidgetDataSerializer
Expand Down Expand Up @@ -79,4 +80,12 @@ class AppWidgetPreferencesStore @Inject constructor(
suspend fun cachePriceData(period: GraphPeriod, price: PriceDTO) {
store.updateData { it.copy(cachedPrices = it.cachedPrices + (period to price)) }
}

suspend fun cacheArticles(articles: List<ArticleDTO>) {
store.updateData { it.copy(cachedArticles = articles) }
}

suspend fun bumpArticleRotationTick() {
store.updateData { it.copy(articleRotationTick = it.articleRotationTick + 1) }
}
}
13 changes: 13 additions & 0 deletions app/src/main/java/to/bitkit/appwidget/AppWidgetRefreshWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import to.bitkit.appwidget.model.AppWidgetType
import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceReceiver
import to.bitkit.appwidget.ui.headlines.HeadlinesGlanceWidget
import to.bitkit.appwidget.ui.price.PriceGlanceReceiver
import to.bitkit.appwidget.ui.price.PriceGlanceWidget
import to.bitkit.utils.Logger
Expand Down Expand Up @@ -62,6 +64,7 @@ class AppWidgetRefreshWorker @AssistedInject constructor(

private fun receiverClassFor(type: AppWidgetType): Class<out GlanceAppWidgetReceiver> = when (type) {
AppWidgetType.PRICE -> PriceGlanceReceiver::class.java
AppWidgetType.HEADLINES -> HeadlinesGlanceReceiver::class.java
}
}

Expand All @@ -84,6 +87,16 @@ class AppWidgetRefreshWorker @AssistedInject constructor(
}
PriceGlanceWidget().updateAll(appContext)
}

AppWidgetType.HEADLINES -> {
dataRepository.fetchArticles()
.onSuccess { preferencesStore.cacheArticles(it) }
.onFailure {
Logger.warn("Failed to refresh headlines", it, context = TAG)
Comment thread
jvsena42 marked this conversation as resolved.
}
preferencesStore.bumpArticleRotationTick()
HeadlinesGlanceWidget().updateAll(appContext)
}
}
}

Expand Down
Loading
Loading