Skip to content

Commit 42e8a51

Browse files
Test new auto contrast feature
1 parent 13e54b9 commit 42e8a51

3 files changed

Lines changed: 201 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
From: root@localhost
2+
To: rt@example.com
3+
Subject: Inline span colors that clash with dark theme
4+
Date: Mon, 5 Jan 2026 09:00:00 -0500
5+
Message-ID: <html-inline-span-colors@example.com>
6+
MIME-Version: 1.0
7+
Content-Type: text/html; charset="UTF-8"
8+
Content-Transfer-Encoding: 8bit
9+
10+
<p><span style="background-color:transparent;color:#222222;font-size:10pt;">Hi,</span></p>
11+
<p><span style="background-color:transparent;color:#000000;font-size:10pt;">This message uses the inline-span color pattern produced by several common webmail clients: each text run is wrapped in a &lt;span&gt; whose style sets color (and sometimes background-color) directly.</span></p>
12+
<p><span style="background-color:#ffffff;color:#000000;font-size:10pt;"><strong>Field label with explicit white background:</strong></span></p>
13+
<p><span style="background-color:#ffffff;color:#000000;font-size:10pt;"><strong>Another field with explicit white background:</strong></span></p>
14+
<ol>
15+
<li><span style="background-color:transparent;color:#222222;font-size:10pt;">A numbered item with dark text on transparent background.</span> <span style="background-color:transparent;color:#0000ff;font-size:10pt;">Followed by a blue annotation on transparent background.</span></li>
16+
<li><span style="background-color:transparent;color:#000000;font-size:10pt;">A second numbered item with near-black text on transparent background.</span></li>
17+
</ol>
18+
<p><span style="background-color:transparent;color:#222222;font-size:10pt;">Thanks,</span></p>
19+
<p><span style="background-color:transparent;color:#222222;font-size:10pt;">A Sender</span></p>
20+
<p>&nbsp;</p>
21+
<p>On Fri, 2 Jan 2026 15:00:00 -0500, recipient@example.com wrote:</p>
22+
<blockquote>
23+
<p>Original message with no inline colors.</p>
24+
<p>Should inherit RT's theme text color and render normally in both themes.</p>
25+
</blockquote>

t/playwright/ticket_display.t

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,76 @@ $p->{page}->locator('[data-show-label="Show unset fields"]')->click;
4646
ok( $p->{page}->locator('div.date.created')->isVisible, 'Created date is still visible' );
4747
ok( $p->{page}->locator('div.date.starts')->isVisible, 'Starts date is visible' );
4848

