blob: 5a6efd8364af900854b502dea67d96600eb21a18 [file] [edit]
# Copyright 2026 The Pigweed 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
#
# https://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.
"""Theme UI tests for pigweed.dev."""
import unittest
from helpers import (
DocsServer,
get_chromium_executable,
get_docs_dir,
)
from playwright.sync_api import sync_playwright
class ThemeTest(unittest.TestCase):
"""Theme UI tests."""
@classmethod
def setUpClass(cls):
cls.docs_dir = get_docs_dir()
cls.server = DocsServer(cls.docs_dir)
cls.server.start()
@classmethod
def tearDownClass(cls):
cls.server.stop()
def test_theme_switch_and_persistence(self):
"""Verifies theme switching and persistence across Sphinx, Rustdoc, and
Doxygen."""
chromium_bin = get_chromium_executable()
with sync_playwright() as p:
browser = p.chromium.launch(
executable_path=chromium_bin,
headless=True,
)
context = browser.new_context(
viewport={"width": 1280, "height": 800}
)
page = context.new_page()
def verify_theme(expected_theme: str):
page.wait_for_function(
"() => document.documentElement.getAttribute('data-theme')"
f" === '{expected_theme}'",
timeout=5000,
)
theme_val = page.evaluate(
"document.documentElement.getAttribute('data-theme')"
)
self.assertEqual(theme_val, expected_theme)
btn = page.locator(
f"pw-theme button[data-theme-val='{expected_theme}']"
)
btn.wait_for(state="visible", timeout=5000)
self.assertIn("active", btn.get_attribute("class") or "")
# 1. Start on a Sphinx page
page.goto(
self.server.url_for("pw_bytes/docs.html"),
wait_until="domcontentloaded",
)
# 2. Set theme to dark and verify switch
dark_btn = page.locator("pw-theme button[data-theme-val='dark']")
dark_btn.wait_for(state="visible", timeout=5000)
dark_btn.click()
verify_theme("dark")
# 3. Navigate to Rustdoc page and verify dark theme persists
page.goto(
self.server.url_for("rustdoc/pw_bytes/index.html"),
wait_until="domcontentloaded",
)
verify_theme("dark")
# 4. Navigate to Doxygen page and verify dark theme persists
page.goto(
self.server.url_for("api/cc/group__pw__bytes.html"),
wait_until="domcontentloaded",
)
verify_theme("dark")
# 5. Set theme to light and verify switch
light_btn = page.locator("pw-theme button[data-theme-val='light']")
light_btn.wait_for(state="visible", timeout=5000)
light_btn.click()
verify_theme("light")
# 6. Navigate to a different Doxygen page and verify light theme
# persists
page.goto(
self.server.url_for("api/cc/group__pw__kvs.html"),
wait_until="domcontentloaded",
)
verify_theme("light")
# 7. Navigate to a different Sphinx page and verify light theme
# persists
page.goto(
self.server.url_for("pw_allocator/docs.html"),
wait_until="domcontentloaded",
)
verify_theme("light")
browser.close()
def test_rustdoc_ayu_theme_option_hidden(self):
"""Verifies that the ayu theme option is hidden in Rustdoc settings."""
chromium_bin = get_chromium_executable()
with sync_playwright() as p:
browser = p.chromium.launch(
executable_path=chromium_bin,
headless=True,
)
context = browser.new_context(
viewport={"width": 1280, "height": 800}
)
page = context.new_page()
page.goto(
self.server.url_for("rustdoc/settings.html"),
wait_until="domcontentloaded",
)
# Wait for settings to load
page.wait_for_selector('label[for="theme-dark"]', timeout=5000)
self.assertTrue(
page.locator('label[for="theme-dark"]').is_visible()
)
self.assertTrue(
page.locator('label[for="theme-light"]').is_visible()
)
# Verify label[for="theme-ayu"] is not shown
ayu_label = page.locator('label[for="theme-ayu"]')
self.assertFalse(
ayu_label.is_visible(),
"label[for='theme-ayu'] should be hidden in Rustdoc settings",
)
browser.close()
def test_os_color_scheme_preference(self):
"""Verifies that PwTheme respects OS-level prefers-color-scheme when no
explicit localStorage theme has been set."""
chromium_bin = get_chromium_executable()
with sync_playwright() as p:
browser = p.chromium.launch(
executable_path=chromium_bin,
headless=True,
)
# 1. Test OS light mode preference
context_light = browser.new_context(
viewport={"width": 1280, "height": 800},
color_scheme="light",
)
page_light = context_light.new_page()
page_light.goto(
self.server.url_for("pw_bytes/docs.html"),
wait_until="domcontentloaded",
)
page_light.wait_for_function(
"() => document.documentElement.getAttribute('data-theme')"
" === 'light'",
timeout=5000,
)
self.assertEqual(
page_light.evaluate(
"document.documentElement.getAttribute('data-theme')"
),
"light",
"Theme should default to light when prefers-color-scheme is light",
)
light_btn = page_light.locator(
"pw-theme button[data-theme-val='light']"
)
self.assertIn("active", light_btn.get_attribute("class") or "")
self.assertEqual(light_btn.get_attribute("aria-checked"), "true")
context_light.close()
# 2. Test OS dark mode preference
context_dark = browser.new_context(
viewport={"width": 1280, "height": 800},
color_scheme="dark",
)
page_dark = context_dark.new_page()
page_dark.goto(
self.server.url_for("pw_bytes/docs.html"),
wait_until="domcontentloaded",
)
page_dark.wait_for_function(
"() => document.documentElement.getAttribute('data-theme')"
" === 'dark'",
timeout=5000,
)
self.assertEqual(
page_dark.evaluate(
"document.documentElement.getAttribute('data-theme')"
),
"dark",
"Theme should default to dark when prefers-color-scheme is dark",
)
dark_btn = page_dark.locator(
"pw-theme button[data-theme-val='dark']"
)
self.assertIn("active", dark_btn.get_attribute("class") or "")
self.assertEqual(dark_btn.get_attribute("aria-checked"), "true")
context_dark.close()
browser.close()
def test_localStorage_security_error_graceful_handling(self):
"""Verifies that PwTheme handles localStorage throwing a SecurityError
without throwing unhandled exceptions or breaking theme initialization.
"""
chromium_bin = get_chromium_executable()
with sync_playwright() as p:
browser = p.chromium.launch(
executable_path=chromium_bin,
headless=True,
)
context = browser.new_context(
viewport={"width": 1280, "height": 800},
color_scheme="light",
)
# Simulate SecurityError on window.localStorage access
context.add_init_script(
"""
Object.defineProperty(window, 'localStorage', {
get: function() {
throw new DOMException(
"The operation is insecure.",
"SecurityError"
);
}
});
"""
)
page = context.new_page()
# Verify PwTheme on Rustdoc page (which does not use Sphinx scripts)
rustdoc_errors = []
page.on("pageerror", lambda err: rustdoc_errors.append(str(err)))
page.goto(
self.server.url_for("rustdoc/pw_bytes/index.html"),
wait_until="domcontentloaded",
)
self.assertEqual(
rustdoc_errors,
[],
f"Rustdoc page had uncaught errors: {rustdoc_errors}",
)
# Theme should default to system preference (light)
page.wait_for_function(
"() => document.documentElement.getAttribute('data-theme')"
" === 'light'",
timeout=5000,
)
self.assertEqual(
page.evaluate(
"document.documentElement.getAttribute('data-theme')"
),
"light",
)
dark_btn = page.locator("pw-theme button[data-theme-val='dark']")
dark_btn.wait_for(state="visible", timeout=5000)
dark_btn.click()
page.wait_for_function(
"() => document.documentElement.getAttribute('data-theme')"
" === 'dark'",
timeout=5000,
)
self.assertEqual(
page.evaluate(
"document.documentElement.getAttribute('data-theme')"
),
"dark",
)
is_bc_defined = page.evaluate(
"() => customElements.get('pw-breadcrumbs') !== undefined"
)
self.assertTrue(
is_bc_defined,
"pw-breadcrumbs custom element should be successfully defined",
)
browser.close()
if __name__ == "__main__":
unittest.main()