#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=../main.sh source "$ROOT_DIR/main.sh" TEST_TMP="$(mktemp -d "${TMPDIR:-/tmp}/blackout-cli-tests.XXXXXX")" trap 'rm -rf -- "$TEST_TMP"' EXIT assert_equal() { local expected="$1" local actual="$2" local message="$3" if [[ "$expected" != "$actual" ]]; then printf 'FAIL: %s\nExpected: %s\nActual: %s\n' "$message" "$expected" "$actual" >&2 exit 1 fi } assert_contains() { local text="$1" local expected="$2" local message="$3" if [[ "$text" != *"$expected"* ]]; then printf 'FAIL: %s\nMissing: %s\n' "$message" "$expected" >&2 exit 1 fi } FIXTURE="$TEST_TMP/outages.json" cat >"$FIXTURE" <<'JSON' { "schema_version": 1, "updated_at": "2026-07-18T16:00:00+03:30", "bills": { "222": { "events": { "5": { "status": "active", "snapshot": { "outage_number": "5", "outage_date": "1405/04/28", "start_time": "09:00", "stop_time": "11:00", "address": "Future planned address", "reason": "Maintenance", "is_planned": true } } } }, "111": { "events": { "1": { "status": "active", "snapshot": { "outage_number": "1", "outage_date": "1405/04/27", "start_time": "16:00", "stop_time": "18:00", "address": "Current address", "reason": "Load management", "is_planned": true } }, "2": { "status": "active", "snapshot": { "outage_number": "2", "outage_date": "1405/04/27", "start_time": "18:00", "stop_time": "20:00", "address": "Emergency address", "reason": "Emergency load management", "is_planned": false } }, "3": { "status": "cancelled", "snapshot": { "outage_number": "3", "outage_date": "1405/04/27", "start_time": "19:00", "stop_time": "21:00", "address": "Cancelled address", "reason": "Cancelled maintenance", "is_planned": true } }, "4": { "status": "active", "snapshot": { "outage_number": "4", "outage_date": "1405/04/27", "start_time": "10:00", "stop_time": "11:00", "address": "Expired address", "reason": "Old event", "is_planned": true } } } } } } JSON assert_equal "2026-07-18" "$(jalali_to_gregorian "1405/04/27")" \ "Jalali dates should convert to Gregorian dates" read -r cross_start cross_stop < <(event_epochs "1405/04/27" "23:00" "01:00") assert_equal "7200" "$((cross_stop - cross_start))" \ "cross-midnight outages should end on the following day" assert_equal "01 : 01 : 01" "$(banner_duration 3661)" \ "countdowns should include hours, minutes, and seconds" assert_equal "49 : 30 : 00" "$(banner_duration 178200)" \ "countdowns should use total hours beyond one day" FONT_OUTPUT="$(print_large_countdown "00 : 00 : 00" 150)" assert_contains "$FONT_OUTPUT" ',a8888a,' \ "the countdown should consistently use the univers-style font" if ! validate_state "$FIXTURE"; then printf 'FAIL: valid fixture was rejected\n' >&2 exit 1 fi NOW_EPOCH="$(TZ=Asia/Tehran date -d '2026-07-18 17:00:00' +%s)" build_schedule "$FIXTURE" "$NOW_EPOCH" assert_equal "3" "${#SCHEDULE[@]}" \ "only active current and future events should be scheduled" assert_equal "ONGOING" "${HERO%%$'\t'*}" \ "an ongoing outage should take the hero position" assert_contains "${SCHEDULE[0]}" '"outage_number":"1"' \ "ongoing events should sort first" assert_contains "${SCHEDULE[1]}" '"outage_number":"2"' \ "the nearest future event should sort second" assert_contains "${SCHEDULE[2]}" '"outage_number":"5"' \ "later events should remain in the schedule" build_schedule "$FIXTURE" "$NOW_EPOCH" "222" assert_equal "3" "${#SCHEDULE[@]}" \ "selecting a countdown bill should not filter the schedule" assert_equal "UPCOMING" "${HERO%%$'\t'*}" \ "the countdown should ignore another bill's ongoing outage" assert_contains "$HERO" '"bill_id":"222"' \ "the countdown should use only the selected bill" SELECTED_OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_dashboard "$NOW_EPOCH" 80 "222")" assert_contains "$SELECTED_OUTPUT" "COUNTDOWN FOR BILL 222" \ "the selected countdown bill should be labeled" assert_contains "$SELECTED_OUTPUT" "OUTAGE SCHEDULE (3)" \ "the selected countdown bill should not hide other bills from the list" build_schedule "$FIXTURE" "$NOW_EPOCH" "333" assert_equal "" "$HERO" \ "a bill without upcoming outages should not borrow another bill's countdown" NO_HERO_OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_dashboard "$NOW_EPOCH" 80 "333")" assert_contains "$NO_HERO_OUTPUT" "NO CURRENT OR UPCOMING OUTAGE" \ "a selected bill without outages should have a clear countdown status" assert_contains "$NO_HERO_OUTPUT" "OUTAGE SCHEDULE (3)" \ "all bills should remain listed when the selected bill has no outage" build_schedule "$FIXTURE" "$NOW_EPOCH" OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_dashboard "$NOW_EPOCH")" if [[ "$OUTPUT" == *"POWER RETURNS IN"* || "$OUTPUT" == *"NEXT OUTAGE IN"* ]]; then printf 'FAIL: countdown output contains a redundant heading\n' >&2 exit 1 fi assert_contains "$OUTPUT" "PLANNED" \ "planned events should be labeled" assert_contains "$OUTPUT" "EMERGENCY" \ "unplanned events should be labeled" assert_contains "$OUTPUT" "OUTAGE SCHEDULE (3)" \ "the dashboard should include every selected event" if [[ "$OUTPUT" == *"Address"* || "$OUTPUT" == *"Reason"* \ || "$OUTPUT" == *"Emergency address"* || "$OUTPUT" == *"Load management"* ]]; then printf 'FAIL: dashboard output exposes address or reason text\n' >&2 exit 1 fi if [[ "$OUTPUT" == *$'\033['* ]]; then printf 'FAIL: NO_COLOR output contains ANSI escapes\n' >&2 exit 1 fi WIDE_OUTPUT="$(NO_COLOR=1 COLUMNS=180 render_dashboard "$NOW_EPOCH")" WIDE_TOP_LINE="${WIDE_OUTPUT%%$'\n'*}" assert_equal "180" "${#WIDE_TOP_LINE}" \ "the countdown divider should span the full terminal width" WIDE_BOTTOM_LINE="${WIDE_OUTPUT##*$'\n'}" assert_equal "180" "${#WIDE_BOTTOM_LINE}" \ "the dashboard bottom divider should span the full terminal width" FUTURE_NOW="$(TZ=Asia/Tehran date -d '2026-07-18 15:00:00' +%s)" build_schedule "$FIXTURE" "$FUTURE_NOW" assert_equal "UPCOMING" "${HERO%%$'\t'*}" \ "the nearest future event should be the hero when none are ongoing" EMPTY_NOW="$(TZ=Asia/Tehran date -d '2026-07-20 12:00:00' +%s)" build_schedule "$FIXTURE" "$EMPTY_NOW" assert_equal "0" "${#SCHEDULE[@]}" \ "expired schedules should become empty" EMPTY_OUTPUT="$(NO_COLOR=1 COLUMNS=60 render_dashboard "$EMPTY_NOW")" assert_contains "$EMPTY_OUTPUT" "ALL CLEAR" \ "an empty schedule should render the all-clear screen" WIDE_EMPTY_OUTPUT="$(NO_COLOR=1 COLUMNS=180 render_dashboard "$EMPTY_NOW")" WIDE_EMPTY_TOP_LINE="${WIDE_EMPTY_OUTPUT%%$'\n'*}" WIDE_EMPTY_BOTTOM_LINE="${WIDE_EMPTY_OUTPUT##*$'\n'}" assert_equal "180" "${#WIDE_EMPTY_TOP_LINE}" \ "the all-clear top divider should span the full terminal width" assert_equal "180" "${#WIDE_EMPTY_BOTTOM_LINE}" \ "the all-clear bottom divider should span the full terminal width" WIDE_UNAVAILABLE_OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_unavailable 180)" WIDE_UNAVAILABLE_TOP_LINE="${WIDE_UNAVAILABLE_OUTPUT%%$'\n'*}" WIDE_UNAVAILABLE_BOTTOM_LINE="${WIDE_UNAVAILABLE_OUTPUT##*$'\n'}" assert_equal "180" "${#WIDE_UNAVAILABLE_TOP_LINE}" \ "the unavailable top divider should span the full terminal width" assert_equal "180" "${#WIDE_UNAVAILABLE_BOTTOM_LINE}" \ "the unavailable bottom divider should use the detected terminal width" TERMINAL_ACTIVE=1 DRAW_OUTPUT="$(draw_frame $'short\nlonger')" TERMINAL_ACTIVE=0 assert_contains "$DRAW_OUTPUT" $'short\033[K\nlonger\033[K' \ "interactive redraws should erase old content from every line" MALFORMED="$TEST_TMP/malformed.json" printf '{"schema_version": 999, "bills": {}}\n' >"$MALFORMED" if validate_state "$MALFORMED"; then printf 'FAIL: unsupported schema was accepted\n' >&2 exit 1 fi if ( curl() { return 22; } fetch_state "$TEST_TMP/download.json" "123" ); then printf 'FAIL: download failure was reported as success\n' >&2 exit 1 fi if main --bill-id invalid >/dev/null 2>&1; then printf 'FAIL: a non-decimal countdown bill ID was accepted\n' >&2 exit 1 fi printf 'Bash CLI tests passed\n'