Tests/Tests-MeasureDisplayWidth.ps1

#Requires -Modules @{ ModuleName='Pester'; ModuleVersion='5.0.0' }

BeforeDiscovery {
    # Each case: a name, the code points of the text, its width, and its width with -AmbiguousAsWide
    $widthCases = @(
        @{ Name = 'printable ASCII'; CodePoints = @(0x48, 0x65, 0x6C, 0x6C, 0x6F); Width = 5; WideWidth = 5 }
        @{ Name = 'CJK characters'; CodePoints = @(0x4E16, 0x754C); Width = 4; WideWidth = 4 }
        @{ Name = 'ASCII and CJK'; CodePoints = @(0x48, 0x69, 0x20, 0x4E16, 0x754C); Width = 7; WideWidth = 7 }
        @{ Name = 'fullwidth letter'; CodePoints = @(0xFF21); Width = 2; WideWidth = 2 }
        @{ Name = 'check mark U+2713'; CodePoints = @(0x2713); Width = 1; WideWidth = 1 }
        @{ Name = 'check mark button U+2705'; CodePoints = @(0x2705); Width = 2; WideWidth = 2 }
        @{ Name = 'emoji outside the BMP'; CodePoints = @(0x1F600); Width = 2; WideWidth = 2 }
        @{ Name = 'two emoji'; CodePoints = @(0x1F600, 0x1F44D); Width = 4; WideWidth = 4 }
        @{ Name = 'black circle (ambiguous)'; CodePoints = @(0x25CF); Width = 1; WideWidth = 2 }
        @{ Name = 'rightwards arrow (ambiguous)'; CodePoints = @(0x2192); Width = 1; WideWidth = 2 }
        @{ Name = 'box drawing (ambiguous)'; CodePoints = @(0x2554, 0x2550, 0x2550, 0x2550, 0x2557); Width = 5; WideWidth = 10 }
        @{ Name = 'precomposed e acute'; CodePoints = @(0xE9); Width = 1; WideWidth = 1 }
        @{ Name = 'e and a combining acute accent'; CodePoints = @(0x65, 0x301); Width = 1; WideWidth = 1 }
        @{ Name = 'zero width space'; CodePoints = @(0x200B); Width = 0; WideWidth = 0 }
        @{ Name = 'tab between letters'; CodePoints = @(0x61, 0x9, 0x62); Width = 2; WideWidth = 2 }
        @{ Name = 'Hangul jamo syllable'; CodePoints = @(0x1100, 0x1161); Width = 2; WideWidth = 2 }
        @{ Name = 'warning sign alone'; CodePoints = @(0x26A0); Width = 1; WideWidth = 1 }
        @{ Name = 'warning sign with U+FE0F'; CodePoints = @(0x26A0, 0xFE0F); Width = 2; WideWidth = 2 }
        @{ Name = 'heart with U+FE0F'; CodePoints = @(0x2764, 0xFE0F); Width = 2; WideWidth = 2 }
        @{ Name = 'watch alone'; CodePoints = @(0x231A); Width = 2; WideWidth = 2 }
        @{ Name = 'watch with U+FE0E'; CodePoints = @(0x231A, 0xFE0E); Width = 1; WideWidth = 2 }
        @{ Name = 'thumbs up with skin tone'; CodePoints = @(0x1F44D, 0x1F3FD); Width = 2; WideWidth = 2 }
        @{ Name = 'family joined by U+200D'; CodePoints = @(0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467); Width = 2; WideWidth = 2 }
        @{ Name = 'technologist with skin tone'; CodePoints = @(0x1F469, 0x1F3FD, 0x200D, 0x1F4BB); Width = 2; WideWidth = 2 }
        @{ Name = 'eye in speech bubble'; CodePoints = @(0x1F441, 0xFE0F, 0x200D, 0x1F5E8, 0xFE0F); Width = 2; WideWidth = 2 }
        @{ Name = 'heart on fire'; CodePoints = @(0x2764, 0xFE0F, 0x200D, 0x1F525); Width = 2; WideWidth = 2 }
        @{ Name = 'flag'; CodePoints = @(0x1F1FA, 0x1F1F8); Width = 2; WideWidth = 2 }
        @{ Name = 'text after an emoji sequence'; CodePoints = @(0x1F44D, 0x1F3FD, 0x20, 0x6F, 0x6B); Width = 5; WideWidth = 5 }
    )

    # Each case: a name, the code points of the text, and the code points of each character a
    # terminal draws from it
    $splitCases = @(
        @{ Name = 'letters'; CodePoints = @(0x61, 0x62); Characters = @(@(0x61), @(0x62)) }
        @{ Name = 'an emoji outside the BMP'; CodePoints = @(0x61, 0x1F600, 0x62); Characters = @(@(0x61), @(0x1F600), @(0x62)) }
        @{ Name = 'a letter and a combining accent'; CodePoints = @(0x65, 0x301, 0x78); Characters = @(@(0x65, 0x301), @(0x78)) }
        @{ Name = 'a tab between letters'; CodePoints = @(0x61, 0x9, 0x62); Characters = @(@(0x61), @(0x9), @(0x62)) }
        @{ Name = 'a warning sign with U+FE0F'; CodePoints = @(0x26A0, 0xFE0F, 0x21); Characters = @(@(0x26A0, 0xFE0F), @(0x21)) }
        @{ Name = 'a thumbs up with skin tone'; CodePoints = @(0x1F44D, 0x1F3FD, 0x20, 0x6F, 0x6B); Characters = @(@(0x1F44D, 0x1F3FD), @(0x20), @(0x6F), @(0x6B)) }
        @{ Name = 'a family joined by U+200D'; CodePoints = @(0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467); Characters = @(, @(0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467)) }
        @{ Name = 'a heart on fire'; CodePoints = @(0x2764, 0xFE0F, 0x200D, 0x1F525); Characters = @(, @(0x2764, 0xFE0F, 0x200D, 0x1F525)) }
        @{ Name = 'two flags'; CodePoints = @(0x1F1FA, 0x1F1F8, 0x1F1EC, 0x1F1E7); Characters = @(@(0x1F1FA, 0x1F1F8), @(0x1F1EC, 0x1F1E7)) }
        @{ Name = 'CJK characters'; CodePoints = @(0x4E16, 0x754C); Characters = @(@(0x4E16), @(0x754C)) }
    )
}

