Add frontend branch-coverage tests for eight untested pure models

A full V8 branch-coverage regen (18326-test suite clean, so the counts are
ground truth) flagged eight pure frontend modules as the only remaining
defensible coverage gaps. This adds one branchcov test per module, exercising
genuinely-untested exported functions and previously-uncovered branch arms.

Modules and targets covered.
- utils/agentInstallCommand buildPowerShellInstallScriptBootstrap, a
  completely untested export, plus its empty-URL throw arm
- i18n/locales resolveSupportedLocale across all four resolution strategies
  and the unsupported-base null arm, plus getLocaleFallbackChain
- shared/helpIconModel calculateHelpPopoverPosition geometry (top and bottom
  flip arms, horizontal clamp) and resolveHelpContent
- Workloads/workloadsFilterModel countActiveWorkloadsFilters across all eight
  filter arms and hasActiveWorkloadsFilters
- Workloads/metricBarModel buildMetricBarPresentation showLabel and
  showSublabel threshold arms
- shared/selectionCardGroupModel variant and tone resolvers and the class helpers
- shared/animatedNumberModel sanitizeAnimatedNumberValue non-finite arm
- shared/tagInputModel getTagInputPlaceholder arms, getNextTagsAfterRemove,
  canAddTag

Tests only, no source changes. 173 new test cases, all green. tsc and eslint clean.
This commit is contained in:
rcourtman
2026-07-20 14:22:43 +01:00
parent 674b70a02a
commit fbebdddb7d
8 changed files with 1487 additions and 0 deletions
@@ -0,0 +1,121 @@
import { describe, expect, it } from 'vitest';
import { estimateTextWidth } from '@/utils/format';
import type { MetricBarProps } from '../metricBarModel';
import { buildMetricBarPresentation } from '../metricBarModel';
// estimateTextWidth(text) = text.length * 5.5 + 8 (mirrored from @/utils/format).
// For label='CPU' + sublabel='8c' the composed threshold text is 'CPU (8c)'
// (length 8) -> estimateTextWidth = 8 * 5.5 + 8 = 52. So a containerWidth of 52
// trips the >= true arm of the showSublabel check, and 51 trips the false arm.
const LABEL = 'CPU';
const SUBLABEL = '8c';
const SUBLABEL_THRESHOLD = estimateTextWidth(`${LABEL} (${SUBLABEL})`); // 52
// Tailwind background class mirrored from metricThresholds.BG_CLASSES.normal.
// Default cpu thresholds are warning 80 / critical 90 (METRIC_THRESHOLDS.cpu),
// used whenever thresholds is undefined or null. value 42 < 80 -> normal.
const NORMAL_CLASS = 'bg-metric-normal-bg dark:bg-metric-normal-bg';
const makeProps = (overrides: Partial<MetricBarProps> = {}): MetricBarProps => ({
value: 42,
label: LABEL,
sublabel: SUBLABEL,
...overrides,
});
describe('buildMetricBarPresentation (branch coverage 0720pm)', () => {
describe('showLabel branch (props.showLabel !== false && label.trim().length > 0)', () => {
it('returns showLabel=true when showLabel is omitted (true arm of !== false)', () => {
// showLabel omitted -> props.showLabel is undefined -> !== false is true.
// label 'CPU' trims to a non-empty string -> showLabel true.
const p = buildMetricBarPresentation(makeProps(), 200);
expect(p.showLabel).toBe(true);
// The other emitted fields are also concrete and verified here.
expect(p.width).toBe(42);
expect(p.progressColorClass).toBe(NORMAL_CLASS);
// With a wide container the sublabel is also shown.
expect(p.showSublabel).toBe(true);
});
it('returns showLabel=true when showLabel is explicitly true (true arm of !== false)', () => {
const p = buildMetricBarPresentation(makeProps({ showLabel: true }), 200);
expect(p.showLabel).toBe(true);
});
it('returns showLabel=false when showLabel is explicitly false (false arm of !== false)', () => {
// Even with a wide container and a non-empty sublabel, showLabel=false
// propagates straight through to showLabel and forces showSublabel=false
// via the leading && of the showSublabel expression.
const p = buildMetricBarPresentation(makeProps({ showLabel: false }), 200);
expect(p.showLabel).toBe(false);
expect(p.showSublabel).toBe(false);
// width and progressColorClass are computed independently of showLabel.
expect(p.width).toBe(42);
expect(p.progressColorClass).toBe(NORMAL_CLASS);
});
it('returns showLabel=false when the label is whitespace-only (label.trim().length > 0 false arm)', () => {
// showLabel !== false is true, but label.trim().length === 0 -> the
// right operand of the && is false -> showLabel false -> showSublabel false.
const p = buildMetricBarPresentation(makeProps({ label: ' ', showLabel: true }), 200);
expect(p.showLabel).toBe(false);
expect(p.showSublabel).toBe(false);
});
});
describe('showSublabel threshold branch (containerWidth >= estimateTextWidth(...))', () => {
it('returns showSublabel=true when containerWidth meets the threshold exactly (>= true arm)', () => {
// estimateTextWidth('CPU (8c)') === 52; containerWidth 52 -> 52 >= 52 true.
expect(SUBLABEL_THRESHOLD).toBe(52);
const p = buildMetricBarPresentation(makeProps(), SUBLABEL_THRESHOLD);
expect(p.showLabel).toBe(true);
expect(p.showSublabel).toBe(true);
});
it('returns showSublabel=false when containerWidth is one below the threshold (false arm)', () => {
// containerWidth 51 < 52 -> the >= arm is false -> showSublabel false.
// showLabel itself is unaffected by the width check (still true here).
const p = buildMetricBarPresentation(makeProps(), SUBLABEL_THRESHOLD - 1);
expect(p.showLabel).toBe(true);
expect(p.showSublabel).toBe(false);
});
it('returns showSublabel=false when sublabel is empty (Boolean(props.sublabel) false arm)', () => {
// Even with a very wide container, Boolean('') is false -> showSublabel false.
const p = buildMetricBarPresentation(makeProps({ sublabel: '' }), 1000);
expect(p.showLabel).toBe(true);
expect(p.showSublabel).toBe(false);
});
});
describe('width field (Math.min(props.value, 100))', () => {
it('returns the raw value when value <= 100', () => {
const p = buildMetricBarPresentation(makeProps({ value: 42 }), 200);
expect(p.width).toBe(42);
});
it('clamps to 100 when value > 100', () => {
// Math.min(150, 100) -> 100.
const p = buildMetricBarPresentation(makeProps({ value: 150 }), 200);
expect(p.width).toBe(100);
});
});
describe('progressColorClass threading (type/thresholds)', () => {
it('maps a sub-warning value to the normal class with default cpu thresholds', () => {
// value 42 < 80 (warning) -> normal; metric defaults to 'cpu' when type
// is omitted.
const p = buildMetricBarPresentation(makeProps({ value: 42 }), 200);
expect(p.progressColorClass).toBe(NORMAL_CLASS);
});
it('honors type=generic by mapping it to cpu for color purposes', () => {
// metric = props.type || 'cpu' -> 'generic'; metricType = metric ===
// 'generic' ? 'cpu' : metric -> 'cpu'. Same normal-band class.
const p = buildMetricBarPresentation(makeProps({ value: 42, type: 'generic' }), 200);
expect(p.progressColorClass).toBe(NORMAL_CLASS);
});
});
});
@@ -0,0 +1,138 @@
import { describe, expect, it } from 'vitest';
import type { CountActiveWorkloadsFiltersOptions } from '../workloadsFilterModel';
import {
countActiveWorkloadsFilters,
DEFAULT_WORKLOADS_STATUS_MODE,
DEFAULT_WORKLOADS_VIEW_MODE,
hasActiveWorkloadsFilters,
} from '../workloadsFilterModel';
// `countActiveWorkloadsFilters` is an 8-accumulator reducer with eight independent
// `if` arms (search / viewMode / statusMode / hostFilter / platformFilter /
// namespaceFilter / clusterFilter / containerRuntimeFilter). Each arm is exercised
// below in both the true (active) and false (default) direction by asserting on
// the real returned count, not on source text.
// Build a fixture where EVERY arm is at its inactive default. The five optional
// filter-value fields are omitted entirely so the `(value ?? '')` nullish arms
// are also driven (they must collapse to the empty string and NOT count).
const inactiveOptions = (): CountActiveWorkloadsFiltersOptions => ({
search: '',
viewMode: DEFAULT_WORKLOADS_VIEW_MODE,
statusMode: DEFAULT_WORKLOADS_STATUS_MODE,
});
describe('countActiveWorkloadsFilters — all-default baseline (every if false)', () => {
it('returns 0 when every field is at its real default', () => {
expect(countActiveWorkloadsFilters(inactiveOptions())).toBe(0);
});
it('returns 0 when the five optional filter values are explicitly undefined', () => {
// Drives the `(value ?? '')` nullish-coalesce arm on each of the five
// filter-value guards — undefined must collapse to '' and not count.
expect(
countActiveWorkloadsFilters({
...inactiveOptions(),
hostFilterValue: undefined,
platformFilterValue: undefined,
namespaceFilterValue: undefined,
clusterFilterValue: undefined,
containerRuntimeFilterValue: undefined,
}),
).toBe(0);
});
it('treats a whitespace-only search as inactive (search.trim() !== "" false arm)', () => {
expect(countActiveWorkloadsFilters({ ...inactiveOptions(), search: ' ' })).toBe(0);
});
});
describe('countActiveWorkloadsFilters — single-arm active (each if true independently)', () => {
it('counts only the search arm when search has non-whitespace content', () => {
expect(countActiveWorkloadsFilters({ ...inactiveOptions(), search: 'nginx' })).toBe(1);
});
it('counts only the viewMode arm when viewMode differs from DEFAULT_WORKLOADS_VIEW_MODE', () => {
expect(countActiveWorkloadsFilters({ ...inactiveOptions(), viewMode: 'vm' })).toBe(1);
});
it('counts only the statusMode arm when statusMode differs from DEFAULT_WORKLOADS_STATUS_MODE', () => {
expect(countActiveWorkloadsFilters({ ...inactiveOptions(), statusMode: 'running' })).toBe(1);
});
it('counts only the hostFilter arm when hostFilterValue is non-empty', () => {
expect(countActiveWorkloadsFilters({ ...inactiveOptions(), hostFilterValue: 'host-1' })).toBe(
1,
);
});
it('counts only the platformFilter arm when platformFilterValue is non-empty', () => {
expect(
countActiveWorkloadsFilters({ ...inactiveOptions(), platformFilterValue: 'vmware' }),
).toBe(1);
});
it('counts only the namespaceFilter arm when namespaceFilterValue is non-empty', () => {
expect(
countActiveWorkloadsFilters({ ...inactiveOptions(), namespaceFilterValue: 'kube-system' }),
).toBe(1);
});
it('counts only the clusterFilter arm when clusterFilterValue is non-empty', () => {
expect(
countActiveWorkloadsFilters({ ...inactiveOptions(), clusterFilterValue: 'prod-us-1' }),
).toBe(1);
});
it('counts only the containerRuntimeFilter arm when containerRuntimeFilterValue is non-empty', () => {
expect(
countActiveWorkloadsFilters({
...inactiveOptions(),
containerRuntimeFilterValue: 'containerd',
}),
).toBe(1);
});
});
describe('countActiveWorkloadsFilters — all arms active simultaneously', () => {
it('returns 8 when every arm is set to a non-default value', () => {
expect(
countActiveWorkloadsFilters({
search: 'nginx',
viewMode: 'vm',
statusMode: 'running',
hostFilterValue: 'host-1',
platformFilterValue: 'vmware',
namespaceFilterValue: 'kube-system',
clusterFilterValue: 'prod-us-1',
containerRuntimeFilterValue: 'containerd',
}),
).toBe(8);
});
});
describe('hasActiveWorkloadsFilters — wraps countActiveWorkloadsFilters', () => {
it('returns false when the count is 0 (all-default baseline)', () => {
expect(hasActiveWorkloadsFilters(inactiveOptions())).toBe(false);
});
it('returns true when the count is greater than 0 (single search arm active)', () => {
expect(hasActiveWorkloadsFilters({ ...inactiveOptions(), search: 'nginx' })).toBe(true);
});
it('returns true when every arm is active (count === 8)', () => {
expect(
hasActiveWorkloadsFilters({
search: 'nginx',
viewMode: 'vm',
statusMode: 'running',
hostFilterValue: 'host-1',
platformFilterValue: 'vmware',
namespaceFilterValue: 'kube-system',
clusterFilterValue: 'prod-us-1',
containerRuntimeFilterValue: 'containerd',
}),
).toBe(true);
});
});
@@ -0,0 +1,148 @@
import { describe, expect, it } from 'vitest';
import {
easeAnimatedNumberProgress,
formatAnimatedInteger,
sanitizeAnimatedNumberValue,
} from '@/components/shared/animatedNumberModel';
// Every assertion below is a hand-computed expected value against the real
// runtime output of the three exported helpers in animatedNumberModel.ts
// (no `?raw` source-string reads, no snapshots, no constant-equals-itself
// tautologies). See src/components/shared/animatedNumberModel.ts.
describe('animatedNumberModel.branchcov0720pm', () => {
describe('sanitizeAnimatedNumberValue', () => {
it('returns 0 for NaN (!Number.isFinite(value) true arm)', () => {
// NaN is not finite -> early-return 0.
expect(sanitizeAnimatedNumberValue(NaN)).toBe(0);
});
it('returns 0 for +Infinity (!Number.isFinite(value) true arm)', () => {
expect(sanitizeAnimatedNumberValue(Infinity)).toBe(0);
});
it('returns 0 for -Infinity (!Number.isFinite(value) true arm)', () => {
expect(sanitizeAnimatedNumberValue(-Infinity)).toBe(0);
});
it('returns a positive finite value verbatim (happy path, identity arm)', () => {
// Number.isFinite(42) -> true -> fall through to `return value`.
expect(sanitizeAnimatedNumberValue(42)).toBe(42);
});
it('returns a negative finite value verbatim (happy path, identity arm)', () => {
expect(sanitizeAnimatedNumberValue(-7)).toBe(-7);
});
it('returns 0 verbatim at the zero boundary (happy path, identity arm)', () => {
// 0 is finite, so the guard is skipped — the value flows through unchanged.
expect(sanitizeAnimatedNumberValue(0)).toBe(0);
});
it('returns a fractional finite value verbatim (happy path, identity arm)', () => {
expect(sanitizeAnimatedNumberValue(3.14)).toBe(3.14);
});
});
describe('formatAnimatedInteger', () => {
// `String(Math.round(sanitizeAnimatedNumberValue(value)))` — composition of
// sanitize (guard) + Math.round + String(). Each case locks a distinct arm
// of that pipeline by asserting on the observable string output.
it('formats a representative positive integer as its decimal string', () => {
// sanitize(42) -> 42; Math.round(42) -> 42; String(42) -> '42'.
expect(formatAnimatedInteger(42)).toBe('42');
});
it('rounds a fractional value up to the nearest integer before formatting', () => {
// Math.round(42.7) -> 43.
expect(formatAnimatedInteger(42.7)).toBe('43');
});
it('rounds a fractional value down to the nearest integer before formatting', () => {
// Math.round(42.4) -> 42.
expect(formatAnimatedInteger(42.4)).toBe('42');
});
it('formats a negative integer (preserves the leading minus)', () => {
expect(formatAnimatedInteger(-5)).toBe('-5');
});
it('formats 0 as "0" (zero boundary, no rounding artefacts)', () => {
expect(formatAnimatedInteger(0)).toBe('0');
});
it('formats a value just below .5 by rounding down (Math.round half-up boundary)', () => {
// Math.round(0.4999) -> 0.
expect(formatAnimatedInteger(0.4999)).toBe('0');
});
it('formats a value at exactly .5 by rounding up (Math.round half-up boundary)', () => {
// Math.round(0.5) -> 1.
expect(formatAnimatedInteger(0.5)).toBe('1');
});
it('sanitizes NaN to "0" via the sanitize guard before rounding', () => {
// sanitize(NaN) -> 0; Math.round(0) -> 0; String(0) -> '0'.
expect(formatAnimatedInteger(NaN)).toBe('0');
});
it('sanitizes +Infinity to "0" via the sanitize guard before rounding', () => {
expect(formatAnimatedInteger(Infinity)).toBe('0');
});
it('sanitizes -Infinity to "0" via the sanitize guard before rounding', () => {
expect(formatAnimatedInteger(-Infinity)).toBe('0');
});
});
describe('easeAnimatedNumberProgress', () => {
// `Math.max(0, Math.min(progress, 1))` clamps to [0, 1]; then cubic ease-out
// `1 - (1 - bounded) ^ 3`. Each case drives a distinct clamp arm and asserts
// a hand-computed expected numeric output.
it('clamps progress < 0 to 0 and yields the eased value 0 (max-wins arm)', () => {
// bounded = max(0, min(-0.5, 1)) = max(0, -0.5) = 0
// eased = 1 - (1 - 0) ^ 3 = 1 - 1 = 0
expect(easeAnimatedNumberProgress(-0.5)).toBe(0);
});
it('clamps progress > 1 to 1 and yields the eased value 1 (min-wins arm)', () => {
// bounded = max(0, min(1.5, 1)) = max(0, 1) = 1
// eased = 1 - (1 - 1) ^ 3 = 1 - 0 = 1
expect(easeAnimatedNumberProgress(1.5)).toBe(1);
});
it('passes an in-range progress through both clamps untouched (identity arm)', () => {
// bounded = max(0, min(0.5, 1)) = max(0, 0.5) = 0.5
// eased = 1 - (1 - 0.5) ^ 3 = 1 - 0.125 = 0.875
expect(easeAnimatedNumberProgress(0.5)).toBe(0.875);
});
it('eases the quarter-progress point with the cubic curve (identity arm, second sample)', () => {
// bounded = 0.25 -> eased = 1 - (0.75) ^ 3 = 1 - 0.421875 = 0.578125
expect(easeAnimatedNumberProgress(0.25)).toBe(0.578125);
});
it('returns 0 at the progress === 0 lower boundary (no clamp needed)', () => {
// bounded = 0 -> eased = 1 - 1 = 0.
expect(easeAnimatedNumberProgress(0)).toBe(0);
});
it('returns 1 at the progress === 1 upper boundary (no clamp needed)', () => {
// bounded = 1 -> eased = 1 - 0 = 1.
expect(easeAnimatedNumberProgress(1)).toBe(1);
});
it('clamps a deeply-negative progress to 0 (max-wins arm, far side)', () => {
// bounded = max(0, min(-100, 1)) = 0 -> eased = 0.
expect(easeAnimatedNumberProgress(-100)).toBe(0);
});
it('clamps a large overflow progress to 1 (min-wins arm, far side)', () => {
// bounded = max(0, min(100, 1)) = 1 -> eased = 1.
expect(easeAnimatedNumberProgress(100)).toBe(1);
});
});
});
@@ -0,0 +1,317 @@
import { describe, expect, it } from 'vitest';
import {
calculateHelpPopoverPosition,
getHelpIconSize,
getHelpPopoverMaxWidth,
getHelpPopoverPreferredPosition,
getMissingHelpContentWarning,
helpIconSizeClasses,
resolveHelpContent,
} from '@/components/shared/helpIconModel';
import type { HelpIconProps } from '@/components/shared/helpIconModel';
// Module-internal constants mirrored here so each geometry assertion is a
// concrete, hand-computed expected value rather than a re-derivation of source.
// VIEWPORT_PADDING (min inset from any viewport edge) = 8
// POPOVER_OFFSET (gap between the anchor button and the popover) = 8
const VIEWPORT_PADDING = 8;
const POPOVER_OFFSET = 8;
// Minimal DOMRect builder. `calculateHelpPopoverPosition` reads
// `left`, `width`, `top`, `bottom` off buttonRect and `width`, `height` off
// popoverRect, so we derive `right`/`bottom` from the position + size to mirror
// real DOMRect semantics (callers pass only left/top/width/height).
const makeRect = (rect: {
left?: number;
top?: number;
width?: number;
height?: number;
}): DOMRect => {
const left = rect.left ?? 0;
const top = rect.top ?? 0;
const width = rect.width ?? 0;
const height = rect.height ?? 0;
return {
left,
top,
right: left + width,
bottom: top + height,
width,
height,
x: left,
y: top,
toJSON: () => ({}),
} as DOMRect;
};
describe('helpIconModel.branchcov0720pm', () => {
describe('resolveHelpContent — branch coverage', () => {
it('returns the inline-shaped HelpContent when props.inline is truthy (inline arm)', () => {
// The inline arm builds a brand-new object with id: 'inline' and copies
// title/description/examples/docUrl off props.inline verbatim.
const props: HelpIconProps = {
inline: {
title: 'Inline help',
description: 'Inline description',
examples: ['Example A', 'Example B'],
docUrl: 'https://example.com/docs/inline',
},
};
const result = resolveHelpContent(props);
expect(result).toEqual({
id: 'inline',
title: 'Inline help',
description: 'Inline description',
examples: ['Example A', 'Example B'],
docUrl: 'https://example.com/docs/inline',
});
});
it('omits examples/docUrl from the inline shape when the inline content omits them', () => {
// Same inline arm; the source spreads `examples: props.inline.examples`
// and `docUrl: props.inline.docUrl` directly, so undefined fields stay
// undefined and do not appear on the returned object.
const result = resolveHelpContent({
inline: { title: 'Bare', description: 'No extras' },
});
expect(result).toEqual({
id: 'inline',
title: 'Bare',
description: 'No extras',
examples: undefined,
docUrl: undefined,
});
expect(result).not.toHaveProperty('related');
expect(result).not.toHaveProperty('addedInVersion');
});
it('prefers inline content over a contentId when both are supplied (inline arm wins)', () => {
// The inline check comes first, so a present `inline` short-circuits the
// contentId lookup entirely.
const result = resolveHelpContent({
contentId: 'alerts.thresholds.delay',
inline: { title: 'Wins', description: 'Inline beats contentId' },
});
expect(result?.id).toBe('inline');
expect(result?.title).toBe('Wins');
});
it('delegates to getHelpContent when only props.contentId is set (contentId arm)', () => {
// Real registry hit: 'alerts.thresholds.delay' is a registered entry in
// src/content/help/alerts.ts, so we get the live HelpContent back.
const result = resolveHelpContent({ contentId: 'alerts.thresholds.delay' });
expect(result).toBeDefined();
expect(result?.id).toBe('alerts.thresholds.delay');
expect(result?.title).toBe('Alert Delay (Sustained Duration)');
expect(typeof result?.description).toBe('string');
expect(result?.description).toContain('Alert delay');
expect(result?.examples).toEqual(
expect.arrayContaining([expect.stringContaining('30 seconds')]),
);
expect(result?.addedInVersion).toBe('v4.0.0');
});
it('returns undefined from the contentId arm when the id is not in the registry', () => {
// getHelpContent returns undefined for an unknown id -> resolveHelpContent
// returns that undefined verbatim.
const result = resolveHelpContent({ contentId: 'no.such.content.id' });
expect(result).toBeUndefined();
});
it('returns undefined when neither inline nor contentId is supplied (neither arm)', () => {
// Both guards (props.inline, props.contentId) are falsy on an empty
// object -> falls through to the trailing `return undefined`.
expect(resolveHelpContent({})).toBeUndefined();
});
});
describe('calculateHelpPopoverPosition — top/bottom and viewport-flip branches', () => {
it("opens above the button when preferredPosition='top' and there is room (top-fits arm)", () => {
// viewport 1000x800, button at top:200 height:20 -> bottom:220.
// popover height 100 -> top = 200 - 100 - 8 = 92 (>= padding, no flip).
const result = calculateHelpPopoverPosition({
buttonRect: makeRect({ left: 500, top: 200, width: 20, height: 20 }),
popoverRect: makeRect({ width: 200, height: 100 }),
preferredPosition: 'top',
viewportWidth: 1000,
viewportHeight: 800,
});
// left = 500 + 20/2 - 200/2 = 500 + 10 - 100 = 410 (in range, no clamp).
expect(result).toEqual({ top: 200 - 100 - POPOVER_OFFSET, left: 410 });
});
it("flips below the button when preferredPosition='top' would overflow the top of the viewport", () => {
// button top:50, popover height:100 -> top = 50 - 100 - 8 = -58 (< 8).
// Flip arm: top = buttonRect.bottom + POPOVER_OFFSET = 70 + 8 = 78.
const result = calculateHelpPopoverPosition({
buttonRect: makeRect({ left: 500, top: 50, width: 20, height: 20 }),
popoverRect: makeRect({ width: 200, height: 100 }),
preferredPosition: 'top',
viewportWidth: 1000,
viewportHeight: 800,
});
expect(result.top).toBe(50 + 20 + POPOVER_OFFSET);
expect(result.left).toBe(410);
});
it("opens below the button when preferredPosition='bottom' and there is room (bottom-fits arm)", () => {
// button bottom:420, popover height:100 -> top = 420 + 8 = 428.
// 428 + 100 = 528 <= 800 - 8 = 792 -> no flip.
const result = calculateHelpPopoverPosition({
buttonRect: makeRect({ left: 500, top: 400, width: 20, height: 20 }),
popoverRect: makeRect({ width: 200, height: 100 }),
preferredPosition: 'bottom',
viewportWidth: 1000,
viewportHeight: 800,
});
expect(result).toEqual({ top: 420 + POPOVER_OFFSET, left: 410 });
});
it("flips above the button when preferredPosition='bottom' would overflow the bottom of the viewport", () => {
// button bottom:770, popover height:100 -> top = 770 + 8 = 778.
// 778 + 100 = 878 > 800 - 8 = 792 -> flip arm.
// top = buttonRect.top - popoverRect.height - POPOVER_OFFSET = 750 - 100 - 8 = 642.
const result = calculateHelpPopoverPosition({
buttonRect: makeRect({ left: 500, top: 750, width: 20, height: 20 }),
popoverRect: makeRect({ width: 200, height: 100 }),
preferredPosition: 'bottom',
viewportWidth: 1000,
viewportHeight: 800,
});
expect(result.top).toBe(750 - 100 - POPOVER_OFFSET);
expect(result.left).toBe(410);
});
it('clamps left to VIEWPORT_PADDING when the centered popover would overflow the left edge', () => {
// button at left:0 width:20 -> center at x=10 -> popover left = 10 - 100 = -90.
// max(8, min(-90, 792)) = max(8, -90) = 8.
const result = calculateHelpPopoverPosition({
buttonRect: makeRect({ left: 0, top: 400, width: 20, height: 20 }),
popoverRect: makeRect({ width: 200, height: 100 }),
preferredPosition: 'top',
viewportWidth: 1000,
viewportHeight: 800,
});
expect(result.left).toBe(VIEWPORT_PADDING);
// top axis is unaffected: 400 - 100 - 8 = 292.
expect(result.top).toBe(292);
});
it('clamps left to viewportWidth - popoverRect.width - VIEWPORT_PADDING when overflowing right', () => {
// button at left:990 width:20 -> center at x=1000 -> popover left = 1000 - 100 = 900.
// maxRight = 1000 - 200 - 8 = 792 -> min(900, 792) = 792 -> max(8, 792) = 792.
const result = calculateHelpPopoverPosition({
buttonRect: makeRect({ left: 990, top: 400, width: 20, height: 20 }),
popoverRect: makeRect({ width: 200, height: 100 }),
preferredPosition: 'top',
viewportWidth: 1000,
viewportHeight: 800,
});
expect(result.left).toBe(1000 - 200 - VIEWPORT_PADDING);
expect(result.top).toBe(292);
});
it('clamps the final top to VIEWPORT_PADDING when the post-flip value is still negative', () => {
// Edge case: top arm flips to bottom, but bottom also pushes past viewport
// bottom, so the trailing top clamp pulls it back down to VIEWPORT_PADDING.
// viewport 100x80, button top:0 height:4 bottom:4, popover height:100.
// top arm: top = 0 - 100 - 8 = -108 (< 8) -> flip -> top = 4 + 8 = 12.
// Final clamp: max(8, min(12, 80 - 100 - 8 = -28)) = max(8, -28) = 8.
const result = calculateHelpPopoverPosition({
buttonRect: makeRect({ left: 10, top: 0, width: 4, height: 4 }),
popoverRect: makeRect({ width: 20, height: 100 }),
preferredPosition: 'top',
viewportWidth: 100,
viewportHeight: 80,
});
expect(result.top).toBe(VIEWPORT_PADDING);
});
it('clamps the final top to viewportHeight - popoverRect.height - VIEWPORT_PADDING on the bottom arm', () => {
// viewport 100x80, button bottom near the bottom edge, popover taller than viewport.
// bottom arm: top = buttonRect.bottom + 8.
// Final clamp: max(8, min(top, 80 - 100 - 8 = -28)) = max(8, -28) = 8.
const result = calculateHelpPopoverPosition({
buttonRect: makeRect({ left: 10, top: 70, width: 4, height: 4 }),
popoverRect: makeRect({ width: 20, height: 100 }),
preferredPosition: 'bottom',
viewportWidth: 100,
viewportHeight: 80,
});
expect(result.top).toBe(VIEWPORT_PADDING);
});
it('returns a plain { top, left } object with no extra enumerable keys', () => {
const result = calculateHelpPopoverPosition({
buttonRect: makeRect({ left: 100, top: 100, width: 10, height: 10 }),
popoverRect: makeRect({ width: 50, height: 50 }),
preferredPosition: 'top',
viewportWidth: 1000,
viewportHeight: 800,
});
expect(Object.keys(result).sort()).toEqual(['left', 'top']);
expect(typeof result.left).toBe('number');
expect(typeof result.top).toBe('number');
});
});
describe('getHelpIconSize — ?? fallback branches', () => {
it("returns 'sm' when size is omitted (right operand arm)", () => {
expect(getHelpIconSize(undefined)).toBe('sm');
});
it('returns the supplied size verbatim when set (left operand arm)', () => {
expect(getHelpIconSize('xs')).toBe('xs');
expect(getHelpIconSize('sm')).toBe('sm');
expect(getHelpIconSize('md')).toBe('md');
});
});
describe('getHelpPopoverMaxWidth — ?? fallback branches', () => {
it('returns 320 when maxWidth is omitted (right operand arm)', () => {
expect(getHelpPopoverMaxWidth(undefined)).toBe(320);
});
it('returns the supplied maxWidth verbatim when set (left operand arm)', () => {
expect(getHelpPopoverMaxWidth(200)).toBe(200);
expect(getHelpPopoverMaxWidth(0)).toBe(0);
});
});
describe('getHelpPopoverPreferredPosition — ?? fallback branches', () => {
it("returns 'top' when position is omitted (right operand arm)", () => {
expect(getHelpPopoverPreferredPosition(undefined)).toBe('top');
});
it('returns the supplied position verbatim when set (left operand arm)', () => {
expect(getHelpPopoverPreferredPosition('top')).toBe('top');
expect(getHelpPopoverPreferredPosition('bottom')).toBe('bottom');
});
});
describe('getMissingHelpContentWarning — branch coverage', () => {
it('returns undefined when contentId is falsy (early-return arm)', () => {
expect(getMissingHelpContentWarning(undefined)).toBeUndefined();
expect(getMissingHelpContentWarning('')).toBeUndefined();
});
it('formats the warning string with the supplied contentId (template arm)', () => {
expect(getMissingHelpContentWarning('alerts.thresholds.delay')).toBe(
'[HelpIcon] No content found for ID: alerts.thresholds.delay',
);
expect(getMissingHelpContentWarning('some.missing.id')).toBe(
'[HelpIcon] No content found for ID: some.missing.id',
);
});
});
describe('helpIconSizeClasses — constant map contents', () => {
it('exposes a tailwind class for every declared size and nothing else', () => {
expect(Object.keys(helpIconSizeClasses).sort()).toEqual(['md', 'sm', 'xs']);
expect(helpIconSizeClasses.xs).toBe('w-3 h-3');
expect(helpIconSizeClasses.sm).toBe('w-3.5 h-3.5');
expect(helpIconSizeClasses.md).toBe('w-4 h-4');
});
});
});
@@ -0,0 +1,276 @@
import { describe, expect, it } from 'vitest';
import {
getSelectionCardButtonClass,
getSelectionCardDescriptionClass,
getSelectionCardGroupClass,
getSelectionCardIconContainerClass,
getSelectionCardTitleClass,
resolveSelectionCardGroupVariant,
resolveSelectionCardTone,
} from '@/components/shared/selectionCardGroupModel';
import type {
SelectionCardGroupVariant,
SelectionCardTone,
} from '@/components/shared/selectionCardGroupModel';
// Module-internal class-string fragments mirrored here so every assertion is a
// concrete, hand-copied expected value (no `?raw` source string reads, no
// snapshots, no constant-equals-itself tautologies). See
// src/components/shared/selectionCardGroupModel.ts.
const GROUP_CLASS_COMPACT = 'grid grid-cols-2 gap-2';
const GROUP_CLASS_DETAIL = 'grid grid-cols-1 gap-3';
const BUTTON_BASE_DETAIL = 'p-4 rounded-md border-2 transition-all text-left';
const BUTTON_BASE_COMPACT = 'p-3 rounded-md border-2 transition-all text-center';
const ACTIVE_SUCCESS = 'border-green-500 bg-green-50 dark:bg-green-900';
const ACTIVE_ACCENT = 'border-blue-500 bg-blue-50 dark:bg-blue-900';
const INACTIVE_COMPACT = 'border-border hover:border-blue-300';
const INACTIVE_DETAIL = 'border-border hover:border-border';
const DISABLED_CLASSES = 'disabled:opacity-50 disabled:cursor-not-allowed';
describe('selectionCardGroupModel.branchcov0720pm', () => {
describe('resolveSelectionCardGroupVariant', () => {
it("defaults to 'detail' when variant is undefined (?? right arm)", () => {
expect(resolveSelectionCardGroupVariant(undefined)).toBe('detail');
});
it("returns 'compact' verbatim when explicitly passed (identity arm)", () => {
expect(resolveSelectionCardGroupVariant('compact')).toBe('compact');
});
it("returns 'detail' verbatim when explicitly passed (identity arm)", () => {
expect(resolveSelectionCardGroupVariant('detail')).toBe('detail');
});
});
describe('resolveSelectionCardTone', () => {
it("defaults to 'accent' when tone is undefined (?? right arm)", () => {
expect(resolveSelectionCardTone(undefined)).toBe('accent');
});
it("returns 'accent' verbatim when explicitly passed (identity arm)", () => {
expect(resolveSelectionCardTone('accent')).toBe('accent');
});
it("returns 'success' verbatim when explicitly passed (identity arm)", () => {
expect(resolveSelectionCardTone('success')).toBe('success');
});
});
describe('getSelectionCardGroupClass', () => {
it("emits the detail grid tokens when variant='detail' and no className is supplied (?? '' arm, then trim)", () => {
// `${groupClassByVariant[variant]} ${className ?? ''}`.trim()
// With className undefined the trailing ' ' is trimmed away.
expect(getSelectionCardGroupClass('detail', undefined)).toBe(GROUP_CLASS_DETAIL);
});
it("emits the compact grid tokens when variant='compact' and no className is supplied", () => {
expect(getSelectionCardGroupClass('compact', undefined)).toBe(GROUP_CLASS_COMPACT);
});
it("appends a caller-supplied className for variant='detail' (className truthy arm)", () => {
expect(getSelectionCardGroupClass('detail', 'mt-4')).toBe(`${GROUP_CLASS_DETAIL} mt-4`);
});
it("appends a caller-supplied className for variant='compact' (className truthy arm)", () => {
expect(getSelectionCardGroupClass('compact', 'sm:grid-cols-3')).toBe(
`${GROUP_CLASS_COMPACT} sm:grid-cols-3`,
);
});
});
describe('getSelectionCardButtonClass', () => {
it("uses the detail (text-left) base, accent active classes, and skips the disabled tail when disabled=false and active=true with tone='accent'", () => {
// base: variant === 'detail' true arm.
// active true + tone accent -> getSelectionCardActiveClass('accent').
// disabled false -> '' third slot, which still contributes a single space.
const expected = [BUTTON_BASE_DETAIL, ACTIVE_ACCENT, ''].join(' ');
expect(getSelectionCardButtonClass('detail', 'accent', true, false)).toBe(expected);
// Sanity-check the three observable fragments directly.
expect(getSelectionCardButtonClass('detail', 'accent', true, false)).toContain(
BUTTON_BASE_DETAIL,
);
expect(getSelectionCardButtonClass('detail', 'accent', true, false)).toContain(ACTIVE_ACCENT);
expect(getSelectionCardButtonClass('detail', 'accent', true, false)).not.toContain(
DISABLED_CLASSES,
);
});
it("uses the success active classes for tone='success' (getSelectionCardActiveClass success arm)", () => {
const result = getSelectionCardButtonClass('detail', 'success', true, false);
expect(result).toContain(ACTIVE_SUCCESS);
expect(result).not.toContain(ACTIVE_ACCENT);
});
it("uses the compact (text-center) base when variant='compact' (variant === 'detail' false arm)", () => {
const result = getSelectionCardButtonClass('compact', 'accent', true, false);
expect(result).toContain(BUTTON_BASE_COMPACT);
expect(result).not.toContain(BUTTON_BASE_DETAIL);
});
it("uses the compact inactive classes when active=false and variant='compact' (getSelectionCardInactiveClass compact arm)", () => {
const result = getSelectionCardButtonClass('compact', 'accent', false, false);
expect(result).toContain(INACTIVE_COMPACT);
expect(result).not.toContain(INACTIVE_DETAIL);
expect(result).not.toContain(ACTIVE_ACCENT);
});
it("uses the detail inactive classes when active=false and variant='detail' (getSelectionCardInactiveClass else arm)", () => {
const result = getSelectionCardButtonClass('detail', 'accent', false, false);
expect(result).toContain(INACTIVE_DETAIL);
expect(result).not.toContain(INACTIVE_COMPACT);
});
it("appends the disabled tail when disabled=true (disabled ? ... : '' true arm)", () => {
const result = getSelectionCardButtonClass('detail', 'accent', false, true);
expect(result).toContain(DISABLED_CLASSES);
// The button is still inactive here, so the inactive-detail classes must
// remain present alongside the disabled tail.
expect(result).toContain(INACTIVE_DETAIL);
});
it('joins base + active + disabled into a single string when all three slots are populated', () => {
const expected = [BUTTON_BASE_DETAIL, ACTIVE_SUCCESS, DISABLED_CLASSES].join(' ');
expect(getSelectionCardButtonClass('detail', 'success', true, true)).toBe(expected);
});
});
describe('getSelectionCardIconContainerClass', () => {
it("emits the green active container when tone='success' and active=true (tone === 'success' arm)", () => {
const result = getSelectionCardIconContainerClass('success', true);
expect(result).toBe('p-2 rounded-md bg-green-100 dark:bg-green-800');
expect(result).toContain('bg-green-100');
expect(result).toContain('dark:bg-green-800');
});
it("emits the blue active container when tone='accent' and active=true (else arm)", () => {
const result = getSelectionCardIconContainerClass('accent', true);
expect(result).toBe('p-2 rounded-md bg-blue-100 dark:bg-blue-800');
expect(result).toContain('bg-blue-100');
expect(result).not.toContain('bg-green-100');
});
it('emits the surface-alt container when active=false regardless of tone (active false arm)', () => {
expect(getSelectionCardIconContainerClass('accent', false)).toBe(
'p-2 rounded-md bg-surface-alt',
);
// Same surface-alt path for the success tone proves the inactive arm is
// independent of tone.
expect(getSelectionCardIconContainerClass('success', false)).toBe(
'p-2 rounded-md bg-surface-alt',
);
});
});
describe('getSelectionCardTitleClass', () => {
it("returns the compact title class for variant='compact' regardless of tone/active (variant === 'compact' early return)", () => {
const expected = 'text-sm font-medium text-base-content';
// active true, success tone — but compact short-circuits before tone is read.
expect(getSelectionCardTitleClass('compact', 'success', true)).toBe(expected);
// active false, accent tone — same early return.
expect(getSelectionCardTitleClass('compact', 'accent', false)).toBe(expected);
});
it("returns the muted title class for variant='detail' and active=false (!active early return)", () => {
// tone is irrelevant on this arm; only the !active check matters.
expect(getSelectionCardTitleClass('detail', 'success', false)).toBe(
'text-sm font-semibold text-base-content',
);
expect(getSelectionCardTitleClass('detail', 'accent', false)).toBe(
'text-sm font-semibold text-base-content',
);
});
it("returns the green title class for variant='detail', active=true, tone='success' (tone === 'success' arm)", () => {
expect(getSelectionCardTitleClass('detail', 'success', true)).toBe(
'text-sm font-semibold text-green-900 dark:text-green-100',
);
});
it("returns the blue title class for variant='detail', active=true, tone='accent' (else arm)", () => {
expect(getSelectionCardTitleClass('detail', 'accent', true)).toBe(
'text-sm font-semibold text-blue-900 dark:text-blue-100',
);
});
});
describe('getSelectionCardDescriptionClass', () => {
it("emits the compact description class for variant='compact' (variant === 'compact' true arm)", () => {
expect(getSelectionCardDescriptionClass('compact')).toBe('text-xs text-slate-500 mt-0.5');
});
it("emits the detail description class for variant='detail' (else arm)", () => {
expect(getSelectionCardDescriptionClass('detail')).toBe('text-xs text-muted');
});
});
// Cross-product sanity sweep — locks the tone x variant x active matrix that
// callers rely on so a future refactor cannot silently swap a class fragment
// for a different arm. Each cell asserts the tone/variant/active-specific
// tokens that the source actually emits for that combination.
describe('tone x variant x active matrix', () => {
const tones: SelectionCardTone[] = ['accent', 'success'];
const variants: SelectionCardGroupVariant[] = ['compact', 'detail'];
it('selects the green or blue active class on the button based on tone, for every variant', () => {
for (const variant of variants) {
for (const tone of tones) {
const result = getSelectionCardButtonClass(variant, tone, true, false);
const activeToken =
tone === 'success' ? 'border-green-500 bg-green-50' : 'border-blue-500 bg-blue-50';
expect(result).toContain(activeToken);
}
}
});
it('selects the inactive class on the button based on variant, not tone, when active=false', () => {
for (const variant of variants) {
for (const tone of tones) {
const result = getSelectionCardButtonClass(variant, tone, false, false);
const inactiveToken =
variant === 'compact' ? 'hover:border-blue-300' : 'hover:border-border';
expect(result).toContain(inactiveToken);
// No active classes leak through when inactive.
expect(result).not.toContain('bg-green-50');
expect(result).not.toContain('bg-blue-50');
}
}
});
it('routes the icon container class by tone when active and by surface-alt when inactive', () => {
for (const tone of tones) {
const activeResult = getSelectionCardIconContainerClass(tone, true);
expect(activeResult).toContain(tone === 'success' ? 'bg-green-100' : 'bg-blue-100');
const inactiveResult = getSelectionCardIconContainerClass(tone, false);
expect(inactiveResult).toContain('bg-surface-alt');
}
});
it('switches the detail-mode active title colour between blue and green by tone', () => {
for (const tone of tones) {
const result = getSelectionCardTitleClass('detail', tone, true);
const titleToken =
tone === 'success'
? 'text-green-900 dark:text-green-100'
: 'text-blue-900 dark:text-blue-100';
expect(result).toContain(titleToken);
}
});
it('keeps the compact title class tone- and active-independent across the matrix', () => {
for (const tone of tones) {
for (const active of [true, false]) {
expect(getSelectionCardTitleClass('compact', tone, active)).toBe(
'text-sm font-medium text-base-content',
);
}
}
});
});
});
@@ -0,0 +1,127 @@
import { describe, expect, it } from 'vitest';
import {
canAddTag,
getNextTagsAfterRemove,
getTagInputPlaceholder,
} from '@/components/shared/tagInputModel';
// Every assertion below is a hand-computed expected value against the real
// runtime output of the three branching helpers in tagInputModel.ts (no
// `?raw` source-string reads, no snapshots, no constant-equals-itself
// tautologies). See src/components/shared/tagInputModel.ts.
describe('tagInputModel.branchcov0720pm', () => {
describe('getTagInputPlaceholder', () => {
// `tagCount === 0 ? (placeholder ?? '') : ''` — three arms: the
// tagCount===0 branch with a truthy placeholder, the `?? ''` fallback when
// placeholder is omitted, and the tagCount>0 else arm that always yields ''.
it('returns the placeholder verbatim when tagCount is 0 and a placeholder is supplied (ternary true arm)', () => {
expect(getTagInputPlaceholder(0, 'Add tags…')).toBe('Add tags…');
});
it('falls back to the empty string when tagCount is 0 and placeholder is undefined (?? right arm)', () => {
expect(getTagInputPlaceholder(0, undefined)).toBe('');
});
it('falls back to the empty string when tagCount is 0 and placeholder is omitted entirely (?? right arm, parameter missing)', () => {
// No second argument at all — same `?? ''` arm as passing undefined.
expect(getTagInputPlaceholder(0)).toBe('');
});
it('returns the empty string when tagCount > 0 regardless of placeholder (ternary false arm, placeholder supplied)', () => {
// A populated tag list hides the placeholder even when one is provided.
expect(getTagInputPlaceholder(3, 'Add tags…')).toBe('');
});
it('returns the empty string when tagCount > 0 and no placeholder is supplied (ternary false arm, placeholder missing)', () => {
expect(getTagInputPlaceholder(1, undefined)).toBe('');
});
it('returns the empty string when tagCount is large and a placeholder is supplied (ternary false arm, far side)', () => {
expect(getTagInputPlaceholder(1000, 'Add tags…')).toBe('');
});
});
describe('getNextTagsAfterRemove', () => {
// `tags.filter((_, index) => index !== indexToRemove)` — the filter
// callback branches true (keep) for non-matching indices and false (drop)
// for the matching index. Each case locks the observable output array.
it('removes the target tag at the given index and leaves the others untouched (middle element)', () => {
expect(getNextTagsAfterRemove(['a', 'b', 'c'], 1)).toEqual(['a', 'c']);
});
it('removes the first tag when indexToRemove === 0 (head element)', () => {
expect(getNextTagsAfterRemove(['a', 'b', 'c'], 0)).toEqual(['b', 'c']);
});
it('removes the last tag when indexToRemove === tags.length - 1 (tail element)', () => {
expect(getNextTagsAfterRemove(['a', 'b', 'c'], 2)).toEqual(['a', 'b']);
});
it('returns all tags unchanged when indexToRemove matches no index (out-of-range high)', () => {
// No index satisfies `index === indexToRemove`, so every element is kept.
expect(getNextTagsAfterRemove(['a', 'b', 'c'], 99)).toEqual(['a', 'b', 'c']);
});
it('returns all tags unchanged when indexToRemove is negative (no index matches)', () => {
// filter indices are 0-based and non-negative; -1 never matches.
expect(getNextTagsAfterRemove(['a', 'b', 'c'], -1)).toEqual(['a', 'b', 'c']);
});
it('returns a new empty array when removing from a single-element list at index 0', () => {
const result = getNextTagsAfterRemove(['only'], 0);
expect(result).toEqual([]);
// Defensive: confirm a fresh array, not the same reference.
expect(result).not.toBe(['only']);
});
it('returns a new array (does not mutate the input) and drops only the first match', () => {
// Duplicate tag values are independent by index — only index 1 is dropped.
const input = ['x', 'x', 'y'];
const result = getNextTagsAfterRemove(input, 1);
expect(result).toEqual(['x', 'y']);
expect(input).toEqual(['x', 'x', 'y']);
});
});
describe('canAddTag', () => {
// `Boolean(value) && !tags.includes(value)` — short-circuit AND with two
// distinct false arms (empty value, duplicate value) and one true arm.
it('returns true when value is non-empty and not already present (Boolean truthy && !includes true)', () => {
expect(canAddTag(['a', 'b'], 'c')).toBe(true);
});
it('returns true when the tags list is empty and value is non-empty (truthy && !includes on empty list)', () => {
expect(canAddTag([], 'first')).toBe(true);
});
it('returns false when value is an empty string (Boolean(value) falsy short-circuit)', () => {
// Empty string never reaches the .includes check.
expect(canAddTag(['a', 'b'], '')).toBe(false);
});
it('returns false when value is already in the tags list (duplicate, !includes false arm)', () => {
expect(canAddTag(['a', 'b', 'c'], 'b')).toBe(false);
});
it('returns false when the duplicate check fires even on an otherwise-empty list', () => {
// Single-element duplicate must still be rejected.
expect(canAddTag(['only'], 'only')).toBe(false);
});
it('treats a whitespace-only value as truthy (Boolean(" ") is true) and adds it when not duplicated', () => {
// NOTE: this is a real behaviour assertion — canAddTag does NOT trim,
// so a whitespace value passes the Boolean check. Trimming is the
// caller's responsibility (normalizeTagInputValue).
expect(canAddTag([], ' ')).toBe(true);
});
it('treats the string "0" as a valid addable tag (Boolean("0") is true)', () => {
expect(canAddTag([], '0')).toBe(true);
});
});
});
@@ -0,0 +1,180 @@
import { describe, expect, it } from 'vitest';
import {
DEFAULT_LOCALE,
SUPPORTED_LOCALES,
getLocaleFallbackChain,
resolveSupportedLocale,
} from '@/i18n/locales';
// `LOCALE_ALIASES`, `SUPPORTED_LOCALE_SET`, `isSupportedLocale`, and
// `normalizeLocale` are module-private (not exported); their branches are
// exercised transitively through `resolveSupportedLocale` and
// `getLocaleFallbackChain`, asserting on the observable return values.
describe('resolveSupportedLocale — branch coverage', () => {
describe('(a) direct supported-locale hit — isSupportedLocale(normalized) true arm', () => {
it('returns every supported locale verbatim (locks the supported set)', () => {
expect(SUPPORTED_LOCALES).toEqual(['en', 'de', 'es']);
for (const locale of SUPPORTED_LOCALES) {
expect(resolveSupportedLocale(locale)).toBe(locale);
}
});
it('lowercases upper/mixed-case input before the direct lookup (toLowerCase step)', () => {
// 'EN' -> trim -> 'EN' -> replace _ -> 'EN' -> toLowerCase -> 'en' -> direct hit.
expect(resolveSupportedLocale('EN')).toBe('en');
expect(resolveSupportedLocale('De')).toBe('de');
expect(resolveSupportedLocale('eS')).toBe('es');
});
});
describe('(b) normalization arm (trim + underscore->hyphen)', () => {
it('trims surrounding whitespace before the direct-hit lookup', () => {
// ' en ' -> trim -> 'en' -> direct hit.
expect(resolveSupportedLocale(' en ')).toBe('en');
// trim() also strips tabs and newlines.
expect(resolveSupportedLocale('\tde\n')).toBe('de');
});
it('rewrites underscores to hyphens before the alias lookup', () => {
// 'es_MX' -> 'es-MX' -> toLowerCase 'es-mx' -> LOCALE_ALIASES hit -> 'es'.
expect(resolveSupportedLocale('es_MX')).toBe('es');
// 'DE_AT' exercises both toLowerCase and underscore->hyphen before the alias hit.
expect(resolveSupportedLocale('DE_AT')).toBe('de');
});
it('rewrites underscores to hyphens before the base-locale fallback', () => {
// 'de_XX' (no alias entry) -> 'de-xx' -> not alias -> base 'de' supported -> 'de'.
expect(resolveSupportedLocale('de_XX')).toBe('de');
});
});
describe('(c) LOCALE_ALIASES lookup hit arm', () => {
it('maps English regional variants to en', () => {
expect(resolveSupportedLocale('en-GB')).toBe('en');
expect(resolveSupportedLocale('en-US')).toBe('en');
});
it('maps German regional variants to de', () => {
expect(resolveSupportedLocale('de-AT')).toBe('de');
expect(resolveSupportedLocale('de-CH')).toBe('de');
expect(resolveSupportedLocale('de-DE')).toBe('de');
});
it('maps Spanish regional variants (including es-419) to es', () => {
expect(resolveSupportedLocale('es-419')).toBe('es');
expect(resolveSupportedLocale('es-AR')).toBe('es');
expect(resolveSupportedLocale('es-MX')).toBe('es');
expect(resolveSupportedLocale('es-ES')).toBe('es');
expect(resolveSupportedLocale('es-US')).toBe('es');
});
});
describe('(d) base-locale fallback arm (region unlisted, base supported)', () => {
it('falls back to the base locale when the region is not aliased but the base is supported', () => {
// 'de-XX' -> not direct, not in LOCALE_ALIASES, base 'de' supported -> 'de'.
expect(resolveSupportedLocale('de-XX')).toBe('de');
// 'en-CA' -> not in alias map -> base 'en' supported -> 'en'.
expect(resolveSupportedLocale('en-CA')).toBe('en');
// 'es-EC' -> not in alias map -> base 'es' supported -> 'es'.
expect(resolveSupportedLocale('es-EC')).toBe('es');
});
});
describe('null/false arm — base unsupported or input falsy', () => {
it('returns null when the base locale is itself unsupported (ternary false arm)', () => {
// 'fr-FR' -> not direct, not alias, base 'fr' NOT supported -> null.
expect(resolveSupportedLocale('fr-FR')).toBeNull();
// 'zh-Hans' -> base 'zh' not supported -> null.
expect(resolveSupportedLocale('zh-Hans')).toBeNull();
// 'pt-BR' -> base 'pt' not supported -> null.
expect(resolveSupportedLocale('pt-BR')).toBeNull();
// 'ja-JP' -> base 'ja' not supported -> null.
expect(resolveSupportedLocale('ja-JP')).toBeNull();
// A bare unsupported code with no region also returns null.
expect(resolveSupportedLocale('fr')).toBeNull();
expect(resolveSupportedLocale('xyz')).toBeNull();
});
it('returns null for null input (optional-chain short-circuits to undefined)', () => {
// value?.trim().replace().toLowerCase() -> undefined -> !normalized -> null.
expect(resolveSupportedLocale(null)).toBeNull();
});
it('returns null for undefined input (optional-chain short-circuits to undefined)', () => {
expect(resolveSupportedLocale(undefined)).toBeNull();
});
it('returns null for the empty string (normalized is falsy)', () => {
// '' -> trim '' -> replace '' -> toLowerCase '' -> !normalized -> null.
expect(resolveSupportedLocale('')).toBeNull();
});
it('returns null for a whitespace-only string (trims to empty -> falsy)', () => {
expect(resolveSupportedLocale(' ')).toBeNull();
expect(resolveSupportedLocale('\t\n')).toBeNull();
});
});
});
describe('getLocaleFallbackChain — branch coverage', () => {
describe('single-element chain arm (locale === fallbackLocale)', () => {
it('returns [locale] when the resolved locale is its own fallback (en)', () => {
// SUPPORTED_LOCALE_REGISTRY.en.fallbackLocale === 'en' -> locale === fallbackLocale
// -> ternary true -> [locale].
expect(getLocaleFallbackChain('en')).toEqual(['en']);
});
it('returns a single-element chain when a regional alias resolves to en', () => {
// 'en-GB' -> resolveSupportedLocale -> 'en' -> 'en' === 'en' -> ['en'].
expect(getLocaleFallbackChain('en-GB')).toEqual(['en']);
});
it('returns a single-element chain for null/undefined/empty input (defaults to en)', () => {
// normalizeLocale falls back to DEFAULT_LOCALE -> ['en'].
expect(DEFAULT_LOCALE).toBe('en');
expect(getLocaleFallbackChain(null)).toEqual(['en']);
expect(getLocaleFallbackChain(undefined)).toEqual(['en']);
expect(getLocaleFallbackChain('')).toEqual(['en']);
});
it('returns a single-element chain for an unsupported locale (defaults to en)', () => {
// 'fr-FR' resolves to null -> normalizeLocale -> DEFAULT_LOCALE 'en' -> ['en'].
expect(getLocaleFallbackChain('fr-FR')).toEqual(['en']);
});
});
describe('multi-element chain arm (locale !== fallbackLocale)', () => {
it('returns [locale, fallback] for the de supported locale', () => {
// SUPPORTED_LOCALE_REGISTRY.de.fallbackLocale === 'en', 'de' !== 'en'
// -> ternary false -> [locale, fallbackLocale].
expect(getLocaleFallbackChain('de')).toEqual(['de', 'en']);
});
it('returns [locale, fallback] for the es supported locale', () => {
expect(getLocaleFallbackChain('es')).toEqual(['es', 'en']);
});
it('returns a multi-element chain when a regional alias resolves to de/es', () => {
expect(getLocaleFallbackChain('de-AT')).toEqual(['de', 'en']);
expect(getLocaleFallbackChain('es-MX')).toEqual(['es', 'en']);
});
it('returns a multi-element chain when a base-fallback region resolves to de', () => {
// 'de-XX' base-fallback -> 'de' -> ['de', 'en'].
expect(getLocaleFallbackChain('de-XX')).toEqual(['de', 'en']);
});
});
describe('return shape', () => {
it('always emits members of SUPPORTED_LOCALES', () => {
for (const input of ['en', 'de', 'es', 'en-GB', 'de-AT', 'es-MX', 'de-XX', 'fr-FR', null]) {
const chain = getLocaleFallbackChain(input);
for (const member of chain) {
expect((SUPPORTED_LOCALES as readonly string[]).includes(member)).toBe(true);
}
}
});
});
});
@@ -0,0 +1,180 @@
import { describe, expect, it } from 'vitest';
import { buildPowerShellInstallScriptBootstrap } from '@/utils/agentInstallCommand';
// `normalizeInstallerBaseUrl` and `powerShellQuote` are module-private to the
// bootstrap builder's call graph; their branches are exercised transitively
// through the single exported entry point below, asserting on the observable
// PowerShell string that the builder emits.
describe('buildPowerShellInstallScriptBootstrap — URL validation branch coverage', () => {
it('throws the required-URL error when baseUrl is the empty string', () => {
expect(() => buildPowerShellInstallScriptBootstrap('')).toThrow(
'Pulse install endpoint URL is required.',
);
});
it('throws the required-URL error when baseUrl is whitespace-only', () => {
// normalizeInstallerBaseUrl does not trim; only the post-normalize .trim()
// guard catches this. Asserts the guard fires on whitespace-only input.
expect(() => buildPowerShellInstallScriptBootstrap(' ')).toThrow(
'Pulse install endpoint URL is required.',
);
});
it('throws the required-URL error when baseUrl is only trailing slashes', () => {
// normalizeInstallerBaseUrl strips the slashes -> '' -> trim() === '' -> throw.
expect(() => buildPowerShellInstallScriptBootstrap('///')).toThrow(
'Pulse install endpoint URL is required.',
);
});
it('does NOT throw on the non-empty happy path (https URL)', () => {
expect(() => buildPowerShellInstallScriptBootstrap('https://pulse.example')).not.toThrow();
});
it('does NOT throw on the non-empty happy path (plain-http URL)', () => {
expect(() => buildPowerShellInstallScriptBootstrap('http://pulse.example:7655')).not.toThrow();
});
});
describe('buildPowerShellInstallScriptBootstrap — baseUrl normalization branch coverage', () => {
it('strips a single trailing slash before appending /install.ps1', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example/');
expect(script).toContain('$pulseScriptUrl="https://pulse.example/install.ps1"');
expect(script).not.toContain('https://pulse.example//install.ps1');
});
it('strips multiple trailing slashes before appending /install.ps1', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example/base///');
expect(script).toContain('$pulseScriptUrl="https://pulse.example/base/install.ps1"');
expect(script).not.toContain('https://pulse.example/base//install.ps1');
expect(script).not.toContain('https://pulse.example/base///install.ps1');
});
it('preserves a baseUrl that already has no trailing slash (no-op normalization arm)', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example:7655');
expect(script).toContain('$pulseScriptUrl="https://pulse.example:7655/install.ps1"');
});
it('does not rewrite the URL scheme (https stays https, http stays http)', () => {
// The builder performs no scheme upgrade/downgrade — it only strips trailing slashes.
const httpsScript = buildPowerShellInstallScriptBootstrap('https://pulse.example');
const httpScript = buildPowerShellInstallScriptBootstrap('http://pulse.example');
expect(httpsScript).toContain('$pulseScriptUrl="https://pulse.example/install.ps1"');
expect(httpScript).toContain('$pulseScriptUrl="http://pulse.example/install.ps1"');
});
});
describe('buildPowerShellInstallScriptBootstrap — bootstrap script wiring', () => {
it('emits the $pulseScriptUrl assignment as the first statement inside the script block', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script.startsWith('& { $pulseScriptUrl="https://pulse.example/install.ps1"; ')).toBe(
true,
);
});
it('gates the custom-CA / insecure branch on the PULSE_INSECURE_SKIP_VERIFY env var', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain('if ($env:PULSE_INSECURE_SKIP_VERIFY -eq "true"');
});
it('also gates the custom-CA / insecure branch on the PULSE_CACERT env var (OR arm)', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain('-or -not [string]::IsNullOrWhiteSpace($env:PULSE_CACERT))');
});
it('reads the custom CA bytes from $env:PULSE_CACERT when populated', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain('if (-not [string]::IsNullOrWhiteSpace($env:PULSE_CACERT)) {');
expect(script).toContain(
'$pulseCustomCaBytes = [System.IO.File]::ReadAllBytes($env:PULSE_CACERT);',
);
});
it('routes a PEM-encoded CA through X509Certificate2::CreateFromPem', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain('if ($pulseCustomCaText.Contains("-----BEGIN CERTIFICATE-----"))');
expect(script).toContain(
'$pulseCustomCa = [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromPem($pulseCustomCaText)',
);
});
it('routes a DER-encoded CA through X509Certificate2::new (else arm)', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain(
'$pulseCustomCa = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pulseCustomCaBytes)',
);
});
it('installs the X509 chain-validation ServerCertificateValidationCallback', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain(
'$pulsePrev = [System.Net.ServicePointManager]::ServerCertificateValidationCallback;',
);
expect(script).toContain(
'[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { param($sender, $certificate, $chain, $sslPolicyErrors)',
);
expect(script).toContain(
'} finally { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $pulsePrev }',
);
});
it('short-circuits the callback to $true when PULSE_INSECURE_SKIP_VERIFY is "true"', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain('if ($env:PULSE_INSECURE_SKIP_VERIFY -eq "true") { return $true };');
});
it('returns the raw sslPolicyErrors verdict when no custom CA was loaded', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain(
'if ($null -eq $pulseCustomCa) { return $sslPolicyErrors -eq [System.Net.Security.SslPolicyErrors]::None };',
);
});
it('returns $false when the server supplied no certificate', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain('if ($null -eq $certificate) { return $false };');
});
it('builds an X509Chain with NoCheck revocation and the custom CA in ExtraStore', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain(
'$pulseChain = [System.Security.Cryptography.X509Certificates.X509Chain]::new();',
);
expect(script).toContain(
'$pulseChain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck;',
);
expect(script).toContain('$null = $pulseChain.ChainPolicy.ExtraStore.Add($pulseCustomCa);');
expect(script).toContain('$null = $pulseChain.Build($certificate);');
});
it('walks ChainElements and trusts the chain when the custom CA Thumbprint matches', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain('foreach ($pulseElement in $pulseChain.ChainElements) {');
expect(script).toContain(
'if ($pulseElement.Certificate.Thumbprint -eq $pulseCustomCa.Thumbprint) { return $true }',
);
expect(script).toContain('return $false };');
});
it('fetches the script via `irm $pulseScriptUrl` inside both the custom-trust and the bare else arm', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
// custom-trust arm:
expect(script).toContain('irm $pulseScriptUrl ');
// bare else arm (no env-var set):
expect(script).toContain('} else { irm $pulseScriptUrl } } | iex');
});
it('pipes the entire bootstrap through `| iex` so it is executed inline', () => {
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script.endsWith('} | iex')).toBe(true);
});
it('does not PowerShell-escape a plain https URL (no backticks/quotes injected)', () => {
// powerShellQuote only transforms `, ", and $ — none appear in a bare URL,
// so the scriptUrl should pass through verbatim.
const script = buildPowerShellInstallScriptBootstrap('https://pulse.example');
expect(script).toContain('$pulseScriptUrl="https://pulse.example/install.ps1"');
});
});