diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index a8c8d41..ad20815 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -1423,23 +1423,6 @@ export async function verifyAmountToSend(amount: number) { await expectTextWithin('SendNumberField', formatSats(amount)); } -export async function deleteAllDefaultWidgets() { - await swipeFullScreen('up'); - await swipeFullScreen('up'); - await tap('WidgetsEdit'); - for (const w of ['Bitcoin Price', 'Bitcoin Blocks', 'Bitkit Suggestions']) { - tap(w + '_WidgetActionDelete'); - await elementByText('Yes, Delete').waitForDisplayed(); - await elementByText('Yes, Delete').click(); - await elementById(w).waitForDisplayed({ reverse: true, timeout: 5000 }); - await sleep(1000); - } - await tap('WidgetsEdit'); - await elementById('PriceWidget').waitForDisplayed({ reverse: true }); - await elementById('SuggestionsWidget').waitForDisplayed({ reverse: true }); - await elementById('BlocksWidget').waitForDisplayed({ reverse: true }); -} - export async function attemptRefreshOnHomeScreen() { await swipeFullScreen('down'); await sleep(2000); // wait for the app to settle diff --git a/test/helpers/widgets.ts b/test/helpers/widgets.ts new file mode 100644 index 0000000..53ce9ad --- /dev/null +++ b/test/helpers/widgets.ts @@ -0,0 +1,204 @@ +import { elementById, elementByText, sleep, swipeFullScreen, tap } from './actions'; + +export type WidgetId = + | 'price' + | 'blocks' + | 'news' + | 'facts' + | 'weather' + | 'suggestions' + | 'calculator'; + +type WidgetMetadata = { + listItemId: string; + actionName: string; + homeId?: () => string | undefined; + hasSettings: () => boolean; +}; + +const WIDGETS: Record = { + price: { + listItemId: 'WidgetListItem-price', + actionName: 'Bitcoin Price', + homeId: () => 'PriceWidget', + hasSettings: () => true, + }, + blocks: { + listItemId: 'WidgetListItem-blocks', + actionName: 'Bitcoin Blocks', + homeId: () => 'BlocksWidget', + hasSettings: () => true, + }, + news: { + listItemId: 'WidgetListItem-news', + actionName: 'Bitcoin Headlines', + homeId: () => 'NewsWidget', + hasSettings: () => true, + }, + facts: { + listItemId: 'WidgetListItem-facts', + actionName: 'Bitcoin Facts', + homeId: () => (driver.isIOS ? 'FactsWidget' : undefined), + hasSettings: () => driver.isIOS, + }, + weather: { + listItemId: 'WidgetListItem-weather', + actionName: 'Bitcoin Weather', + homeId: () => (driver.isIOS ? 'WeatherWidget' : undefined), + hasSettings: () => true, + }, + suggestions: { + listItemId: 'WidgetListItem-suggestions', + actionName: 'Bitkit Suggestions', + homeId: () => 'SuggestionsWidget', + hasSettings: () => false, + }, + calculator: { + listItemId: 'WidgetListItem-calculator', + actionName: 'Calculator', + homeId: () => 'CalculatorWidget', + hasSettings: () => false, + }, +}; + +const DEFAULT_WIDGETS: WidgetId[] = ['price', 'blocks', 'suggestions']; + +function widgetMetadata(widget: WidgetId): WidgetMetadata { + return WIDGETS[widget]; +} + +function widgetActionId(widget: WidgetId, action: 'Delete' | 'Edit' | 'Drag') { + return `${widgetMetadata(widget).actionName}_WidgetAction${action}`; +} + +async function tapIfDisplayed(testId: string, timeout = 2_000): Promise { + const element = await elementById(testId); + try { + await element.waitForDisplayed({ timeout }); + await element.click(); + await sleep(300); + return true; + } catch { + return false; + } +} + +async function tapWidgetListItem(widget: WidgetId) { + const { listItemId } = widgetMetadata(widget); + if (await tapIfDisplayed(listItemId)) { + return; + } + await swipeFullScreen('up'); + await tap(listItemId); +} + +export async function scrollHomeToWidgets() { + await swipeFullScreen('up'); + await swipeFullScreen('up'); + await sleep(500); +} + +export async function openWidgetsFeed() { + await scrollHomeToWidgets(); + await tap('WidgetsAdd'); + await tapIfDisplayed('WidgetsOnboarding-button'); +} + +export async function openWidgetPreview(widget: WidgetId) { + await openWidgetsFeed(); + await tapWidgetListItem(widget); +} + +export async function addWidget(widget: WidgetId) { + await openWidgetPreview(widget); + await tap('WidgetSave'); + await elementById('WidgetsAdd').waitForDisplayed({ timeout: 15_000 }); +} + +export async function openWidgetSettings(widget: WidgetId) { + if (!widgetMetadata(widget).hasSettings()) { + throw new Error(`Widget '${widget}' does not have editable settings on this platform`); + } + await tap('WidgetEdit'); + await elementById('WidgetEditPreview').waitForDisplayed(); +} + +export async function openSavedWidgetPreview(widget: WidgetId) { + await scrollHomeToWidgets(); + await tap('WidgetsEdit'); + await tap(widgetActionId(widget, 'Edit')); + await elementById('WidgetSave').waitForDisplayed(); +} + +export async function expectWidgetPresent( + widget: WidgetId, + present = true, + { timeout = 8_000 }: { timeout?: number } = {} +) { + const homeId = widgetMetadata(widget).homeId?.(); + if (!homeId) { + await expectWidgetSavedInEditList(widget, present, { timeout }); + return; + } + await elementById(homeId).waitForDisplayed({ + reverse: !present, + timeout, + interval: 250, + }); +} + +export async function expectWidgetSavedInEditList( + widget: WidgetId, + present = true, + { timeout = 8_000 }: { timeout?: number } = {} +) { + await scrollHomeToWidgets(); + await tap('WidgetsEdit'); + await elementById(widgetActionId(widget, 'Delete')).waitForDisplayed({ + reverse: !present, + timeout, + interval: 250, + }); + await tap('WidgetsEdit'); +} + +export async function deleteWidget(widget: WidgetId) { + await scrollHomeToWidgets(); + await tap('WidgetsEdit'); + await tap(widgetActionId(widget, 'Delete')); + await elementByText('Yes, Delete').waitForDisplayed(); + await elementByText('Yes, Delete').click(); + await elementById(widgetActionId(widget, 'Delete')).waitForDisplayed({ + reverse: true, + timeout: 8_000, + interval: 250, + }); + await tap('WidgetsEdit'); + await sleep(500); +} + +export async function deleteWidgets(widgets: WidgetId[]) { + await scrollHomeToWidgets(); + await tap('WidgetsEdit'); + for (const widget of widgets) { + if (!(await tapIfDisplayed(widgetActionId(widget, 'Delete')))) { + continue; + } + await elementByText('Yes, Delete').waitForDisplayed(); + await elementByText('Yes, Delete').click(); + await elementById(widgetActionId(widget, 'Delete')).waitForDisplayed({ + reverse: true, + timeout: 8_000, + interval: 250, + }); + await sleep(500); + } + await tap('WidgetsEdit'); +} + +export async function deleteAllDefaultWidgets() { + await deleteWidgets(DEFAULT_WIDGETS); + for (const widget of DEFAULT_WIDGETS) { + await expectWidgetPresent(widget, false); + } +} diff --git a/test/specs/backup.e2e.ts b/test/specs/backup.e2e.ts index c96e2f6..6b771be 100644 --- a/test/specs/backup.e2e.ts +++ b/test/specs/backup.e2e.ts @@ -3,7 +3,6 @@ import { reinstallApp } from '../helpers/setup'; import { completeOnboarding, confirmInputOnKeyboard, - deleteAllDefaultWidgets, doNavigationClose, elementById, elementByIdWithin, @@ -20,6 +19,7 @@ import { import { ciIt } from '../helpers/suite'; import { ensureLocalFunds } from '../helpers/regtest'; import { openSettings } from '../helpers/navigation'; +import { deleteAllDefaultWidgets } from '../helpers/widgets'; describe('@backup - Backup', () => { let electrum: Awaited> | undefined; diff --git a/test/specs/widgets.e2e.ts b/test/specs/widgets.e2e.ts index 8a93122..63c7548 100644 --- a/test/specs/widgets.e2e.ts +++ b/test/specs/widgets.e2e.ts @@ -5,12 +5,23 @@ import { tap, swipeFullScreen, completeOnboarding, - deleteAllDefaultWidgets, doNavigationClose, } from '../helpers/actions'; import { openSettings } from '../helpers/navigation'; import { reinstallApp } from '../helpers/setup'; import { ciIt } from '../helpers/suite'; +import { + addWidget, + deleteAllDefaultWidgets, + deleteWidget, + expectWidgetPresent, + expectWidgetSavedInEditList, + openSavedWidgetPreview, + openWidgetPreview, + openWidgetSettings, + scrollHomeToWidgets, + type WidgetId, +} from '../helpers/widgets'; describe('@widgets - Widgets', () => { beforeEach(async () => { @@ -18,134 +29,107 @@ describe('@widgets - Widgets', () => { await completeOnboarding(); }); - ciIt('@widgets_1 - Can add/edit/remove a widget', async () => { - // delete all default widgets + ciIt('@widgets_1 - Can add/edit/remove the price widget', async () => { await deleteAllDefaultWidgets(); - // Add a widget - await tap('WidgetsAdd'); - // First-time widgets onboarding - await tap('WidgetsOnboarding-button'); - // Pick the Price widget - await tap('WidgetListItem-price'); - // Expect the “Default” preset is selected + await openWidgetPreview('price'); await elementByText('Default').waitForDisplayed(); - // Open edit options - await tap('WidgetEdit'); - await elementById('WidgetEditPreview').waitForDisplayed(); - // Select BTC/EUR row + await openWidgetSettings('price'); + await tap(driver.isAndroid ? 'BTC/EUR_setting_row' : 'WidgetEditField-BTC/EUR'); - await sleep(1000); // Wait for the UI to settle + await sleep(1000); - // Scroll the edit view await swipeFullScreen('up'); await swipeFullScreen('up'); await sleep(500); - // Set timeframe and, on iOS, keep testing the existing source toggle. await tap(driver.isAndroid ? '1W_setting_row' : 'WidgetEditField-1W'); if (driver.isIOS) { await tap('WidgetEditField-showSource'); } - await sleep(1000); // Wait for the UI to settle + await sleep(1000); - // Preview and save await tap('WidgetEditPreview'); await sleep(500); await tap('WidgetSave'); - // sometimes flaky on GH actions, try again try { - await elementById('PriceWidget').waitForDisplayed(); + await expectWidgetPresent('price'); } catch { await tap('WidgetSave'); } - await elementById('PriceWidget').waitForDisplayed(); - - // Back on Home: scroll a bit to ensure widget is in view - await elementById('PriceWidget').waitForDisplayed(); - await swipeFullScreen('up'); + await expectWidgetPresent('price'); - // Assertions - await elementById('PriceWidget').waitForDisplayed(); + await scrollHomeToWidgets(); await elementById('PriceWidgetRow-BTC/EUR').waitForDisplayed(); - // --- Edit the Price widget back to defaults --- - await tap('WidgetsEdit'); - - // Open edit within the Price widget specifically - await tap('Bitcoin Price_WidgetActionEdit'); - - // "Custom" should be visible when editing a customized widget + await openSavedWidgetPreview('price'); await elementByText('Custom').waitForDisplayed(); - - // reset options to defaults and save - await tap('WidgetEdit'); + await openWidgetSettings('price'); await tap('WidgetEditReset'); - await elementById('WidgetEditPreview').waitForDisplayed(); - await sleep(1000); // Wait for the UI to settle + await sleep(1000); await tap('WidgetEditPreview'); await elementById('WidgetSave').waitForDisplayed(); - await sleep(1000); // Wait for the UI to settle + await sleep(1000); await tap('WidgetSave'); - await sleep(1000); // Wait for the UI to settle - - // After saving, widget should remain visible… - await elementById('PriceWidget').waitForDisplayed(); + await sleep(1000); - // …but the BTC/EUR row should be gone + await expectWidgetPresent('price'); await elementById('PriceWidgetRow-BTC/EUR').waitForDisplayed({ reverse: true, timeout: 8000, interval: 250, }); - // Delete Price Widget - await tap('WidgetsEdit'); - await tap('Bitcoin Price_WidgetActionDelete'); - await elementByText('Yes, Delete').waitForDisplayed(); - await elementByText('Yes, Delete').click(); - await elementById('WidgetsAdd').waitForDisplayed(); + await deleteWidget('price'); + }); + + ciIt('@widgets_2 - Can add/remove redesigned content widgets', async () => { + const contentWidgets: WidgetId[] = ['blocks', 'news', 'facts', 'weather']; + + await deleteAllDefaultWidgets(); + + for (const widget of contentWidgets) { + await addWidget(widget); + await expectWidgetSavedInEditList(widget); + + if (widget === 'facts') { + await openSavedWidgetPreview(widget); + await elementById('WidgetEdit').waitForDisplayed({ + reverse: driver.isAndroid, + timeout: 5000, + }); + await tap('NavigationBack'); + } + + await deleteWidget(widget); + } }); - ciIt('@widgets_2 - Widget settings: reset, show/hide, titles', async () => { + ciIt('@widgets_3 - Widget settings: reset, show/hide, titles', async () => { await deleteAllDefaultWidgets(); - // Reset widgets via Widget Settings await openSettings(); await tap('WidgetsSettings'); await tap('ResetWidgets'); await tap('DialogConfirm'); await sleep(1000); - // Verify widgets are restored - await swipeFullScreen('up'); - await elementById('PriceWidget').waitForDisplayed(); - await elementById('SuggestionsWidget').waitForDisplayed(); - await elementById('BlocksWidget').waitForDisplayed(); + await scrollHomeToWidgets(); + await expectWidgetPresent('price'); + await expectWidgetPresent('suggestions'); + await expectWidgetPresent('blocks'); - // Toggle off "Show Widgets" await openSettings(); await tap('WidgetsSettings'); await tap('ShowWidgets'); await tap('NavigationBack'); await doNavigationClose(); - // Verify widgets are hidden on home - await swipeFullScreen('up'); - await elementById('PriceWidget').waitForDisplayed({ - reverse: true, - timeout: 5000, - }); - await elementById('SuggestionsWidget').waitForDisplayed({ - reverse: true, - timeout: 5000, - }); - await elementById('BlocksWidget').waitForDisplayed({ - reverse: true, - timeout: 5000, - }); + await scrollHomeToWidgets(); + await expectWidgetPresent('price', false, { timeout: 5000 }); + await expectWidgetPresent('suggestions', false, { timeout: 5000 }); + await expectWidgetPresent('blocks', false, { timeout: 5000 }); - // Toggle on "Show Widgets" + enable "Show Widget Titles" await openSettings(); await tap('WidgetsSettings'); await tap('ShowWidgets'); @@ -153,11 +137,10 @@ describe('@widgets - Widgets', () => { await tap('NavigationBack'); await doNavigationClose(); - // Verify widgets visible with titles - await swipeFullScreen('up'); - await elementById('PriceWidget').waitForDisplayed(); - await elementById('SuggestionsWidget').waitForDisplayed(); - await elementById('BlocksWidget').waitForDisplayed(); + await scrollHomeToWidgets(); + await expectWidgetPresent('price'); + await expectWidgetPresent('suggestions'); + await expectWidgetPresent('blocks'); if (driver.isAndroid) { await elementByText('Bitcoin Price').waitForDisplayed({ reverse: true,