49+
diag "Transaction auto-contrast scanner";
50+
{
51+
my $ac_ticket = RT::Test->create_ticket(
52+
Queue => 'General',
53+
Subject => 'Auto-contrast test',
54+
ContentType => 'text/html',
55+
Content => '<p style="color: #222222;">Dark text that is unreadable in dark mode</p>',
56+
);
57+
my $ac_id = $ac_ticket->Id;
58+
my $page = $p->{page};
59+
60+
$p->goto_ticket($ac_id);
61+
$p->wait_for_element('.transaction .messagebody');
62+
63+
is( $page->locator('.history-container[data-auto-contrast="1"]')->count, 1,
64+
'history-container emits data-auto-contrast=1' );
65+
66+
ok( !$page->locator('.transaction .messagebody.auto-contrast')->count,
67+
'in default (light) theme, messagebody has no .auto-contrast' );
68+
69+
# Flip to dark theme via MutationObserver path and give the re-scan a tick.
70+
$page->evaluate(q{document.documentElement.setAttribute('data-bs-theme', 'dark');});
71+
$p->wait_for_element('.transaction .messagebody.auto-contrast');
72+
ok( $page->locator('.transaction .messagebody.auto-contrast')->count,
73+
'after flipping to dark theme, messagebody gains .auto-contrast' );
74+
75+
# Manual button override: removes auto, pins with contrast-user-original.
76+
$page->locator('.toggle-contrast-link')->first->click;
77+
$p->wait_for_element('.transaction .messagebody.contrast-user-original');
78+
ok( !$page->locator('.transaction .messagebody.auto-contrast')->count,
79+
'clicking manual button removes .auto-contrast' );
80+
ok( $page->locator('.transaction .messagebody.contrast-user-original')->count,
81+
'clicking manual button adds .contrast-user-original' );
82+
83+
# Theme round-trip: the MutationObserver strips .auto-contrast from
84+
# non-pinned transactions and re-scans; pinned messagebodies must
85+
# stay unflipped.
86+
$page->evaluate(q{document.documentElement.setAttribute('data-bs-theme', 'light');});
87+
$p->wait_for_element('.transaction .messagebody.contrast-user-original');
88+
$page->evaluate(q{document.documentElement.setAttribute('data-bs-theme', 'dark');});
89+
$p->wait_for_element('.transaction .messagebody.contrast-user-original');
90+
ok( !$page->locator('.transaction .messagebody.auto-contrast')->count,
91+
'after theme round-trip, user-pinned transaction stays without .auto-contrast' );
92+
93+
# Disable the config and assert the scanner is inert.
94+
my $cfg = RT::Configuration->new( RT->SystemUser );
95+
my ( $r, $msg ) = $cfg->Create(
96+
Name => 'TransactionAutoContrast',
97+
Content => 0,
98+
);
99+
ok( $r, "Set TransactionAutoContrast=0: $msg" );
100+
101+
$p->goto_ticket($ac_id);
102+
$p->wait_for_element('.transaction .messagebody');
103+
is( $page->locator('.history-container[data-auto-contrast="0"]')->count, 1,
104+
'history-container emits data-auto-contrast=0 when config disabled' );
105+
106+
$page->evaluate(q{document.documentElement.setAttribute('data-bs-theme', 'dark');});
107+
# Give the MutationObserver a tick; if the scanner were going to
108+
# apply the class it would have by now.
109+
my $delay = $page->waitForTimeout(200);
110+
$p->{handle}->await($delay);
111+
ok( !$page->locator('.transaction .messagebody.auto-contrast')->count,
112+
'with config disabled, scanner does not apply .auto-contrast even in dark theme' );
113+
114+
my $row = RT::Configuration->new( RT->SystemUser );
115+
$row->LoadByCols( Name => 'TransactionAutoContrast' );
116+
$row->Delete if $row->Id;
117+
}
118+
49119
$p->logout;
50120

51121
done_testing;

t/web/scrubber.t

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use strict;
22
use warnings;
33

4+
use File::Spec;
45
use RT::Test tests => undef;
56

