From 0e5d3cb0e5a0edb8c7d7ae7e41ab95d773996e1d Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:06:35 -0700 Subject: [PATCH] fix: Found one regression in the exported transcript mobile sidebar tr (#74584) Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com> Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com> --- src/auto-reply/reply/export-html/template.css | 2 +- .../export-html/template.security.test.ts | 43 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/auto-reply/reply/export-html/template.css b/src/auto-reply/reply/export-html/template.css index b99c93e0b4d..9ab42801478 100644 --- a/src/auto-reply/reply/export-html/template.css +++ b/src/auto-reply/reply/export-html/template.css @@ -1063,7 +1063,7 @@ body { display: block; } - #hamburger { + #hamburger.sidebar-menu-trigger { display: inline-flex; } diff --git a/src/auto-reply/reply/export-html/template.security.test.ts b/src/auto-reply/reply/export-html/template.security.test.ts index 1c7836a03c5..870938326e1 100644 --- a/src/auto-reply/reply/export-html/template.security.test.ts +++ b/src/auto-reply/reply/export-html/template.security.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import vm from "node:vm"; import { fileURLToPath } from "node:url"; +import vm from "node:vm"; import { describe, expect, it } from "vitest"; type SessionEntry = { @@ -141,6 +141,32 @@ function now() { return new Date("2026-02-24T00:00:00.000Z").toISOString(); } +function selectorSpecificity(selector: string): [number, number, number] { + const ids = selector.match(/#[\w-]+/g)?.length ?? 0; + const classes = selector.match(/\.[\w-]+/g)?.length ?? 0; + const withoutIdsOrClasses = selector.replace(/#[\w-]+|\.[\w-]+/g, " "); + const elements = withoutIdsOrClasses + .split(/[\s>+~]+/) + .filter((part) => /^[a-z][\w-]*$/i.test(part)).length; + return [ids, classes, elements]; +} + +function compareSpecificity(left: [number, number, number], right: [number, number, number]) { + for (let index = 0; index < left.length; index += 1) { + if (left[index] !== right[index]) { + return left[index] - right[index]; + } + } + return 0; +} + +function firstSelectorForDisplay(css: string, display: string, startAt: number): string | null { + const displayRule = new RegExp(`([^{}]+)\\{[^{}]*\\bdisplay\\s*:\\s*${display}\\s*;`, "g"); + displayRule.lastIndex = startAt; + const match = displayRule.exec(css); + return match?.[1]?.split(",").at(-1)?.trim() ?? null; +} + describe("export html sidebar trigger affordance", () => { it("keeps the hamburger sidebar trigger accessible and visibly interactive", () => { expect(templateHtml).toContain('id="hamburger" class="sidebar-menu-trigger"'); @@ -154,6 +180,21 @@ describe("export html sidebar trigger affordance", () => { expect(templateCss).toContain("background: var(--container-bg);"); expect(templateCss).toContain("#hamburger.sidebar-menu-trigger:focus-visible {"); }); + + it("lets the mobile hamburger display rule win the CSS cascade", () => { + const baseSelector = "#hamburger.sidebar-menu-trigger"; + const mobileMediaIndex = templateCss.indexOf("@media (max-width: 900px)"); + const mobileSelector = firstSelectorForDisplay(templateCss, "inline-flex", mobileMediaIndex); + + expect(mobileMediaIndex).toBeGreaterThan(templateCss.indexOf(`${baseSelector} {`)); + expect(mobileSelector).toBe(baseSelector); + if (!mobileSelector) { + throw new Error("Missing mobile hamburger display rule"); + } + expect( + compareSpecificity(selectorSpecificity(mobileSelector), selectorSpecificity(baseSelector)), + ).toBeGreaterThanOrEqual(0); + }); }); describe("export html security hardening", () => {