current commit
260517.6 · [codex] Fix OSC 8 hyperlink state transitions (#1112)
// Copyright 2024 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tcell

import (
	"testing"
	"unicode/utf8"

	"github.com/gdamore/tcell/v3/color"
)

func TestStyle(t *testing.T) {
	_, s := NewMockScreen(t)
	defer s.Fini()

	style := StyleDefault
	fg, bg, attr := style.fg, style.bg, style.attrs

	if fg != ColorDefault || bg != ColorDefault || attr != AttrNone {
		t.Errorf("Bad default style (%v, %v, %v)", fg, bg, attr)
	}

	s2 := style.
		Background(ColorRed).
		Foreground(ColorBlue).
		Blink(true)

	fg, bg, attr = s2.fg, s2.bg, s2.attrs
	if fg != ColorBlue || bg != ColorRed || attr != AttrBlink {
		t.Errorf("Bad custom style (%v, %v, %v)", fg, bg, attr)
	}

	if !s2.HasBlink() {
		t.Errorf("blink is false")
	}
	if s2.HasItalic() {
		t.Errorf("italic is true")
	}
	if s2.HasDim() {
		t.Errorf("dim is true")
	}
	if s2.HasBold() {
		t.Errorf("bold is true")
	}
	if s2.HasUnderline() {
		t.Errorf("underline is true")
	}
	if s2.HasReverse() {
		t.Errorf("reverse is true")
	}
	if s2.HasStrikeThrough() {
		t.Errorf("strike-through is true")
	}
	if id, url := s2.GetUrl(); id != "" || url != "" {
		t.Errorf("url not empty: %q %q", id, url)
	}
	if s2.GetAttributes() != AttrBlink {
		t.Errorf("wrong attrs %x", s2.GetAttributes())
	}
	if s2.GetBackground() != color.Red || s2.GetForeground() != color.Blue {
		t.Errorf("wrong colors %s %s", s2.GetForeground().String(), s2.GetBackground().String())
	}

	us := s2.Url("http://example.com")
	if id, url := us.GetUrl(); id != "" || url != "http://example.com" {
		t.Errorf("url wrong: %q %q", id, url)
	}
	us = us.Url("http://example.com").UrlId("someId")
	if id, url := us.GetUrl(); id != "someId" || url != "http://example.com" {
		t.Errorf("url wrong: %q %q", id, url)
	}
	us = us.Underline(UnderlineStyleDotted, color.Pink)
	if us.GetUnderlineColor() != color.Pink {
		t.Errorf("underline color wrong: %q", us.GetUnderlineColor().String())
	}
	if us.GetUnderlineStyle() != UnderlineStyleDotted {
		t.Errorf("wrong underline style: %v", us.GetUnderlineStyle())
	}

	us = us.Url("")
	if id, url := us.GetUrl(); id != "someId" || url != "" {
		t.Errorf("url clear should preserve id state: %q %q", id, url)
	}

	us = us.Url("http://example.net")
	if id, url := us.GetUrl(); id != "someId" || url != "http://example.net" {
		t.Errorf("url id not preserved across clear: %q %q", id, url)
	}

	us = us.UrlId("")
	if id, url := us.GetUrl(); id != "" || url != "http://example.net" {
		t.Errorf("url id clear did not preserve url: %q %q", id, url)
	}

	orphan := StyleDefault.UrlId("orphan")
	if id, url := orphan.GetUrl(); id != "orphan" || url != "" {
		t.Errorf("url id without url should be retained: %q %q", id, url)
	}

	orphan = orphan.Url("http://example.org")
	if id, url := orphan.GetUrl(); id != "orphan" || url != "http://example.org" {
		t.Errorf("url id set before url was not preserved: %q %q", id, url)
	}

	if id, url := StyleDefault.Url("").GetUrl(); id != "" || url != "" {
		t.Errorf("empty url should leave no hyperlink state: %q %q", id, url)
	}

	if id, url := StyleDefault.UrlId("").GetUrl(); id != "" || url != "" {
		t.Errorf("empty url id should leave no hyperlink state: %q %q", id, url)
	}

	us = us.Normal().Reverse(true).Italic(false)
	if us.GetAttributes() != AttrReverse {
		t.Errorf("wrong attributes: %v", us.GetAttributes())
	}
}

func TestStyleUrlStripsOSCControls(t *testing.T) {
	s := StyleDefault.
		Url("http://exa\x07mple.com/\x1b\\path").
		UrlId("id\x00\x1f\x7f\x80\x9fend")

	id, url := s.GetUrl()
	combined := id + url
	for i := 0; i < len(combined); i++ {
		c := combined[i]
		if c <= 0x1f || c == 0x7f || (c >= 0x80 && c <= 0x9f) {
			t.Fatalf("control characters survived sanitization: id=%q url=%q", id, url)
		}
	}
	if id != "idend" {
		t.Fatalf("unexpected sanitized id: %q", id)
	}
	if url != "http://example.com/\\path" {
		t.Fatalf("unexpected sanitized url: %q", url)
	}

	s = StyleDefault.
		Url("http://example.com/\u3042\u009b").
		UrlId("id\u3042\u009bend")

	id, url = s.GetUrl()
	combined = id + url
	if !utf8.ValidString(combined) {
		t.Fatalf("UTF-8 was corrupted during sanitization: id=%q url=%q", id, url)
	}
	for _, r := range combined {
		if r <= 0x1f || r == 0x7f || (r >= 0x80 && r <= 0x9f) {
			t.Fatalf("control characters survived UTF-8 sanitization: id=%q url=%q", id, url)
		}
	}
	if id != "id\u3042end" {
		t.Fatalf("unexpected sanitized UTF-8 id: %q", id)
	}
	if url != "http://example.com/\u3042" {
		t.Fatalf("unexpected sanitized UTF-8 url: %q", url)
	}
}

func TestStripOSCControlsIfNeeded(t *testing.T) {
	if got := stripOSCControlsIfNeeded("hello world"); got != "hello world" {
		t.Fatalf("clean string changed: %q", got)
	}

	if got := stripOSCControlsIfNeeded("he\x07llo\x1bworld"); got != "helloworld" {
		t.Fatalf("dirty string not stripped correctly: %q", got)
	}
}

func TestStripOSCControlsInvalidUTF8(t *testing.T) {
	got := stripOSCControls(string([]byte{0xff, 0x1b, 0xfe, 'A'}))
	if got != string([]byte{0xff, 0xfe, 'A'}) {
		t.Fatalf("unexpected invalid-UTF8 sanitization result: %q", got)
	}
}

func TestStyleHelperMethods(t *testing.T) {
	s := StyleDefault.
		Dim(true).
		StrikeThrough(true).
		Underline(true)
	if !s.HasDim() {
		t.Fatalf("Dim(true) should set dim")
	}
	if !s.HasStrikeThrough() {
		t.Fatalf("StrikeThrough(true) should set strike-through")
	}
	if !s.HasUnderline() {
		t.Fatalf("Underline(true) should set underline")
	}

	s = s.Underline(false)
	if s.HasUnderline() {
		t.Fatalf("Underline(false) should clear underline")
	}

	s = s.Attributes(AttrBold | AttrItalic)
	if got := s.GetAttributes(); got != AttrBold|AttrItalic {
		t.Fatalf("unexpected attributes: %v", got)
	}
}

func TestUnderlineBadTypePanics(t *testing.T) {
	defer func() {
		if recover() == nil {
			t.Fatal("Underline should panic on unsupported parameter type")
		}
	}()

	_ = StyleDefault.Underline("bad")
}