Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0a84466
feat: headline card v61 with wide and compact sizes
jvsena42 Apr 28, 2026
1247862
feat: update HeadlinesEditScreen.kt to v61
jvsena42 Apr 29, 2026
6572e0b
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 29, 2026
49e7dea
feat: update HeadlinesPreviewScreen.kt to v61
jvsena42 Apr 29, 2026
623f348
feat: implement headline OS widget
jvsena42 Apr 29, 2026
690cc32
fix: increase title max lines and fill height for fit in 2 cells
jvsena42 Apr 29, 2026
c371a18
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 29, 2026
6704f95
chore: private constant
jvsena42 Apr 29, 2026
1d30542
refactor: extract compact widget card size constant
jvsena42 Apr 29, 2026
ce80c09
fix: fill height
jvsena42 Apr 29, 2026
d67a7c0
fix: rotate headlines os widget article each tick
jvsena42 Apr 29, 2026
dc44dba
doc: changelog entry
jvsena42 Apr 29, 2026
4207694
fix: remove drawer button
jvsena42 Apr 29, 2026
8e926e3
fix: guard empty url
jvsena42 Apr 29, 2026
4e2b7cf
refactor: logger call
jvsena42 Apr 29, 2026
b0acba0
refactor: add modifier optional parameter
jvsena42 Apr 29, 2026
cc824a1
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 29, 2026
2c5999c
Merge remote-tracking branch 'origin/feat/price-widget-v61' into feat…
jvsena42 Apr 30, 2026
729ef56
chore: lint
jvsena42 Apr 30, 2026
b286fa0
refactor: replace raw spacer
jvsena42 Apr 30, 2026
2c55f30
chore: apply modifier rules
jvsena42 Apr 30, 2026
cb54014
chore: lint
jvsena42 Apr 30, 2026
ce66693
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 30, 2026
0aa64fa
Merge branch 'feat/price-widget-v61' into feat/headlines-v61
jvsena42 Apr 30, 2026
eee104b
fix: ui tests
jvsena42 Apr 30, 2026
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
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,
Comment thread
jvsena42 marked this conversation as resolved.
Comment thread
jvsena42 marked this conversation as resolved.
Comment thread
jvsena42 marked this conversation as resolved.
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand All @@ -52,18 +51,12 @@ class HeadlinesPreviewContentTest {

// Assert main elements exist
composeTestRule.onNodeWithTag("headlines_preview_screen").assertExists()
composeTestRule.onNodeWithTag("main_content").assertExists()

// Verify header elements
composeTestRule.onNodeWithTag("header_row").assertExists()
composeTestRule.onNodeWithTag("widget_title").assertExists()
composeTestRule.onNodeWithTag("widget_icon").assertExists()
composeTestRule.onNodeWithTag("widget_description").assertExists()

// Verify settings and preview section
composeTestRule.onNodeWithTag("WidgetEdit").assertExists()
composeTestRule.onNodeWithTag("preview_label").assertExists()
composeTestRule.onNodeWithTag("headline_card").assertExists()
composeTestRule.onNodeWithTag("headlines_preview_carousel").assertExists()
composeTestRule.onNodeWithTag("headline_card_small").assertExists()

// Verify buttons
composeTestRule.onNodeWithTag("buttons_row").assertExists()
Expand Down Expand Up @@ -97,7 +90,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 +126,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = customPreferences,
article = mockArticle
Expand All @@ -145,7 +136,7 @@ class HeadlinesPreviewContentTest {
// Assert that all elements still exist with custom preferences
composeTestRule.onNodeWithTag("headlines_preview_screen").assertExists()
composeTestRule.onNodeWithTag("WidgetEdit").assertExists()
composeTestRule.onNodeWithTag("headline_card").assertExists()
composeTestRule.onNodeWithTag("headlines_preview_carousel").assertExists()
}

@Test
Expand All @@ -158,7 +149,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand All @@ -168,15 +158,11 @@ class HeadlinesPreviewContentTest {

// Assert all tagged elements exist
composeTestRule.onNodeWithTag("headlines_preview_screen").assertExists()
composeTestRule.onNodeWithTag("main_content").assertExists()
composeTestRule.onNodeWithTag("header_row").assertExists()
composeTestRule.onNodeWithTag("widget_title").assertExists()
composeTestRule.onNodeWithTag("widget_icon").assertExists()
composeTestRule.onNodeWithTag("widget_description").assertExists()
composeTestRule.onNodeWithTag("divider").assertExists()
composeTestRule.onNodeWithTag("WidgetEdit").assertExists()
composeTestRule.onNodeWithTag("preview_label").assertExists()
composeTestRule.onNodeWithTag("headline_card").assertExists()
composeTestRule.onNodeWithTag("headlines_preview_carousel").assertExists()
composeTestRule.onNodeWithTag("headline_card_small").assertExists()
composeTestRule.onNodeWithTag("buttons_row").assertExists()
composeTestRule.onNodeWithTag("WidgetDelete").assertExists()
composeTestRule.onNodeWithTag("WidgetSave").assertExists()
Expand All @@ -194,7 +180,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = true,
isHeadlinesImplemented = true,
headlinePreferences = mockHeadlinePreferences,
article = mockArticle
Expand All @@ -219,7 +204,6 @@ class HeadlinesPreviewContentTest {
onClickEdit = {},
onClickDelete = {},
onClickSave = {},
showWidgetTitles = false,
isHeadlinesImplemented = false,
headlinePreferences = minimalPreferences,
article = mockArticle
Expand All @@ -229,7 +213,7 @@ class HeadlinesPreviewContentTest {

// Assert core elements still exist
composeTestRule.onNodeWithTag("headlines_preview_screen").assertExists()
composeTestRule.onNodeWithTag("headline_card").assertExists()
composeTestRule.onNodeWithTag("headlines_preview_carousel").assertExists()
composeTestRule.onNodeWithTag("WidgetSave").assertExists()
composeTestRule.onNodeWithTag("WidgetDelete").assertDoesNotExist()
}
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()
}
}
Loading
Loading