mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Merge pull request #1958 from rcourtman/maintainer/20260907T131524Z
Protect alert settings navigation and threshold edits from regressions
This commit is contained in:
+42
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { CollapsibleSection, SectionActionButton, NestedGroupHeader } from '../CollapsibleSection';
|
||||
import {
|
||||
@@ -173,6 +174,47 @@ describe('CollapsibleSection', () => {
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
it('preserves an edited threshold across controlled collapse and reopen', () => {
|
||||
const [collapsed, setCollapsed] = createSignal(true);
|
||||
render(() => (
|
||||
<CollapsibleSection
|
||||
id="snapshots"
|
||||
title="Snapshot Age"
|
||||
collapsed={collapsed()}
|
||||
onToggle={setCollapsed}
|
||||
>
|
||||
<label>
|
||||
Warning days
|
||||
<input type="number" value="7" />
|
||||
</label>
|
||||
</CollapsibleSection>
|
||||
));
|
||||
|
||||
const disclosure = screen.getByRole('button', { name: 'Snapshot Age' });
|
||||
const panel = document.getElementById(disclosure.getAttribute('aria-controls')!);
|
||||
expect(panel).toHaveAttribute('inert');
|
||||
expect(panel).toHaveAttribute('aria-hidden', 'true');
|
||||
|
||||
fireEvent.click(disclosure);
|
||||
expect(disclosure).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(panel).not.toHaveAttribute('inert');
|
||||
expect(panel).not.toHaveAttribute('aria-hidden');
|
||||
const input = screen.getByRole('spinbutton', { name: 'Warning days' });
|
||||
fireEvent.input(input, { target: { value: '14' } });
|
||||
|
||||
fireEvent.click(disclosure);
|
||||
expect(disclosure).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(panel).toHaveAttribute('inert');
|
||||
expect(panel).toHaveAttribute('aria-hidden', 'true');
|
||||
|
||||
fireEvent.click(disclosure);
|
||||
expect(disclosure).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(panel).not.toHaveAttribute('inert');
|
||||
expect(panel).not.toHaveAttribute('aria-hidden');
|
||||
expect(screen.getByRole('spinbutton', { name: 'Warning days' })).toBe(input);
|
||||
expect(input).toHaveValue(14);
|
||||
});
|
||||
|
||||
it('shows "Disabled" badge when isGloballyDisabled is true', () => {
|
||||
render(() => (
|
||||
<CollapsibleSection id="test" title="Title" isGloballyDisabled={true}>
|
||||
|
||||
@@ -5,6 +5,9 @@ import { spawn } from 'node:child_process';
|
||||
const truthyValues = new Set(['1', 'true', 'yes', 'on']);
|
||||
|
||||
const BILLING_PROFILES = {
|
||||
// General-purpose organization/billing fixture, not a white-label runtime.
|
||||
// white_label deliberately hides commercial routes via security/status;
|
||||
// commercial suppression belongs in dedicated boundary scenarios.
|
||||
'multi-tenant': {
|
||||
capabilities: [
|
||||
'advanced_reporting',
|
||||
@@ -25,7 +28,6 @@ const BILLING_PROFILES = {
|
||||
'sso',
|
||||
'unlimited',
|
||||
'update_alerts',
|
||||
'white_label',
|
||||
],
|
||||
limits: {},
|
||||
meters_enabled: [],
|
||||
|
||||
@@ -36,6 +36,16 @@ test('buildBillingState returns enterprise capabilities for multi-tenant profile
|
||||
assert.ok(state.capabilities.includes('rbac'));
|
||||
});
|
||||
|
||||
test('general-purpose entitlement profiles keep commercial routes available', () => {
|
||||
for (const profile of ['multi-tenant', 'infra']) {
|
||||
const state = buildBillingState(profile);
|
||||
assert.ok(
|
||||
!state.capabilities.includes('white_label'),
|
||||
`${profile} must not opt the shared runtime into commercial suppression`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('applyRequestedEntitlementProfile writes a billing state file when a path is provided', async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'pulse-entitlement-'));
|
||||
const billingPath = path.join(dir, 'billing.json');
|
||||
|
||||
@@ -21,18 +21,21 @@ const SETTINGS_SHELL_ROUTES = [
|
||||
{
|
||||
route: '/settings/organization',
|
||||
title: 'Organization Overview',
|
||||
mobileTitle: 'Overview',
|
||||
description: 'Review organization metadata, membership footprint, and ownership.',
|
||||
requiresMultiTenant: true,
|
||||
},
|
||||
{
|
||||
route: '/settings/organization/access',
|
||||
title: 'Organization Access',
|
||||
mobileTitle: 'Access',
|
||||
description: 'Manage organization invitations, member roles, and ownership transfers.',
|
||||
requiresMultiTenant: true,
|
||||
},
|
||||
{
|
||||
route: '/settings/organization/billing',
|
||||
title: 'Billing & Usage',
|
||||
mobileTitle: 'Billing',
|
||||
description:
|
||||
'Review your organization plan, applicable usage policies, and subscription status for paid access.',
|
||||
requiresMultiTenant: true,
|
||||
@@ -50,13 +53,14 @@ const SETTINGS_SHELL_ROUTES = [
|
||||
},
|
||||
{
|
||||
route: '/settings/system-ai',
|
||||
canonicalRoute: '/settings/pulse-intelligence/provider',
|
||||
title: 'Provider & Models',
|
||||
description:
|
||||
'Configure providers, default models, provider health, budget, and usage for Pulse Intelligence.',
|
||||
},
|
||||
{
|
||||
route: '/settings/system-updates',
|
||||
title: 'Updates',
|
||||
title: 'Pulse server updates',
|
||||
description:
|
||||
'Manage Pulse server runtime version checks, update channels, and automatic updates. Agent updates stay under Infrastructure.',
|
||||
},
|
||||
@@ -144,8 +148,13 @@ test.describe('Settings shell consistency', () => {
|
||||
await expect(navigation).toBeHidden();
|
||||
}
|
||||
|
||||
const pageHeading = page.getByRole('heading', { level: 1, name: panel.title });
|
||||
// The compact mobile header uses the navigation label, not the
|
||||
// desktop page title (SettingsPageShell + settingsNavCatalog).
|
||||
const title = isMobile && 'mobileTitle' in panel ? panel.mobileTitle : panel.title;
|
||||
const pageHeading = page.getByRole('heading', { level: 1, name: title, exact: true });
|
||||
await expect(pageHeading, `${panel.route} should render the canonical page-shell heading`).toBeVisible();
|
||||
const canonicalRoute = 'canonicalRoute' in panel ? panel.canonicalRoute : panel.route;
|
||||
await expect(page).toHaveURL(new RegExp(`${canonicalRoute}$`));
|
||||
if (!isMobile) {
|
||||
// Panel descriptions are desktop-only copy (hidden sm:block).
|
||||
await expect(
|
||||
|
||||
Reference in New Issue
Block a user