~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-android

1 collaborator
0
1
main
193 lines · 6.7 KB Raw History
package social.grain

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import social.grain.data.api.PinnedFeed
import social.grain.data.model.AspectRatio
import social.grain.data.model.GrainExif
import social.grain.util.compactCount
import social.grain.util.monthDay
import social.grain.util.nowIso
import social.grain.util.parseIso
import social.grain.util.relativeTime
import java.time.Instant

/**
 * The small conversions that sit under every screen.
 *
 * None of them can fail loudly: a mis-rounded shutter speed or a count that
 * reads "1.0K" is simply wrong on screen, in a caption nobody is checking
 * against the source.
 */
class FormattingTest {

    // MARK: - Counts

    @Test
    fun countsBelowAThousandAreWrittenOutInFull() {
        assertEquals("0", 0.compactCount())
        assertEquals("999", 999.compactCount())
    }

    @Test
    fun thousandsAndMillionsDropTheirTrailingZero() {
        assertEquals("1K", 1_000.compactCount())
        assertEquals("1.2K", 1_200.compactCount())
        assertEquals("2K", 2_000.compactCount())
        assertEquals("12.3K", 12_345.compactCount())
        assertEquals("1M", 1_000_000.compactCount())
        assertEquals("1.5M", 1_500_000.compactCount())
    }

    // MARK: - Dates

    @Test
    fun timestampsAreWrittenTheWayJavascriptWritesThem() {
        assertEquals(
            "2026-03-01T12:00:00.000Z",
            nowIso(Instant.parse("2026-03-01T12:00:00Z")),
        )
    }

    @Test
    fun timestampsParseWithOrWithoutFractionalSecondsAndAnOffset() {
        assertEquals(
            Instant.parse("2026-03-01T12:00:00Z"),
            parseIso("2026-03-01T12:00:00.000Z"),
        )
        assertEquals(Instant.parse("2026-03-01T12:00:00Z"), parseIso("2026-03-01T12:00:00Z"))
        assertEquals(
            "An offset is still a moment in time",
            Instant.parse("2026-03-01T12:00:00Z"),
            parseIso("2026-03-01T13:00:00+01:00"),
        )
        assertNull(parseIso("some time last week"))
    }

    @Test
    fun relativeTimeCountsUpThroughTheUnitsAndThenGivesADate() {
        val now = System.currentTimeMillis()
        fun ago(seconds: Long) = relativeTime(Instant.ofEpochMilli(now - seconds * 1000))

        assertEquals("now", ago(5))
        assertEquals("59m", ago(59 * 60))
        assertEquals("2h", ago(2 * 3600))
        assertEquals("3d", ago(3 * 86_400))
        assertEquals("2w", ago(14 * 86_400))
        assertEquals("Anything older is a date", "Mar 5", relativeTime("2020-03-05T12:00:00Z"))
    }

    @Test
    fun aMissingOrUnreadableTimestampFormatsAsNothingAtAll() {
        assertEquals("", relativeTime(null))
        assertEquals("", relativeTime("not a date"))
        assertEquals("", monthDay(null))
        assertEquals("Mar 5", monthDay("2020-03-05T12:00:00Z"))
    }

    // MARK: - Aspect ratios

    @Test
    fun anAspectRatioWithNoHeightIsTreatedAsSquare() {
        assertEquals(1.5f, AspectRatio(3, 2).ratio)
        assertEquals("A zero height would otherwise divide by zero", 1f, AspectRatio(3, 0).ratio)
    }

    // MARK: - Exif

    private fun exif(
        make: String? = null,
        model: String? = null,
        lensMake: String? = null,
        lensModel: String? = null,
        fNumber: String? = null,
        exposureTime: String? = null,
        focal: String? = null,
        iso: Int? = null,
    ) = GrainExif(
        make = make,
        model = model,
        lensMake = lensMake,
        lensModel = lensModel,
        fNumber = fNumber,
        exposureTime = exposureTime,
        focalLengthIn35mmFormat = focal,
        iSO = iso,
    )

    @Test
    fun theCameraNameJoinsTheMakeAndModelAndSkipsWhatIsMissing() {
        assertEquals("Fujifilm X100V", exif(make = "Fujifilm", model = "X100V").cameraName)
        assertEquals("X100V", exif(model = "X100V").cameraName)
        assertNull(exif().cameraName)
        assertNull("Empty strings are as absent as nulls", exif(make = "", model = "").cameraName)
    }

    @Test
    fun theLensNamePrefersTheModelAndFallsBackToTheMake() {
        assertEquals("XF 23mm f/2", exif(lensModel = "XF 23mm f/2").lensName)
        assertEquals("Fujinon", exif(lensMake = "Fujinon").lensName)
        assertNull(exif().lensName)
    }

    @Test
    fun apertureDropsTheZeroesTheServerLeavesOn() {
        assertEquals("f/2", exif(fNumber = "2.0").formattedFNumber)
        assertEquals("f/2.8", exif(fNumber = "f/2.80").formattedFNumber)
        assertEquals("f/1.4", exif(fNumber = "1.4").formattedFNumber)
        assertEquals("Something unparseable is passed through", "wide", exif(fNumber = "wide").formattedFNumber)
        assertNull(exif().formattedFNumber)
    }

    @Test
    fun shutterSpeedIsNormalisedToAWholeFractionOrSeconds() {
        assertEquals("1/500s", exif(exposureTime = "1/500").formattedExposureTime)
        assertEquals("1/500s", exif(exposureTime = "0.002s").formattedExposureTime)
        assertEquals("30s", exif(exposureTime = "30").formattedExposureTime)
        assertEquals("1.5s", exif(exposureTime = "1.5").formattedExposureTime)
        assertEquals("fast", exif(exposureTime = "fast").formattedExposureTime)
        assertNull(exif().formattedExposureTime)
    }

    @Test
    fun focalLengthIsRoundedToWholeMillimetres() {
        assertEquals("35mm", exif(focal = "35").formattedFocalLength)
        assertEquals("35mm", exif(focal = "34.6mm").formattedFocalLength)
        assertEquals("long", exif(focal = "long").formattedFocalLength)
        assertNull(exif().formattedFocalLength)
    }

    @Test
    fun theSettingsLineJoinsWhateverIsKnownAndIsAbsentWhenNothingIs() {
        val full = exif(fNumber = "2.0", exposureTime = "1/500", focal = "35", iso = 400)
        assertEquals("35mm  ·  f/2  ·  1/500s  ·  ISO 400", full.settingsLine)
        assertEquals("ISO 400", exif(iso = 400).settingsLine)
        assertNull(exif().settingsLine)
    }

    @Test
    fun aPhotoWithNothingToSayHidesTheExifPanel() {
        assertEquals(false, exif().hasDisplayableData)
        assertEquals(true, exif(iso = 100).hasDisplayableData)
        assertEquals(true, exif(make = "Leica").hasDisplayableData)
    }

    // MARK: - Pinned feeds

    @Test
    fun theDefaultPinsAreTheThreeGrainShipsWith() {
        assertEquals(listOf("recent", "following", "foryou"), PinnedFeed.defaults.map { it.id })
        assertEquals("Recent", PinnedFeed.defaults.first().label)
    }

    @Test
    fun aPlainFeedNamesItselfByIdAndACompoundOneByType() {
        assertEquals("following", PinnedFeed.defaults[1].feedName)
        assertNull(PinnedFeed.defaults[1].feedValue)

        val location = PinnedFeed("location:8928308280fffff", "Mission", "location", "/l")
        assertEquals("location", location.feedName)
        assertEquals("8928308280fffff", location.feedValue)
    }
}