67
my $ticket = RT::Test->create_ticket(
@@ -30,4 +31,109 @@ ok( $ret, 'Updated config' );
3031
$m->reload;
3132
$m->content_contains('<img src="https://example.com/test.png">', 'Remote images are shown with ShowRemoteImages=1');
3233

34+
# Tests covering the inputs the client-side auto-contrast scanner relies on:
35+
# colors set on email HTML must survive scrubbing + CSS::Inliner, and the
36+
# history container must emit data-auto-contrast reflecting the config.
37+
38+
diag "Inline color style survives into the rendered messagebody";
39+
{
40+
my $t = RT::Test->create_ticket(
41+
Queue => 'General',
42+
Subject => 'auto-contrast inline color',
43+
ContentType => 'text/html',
44+
Content => '<p style="color: #222222;">Dark text that is unreadable in dark mode</p>',
45+
);
46+
$m->goto_ticket( $t->Id );
47+
$m->content_contains( 'color: #222222',
48+
'rendered messagebody retains inline color style' );
49+
}
50+
51+
diag "Stylesheet block gets inlined by CSS::Inliner";
52+
{
53+
my $t = RT::Test->create_ticket(
54+
Queue => 'General',
55+
Subject => 'auto-contrast style block',
56+
ContentType => 'text/html',
57+
Content => '<style>p { color: #333333; }</style><p>Stylesheet-driven dark text</p>',
58+
);
59+
$m->goto_ticket( $t->Id );
60+
$m->content_contains( 'color: #333333',
61+
'CSS::Inliner pushed stylesheet color onto the element style attribute' );
62+
}
63+
64+
diag "An explicit inline background-color survives alongside a paired color";
65+
{
66+
my $t = RT::Test->create_ticket(
67+
Queue => 'General',
68+
Subject => 'auto-contrast own-bg',
69+
ContentType => 'text/html',
70+
Content => '<p style="color: #111111; background-color: #dddddd;">Both colors set inline</p>',
71+
);
72+
$m->goto_ticket( $t->Id );
73+
$m->content_contains( 'background-color: #dddddd',
74+
'inline background-color is not scrubbed' );
75+
$m->content_contains( 'color: #111111',
76+
'paired inline color is not scrubbed' );
77+
}
78+
79+
diag "History container emits data-auto-contrast=1 by default";
80+
{
81+
$m->goto_ticket( $ticket->Id );
82+
$m->content_contains( 'data-auto-contrast="1"',
83+
'history-container has data-auto-contrast=1 by default' );
84+
}
85+
86+
diag "Real-world fixture: inline span color pattern from webmail senders";
87+
{
88+
my $path = RT::Test::get_relocatable_file(
89+
'html-inline-span-colors',
90+
( File::Spec->updir(), 'data', 'emails' ),
91+
);
92+
my $mail = RT::Test->file_content($path);
93+
my ( $status, $id ) = RT::Test->send_via_mailgate($mail);
94+
is( $status >> 8, 0, 'mail gateway accepted the fixture' );
95+
ok( $id, "created ticket $id from fixture" );
96+
97+
$m->goto_ticket($id);
98+
99+
# Span-level colors on transparent bg — the pattern that triggers the
100+
# scanner in dark mode (but renders fine in light mode).
101+
$m->content_contains( 'color:#222222',
102+
'span-on-transparent #222 color survives into rendered messagebody' );
103+
$m->content_contains( 'color:#0000ff',
104+
'span blue annotation color survives into rendered messagebody' );
105+
106+
# Span-level white background with dark text — should never trigger the
107+
# scanner regardless of theme because the inner bg/fg pair is high-contrast.
108+
$m->content_contains( 'background-color:#ffffff',
109+
'span-level explicit white background survives scrubbing' );
110+
$m->content_contains( 'color:#000000',
111+
'span-level explicit black text color survives scrubbing' );
112+
113+
# The transparent-bg spans also need to keep their transparency so the
114+
# scanner walks up to the theme bg instead of stopping at the span.
115+
$m->content_contains( 'background-color:transparent',
116+
'span-level transparent background survives scrubbing' );
117+
}
118+
119+
diag "Disabling \$TransactionAutoContrast flips the data attribute to 0";
120+
{
121+
my $cfg = RT::Configuration->new( RT->SystemUser );
122+
my ( $r, $m2 ) = $cfg->Create(
123+
Name => 'TransactionAutoContrast',
124+
Content => 0,
125+
);
126+
ok( $r, "Set TransactionAutoContrast=0: $m2" );
127+
128+
$m->reload;
129+
$m->goto_ticket( $ticket->Id );
130+
$m->content_contains( 'data-auto-contrast="0"',
131+
'history-container has data-auto-contrast=0 when disabled' );
132+
133+
# Leave config consistent for any further tests in the file.
134+
my $row = RT::Configuration->new( RT->SystemUser );
135+
$row->LoadByCols( Name => 'TransactionAutoContrast' );
136+
$row->Delete if $row->Id;
137+
}
138+
33139
done_testing;

0 commit comments

Comments
 (0)