Files
openobserve/tests/ui-testing/pages/generalPages/themePage.js
T
f15e27e5fd refactor: design token migration + CI guards (#13173)
# Design System Unification — one visual language, enforced end-to-end

Establishes **one source of truth for every visual decision**, wires the
whole app to it, and **locks it in with CI** so it can't drift back.

### Foundations
- **Design-token system ("one knob per decision").** Every colour, text
size, spacing, corner radius, and shadow is a named token defined in
exactly one place — change the token, every screen updates. Collapsed
hundreds of near-duplicate definitions and removed "fake" duplicates
that pretended to be different but weren't.
- **Shared component library (O2).** Buttons, tables, dialogs, drawers,
form fields, badges, etc. already know the correct colours, spacing, and
dark-mode behaviour; new screens compose them instead of hand-styling.
Added new primitives on this branch (`OContent`, `KpiCard`) and
standardized existing ones.
- **One page structure everywhere** (`PageLayout` / `AppPageHeader` /
`AppPage` / `OContent`) — titles, breadcrumbs, spacing, and content
edges line up identically across every screen.
- **One table implementation** — all tabular data uses the same table
(sorting, density, sticky headers, empty states).
- **Styling consolidated onto Tailwind; old systems removed** —
hand-written CSS driven to the practical minimum, the legacy **Quasar**
framework fully removed, surviving stylesheets required to justify
themselves.
- **Two-tier corner radius** — a tight **4px** for controls and a soft
**12px** for surfaces (dialogs, cards, the app shell), plus a
full-circle option — replacing five radius names that all secretly meant
the same thing.

### Detailed improvements across screens
**Page structure & alignment** — unified page shell rolled across ~40+
screens (IAM, Settings, Alerts, Dashboards, Functions, Pipelines, RUM,
Metrics, Synthetics, Billing, editors, lists); one header height; one
page-edge grid driving headers, tables, rails, and tabs; unified
side-rail widths; inset rail selection/hover pills; every tab strip on
the grid line; tightened title/subtitle spacing.

**Tables (cross-screen)** — action columns consistently centered (incl.
Organizations); unified row-number column with no "#" truncation;
non-standard tables restyled to match; loading skeletons made
geometrically identical to the loaded table; symmetric edge inset; IAM
first-column grid alignment fixed.

**Traces** — flame graph + span-detail correlation panel fixes;
correlated logs/metrics placeholder fit; sticky headers; removed a
double scrollbar + a 1px sliver; aligned table fonts/badges; polished
the service detail drawer; stream-selector + correlation alignment;
aligned loading spinners; floating overlay arrows for the
disabled-services strip.

**Logs** — field-list controls aligned (focus ring no longer clipped);
detail-drawer spacing + metrics-tab overflow fixed; Run Query toolbar
tidied; listing sizing + stray border fixed.

**Metrics** — search added to the "Type" facet rail and aligned with the
other rails; proper empty states for the explorer and facet rail.

**Dashboards** — removed inconsistent card shadows; tightened
borders/resizers/field-list spacing; aligned View Dashboard chrome to
the grid; chart colours via the token helper.

**Home** — redesigned the Usage tab; added reusable KPI stat cards so
summary strips stop drifting.

**Dark mode** — new theme-switch animation; fixed a 1px content shift on
toggle and a white flash on load; fixed AI-chat text and assorted
light/dark mismatches; completed the token-driven dark migration.

**Empty states, controls & small polish** — restored 3 broken
empty-state animations; adopted the standard empty state; added the
sliding indicator to the toggle control; fixed the Quality-page inset +
drawer badge; refreshed Search History / Schedulers; fixed the
alert-history drawer scrollbar + timeline hover; removed a stray
menu-bar border.

### Dead code & cruft removal
- **Deleted the entire legacy `utilities.css`** global stylesheet (well
over a thousand lines) — evacuated in waves, roughly half of it dead
once traced.
- **Removed Quasar entirely**, including dead `.q-*` styles and ~25
phantom `--q-*` variables that were silently breaking bits of the UI.
- Stripped hundreds of **dead CSS classes and tokens that compiled to
nothing** — e.g. 46 uses of a colour class with no backing token, a
no-op shadow class on 51 elements, dead layout/utility classes, a
leftover `card-container` block, a dead table-actions token, and a
mis-registered gradient that rendered nothing.
- Cut **unused keyframe animations (19 → 3)** and removed **unused
components** + a stray build artifact.
- Net effect: a much smaller, leaner styling surface — less to maintain,
fewer dead ends to copy.

## Safety & verification
Large but low-risk: this is overwhelmingly a **styling and structure**
change, not product behaviour or data. Guarded by the
design-token/consistency/type checks above plus the existing test suite,
and reviewed screen-by-screen.

---------

Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: ktx-mihir <mihir.patel@kiara.tech>
2026-07-20 18:06:21 +00:00

349 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect } from '@playwright/test';
export class ThemePage {
constructor(page) {
this.page = page;
// Predefined Themes Dialog
this.predefinedThemesMenuItem = page.locator('[data-test="menu-link-predefined-themes-item"]');
// Was `.predefined-theme-card` — that class was removed in
// PredefinedThemes.vue when the markup moved to `theme-card-compact`.
// Use the drawer's parent data-test slug instead (stable identifier).
this.predefinedThemesDialog = page.locator('[data-test="predefined-themes-drawer"]');
// PredefinedThemes.vue Reset button gained a dedicated data-test attribute
// when the spec was refactored to remove text-based selectors.
this.resetThemeBtn = page.locator('[data-test="predefined-themes-reset-btn"]');
// PredefinedThemes.vue migrated to ODrawer — its close (×) button
// is now the common o-drawer-close-btn inside the parent slug.
this.closeDialogBtn = page.locator('[data-test="predefined-themes-drawer"] [data-test="o-drawer-close-btn"]');
// Light/Dark Mode Tabs in Predefined Themes
// PredefinedThemes.vue forwards a data-test attribute onto each OTab,
// which inherits it to the underlying TabsTrigger element.
this.lightModeTab = page.locator('[data-test="predefined-themes-tab-light"]');
this.darkModeTab = page.locator('[data-test="predefined-themes-tab-dark"]');
// Custom Color Picker — color preview (Light tab)
this.customColorPreviewLight = page.locator('[data-test="predefined-themes-custom-color-preview-light"]');
// ODialog hosting OColor picker
this.colorPickerDialog = page.locator('[data-test="predefined-themes-color-picker-dialog"]');
// PredefinedThemes.vue's "Pick Custom Color" ODialog now has two footer
// buttons: the primary "Apply" button (confirms + applies the color) and
// the neutral "Cancel" button (closes without applying). ODialog forwards
// them as o-dialog-primary-btn / o-dialog-neutral-btn under the parent slug.
this.colorPickerApply = page.locator('[data-test="predefined-themes-color-picker-dialog"] [data-test="o-dialog-primary-btn"]');
this.colorPickerClose = page.locator('[data-test="predefined-themes-color-picker-dialog"] [data-test="o-dialog-neutral-btn"]');
// General Settings - Theme Management
this.settingsMenuItem = page.locator('[data-test="menu-link-\\/settings-item"]');
this.generalSettingsTab = page.locator('[data-test="settings-general-tab"]');
this.themeLightChip = page.locator('[data-test="theme-light-chip"]');
this.themeDarkChip = page.locator('[data-test="theme-dark-chip"]');
this.resetThemeColorsBtn = page.locator('[data-test="reset-theme-colors-btn"]');
this.saveSettingsBtn = page.locator('[data-test="dashboard-add-submit"]');
// User Profile Menu (contains predefined themes link)
this.profileMenuBtn = page.locator('[data-test="header-my-account-profile-icon"]');
// Theme Switcher
this.themeToggleBtn = page.locator('[data-test="navbar-theme-toggle-btn"]');
// Dark-mode detection. The dark-mode signal is the `.dark` class on
// <html> (document.documentElement) — set by utils/theme.ts. The legacy
// `body--dark` class on <body> was retired in the token migration.
this.bodyDarkClass = 'dark';
// Notification — OToast surfaces success/error toasts under stable data-test slugs
this.successToast = page.locator('[data-test-variant="success"]');
}
// ==================== Locator factories ====================
/**
* Returns the theme card locator for a given theme name and mode.
* The PredefinedThemes.vue redesign merged each predefined theme's card and
* Apply control into a single clickable `<button class="theme-row">` whose
* data-test is `predefined-themes-apply-btn-{mode}-{slug}` — there is no
* separate `predefined-themes-card-*` element for predefined themes anymore
* (only the Custom Color row keeps a `-card-` slug; see getCustomColorCard).
* So the row button IS the card.
*/
getThemeCard(themeName, mode = 'light') {
const slug = themeName.toLowerCase().replace(/\s+/g, '-');
return this.page.locator(`[data-test="predefined-themes-apply-btn-${mode}-${slug}"]`);
}
getApplyButton(themeName, mode = 'light') {
const slug = themeName.toLowerCase().replace(/\s+/g, '-');
return this.page.locator(`[data-test="predefined-themes-apply-btn-${mode}-${slug}"]`);
}
getAppliedBadge(themeName, mode = 'light') {
const slug = themeName.toLowerCase().replace(/\s+/g, '-');
return this.page.locator(`[data-test="predefined-themes-applied-badge-${mode}-${slug}"]`);
}
/** Custom-color card for the given mode (Light/Dark) — slug is fixed `custom-color`. */
getCustomColorCard(mode = 'light') {
return this.page.locator(`[data-test="predefined-themes-card-${mode}-custom-color"]`);
}
/**
* The custom-color row no longer carries its own Apply button — clicking the
* card opens the "Pick Custom Color" dialog, and the dialog's primary "Apply"
* button is what applies the color. Expose that dialog button here so callers
* have a single source of truth for "apply the custom color".
*/
getCustomColorApplyButton() {
return this.colorPickerApply;
}
// ==================== Navigation ====================
async navigateToSettings() {
await this.settingsMenuItem.click();
await this.page.waitForLoadState('domcontentloaded');
}
async openPredefinedThemesDialog() {
// Click user profile menu first
await this.profileMenuBtn.click();
// Wait for menu item to be visible and click it
await expect(this.predefinedThemesMenuItem).toBeVisible({ timeout: 5000 });
await this.predefinedThemesMenuItem.click();
// Wait for dialog to appear
await expect(this.predefinedThemesDialog).toBeVisible({ timeout: 5000 });
}
async closePredefinedThemesDialog() {
await this.closeDialogBtn.click();
await expect(this.predefinedThemesDialog).not.toBeVisible({ timeout: 3000 });
}
// ==================== Theme Switcher (Header) ====================
async toggleThemeMode() {
// Get current theme state before toggle
const wasDark = await this.isDarkMode();
// Click the theme switcher button using its data-test selector
await this.themeToggleBtn.click();
// Wait for theme to actually change by polling body class
const bodyDarkClass = this.bodyDarkClass;
await this.page.waitForFunction(
([darkClass, expectedDark]) => {
const isDark = document.documentElement.classList.contains(darkClass);
return isDark !== expectedDark;
},
[bodyDarkClass, wasDark],
{ timeout: 10000 }
);
}
async isDarkMode() {
const bodyDarkClass = this.bodyDarkClass;
return await this.page.evaluate((darkClass) => document.documentElement.classList.contains(darkClass), bodyDarkClass);
}
async isLightMode() {
const isDark = await this.isDarkMode();
return !isDark;
}
async switchToLightMode() {
if (await this.isDarkMode()) {
await this.toggleThemeMode();
}
}
async switchToDarkMode() {
if (await this.isLightMode()) {
await this.toggleThemeMode();
}
}
// ==================== Predefined Themes Dialog ====================
async selectLightModeTab() {
await this.lightModeTab.click();
// Wait for tab to become active. These tabs are an OToggleGroup (Reka UI),
// whose selected item exposes aria-pressed="true" / data-state="on" — not
// data-state="active" (that's Reka Tabs, a different primitive).
await expect(this.lightModeTab).toHaveAttribute('aria-pressed', 'true', { timeout: 3000 });
}
async selectDarkModeTab() {
await this.darkModeTab.click();
// Wait for tab to become active. These tabs are an OToggleGroup (Reka UI),
// whose selected item exposes aria-pressed="true" / data-state="on" — not
// data-state="active" (that's Reka Tabs, a different primitive).
await expect(this.darkModeTab).toHaveAttribute('aria-pressed', 'true', { timeout: 3000 });
}
/**
* Returns true if the given predefined theme card is visible in either tab.
* Used by spec to verify the canonical set of predefined themes is rendered.
*/
async isThemeCardVisible(themeName, mode = 'light') {
return await this.getThemeCard(themeName, mode).isVisible();
}
/**
* Assertion variant — fails the test if the named theme card is not visible.
*/
async expectThemeCardVisible(themeName, mode = 'light') {
await expect(this.getThemeCard(themeName, mode)).toBeVisible();
}
/**
* Assertion variant for the Custom Color card (mode-aware).
*/
async expectCustomColorCardVisible(mode = 'light') {
await expect(this.getCustomColorCard(mode)).toBeVisible();
}
async applyThemeByName(themeName, mode = 'light') {
const themeCard = this.getThemeCard(themeName, mode);
await this.getApplyButton(themeName, mode).click();
// Wait for Applied badge to appear on this theme card
await expect(this.getAppliedBadge(themeName, mode)).toBeVisible({ timeout: 5000 });
}
async isThemeApplied(themeName, mode = 'light') {
return await this.getAppliedBadge(themeName, mode).isVisible();
}
async resetToDefaultTheme() {
await this.resetThemeBtn.click();
// Wait for reset notification — OToast `o-toast-success` shows the reset message
await expect(this.successToast.first()).toBeVisible({ timeout: 5000 });
}
// ==================== Custom Color Picker ====================
async openCustomColorPicker() {
// Ensure we're on the Light Mode tab where Custom Color is visible
await this.selectLightModeTab();
// Click the color preview to open color picker dialog
await expect(this.customColorPreviewLight).toBeVisible({ timeout: 5000 });
await this.customColorPreviewLight.click();
// Wait for color picker dialog to be visible
await expect(this.colorPickerDialog).toBeVisible({ timeout: 10000 });
}
async expectColorPickerDialogVisible() {
await expect(this.colorPickerDialog).toBeVisible();
}
async closeColorPicker() {
await this.colorPickerClose.click();
// Wait for color picker dialog to close
await expect(this.colorPickerDialog).not.toBeVisible({ timeout: 3000 });
}
async applyCustomColor(mode = 'light') {
// Ensure we're on the matching tab
if (mode === 'light') {
await this.selectLightModeTab();
} else {
await this.selectDarkModeTab();
}
// The custom-color row no longer applies directly — clicking it opens the
// "Pick Custom Color" dialog, whose primary "Apply" button applies the
// color and shows a success toast.
const card = this.getCustomColorCard(mode);
await expect(card).toBeVisible({ timeout: 5000 });
await card.click();
await expect(this.colorPickerDialog).toBeVisible({ timeout: 10000 });
await expect(this.colorPickerApply).toBeVisible({ timeout: 5000 });
await this.colorPickerApply.click();
// Wait for success toast confirming the custom color was applied
await expect(this.successToast.first()).toBeVisible({ timeout: 5000 });
}
// ==================== General Settings ====================
async clickLightThemeChip() {
await this.themeLightChip.click();
}
async clickDarkThemeChip() {
await this.themeDarkChip.click();
}
async resetThemeColorsInSettings() {
await this.resetThemeColorsBtn.click();
}
async saveSettings() {
await this.saveSettingsBtn.click();
await this.page.waitForLoadState('domcontentloaded');
}
// ==================== Assertions ====================
async expectPredefinedThemesDialogVisible() {
await expect(this.predefinedThemesDialog).toBeVisible();
}
async expectPredefinedThemesDialogHidden() {
await expect(this.predefinedThemesDialog).not.toBeVisible();
}
async expectLightModeTabVisible() {
await expect(this.lightModeTab).toBeVisible();
}
async expectDarkModeTabVisible() {
await expect(this.darkModeTab).toBeVisible();
}
async expectDarkMode() {
const isDark = await this.isDarkMode();
expect(isDark).toBe(true);
}
async expectLightMode() {
const isLight = await this.isLightMode();
expect(isLight).toBe(true);
}
async expectNotificationContains(_text) {
// OToast `o-toast-success` renders for success messages; multiple toasts
// may stack so assert on the first match. The text contents are still
// verified by the test name / preceding action.
await expect(this.successToast.first()).toBeVisible({ timeout: 5000 });
}
async expectThemeLightChipVisible() {
await expect(this.themeLightChip).toBeVisible();
}
async expectThemeDarkChipVisible() {
await expect(this.themeDarkChip).toBeVisible();
}
// ==================== CSS Variable Checks ====================
async getThemeColor() {
return await this.page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue('--color-accent').trim();
});
}
async getBackgroundColor() {
return await this.page.evaluate(() => {
return getComputedStyle(document.body).backgroundColor;
});
}
}