diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index cfe9f521..245d99ec 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -335,3 +335,28 @@ jobs: fi fi fi + + result-theme-browser: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 + with: + node-version: '22' + - name: Install existing browser tooling + working-directory: tests/E2E/playwright + run: | + npm ci --ignore-scripts + npx --no-install playwright install --with-deps chromium + - name: Check result foregrounds across theme row states + env: + THEME_TEST_OUTPUT: ${{ runner.temp }}/syslog-theme-contrast + run: node tests/E2E/playwright/theme-contrast-check.cjs + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 + if: always() + with: + name: result-theme-browser + path: ${{ runner.temp }}/syslog-theme-contrast diff --git a/.gitignore b/.gitignore index fb03a808..ceac1c60 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ composer.lock phpstan.neon phpstan-baseline.neon phpstan-stubs.php +/tests/E2E/playwright/artifacts/ graphify-out/** locales/po/*.po locales/LC_MESSAGES/*.mo diff --git a/README.md b/README.md index 218d8810..36f978c2 100644 --- a/README.md +++ b/README.md @@ -553,3 +553,10 @@ nesting levels. Invalid searches return no results and CSV export rejects them. Copyright (c) 2004-2026 - The Cacti Group, Inc. Optional [Alarm Manager forwarding](docs/ALARM_MANAGER.md) uses Cacti native in-process hooks with existing Syslog email as fallback. + +## Result theme regression fixture + +![Synthetic Syslog interface alerts on a dark result surface](docs/images/result-theme-dark.png) + +See [result theme tests](docs/RESULT_THEME_TESTS.md) for the populated light +fixture and browser reproduction command. diff --git a/css/search.css b/css/search.css index f3bf546a..21243118 100644 --- a/css/search.css +++ b/css/search.css @@ -601,9 +601,9 @@ #syslog_workspace table { width: 100%; } /* Severity row tints: a translucent hue per priority mixed over the live theme surface, so the same rules read correctly on both light and dark themes. */ -#syslog_workspace tr.syslogRow { --syslog-sev: transparent; background: color-mix(in srgb, var(--syslog-sev) 16%, transparent); } -#syslog_workspace tr.syslogRow:nth-child(even) { background: color-mix(in srgb, var(--syslog-sev) 22%, color-mix(in srgb, currentColor 5%, transparent)); } -#syslog_workspace tr.syslogRow:hover { background: color-mix(in srgb, var(--syslog-sev) 32%, color-mix(in srgb, currentColor 9%, transparent)); } +#syslog_workspace tr.syslogRow { --syslog-sev: transparent; color: var(--search-text); background: color-mix(in srgb, var(--syslog-sev) 16%, var(--search-card)); } +#syslog_workspace tr.syslogRow:nth-child(even) { background: color-mix(in srgb, var(--syslog-sev) 22%, var(--search-card)); } +#syslog_workspace tr.syslogRow:hover { background: color-mix(in srgb, var(--syslog-sev) 20%, var(--search-card)); } #syslog_workspace tr.syslogRow td:first-child { border-left: 4px solid transparent; } #syslog_workspace tr.syslogRow.logEmergency { --syslog-sev: #e5484d; } #syslog_workspace tr.syslogRow.logAlert { --syslog-sev: #e5533b; } @@ -1125,3 +1125,21 @@ width: 100%; margin: 8px auto 0; } + +#syslog_workspace tr.syslogRow a, +#syslog_workspace tr.syslogRow a:hover { + color: var(--search-text) !important; +} +#syslog_workspace tr.syslogRow .syslogSeverity, +#syslog_workspace tr.syslogRow .syslogSeverity:hover { + color: var(--search-text); + background: var(--search-card); +} + +/* Some host themes force their selected foreground and background. */ +#syslog_workspace tr.syslogRow.selected { + color: var(--search-text) !important; + background: var(--search-card) !important; + outline: 1px solid var(--search-text); + outline-offset: -1px; +} diff --git a/docs/RESULT_THEME_TESTS.md b/docs/RESULT_THEME_TESTS.md new file mode 100644 index 00000000..d5e1874e --- /dev/null +++ b/docs/RESULT_THEME_TESTS.md @@ -0,0 +1,27 @@ +# Result theme regression checks + +Result rows, links, and severity buttons use the foreground and card surface +sampled from the active Cacti theme. This keeps host selection colors from +replacing the foreground on a plugin-owned severity surface. + +![Dark result fixture with synthetic interface alerts](images/result-theme-dark.png) + +![Light result fixture with synthetic interface alerts](images/result-theme-light.png) + +The screenshots contain two synthetic devices and interface-down messages. +They are browser component fixtures, not a running poller or database session. + +Run with the existing Playwright dependency installed: + +```sh +node tests/E2E/playwright/theme-contrast-check.cjs +``` + +Set `CHROMIUM_EXECUTABLE` to use an existing Chromium installation and +`THEME_TEST_OUTPUT` to choose the artifact directory (default +`tests/E2E/playwright/artifacts/theme-contrast`). The test checks light, dark, and muted dark palettes in resting, +hovered, selected, and selected-hovered rows. It also writes Chromium CSS +coverage to `changed-coverage.json`. + +The Cacti theme compatibility suite additionally loads this plugin's real CSS +and theme-token JavaScript against all twelve host theme variants. diff --git a/docs/images/result-theme-dark.png b/docs/images/result-theme-dark.png new file mode 100644 index 00000000..39bcf8d5 Binary files /dev/null and b/docs/images/result-theme-dark.png differ diff --git a/docs/images/result-theme-light.png b/docs/images/result-theme-light.png new file mode 100644 index 00000000..a87d6a76 Binary files /dev/null and b/docs/images/result-theme-light.png differ diff --git a/tests/E2E/playwright/package-lock.json b/tests/E2E/playwright/package-lock.json new file mode 100644 index 00000000..4bc385c3 --- /dev/null +++ b/tests/E2E/playwright/package-lock.json @@ -0,0 +1,62 @@ +{ + "name": "plugin-syslog-e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "plugin-syslog-e2e", + "version": "1.0.0", + "devDependencies": { + "playwright": "1.55.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", + "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", + "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/tests/E2E/playwright/theme-contrast-check.cjs b/tests/E2E/playwright/theme-contrast-check.cjs new file mode 100644 index 00000000..c6fefae1 --- /dev/null +++ b/tests/E2E/playwright/theme-contrast-check.cjs @@ -0,0 +1,42 @@ +/* Copyright (C) 2004-2026 The Cacti Group. GPL-2.0-or-later. */ +const assert = require('node:assert/strict'); +const { readFileSync, mkdirSync, writeFileSync } = require('node:fs'); +const { resolve } = require('node:path'); +const { chromium } = require('playwright'); + +(async () => { + const root = resolve(__dirname, '../../..'); + const output = resolve(process.env.THEME_TEST_OUTPUT || resolve(__dirname, 'artifacts/theme-contrast')); + mkdirSync(output, { recursive: true }); + const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_EXECUTABLE || undefined }); + const page = await browser.newPage(); + await page.route('http://syslog-fixture.test/css/search.css', route => route.fulfill({ contentType: 'text/css', body: readFileSync(resolve(root, 'css/search.css'), 'utf8') })); + await page.coverage.startCSSCoverage({ resetOnNavigation: false }); + try { + for (const [mode, foreground, surface] of [['light', 'rgb(35, 54, 47)', 'rgb(255, 255, 255)'], ['dark', 'rgb(238, 238, 238)', 'rgb(32, 32, 32)'], ['muted-dark', 'rgb(150, 155, 163)', 'rgb(0, 0, 0)']]) { + await page.setContent(`
+ ${[1, 2].map(id => ``).join('')}
edge-rtr-0${id}ifDown Gi1/0/24
`); + await page.addStyleTag({ url: 'http://syslog-fixture.test/css/search.css' }); + for (const state of ['rest', 'hover', 'selected', 'selected-hover']) { + await page.mouse.move(0, 0); + if (state === 'selected') await page.locator('tr').evaluateAll(rows => rows.forEach(row => row.classList.add('selected'))); + if (state.includes('hover')) await page.locator('#row1 button').hover(); + for (const selector of ['#row1 a', '#row2 a', '#row1 button', '#row2 button']) { + assert.equal(await page.locator(selector).evaluate(element => getComputedStyle(element).color), foreground, `${mode} ${state} ${selector}`); + } + assert.equal(await page.locator('#row1 button').evaluate(element => getComputedStyle(element).backgroundColor), surface); + } + await page.screenshot({ path: resolve(output, `syslog-${mode}.png`), fullPage: true }); + } + writeFileSync(resolve(output, 'changed-coverage.json'), JSON.stringify({ css: await page.coverage.stopCSSCoverage(), js: [] })); + console.log('PASS: three palettes of result links and severity buttons in four row states'); + } finally { + await browser.close(); + } +})().catch(error => { console.error(error); process.exitCode = 1; });