diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 62e6eab..f244d7b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -305,6 +305,12 @@ jobs: if grep -q 'SCRIPT_ERROR:' output.log || grep -q 'Tests none' output.log; then exit 1 fi + - name: Upload screenshots + uses: actions/upload-artifact@v4 + if: failure() + with: + name: failed-screenshots + path: test/visual_regression/screenshots merge-artifacts: name: Merge Artifacts diff --git a/.gitignore b/.gitignore index 2cb4eb2..606c401 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ mono_crash.* .gutconfig.json test/results.xml test/test_metadata.json +test/visual_regression/screenshots/ # GodotXterm-specific ignores .gdxterm diff --git a/Justfile b/Justfile index d21252f..10ace7b 100644 --- a/Justfile +++ b/Justfile @@ -22,7 +22,7 @@ test: {{godot}} --headless -s addons/gut/gut_cmdln.gd -gtest={{test_files}} -gexit test-all: - {{godot}} --windowed --resolution 400x200 --position 0,0 -s addons/gut/gut_cmdln.gd -gdir=res://test -gopacity=0 -gexit + {{godot}} --windowed --resolution 400x200 --position 0,0 -s addons/gut/gut_cmdln.gd -gdir=res://test -ginclude_subdirs=true -gopacity=0 -gexit test-rendering: {{godot}} --windowed --resolution 400x200 --position 0,0 -s addons/gut/gut_cmdln.gd -gtest=res://test/test_rendering.gd -gopacity=0 -gexit diff --git a/plug.gd b/plug.gd index 9cca6cb..9875fa4 100644 --- a/plug.gd +++ b/plug.gd @@ -5,3 +5,4 @@ extends "res://addons/gd-plug/plug.gd" func _plugging(): plug("bitwes/Gut", {tag = "v9.2.0"}) + plug("lihop/godot-pixelmatch", {tag = "v2.0.0", include = ["addons/pixelmatch"]}) diff --git a/test/visual_regression/baseline/.gdignore b/test/visual_regression/baseline/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/test/visual_regression/baseline/default_theme.png b/test/visual_regression/baseline/default_theme.png new file mode 100644 index 0000000..e0d0e25 Binary files /dev/null and b/test/visual_regression/baseline/default_theme.png differ diff --git a/test/visual_regression/baseline/empty.png b/test/visual_regression/baseline/empty.png new file mode 100644 index 0000000..fc9d98d Binary files /dev/null and b/test/visual_regression/baseline/empty.png differ diff --git a/test/visual_regression/baseline/transparency.png b/test/visual_regression/baseline/transparency.png new file mode 100644 index 0000000..c861e6c Binary files /dev/null and b/test/visual_regression/baseline/transparency.png differ diff --git a/test/visual_regression/screenshots/.gdignore b/test/visual_regression/screenshots/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/test/visual_regression/test_visual_regression.gd b/test/visual_regression/test_visual_regression.gd new file mode 100644 index 0000000..d406b6b --- /dev/null +++ b/test/visual_regression/test_visual_regression.gd @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2024 Leroy Hopson +# SPDX-License-Identifier: MIT + +class_name VisualRegressionTest extends RenderingTest + +const Pixelmatch = preload("res://addons/pixelmatch/pixelmatch.gd") +const MenuScene = preload("res://examples/menu/menu.tscn") + +const TERMINAL_SIZE = Vector2i(200, 100) +const UPDATE = false # Set to true when you want to update baseline images. + +var matcher = Pixelmatch.new() + + +func get_described_class(): + return Terminal + + +func before_each(): + await super.before_each() + subject.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT) + subject.call_deferred("set_size", TERMINAL_SIZE) + await wait_for_signal(subject.size_changed, 5) + + +func assert_match(reference: String): + var image = get_viewport().get_texture().get_image() + image.crop(TERMINAL_SIZE.x, TERMINAL_SIZE.y) + var reference_path = "res://test/visual_regression/baseline/%s.png" % reference + + if UPDATE or not FileAccess.file_exists(reference_path): + image.save_png(reference_path) + + var reference_image = Image.new() + reference_image.load(reference_path) + assert(reference_image, "Could not load reference image: " + reference) + var diff_image = Image.create(TERMINAL_SIZE.x, TERMINAL_SIZE.y, false, Image.FORMAT_RGBA8) + var diff = matcher.diff(image, reference_image, diff_image, TERMINAL_SIZE.x, TERMINAL_SIZE.y) + + if diff != 0: + diff_image.save_png("res://test/visual_regression/screenshots/%s.diff.png" % reference) + image.save_png("res://test/visual_regression/screenshots/%s.png" % reference) + + assert_eq(diff, 0, "Screenshot matches baseline image") + + +class TestVisualRegression: + extends VisualRegressionTest + + func test_empty(): + await wait_frames(30) + assert_match("empty") + + func test_default_theme(): + # Print every background color. + for i in range(8): + subject.write("\u001b[4%dm " % i) # Regular. + + # Print every foreground color. + + # Print every font. + for i in range(8): + subject.write("\u001b[10%dm " % i) # Bright. + + # Print every foreground color. + + # Print every font. + subject.write("\u001b[0m") # Reset. + + # Print every foreground color. + for i in range(8): + subject.write("\u001b[3%dm█" % i) # Regular. + + # Print every font. + for i in range(8): + subject.write("\u001b[9%dm█" % i) # Bright. + + # Print every font. + subject.write("\u001b[0m") # Reset. + + # Print every font. + subject.write("L\u001b[0m") # Regular. + subject.write("\u001b[1mL\u001b[0m") # Bold. + subject.write("\u001b[3mL\u001b[0m") # Italic. + subject.write("\u001b[1m\u001b[3mL\u001b[0m") # Bold Italic. + + await wait_frames(30) + assert_match("default_theme") + + func test_transparency(): + subject.add_theme_color_override("foreground_color", Color(0, 1, 0, 0.5)) + subject.add_theme_color_override("background_color", Color(1, 0, 0, 0.5)) + subject.write("bg red, 50% transparency\r\n") + subject.write("fg green, 50% transparency") + await wait_frames(30) + assert_match("transparency")