mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
feat(attachments): make the header attachments button opt-in (default OFF)
The COD-39 attachments button was hard-visible in the header — first on
mobile, then (after the mobile-only hide) still on desktop. Make it a
proper opt-in App Settings → Display toggle ("Attachments Button"),
default OFF everywhere, mirroring the Response Viewer button:
- index.html: button ships with the `btn-attachments-history--hidden`
marker; new settings checkbox #appSettingsShowAttachmentsButton.
- styles.css: base `display:inline-flex !important` + a more-specific
`--hidden` rule (same pattern as the response viewer).
- settings-ui.js: load/save/getDefaultSettings(false) + a live toggle in
applyHeaderVisibilitySettings. Per-device and NON-leaking — added to
displayKeys AND stripped from the server payload, so enabling it on
desktop never makes it appear on mobile (or any other device). No
server-side render step (purely client display, like the eye button).
- mobile.css: dropped the now-redundant phone-only hide — the opt-in
marker hides it everywhere by default; the per-device toggle governs
both desktop and phone.
Tests updated: the CI static guard drops btn-attachments-history from the
phone-hidden lock (it's opt-in now, excluded from the default-visible
enumeration — the guard still gates any NEW default-visible button); the
real-browser E2E now asserts default-hidden on a desktop-class viewport
and visible after enabling the setting.
Verified on a real desktop browser: hidden by default, the settings
toggle exists, enabling it shows the button. tsc + frontend-syntax +
prettier + public-asset checks + both test suites green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,8 +35,10 @@ const PHONE_WIDTH = 393;
|
||||
const MOBILE_VISIBLE_ALLOWLIST = new Set<string>([]);
|
||||
|
||||
// Buttons we expect to STAY hidden on phones — an explicit lock so a future edit
|
||||
// that removes a hide rule fails loudly (not silently).
|
||||
const KNOWN_PHONE_HIDDEN = ['btn-attachments-history', 'btn-settings', 'btn-lifecycle-log'];
|
||||
// that removes a hide rule fails loudly (not silently). The attachments button is
|
||||
// NOT here: it's opt-in (default-hidden everywhere via its own --hidden marker), so
|
||||
// it's excluded from the default-visible enumeration rather than mobile-hidden.
|
||||
const KNOWN_PHONE_HIDDEN = ['btn-settings', 'btn-lifecycle-log'];
|
||||
|
||||
function attrOf(openTag: string, name: string): string {
|
||||
const m = openTag.match(new RegExp(`${name}="([^"]*)"`));
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Port 3208 - Mobile header button visibility (real-browser E2E).
|
||||
// Port 3208 - Header button visibility (real-browser E2E).
|
||||
//
|
||||
// Companion to the CI static guard (test/mobile-header-buttons-policy.test.ts).
|
||||
// This one renders the real app in an emulated phone vs. a desktop-class tablet
|
||||
// and asserts the ACTUAL computed visibility — catching CSS/layout regressions
|
||||
// the static parser can't see. Regression history: the COD-39 attachments button
|
||||
// (and earlier the plan-usage chip) shipped visible on the cramped phone header.
|
||||
// Renders the real app and asserts ACTUAL computed visibility, catching CSS/layout
|
||||
// regressions the static parser can't see. Two behaviours are covered:
|
||||
// 1. The minimal phone header hides the settings gear + lifecycle log.
|
||||
// 2. The attachments button (COD-39) is OPT-IN — default-hidden everywhere, shown
|
||||
// only when App Settings → Display → "Attachments Button" is enabled. It first
|
||||
// shipped always-visible (mobile, then desktop), which is the regression here.
|
||||
import { describe, it, beforeAll, afterAll } from 'vitest';
|
||||
import type { WebServer } from '../../src/web/server.js';
|
||||
import { createTestServer, stopTestServer } from './helpers/server.js';
|
||||
@@ -16,15 +18,7 @@ import { PORTS, WAIT } from './helpers/constants.js';
|
||||
const PORT = PORTS.HEADER_BUTTONS;
|
||||
const BASE_URL = `http://localhost:${PORT}`;
|
||||
|
||||
// Secondary header buttons that must NOT clutter the minimal phone header
|
||||
// (settings/case controls live in the mobile toolbar instead).
|
||||
const PHONE_HIDDEN = [
|
||||
'#attachmentsHistoryBtn', // COD-39 — the reported regression
|
||||
'.btn-icon-header.btn-settings',
|
||||
'.btn-icon-header.btn-lifecycle-log',
|
||||
];
|
||||
|
||||
describe('Mobile header button visibility (E2E)', () => {
|
||||
describe('Header button visibility (E2E)', () => {
|
||||
let server: WebServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -36,19 +30,29 @@ describe('Mobile header button visibility (E2E)', () => {
|
||||
await stopTestServer(server);
|
||||
});
|
||||
|
||||
it('hides secondary header buttons on a standard phone (iPhone 14 Pro, 393px)', async () => {
|
||||
it('hides the settings gear + lifecycle log on a standard phone (iPhone 14 Pro)', async () => {
|
||||
const { page } = await createDevicePage(REPRESENTATIVE_DEVICES['standard-phone'], BASE_URL);
|
||||
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
||||
for (const sel of PHONE_HIDDEN) {
|
||||
await assertHidden(page, sel);
|
||||
}
|
||||
await assertHidden(page, '.btn-icon-header.btn-settings');
|
||||
await assertHidden(page, '.btn-icon-header.btn-lifecycle-log');
|
||||
});
|
||||
|
||||
it('keeps the attachments button visible on a desktop-class tablet', async () => {
|
||||
// assertHidden also passes when an element is ABSENT, so prove the selector is
|
||||
// real by asserting the same button IS visible where the phone rule doesn't apply.
|
||||
it('keeps the opt-in attachments button HIDDEN by default on a desktop-class viewport', async () => {
|
||||
const { page } = await createDevicePage(REPRESENTATIVE_DEVICES['large-tablet'], BASE_URL);
|
||||
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
||||
await assertHidden(page, '#attachmentsHistoryBtn');
|
||||
});
|
||||
|
||||
it('shows the attachments button once the setting is enabled', async () => {
|
||||
const { page } = await createDevicePage(REPRESENTATIVE_DEVICES['large-tablet'], BASE_URL);
|
||||
// Desktop-class devices use the non-mobile settings blob.
|
||||
await page.evaluate(() => {
|
||||
const cur = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
|
||||
cur.showAttachmentsButton = true;
|
||||
localStorage.setItem('codeman-app-settings', JSON.stringify(cur));
|
||||
});
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
||||
await assertVisible(page, '#attachmentsHistoryBtn');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user