All files / packages/sds-customization-compliance/src/checks no-unintended-visual-changes.ts

96.77% Statements 30/31
71.42% Branches 20/28
88.88% Functions 8/9
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130                                                        4x 4x               4x                         4x           2x 2x                 2x 2x 2x 2x   2x           2x 2x 2x   2x 2x     4x 13x 13x 4x               9x 13x 13x   13x 7x                   2x 2x             2x                    
/**
 * Compliance row: no unintended default visual changes.
 *
 * `warning`-severity flags in the theme-regression audit indicate the
 * component's default rendering may have shifted between the legacy
 * `<component>.css` and the new theme architecture
 * (`themes/base.css` + `themes/<theme>.css`), even with no customer
 * override in play. Any non-zero warning count fails the row.
 *
 * Flags whose selector is listed in `config/ignoredSelectors.ts` for
 * this component are treated as cross-component cleanup and excluded
 * from both scoring and the offenders list.
 *
 * The detail text enumerates which warning categories are actually
 * present (with one-line descriptions of each) so the row is readable
 * without cross-referencing the audit source. Per-offender notes also
 * carry the same short description in addition to the raw flag id.
 */
 
import { flagLocation, partitionIgnoredFlags, regressionFlagFix } from './helpers/index.js';
import type { Flag } from '../audit/types.js';
import type {
  ComplianceCheck,
  ComplianceRow,
  ComplianceRowCategory,
  ComplianceRowSummary,
} from '../types.js';
 
const LABEL = 'No unintended default visual changes';
const NOT_RUN_DETAIL = 'No regression audit on disk yet. Run `sds-compliance build` to populate.';
 
/**
 * Per-flag-id metadata for the structured row body. Each entry produces a
 * titled category block (`heading` + `description`) when at least one flag
 * with that id survives ignored-selector filtering. Keep `description`
 * action-oriented; it shows once per category, not once per offender.
 */
const WARNING_CATEGORY_COPY: Readonly<Record<string, { heading: string; description: string }>> = {
  'pseudo-element-content-hookified': {
    heading: 'Pseudo-element `content` hookified',
    description:
      'A pseudo-element `content` changed from a literal to a `var()` expression. A consumer rendering without a theme will see no content.',
  },
  'kinetics-or-motion-removed': {
    heading: 'Kinetics or motion removed',
    description:
      'The legacy CSS contained `prefers-reduced-motion` blocks or `[kx-*]` rules; none are present in the new CSS.',
  },
};
 
const GUIDANCE_PARAGRAPHS: ReadonlyArray<string> = [
  'These do not break the rendered page on their own (the legacy `<component>.css` is still loaded), but each entry is a gap or shift the new theme architecture has not absorbed.',
  'To clear, mirror the missing surface into `themes/base.css` as paint declarations (no hook writes on attribute-substring or descendant selectors), or suppress in `config/ignoredSelectors.ts` if the selector is genuinely cross-component cleanup.',
];
 
function categoryFor(id: string, count: number): ComplianceRowCategory {
  const meta = WARNING_CATEGORY_COPY[id];
  return {
    id,
    heading: meta?.heading ?? id,
    count,
    description: meta?.description ?? `\`${id}\` flag emitted by the regression audit.`,
  };
}
 
function summaryFor(warningFlags: readonly Flag[]): ComplianceRowSummary {
  const byId = new Map<string, number>();
  for (const f of warningFlags) byId.set(f.id, (byId.get(f.id) ?? 0) + 1);
  const total = warningFlags.length;
  return {
    callout: `${total} warning-level regression${total === 1 ? '' : 's'} detected.`,
    categories: [...byId.entries()].sort((a, b) => b[1] - a[1]).map(([id, count]) => categoryFor(id, count)),
    guidance: GUIDANCE_PARAGRAPHS,
  };
}
 
function detailFor(summary: ComplianceRowSummary): string {
  const callout = summary.callout ?? '';
  const breakdown = (summary.categories ?? [])
    .map((c) => `**${c.heading}** (${c.count ?? 0}): ${c.description}`)
    .join(' ');
  const guidance = (summary.guidance ?? []).join(' ');
  return [callout, breakdown, guidance].filter(Boolean).join(' ');
}
 
export const noUnintendedVisualChanges: ComplianceCheck = (input): ComplianceRow => {
  const regression = input.regression;
  if (!regression) {
    return {
      id: 'no-unintended-visual-changes',
      label: LABEL,
      status: 'info',
      count: 0,
      detail: NOT_RUN_DETAIL,
    };
  }
  const rawFlags = regression.flags ?? [];
  const { kept: flags } = partitionIgnoredFlags(input.componentName, rawFlags);
  const warningFlags = flags.filter((f) => f.severity === 'warning');
 
  if (warningFlags.length === 0) {
    return {
      id: 'no-unintended-visual-changes',
      label: LABEL,
      status: 'pass',
      count: 0,
      detail:
        "No warning-level regressions detected — the component's default rendering should match the legacy output.",
    };
  }
 
  const summary = summaryFor(warningFlags);
  return {
    id: 'no-unintended-visual-changes',
    label: LABEL,
    status: 'fail',
    count: warningFlags.length,
    detail: detailFor(summary),
    summary,
    offenders: warningFlags.slice(0, 10).map((f) => ({
      selector: f.selector,
      hook: f.legacy?.hook ?? f.next?.hook,
      prop: f.legacy?.prop ?? f.next?.prop,
      location: flagLocation(f) ?? undefined,
      note: f.id,
      fix: regressionFlagFix(f),
    })),
  };
};