diff --git a/.gitignore b/.gitignore
index f8fb8b4..a7b6488 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ dist
npm-debug.log
src/livesplit_core.wasm
src/livesplit-core
+
+test/screenshots
diff --git a/test/layouts/all_components.ls1l b/test/layouts/all_components.ls1l
new file mode 100644
index 0000000..bf8ad09
--- /dev/null
+++ b/test/layouts/all_components.ls1l
@@ -0,0 +1,520 @@
+{
+ "components": [
+ {
+ "CurrentComparison": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "display_two_rows": false,
+ "label_color": null,
+ "value_color": null
+ }
+ },
+ {
+ "CurrentPace": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "comparison_override": null,
+ "display_two_rows": false,
+ "label_color": null,
+ "value_color": null,
+ "accuracy": "Seconds"
+ }
+ },
+ {
+ "Delta": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "comparison_override": null,
+ "display_two_rows": false,
+ "label_color": null,
+ "drop_decimals": true,
+ "accuracy": "Tenths"
+ }
+ },
+ {
+ "DetailedTimer": {
+ "background": "Transparent",
+ "comparison1": null,
+ "comparison2": "Best Segments",
+ "hide_second_comparison": false,
+ "timer": {
+ "background": "Transparent",
+ "timing_method": null,
+ "height": 40,
+ "color_override": null,
+ "show_gradient": true,
+ "digits_format": "SingleDigitSeconds",
+ "accuracy": "Hundredths",
+ "is_segment_timer": false
+ },
+ "segment_timer": {
+ "background": "Transparent",
+ "timing_method": null,
+ "height": 25,
+ "color_override": [
+ 0.6666667,
+ 0.6666667,
+ 0.6666667,
+ 1
+ ],
+ "show_gradient": true,
+ "digits_format": "SingleDigitSeconds",
+ "accuracy": "Hundredths",
+ "is_segment_timer": true
+ },
+ "display_icon": false,
+ "show_segment_name": false
+ }
+ },
+ {
+ "Graph": {
+ "comparison_override": null,
+ "show_best_segments": false,
+ "live_graph": true,
+ "flip_graph": false,
+ "behind_background_color": [
+ 0.4509804,
+ 0.15686275,
+ 0.15686275,
+ 1
+ ],
+ "ahead_background_color": [
+ 0.15686275,
+ 0.4509804,
+ 0.20392157,
+ 1
+ ],
+ "grid_lines_color": [
+ 0,
+ 0,
+ 0,
+ 0.15
+ ],
+ "graph_lines_color": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "partial_fill_color": [
+ 1,
+ 1,
+ 1,
+ 0.25
+ ],
+ "complete_fill_color": [
+ 1,
+ 1,
+ 1,
+ 0.4
+ ],
+ "height": 80
+ }
+ },
+ {
+ "PbChance": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "display_two_rows": false,
+ "label_color": null,
+ "value_color": null
+ }
+ },
+ {
+ "PossibleTimeSave": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "comparison_override": null,
+ "display_two_rows": false,
+ "total_possible_time_save": false,
+ "label_color": null,
+ "value_color": null,
+ "accuracy": "Hundredths"
+ }
+ },
+ {
+ "PreviousSegment": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "comparison_override": null,
+ "display_two_rows": false,
+ "label_color": null,
+ "drop_decimals": true,
+ "accuracy": "Tenths",
+ "show_possible_time_save": false
+ }
+ },
+ {
+ "SegmentTime": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "comparison_override": null,
+ "display_two_rows": false,
+ "label_color": null,
+ "value_color": null,
+ "accuracy": "Hundredths"
+ }
+ },
+ {
+ "Splits": {
+ "background": {
+ "Alternating": [
+ [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.04
+ ]
+ ]
+ },
+ "visual_split_count": 16,
+ "split_preview_count": 1,
+ "show_thin_separators": true,
+ "separator_last_split": true,
+ "always_show_last_split": true,
+ "fill_with_blank_space": true,
+ "display_two_rows": false,
+ "current_split_gradient": {
+ "Vertical": [
+ [
+ 0.2,
+ 0.4509804,
+ 0.95686275,
+ 1
+ ],
+ [
+ 0.08235294,
+ 0.20784314,
+ 0.45490196,
+ 1
+ ]
+ ]
+ },
+ "show_column_labels": false,
+ "columns": [
+ {
+ "name": "Time",
+ "start_with": "ComparisonTime",
+ "update_with": "SplitTime",
+ "update_trigger": "OnEndingSegment",
+ "comparison_override": null,
+ "timing_method": null
+ },
+ {
+ "name": "+/−",
+ "start_with": "Empty",
+ "update_with": "Delta",
+ "update_trigger": "Contextual",
+ "comparison_override": null,
+ "timing_method": null
+ }
+ ]
+ }
+ },
+ {
+ "SumOfBest": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "display_two_rows": false,
+ "label_color": null,
+ "value_color": null,
+ "accuracy": "Seconds"
+ }
+ },
+ {
+ "Text": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "display_two_rows": false,
+ "left_center_color": null,
+ "right_color": null,
+ "text": {
+ "Center": ""
+ }
+ }
+ },
+ {
+ "Timer": {
+ "background": "Transparent",
+ "timing_method": null,
+ "height": 60,
+ "color_override": null,
+ "show_gradient": true,
+ "digits_format": "SingleDigitSeconds",
+ "accuracy": "Hundredths",
+ "is_segment_timer": false
+ }
+ },
+ {
+ "Title": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.13
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0
+ ]
+ ]
+ },
+ "text_color": null,
+ "show_game_name": true,
+ "show_category_name": true,
+ "show_finished_runs_count": false,
+ "show_attempt_count": true,
+ "text_alignment": "Auto",
+ "display_as_single_line": false,
+ "display_game_icon": true,
+ "show_region": false,
+ "show_platform": false,
+ "show_variables": true
+ }
+ },
+ {
+ "TotalPlaytime": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "display_two_rows": false,
+ "show_days": true,
+ "label_color": null,
+ "value_color": null
+ }
+ },
+ {
+ "BlankSpace": {
+ "background": "Transparent",
+ "size": 24
+ }
+ },
+ "Separator"
+ ],
+ "general": {
+ "direction": "Vertical",
+ "background": {
+ "Plain": [
+ 0.06,
+ 0.06,
+ 0.06,
+ 1
+ ]
+ },
+ "best_segment_color": [
+ 1,
+ 0.8333333,
+ 0,
+ 1
+ ],
+ "ahead_gaining_time_color": [
+ 0,
+ 0.8,
+ 0.21333332,
+ 1
+ ],
+ "ahead_losing_time_color": [
+ 0.38000003,
+ 0.82000005,
+ 0.49733335,
+ 1
+ ],
+ "behind_gaining_time_color": [
+ 0.82000005,
+ 0.38000003,
+ 0.38000003,
+ 1
+ ],
+ "behind_losing_time_color": [
+ 0.8,
+ 0,
+ 0,
+ 1
+ ],
+ "not_running_color": [
+ 0.67,
+ 0.67,
+ 0.67,
+ 1
+ ],
+ "personal_best_color": [
+ 0.08000004,
+ 0.64733326,
+ 1,
+ 1
+ ],
+ "paused_color": [
+ 0.48,
+ 0.48,
+ 0.48,
+ 1
+ ],
+ "thin_separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.09
+ ],
+ "separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.35
+ ],
+ "text_color": [
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/layouts/default.ls1l b/test/layouts/default.ls1l
new file mode 100644
index 0000000..0271ff7
--- /dev/null
+++ b/test/layouts/default.ls1l
@@ -0,0 +1,212 @@
+{
+ "components": [
+ {
+ "Title": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.13
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0
+ ]
+ ]
+ },
+ "text_color": null,
+ "show_game_name": true,
+ "show_category_name": true,
+ "show_finished_runs_count": false,
+ "show_attempt_count": true,
+ "text_alignment": "Auto",
+ "display_as_single_line": false,
+ "display_game_icon": true,
+ "show_region": false,
+ "show_platform": false,
+ "show_variables": true
+ }
+ },
+ {
+ "Splits": {
+ "background": {
+ "Alternating": [
+ [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.04
+ ]
+ ]
+ },
+ "visual_split_count": 16,
+ "split_preview_count": 1,
+ "show_thin_separators": true,
+ "separator_last_split": true,
+ "always_show_last_split": true,
+ "fill_with_blank_space": true,
+ "display_two_rows": false,
+ "current_split_gradient": {
+ "Vertical": [
+ [
+ 0.2,
+ 0.4509804,
+ 0.95686275,
+ 1
+ ],
+ [
+ 0.08235294,
+ 0.20784314,
+ 0.45490196,
+ 1
+ ]
+ ]
+ },
+ "show_column_labels": false,
+ "columns": [
+ {
+ "name": "Time",
+ "start_with": "ComparisonTime",
+ "update_with": "SplitTime",
+ "update_trigger": "OnEndingSegment",
+ "comparison_override": null,
+ "timing_method": null
+ },
+ {
+ "name": "+/−",
+ "start_with": "Empty",
+ "update_with": "Delta",
+ "update_trigger": "Contextual",
+ "comparison_override": null,
+ "timing_method": null
+ }
+ ]
+ }
+ },
+ {
+ "Timer": {
+ "background": "Transparent",
+ "timing_method": null,
+ "height": 60,
+ "color_override": null,
+ "show_gradient": true,
+ "digits_format": "SingleDigitSeconds",
+ "accuracy": "Hundredths",
+ "is_segment_timer": false
+ }
+ },
+ {
+ "PreviousSegment": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.06
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.005
+ ]
+ ]
+ },
+ "comparison_override": null,
+ "display_two_rows": false,
+ "label_color": null,
+ "drop_decimals": true,
+ "accuracy": "Tenths",
+ "show_possible_time_save": false
+ }
+ }
+ ],
+ "general": {
+ "direction": "Vertical",
+ "background": {
+ "Plain": [
+ 0.06,
+ 0.06,
+ 0.06,
+ 1
+ ]
+ },
+ "best_segment_color": [
+ 1,
+ 0.8333333,
+ 0,
+ 1
+ ],
+ "ahead_gaining_time_color": [
+ 0,
+ 0.8,
+ 0.21333332,
+ 1
+ ],
+ "ahead_losing_time_color": [
+ 0.38000003,
+ 0.82000005,
+ 0.49733335,
+ 1
+ ],
+ "behind_gaining_time_color": [
+ 0.82000005,
+ 0.38000003,
+ 0.38000003,
+ 1
+ ],
+ "behind_losing_time_color": [
+ 0.8,
+ 0,
+ 0,
+ 1
+ ],
+ "not_running_color": [
+ 0.67,
+ 0.67,
+ 0.67,
+ 1
+ ],
+ "personal_best_color": [
+ 0.08000004,
+ 0.64733326,
+ 1,
+ 1
+ ],
+ "paused_color": [
+ 0.48,
+ 0.48,
+ 0.48,
+ 1
+ ],
+ "thin_separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.09
+ ],
+ "separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.35
+ ],
+ "text_color": [
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/layouts/splits_two_rows.ls1l b/test/layouts/splits_two_rows.ls1l
new file mode 100644
index 0000000..687f20a
--- /dev/null
+++ b/test/layouts/splits_two_rows.ls1l
@@ -0,0 +1,143 @@
+{
+ "components": [
+ {
+ "Splits": {
+ "background": {
+ "Alternating": [
+ [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.04
+ ]
+ ]
+ },
+ "visual_split_count": 12,
+ "split_preview_count": 1,
+ "show_thin_separators": true,
+ "separator_last_split": true,
+ "always_show_last_split": true,
+ "fill_with_blank_space": true,
+ "display_two_rows": true,
+ "current_split_gradient": {
+ "Vertical": [
+ [
+ 0.2,
+ 0.4509804,
+ 0.95686275,
+ 1
+ ],
+ [
+ 0.08235294,
+ 0.20784314,
+ 0.45490196,
+ 1
+ ]
+ ]
+ },
+ "show_column_labels": false,
+ "columns": [
+ {
+ "name": "Time",
+ "start_with": "ComparisonTime",
+ "update_with": "SplitTime",
+ "update_trigger": "OnEndingSegment",
+ "comparison_override": null,
+ "timing_method": null
+ },
+ {
+ "name": "+/−",
+ "start_with": "Empty",
+ "update_with": "Delta",
+ "update_trigger": "Contextual",
+ "comparison_override": null,
+ "timing_method": null
+ }
+ ]
+ }
+ }
+ ],
+ "general": {
+ "direction": "Vertical",
+ "background": {
+ "Plain": [
+ 0.06,
+ 0.06,
+ 0.06,
+ 1
+ ]
+ },
+ "best_segment_color": [
+ 1,
+ 0.8333333,
+ 0,
+ 1
+ ],
+ "ahead_gaining_time_color": [
+ 0,
+ 0.8,
+ 0.21333332,
+ 1
+ ],
+ "ahead_losing_time_color": [
+ 0.38000003,
+ 0.82000005,
+ 0.49733335,
+ 1
+ ],
+ "behind_gaining_time_color": [
+ 0.82000005,
+ 0.38000003,
+ 0.38000003,
+ 1
+ ],
+ "behind_losing_time_color": [
+ 0.8,
+ 0,
+ 0,
+ 1
+ ],
+ "not_running_color": [
+ 0.67,
+ 0.67,
+ 0.67,
+ 1
+ ],
+ "personal_best_color": [
+ 0.08000004,
+ 0.64733326,
+ 1,
+ 1
+ ],
+ "paused_color": [
+ 0.48,
+ 0.48,
+ 0.48,
+ 1
+ ],
+ "thin_separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.09
+ ],
+ "separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.35
+ ],
+ "text_color": [
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/layouts/splits_with_labels.ls1l b/test/layouts/splits_with_labels.ls1l
new file mode 100644
index 0000000..271232a
--- /dev/null
+++ b/test/layouts/splits_with_labels.ls1l
@@ -0,0 +1,143 @@
+{
+ "components": [
+ {
+ "Splits": {
+ "background": {
+ "Alternating": [
+ [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0.04
+ ]
+ ]
+ },
+ "visual_split_count": 3,
+ "split_preview_count": 1,
+ "show_thin_separators": true,
+ "separator_last_split": true,
+ "always_show_last_split": true,
+ "fill_with_blank_space": true,
+ "display_two_rows": false,
+ "current_split_gradient": {
+ "Vertical": [
+ [
+ 0.2,
+ 0.4509804,
+ 0.95686275,
+ 1
+ ],
+ [
+ 0.08235294,
+ 0.20784314,
+ 0.45490196,
+ 1
+ ]
+ ]
+ },
+ "show_column_labels": true,
+ "columns": [
+ {
+ "name": "Time",
+ "start_with": "ComparisonTime",
+ "update_with": "SplitTime",
+ "update_trigger": "OnEndingSegment",
+ "comparison_override": null,
+ "timing_method": null
+ },
+ {
+ "name": "+/−",
+ "start_with": "Empty",
+ "update_with": "Delta",
+ "update_trigger": "Contextual",
+ "comparison_override": null,
+ "timing_method": null
+ }
+ ]
+ }
+ }
+ ],
+ "general": {
+ "direction": "Vertical",
+ "background": {
+ "Plain": [
+ 0.06,
+ 0.06,
+ 0.06,
+ 1
+ ]
+ },
+ "best_segment_color": [
+ 1,
+ 0.8333333,
+ 0,
+ 1
+ ],
+ "ahead_gaining_time_color": [
+ 0,
+ 0.8,
+ 0.21333332,
+ 1
+ ],
+ "ahead_losing_time_color": [
+ 0.38000003,
+ 0.82000005,
+ 0.49733335,
+ 1
+ ],
+ "behind_gaining_time_color": [
+ 0.82000005,
+ 0.38000003,
+ 0.38000003,
+ 1
+ ],
+ "behind_losing_time_color": [
+ 0.8,
+ 0,
+ 0,
+ 1
+ ],
+ "not_running_color": [
+ 0.67,
+ 0.67,
+ 0.67,
+ 1
+ ],
+ "personal_best_color": [
+ 0.08000004,
+ 0.64733326,
+ 1,
+ 1
+ ],
+ "paused_color": [
+ 0.48,
+ 0.48,
+ 0.48,
+ 1
+ ],
+ "thin_separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.09
+ ],
+ "separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.35
+ ],
+ "text_color": [
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/layouts/title_centered_no_game_icon.ls1l b/test/layouts/title_centered_no_game_icon.ls1l
new file mode 100644
index 0000000..7319aa5
--- /dev/null
+++ b/test/layouts/title_centered_no_game_icon.ls1l
@@ -0,0 +1,112 @@
+{
+ "components": [
+ {
+ "Title": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.13
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0
+ ]
+ ]
+ },
+ "text_color": null,
+ "show_game_name": true,
+ "show_category_name": true,
+ "show_finished_runs_count": false,
+ "show_attempt_count": true,
+ "text_alignment": "Center",
+ "display_as_single_line": false,
+ "display_game_icon": false,
+ "show_region": false,
+ "show_platform": false,
+ "show_variables": true
+ }
+ }
+ ],
+ "general": {
+ "direction": "Vertical",
+ "background": {
+ "Plain": [
+ 0.06,
+ 0.06,
+ 0.06,
+ 1
+ ]
+ },
+ "best_segment_color": [
+ 1,
+ 0.8333333,
+ 0,
+ 1
+ ],
+ "ahead_gaining_time_color": [
+ 0,
+ 0.8,
+ 0.21333332,
+ 1
+ ],
+ "ahead_losing_time_color": [
+ 0.38000003,
+ 0.82000005,
+ 0.49733335,
+ 1
+ ],
+ "behind_gaining_time_color": [
+ 0.82000005,
+ 0.38000003,
+ 0.38000003,
+ 1
+ ],
+ "behind_losing_time_color": [
+ 0.8,
+ 0,
+ 0,
+ 1
+ ],
+ "not_running_color": [
+ 0.67,
+ 0.67,
+ 0.67,
+ 1
+ ],
+ "personal_best_color": [
+ 0.08000004,
+ 0.64733326,
+ 1,
+ 1
+ ],
+ "paused_color": [
+ 0.48,
+ 0.48,
+ 0.48,
+ 1
+ ],
+ "thin_separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.09
+ ],
+ "separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.35
+ ],
+ "text_color": [
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/layouts/title_centered_with_game_icon.ls1l b/test/layouts/title_centered_with_game_icon.ls1l
new file mode 100644
index 0000000..26e246e
--- /dev/null
+++ b/test/layouts/title_centered_with_game_icon.ls1l
@@ -0,0 +1,112 @@
+{
+ "components": [
+ {
+ "Title": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.13
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0
+ ]
+ ]
+ },
+ "text_color": null,
+ "show_game_name": true,
+ "show_category_name": true,
+ "show_finished_runs_count": false,
+ "show_attempt_count": true,
+ "text_alignment": "Center",
+ "display_as_single_line": false,
+ "display_game_icon": true,
+ "show_region": false,
+ "show_platform": false,
+ "show_variables": true
+ }
+ }
+ ],
+ "general": {
+ "direction": "Vertical",
+ "background": {
+ "Plain": [
+ 0.06,
+ 0.06,
+ 0.06,
+ 1
+ ]
+ },
+ "best_segment_color": [
+ 1,
+ 0.8333333,
+ 0,
+ 1
+ ],
+ "ahead_gaining_time_color": [
+ 0,
+ 0.8,
+ 0.21333332,
+ 1
+ ],
+ "ahead_losing_time_color": [
+ 0.38000003,
+ 0.82000005,
+ 0.49733335,
+ 1
+ ],
+ "behind_gaining_time_color": [
+ 0.82000005,
+ 0.38000003,
+ 0.38000003,
+ 1
+ ],
+ "behind_losing_time_color": [
+ 0.8,
+ 0,
+ 0,
+ 1
+ ],
+ "not_running_color": [
+ 0.67,
+ 0.67,
+ 0.67,
+ 1
+ ],
+ "personal_best_color": [
+ 0.08000004,
+ 0.64733326,
+ 1,
+ 1
+ ],
+ "paused_color": [
+ 0.48,
+ 0.48,
+ 0.48,
+ 1
+ ],
+ "thin_separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.09
+ ],
+ "separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.35
+ ],
+ "text_color": [
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/layouts/title_left_no_attempt_count.ls1l b/test/layouts/title_left_no_attempt_count.ls1l
new file mode 100644
index 0000000..ba031c9
--- /dev/null
+++ b/test/layouts/title_left_no_attempt_count.ls1l
@@ -0,0 +1,112 @@
+{
+ "components": [
+ {
+ "Title": {
+ "background": {
+ "Vertical": [
+ [
+ 1,
+ 1,
+ 1,
+ 0.13
+ ],
+ [
+ 1,
+ 1,
+ 1,
+ 0
+ ]
+ ]
+ },
+ "text_color": null,
+ "show_game_name": true,
+ "show_category_name": true,
+ "show_finished_runs_count": false,
+ "show_attempt_count": false,
+ "text_alignment": "Left",
+ "display_as_single_line": false,
+ "display_game_icon": false,
+ "show_region": false,
+ "show_platform": false,
+ "show_variables": true
+ }
+ }
+ ],
+ "general": {
+ "direction": "Vertical",
+ "background": {
+ "Plain": [
+ 0.06,
+ 0.06,
+ 0.06,
+ 1
+ ]
+ },
+ "best_segment_color": [
+ 1,
+ 0.8333333,
+ 0,
+ 1
+ ],
+ "ahead_gaining_time_color": [
+ 0,
+ 0.8,
+ 0.21333332,
+ 1
+ ],
+ "ahead_losing_time_color": [
+ 0.38000003,
+ 0.82000005,
+ 0.49733335,
+ 1
+ ],
+ "behind_gaining_time_color": [
+ 0.82000005,
+ 0.38000003,
+ 0.38000003,
+ 1
+ ],
+ "behind_losing_time_color": [
+ 0.8,
+ 0,
+ 0,
+ 1
+ ],
+ "not_running_color": [
+ 0.67,
+ 0.67,
+ 0.67,
+ 1
+ ],
+ "personal_best_color": [
+ 0.08000004,
+ 0.64733326,
+ 1,
+ 1
+ ],
+ "paused_color": [
+ 0.48,
+ 0.48,
+ 0.48,
+ 1
+ ],
+ "thin_separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.09
+ ],
+ "separators_color": [
+ 1,
+ 1,
+ 1,
+ 0.35
+ ],
+ "text_color": [
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/rendering-test.js b/test/rendering-test.js
new file mode 100644
index 0000000..346789e
--- /dev/null
+++ b/test/rendering-test.js
@@ -0,0 +1,133 @@
+const fs = require("fs");
+const path = require("path");
+const { fork } = require("child_process");
+const {
+ Builder,
+ By,
+ until,
+} = require("selenium-webdriver");
+const chrome = require("selenium-webdriver/chrome");
+const chromedriver = require("chromedriver");
+const imghash = require("imghash");
+const hex64 = require("hex64");
+const pixelmatch = require("pixelmatch");
+const PNG = require("pngjs").PNG;
+
+describe("Layout Rendering Tests", () => {
+ let driver, serverProcess;
+
+ const LAYOUTS_FOLDER = "test/layouts";
+ const SCREENSHOTS_FOLDER = "test/screenshots";
+ const SPLITS_FOLDER = "test/splits";
+
+ const findElement = async (selector) => {
+ return driver.wait(until.elementLocated(selector));
+ };
+
+ const clickElement = async (selector) => {
+ const element = await driver.wait(until.elementLocated(selector));
+ await element.click();
+ }
+
+ const loadFile = async (filePath) => {
+ const inputElement = await findElement(By.id("file-input"));
+ await inputElement.sendKeys(path.resolve(filePath));
+ }
+
+ before(async () => {
+ serverProcess = fork("./node_modules/webpack-dev-server/bin/webpack-dev-server.js");
+
+ const service = new chrome.ServiceBuilder(chromedriver.path).build();
+ chrome.setDefaultService(service);
+ const options = new chrome.Options().windowSize({ width: 1200, height: 2400 }).headless();
+ driver = await new Builder().forBrowser("chrome").setChromeOptions(options).build();
+
+ await driver.get("http://localhost:8080");
+
+ await driver.executeScript(() => {
+ HTMLInputElement.prototype.click = function() {
+ const existingFileInput = document.getElementById("file-input");
+ if (existingFileInput) {
+ existingFileInput.remove();
+ }
+ this.style.display = "none";
+ this.id = "file-input";
+ document.body.appendChild(this);
+ }
+ });
+
+ if (!fs.existsSync(SCREENSHOTS_FOLDER)) {
+ fs.mkdirSync(SCREENSHOTS_FOLDER);
+ }
+ });
+
+ const testRendering = (layoutName, splitsName, expectedHash) => {
+ it(`Renders the ${layoutName} layout with the ${splitsName} splits correctly`, async () => {
+ await clickElement(By.xpath(".//button[contains(text(), 'Layout')]"));
+ await clickElement(By.xpath(".//button[contains(text(), 'Import')]"));
+ await loadFile(`${LAYOUTS_FOLDER}/${layoutName}.ls1l`);
+ await clickElement(By.xpath(".//button[contains(text(), 'Back')]"));
+
+ await clickElement(By.xpath(".//button[contains(text(), 'Splits')]"));
+ await clickElement(By.xpath(".//button[contains(text(), 'Import')]"));
+ await loadFile(`${SPLITS_FOLDER}/${splitsName}.lss`);
+ await clickElement(By.xpath(".//button[contains(text(), 'Back')]"));
+
+ const layoutElement = await findElement(By.className("layout"));
+ const layoutScreenshot = await layoutElement.takeScreenshot();
+
+ const tempFilePath = `${SCREENSHOTS_FOLDER}/${layoutName}.png`;
+
+ fs.writeFileSync(tempFilePath, layoutScreenshot, "base64");
+ const actualHashHex = await imghash.hash(tempFilePath, 12);
+ const actualHash = hex64.encode(actualHashHex);
+
+ const actualScreenshotPath = `${SCREENSHOTS_FOLDER}/${layoutName}_${splitsName}_${actualHash.replace("/", "$")}.png`;
+ const expectedScreenshotPath = `${SCREENSHOTS_FOLDER}/${layoutName}_${splitsName}_${expectedHash.replace("/", "$")}.png`;
+
+ fs.renameSync(tempFilePath, actualScreenshotPath);
+
+ if (actualHash !== expectedHash) {
+ let showWarning = false;
+ try {
+ if (fs.existsSync(expectedScreenshotPath)) {
+ const actualImage = PNG.sync.read(fs.readFileSync(actualScreenshotPath));
+ const expectedImage = PNG.sync.read(fs.readFileSync(expectedScreenshotPath));
+ const { width, height } = actualImage;
+ const diff = new PNG({ width, height });
+
+ const numPixelsDifferent = pixelmatch(actualImage.data, expectedImage.data, diff.data, width, height);
+
+ if (numPixelsDifferent === 0) {
+ showWarning = true;
+ }
+
+ fs.writeFileSync(`${SCREENSHOTS_FOLDER}/${layoutName}_${splitsName}_diff.png`, PNG.sync.write(diff));
+ }
+ }
+ finally {
+ if (showWarning) {
+ console.warn(`Render match despite mismatching hashes! Expected hash: ${expectedHash}, actual hash: ${actualHash}`);
+ } else {
+ throw Error(`Render mismatch! Expected hash: ${expectedHash}, actual hash: ${actualHash}`)
+ }
+ }
+ }
+ });
+ }
+
+ testRendering("all_components", "default", "8AAD____-BAAAAAAAALABv_j");
+ testRendering("all_components", "pmw3", "8AAD__8A-B_DuDvDLDACRP-_");
+ testRendering("default", "default", "__wBAAAAAAAAAAAAAAAAAP__");
+ testRendering("default", "pmw3", "_4uBiDvDODPDvDLDPDrDAP-H");
+ testRendering("splits_two_rows", "celeste", "-D-D0B_DyB8D0B_D1D_jxDwD");
+ testRendering("splits_with_labels", "celeste", "AKAL__8D8DkAgBsD-D6D-DOD");
+ testRendering("title_centered_no_game_icon", "celeste", "__BgDw__BwAAAwDxDx__DwAA");
+ testRendering("title_centered_with_game_icon", "celeste", "__AwRw__wwwAwYx5x5__x4AA");
+ testRendering("title_left_no_attempt_count", "celeste", "__4A4A__4AAAMA8A8A__cAAA");
+
+ after(async () => {
+ await driver.quit();
+ serverProcess.kill();
+ });
+});
diff --git a/test/splits/celeste.lss b/test/splits/celeste.lss
new file mode 100644
index 0000000..7749488
--- /dev/null
+++ b/test/splits/celeste.lss
@@ -0,0 +1 @@
+CelesteAny%PC1.2.1.500:00:00.00000003201:04:31.170000001:00:42.573000000:59:28.965000000:55:54.763000000:47:58.618000000:44:33.114000000:50:12.448000000:46:37.826000000:47:10.743000000:43:48.642000000:44:14.143000000:40:58.659000000:42:55.145000000:39:40.986000000:41:28.546000000:38:16.768000000:40:44.782000000:37:33.299000000:39:12.517000000:36:05.5790000Prologue00:00:46.769000000:00:44.404000000:00:45.237000000:00:43.6390000-Crossing00:01:20.664000000:01:06.096000000:00:31.977000000:00:20.2130000-Chasm00:01:49.624000000:01:34.009999000:00:28.960000000:00:27.9140000Forsaken City00:02:14.850000000:01:58.864000000:00:25.226000000:00:24.8540000-Intervention00:03:14.457000000:02:39.545000000:00:57.463000000:00:38.4030000-Awake00:04:30.012000000:03:52.781000000:01:13.782000000:01:10.9240000Old Site00:04:46.003000000:04:08.404000000:00:14.608000000:00:14.1780000-Huge Mess00:06:32.490000000:05:36.345000000:01:41.681000000:01:21.2430000-Elevator Shaft00:08:49.245000000:07:49.421000000:02:09.690000000:02:08.0099990-Presidential Suite00:09:50.333000000:08:48.665999000:00:56.622000000:00:55.3690000Celestial Resort00:10:59.555000000:09:54.728000000:01:02.398000000:00:59.4320000-Shrine00:12:14.659000000:10:48.703000000:01:09.932000000:00:49.0620000-Old Trial00:12:45.035000000:11:18.810000000:00:27.924000000:00:27.7100000-Cliff Face00:13:57.312000000:12:30.074000000:01:11.829000000:01:10.3120000Golden Ridge00:14:59.560000000:13:30.084000000:01:01.224000000:00:58.5480000-Depths00:15:55.469000000:14:06.396000000:00:55.909000000:00:36.3120000-Unravelling00:17:12.483000000:15:22.709000000:01:16.131000000:01:14.9700000-Search00:18:23.249000000:16:31.525000000:01:10.766000000:01:08.8160000-Rescue00:19:18.915000000:17:25.500000000:00:49.846000000:00:49.0620000Mirror Temple00:21:07.225000000:19:11.937000000:01:30.570000000:01:29.1480000-Lake00:21:46.404000000:19:32.609000000:00:39.179000000:00:20.6720000-Hollows00:22:25.601000000:20:10.451000000:00:39.197000000:00:37.8420000-Reflection00:24:24.105000000:22:05.218000000:01:54.336000000:01:51.0270000-Rock Bottom00:25:12.998000000:22:52.495000000:00:48.893000000:00:47.2770000-Resolution00:28:04.506000000:25:36.443000000:02:51.508000000:02:43.9480000Reflection00:28:28.494000000:26:00.226000000:00:21.310000000:00:21.1820000-500M00:29:34.947000000:26:49.713000000:01:01.628000000:00:45.2710000-1000M00:30:30.753000000:27:43.875000000:00:55.806000000:00:54.1620000-1500M00:31:27.444000000:28:36.303000000:00:56.691000000:00:52.4280000-2000M00:32:38.059000000:29:44.303000000:01:10.615000000:01:08.0000000-2500M00:34:15.286999000:31:18.279000000:01:37.228000000:01:33.9760000-3000M00:35:40.926000000:32:40.661000000:01:25.639000000:01:22.3820000The Summit00:39:12.517000000:36:05.579000000:03:31.591000000:03:24.9180000PrologueChapter1Checkpoint1Chapter1Checkpoint2Chapter1Chapter2Checkpoint1Chapter2Checkpoint2Chapter2Chapter3Checkpoint1Chapter3Checkpoint2Chapter3Checkpoint3Chapter3Chapter4Checkpoint1Chapter4Checkpoint2Chapter4Checkpoint3Chapter4Chapter5Checkpoint1Chapter5Checkpoint2Chapter5Checkpoint3Chapter5Checkpoint4Chapter5Chapter6Checkpoint1Chapter6Checkpoint2Chapter6Checkpoint3Chapter6Checkpoint4Chapter6Checkpoint5Chapter6Chapter7Checkpoint1Chapter7Checkpoint2Chapter7Checkpoint3Chapter7Checkpoint4Chapter7Checkpoint5Chapter7Checkpoint6Chapter7Epilogue
\ No newline at end of file
diff --git a/test/splits/default.lss b/test/splits/default.lss
new file mode 100644
index 0000000..2dff81a
--- /dev/null
+++ b/test/splits/default.lss
@@ -0,0 +1 @@
+GameCategory00:00:00.00000000Time
\ No newline at end of file
diff --git a/test/splits/pmw3.lss b/test/splits/pmw3.lss
new file mode 100644
index 0000000..1523fd1
--- /dev/null
+++ b/test/splits/pmw3.lss
@@ -0,0 +1 @@
+Pac-Man World 3Any%PCUSA / NTSCEasyConsole/PC00:00:00.000000027002:31:48.750000002:14:45.469000002:03:12.442000002:01:46.219000001:56:59.463000001:54:35.416000001:53:00.292000001:51:31.969000001:49:45.527000001:47:07.064000001:46:34.242000001:45:48.406000001:45:12.156000001:45:04.803000001:44:28.952000001:43:29.314000001:42:55.224000001:45:20.238000001:44:45.611000001:39:54.092000001:39:29.106000001:38:09.506000001:37:37.452000001:41:36.143000001:41:00.570000001:43:23.296000001:42:40.970000001:35:29.150000001:35:00.043000001:34:54.273000001:34:22.109000001:34:20.966000001:33:54.962000001:34:44.555000001:34:12.337000001:34:13.526000001:33:42.968000001:34:05.269000001:33:36.712000001:32:24.213000001:31:58.061000001:34:52.158000001:34:18.401000001:38:04.451000001:37:25.418000001:35:30.995000001:34:45.432000001:31:20.925000001:30:46.918000001:29:47.059000001:29:13.170000001:29:20.978000001:28:50.672000001:30:27.544000001:29:50.350000001:29:24.791000001:28:51.061000001:29:55.958000001:29:19.603000001:30:07.321000001:29:32.766000001:29:22.271000001:28:55.219000001:28:33.720000001:28:06.441000001:34:54.658000001:34:12.620000001:33:13.440000001:32:23.871000001:33:36.570000001:32:51.576000001:32:13.766000001:31:39.558000001:32:26.898000001:31:47.983000001:33:12.723000001:32:24.265000001:29:34.189000001:28:52.675000001:28:21.506000001:27:47.488000001:26:46.925000001:26:08.172000001:26:33.933000001:25:55.233000001:23:34.585000001:23:00.2630000Bot Boneyard00:01:57.969000000:01:55.818000000:01:54.505000000:01:52.5410000Banni Wastelands00:06:26.113000000:06:23.026000000:04:11.169000000:04:10.2890000The Spectral Cliffs00:12:55.180000000:12:49.276000000:06:29.067000000:06:26.2500000Gogekka Central00:19:25.296000000:19:17.310000000:06:30.116000000:06:28.0340000The Spectral Vale00:25:49.512000000:25:39.429000000:05:47.618000000:05:46.5240000Zephyr Heights00:33:59.265000000:33:44.124000000:07:51.283000000:07:46.2090000Ancient Catacombs00:38:37.463000000:38:20.200000000:04:07.738000000:04:06.7680000Gogekka Heights00:45:40.276000000:45:20.902000000:06:28.814000000:06:27.8250000Banni Canyon00:52:04.069000000:51:42.654000000:06:23.793000000:06:21.7520000Toc-Man Battle00:54:50.445000000:54:28.166000000:02:46.287000000:02:45.4189990Cragstone Bridge00:59:08.122000000:58:44.759000000:04:17.677000000:04:16.5930000Erwin's Fortress01:06:40.317000001:06:14.793000000:07:32.195000000:07:30.0340000Dungen Gunden01:13:16.366000001:12:45.737000000:06:36.049000000:06:30.9440000The Spectral Zenith01:20:31.371000001:19:58.646000000:07:12.519000000:07:10.2650000Shadow Temple01:23:34.585000001:23:00.263000000:03:03.214000000:03:01.61700001.5C:\Users\Sergey\Downloads\LiveSplit_1.7.5\Components\LiveSplit.PMW3.aslTrueTrueTrueFalse
\ No newline at end of file