BeforeAll {
    $ModuleRoot = Split-Path -Parent $PSScriptRoot
    Import-Module "$ModuleRoot\PSWriteColorEX.psd1" -Force

    # Builds a string from code points, so this file holds only ASCII
    function ConvertTo-TestString {
        param([int[]]$CodePoints)
        -join ($CodePoints | ForEach-Object { [char]::ConvertFromUtf32($_) })
    }
}

Describe 'Measure-DisplayWidth' -Tag 'Unit', 'Function', 'DisplayWidth' {

    Context 'Widths' {
        It 'Measures <Name> as <Width> cells' -TestCases $widthCases {
            $text = ConvertTo-TestString $CodePoints
            Measure-DisplayWidth -Text $text | Should -Be $Width
        }

        It 'Measures <Name> as <WideWidth> cells with -AmbiguousAsWide' -TestCases $widthCases {
            $text = ConvertTo-TestString $CodePoints
            Measure-DisplayWidth -Text $text -AmbiguousAsWide | Should -Be $WideWidth
        }
    }

    Context 'Input' {
        It 'Measures an empty string as 0' {
            Measure-DisplayWidth -Text '' | Should -Be 0
        }

        It 'Measures each string piped in' {
            $results = @('ab', (ConvertTo-TestString 0x4E16, 0x754C)) | Measure-DisplayWidth
            $results | Should -Be @(2, 4)
        }

        It 'Takes the text by position' {
            Measure-DisplayWidth 'abc' | Should -Be 3
        }

        It 'Answers an integer' {
            Measure-DisplayWidth 'abc' | Should -BeOfType [int]
        }

        It 'Measures a lone surrogate as 1' {
            Measure-DisplayWidth ([string][char]0xD83D) | Should -Be 1
        }
    }

    Context 'Aliases' {
        It 'MDW runs Measure-DisplayWidth' {
            (Get-Command MDW).ResolvedCommandName | Should -Be 'Measure-DisplayWidth'
        }

        It 'Get-DisplayWidth runs Measure-DisplayWidth' {
            (Get-Command Get-DisplayWidth).ResolvedCommandName | Should -Be 'Measure-DisplayWidth'
        }
    }
}

Describe 'Split-DisplayCharacter' -Tag 'Unit', 'Function', 'DisplayWidth' {

    It 'Splits <Name> into the characters a terminal draws' -TestCases $splitCases {
        $text = ConvertTo-TestString $CodePoints
        $expected = @(foreach ($character in $Characters) { ConvertTo-TestString $character })

        $result = @(& (Get-Module PSWriteColorEX) { param($t) Split-DisplayCharacter -Text $t } $text)

        $result.Count | Should -Be $expected.Count
        for ($i = 0; $i -lt $expected.Count; $i++) {
            $result[$i] | Should -BeExactly $expected[$i]
        }
    }

    It 'Answers nothing for an empty string' {
        $result = @(& (Get-Module PSWriteColorEX) { Split-DisplayCharacter -Text '' })
        $result.Count | Should -Be 0
    }
}